diff --git a/Directory.Build.props b/Directory.Build.props index cd12396..808f39e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,7 +26,12 @@ type, and graph-level HttpGraphUsage.MiddlewareTypes — the same operator-facing pipeline information is available on demand via the chain's generated source code, so the descriptor copies were redundant payload (~30-50 KB compressed per ~163-endpoint topology). --> - 2.6.1 + + 2.7.0 13 1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618 Jeremy D. Miller;Jaedyn Tonee diff --git a/src/CoreTests/MultiTenancy/dynamic_tenancy_admin_surface.cs b/src/CoreTests/MultiTenancy/dynamic_tenancy_admin_surface.cs index 33f972a..a0a583c 100644 --- a/src/CoreTests/MultiTenancy/dynamic_tenancy_admin_surface.cs +++ b/src/CoreTests/MultiTenancy/dynamic_tenancy_admin_surface.cs @@ -20,8 +20,9 @@ public async Task auto_assign_overload_throws_by_default() public async Task auto_assign_overload_is_used_when_a_source_supports_it() { var source = new AutoAssignTenantSource(); - await ((IDynamicTenantSource)source).AddTenantAsync("acme"); + var resolved = await ((IDynamicTenantSource)source).AddTenantAsync("acme"); source.AutoAssigned.ShouldBe(["acme"]); + resolved.ShouldBe("shard-acme"); // resolved database id / partition suffix } [Fact] @@ -41,9 +42,10 @@ public async Task auto_assign_add_dispatches_to_registered_sources() var source = new AutoAssignTenantSource(); var services = provider(source); - await services.AddTenantAsync("acme"); + var resolved = await services.AddTenantAsync("acme"); source.AutoAssigned.ShouldBe(["acme"]); + resolved.ShouldBe("shard-acme"); } [Fact] @@ -87,7 +89,7 @@ public async Task is_a_graceful_no_op_when_no_dynamic_source_is_registered() // None of these should throw with no source registered await services.AddTenantAsync("acme", "Host=db1"); - await services.AddTenantAsync("acme"); + (await services.AddTenantAsync("acme")).ShouldBeNull(); // no source -> no resolved assignment await services.DisableTenantAsync("acme"); await services.EnableTenantAsync("acme"); await services.RemoveTenantAsync("acme"); @@ -167,10 +169,11 @@ internal class AutoAssignTenantSource : ValueOnlyTenantSource, IDynamicTenantSou { public List AutoAssigned { get; } = new(); - public Task AddTenantAsync(string tenantId, CancellationToken token = default) + public Task AddTenantAsync(string tenantId, CancellationToken token = default) { AutoAssigned.Add(tenantId); - return Task.CompletedTask; + // Simulate the assignment strategy resolving the tenant onto a shard / partition. + return Task.FromResult($"shard-{tenantId}"); } } diff --git a/src/JasperFx/MultiTenancy/DynamicTenancyAdminExtensions.cs b/src/JasperFx/MultiTenancy/DynamicTenancyAdminExtensions.cs index a2ad8e2..a3449a3 100644 --- a/src/JasperFx/MultiTenancy/DynamicTenancyAdminExtensions.cs +++ b/src/JasperFx/MultiTenancy/DynamicTenancyAdminExtensions.cs @@ -32,11 +32,22 @@ public static Task AddTenantAsync(this IServiceProvider services, string tenantI /// Add a tenant whose connection/partition is auto-assigned by the source (sharded/partitioned style; /// no caller-supplied value). Dispatched to every registered dynamic tenant source — a source that /// requires a connection value will surface from its default - /// . + /// . Returns the + /// resolved assignment (database id / partition suffix) from the first source that provisioned the + /// tenant, or when no dynamic source is registered. /// - public static Task AddTenantAsync(this IServiceProvider services, string tenantId, + public static async Task AddTenantAsync(this IServiceProvider services, string tenantId, CancellationToken token = default) - => forEachSource(services, source => source.AddTenantAsync(tenantId, token)); + { + string? resolved = null; + foreach (var source in services.DynamicTenantSources()) + { + var assignment = await source.AddTenantAsync(tenantId, token).ConfigureAwait(false); + resolved ??= assignment; + } + + return resolved; + } /// /// Disable (soft delete) a tenant across every registered dynamic tenant source. @@ -91,7 +102,7 @@ public static Task AddTenantAsync(this IHost host, string tenantId, string conne => host.Services.AddTenantAsync(tenantId, connectionValue); /// - public static Task AddTenantAsync(this IHost host, string tenantId, CancellationToken token = default) + public static Task AddTenantAsync(this IHost host, string tenantId, CancellationToken token = default) => host.Services.AddTenantAsync(tenantId, token); /// diff --git a/src/JasperFx/MultiTenancy/IDynamicTenantSource.cs b/src/JasperFx/MultiTenancy/IDynamicTenantSource.cs index feaf735..acaff5d 100644 --- a/src/JasperFx/MultiTenancy/IDynamicTenantSource.cs +++ b/src/JasperFx/MultiTenancy/IDynamicTenantSource.cs @@ -14,15 +14,16 @@ public interface IDynamicTenantSource : ITenantedSource /// /// Provision a new tenant whose connection/partition is auto-assigned by the source's configured /// strategy — e.g. sharded-database pooling or per-tenant event partitions — rather than a - /// caller-supplied connection value. The default implementation throws - /// ; sources that auto-assign (e.g. Marten's sharded tenancy) - /// override it. This is the uniform "add a tenant" entrypoint for provisioning models that own the - /// physical assignment, so a tool such as CritterWatch never has to sniff the concrete tenancy type. - /// See jasperfx#409. + /// caller-supplied connection value. Returns the resolved assignment: the database id (sharded pool) + /// or partition suffix (managed partitions) the tenant landed on, so the caller (e.g. CritterWatch) + /// learns where it was placed without sniffing the concrete tenancy type. The default implementation + /// throws ; auto-assign sources (e.g. Marten's sharded tenancy) + /// override it, while caller-supplies-value sources keep + /// . See jasperfx#413 (split from #409). /// - Task AddTenantAsync(string tenantId, CancellationToken token = default) + Task AddTenantAsync(string tenantId, CancellationToken token = default) => throw new NotSupportedException( - $"This tenant source ({GetType().FullName}) requires a caller-supplied connection value; call AddTenantAsync(tenantId, connectionValue) instead, or use a source that supports auto-assignment."); + $"This tenant source ({GetType().FullName}) does not support auto-assignment; call AddTenantAsync(tenantId, connectionValue) with a caller-supplied connection value instead."); /// /// Disable a tenant (soft delete). The tenant data is preserved but