Skip to content
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
7 changes: 6 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -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). -->
<JasperFxVersion>2.6.1</JasperFxVersion>
<!-- 2.7.0: auto-assign dynamic-tenant abstraction (#413, split from #409). The auto-assign
IDynamicTenantSource<T>.AddTenantAsync(tenantId, CancellationToken) overload now returns
Task<string> — the resolved database id / partition suffix — so pooled/sharded tenancy models
(Marten ShardedTenancy, marten#4598) report where an auto-assigned tenant landed. Default still
throws NotSupportedException (additive). DynamicTenancyAdminExtensions surfaces the resolved id. -->
<JasperFxVersion>2.7.0</JasperFxVersion>
<LangVersion>13</LangVersion>
<NoWarn>1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618</NoWarn>
<Authors>Jeremy D. Miller;Jaedyn Tonee</Authors>
Expand Down
13 changes: 8 additions & 5 deletions src/CoreTests/MultiTenancy/dynamic_tenancy_admin_surface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>)source).AddTenantAsync("acme");
var resolved = await ((IDynamicTenantSource<string>)source).AddTenantAsync("acme");
source.AutoAssigned.ShouldBe(["acme"]);
resolved.ShouldBe("shard-acme"); // resolved database id / partition suffix
}

[Fact]
Expand All @@ -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]
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -167,10 +169,11 @@ internal class AutoAssignTenantSource : ValueOnlyTenantSource, IDynamicTenantSou
{
public List<string> AutoAssigned { get; } = new();

public Task AddTenantAsync(string tenantId, CancellationToken token = default)
public Task<string> 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}");
}
}

Expand Down
19 changes: 15 additions & 4 deletions src/JasperFx/MultiTenancy/DynamicTenancyAdminExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see cref="NotSupportedException" /> from its default
/// <see cref="IDynamicTenantSource{T}.AddTenantAsync(string,CancellationToken)" />.
/// <see cref="IDynamicTenantSource{T}.AddTenantAsync(string,CancellationToken)" />. Returns the
/// resolved assignment (database id / partition suffix) from the first source that provisioned the
/// tenant, or <see langword="null" /> when no dynamic source is registered.
/// </summary>
public static Task AddTenantAsync(this IServiceProvider services, string tenantId,
public static async Task<string?> 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;
}

/// <summary>
/// Disable (soft delete) a tenant across every registered dynamic tenant source.
Expand Down Expand Up @@ -91,7 +102,7 @@ public static Task AddTenantAsync(this IHost host, string tenantId, string conne
=> host.Services.AddTenantAsync(tenantId, connectionValue);

/// <summary><inheritdoc cref="AddTenantAsync(IServiceProvider,string,CancellationToken)" /></summary>
public static Task AddTenantAsync(this IHost host, string tenantId, CancellationToken token = default)
public static Task<string?> AddTenantAsync(this IHost host, string tenantId, CancellationToken token = default)
=> host.Services.AddTenantAsync(tenantId, token);

/// <summary><inheritdoc cref="DisableTenantAsync(IServiceProvider,string)" /></summary>
Expand Down
15 changes: 8 additions & 7 deletions src/JasperFx/MultiTenancy/IDynamicTenantSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ public interface IDynamicTenantSource<T> : ITenantedSource<T>
/// <summary>
/// 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
/// <see cref="NotSupportedException" />; 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 <see cref="NotSupportedException" />; auto-assign sources (e.g. Marten's sharded tenancy)
/// override it, while caller-supplies-value sources keep
/// <see cref="AddTenantAsync(string,T)" />. See jasperfx#413 (split from #409).
/// </summary>
Task AddTenantAsync(string tenantId, CancellationToken token = default)
Task<string> 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.");

/// <summary>
/// Disable a tenant (soft delete). The tenant data is preserved but
Expand Down