diff --git a/csharp/src/Google.Protobuf.Test.TestProtos/Google.Protobuf.Test.TestProtos.csproj b/csharp/src/Google.Protobuf.Test.TestProtos/Google.Protobuf.Test.TestProtos.csproj index 5030043d76..ad8445b7bf 100644 --- a/csharp/src/Google.Protobuf.Test.TestProtos/Google.Protobuf.Test.TestProtos.csproj +++ b/csharp/src/Google.Protobuf.Test.TestProtos/Google.Protobuf.Test.TestProtos.csproj @@ -6,7 +6,7 @@ and without the internal visibility from the test project (all of which have caused issues in the past). --> - net45;netstandard1.1;netstandard2.0 + net462;netstandard1.1;netstandard2.0 3.0 ../../keys/Google.Protobuf.snk true diff --git a/csharp/src/Google.Protobuf.Test/ExtensionSetTest.cs b/csharp/src/Google.Protobuf.Test/ExtensionSetTest.cs index 951e856a59..d163810b6e 100644 --- a/csharp/src/Google.Protobuf.Test/ExtensionSetTest.cs +++ b/csharp/src/Google.Protobuf.Test/ExtensionSetTest.cs @@ -1,4 +1,6 @@ -using Google.Protobuf.TestProtos.Proto2; +using System; +using System.Collections; +using Google.Protobuf.TestProtos.Proto2; using NUnit.Framework; using static Google.Protobuf.TestProtos.Proto2.UnittestExtensions; @@ -79,6 +81,63 @@ namespace Google.Protobuf Assert.AreEqual("abcd", ExtensionSet.Get(ref extensionSet, OptionalStringExtension)); } + [Test] + public void GetSingle() + { + var extensionValue = new TestAllTypes.Types.NestedMessage() { Bb = 42 }; + var untypedExtension = new Extension(OptionalNestedMessageExtension.FieldNumber, codec: null); + var wrongTypedExtension = new Extension(OptionalNestedMessageExtension.FieldNumber, codec: null); + + var message = new TestAllExtensions(); + + var value1 = message.GetExtension(untypedExtension); + Assert.IsNull(value1); + + message.SetExtension(OptionalNestedMessageExtension, extensionValue); + var value2 = message.GetExtension(untypedExtension); + Assert.IsNotNull(value2); + + var valueBytes = ((IMessage)value2).ToByteArray(); + var parsedValue = TestProtos.Proto2.TestAllTypes.Types.NestedMessage.Parser.ParseFrom(valueBytes); + Assert.AreEqual(extensionValue, parsedValue); + + var ex = Assert.Throws(() => message.GetExtension(wrongTypedExtension)); + + var expectedMessage = "The stored extension value has a type of 'Google.Protobuf.TestProtos.Proto2.TestAllTypes+Types+NestedMessage, Google.Protobuf.Test.TestProtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604'. " + + "This a different from the requested type of 'Google.Protobuf.TestProtos.Proto2.TestAllTypes, Google.Protobuf.Test.TestProtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604'."; + Assert.AreEqual(expectedMessage, ex.Message); + } + + [Test] + public void GetRepeated() + { + var extensionValue = new TestAllTypes.Types.NestedMessage() { Bb = 42 }; + var untypedExtension = new Extension(RepeatedNestedMessageExtension.FieldNumber, codec: null); + var wrongTypedExtension = new RepeatedExtension(RepeatedNestedMessageExtension.FieldNumber, codec: null); + + var message = new TestAllExtensions(); + + var value1 = message.GetExtension(untypedExtension); + Assert.IsNull(value1); + + var repeatedField = message.GetOrInitializeExtension(RepeatedNestedMessageExtension); + repeatedField.Add(extensionValue); + + var value2 = message.GetExtension(untypedExtension); + Assert.IsNotNull(value2); + Assert.AreEqual(1, value2.Count); + + var valueBytes = ((IMessage)value2[0]).ToByteArray(); + var parsedValue = TestProtos.Proto2.TestAllTypes.Types.NestedMessage.Parser.ParseFrom(valueBytes); + Assert.AreEqual(extensionValue, parsedValue); + + var ex = Assert.Throws(() => message.GetExtension(wrongTypedExtension)); + + var expectedMessage = "The stored extension value has a type of 'Google.Protobuf.TestProtos.Proto2.TestAllTypes+Types+NestedMessage, Google.Protobuf.Test.TestProtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604'. " + + "This a different from the requested type of 'Google.Protobuf.TestProtos.Proto2.TestAllTypes, Google.Protobuf.Test.TestProtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604'."; + Assert.AreEqual(expectedMessage, ex.Message); + } + [Test] public void TestEquals() { diff --git a/csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj b/csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj index deb17e9f52..641cb0a088 100644 --- a/csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj +++ b/csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj @@ -1,7 +1,7 @@  - net451;netcoreapp3.1;net60 + net462;netcoreapp3.1;net60 ../../keys/Google.Protobuf.snk true False diff --git a/csharp/src/Google.Protobuf.Test/RefStructCompatibilityTest.cs b/csharp/src/Google.Protobuf.Test/RefStructCompatibilityTest.cs index f2d9a2d4e2..9dca501523 100644 --- a/csharp/src/Google.Protobuf.Test/RefStructCompatibilityTest.cs +++ b/csharp/src/Google.Protobuf.Test/RefStructCompatibilityTest.cs @@ -60,7 +60,7 @@ namespace Google.Protobuf var currentAssemblyDir = Path.GetDirectoryName(typeof(RefStructCompatibilityTest).GetTypeInfo().Assembly.Location); var testProtosProjectDir = Path.GetFullPath(Path.Combine(currentAssemblyDir, "..", "..", "..", "..", "Google.Protobuf.Test.TestProtos")); - var testProtosOutputDir = (currentAssemblyDir.Contains("bin/Debug/") || currentAssemblyDir.Contains("bin\\Debug\\")) ? "bin\\Debug\\net45" : "bin\\Release\\net45"; + var testProtosOutputDir = (currentAssemblyDir.Contains("bin/Debug/") || currentAssemblyDir.Contains("bin\\Debug\\")) ? "bin\\Debug\\net462" : "bin\\Release\\net462"; // If "ref struct" types are used in the generated code, compilation with an old compiler will fail with the following error: // "XYZ is obsolete: 'Types with embedded references are not supported in this version of your compiler.'" diff --git a/csharp/src/Google.Protobuf/Extension.cs b/csharp/src/Google.Protobuf/Extension.cs index 6dd1ceaa8e..d10a668452 100644 --- a/csharp/src/Google.Protobuf/Extension.cs +++ b/csharp/src/Google.Protobuf/Extension.cs @@ -77,7 +77,7 @@ namespace Google.Protobuf this.codec = codec; } - internal TValue DefaultValue => codec.DefaultValue; + internal TValue DefaultValue => codec != null ? codec.DefaultValue : default(TValue); internal override Type TargetType => typeof(TTarget); diff --git a/csharp/src/Google.Protobuf/ExtensionSet.cs b/csharp/src/Google.Protobuf/ExtensionSet.cs index 895b9ae6ea..4967ef646d 100644 --- a/csharp/src/Google.Protobuf/ExtensionSet.cs +++ b/csharp/src/Google.Protobuf/ExtensionSet.cs @@ -34,6 +34,7 @@ using Google.Protobuf.Collections; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Security; namespace Google.Protobuf @@ -63,7 +64,39 @@ namespace Google.Protobuf IExtensionValue value; if (TryGetValue(ref set, extension, out value)) { - return ((ExtensionValue)value).GetValue(); + // The stored ExtensionValue can be a different type to what is being requested. + // This happens when the same extension proto is compiled in different assemblies. + // To allow consuming assemblies to still get the value when the TValue type is + // different, this get method: + // 1. Attempts to cast the value to the expected ExtensionValue. + // This is the usual case. It is used first because it avoids possibly boxing the value. + // 2. Fallback to get the value as object from IExtensionValue then casting. + // This allows for someone to specify a TValue of object. They can then convert + // the values to bytes and reparse using expected value. + // 3. If neither of these work, throw a user friendly error that the types aren't compatible. + if (value is ExtensionValue extensionValue) + { + return extensionValue.GetValue(); + } + else if (value.GetValue() is TValue underlyingValue) + { + return underlyingValue; + } + else + { + var valueType = value.GetType().GetTypeInfo(); + if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(ExtensionValue<>)) + { + var storedType = valueType.GenericTypeArguments[0]; + throw new InvalidOperationException( + "The stored extension value has a type of '" + storedType.AssemblyQualifiedName + "'. " + + "This a different from the requested type of '" + typeof(TValue).AssemblyQualifiedName + "'."); + } + else + { + throw new InvalidOperationException("Unexpected extension value type: " + valueType.AssemblyQualifiedName); + } + } } else { @@ -79,7 +112,25 @@ namespace Google.Protobuf IExtensionValue value; if (TryGetValue(ref set, extension, out value)) { - return ((RepeatedExtensionValue)value).GetValue(); + if (value is RepeatedExtensionValue extensionValue) + { + return extensionValue.GetValue(); + } + else + { + var valueType = value.GetType().GetTypeInfo(); + if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(RepeatedExtensionValue<>)) + { + var storedType = valueType.GenericTypeArguments[0]; + throw new InvalidOperationException( + "The stored extension value has a type of '" + storedType.AssemblyQualifiedName + "'. " + + "This a different from the requested type of '" + typeof(TValue).AssemblyQualifiedName + "'."); + } + else + { + throw new InvalidOperationException("Unexpected extension value type: " + valueType.AssemblyQualifiedName); + } + } } else { diff --git a/csharp/src/Google.Protobuf/ExtensionValue.cs b/csharp/src/Google.Protobuf/ExtensionValue.cs index 5257c4c955..1329b2f4d5 100644 --- a/csharp/src/Google.Protobuf/ExtensionValue.cs +++ b/csharp/src/Google.Protobuf/ExtensionValue.cs @@ -44,6 +44,7 @@ namespace Google.Protobuf void WriteTo(ref WriteContext ctx); int CalculateSize(); bool IsInitialized(); + object GetValue(); } internal sealed class ExtensionValue : IExtensionValue @@ -118,6 +119,8 @@ namespace Google.Protobuf public T GetValue() => field; + object IExtensionValue.GetValue() => field; + public void SetValue(T value) { field = value; @@ -201,6 +204,8 @@ namespace Google.Protobuf public RepeatedField GetValue() => field; + object IExtensionValue.GetValue() => field; + public bool IsInitialized() { for (int i = 0; i < field.Count; i++)