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