Skip to content

Commit a18a935

Browse files
authoredJan 28, 2025··
feat: add WithDelegateStrategy<TContext, TTenantInfo> (#932)
1 parent 9e84fa6 commit a18a935

File tree

7 files changed

+90
-13
lines changed

7 files changed

+90
-13
lines changed
 

‎docs/ConfigurationAndUsage.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ in the order registered. See [MultiTenant Strategies](Strategies) for more infor
5858
- `WithBasePathStrategy`
5959
- `WithClaimStrategy`
6060
- `WithDelegateStrategy`
61-
- `WithHeaderStrateg`y
61+
- `WithDelegateStrategy<TContext, TTenantInfo>`
62+
- `WithHeaderStrategy`
6263
- `WithHostStrategy`
6364
- `WithRouteStrategy`
6465
- `WithSessionStrategy`

‎docs/Strategies.md

+7-9
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ This strategy is good to use for testing or simple logic. This strategy is confi
6565
order configured.
6666

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

7072
```csharp
7173
// use async logic to get the tenant identifier
@@ -75,15 +77,11 @@ builder.Services.AddMultiTenant<TenantInfo>()
7577
string? tenantIdentifier = await DoSomethingAsync(context);
7678
return tenantIdentifier
7779
})...
78-
79-
// or do it without async
80+
81+
// or register with a typed lambda, HttpContext in this case
8082
builder.Services.AddMultiTenant<TenantInfo>()
81-
.WithDelegateStrategy(context =>
82-
{
83-
var httpContext = context as HttpContext;
84-
if (httpContext == null)
85-
return null;
86-
83+
.WithDelegateStrategy<HttpContext, TenantInfo>(httpContext =>
84+
{
8785
httpContext.Request.Query.TryGetValue("tenant", out StringValues tenantIdentifier);
8886

8987
if (tenantIdentifier is null)

‎src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs

+51-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ namespace Finbuckle.MultiTenant;
2222
public static class MultiTenantBuilderExtensions
2323
{
2424
/// <summary>
25-
/// Adds a DistributedCacheStore to the application.
25+
/// Adds a DistributedCacheStore to the application with maximum sliding expiration.
2626
/// </summary>
27+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
28+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
2729
public static MultiTenantBuilder<TTenantInfo> WithDistributedCacheStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder)
2830
where TTenantInfo : class, ITenantInfo, new()
2931
=> builder.WithDistributedCacheStore(TimeSpan.MaxValue);
@@ -32,8 +34,10 @@ public static MultiTenantBuilder<TTenantInfo> WithDistributedCacheStore<TTenantI
3234
/// <summary>
3335
/// Adds a DistributedCacheStore to the application.
3436
/// </summary>
37+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
3538
/// <param name="builder">The builder instance.</param>
3639
/// <param name="slidingExpiration">The timespan for a cache entry's sliding expiration.</param>
40+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
3741
public static MultiTenantBuilder<TTenantInfo> WithDistributedCacheStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder, TimeSpan? slidingExpiration)
3842
where TTenantInfo : class, ITenantInfo, new()
3943
{
@@ -45,18 +49,22 @@ public static MultiTenantBuilder<TTenantInfo> WithDistributedCacheStore<TTenantI
4549
/// <summary>
4650
/// Adds a HttpRemoteStore to the application.
4751
/// </summary>
52+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
4853
/// <param name="builder">The builder instance.</param>
4954
/// <param name="endpointTemplate">The endpoint URI template.</param>
55+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
5056
public static MultiTenantBuilder<TTenantInfo> WithHttpRemoteStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder, string endpointTemplate)
5157
where TTenantInfo : class, ITenantInfo, new()
5258
=> builder.WithHttpRemoteStore(endpointTemplate, null);
5359

5460
/// <summary>
5561
/// Adds a HttpRemoteStore to the application.
5662
/// </summary>
63+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
5764
/// <param name="builder">The builder instance.</param>
5865
/// <param name="endpointTemplate">The endpoint URI template.</param>
5966
/// <param name="clientConfig">An action to configure the underlying HttpClient.</param>
67+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
6068
public static MultiTenantBuilder<TTenantInfo> WithHttpRemoteStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
6169
string endpointTemplate,
6270
Action<IHttpClientBuilder>? clientConfig) where TTenantInfo : class, ITenantInfo, new()
@@ -72,17 +80,21 @@ public static MultiTenantBuilder<TTenantInfo> WithHttpRemoteStore<TTenantInfo>(t
7280
/// <summary>
7381
/// Adds a ConfigurationStore to the application. Uses the default IConfiguration and section "Finbuckle:MultiTenant:Stores:ConfigurationStore".
7482
/// </summary>
83+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
7584
/// <param name="builder">The builder instance.</param>
85+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
7686
public static MultiTenantBuilder<TTenantInfo> WithConfigurationStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder)
7787
where TTenantInfo : class, ITenantInfo, new()
7888
=> builder.WithStore<ConfigurationStore<TTenantInfo>>(ServiceLifetime.Singleton);
7989

8090
/// <summary>
8191
/// Adds a ConfigurationStore to the application.
8292
/// </summary>
93+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
8394
/// <param name="builder">The builder instance.</param>
8495
/// <param name="configuration">The IConfiguration to load the section from.</param>
8596
/// <param name="sectionName">The configuration section to load.</param>
97+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
8698
public static MultiTenantBuilder<TTenantInfo> WithConfigurationStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
8799
IConfiguration configuration,
88100
string sectionName)
@@ -92,17 +104,20 @@ public static MultiTenantBuilder<TTenantInfo> WithConfigurationStore<TTenantInfo
92104
/// <summary>
93105
/// Adds an empty InMemoryStore to the application.
94106
/// </summary>
107+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
95108
/// <param name="builder">The builder instance.</param>
109+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
96110
public static MultiTenantBuilder<TTenantInfo> WithInMemoryStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder)
97111
where TTenantInfo : class, ITenantInfo, new()
98-
// ReSharper disable once RedundantTypeArgumentsOfMethod
99-
=> builder.WithInMemoryStore<TTenantInfo>(_ => {});
112+
=> builder.WithInMemoryStore(_ => {});
100113

101114
/// <summary>
102115
/// Adds and configures InMemoryStore to the application using the provided action.
103116
/// </summary>
117+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
104118
/// <param name="builder">The builder instance.</param>
105119
/// <param name="config">An action for configuring the store.</param>
120+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
106121
public static MultiTenantBuilder<TTenantInfo> WithInMemoryStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
107122
Action<InMemoryStoreOptions<TTenantInfo>> config)
108123
where TTenantInfo : class, ITenantInfo, new()
@@ -121,16 +136,20 @@ public static MultiTenantBuilder<TTenantInfo> WithInMemoryStore<TTenantInfo>(thi
121136
/// <summary>
122137
/// Adds an EchoStore to the application.
123138
/// </summary>
139+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
124140
/// <param name="builder">The builder instance.</param>
141+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
125142
public static MultiTenantBuilder<TTenantInfo> WithEchoStore<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder)
126143
where TTenantInfo : class, ITenantInfo, new()
127144
=> builder.WithStore<EchoStore<TTenantInfo>>(ServiceLifetime.Singleton);
128145

129146
/// <summary>
130147
/// Adds and configures a StaticStrategy to the application.
131148
/// </summary>
149+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
132150
/// <param name="builder">The builder instance.</param>
133151
/// <param name="identifier">The tenant identifier to use for all tenant resolution.</param>
152+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
134153
public static MultiTenantBuilder<TTenantInfo> WithStaticStrategy<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
135154
string identifier)
136155
where TTenantInfo : class, ITenantInfo, new()
@@ -146,8 +165,10 @@ public static MultiTenantBuilder<TTenantInfo> WithStaticStrategy<TTenantInfo>(th
146165
/// <summary>
147166
/// Adds and configures a DelegateStrategy to the application.
148167
/// </summary>
168+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
149169
/// <param name="builder">The builder instance.</param>
150170
/// <param name="doStrategy">The delegate implementing the strategy.</param>
171+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
151172
public static MultiTenantBuilder<TTenantInfo> WithDelegateStrategy<TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
152173
Func<object, Task<string?>> doStrategy)
153174
where TTenantInfo : class, ITenantInfo, new()
@@ -159,4 +180,31 @@ public static MultiTenantBuilder<TTenantInfo> WithDelegateStrategy<TTenantInfo>(
159180

160181
return builder.WithStrategy<DelegateStrategy>(ServiceLifetime.Singleton, new object[] { doStrategy });
161182
}
183+
184+
/// <summary>
185+
/// Adds and configures a typed DelegateStrategy&lt;TContext&gt; to the application.
186+
/// </summary>
187+
/// <typeparam name="TContext">The strategy context type.</typeparam>
188+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
189+
/// <param name="builder"></param>
190+
/// <param name="doStrategy"></param>
191+
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
192+
public static MultiTenantBuilder<TTenantInfo> WithDelegateStrategy<TContext, TTenantInfo>(this MultiTenantBuilder<TTenantInfo> builder,
193+
Func<TContext, Task<string?>> doStrategy)
194+
where TTenantInfo : class, ITenantInfo, new()
195+
{
196+
ArgumentNullException.ThrowIfNull(doStrategy, nameof(doStrategy));
197+
198+
Func<object, Task<string?>> wrapStrategy = context =>
199+
{
200+
if (context.GetType() == typeof(TContext))
201+
{
202+
return doStrategy((TContext)context);
203+
}
204+
205+
return Task.FromResult<string?>(null);
206+
};
207+
208+
return builder.WithDelegateStrategy(wrapStrategy);
209+
}
162210
}

‎src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
// ReSharper disable once CheckNamespace
1212
namespace Finbuckle.MultiTenant;
1313

14+
// TODO XML comments
1415
public static class OptionsBuilderExtensions
1516
{
1617
public static OptionsBuilder<TOptions> ConfigurePerTenant<TOptions, TTenantInfo>(
1718
this OptionsBuilder<TOptions> optionsBuilder, Action<TOptions, TTenantInfo> configureOptions)
1819
where TOptions : class
1920
where TTenantInfo : class, ITenantInfo, new()
2021
{
22+
// TODO use ThrowNull here?
2123
if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
2224

2325
FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs<TOptions>(optionsBuilder.Services);

‎src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public static class FinbuckleServiceCollectionExtensions
2121
/// <summary>
2222
/// Configure Finbuckle.MultiTenant services for the application.
2323
/// </summary>
24+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
2425
/// <param name="services">The <c>IServiceCollection</c> instance the extension method applies to.</param>
2526
/// <param name="config">An action to configure the MultiTenantOptions instance.</param>
2627
/// <returns>A new instance of MultiTenantBuilder.</returns>

‎src/Finbuckle.MultiTenant/MultiTenantBuilder.cs

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
namespace Finbuckle.MultiTenant;
88

9+
// TODO: factor TTenantInfo into WithStore only
10+
911
/// <summary>
1012
/// Builder class for Finbuckle.MultiTenant configuration.
1113
/// </summary>

‎test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderExtensionsShould.cs

+25
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,31 @@ public void AddDelegateStrategy()
189189
var strategy = sp.GetRequiredService<IMultiTenantStrategy>();
190190
Assert.IsType<DelegateStrategy>(strategy);
191191
}
192+
193+
[Fact]
194+
public void AddTypedDelegateStrategy()
195+
{
196+
var services = new ServiceCollection();
197+
var builder = new MultiTenantBuilder<TenantInfo>(services);
198+
builder.WithDelegateStrategy<int, TenantInfo>(context => Task.FromResult(context.ToString())!);
199+
var sp = services.BuildServiceProvider();
200+
201+
var strategy = sp.GetRequiredService<IMultiTenantStrategy>();
202+
Assert.IsType<DelegateStrategy>(strategy);
203+
}
204+
205+
[Fact]
206+
public async Task ReturnNullForWrongTypeSendToTypedDelegateStrategy()
207+
{
208+
var services = new ServiceCollection();
209+
var builder = new MultiTenantBuilder<TenantInfo>(services);
210+
builder.WithDelegateStrategy<int, TenantInfo>(context => Task.FromResult("Shouldn't ever get here")!);
211+
var sp = services.BuildServiceProvider();
212+
213+
var strategy = sp.GetRequiredService<IMultiTenantStrategy>();
214+
var identifier = await strategy.GetIdentifierAsync(new object());
215+
Assert.Null(identifier);
216+
}
192217

193218
[Fact]
194219
public void AddStaticStrategy()

0 commit comments

Comments
 (0)
Please sign in to comment.