diff --git a/publisher/Directory.Packages.props b/publisher/Directory.Packages.props index e0870fc..76f723a 100644 --- a/publisher/Directory.Packages.props +++ b/publisher/Directory.Packages.props @@ -2,7 +2,7 @@ true - 10.0.2750-preview + 10.0.2794-preview 8.5.7 @@ -13,6 +13,7 @@ + diff --git a/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs b/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs index 5304992..9c4a801 100644 --- a/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs +++ b/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs @@ -1,3 +1,5 @@ +using LeanCode.Serialization; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; namespace LeanCode.Pipe.Funnel.Instance; @@ -9,15 +11,23 @@ public static class ServiceCollectionExtensions /// public static IServiceCollection AddLeanPipeFunnel( this IServiceCollection services, - FunnelConfiguration? config = null + FunnelConfiguration? config = null, + Action>? configureLeanPipeHub = null, + Action? overrideJsonHubProtocolOptions = null ) { - services + var signalRBuilder = services .AddSignalR() - .AddJsonProtocol(options => - options.PayloadSerializerOptions.PropertyNamingPolicy = null + .AddJsonProtocol( + overrideJsonHubProtocolOptions + ?? (options => options.PayloadSerializerOptions.ConfigureForCQRS()) ); + if (configureLeanPipeHub is not null) + { + signalRBuilder.AddHubOptions(configureLeanPipeHub); + } + services.AddMemoryCache(); services.AddSingleton(config ?? FunnelConfiguration.Default); diff --git a/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs b/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs index 0165086..bd15c35 100644 --- a/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs +++ b/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using LeanCode.Components; +using LeanCode.Serialization; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection; @@ -17,7 +18,8 @@ public static class ServiceCollectionExtensions public static LeanPipeServicesBuilder AddFunnelledLeanPipe( this IServiceCollection services, TypesCatalog topics, - TypesCatalog handlers + TypesCatalog handlers, + Action? overrideJsonHubProtocolOptions = null ) { services.AddTransient(); @@ -28,8 +30,8 @@ TypesCatalog handlers services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.Configure( - (JsonHubProtocolOptions options) => - options.PayloadSerializerOptions.PropertyNamingPolicy = null + overrideJsonHubProtocolOptions + ?? (options => options.PayloadSerializerOptions.ConfigureForCQRS()) ); return new LeanPipeServicesBuilder(services, topics).AddHandlers(handlers); diff --git a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs index c00f800..73605d6 100644 --- a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs +++ b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ using System.Text.Json; using LeanCode.Components; using LeanCode.Contracts; +using LeanCode.Serialization; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -16,15 +18,23 @@ public static class LeanPipeServiceCollectionExtensions public static LeanPipeServicesBuilder AddLeanPipe( this IServiceCollection services, TypesCatalog topics, - TypesCatalog handlers + TypesCatalog handlers, + Action>? configureLeanPipeHub = null, + Action? overrideJsonHubProtocolOptions = null ) { - services + var signalRBuilder = services .AddSignalR() - .AddJsonProtocol(options => - options.PayloadSerializerOptions.PropertyNamingPolicy = null + .AddJsonProtocol( + overrideJsonHubProtocolOptions + ?? (options => options.PayloadSerializerOptions.ConfigureForCQRS()) ); + if (configureLeanPipeHub is not null) + { + signalRBuilder.AddHubOptions(configureLeanPipeHub); + } + services.AddTransient(); services.AddTransient(); services.AddTransient(typeof(ISubscriptionHandler<>), typeof(KeyedSubscriptionHandler<>)); @@ -44,14 +54,17 @@ public class LeanPipeServicesBuilder public IServiceCollection Services { get; } public TypesCatalog Topics { get; private set; } - private JsonSerializerOptions? options; + private JsonSerializerOptions options; public LeanPipeServicesBuilder(IServiceCollection services, TypesCatalog topics) { Services = services; Topics = topics; - Services.AddSingleton(new DefaultTopicExtractor(topics, null)); + options = new(); + options.ConfigureForCQRS(); + + Services.AddSingleton(new DefaultTopicExtractor(topics, options)); } /// diff --git a/publisher/src/LeanCode.Pipe/LeanCode.Pipe.csproj b/publisher/src/LeanCode.Pipe/LeanCode.Pipe.csproj index fdad09a..0502920 100644 --- a/publisher/src/LeanCode.Pipe/LeanCode.Pipe.csproj +++ b/publisher/src/LeanCode.Pipe/LeanCode.Pipe.csproj @@ -14,6 +14,7 @@ + diff --git a/publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/Instance/ServiceCollectionExtensionsTests.cs b/publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/Instance/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..96168d7 --- /dev/null +++ b/publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/Instance/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using LeanCode.Pipe.Funnel.Instance; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace LeanCode.Pipe.Funnel.Tests.Instance; + +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void Registers_required_basic_types_when_service_is_funnel() + { + var collection = new ServiceCollection(); + collection.AddLeanPipeFunnel(); + collection + .Should() + .ContainSingle(d => + d.ServiceType == typeof(HubLifetimeManager<>) + && d.Lifetime == ServiceLifetime.Singleton + ) + .And.ContainSingle(d => + d.ServiceType == typeof(IMemoryCache) && d.Lifetime == ServiceLifetime.Singleton + ) + .And.ContainSingle(d => + d.ServiceType == typeof(FunnelConfiguration) + && d.Lifetime == ServiceLifetime.Singleton + ) + .And.ContainSingle(d => + d.ServiceType == typeof(ISubscriptionExecutor) + && d.Lifetime == ServiceLifetime.Transient + ); + } + + [Fact] + public void Default_serializer_configuration_is_applied_to_funnel_when_no_override_is_provided() + { + var collection = new ServiceCollection(); + collection.AddLeanPipeFunnel(); + + var provider = collection.BuildServiceProvider(); + var hubProtocolOptions = provider + .GetRequiredService>() + .Value; + + hubProtocolOptions.PayloadSerializerOptions.PropertyNamingPolicy.Should().BeNull(); + } + + [Fact] + public void Override_replaces_default_serializer_configuration_in_funnel() + { + var collection = new ServiceCollection(); + collection.AddLeanPipeFunnel(overrideJsonHubProtocolOptions: options => + options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase + ); + + var provider = collection.BuildServiceProvider(); + var hubProtocolOptions = provider + .GetRequiredService>() + .Value; + + hubProtocolOptions + .PayloadSerializerOptions.PropertyNamingPolicy.Should() + .Be(JsonNamingPolicy.CamelCase); + } + + [Fact] + public void Hub_options_delegate_is_invoked_when_provided_to_funnel() + { + var collection = new ServiceCollection(); + collection.AddLeanPipeFunnel(configureLeanPipeHub: options => + options.MaximumReceiveMessageSize = 12345 + ); + + var provider = collection.BuildServiceProvider(); + var hubOptions = provider + .GetRequiredService>>() + .Value; + + hubOptions.MaximumReceiveMessageSize.Should().Be(12345); + } +} diff --git a/publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/ServiceCollectionExtensionsTests.cs b/publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/Publishing/ServiceCollectionExtensionsTests.cs similarity index 58% rename from publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/ServiceCollectionExtensionsTests.cs rename to publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/Publishing/ServiceCollectionExtensionsTests.cs index 5fe4939..687c8d1 100644 --- a/publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/ServiceCollectionExtensionsTests.cs +++ b/publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/Publishing/ServiceCollectionExtensionsTests.cs @@ -1,12 +1,12 @@ +using System.Text.Json; using LeanCode.Components; -using LeanCode.Pipe.Funnel.Instance; using LeanCode.Pipe.Funnel.Publishing; using LeanCode.Pipe.Tests; using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; -namespace LeanCode.Pipe.Funnel.Tests; +namespace LeanCode.Pipe.Funnel.Tests.Publishing; public class ServiceCollectionExtensionsTests { @@ -48,26 +48,37 @@ public void Registers_required_basic_types_when_service_is_funnelled() } [Fact] - public void Registers_required_basic_types_when_service_is_funnel() + public void Default_serializer_configuration_is_applied_to_funnelled_when_no_override_is_provided() { var collection = new ServiceCollection(); - collection.AddLeanPipeFunnel(); - collection - .Should() - .ContainSingle(d => - d.ServiceType == typeof(HubLifetimeManager<>) - && d.Lifetime == ServiceLifetime.Singleton - ) - .And.ContainSingle(d => - d.ServiceType == typeof(IMemoryCache) && d.Lifetime == ServiceLifetime.Singleton - ) - .And.ContainSingle(d => - d.ServiceType == typeof(FunnelConfiguration) - && d.Lifetime == ServiceLifetime.Singleton - ) - .And.ContainSingle(d => - d.ServiceType == typeof(ISubscriptionExecutor) - && d.Lifetime == ServiceLifetime.Transient - ); + collection.AddFunnelledLeanPipe(ThisCatalog, ThisCatalog); + + var provider = collection.BuildServiceProvider(); + var hubProtocolOptions = provider + .GetRequiredService>() + .Value; + + hubProtocolOptions.PayloadSerializerOptions.PropertyNamingPolicy.Should().BeNull(); + } + + [Fact] + public void Override_replaces_default_serializer_configuration_in_funnelled() + { + var collection = new ServiceCollection(); + collection.AddFunnelledLeanPipe( + ThisCatalog, + ThisCatalog, + overrideJsonHubProtocolOptions: options => + options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase + ); + + var provider = collection.BuildServiceProvider(); + var hubProtocolOptions = provider + .GetRequiredService>() + .Value; + + hubProtocolOptions + .PayloadSerializerOptions.PropertyNamingPolicy.Should() + .Be(JsonNamingPolicy.CamelCase); } } diff --git a/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs b/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs index 13a229e..eba337f 100644 --- a/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs +++ b/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs @@ -1,7 +1,10 @@ +using System.Text.Json; using LeanCode.Components; +using LeanCode.Contracts; using LeanCode.Pipe.Tests.Additional; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace LeanCode.Pipe.Tests; @@ -46,15 +49,20 @@ public void Registers_all_basic_types() } [Fact] - public void Updates_deserializer_when_registering_additional_types() + public void AddTopics_allows_extracting_old_and_new_topics() { var collection = new ServiceCollection(); collection.AddLeanPipe(ThisCatalog, ThisCatalog).AddTopics(ExternalCatalog); - var deserializer = collection.BuildServiceProvider().GetRequiredService(); - var topic = deserializer.Extract(Envelope.Empty()); + var provider = collection.BuildServiceProvider(); + var extractor = provider.GetRequiredService(); - topic.Should().NotBeNull().And.BeOfType(); + extractor.Extract(Envelope.Empty()).Should().NotBeNull().And.BeOfType(); + extractor + .Extract(Envelope.Empty()) + .Should() + .NotBeNull() + .And.BeOfType(); } [Fact] @@ -136,4 +144,140 @@ public void Throws_if_user_does_not_provide_all_notification_keys_implementation var act = () => builder.AddHandlers(ExternalCatalog); act.Should().Throw(); } + + [Fact] + public void Default_serializer_configuration_is_applied_to_hub_protocol_options_when_no_override_is_provided() + { + var collection = new ServiceCollection(); + collection.AddLeanPipe(ThisCatalog, ThisCatalog); + + var provider = collection.BuildServiceProvider(); + var hubProtocolOptions = provider + .GetRequiredService>() + .Value; + + hubProtocolOptions.PayloadSerializerOptions.PropertyNamingPolicy.Should().BeNull(); + } + + [Fact] + public void Override_replaces_default_hub_protocol_options_serializer_configuration() + { + var collection = new ServiceCollection(); + collection.AddLeanPipe( + ThisCatalog, + ThisCatalog, + overrideJsonHubProtocolOptions: options => + options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase + ); + + var provider = collection.BuildServiceProvider(); + var hubProtocolOptions = provider + .GetRequiredService>() + .Value; + + hubProtocolOptions + .PayloadSerializerOptions.PropertyNamingPolicy.Should() + .Be(JsonNamingPolicy.CamelCase); + } + + [Fact] + public void Hub_options_delegate_is_invoked_when_provided() + { + var collection = new ServiceCollection(); + collection.AddLeanPipe( + ThisCatalog, + ThisCatalog, + configureLeanPipeHub: options => options.MaximumReceiveMessageSize = 12345 + ); + + var provider = collection.BuildServiceProvider(); + var hubOptions = provider + .GetRequiredService>>() + .Value; + + hubOptions.MaximumReceiveMessageSize.Should().Be(12345); + } + + [Fact] + public void Default_serializer_options_are_applied_to_envelope_deserializer() + { + var collection = new ServiceCollection(); + collection.AddLeanPipe(TypesCatalog.Of(), ThisCatalog); + + var provider = collection.BuildServiceProvider(); + var extractor = provider.GetRequiredService(); + + var envelope = CreateEnvelope("""{"SomeValue": "test"}"""); + (extractor.Extract(envelope) as TopicWithProperty) + .Should() + .BeEquivalentTo(new TopicWithProperty { SomeValue = "test" }); + } + + [Fact] + public void WithEnvelopeDeserializerOptions_overrides_default_serializer_configuration() + { + var collection = new ServiceCollection(); + var customOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + collection + .AddLeanPipe(TypesCatalog.Of(), ThisCatalog) + .WithEnvelopeDeserializerOptions(customOptions); + + var provider = collection.BuildServiceProvider(); + var extractor = provider.GetRequiredService(); + + var envelope = CreateEnvelope("""{"someValue": "test"}"""); + (extractor.Extract(envelope) as TopicWithProperty) + .Should() + .BeEquivalentTo(new TopicWithProperty { SomeValue = "test" }); + } + + [Fact] + public void WithEnvelopeDeserializer_replaces_default_topic_extractor() + { + var collection = new ServiceCollection(); + var customExtractor = new CustomTopicExtractor(); + + collection.AddLeanPipe(ThisCatalog, ThisCatalog).WithEnvelopeDeserializer(customExtractor); + + var provider = collection.BuildServiceProvider(); + var extractor = provider.GetRequiredService(); + + extractor.Should().BeSameAs(customExtractor); + } + + [Fact] + public void Builder_returns_itself_for_method_chaining() + { + var collection = new ServiceCollection(); + var builder = collection.AddLeanPipe(ThisCatalog, ThisCatalog); + + builder + .WithEnvelopeDeserializerOptions(new JsonSerializerOptions()) + .Should() + .BeSameAs(builder); + builder.WithEnvelopeDeserializer(new CustomTopicExtractor()).Should().BeSameAs(builder); + builder.AddTopics(ExternalCatalog).Should().BeSameAs(builder); + builder.AddHandlers(ThisCatalog).Should().BeSameAs(builder); + } + + private static SubscriptionEnvelope CreateEnvelope(string json) + { + return new() + { + Id = Guid.NewGuid(), + TopicType = typeof(T).FullName!, + Topic = JsonDocument.Parse(json), + }; + } + + private sealed class CustomTopicExtractor : ITopicExtractor + { + public ITopic? Extract(SubscriptionEnvelope envelope) => null; + + public bool TopicExists(string topicType) => false; + } } diff --git a/publisher/test/LeanCode.Pipe.Tests/Topics.cs b/publisher/test/LeanCode.Pipe.Tests/Topics.cs index db2fb34..35e84e7 100644 --- a/publisher/test/LeanCode.Pipe.Tests/Topics.cs +++ b/publisher/test/LeanCode.Pipe.Tests/Topics.cs @@ -15,6 +15,11 @@ public class TopicWithAllKeys IProduceNotification, IProduceNotification { } +public class TopicWithProperty : ITopic +{ + public string SomeValue { get; set; } = string.Empty; +} + public class Notification1 { } public class Notification2 { }