feat(localdb): gRPC sync adapters, background service, replication DI

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
Joseph Doherty
2026-07-17 23:01:29 -04:00
parent 183901e8ac
commit a64fc26391
7 changed files with 701 additions and 0 deletions
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace ZB.MOM.WW.LocalDb.Replication;
/// <summary>DI + endpoint wiring for the 2-node gRPC replication engine.</summary>
public static class ReplicationServiceCollectionExtensions
{
/// <summary>
/// Binds and validates <see cref="ReplicationOptions"/> from the <c>"LocalDb:Replication"</c>
/// configuration section, registers the passive <see cref="LocalDbSyncService"/> as a singleton
/// (its single-stream guard must span calls), and registers the initiating
/// <see cref="SyncBackgroundService"/> as a hosted service ONLY when a peer address is configured
/// (a passive node leaves it empty and never dials). Idempotent. Requires
/// <see cref="LocalDbServiceCollectionExtensions.AddZbLocalDb"/> to have been called for the
/// <see cref="ILocalDb"/> the adapters depend on.
/// </summary>
public static IServiceCollection AddZbLocalDbReplication(
this IServiceCollection services, IConfiguration configuration)
{
// Idempotent: a second call must not re-bind options or double-register the service/host.
if (services.Any(d => d.ServiceType == typeof(LocalDbSyncService)))
return services;
services.AddOptions<ReplicationOptions>()
.Bind(configuration.GetSection("LocalDb:Replication"))
.ValidateOnStart();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IValidateOptions<ReplicationOptions>, ReplicationOptionsValidator>());
services.AddSingleton<LocalDbSyncService>();
// Read the section directly (before options are built) to decide whether this node initiates.
var peerAddress = configuration.GetSection("LocalDb:Replication")["PeerAddress"];
if (!string.IsNullOrEmpty(peerAddress))
services.AddHostedService<SyncBackgroundService>();
return services;
}
/// <summary>
/// Maps the passive <see cref="LocalDbSyncService"/> gRPC endpoint. The host must have called
/// <c>AddGrpc()</c> and set up routing.
/// </summary>
public static IEndpointRouteBuilder MapZbLocalDbSync(this IEndpointRouteBuilder endpoints)
{
endpoints.MapGrpcService<LocalDbSyncService>();
return endpoints;
}
}