From 990a88700d086645a2b915c3aa5dbf89a52b7dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Draga=C5=84czuk?= Date: Fri, 2 Jan 2026 13:55:31 +0100 Subject: [PATCH 1/6] Configure CQRS JSON serialization by default in SignalR --- publisher/Directory.Packages.props | 1 + .../Instance/ServiceCollectionExtensions.cs | 9 ++++++--- .../Publishing/ServiceCollectionExtensions.cs | 7 ++++--- .../CQRSJsonSerializerOptionsExtensions.cs | 15 +++++++++++++++ .../LeanPipeServiceCollectionExtensions.cs | 9 ++++++--- publisher/src/LeanCode.Pipe/LeanCode.Pipe.csproj | 1 + 6 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 publisher/src/LeanCode.Pipe/Extensions/CQRSJsonSerializerOptionsExtensions.cs diff --git a/publisher/Directory.Packages.props b/publisher/Directory.Packages.props index e0870fc..7fdcc51 100644 --- a/publisher/Directory.Packages.props +++ b/publisher/Directory.Packages.props @@ -13,6 +13,7 @@ + diff --git a/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs b/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs index 5304992..1aa455e 100644 --- a/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs +++ b/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; namespace LeanCode.Pipe.Funnel.Instance; @@ -9,13 +10,15 @@ public static class ServiceCollectionExtensions /// public static IServiceCollection AddLeanPipeFunnel( this IServiceCollection services, - FunnelConfiguration? config = null + FunnelConfiguration? config = null, + Action? overrideJsonHubProtocolOptions = null ) { services .AddSignalR() - .AddJsonProtocol(options => - options.PayloadSerializerOptions.PropertyNamingPolicy = null + .AddJsonProtocol( + overrideJsonHubProtocolOptions + ?? (options => options.PayloadSerializerOptions.ConfigureForCQRS()) ); services.AddMemoryCache(); diff --git a/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs b/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs index 0165086..2cb44d3 100644 --- a/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs +++ b/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs @@ -17,7 +17,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 +29,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/CQRSJsonSerializerOptionsExtensions.cs b/publisher/src/LeanCode.Pipe/Extensions/CQRSJsonSerializerOptionsExtensions.cs new file mode 100644 index 0000000..4b0343f --- /dev/null +++ b/publisher/src/LeanCode.Pipe/Extensions/CQRSJsonSerializerOptionsExtensions.cs @@ -0,0 +1,15 @@ +using System.Text.Json; +using LeanCode.Serialization; + +namespace LeanCode.Pipe; + +public static class LeanPipeJsonSerializerOptionsExtensions +{ + public static void ConfigureForCQRS(this JsonSerializerOptions options) + { + options.Converters.Add(new JsonLaxDateOnlyConverter()); + options.Converters.Add(new JsonLaxTimeOnlyConverter()); + options.Converters.Add(new JsonLaxDateTimeOffsetConverter()); + options.PropertyNamingPolicy = null; + } +} diff --git a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs index c00f800..ee42e4e 100644 --- a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs +++ b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System.Text.Json; using LeanCode.Components; using LeanCode.Contracts; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -16,13 +17,15 @@ public static class LeanPipeServiceCollectionExtensions public static LeanPipeServicesBuilder AddLeanPipe( this IServiceCollection services, TypesCatalog topics, - TypesCatalog handlers + TypesCatalog handlers, + Action? overrideJsonHubProtocolOptions = null ) { services .AddSignalR() - .AddJsonProtocol(options => - options.PayloadSerializerOptions.PropertyNamingPolicy = null + .AddJsonProtocol( + overrideJsonHubProtocolOptions + ?? (options => options.PayloadSerializerOptions.ConfigureForCQRS()) ); services.AddTransient(); 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 @@ + From dffdca1e7ca32d458e53e2ab8f3eb9e71a669dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Draga=C5=84czuk?= Date: Fri, 2 Jan 2026 14:18:39 +0100 Subject: [PATCH 2/6] Add extensibility for LeanPipeHub configuration in SignalR setup --- .../src/Funnel/Instance/ServiceCollectionExtensions.cs | 8 +++++++- .../Extensions/LeanPipeServiceCollectionExtensions.cs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs b/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs index 1aa455e..bbd6c84 100644 --- a/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs +++ b/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs @@ -11,16 +11,22 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddLeanPipeFunnel( this IServiceCollection services, FunnelConfiguration? config = null, + Action>? configureLeanPipeHub = null, Action? overrideJsonHubProtocolOptions = null ) { - services + var signalRBuilder = services .AddSignalR() .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/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs index ee42e4e..4accefd 100644 --- a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs +++ b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs @@ -18,16 +18,22 @@ public static LeanPipeServicesBuilder AddLeanPipe( this IServiceCollection services, TypesCatalog topics, TypesCatalog handlers, + Action>? configureLeanPipeHub = null, Action? overrideJsonHubProtocolOptions = null ) { - services + var signalRBuilder = services .AddSignalR() .AddJsonProtocol( overrideJsonHubProtocolOptions ?? (options => options.PayloadSerializerOptions.ConfigureForCQRS()) ); + if (configureLeanPipeHub is not null) + { + signalRBuilder.AddHubOptions(configureLeanPipeHub); + } + services.AddTransient(); services.AddTransient(); services.AddTransient(typeof(ISubscriptionHandler<>), typeof(KeyedSubscriptionHandler<>)); From d699bbc537fe87df59e731603db81b1ac4e84933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Draga=C5=84czuk?= Date: Fri, 2 Jan 2026 14:41:07 +0100 Subject: [PATCH 3/6] Test SignalR configurations extensions --- .../ServiceCollectionExtensionsTests.cs | 83 +++++++++++++++++++ .../ServiceCollectionExtensionsTests.cs | 55 +++++++----- ...eanPipeServiceCollectionExtensionsTests.cs | 55 ++++++++++++ 3 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/Instance/ServiceCollectionExtensionsTests.cs rename publisher/test/Funnel.Tests/LeanCode.Pipe.Funnel.Tests/{ => Publishing}/ServiceCollectionExtensionsTests.cs (58%) 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..242ac27 100644 --- a/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs +++ b/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs @@ -1,7 +1,9 @@ +using System.Text.Json; using LeanCode.Components; using LeanCode.Pipe.Tests.Additional; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace LeanCode.Pipe.Tests; @@ -136,4 +138,57 @@ 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_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_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); + } } From bab0d16dff5cde985934e090363d5a0fb96ac989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Draga=C5=84czuk?= Date: Mon, 5 Jan 2026 09:42:44 +0100 Subject: [PATCH 4/6] Configure CQRS JSON serialization by default in topic extractor # Conflicts: # publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs --- .../Extensions/LeanPipeServiceCollectionExtensions.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs index 4accefd..eb638a5 100644 --- a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs +++ b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs @@ -53,14 +53,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)); } /// From 9b2857f0495cdc5404e2842e319ae83fbdfbefc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Draga=C5=84czuk?= Date: Mon, 5 Jan 2026 09:43:52 +0100 Subject: [PATCH 5/6] Test LeanPipe services builder --- ...eanPipeServiceCollectionExtensionsTests.cs | 101 ++++++++++++++++-- publisher/test/LeanCode.Pipe.Tests/Topics.cs | 5 + 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs b/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs index 242ac27..eba337f 100644 --- a/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs +++ b/publisher/test/LeanCode.Pipe.Tests/LeanPipeServiceCollectionExtensionsTests.cs @@ -1,5 +1,6 @@ using System.Text.Json; using LeanCode.Components; +using LeanCode.Contracts; using LeanCode.Pipe.Tests.Additional; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; @@ -48,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] @@ -140,7 +146,7 @@ public void Throws_if_user_does_not_provide_all_notification_keys_implementation } [Fact] - public void Default_serializer_configuration_is_applied_when_no_override_is_provided() + public void Default_serializer_configuration_is_applied_to_hub_protocol_options_when_no_override_is_provided() { var collection = new ServiceCollection(); collection.AddLeanPipe(ThisCatalog, ThisCatalog); @@ -154,7 +160,7 @@ public void Default_serializer_configuration_is_applied_when_no_override_is_prov } [Fact] - public void Override_replaces_default_serializer_configuration() + public void Override_replaces_default_hub_protocol_options_serializer_configuration() { var collection = new ServiceCollection(); collection.AddLeanPipe( @@ -191,4 +197,87 @@ public void Hub_options_delegate_is_invoked_when_provided() 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 { } From 78879802f1dace87792ef380527e93a1b7fd013f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Draga=C5=84czuk?= Date: Mon, 5 Jan 2026 10:03:00 +0100 Subject: [PATCH 6/6] Bump CoreLib version and use exposed CQRS Serialization options --- publisher/Directory.Packages.props | 2 +- .../Instance/ServiceCollectionExtensions.cs | 1 + .../Publishing/ServiceCollectionExtensions.cs | 1 + .../CQRSJsonSerializerOptionsExtensions.cs | 15 --------------- .../LeanPipeServiceCollectionExtensions.cs | 1 + 5 files changed, 4 insertions(+), 16 deletions(-) delete mode 100644 publisher/src/LeanCode.Pipe/Extensions/CQRSJsonSerializerOptionsExtensions.cs diff --git a/publisher/Directory.Packages.props b/publisher/Directory.Packages.props index 7fdcc51..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 diff --git a/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs b/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs index bbd6c84..9c4a801 100644 --- a/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs +++ b/publisher/src/Funnel/Instance/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using LeanCode.Serialization; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; diff --git a/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs b/publisher/src/Funnel/Publishing/ServiceCollectionExtensions.cs index 2cb44d3..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; diff --git a/publisher/src/LeanCode.Pipe/Extensions/CQRSJsonSerializerOptionsExtensions.cs b/publisher/src/LeanCode.Pipe/Extensions/CQRSJsonSerializerOptionsExtensions.cs deleted file mode 100644 index 4b0343f..0000000 --- a/publisher/src/LeanCode.Pipe/Extensions/CQRSJsonSerializerOptionsExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json; -using LeanCode.Serialization; - -namespace LeanCode.Pipe; - -public static class LeanPipeJsonSerializerOptionsExtensions -{ - public static void ConfigureForCQRS(this JsonSerializerOptions options) - { - options.Converters.Add(new JsonLaxDateOnlyConverter()); - options.Converters.Add(new JsonLaxTimeOnlyConverter()); - options.Converters.Add(new JsonLaxDateTimeOffsetConverter()); - options.PropertyNamingPolicy = null; - } -} diff --git a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs index eb638a5..73605d6 100644 --- a/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs +++ b/publisher/src/LeanCode.Pipe/Extensions/LeanPipeServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ 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;