Skip to content

Commit

Permalink
Update NATS.Client.Hosting package as NATS.Extensions.Microsoft.Depen…
Browse files Browse the repository at this point in the history
…dencyInjection (#433)

* Added NATS.Extensions.Microsoft.DependencyInjection project. Added NatsBuilder for customization of NATS configuration.

* dotnet format

* Marked AddNats method as obsolete and suggested to use NATS.Extensions.Microsoft.DependencyInjection instead.

* post-rebase clean up

* rename hosting method

* Test fix

* Project refs

---------

Co-authored-by: Ziya Suzen <ziya@suzen.net>
  • Loading branch information
rickdotnet and mtmk authored Apr 3, 2024
1 parent 4bda883 commit 8705f64
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 11 deletions.
9 changes: 8 additions & 1 deletion NATS.Client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTrimmable>true</IsTrimmable>

<!-- NuGet Packaging -->
<PackageTags>pubsub;messaging</PackageTags>
<Description>ASP.NET Core and Generic Host support for NATS.Net.</Description>
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0"/>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\NATS.Net\NATS.Net.csproj" />
</ItemGroup>

</Project>
135 changes: 135 additions & 0 deletions src/NATS.Extensions.Microsoft.DependencyInjection/NatsBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<NatsOpts, NatsOpts>? _configureOpts;
private Action<NatsConnection>? _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<NatsOpts, NatsOpts> 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<NatsConnection> 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<NatsConnectionPool>(provider => PoolFactory(provider));
_services.TryAddSingleton<INatsConnectionPool>(static provider => provider.GetRequiredService<NatsConnectionPool>());
_services.TryAddTransient<NatsConnection>(static provider => PooledConnectionFactory(provider, null));
_services.TryAddTransient<INatsConnection>(static provider => provider.GetRequiredService<NatsConnection>());
}
else
{
#if NET8_0_OR_GREATER
_services.TryAddKeyedSingleton<NatsConnectionPool>(_diKey, PoolFactory);
_services.TryAddKeyedSingleton<INatsConnectionPool>(_diKey, static (provider, key) => provider.GetRequiredKeyedService<NatsConnectionPool>(key));
_services.TryAddKeyedTransient(_diKey, PooledConnectionFactory);
_services.TryAddKeyedTransient<INatsConnection>(_diKey, static (provider, key) => provider.GetRequiredKeyedService<NatsConnection>(key));
#endif
}
}
else
{
if (_diKey == null)
{
_services.TryAddSingleton<NatsConnection>(provider => SingleConnectionFactory(provider));
_services.TryAddSingleton<INatsConnection>(static provider => provider.GetRequiredService<NatsConnection>());
}
else
{
#if NET8_0_OR_GREATER
_services.TryAddKeyedSingleton(_diKey, SingleConnectionFactory);
_services.TryAddKeyedSingleton<INatsConnection>(_diKey, static (provider, key) => provider.GetRequiredKeyedService<NatsConnection>(key));
#endif
}
}

return _services;
}

private static NatsConnection PooledConnectionFactory(IServiceProvider provider, object? key)
{
#if NET8_0_OR_GREATER
if (key != null)
{
var keyedConnection = provider.GetRequiredKeyedService<NatsConnectionPool>(key).GetConnection();
return keyedConnection as NatsConnection ?? throw new InvalidOperationException("Connection is not of type NatsConnection");
}
#endif
var connection = provider.GetRequiredService<NatsConnectionPool>().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<ILoggerFactory>() };
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<ILoggerFactory>() };
options = _configureOpts?.Invoke(options) ?? options;

var conn = new NatsConnection(options);
_configureConnection?.Invoke(conn);

return conn;
}
}
Original file line number Diff line number Diff line change
@@ -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<NatsBuilder>? buildAction = null)
{
var builder = new NatsBuilder(services);
buildAction?.Invoke(builder);

builder.Build();
return services;
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\NATS.Client.Hosting\NATS.Client.Hosting.csproj" />
<ProjectReference Include="..\..\src\NATS.Extensions.Microsoft.DependencyInjection\NATS.Extensions.Microsoft.DependencyInjection.csproj" />
<ProjectReference Include="..\NATS.Client.TestUtilities\NATS.Client.TestUtilities.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -12,10 +13,9 @@ public void AddNats_RegistersNatsConnectionAsSingleton_WhenPoolSizeIsOne()
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
services.AddNatsClient();

services.AddNats(poolSize: 1);
var provider = services.BuildServiceProvider();

var natsConnection1 = provider.GetRequiredService<INatsConnection>();
var natsConnection2 = provider.GetRequiredService<INatsConnection>();

Expand All @@ -28,17 +28,43 @@ public void AddNats_RegistersNatsConnectionAsTransient_WhenPoolSizeIsGreaterThan
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
services.AddNatsClient(builder => builder.WithPoolSize(2));

services.AddNats(poolSize: 2);
var provider = services.BuildServiceProvider();

var natsConnection1 = provider.GetRequiredService<INatsConnection>();
var natsConnection2 = provider.GetRequiredService<INatsConnection>();

Assert.NotNull(natsConnection1);
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<ILoggerFactory, NullLoggerFactory>();
services.AddNatsClient(builder =>
{
builder.ConfigureOptions(opts => server.ClientOpts(opts));
builder.AddJsonSerialization(MyJsonContext.Default);
});

var provider = services.BuildServiceProvider();
var nats = provider.GetRequiredService<INatsConnection>();

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var cancellationToken = cts.Token;

await using var sub = await nats.SubscribeCoreAsync<MyData>("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()
Expand All @@ -49,8 +75,9 @@ public void AddNats_RegistersKeyedNatsConnection_WhenKeyIsProvided()
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

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<INatsConnection>(key1);
Expand All @@ -73,8 +100,8 @@ public void AddNats_RegistersKeyedNatsConnection_WhenKeyIsProvided_pooled()
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

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<string, List<object>> connections = new();
Expand Down

0 comments on commit 8705f64

Please sign in to comment.