Skip to content

Commit

Permalink
feat: add WithDelegateStrategy<TContext, TTenantInfo> (#932)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewTriesToCode authored Jan 28, 2025
1 parent 9e84fa6 commit a18a935
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 13 deletions.
3 changes: 2 additions & 1 deletion docs/ConfigurationAndUsage.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ in the order registered. See [MultiTenant Strategies](Strategies) for more infor
- `WithBasePathStrategy`
- `WithClaimStrategy`
- `WithDelegateStrategy`
- `WithHeaderStrateg`y
- `WithDelegateStrategy<TContext, TTenantInfo>`
- `WithHeaderStrategy`
- `WithHostStrategy`
- `WithRouteStrategy`
- `WithSessionStrategy`
Expand Down
16 changes: 7 additions & 9 deletions docs/Strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ This strategy is good to use for testing or simple logic. This strategy is confi
order configured.

Configure by calling `WithDelegateStrategy` after `AddMultiTenant<TTenantInfo>` A `Func<object, Task<string?>>`is passed
in which will be used with each request to resolve the tenant. A lambda or async lambda can be used as the parameter:
in which will be used with each request to resolve the tenant. A lambda or async lambda can be used as the parameter.
Alternatively, `WithDelegateStrategy<TContext, TTenantInfo>` accepts a typed context parameter. Tenant resolution will
ignore this strategy if the context is not of the correct type:

```csharp
// use async logic to get the tenant identifier
Expand All @@ -75,15 +77,11 @@ builder.Services.AddMultiTenant<TenantInfo>()
string? tenantIdentifier = await DoSomethingAsync(context);
return tenantIdentifier
})...
// or do it without async

// or register with a typed lambda, HttpContext in this case
builder.Services.AddMultiTenant<TenantInfo>()
.WithDelegateStrategy(context =>
{
var httpContext = context as HttpContext;
if (httpContext == null)
return null;

.WithDelegateStrategy<HttpContext, TenantInfo>(httpContext =>
{
httpContext.Request.Query.TryGetValue("tenant", out StringValues tenantIdentifier);

if (tenantIdentifier is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ namespace Finbuckle.MultiTenant;
public static class MultiTenantBuilderExtensions
{
/// <summary>
/// Adds a DistributedCacheStore to the application.
/// Adds a DistributedCacheStore to the application with maximum sliding expiration.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithDistributedCacheStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder)
where TTenantInfo : class, ITenantInfo, new()
=> builder.WithDistributedCacheStore(TimeSpan.MaxValue);
Expand All @@ -32,8 +34,10 @@ public static MultiTenantBuilder<TTenantInfo> WithDistributedCacheStore<TTenantI
/// <summary>
/// Adds a DistributedCacheStore to the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="slidingExpiration">The timespan for a cache entry's sliding expiration.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithDistributedCacheStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder, TimeSpan? slidingExpiration)
where TTenantInfo : class, ITenantInfo, new()
{
Expand All @@ -45,18 +49,22 @@ public static MultiTenantBuilder<TTenantInfo> WithDistributedCacheStore<TTenantI
/// <summary>
/// Adds a HttpRemoteStore to the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="endpointTemplate">The endpoint URI template.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithHttpRemoteStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder, string endpointTemplate)
where TTenantInfo : class, ITenantInfo, new()
=> builder.WithHttpRemoteStore(endpointTemplate, null);

/// <summary>
/// Adds a HttpRemoteStore to the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="endpointTemplate">The endpoint URI template.</param>
/// <param name="clientConfig">An action to configure the underlying HttpClient.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithHttpRemoteStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
string endpointTemplate,
Action<IHttpClientBuilder>? clientConfig) where TTenantInfo : class, ITenantInfo, new()
Expand All @@ -72,17 +80,21 @@ public static MultiTenantBuilder<TTenantInfo> WithHttpRemoteStore<TTenantInfo>(t
/// <summary>
/// Adds a ConfigurationStore to the application. Uses the default IConfiguration and section "Finbuckle:MultiTenant:Stores:ConfigurationStore".
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithConfigurationStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder)
where TTenantInfo : class, ITenantInfo, new()
=> builder.WithStore<ConfigurationStore<TTenantInfo>>(ServiceLifetime.Singleton);

/// <summary>
/// Adds a ConfigurationStore to the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="configuration">The IConfiguration to load the section from.</param>
/// <param name="sectionName">The configuration section to load.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithConfigurationStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
IConfiguration configuration,
string sectionName)
Expand All @@ -92,17 +104,20 @@ public static MultiTenantBuilder<TTenantInfo> WithConfigurationStore<TTenantInfo
/// <summary>
/// Adds an empty InMemoryStore to the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithInMemoryStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder)
where TTenantInfo : class, ITenantInfo, new()
// ReSharper disable once RedundantTypeArgumentsOfMethod
=> builder.WithInMemoryStore<TTenantInfo>(_ => {});
=> builder.WithInMemoryStore(_ => {});

/// <summary>
/// Adds and configures InMemoryStore to the application using the provided action.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="config">An action for configuring the store.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithInMemoryStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
Action<InMemoryStoreOptions<TTenantInfo>> config)
where TTenantInfo : class, ITenantInfo, new()
Expand All @@ -121,16 +136,20 @@ public static MultiTenantBuilder<TTenantInfo> WithInMemoryStore<TTenantInfo>(thi
/// <summary>
/// Adds an EchoStore to the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithEchoStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder)
where TTenantInfo : class, ITenantInfo, new()
=> builder.WithStore<EchoStore<TTenantInfo>>(ServiceLifetime.Singleton);

/// <summary>
/// Adds and configures a StaticStrategy to the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="identifier">The tenant identifier to use for all tenant resolution.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithStaticStrategy<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
string identifier)
where TTenantInfo : class, ITenantInfo, new()
Expand All @@ -146,8 +165,10 @@ public static MultiTenantBuilder<TTenantInfo> WithStaticStrategy<TTenantInfo>(th
/// <summary>
/// Adds and configures a DelegateStrategy to the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="doStrategy">The delegate implementing the strategy.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithDelegateStrategy<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
Func<object, Task<string?>> doStrategy)
where TTenantInfo : class, ITenantInfo, new()
Expand All @@ -159,4 +180,31 @@ public static MultiTenantBuilder<TTenantInfo> WithDelegateStrategy<TTenantInfo>(

return builder.WithStrategy<DelegateStrategy>(ServiceLifetime.Singleton, new object[] { doStrategy });
}

/// <summary>
/// Adds and configures a typed DelegateStrategy&lt;TContext&gt; to the application.
/// </summary>
/// <typeparam name="TContext">The strategy context type.</typeparam>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder"></param>
/// <param name="doStrategy"></param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> WithDelegateStrategy<TContext, TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
Func<TContext, Task<string?>> doStrategy)
where TTenantInfo : class, ITenantInfo, new()
{
ArgumentNullException.ThrowIfNull(doStrategy, nameof(doStrategy));

Func<object, Task<string?>> wrapStrategy = context =>
{
if (context.GetType() == typeof(TContext))
{
return doStrategy((TContext)context);
}

return Task.FromResult<string?>(null);
};

return builder.WithDelegateStrategy(wrapStrategy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
// ReSharper disable once CheckNamespace
namespace Finbuckle.MultiTenant;

// TODO XML comments
public static class OptionsBuilderExtensions
{
public static OptionsBuilder<TOptions> ConfigurePerTenant<TOptions, TTenantInfo>(
this OptionsBuilder<TOptions> optionsBuilder, Action<TOptions, TTenantInfo> configureOptions)
where TOptions : class
where TTenantInfo : class, ITenantInfo, new()
{
// TODO use ThrowNull here?
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));

FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs<TOptions>(optionsBuilder.Services);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static class FinbuckleServiceCollectionExtensions
/// <summary>
/// Configure Finbuckle.MultiTenant services for the application.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="services">The <c>IServiceCollection</c> instance the extension method applies to.</param>
/// <param name="config">An action to configure the MultiTenantOptions instance.</param>
/// <returns>A new instance of MultiTenantBuilder.</returns>
Expand Down
2 changes: 2 additions & 0 deletions src/Finbuckle.MultiTenant/MultiTenantBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

namespace Finbuckle.MultiTenant;

// TODO: factor TTenantInfo into WithStore only

/// <summary>
/// Builder class for Finbuckle.MultiTenant configuration.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,31 @@ public void AddDelegateStrategy()
var strategy = sp.GetRequiredService<IMultiTenantStrategy>();
Assert.IsType<DelegateStrategy>(strategy);
}

[Fact]
public void AddTypedDelegateStrategy()
{
var services = new ServiceCollection();
var builder = new MultiTenantBuilder<TenantInfo>(services);
builder.WithDelegateStrategy<int, TenantInfo>(context => Task.FromResult(context.ToString())!);
var sp = services.BuildServiceProvider();

var strategy = sp.GetRequiredService<IMultiTenantStrategy>();
Assert.IsType<DelegateStrategy>(strategy);
}

[Fact]
public async Task ReturnNullForWrongTypeSendToTypedDelegateStrategy()
{
var services = new ServiceCollection();
var builder = new MultiTenantBuilder<TenantInfo>(services);
builder.WithDelegateStrategy<int, TenantInfo>(context => Task.FromResult("Shouldn't ever get here")!);
var sp = services.BuildServiceProvider();

var strategy = sp.GetRequiredService<IMultiTenantStrategy>();
var identifier = await strategy.GetIdentifierAsync(new object());
Assert.Null(identifier);
}

[Fact]
public void AddStaticStrategy()
Expand Down

0 comments on commit a18a935

Please sign in to comment.