Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add WithDelegateStratey<TContext, TTenantInfo> #932

Merged
merged 2 commits into from
Jan 28, 2025
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
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
Loading