From 044ed02d904666df56cd206fe9b3c36fb991fa74 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Mon, 27 Jan 2025 21:44:19 -0700 Subject: [PATCH 1/2] feat: add WithDelegateStratey --- docs/ConfigurationAndUsage.md | 3 +- docs/Strategies.md | 16 +++--- .../MultiTenantBuilderExtensions.cs | 54 +++++++++++++++++-- .../OptionsBuilderExtensions.cs | 2 + .../ServiceCollectionExtensions.cs | 1 + .../MultiTenantBuilder.cs | 2 + .../MultiTenantBuilderExtensionsShould.cs | 25 +++++++++ 7 files changed, 90 insertions(+), 13 deletions(-) diff --git a/docs/ConfigurationAndUsage.md b/docs/ConfigurationAndUsage.md index 69d792af..1a7a9fa4 100644 --- a/docs/ConfigurationAndUsage.md +++ b/docs/ConfigurationAndUsage.md @@ -58,7 +58,8 @@ in the order registered. See [MultiTenant Strategies](Strategies) for more infor - `WithBasePathStrategy` - `WithClaimStrategy` - `WithDelegateStrategy` -- `WithHeaderStrateg`y +- `WithDelegateStrategy` +- `WithHeaderStrategy` - `WithHostStrategy` - `WithRouteStrategy` - `WithSessionStrategy` diff --git a/docs/Strategies.md b/docs/Strategies.md index 473f36f9..8a95a2a5 100644 --- a/docs/Strategies.md +++ b/docs/Strategies.md @@ -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` A `Func>`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` 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 @@ -75,15 +77,11 @@ builder.Services.AddMultiTenant() 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() - .WithDelegateStrategy(context => - { - var httpContext = context as HttpContext; - if (httpContext == null) - return null; - + .WithDelegateStrategy(httpContext => + { httpContext.Request.Query.TryGetValue("tenant", out StringValues tenantIdentifier); if (tenantIdentifier is null) diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs index 4e43ee9b..08f8b5e4 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs @@ -22,8 +22,10 @@ namespace Finbuckle.MultiTenant; public static class MultiTenantBuilderExtensions { /// - /// Adds a DistributedCacheStore to the application. + /// Adds a DistributedCacheStore to the application with maximum sliding expiration. /// + /// The ITenantInfo implementation type. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithDistributedCacheStore(this MultiTenantBuilder builder) where TTenantInfo : class, ITenantInfo, new() => builder.WithDistributedCacheStore(TimeSpan.MaxValue); @@ -32,8 +34,10 @@ public static MultiTenantBuilder WithDistributedCacheStore /// Adds a DistributedCacheStore to the application. /// + /// The ITenantInfo implementation type. /// The builder instance. /// The timespan for a cache entry's sliding expiration. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithDistributedCacheStore(this MultiTenantBuilder builder, TimeSpan? slidingExpiration) where TTenantInfo : class, ITenantInfo, new() { @@ -45,8 +49,10 @@ public static MultiTenantBuilder WithDistributedCacheStore /// Adds a HttpRemoteStore to the application. /// + /// The ITenantInfo implementation type. /// The builder instance. /// The endpoint URI template. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithHttpRemoteStore(this MultiTenantBuilder builder, string endpointTemplate) where TTenantInfo : class, ITenantInfo, new() => builder.WithHttpRemoteStore(endpointTemplate, null); @@ -54,9 +60,11 @@ public static MultiTenantBuilder WithHttpRemoteStore(t /// /// Adds a HttpRemoteStore to the application. /// + /// The ITenantInfo implementation type. /// The builder instance. /// The endpoint URI template. /// An action to configure the underlying HttpClient. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithHttpRemoteStore(this MultiTenantBuilder builder, string endpointTemplate, Action? clientConfig) where TTenantInfo : class, ITenantInfo, new() @@ -72,7 +80,9 @@ public static MultiTenantBuilder WithHttpRemoteStore(t /// /// Adds a ConfigurationStore to the application. Uses the default IConfiguration and section "Finbuckle:MultiTenant:Stores:ConfigurationStore". /// + /// The ITenantInfo implementation type. /// The builder instance. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithConfigurationStore(this MultiTenantBuilder builder) where TTenantInfo : class, ITenantInfo, new() => builder.WithStore>(ServiceLifetime.Singleton); @@ -80,9 +90,11 @@ public static MultiTenantBuilder WithConfigurationStore /// Adds a ConfigurationStore to the application. /// + /// The ITenantInfo implementation type. /// The builder instance. /// The IConfiguration to load the section from. /// The configuration section to load. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithConfigurationStore(this MultiTenantBuilder builder, IConfiguration configuration, string sectionName) @@ -92,17 +104,20 @@ public static MultiTenantBuilder WithConfigurationStore /// Adds an empty InMemoryStore to the application. /// + /// The ITenantInfo implementation type. /// The builder instance. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithInMemoryStore(this MultiTenantBuilder builder) where TTenantInfo : class, ITenantInfo, new() - // ReSharper disable once RedundantTypeArgumentsOfMethod - => builder.WithInMemoryStore(_ => {}); + => builder.WithInMemoryStore(_ => {}); /// /// Adds and configures InMemoryStore to the application using the provided action. /// + /// The ITenantInfo implementation type. /// The builder instance. /// An action for configuring the store. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithInMemoryStore(this MultiTenantBuilder builder, Action> config) where TTenantInfo : class, ITenantInfo, new() @@ -121,7 +136,9 @@ public static MultiTenantBuilder WithInMemoryStore(thi /// /// Adds an EchoStore to the application. /// + /// The ITenantInfo implementation type. /// The builder instance. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithEchoStore(this MultiTenantBuilder builder) where TTenantInfo : class, ITenantInfo, new() => builder.WithStore>(ServiceLifetime.Singleton); @@ -129,8 +146,10 @@ public static MultiTenantBuilder WithEchoStore(this Mu /// /// Adds and configures a StaticStrategy to the application. /// + /// The ITenantInfo implementation type. /// The builder instance. /// The tenant identifier to use for all tenant resolution. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithStaticStrategy(this MultiTenantBuilder builder, string identifier) where TTenantInfo : class, ITenantInfo, new() @@ -146,8 +165,10 @@ public static MultiTenantBuilder WithStaticStrategy(th /// /// Adds and configures a DelegateStrategy to the application. /// + /// The ITenantInfo implementation type. /// The builder instance. /// The delegate implementing the strategy. + /// The so that additional calls can be chained. public static MultiTenantBuilder WithDelegateStrategy(this MultiTenantBuilder builder, Func> doStrategy) where TTenantInfo : class, ITenantInfo, new() @@ -159,4 +180,31 @@ public static MultiTenantBuilder WithDelegateStrategy( return builder.WithStrategy(ServiceLifetime.Singleton, new object[] { doStrategy }); } + + /// + /// Adds and configures a typed DelegateStrategy<TContext> to the application. + /// + /// The strategy context type. + /// The ITenantInfo implementation type. + /// + /// + /// The so that additional calls can be chained. + public static MultiTenantBuilder WithDelegateStrategy(this MultiTenantBuilder builder, + Func> doStrategy) + where TTenantInfo : class, ITenantInfo, new() + { + ArgumentNullException.ThrowIfNull(doStrategy, nameof(doStrategy)); + + Func> wrapStrategy = context => + { + if (context is TContext typedContext) + { + return doStrategy(typedContext); + } + + return Task.FromResult(null); + }; + + return builder.WithDelegateStrategy(wrapStrategy); + } } \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs index 24605d9f..e0130795 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs @@ -11,6 +11,7 @@ // ReSharper disable once CheckNamespace namespace Finbuckle.MultiTenant; +// TODO XML comments public static class OptionsBuilderExtensions { public static OptionsBuilder ConfigurePerTenant( @@ -18,6 +19,7 @@ public static OptionsBuilder ConfigurePerTenant where TOptions : class where TTenantInfo : class, ITenantInfo, new() { + // TODO use ThrowNull here? if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs index d3ec3598..55369690 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs @@ -21,6 +21,7 @@ public static class FinbuckleServiceCollectionExtensions /// /// Configure Finbuckle.MultiTenant services for the application. /// + /// The ITenantInfo implementation type. /// The IServiceCollection instance the extension method applies to. /// An action to configure the MultiTenantOptions instance. /// A new instance of MultiTenantBuilder. diff --git a/src/Finbuckle.MultiTenant/MultiTenantBuilder.cs b/src/Finbuckle.MultiTenant/MultiTenantBuilder.cs index 8a2fe062..3f9e7665 100644 --- a/src/Finbuckle.MultiTenant/MultiTenantBuilder.cs +++ b/src/Finbuckle.MultiTenant/MultiTenantBuilder.cs @@ -6,6 +6,8 @@ namespace Finbuckle.MultiTenant; +// TODO: factor TTenantInfo into WithStore only + /// /// Builder class for Finbuckle.MultiTenant configuration. /// diff --git a/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderExtensionsShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderExtensionsShould.cs index 6c2b729a..9d1c7255 100644 --- a/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderExtensionsShould.cs +++ b/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderExtensionsShould.cs @@ -189,6 +189,31 @@ public void AddDelegateStrategy() var strategy = sp.GetRequiredService(); Assert.IsType(strategy); } + + [Fact] + public void AddTypedDelegateStrategy() + { + var services = new ServiceCollection(); + var builder = new MultiTenantBuilder(services); + builder.WithDelegateStrategy(context => Task.FromResult(context.ToString())!); + var sp = services.BuildServiceProvider(); + + var strategy = sp.GetRequiredService(); + Assert.IsType(strategy); + } + + [Fact] + public async Task ReturnNullForWrongTypeSendToTypedDelegateStrategy() + { + var services = new ServiceCollection(); + var builder = new MultiTenantBuilder(services); + builder.WithDelegateStrategy(context => Task.FromResult("Shouldn't ever get here")!); + var sp = services.BuildServiceProvider(); + + var strategy = sp.GetRequiredService(); + var identifier = await strategy.GetIdentifierAsync(new object()); + Assert.Null(identifier); + } [Fact] public void AddStaticStrategy() From f4cf2d9806261300c21841b7d41368997e475d81 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Mon, 27 Jan 2025 21:50:06 -0700 Subject: [PATCH 2/2] Update so that TContext must matched context type exactly (e.g. no derived classes) --- .../DependencyInjection/MultiTenantBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs index 08f8b5e4..60c3dacf 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilderExtensions.cs @@ -197,9 +197,9 @@ public static MultiTenantBuilder WithDelegateStrategy> wrapStrategy = context => { - if (context is TContext typedContext) + if (context.GetType() == typeof(TContext)) { - return doStrategy(typedContext); + return doStrategy((TContext)context); } return Task.FromResult(null);