Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update NATS.Client.Hosting package as NATS.Extensions.Microsoft.DependencyInjection #433

Merged
merged 8 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading