diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 19cd60b3cf..721a1e48c6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -28,4 +28,9 @@ true https://github.com/dotnet/orleans + + + + + \ No newline at end of file diff --git a/src/Orleans.Analyzers/Orleans.Analyzers.csproj b/src/Orleans.Analyzers/Orleans.Analyzers.csproj index 2f0ad28ff7..82a525bb1e 100644 --- a/src/Orleans.Analyzers/Orleans.Analyzers.csproj +++ b/src/Orleans.Analyzers/Orleans.Analyzers.csproj @@ -37,4 +37,8 @@ + + + + \ No newline at end of file diff --git a/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj b/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj index dd65e96f3d..8178fa8441 100644 --- a/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj +++ b/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj @@ -62,4 +62,8 @@ + + + + diff --git a/src/Orleans.Serialization.Abstractions/FrameworkPartAttribute.cs b/src/Orleans.Serialization.Abstractions/FrameworkPartAttribute.cs new file mode 100644 index 0000000000..d219050973 --- /dev/null +++ b/src/Orleans.Serialization.Abstractions/FrameworkPartAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.ComponentModel; + +namespace Orleans.Metadata; + +/// +/// Specifies that an assembly does not contain application code. +/// +[AttributeUsage(AttributeTargets.Assembly)] +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class FrameworkPartAttribute : Attribute +{ +} diff --git a/src/Orleans.Serialization.NewtonsoftJson/NewtonsoftJsonCodec.cs b/src/Orleans.Serialization.NewtonsoftJson/NewtonsoftJsonCodec.cs index 227ca827b4..97b4e96dca 100644 --- a/src/Orleans.Serialization.NewtonsoftJson/NewtonsoftJsonCodec.cs +++ b/src/Orleans.Serialization.NewtonsoftJson/NewtonsoftJsonCodec.cs @@ -1,9 +1,12 @@ using System; +using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Orleans.Metadata; using Orleans.Serialization.Buffers; using Orleans.Serialization.Buffers.Adaptors; using Orleans.Serialization.Cloning; @@ -124,6 +127,11 @@ bool IGeneralizedCodec.IsSupportedType(Type type) return true; } + if (CommonCodecTypeFilter.IsAbstractOrFrameworkType(type)) + { + return false; + } + foreach (var selector in _serializableTypeSelectors) { if (selector.IsSupportedType(type)) @@ -173,6 +181,11 @@ object IDeepCopier.DeepCopy(object input, CopyContext context) /// bool IGeneralizedCopier.IsSupportedType(Type type) { + if (CommonCodecTypeFilter.IsAbstractOrFrameworkType(type)) + { + return false; + } + foreach (var selector in _copyableTypeSelectors) { if (selector.IsSupportedType(type)) diff --git a/src/Orleans.Serialization.SystemTextJson/JsonCodec.cs b/src/Orleans.Serialization.SystemTextJson/JsonCodec.cs index 49006c6a9b..1ffed558da 100644 --- a/src/Orleans.Serialization.SystemTextJson/JsonCodec.cs +++ b/src/Orleans.Serialization.SystemTextJson/JsonCodec.cs @@ -2,8 +2,10 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text.Json; using Microsoft.Extensions.Options; +using Orleans.Metadata; using Orleans.Serialization.Buffers; using Orleans.Serialization.Buffers.Adaptors; using Orleans.Serialization.Cloning; @@ -158,6 +160,11 @@ bool IGeneralizedCodec.IsSupportedType(Type type) return true; } + if (CommonCodecTypeFilter.IsAbstractOrFrameworkType(type)) + { + return false; + } + foreach (var selector in _serializableTypeSelectors) { if (selector.IsSupportedType(type)) @@ -204,6 +211,11 @@ object IDeepCopier.DeepCopy(object input, CopyContext context) /// bool IGeneralizedCopier.IsSupportedType(Type type) { + if (CommonCodecTypeFilter.IsAbstractOrFrameworkType(type)) + { + return false; + } + foreach (var selector in _copyableTypeSelectors) { if (selector.IsSupportedType(type)) diff --git a/src/Orleans.Serialization/Codecs/CommonCodecTypeFilter.cs b/src/Orleans.Serialization/Codecs/CommonCodecTypeFilter.cs new file mode 100644 index 0000000000..12f0d750c9 --- /dev/null +++ b/src/Orleans.Serialization/Codecs/CommonCodecTypeFilter.cs @@ -0,0 +1,31 @@ +using System; +using System.CodeDom.Compiler; +using System.Linq; +using System.Reflection; + +using Orleans.Metadata; + +namespace Orleans.Serialization.Codecs; + +/// +/// Defines common type filtering operations. +/// +public class CommonCodecTypeFilter +{ + /// + /// Returns true if the provided type is a framework or abstract type. + /// + /// The type to check. + /// if the type is a framework or abstract type, otherwise . + public static bool IsAbstractOrFrameworkType(Type type) + { + if (type.IsAbstract + || type.GetCustomAttributes().Any(a => a.Tool.Equals("OrleansCodeGen")) + || type.Assembly.GetCustomAttribute() is not null) + { + return true; + } + + return false; + } +} diff --git a/src/Serializers/Orleans.Serialization.Protobuf/ProtobufCodec.cs b/src/Serializers/Orleans.Serialization.Protobuf/ProtobufCodec.cs index e971914f2f..3a32c68673 100644 --- a/src/Serializers/Orleans.Serialization.Protobuf/ProtobufCodec.cs +++ b/src/Serializers/Orleans.Serialization.Protobuf/ProtobufCodec.cs @@ -1,4 +1,5 @@ using Google.Protobuf; +using Orleans.Metadata; using Orleans.Serialization.Buffers; using Orleans.Serialization.Cloning; using Orleans.Serialization.Codecs; @@ -8,6 +9,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace Orleans.Serialization; @@ -68,6 +70,11 @@ bool IGeneralizedCodec.IsSupportedType(Type type) return true; } + if (CommonCodecTypeFilter.IsAbstractOrFrameworkType(type)) + { + return false; + } + foreach (var selector in _serializableTypeSelectors) { if (selector.IsSupportedType(type)) @@ -82,6 +89,11 @@ bool IGeneralizedCodec.IsSupportedType(Type type) /// bool IGeneralizedCopier.IsSupportedType(Type type) { + if (CommonCodecTypeFilter.IsAbstractOrFrameworkType(type)) + { + return false; + } + foreach (var selector in _copyableTypeSelectors) { if (selector.IsSupportedType(type)) diff --git a/test/NonSilo.Tests/NonSilo.Tests.csproj b/test/NonSilo.Tests/NonSilo.Tests.csproj index a799cc2483..1f4a4221a1 100644 --- a/test/NonSilo.Tests/NonSilo.Tests.csproj +++ b/test/NonSilo.Tests/NonSilo.Tests.csproj @@ -9,6 +9,8 @@ + + diff --git a/test/NonSilo.Tests/Serialization/ExternalCodecTests.cs b/test/NonSilo.Tests/Serialization/ExternalCodecTests.cs new file mode 100644 index 0000000000..8d6562b7fa --- /dev/null +++ b/test/NonSilo.Tests/Serialization/ExternalCodecTests.cs @@ -0,0 +1,216 @@ +using Microsoft.Extensions.DependencyInjection; +using Orleans.Configuration; +using System.Reflection; +using TestExtensions; +using Xunit; +using System.Text; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; + +using Orleans.Serialization; +using Orleans.Runtime; +using Orleans.Serialization.Serializers; +using Orleans.Streaming.EventHubs; +using Microsoft.Extensions.Options; +using Orleans.Serialization.Configuration; +using Orleans.Metadata; + +namespace UnitTests.Serialization +{ + [TestCategory("Serialization"), TestCategory("BVT")] + public class ExternalCodecTests + { + private readonly SerializationTestEnvironment environment; + + public ExternalCodecTests() + { + this.environment = SerializationTestEnvironment.InitializeWithDefaults( + builder => + builder.ConfigureServices(services => + services.AddSerializer(serializerBuilder => + { + serializerBuilder.AddNewtonsoftJsonSerializer(type => type.GetCustomAttribute() is not null); + }))); + } + + [Fact] + public void NewtonsoftJsonCodec_ExternalSerializer_Client() + { + TestSerializationRoundTrip(this.environment.Serializer); + } + + [Fact] + public void NewtonsoftJsonCodec_ExternalSerializer_Silo() + { + var silo = new HostBuilder() + .UseOrleans((ctx, siloBuilder) => + { + siloBuilder + .Configure(o => o.ClusterId = o.ServiceId = "s") + .UseLocalhostClustering() + .ConfigureServices(services => + services.AddSerializer(serializerBuilder => + { + serializerBuilder.AddNewtonsoftJsonSerializer(type => type.GetCustomAttribute() is not null); + })); + }) + .Build(); + var serializer = silo.Services.GetRequiredService(); + TestSerializationRoundTrip(serializer); + } + + [Fact] + public void NewtonsoftJsonCodec_CanModifySerializerSettings() + { + var silo = new HostBuilder() + .UseOrleans((ctx, siloBuilder) => + { + siloBuilder + .Configure(o => o.ClusterId = o.ServiceId = "s") + .Configure(options => options.JsonSerializerSettings.DefaultValueHandling = DefaultValueHandling.Include) + .UseLocalhostClustering(); + }) + .Build(); + var serializer = silo.Services.GetRequiredService(); + var data = new JsonPoco(); + var serialized = serializer.Serialize(data, typeof(JsonPoco)); + Assert.Contains("some_flag", serialized); + } + + [Fact] + public void NewtonsoftJsonCodec_DoesNotSerializeFrameworkTypes() + { + var silo = new HostBuilder() + .UseOrleans((ctx, siloBuilder) => + { + siloBuilder.Services.AddSerializer(serializerBuilder => serializerBuilder.AddNewtonsoftJsonSerializer(type => + { + Assert.Fail($"Custom type filter should not be consulted for any type in this test, but it was used for type {type}."); + return true; + })); + siloBuilder.UseLocalhostClustering(); + }) + .Build(); + var services = silo.Services; + var serializer = services.GetRequiredService(); + var generatedGrainReferenceType = services.GetRequiredService>().Value + .InterfaceProxies.First(i => ! i.IsGenericType && i.Assembly.GetCustomAttribute() is null); + var codecProvider = services.GetRequiredService(); + foreach (var type in new[] { typeof(SiloAddress), typeof(GrainReference), typeof(EventHubBatchContainer), generatedGrainReferenceType }) + { + var codec = codecProvider.GetCodec(type); + Assert.IsNotType(codec); + + var copier = codecProvider.GetDeepCopier(type); + Assert.IsNotType(copier); + } + } + + [Fact] + public void SystemTextJsonCodec_DoesNotSerializeFrameworkTypes() + { + var silo = new HostBuilder() + .UseOrleans((ctx, siloBuilder) => + { + siloBuilder.Services.AddSerializer(serializerBuilder => serializerBuilder.AddJsonSerializer(type => + { + Assert.Fail($"Custom type filter should not be consulted for any type in this test, but it was used for type {type}."); + return true; + })); + siloBuilder.UseLocalhostClustering(); + }) + .Build(); + var services = silo.Services; + var serializer = services.GetRequiredService(); + var generatedGrainReferenceType = services.GetRequiredService>().Value + .InterfaceProxies.First(i => ! i.IsGenericType && i.Assembly.GetCustomAttribute() is null); + var codecProvider = services.GetRequiredService(); + foreach (var type in new[] { typeof(SiloAddress), typeof(GrainReference), typeof(EventHubBatchContainer), generatedGrainReferenceType }) + { + var codec = codecProvider.GetCodec(type); + Assert.IsNotType(codec); + + var copier = codecProvider.GetDeepCopier(type); + Assert.IsNotType(copier); + } + } + + [Fact] + public void ProtocolBuffersCodec_DoesNotSerializeFrameworkTypes() + { + var silo = new HostBuilder() + .UseOrleans((ctx, siloBuilder) => + { + siloBuilder.Services.AddSerializer(serializerBuilder => serializerBuilder.AddProtobufSerializer(type => + { + Assert.Fail($"Custom type filter should not be consulted for any type in this test, but it was used for type {type}."); + return true; + }, + type => + { + Assert.Fail($"Custom type filter should not be consulted for any type in this test, but it was used for type {type}."); + return true; + })); + siloBuilder.UseLocalhostClustering(); + }) + .Build(); + var services = silo.Services; + var serializer = services.GetRequiredService(); + var generatedGrainReferenceType = services.GetRequiredService>().Value + .InterfaceProxies.First(i => ! i.IsGenericType && i.Assembly.GetCustomAttribute() is null); + var codecProvider = services.GetRequiredService(); + foreach (var type in new[] { typeof(SiloAddress), typeof(GrainReference), typeof(EventHubBatchContainer), generatedGrainReferenceType }) + { + var codec = codecProvider.GetCodec(type); + Assert.IsNotType(codec); + + var copier = codecProvider.GetDeepCopier(type); + Assert.IsNotType(copier); + } + } + + private static void TestSerializationRoundTrip(Serializer serializer) + { + var data = new JsonPoco { Prop = "some data" }; + var serialized = serializer.SerializeToArray(data); + var subSequence = Encoding.UTF8.GetBytes("crazy_name"); + + // The serialized data should have our custom [JsonProperty] name, 'crazy_name', in it. + Assert.Contains(ToString(subSequence), ToString(serialized)); + + var deserialized = serializer.Deserialize(serialized); + + Assert.Equal(data.Prop, deserialized.Prop); + } + + private static string ToString(byte[] bytes) + { + var result = new StringBuilder(bytes.Length * 2); + foreach (var b in bytes) + { + result.Append($"{b:x2}"); + } + + var str = result.ToString(); + return str; + } + + /// + /// Use a custom attribute to distinguish types which should be serialized using the JSON codec. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] + internal class JsonTypeAttribute : Attribute + { + } + + [JsonType] + public class JsonPoco + { + [JsonProperty("crazy_name")] + public string Prop { get; set; } + + [JsonProperty("some_flag")] + public bool Flag { get; set; } + } + } +} \ No newline at end of file diff --git a/test/NonSilo.Tests/Serialization/OrleansJsonSerializerTests.cs b/test/NonSilo.Tests/Serialization/OrleansJsonSerializerTests.cs deleted file mode 100644 index a574208166..0000000000 --- a/test/NonSilo.Tests/Serialization/OrleansJsonSerializerTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Orleans.Configuration; -using System.Reflection; -using Orleans.Hosting; -using TestExtensions; -using Xunit; -using System.Text; -using Microsoft.Extensions.Hosting; -using Newtonsoft.Json; - -using Orleans.Serialization; - -namespace UnitTests.Serialization -{ - [TestCategory("Serialization"), TestCategory("BVT")] - public class OrleansJsonSerializerTests - { - private readonly SerializationTestEnvironment environment; - - public OrleansJsonSerializerTests() - { - this.environment = SerializationTestEnvironment.InitializeWithDefaults( - builder => - builder.ConfigureServices(services => - services.AddSerializer(serializerBuilder => - { - serializerBuilder.AddNewtonsoftJsonSerializer(type => type.GetCustomAttribute() is not null); - }))); - } - - [Fact] - public void OrleansJsonSerializer_ExternalSerializer_Client() - { - TestSerializationRoundTrip(this.environment.Serializer); - } - - [Fact] - public void OrleansJsonSerializer_ExternalSerializer_Silo() - { - var silo = new HostBuilder() - .UseOrleans((ctx, siloBuilder) => - { - siloBuilder - .Configure(o => o.ClusterId = o.ServiceId = "s") - .UseLocalhostClustering() - .ConfigureServices(services => - services.AddSerializer(serializerBuilder => - { - serializerBuilder.AddNewtonsoftJsonSerializer(type => type.GetCustomAttribute() is not null); - })); - }) - .Build(); - var serializer = silo.Services.GetRequiredService(); - TestSerializationRoundTrip(serializer); - } - - [Fact] - public void OrleansJsonSerializer_CanModifySerializerSettings() - { - var silo = new HostBuilder() - .UseOrleans((ctx, siloBuilder) => - { - siloBuilder - .Configure(o => o.ClusterId = o.ServiceId = "s") - .Configure(options => options.JsonSerializerSettings.DefaultValueHandling = DefaultValueHandling.Include) - .UseLocalhostClustering(); - }) - .Build(); - var serializer = silo.Services.GetRequiredService(); - var data = new JsonPoco(); - var serialized = serializer.Serialize(data, typeof(JsonPoco)); - Assert.Contains("some_flag", serialized); - } - - private static void TestSerializationRoundTrip(Serializer serializer) - { - var data = new JsonPoco {Prop = "some data"}; - var serialized = serializer.SerializeToArray(data); - var subSequence = Encoding.UTF8.GetBytes("crazy_name"); - - // The serialized data should have our custom [JsonProperty] name, 'crazy_name', in it. - Assert.Contains(ToString(subSequence), ToString(serialized)); - - var deserialized = serializer.Deserialize(serialized); - - Assert.Equal(data.Prop, deserialized.Prop); - } - - private static string ToString(byte[] bytes) - { - var result = new StringBuilder(bytes.Length * 2); - foreach (var b in bytes) - { - result.Append($"{b:x2}"); - } - - var str = result.ToString(); - return str; - } - - /// - /// Use a custom attribute to distinguish types which should be serialized using the JSON codec. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] - internal class JsonTypeAttribute : Attribute - { - } - - [JsonType] - public class JsonPoco - { - [JsonProperty("crazy_name")] - public string Prop { get; set; } - - [JsonProperty("some_flag")] - public bool Flag { get; set; } - } - } -} \ No newline at end of file