♻️ Remove etcd, replace with asprie. Move infra to aspire. Disable gateway for now

This commit is contained in:
2025-09-15 00:16:13 +08:00
parent 5c97733b3e
commit 091097a858
46 changed files with 360 additions and 723 deletions

View File

@@ -1,4 +1,3 @@
using dotnet_etcd.interfaces;
using DysonNetwork.Shared.Proto;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -11,32 +10,14 @@ public static class DysonAuthStartup
this IServiceCollection services
)
{
services.AddSingleton<AuthService.AuthServiceClient>(sp =>
services.AddGrpcClient<AuthService.AuthServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://pass");
});
services.AddSingleton<PermissionService.PermissionServiceClient>(sp =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreatePermissionServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
services.AddGrpcClient<PermissionService.PermissionServiceClient>(o =>
{
o.Address = new Uri("https://pass");
});
services.AddAuthentication(options =>

View File

@@ -7,11 +7,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="dotnet-etcd" Version="8.0.1" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" />
<PackageReference Include="Google.Protobuf" Version="3.31.1" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" />
<PackageReference Include="Grpc" Version="2.46.6" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.71.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
<PackageReference Include="Grpc.Tools" Version="2.72.0">
<PrivateAssets>all</PrivateAssets>
@@ -27,7 +27,6 @@
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
@@ -40,4 +39,8 @@
<Folder Include="Error\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
</ItemGroup>
</Project>

View File

@@ -20,20 +20,6 @@ public static class KestrelConfiguration
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = maxRequestBodySize;
var configuredUrl = Environment.GetEnvironmentVariable("ASPNETCORE_URLS");
if (!string.IsNullOrEmpty(configuredUrl)) return;
var certPath = configuration["Service:ClientCert"]!;
var keyPath = configuration["Service:ClientKey"]!;
// Load PEM cert and key manually
var certificate = X509Certificate2.CreateFromPemFile(certPath, keyPath);
// Now pass the full cert
options.ListenAnyIP(5001, listenOptions => { listenOptions.UseHttps(certificate); });
// Optional: HTTP fallback
options.ListenAnyIP(8080);
});
return builder;

View File

@@ -2,171 +2,13 @@ using System.Net;
using Grpc.Net.Client;
using System.Security.Cryptography.X509Certificates;
using Grpc.Core;
using dotnet_etcd.interfaces;
namespace DysonNetwork.Shared.Proto;
public static class GrpcClientHelper
{
public static CallInvoker CreateCallInvoker(
string url,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
public static CallInvoker CreateCallInvoker(string url)
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(
clientCertPassword is null
? X509Certificate2.CreateFromPemFile(clientCertPath, clientKeyPath)
: X509Certificate2.CreateFromEncryptedPemFile(clientCertPath, clientCertPassword, clientKeyPath)
);
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
var httpClient = new HttpClient(handler);
httpClient.DefaultRequestVersion = HttpVersion.Version20;
httpClient.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
return GrpcChannel.ForAddress(url, new GrpcChannelOptions { HttpClient = httpClient }).CreateCallInvoker();
}
private static async Task<string> GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName)
{
var response = await etcdClient.GetAsync($"/services/{serviceName}");
return response.Kvs.Count == 0
? throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd.")
: response.Kvs[0].Value.ToStringUtf8();
}
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<BotAccountReceiverService.BotAccountReceiverServiceClient>
CreateBotAccountReceiverServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
return new BotAccountReceiverService.BotAccountReceiverServiceClient(CreateCallInvoker(url, clientCertPath,
clientKeyPath,
clientCertPassword));
}
public static async Task<ActionLogService.ActionLogServiceClient> CreateActionLogServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
return new ActionLogService.ActionLogServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<AuthService.AuthServiceClient> CreateAuthServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<PermissionService.PermissionServiceClient> CreatePermissionServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
return new PermissionService.PermissionServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<PaymentService.PaymentServiceClient> CreatePaymentServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
return new PaymentService.PaymentServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<RingService.RingServiceClient> CreateRingServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Ring");
return new RingService.RingServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<FileService.FileServiceClient> CreateFileServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Drive");
return new FileService.FileServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<FileReferenceService.FileReferenceServiceClient> CreateFileReferenceServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Drive");
return new FileReferenceService.FileReferenceServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<PublisherService.PublisherServiceClient> CreatePublisherServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Sphere");
return new PublisherService.PublisherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<CustomAppService.CustomAppServiceClient> CreateCustomAppServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Develop");
return new CustomAppService.CustomAppServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
return GrpcChannel.ForAddress(url).CreateCallInvoker();
}
}

View File

@@ -1,83 +0,0 @@
using System.Diagnostics;
using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Yarp.ReverseProxy.Forwarder;
namespace DysonNetwork.Shared.Registry;
public static class GatewayReverseProxy
{
/// <summary>
/// Provides reverse proxy for DysonNetwork.Gateway.
/// Give the ability to the contained frontend to access other services via the gateway.
/// </summary>
/// <param name="app">The asp.net core application</param>
/// <returns>The modified application</returns>
public static WebApplication MapGatewayProxy(this WebApplication app)
{
var httpClient = new HttpMessageInvoker(new SocketsHttpHandler
{
UseProxy = false,
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.All,
UseCookies = true,
EnableMultipleHttp2Connections = true,
ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),
ConnectTimeout = TimeSpan.FromSeconds(15),
});
var transformer = new GatewayReverseProxyTransformer();
var requestConfig = new ForwarderRequestConfig();
app.Map("/cgi/{**catch-all}", async (HttpContext context, IHttpForwarder forwarder) =>
{
var registry = context.RequestServices.GetRequiredService<ServiceRegistry>();
var gatewayUrl = await registry.GetServiceUrl("DysonNetwork.Gateway");
if (gatewayUrl is null)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Gateway not found");
return;
}
var error = await forwarder.SendAsync(
context,
gatewayUrl,
httpClient,
requestConfig,
transformer
);
if (error != ForwarderError.None)
{
var errorFeature = context.GetForwarderErrorFeature();
var exception = errorFeature?.Exception;
context.Response.StatusCode = 502;
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync($"Gateway remote error: {exception?.Message}");
}
});
return app;
}
}
public class GatewayReverseProxyTransformer : HttpTransformer
{
private const string Value = "/cgi";
public override ValueTask TransformRequestAsync(
HttpContext httpContext,
HttpRequestMessage proxyRequest,
string destinationPrefix,
CancellationToken cancellationToken
)
{
httpContext.Request.Path = httpContext.Request.Path.StartsWithSegments(Value, out var remaining)
? remaining
: httpContext.Request.Path;
return Default.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);
}
}

View File

@@ -1,60 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DysonNetwork.Shared.Registry;
public class RegistryHostedService(
ServiceRegistry serviceRegistry,
IConfiguration configuration,
ILogger<RegistryHostedService> logger
)
: IHostedService
{
private CancellationTokenSource? _cts;
public async Task StartAsync(CancellationToken cancellationToken)
{
var serviceName = configuration["Service:Name"];
var serviceUrl = configuration["Service:Url"];
var insecure = configuration.GetValue<bool>("Etcd:Insecure");
var remote = configuration.GetConnectionString("Etcd");
if (insecure)
logger.LogWarning("Etcd is configured to use insecure channel.");
if (string.IsNullOrEmpty(serviceUrl) || string.IsNullOrEmpty(serviceName))
{
logger.LogWarning("Service URL or Service Name was not configured. Skipping Etcd registration.");
return;
}
logger.LogInformation(
"Registering service {ServiceName} at {ServiceUrl} with Etcd ({Remote}).",
serviceName,
serviceUrl,
remote
);
try
{
_cts = new CancellationTokenSource();
await serviceRegistry.RegisterService(serviceName, serviceUrl, cancellationToken: _cts.Token);
logger.LogInformation("Service {ServiceName} registered successfully.", serviceName);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to register service {ServiceName} with Etcd.", serviceName);
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_cts?.Cancel();
// The lease will expire automatically if the service stops ungracefully.
var serviceName = configuration["Service:Name"];
if (serviceName is not null)
await serviceRegistry.UnregisterService(serviceName);
logger.LogInformation("Service registration hosted service is stopping.");
}
}

View File

@@ -1,4 +1,3 @@
using dotnet_etcd.interfaces;
using DysonNetwork.Shared.Proto;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -9,80 +8,35 @@ public static class ServiceInjectionHelper
{
public static IServiceCollection AddRingService(this IServiceCollection services)
{
services.AddSingleton<RingService.RingServiceClient>(sp =>
services.AddGrpcClient<RingService.RingServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateRingServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
});
o.Address = new Uri("https://ring");
});
return services;
}
public static IServiceCollection AddAccountService(this IServiceCollection services)
{
services.AddSingleton<AccountService.AccountServiceClient>(sp =>
services.AddGrpcClient<AccountService.AccountServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateAccountServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://pass");
});
services.AddSingleton<AccountClientHelper>();
services.AddSingleton<BotAccountReceiverService.BotAccountReceiverServiceClient>(sp =>
services.AddGrpcClient<BotAccountReceiverService.BotAccountReceiverServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateBotAccountReceiverServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://pass");
});
services.AddSingleton<ActionLogService.ActionLogServiceClient>(sp =>
services.AddGrpcClient<ActionLogService.ActionLogServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateActionLogServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://pass");
});
services.AddSingleton<PaymentService.PaymentServiceClient>(sp =>
services.AddGrpcClient<PaymentService.PaymentServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreatePaymentServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://pass");
});
return services;
@@ -90,32 +44,14 @@ public static class ServiceInjectionHelper
public static IServiceCollection AddDriveService(this IServiceCollection services)
{
services.AddSingleton<FileService.FileServiceClient>(sp =>
services.AddGrpcClient<FileService.FileServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateFileServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://drive");
});
services.AddSingleton<FileReferenceService.FileReferenceServiceClient>(sp =>
services.AddGrpcClient<FileReferenceService.FileReferenceServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateFileReferenceServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://drive");
});
return services;
@@ -123,18 +59,9 @@ public static class ServiceInjectionHelper
public static IServiceCollection AddPublisherService(this IServiceCollection services)
{
services.AddSingleton<PublisherService.PublisherServiceClient>(sp =>
services.AddGrpcClient<PublisherService.PublisherServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreatePublisherServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://sphere");
});
return services;
@@ -142,18 +69,9 @@ public static class ServiceInjectionHelper
public static IServiceCollection AddDevelopService(this IServiceCollection services)
{
services.AddSingleton<CustomAppService.CustomAppServiceClient>(sp =>
services.AddGrpcClient<CustomAppService.CustomAppServiceClient>(o =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"]!;
var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateCustomAppServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
o.Address = new Uri("https://develop");
});
return services;

View File

@@ -1,55 +0,0 @@
using System.Text;
using dotnet_etcd.interfaces;
using Etcdserverpb;
using Google.Protobuf;
using Microsoft.Extensions.Logging;
namespace DysonNetwork.Shared.Registry;
public class ServiceRegistry(IEtcdClient etcd, ILogger<ServiceRegistry> logger)
{
public async Task RegisterService(
string serviceName,
string serviceUrl,
long leaseTtlSeconds = 60,
CancellationToken cancellationToken = default
)
{
var key = $"/services/{serviceName}";
var leaseResponse = await etcd.LeaseGrantAsync(
new LeaseGrantRequest { TTL = leaseTtlSeconds },
cancellationToken: cancellationToken
);
await etcd.PutAsync(new PutRequest
{
Key = ByteString.CopyFrom(key, Encoding.UTF8),
Value = ByteString.CopyFrom(serviceUrl, Encoding.UTF8),
Lease = leaseResponse.ID
}, cancellationToken: cancellationToken);
_ = Task.Run(async () =>
{
try
{
await etcd.LeaseKeepAlive(leaseResponse.ID, cancellationToken);
}
catch (Exception ex)
{
logger.LogError($"Lease keep-alive failed: {ex.Message}");
}
}, cancellationToken);
}
public async Task UnregisterService(string serviceName)
{
var key = $"/services/{serviceName}";
await etcd.DeleteAsync(key);
}
public async Task<string?> GetServiceUrl(string serviceName)
{
var key = $"/services/{serviceName}";
var response = await etcd.GetAsync(key);
return response.Kvs.Count == 0 ? null : response.Kvs[0].Value.ToStringUtf8();
}
}

View File

@@ -1,28 +0,0 @@
using dotnet_etcd.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DysonNetwork.Shared.Registry;
public static class RegistryStartup
{
public static IServiceCollection AddRegistryService(
this IServiceCollection services,
IConfiguration configuration,
bool addForwarder = true
)
{
services.AddEtcdClient(options =>
{
options.ConnectionString = configuration.GetConnectionString("Etcd");
options.UseInsecureChannel = configuration.GetValue<bool>("Etcd:Insecure");
});
services.AddSingleton<ServiceRegistry>();
services.AddHostedService<RegistryHostedService>();
if (addForwarder)
services.AddHttpForwarder();
return services;
}
}

View File

@@ -1,21 +0,0 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NATS.Client.Core;
namespace DysonNetwork.Shared.Stream;
public static class Connector
{
public static IServiceCollection AddStreamConnection(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("Stream");
if (connectionString is null)
throw new ArgumentNullException(nameof(connectionString));
services.AddSingleton<INatsConnection>(_ => new NatsConnection(new NatsOpts()
{
Url = connectionString
}));
return services;
}
}