diff --git a/NATS.Client.sln b/NATS.Client.sln index 6de6c8971..ee9dac34d 100644 --- a/NATS.Client.sln +++ b/NATS.Client.sln @@ -99,7 +99,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS.Net", "src\NATS.Net\NA EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS.Net.DocsExamples", "tests\NATS.Net.DocsExamples\NATS.Net.DocsExamples.csproj", "{389C05EB-A0B3-4097-8C1F-4D55818438CC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS.Client.Hosting.Tests", "tests\NATS.Client.Hosting.Tests\NATS.Client.Hosting.Tests.csproj", "{766C2486-34C3-4DD1-B31C-540C17C044B0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS.Extensions.Microsoft.DependencyInjection.Tests", "tests\NATS.Extensions.Microsoft.DependencyInjection.Tests\NATS.Extensions.Microsoft.DependencyInjection.Tests.csproj", "{766C2486-34C3-4DD1-B31C-540C17C044B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS.Extensions.Microsoft.DependencyInjection", "src\NATS.Extensions.Microsoft.DependencyInjection\NATS.Extensions.Microsoft.DependencyInjection.csproj", "{2EA0EB68-1AA4-40AC-8FA2-B51532075567}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenTelemetry", "sandbox\Example.OpenTelemetry\Example.OpenTelemetry.csproj", "{474BA453-9CFF-41C2-B2E7-ADD92CC93E86}" EndProject @@ -271,6 +273,10 @@ Global {766C2486-34C3-4DD1-B31C-540C17C044B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {766C2486-34C3-4DD1-B31C-540C17C044B0}.Release|Any CPU.ActiveCfg = Release|Any CPU {766C2486-34C3-4DD1-B31C-540C17C044B0}.Release|Any CPU.Build.0 = Release|Any CPU + {2EA0EB68-1AA4-40AC-8FA2-B51532075567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EA0EB68-1AA4-40AC-8FA2-B51532075567}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EA0EB68-1AA4-40AC-8FA2-B51532075567}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EA0EB68-1AA4-40AC-8FA2-B51532075567}.Release|Any CPU.Build.0 = Release|Any CPU {474BA453-9CFF-41C2-B2E7-ADD92CC93E86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {474BA453-9CFF-41C2-B2E7-ADD92CC93E86}.Debug|Any CPU.Build.0 = Debug|Any CPU {474BA453-9CFF-41C2-B2E7-ADD92CC93E86}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -325,6 +331,7 @@ Global {6A7B9B9F-BFA4-4A6D-9006-0AAF597FC6DD} = {4827B3EC-73D8-436D-AE2A-5E29AC95FD0C} {389C05EB-A0B3-4097-8C1F-4D55818438CC} = {C526E8AB-739A-48D7-8FC4-048978C9B650} {766C2486-34C3-4DD1-B31C-540C17C044B0} = {C526E8AB-739A-48D7-8FC4-048978C9B650} + {2EA0EB68-1AA4-40AC-8FA2-B51532075567} = {4827B3EC-73D8-436D-AE2A-5E29AC95FD0C} {474BA453-9CFF-41C2-B2E7-ADD92CC93E86} = {95A69671-16CA-4133-981C-CC381B7AAA30} {B8554582-DE19-41A2-9784-9B27C9F22429} = {C526E8AB-739A-48D7-8FC4-048978C9B650} EndGlobalSection diff --git a/src/NATS.Extensions.Microsoft.DependencyInjection/NATS.Extensions.Microsoft.DependencyInjection.csproj b/src/NATS.Extensions.Microsoft.DependencyInjection/NATS.Extensions.Microsoft.DependencyInjection.csproj new file mode 100644 index 000000000..e645c3bfd --- /dev/null +++ b/src/NATS.Extensions.Microsoft.DependencyInjection/NATS.Extensions.Microsoft.DependencyInjection.csproj @@ -0,0 +1,27 @@ + + + + net6.0;net8.0 + enable + enable + true + + + pubsub;messaging + ASP.NET Core and Generic Host support for NATS.Net. + true + + + + + + + + + + + + + + + diff --git a/src/NATS.Extensions.Microsoft.DependencyInjection/NatsBuilder.cs b/src/NATS.Extensions.Microsoft.DependencyInjection/NatsBuilder.cs new file mode 100644 index 000000000..73ab65b82 --- /dev/null +++ b/src/NATS.Extensions.Microsoft.DependencyInjection/NatsBuilder.cs @@ -0,0 +1,135 @@ +using System.Text.Json.Serialization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using NATS.Client.Core; + +namespace NATS.Extensions.Microsoft.DependencyInjection; + +public class NatsBuilder +{ + private readonly IServiceCollection _services; + private int _poolSize = 1; + private Func? _configureOpts; + private Action? _configureConnection; + private object? _diKey = null; + + public NatsBuilder(IServiceCollection services) + => _services = services; + + public NatsBuilder WithPoolSize(int size) + { + _poolSize = Math.Max(size, 1); + return this; + } + + public NatsBuilder ConfigureOptions(Func optsFactory) + { + var previousFactory = _configureOpts; + _configureOpts = opts => + { + // Apply the previous configurator if it exists. + if (previousFactory != null) + { + opts = previousFactory(opts); + } + + // Then apply the new configurator. + return optsFactory(opts); + }; + return this; + } + + public NatsBuilder ConfigureConnection(Action connectionOpts) + { + _configureConnection = connectionOpts; + return this; + } + + public NatsBuilder AddJsonSerialization(JsonSerializerContext context) + => ConfigureOptions(opts => + { + var jsonRegistry = new NatsJsonContextSerializerRegistry(context); + return opts with { SerializerRegistry = jsonRegistry }; + }); + +#if NET8_0_OR_GREATER + public NatsBuilder WithKey(object key) + { + _diKey = key; + return this; + } +#endif + + public IServiceCollection Build() + { + if (_poolSize != 1) + { + if (_diKey == null) + { + _services.TryAddSingleton(provider => PoolFactory(provider)); + _services.TryAddSingleton(static provider => provider.GetRequiredService()); + _services.TryAddTransient(static provider => PooledConnectionFactory(provider, null)); + _services.TryAddTransient(static provider => provider.GetRequiredService()); + } + else + { +#if NET8_0_OR_GREATER + _services.TryAddKeyedSingleton(_diKey, PoolFactory); + _services.TryAddKeyedSingleton(_diKey, static (provider, key) => provider.GetRequiredKeyedService(key)); + _services.TryAddKeyedTransient(_diKey, PooledConnectionFactory); + _services.TryAddKeyedTransient(_diKey, static (provider, key) => provider.GetRequiredKeyedService(key)); +#endif + } + } + else + { + if (_diKey == null) + { + _services.TryAddSingleton(provider => SingleConnectionFactory(provider)); + _services.TryAddSingleton(static provider => provider.GetRequiredService()); + } + else + { +#if NET8_0_OR_GREATER + _services.TryAddKeyedSingleton(_diKey, SingleConnectionFactory); + _services.TryAddKeyedSingleton(_diKey, static (provider, key) => provider.GetRequiredKeyedService(key)); +#endif + } + } + + return _services; + } + + private static NatsConnection PooledConnectionFactory(IServiceProvider provider, object? key) + { +#if NET8_0_OR_GREATER + if (key != null) + { + var keyedConnection = provider.GetRequiredKeyedService(key).GetConnection(); + return keyedConnection as NatsConnection ?? throw new InvalidOperationException("Connection is not of type NatsConnection"); + } +#endif + var connection = provider.GetRequiredService().GetConnection(); + return connection as NatsConnection ?? throw new InvalidOperationException("Connection is not of type NatsConnection"); + } + + private NatsConnectionPool PoolFactory(IServiceProvider provider, object? diKey = null) + { + var options = NatsOpts.Default with { LoggerFactory = provider.GetRequiredService() }; + options = _configureOpts?.Invoke(options) ?? options; + + return new NatsConnectionPool(_poolSize, options, _configureConnection ?? (_ => { })); + } + + private NatsConnection SingleConnectionFactory(IServiceProvider provider, object? diKey = null) + { + var options = NatsOpts.Default with { LoggerFactory = provider.GetRequiredService() }; + options = _configureOpts?.Invoke(options) ?? options; + + var conn = new NatsConnection(options); + _configureConnection?.Invoke(conn); + + return conn; + } +} diff --git a/src/NATS.Extensions.Microsoft.DependencyInjection/NatsHostingExtensions.cs b/src/NATS.Extensions.Microsoft.DependencyInjection/NatsHostingExtensions.cs new file mode 100644 index 000000000..4341f2270 --- /dev/null +++ b/src/NATS.Extensions.Microsoft.DependencyInjection/NatsHostingExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace NATS.Extensions.Microsoft.DependencyInjection; + +public static class NatsHostingExtensions +{ + public static IServiceCollection AddNatsClient(this IServiceCollection services, Action? buildAction = null) + { + var builder = new NatsBuilder(services); + buildAction?.Invoke(builder); + + builder.Build(); + return services; + } +} diff --git a/tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/MyJsonContext.cs b/tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/MyJsonContext.cs new file mode 100644 index 000000000..fceba059f --- /dev/null +++ b/tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/MyJsonContext.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace NATS.Extensions.Microsoft.DependencyInjection.Tests; + +[JsonSerializable(typeof(MyData))] +internal partial class MyJsonContext : JsonSerializerContext; + +public record MyData +{ + public MyData(string name) => Name = name; + + public string Name { get; set; } +} diff --git a/tests/NATS.Client.Hosting.Tests/NATS.Client.Hosting.Tests.csproj b/tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/NATS.Extensions.Microsoft.DependencyInjection.Tests.csproj similarity index 83% rename from tests/NATS.Client.Hosting.Tests/NATS.Client.Hosting.Tests.csproj rename to tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/NATS.Extensions.Microsoft.DependencyInjection.Tests.csproj index a023e1ca0..e4d3dd79f 100644 --- a/tests/NATS.Client.Hosting.Tests/NATS.Client.Hosting.Tests.csproj +++ b/tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/NATS.Extensions.Microsoft.DependencyInjection.Tests.csproj @@ -28,7 +28,8 @@ - + + diff --git a/tests/NATS.Client.Hosting.Tests/NatsHostingExtensionsTests.cs b/tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/NatsHostingExtensionsTests.cs similarity index 71% rename from tests/NATS.Client.Hosting.Tests/NatsHostingExtensionsTests.cs rename to tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/NatsHostingExtensionsTests.cs index 0c5e5d091..fe1987de1 100644 --- a/tests/NATS.Client.Hosting.Tests/NatsHostingExtensionsTests.cs +++ b/tests/NATS.Extensions.Microsoft.DependencyInjection.Tests/NatsHostingExtensionsTests.cs @@ -2,8 +2,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NATS.Client.Core; +using NATS.Client.Core.Tests; -namespace NATS.Client.Hosting.Tests; +namespace NATS.Extensions.Microsoft.DependencyInjection.Tests; public class NatsHostingExtensionsTests { @@ -12,10 +13,9 @@ public void AddNats_RegistersNatsConnectionAsSingleton_WhenPoolSizeIsOne() { var services = new ServiceCollection(); services.AddSingleton(); + services.AddNatsClient(); - services.AddNats(poolSize: 1); var provider = services.BuildServiceProvider(); - var natsConnection1 = provider.GetRequiredService(); var natsConnection2 = provider.GetRequiredService(); @@ -28,10 +28,9 @@ public void AddNats_RegistersNatsConnectionAsTransient_WhenPoolSizeIsGreaterThan { var services = new ServiceCollection(); services.AddSingleton(); + services.AddNatsClient(builder => builder.WithPoolSize(2)); - services.AddNats(poolSize: 2); var provider = services.BuildServiceProvider(); - var natsConnection1 = provider.GetRequiredService(); var natsConnection2 = provider.GetRequiredService(); @@ -39,6 +38,33 @@ public void AddNats_RegistersNatsConnectionAsTransient_WhenPoolSizeIsGreaterThan Assert.NotSame(natsConnection1, natsConnection2); // Transient should return different instances } + [Fact] + public async Task AddNats_WithJsonSerializer() + { + await using var server = NatsServer.Start(); + + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddNatsClient(builder => + { + builder.ConfigureOptions(opts => server.ClientOpts(opts)); + builder.AddJsonSerialization(MyJsonContext.Default); + }); + + var provider = services.BuildServiceProvider(); + var nats = provider.GetRequiredService(); + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var cancellationToken = cts.Token; + + await using var sub = await nats.SubscribeCoreAsync("foo", cancellationToken: cancellationToken); + await nats.PingAsync(cancellationToken); + await nats.PublishAsync("foo", new MyData("bar"), cancellationToken: cancellationToken); + + var msg = await sub.Msgs.ReadAsync(cancellationToken); + Assert.Equal("bar", msg.Data?.Name); + } + #if NET8_0_OR_GREATER [Fact] public void AddNats_RegistersKeyedNatsConnection_WhenKeyIsProvided() @@ -49,8 +75,9 @@ public void AddNats_RegistersKeyedNatsConnection_WhenKeyIsProvided() var services = new ServiceCollection(); services.AddSingleton(); - services.AddNats(poolSize: 1, key: key1); - services.AddNats(poolSize: 1, key: key2); + services.AddNatsClient(builder => builder.WithKey(key1)); + services.AddNatsClient(builder => builder.WithKey(key2)); + var provider = services.BuildServiceProvider(); var natsConnection1A = provider.GetKeyedService(key1); @@ -73,8 +100,8 @@ public void AddNats_RegistersKeyedNatsConnection_WhenKeyIsProvided_pooled() var services = new ServiceCollection(); services.AddSingleton(); - services.AddNats(poolSize: 2, key: key1); - services.AddNats(poolSize: 2, key: key2); + services.AddNatsClient(builder => builder.WithPoolSize(2).WithKey(key1)); + services.AddNatsClient(builder => builder.WithPoolSize(2).WithKey(key2)); var provider = services.BuildServiceProvider(); Dictionary> connections = new();