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 { }