diff --git a/.gitignore b/.gitignore index e9dbd9ff..2ee4f388 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # added .vscode/ *.db +*.db-shm +*.db-wal # globs Makefile.in diff --git a/docs/Options.md b/docs/Options.md index 395484cb..5824ff66 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -1,17 +1,39 @@ # Per-Tenant Options -Finbuckle.MultiTenant integrates with the standard ASP.NET -Core [Options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) and lets apps -customize options distinctly for each tenant. The current tenant determines which options are retrieved via -the `IOptions` (or derived) instance's `Value` property and `Get(string name)` method. +Finbuckle.MultiTenant is designed to emphasize using per-tenant options in an app to drive per-tenant behavior. This +approach allows app logic to be written having to add tenant-dependent or +tenant-specific logic to the code. -A specialized variation of this is [per-tenant authentication](Authentication). +By using per-tenant options, the options values used within app logic will automatically +reflect the per-tenant values as configured for the current tenant. Any code already using the Options pattern will gain +multi-tenant capability with minimal code changes. + +Finbuckle.MultiTenant integrates with the +standard [.NET Options pattern](https://learn.microsoft.com/en-us/dotnet/core/extensions/options) (see also the [ASP.NET +Core Options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) and lets apps +customize options distinctly for each tenant. + +Note: For authentication options, Finbuckle.MultiTenant provides special support +for [per-tenant authentication](Authentication). + +The current tenant determines which options are retrieved via +the `IOptions`, `IOptionsSnapshot`, or `IOptionsMonitor` instances' `Value` property and +`Get(string name)` method. Per-tenant options will work with *any* options class when using `IOptions`, `IOptionsSnapshot`, or `IOptionsMonitor` with dependency injection or service resolution. This includes an app's own code *and* -code internal to ASP.NET Core or other libraries that use the Options pattern. There is one potential caveat: ASP.NET -Core and other libraries may internally cache options or exhibit other unexpected behavior resulting in the wrong option -values! +code internal to ASP.NET Core or other libraries that use the Options pattern. + +A potential issue arises when code internally stores or caches options values from +an `IOptions`, `IOptionsSnapshot`, or `IOptionsMonitor` instance. This is usually +unnecessary because the options are already cached within the .NET options infrastructure, and in these cases the +initial instance of the options is always used, regardless of the current tenant. Finbuckle.MultiTenant works around +this for some parts of +ASP.NET Core, and recommends that in your own code to always access options values via +the `IOptions`, `IOptionsSnapshot`, or `IOptionsMonitor` instance. This will ensure the +correct values for the current tenant are used. + +## Options Basics Consider a typical scenario in ASP.Net Core, starting with a simple class: @@ -23,25 +45,24 @@ public class MyOptions } ``` -In the `ConfigureServices` method of the startup class, `services.Configure` is called with a delegate +In the app configuration, `services.Configure` is called with a delegate or `IConfiguration` parameter to set the option values: ```cs -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.Configure(options => options.Option1 = 1); +var builder = WebApplication.CreateBuilder(args); + +// other code omitted... + +builder.Services.Configure(options => options.Option1 = 1); - // Other services configured here... - } -} + // rest of app code... ``` -Dependency injection of `IOptions` into a controller (or anywhere DI can be used) provides access to the -options values, which are the same for every tenant at this point: +Dependency injection of `IOptions` or its siblings into a class constructor, such as a controller, provides +access to the options values. A service provider instance can also provide access to the options values. ```cs +// access options via dependency injection in a class constructor public MyController : Controller { private readonly MyOptions _myOptions; @@ -52,34 +73,64 @@ public MyController : Controller _myOptions = optionsAccessor.Value; } } + +// or with a service provider +httpContext.RequestServices.GetServices(); ``` +With standard options each tenant would get see the same exact options. + ## Customizing Options Per Tenant -This sections assumes Finbuckle.MultiTenant is installed and configured. See [Getting Started](GettingStarted) for -details. +This sections assumes a standard web application builder is configured and Finbuckle.MultiTenant is configured with +a `TTenantInfo` type of `TenantInfo`. +See [Getting Started](GettingStarted) for details. -Call `WithPerTenantOptions` after `AddMultiTenant` in the `ConfigureServices` method: +To configure options per tenant, the standard `Configure` method variants on the service collection now all +have `PerTenant` equivalents which accept a `Action` delegate. When the options are created at +runtime the delegate will be called with the current tenant details. ```cs -services.AddMultiTenant()... - .WithPerTenantOptions((options, tenantInfo) => +// configure options per tenant +builder.Services.ConfigurePerTenant((options, tenantInfo) => + { + options.MyOption1 = tenantInfo.Option1Value; + options.MyOption2 = tenantInfo.Option2Value; + }); + +// or configure named options per tenant +builder.Services.ConfigurePerTenant("scheme2", (options, tenantInfo) => + { + options.MyOption1 = tenantInfo.Option1Value; + options.MyOption2 = tenantInfo.Option2Value; + }); + +// ConfigureAll options variant +builder.Services.ConfigureAllPerTenant((options, tenantInfo) => { options.MyOption1 = tenantInfo.Option1Value; options.MyOption2 = tenantInfo.Option2Value; }); -``` -The type parameter `TOptions` is the options type being customized per-tenant. The method parameter is -an `Action`. This action will modify the options instance *after* the options normal configuration -and *before* its [post configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?#ipostconfigureoptions) -. +// can also configure post options, named post options, and all post options variants +builder.Services.PostConfigurePerTenant((options, tenantInfo) => + { + options.MyOption1 = tenantInfo.Option1Value; + options.MyOption2 = tenantInfo.Option2Value; + }); -`WithPerTenantOptions` can be called multiple times on the same `TOptions` -type and the configuration will run in the respective order. +builder.Services.PostConfigurePerTenant("scheme2", (options, tenantInfo) => + { + options.MyOption1 = tenantInfo.Option1Value; + options.MyOption2 = tenantInfo.Option2Value; + }); -The same delegate passed to `WithPerTenantOptions` is applied to all options generated of type `TOptions` -regardless of the option name, similar to the .NET `ConfigureAll` method. +builder.Services.PostConfigureAllPerTenant((options, tenantInfo) => + { + options.MyOption1 = tenantInfo.Option1Value; + options.MyOption2 = tenantInfo.Option2Value; + }); +``` Now with the same controller example from above, the option values will be specific to the current tenant: @@ -96,42 +147,27 @@ public MyController : Controller } ``` -## Named Options - -You can configure options by name using the `WithPerTenantNamedOptions` method. - -Call `WithPerTenantNamedOptions` after `AddMultiTenant` in the `ConfigureServices` method: - -```cs -services.AddMultiTenant()... - .WithPerTenantNamedOptions(someOptionsName, (options, tenantInfo) => - { - // only update options named "someOptionsName" - options.MyOption1 = tenantInfo.Option1Value; - options.MyOption2 = tenantInfo.Option2Value; - }); +## Using the OptionsBuilder API + +.NET provides +the [OptionsBuilder](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-8.0#optionsbuilder-api) +API to provide more flexibility for configuring options. This pattern simplifies dependency injection and validation for +the standard [Options pattern](https://learn.microsoft.com/en-us/dotnet/core/extensions/options). Finbuckle.MultiTenant +extends this API to enable options configuration for per-tenant options similarly. Note that while the `OptionsBuilder` +normally supports up to five dependencies, Finbuckle.MultiTenant support only supports four. + +```csharp +// use OptionsBuilder API to configure per-tenant options with dependencies +builder.Services + .AddOptions("optionalName") + .ConfigurePerTenant( + (options, es, tenantInfo) => + options.Property = DoSomethingWith(es, tenantInfo)); ``` -The `string` parameter is the name of the options. The type parameter `TOptions` is the options type being customized -per-tenant. The method parameter is an `Action`. This action will modify the options -instance *after* the options normal configuration and *before* -its [post configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?#ipostconfigureoptions) -. - -`WithPerTenantNameOptions` can be called multiple times on the same `TOptions` -type and the configuration will run in the respective order. - -The same delegate passed to `WithPerTenantNameOptions` is applied to all options generated of type `TOptions` -regardless of the option name. You can use the `name` argument in the callback to help you set the correct options by -name. - -`WithPerTenantOptions` can be used in combination with `WithPerTenantNameOptions` for the same -type `TOptions`. The `WithPerTenantOptions` callbacks will be invoked first, followed by -the `WithPerTenantNameOptions` callbacks. - -## Options Caching +## Options and Caching -Internally ASP.NET Core caches options, and Finbuckle.MultiTenant extends this to cache options per tenant. Caching +Internally .NET caches options, and Finbuckle.MultiTenant extends this to cache options per tenant. Caching occurs when a `TOptions` instance is retrieved via `Value` or `Get` on the injected `IOptions` (or derived) instance for the first time for a tenant. diff --git a/src/Finbuckle.MultiTenant.AspNetCore/Extensions/ApplicationBuilderExtensions.cs b/src/Finbuckle.MultiTenant.AspNetCore/Extensions/ApplicationBuilderExtensions.cs index d6f55316..7c82bb57 100644 --- a/src/Finbuckle.MultiTenant.AspNetCore/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant.AspNetCore/Extensions/ApplicationBuilderExtensions.cs @@ -14,7 +14,7 @@ public static class FinbuckleMultiTenantApplicationBuilderExtensions /// /// Use Finbuckle.MultiTenant middleware in processing the request. /// - /// The IApplicationBuilder instance the extension method applies to. + /// The IApplicationBuilder instance the extension method applies to. /// The same IApplicationBuilder passed into the method. public static IApplicationBuilder UseMultiTenant(this IApplicationBuilder builder) => builder.UseMiddleware(); diff --git a/src/Finbuckle.MultiTenant.AspNetCore/Extensions/FinbuckleMultiTenantBuilderExtensions.cs b/src/Finbuckle.MultiTenant.AspNetCore/Extensions/FinbuckleMultiTenantBuilderExtensions.cs index 41bec7e8..2f8af56b 100644 --- a/src/Finbuckle.MultiTenant.AspNetCore/Extensions/FinbuckleMultiTenantBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant.AspNetCore/Extensions/FinbuckleMultiTenantBuilderExtensions.cs @@ -97,7 +97,7 @@ public static FinbuckleMultiTenantBuilder WithPerTenantAuthenticati }); // Set per-tenant cookie options by convention. - builder.WithPerTenantOptions((options, tc) => + builder.Services.ConfigureAllPerTenant((options, tc) => { if (GetPropertyWithValidValue(tc, "CookieLoginPath") is string loginPath) options.LoginPath = loginPath.Replace(Constants.TenantToken, tc.Identifier); @@ -110,7 +110,7 @@ public static FinbuckleMultiTenantBuilder WithPerTenantAuthenticati }); // Set per-tenant OpenIdConnect options by convention. - builder.WithPerTenantOptions((options, tc) => + builder.Services.ConfigureAllPerTenant((options, tc) => { if (GetPropertyWithValidValue(tc, "OpenIdConnectAuthority") is string authority) options.Authority = authority.Replace(Constants.TenantToken, tc.Identifier); @@ -122,7 +122,7 @@ public static FinbuckleMultiTenantBuilder WithPerTenantAuthenticati options.ClientSecret = clientSecret.Replace(Constants.TenantToken, tc.Identifier); }); - builder.WithPerTenantOptions((options, tc) => + builder.Services.ConfigureAllPerTenant((options, tc) => { if (GetPropertyWithValidValue(tc, "ChallengeScheme") is string challengeScheme) options.DefaultChallengeScheme = challengeScheme; @@ -156,7 +156,7 @@ public static FinbuckleMultiTenantBuilder WithPerTenantAuthenticati // properties in the state parameter. if (builder.Services.All(s => s.ServiceType != typeof(IAuthenticationService))) throw new MultiTenantException( - "WithPerTenantAuthenticationCore() must be called after AddAuthentication() in ConfigureServices."); + "WithPerTenantAuthenticationCore() must be called after AddAuthentication()."); builder.Services.DecorateService>(); // We need to "decorate" IAuthenticationScheme provider. diff --git a/src/Finbuckle.MultiTenant.AspNetCore/Internal/MultiTenantAuthenticationSchemeProvider.cs b/src/Finbuckle.MultiTenant.AspNetCore/Internal/MultiTenantAuthenticationSchemeProvider.cs index ecab1941..024d931b 100644 --- a/src/Finbuckle.MultiTenant.AspNetCore/Internal/MultiTenantAuthenticationSchemeProvider.cs +++ b/src/Finbuckle.MultiTenant.AspNetCore/Internal/MultiTenantAuthenticationSchemeProvider.cs @@ -17,6 +17,7 @@ namespace Finbuckle.MultiTenant.AspNetCore /// /// Implements . /// + // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global internal class MultiTenantAuthenticationSchemeProvider : IAuthenticationSchemeProvider { private readonly IAuthenticationSchemeProvider _inner; diff --git a/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantConfigureNamedOptions.cs b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantConfigureNamedOptions.cs new file mode 100644 index 00000000..1eb29bf5 --- /dev/null +++ b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantConfigureNamedOptions.cs @@ -0,0 +1,12 @@ +// Copyright Finbuckle LLC, Andrew White, and Contributors. +// Refer to the solution LICENSE file for more information. + +using Microsoft.Extensions.Options; + +namespace Finbuckle.MultiTenant.Abstractions; + +// ReSharper disable once TypeParameterCanBeVariant +interface IMultiTenantConfigureNamedOptions : IConfigureNamedOptions + where TOptions : class +{ +} diff --git a/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStore.cs b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStore.cs index f7160deb..d13b2059 100644 --- a/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStore.cs +++ b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStore.cs @@ -1,8 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System.Collections.Generic; -using System.Threading.Tasks; + // ReSharper disable once CheckNamespace namespace Finbuckle.MultiTenant; diff --git a/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStrategy.cs b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStrategy.cs index 179374c1..891977e7 100644 --- a/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStrategy.cs +++ b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStrategy.cs @@ -1,7 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System.Threading.Tasks; + // ReSharper disable once CheckNamespace namespace Finbuckle.MultiTenant; diff --git a/src/Finbuckle.MultiTenant/Abstractions/ITenantResolver.cs b/src/Finbuckle.MultiTenant/Abstractions/ITenantResolver.cs index 33ac5908..2c67c089 100644 --- a/src/Finbuckle.MultiTenant/Abstractions/ITenantResolver.cs +++ b/src/Finbuckle.MultiTenant/Abstractions/ITenantResolver.cs @@ -1,8 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System.Collections.Generic; -using System.Threading.Tasks; + // ReSharper disable once CheckNamespace namespace Finbuckle.MultiTenant; diff --git a/src/Finbuckle.MultiTenant/Extensions/FinbuckleMultiTenantBuilderExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/FinbuckleMultiTenantBuilderExtensions.cs similarity index 99% rename from src/Finbuckle.MultiTenant/Extensions/FinbuckleMultiTenantBuilderExtensions.cs rename to src/Finbuckle.MultiTenant/DependencyInjection/FinbuckleMultiTenantBuilderExtensions.cs index d395d535..eb34a789 100644 --- a/src/Finbuckle.MultiTenant/Extensions/FinbuckleMultiTenantBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/FinbuckleMultiTenantBuilderExtensions.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Threading.Tasks; using Finbuckle.MultiTenant; using Finbuckle.MultiTenant.Internal; using Finbuckle.MultiTenant.Stores; diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs index 6dc6e1c2..6ad5bdc0 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs @@ -1,11 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using Finbuckle.MultiTenant; -using Finbuckle.MultiTenant.Options; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -13,8 +9,8 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Builder class for Finbuckle.MultiTenant configuration. /// -/// A type implementing ITenantInfo. -public class FinbuckleMultiTenantBuilder where T : class, ITenantInfo, new() +/// The ITenantInfo implementation type. +public class FinbuckleMultiTenantBuilder where TTenantInfo : class, ITenantInfo, new() { /// /// Gets or sets the IServiceCollection instance used by the builder. @@ -36,11 +32,13 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) /// The configuration action to be run for each tenant. /// The same MultiTenantBuilder passed into the method. /// This is similar to `ConfigureAll` in that it applies to all named and unnamed options of the type. - public FinbuckleMultiTenantBuilder WithPerTenantOptions( - Action tenantConfigureOptions) where TOptions : class, new() + [Obsolete] + public FinbuckleMultiTenantBuilder WithPerTenantOptions( + Action tenantConfigureOptions) where TOptions : class, new() { + // TODO remove this method // TODO maybe change this to string empty so null an be used for all options, note remarks. - return WithPerTenantNamedOptions(null, tenantConfigureOptions); + return WithPerTenantNamedOptions(null, tenantConfigureOptions); } /// @@ -50,50 +48,34 @@ public FinbuckleMultiTenantBuilder WithPerTenantOptions( /// The configuration action to be run for each tenant. /// The same MultiTenantBuilder passed into the method. // ReSharper disable once MemberCanBePrivate.Global - public FinbuckleMultiTenantBuilder WithPerTenantNamedOptions(string? name, - Action tenantConfigureNamedOptions) where TOptions : class, new() + [Obsolete] + public FinbuckleMultiTenantBuilder WithPerTenantNamedOptions(string? name, + Action tenantConfigureNamedOptions) where TOptions : class, new() { - if (tenantConfigureNamedOptions == null) - { - throw new ArgumentNullException(nameof(tenantConfigureNamedOptions)); - } - - // Handles multiplexing cached options. - Services.TryAddSingleton, MultiTenantOptionsCache>(); - - // Necessary to apply tenant named options in between configuration and post configuration - Services.AddSingleton, - TenantConfigureNamedOptions>(_ => new TenantConfigureNamedOptions(name, tenantConfigureNamedOptions)); - Services.TryAddTransient, MultiTenantOptionsFactory>(); - Services.TryAddScoped>(BuildOptionsManager); - Services.TryAddSingleton>(BuildOptionsManager); + // TODO remove this method + // if (tenantConfigureNamedOptions == null) + // { + // throw new ArgumentNullException(nameof(tenantConfigureNamedOptions)); + // } + // + // // Services.AddOptionsCore(); + // Services.TryAddEnumerable(ServiceDescriptor + // .Scoped, TenantConfigureNamedOptionsWrapper>()); + // Services.AddScoped>(sp => + // new MultiTenantConfigureNamedOptions(name, tenantConfigureNamedOptions)); return this; } - // TODO consider per tenant AllOptions variation - // TODO consider per-tenant post options - // TODO consider OptionsBuilder api - - private static MultiTenantOptionsManager BuildOptionsManager(IServiceProvider sp) - where TOptions : class, new() - { - var cache = (IOptionsMonitorCache)ActivatorUtilities.CreateInstance(sp, - typeof(MultiTenantOptionsCache)); - return (MultiTenantOptionsManager) - ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager), cache); - } - /// /// Adds and configures an IMultiTenantStore to the application using default dependency injection. /// > /// The service lifetime. /// a parameter list for any constructor parameters not covered by dependency injection. /// The same MultiTenantBuilder passed into the method. - public FinbuckleMultiTenantBuilder WithStore(ServiceLifetime lifetime, + public FinbuckleMultiTenantBuilder WithStore(ServiceLifetime lifetime, params object[] parameters) - where TStore : IMultiTenantStore + where TStore : IMultiTenantStore => WithStore(lifetime, sp => ActivatorUtilities.CreateInstance(sp, parameters)); /// @@ -103,9 +85,9 @@ public FinbuckleMultiTenantBuilder WithStore(ServiceLifetime lifetime /// A delegate that will create and configure the store. /// The same MultiTenantBuilder passed into the method. // ReSharper disable once MemberCanBePrivate.Global - public FinbuckleMultiTenantBuilder WithStore(ServiceLifetime lifetime, + public FinbuckleMultiTenantBuilder WithStore(ServiceLifetime lifetime, Func factory) - where TStore : IMultiTenantStore + where TStore : IMultiTenantStore { if (factory == null) { @@ -114,7 +96,7 @@ public FinbuckleMultiTenantBuilder WithStore(ServiceLifetime lifetime // Note: can't use TryAddEnumerable here because ServiceDescriptor.Describe with a factory can't set implementation type. Services.Add( - ServiceDescriptor.Describe(typeof(IMultiTenantStore), sp => factory(sp), lifetime)); + ServiceDescriptor.Describe(typeof(IMultiTenantStore), sp => factory(sp), lifetime)); return this; } @@ -125,7 +107,7 @@ public FinbuckleMultiTenantBuilder WithStore(ServiceLifetime lifetime /// The service lifetime. /// a parameter list for any constructor parameters not covered by dependency injection. /// The same MultiTenantBuilder passed into the method. - public FinbuckleMultiTenantBuilder WithStrategy(ServiceLifetime lifetime, + public FinbuckleMultiTenantBuilder WithStrategy(ServiceLifetime lifetime, params object[] parameters) where TStrategy : IMultiTenantStrategy => WithStrategy(lifetime, sp => ActivatorUtilities.CreateInstance(sp, parameters)); @@ -136,7 +118,7 @@ public FinbuckleMultiTenantBuilder WithStrategy(ServiceLifetime li /// A delegate that will create and configure the strategy. /// The same MultiTenantBuilder passed into the method. // ReSharper disable once MemberCanBePrivate.Global - public FinbuckleMultiTenantBuilder WithStrategy(ServiceLifetime lifetime, + public FinbuckleMultiTenantBuilder WithStrategy(ServiceLifetime lifetime, Func factory) where TStrategy : IMultiTenantStrategy { diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs new file mode 100644 index 00000000..6921cd68 --- /dev/null +++ b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs @@ -0,0 +1,363 @@ +// Copyright Finbuckle LLC, Andrew White, and Contributors. +// Refer to the solution LICENSE file for more information. + +// Portions of this file are derived from the .NET Foundation source file located at: +// https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs + +using Finbuckle.MultiTenant; +using Microsoft.Extensions.Options; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +public static class OptionsBuilderExtensions +{ + public static OptionsBuilder ConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions>( + optionsBuilder.Name, + sp.GetRequiredService>(), + (options, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, tenantInfo); + })); + + return optionsBuilder; + } + + public static OptionsBuilder ConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TDep : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions>( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService>(), + (options, dep, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, dep, tenantInfo); + })); + + return optionsBuilder; + } + + public static OptionsBuilder ConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TDep1 : class + where TDep2 : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions>( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + (options, dep1, dep2, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, tenantInfo); + })); + + return optionsBuilder; + } + + public static OptionsBuilder ConfigurePerTenant( + this OptionsBuilder optionsBuilder, + Action configureOptions) + where TOptions : class + where TDep1 : class + where TDep2 : class + where TDep3 : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions>( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + (options, dep1, dep2, dep3, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, dep3, tenantInfo); + })); + + return optionsBuilder; + } + + public static OptionsBuilder ConfigurePerTenant( + this OptionsBuilder optionsBuilder, + Action configureOptions) + where TOptions : class + where TDep1 : class + where TDep2 : class + where TDep3 : class + where TDep4 : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions>( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + (options, dep1, dep2, dep3, dep4, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, dep3, dep4, tenantInfo); + })); + + return optionsBuilder; + } + + // Experimental + // public static OptionsBuilder ConfigurePerTenant( + // this OptionsBuilder optionsBuilder, + // Action configureOptions) + // where TOptions : class + // where TDep1 : class + // where TDep2 : class + // where TDep3 : class + // where TDep4 : class + // where TDep5 : class + // where TTenantInfo : class, ITenantInfo, new() + // { + // if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + // + // FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + // + // optionsBuilder.Services.AddTransient>(sp => + // { + // var mtcAccessor = sp.GetRequiredService>(); + // return new ConfigureNamedOptions( + // optionsBuilder.Name, + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // (options, dep1, dep2, dep3, dep4, dep5) => + // { + // var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + // if (tenantInfo is not null) + // configureOptions(options, dep1, dep2, dep3, dep4, dep5, tenantInfo); + // }); + // }); + // + // return optionsBuilder; + // } + + public static OptionsBuilder PostConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions>( + optionsBuilder.Name, + sp.GetRequiredService>(), + (options, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, tenantInfo); + })); + + return optionsBuilder; + } + + public static OptionsBuilder PostConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TDep : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions>( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService>(), + (options, dep, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, dep, tenantInfo); + })); + + return optionsBuilder; + } + + public static OptionsBuilder PostConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TDep1 : class + where TDep2 : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions>( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + (options, dep1, dep2, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, tenantInfo); + })); + + return optionsBuilder; + } + + public static OptionsBuilder PostConfigurePerTenant( + this OptionsBuilder optionsBuilder, + Action configureOptions) + where TOptions : class + where TDep1 : class + where TDep2 : class + where TDep3 : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions>( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + (options, dep1, dep2, dep3, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, dep3, tenantInfo); + })); + + return optionsBuilder; + } + + public static OptionsBuilder PostConfigurePerTenant( + this OptionsBuilder optionsBuilder, + Action configureOptions) + where TOptions : class + where TDep1 : class + where TDep2 : class + where TDep3 : class + where TDep4 : class + where TTenantInfo : class, ITenantInfo, new() + { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions>( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + (options, dep1, dep2, dep3, dep4, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, dep3, dep4, tenantInfo); + })); + + return optionsBuilder; + } + + // Experimental + // public static OptionsBuilder PostConfigurePerTenant( + // this OptionsBuilder optionsBuilder, + // Action configureOptions) + // where TOptions : class + // where TDep1 : class + // where TDep2 : class + // where TDep3 : class + // where TDep4 : class + // where TDep5 : class + // where TTenantInfo : class, ITenantInfo, new() + // { + // if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + // + // FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + // + // optionsBuilder.Services.AddTransient>(sp => + // { + // var mtcAccessor = sp.GetRequiredService>(); + // return new PostConfigureOptions( + // optionsBuilder.Name, + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // sp.GetRequiredService(), + // (options, dep1, dep2, dep3, dep4, dep5) => + // { + // var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + // if (tenantInfo is not null) + // configureOptions(options, dep1, dep2, dep3, dep4, dep5, tenantInfo); + // }); + // }); + // + // return optionsBuilder; + // } +} \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..66f48e83 --- /dev/null +++ b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,280 @@ +// Copyright Finbuckle LLC, Andrew White, and Contributors. +// Refer to the solution LICENSE file for more information. + +using Finbuckle.MultiTenant; +using Finbuckle.MultiTenant.Internal; +using Finbuckle.MultiTenant.Options; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// IServiceCollection extension methods for Finbuckle.MultiTenant. +/// +public static class FinbuckleServiceCollectionExtensions +{ + /// + /// Configure Finbuckle.MultiTenant services for the application. + /// + /// The IServiceCollection instance the extension method applies to. + /// An action to configure the MultiTenantOptions instance. + /// A new instance of MultiTenantBuilder. + // ReSharper disable once MemberCanBePrivate.Global + public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceCollection services, + Action config) + where TTenantInfo : class, ITenantInfo, new() + { + services.AddScoped, TenantResolver>(); + services.AddScoped( + sp => (ITenantResolver)sp.GetRequiredService>()); + + services.AddScoped>(sp => + sp.GetRequiredService>().MultiTenantContext!); + + services.AddScoped(sp => + sp.GetRequiredService>().MultiTenantContext?.TenantInfo!); + services.AddScoped(sp => sp.GetService()!); + + // TODO this might require instance to ensure it already exists when needed + services + .AddSingleton, + AsyncLocalMultiTenantContextAccessor>(); + services.AddSingleton(sp => + (IMultiTenantContextAccessor)sp.GetRequiredService>()); + + services.Configure(config); + + return new FinbuckleMultiTenantBuilder(services); + } + + /// + /// Configure Finbuckle.MultiTenant services for the application. + /// + /// The IServiceCollection instance the extension method applies to. + /// An new instance of MultiTenantBuilder. + public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceCollection services) + where TTenantInfo : class, ITenantInfo, new() + { + return services.AddMultiTenant(_ => { }); + } + + // TODO: better document and extract + public static bool DecorateService(this IServiceCollection services, params object[] parameters) + { + var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService)); + if (existingService is null) + throw new ArgumentException($"No service of type {typeof(TService).Name} found."); + + ServiceDescriptor? newService; + + if (existingService.ImplementationType is not null) + { + newService = new ServiceDescriptor(existingService.ServiceType, + sp => + { + TService inner = + (TService)ActivatorUtilities.CreateInstance(sp, existingService.ImplementationType); + + if (inner is null) + throw new Exception( + $"Unable to instantiate decorated type via implementation type {existingService.ImplementationType.Name}."); + + var parameters2 = new object[parameters.Length + 1]; + Array.Copy(parameters, 0, parameters2, 1, parameters.Length); + parameters2[0] = inner; + + return ActivatorUtilities.CreateInstance(sp, parameters2)!; + }, + existingService.Lifetime); + } + else if (existingService.ImplementationInstance is not null) + { + newService = new ServiceDescriptor(existingService.ServiceType, + sp => + { + TService inner = (TService)existingService.ImplementationInstance; + return ActivatorUtilities.CreateInstance(sp, inner, parameters)!; + }, + existingService.Lifetime); + } + else if (existingService.ImplementationFactory is not null) + { + newService = new ServiceDescriptor(existingService.ServiceType, + sp => + { + TService inner = (TService)existingService.ImplementationFactory(sp); + if (inner is null) + throw new Exception( + $"Unable to instantiate decorated type via implementation factory."); + + return ActivatorUtilities.CreateInstance(sp, inner, parameters)!; + }, + existingService.Lifetime); + } + else + { + throw new Exception( + $"Unable to instantiate decorated type."); + } + + services.Remove(existingService); + services.Add(newService); + + return true; + } + + /// + /// Registers an action used to configure a particular type of options per tenant. + /// + /// The options type to be configured. + /// The ITenantInfo implementation type. + /// The to add the services to. + /// The name of the options instance. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection ConfigurePerTenant( + this IServiceCollection services, + string? name, Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + ConfigurePerTenantReqs(services); + + services.AddTransient>(sp => + new ConfigureNamedOptions>( + name, + sp.GetRequiredService>(), + (options, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, tenantInfo); + })); + + return services; + } + + /// + /// Registers an action used to configure a particular type of options. + /// + /// The options type to be configured. + /// The ITenantInfo implementation type. + /// The to add the services to. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection ConfigurePerTenant( + this IServiceCollection services, + Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + return services.ConfigurePerTenant(Options.Options.DefaultName, configureOptions); + } + + /// + /// Registers an action used to configure all instances of a particular type of options per tenant. + /// + /// The options type to be configured. + /// The ITenantInfo implementation type. + /// The to add the services to. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection ConfigureAllPerTenant( + this IServiceCollection services, + Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + return services.ConfigurePerTenant(null, configureOptions); + } + + /// + /// Registers a post configure action used to configure a particular type of options per tenant. + /// + /// The options type to be configured. + /// The ITenantInfo implementation type. + /// The to add the services to. + /// The name of the options instance. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection PostConfigurePerTenant( + this IServiceCollection services, + string? name, Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + ConfigurePerTenantReqs(services); + + services.AddTransient>(sp => + new PostConfigureOptions>( + name, + sp.GetRequiredService>(), + (options, mtcAccessor) => + { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, tenantInfo); + })); + + return services; + } + + /// + /// Registers a post configure action used to configure a particular type of options per tenant. + /// + /// The options type to be configured. + /// The ITenantInfo implementation type. + /// The to add the services to. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection PostConfigurePerTenant( + this IServiceCollection services, + Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + return services.PostConfigurePerTenant(Options.Options.DefaultName, configureOptions); + } + + /// + /// Registers a post configure action used to configure all instances of a particular type of options per tenant. + /// + /// The options type to be configured. + /// The ITenantInfo implementation type. + /// The to add the services to. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection PostConfigureAllPerTenant( + this IServiceCollection services, + Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + return services.PostConfigurePerTenant(null, configureOptions); + } + + internal static void ConfigurePerTenantReqs(IServiceCollection services) + where TOptions : class + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + // Required infrastructure. + services.AddOptions(); + + // TODO: Add check for success + services.TryAddSingleton, MultiTenantOptionsCache>(); + services.TryAddScoped>(BuildOptionsManager); + services.TryAddSingleton>(BuildOptionsManager); + return; + + MultiTenantOptionsManager BuildOptionsManager(IServiceProvider sp) + { + var cache = (IOptionsMonitorCache)ActivatorUtilities.CreateInstance(sp, + typeof(MultiTenantOptionsCache)); + return (MultiTenantOptionsManager) + ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager), cache); + } + } +} \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/Events/MultiTenantEvents.cs b/src/Finbuckle.MultiTenant/Events/MultiTenantEvents.cs index a95195a7..93e3a7cb 100644 --- a/src/Finbuckle.MultiTenant/Events/MultiTenantEvents.cs +++ b/src/Finbuckle.MultiTenant/Events/MultiTenantEvents.cs @@ -1,8 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Threading.Tasks; + // ReSharper disable once CheckNamespace namespace Finbuckle.MultiTenant; diff --git a/src/Finbuckle.MultiTenant/Events/TenantResolvedContext.cs b/src/Finbuckle.MultiTenant/Events/TenantResolvedContext.cs index 1aa9be94..d35d273d 100644 --- a/src/Finbuckle.MultiTenant/Events/TenantResolvedContext.cs +++ b/src/Finbuckle.MultiTenant/Events/TenantResolvedContext.cs @@ -1,7 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; + // ReSharper disable once CheckNamespace namespace Finbuckle.MultiTenant; diff --git a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs b/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 62b1c35a..00000000 --- a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -using System; -using System.Linq; -using Finbuckle.MultiTenant; -using Finbuckle.MultiTenant.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// IServiceCollection extension methods for Finbuckle.MultiTenant. -/// -public static class FinbuckleServiceCollectionExtensions -{ - /// - /// Configure Finbuckle.MultiTenant services for the application. - /// - /// The IServiceCollection instance the extension method applies to. - /// An action to configure the MultiTenantOptions instance. - /// A new instance of MultiTenantBuilder. - // ReSharper disable once MemberCanBePrivate.Global - public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceCollection services, - Action config) - where T : class, ITenantInfo, new() - { - services.AddScoped, TenantResolver>(); - services.AddScoped(sp => (ITenantResolver)sp.GetRequiredService>()); - - services.AddScoped>(sp => - sp.GetRequiredService>().MultiTenantContext!); - - services.AddScoped(sp => - sp.GetRequiredService>().MultiTenantContext?.TenantInfo!); - services.AddScoped(sp => sp.GetService()!); - - services.AddSingleton, AsyncLocalMultiTenantContextAccessor>(); - services.AddSingleton(sp => - (IMultiTenantContextAccessor)sp.GetRequiredService>()); - - services.Configure(config); - - return new FinbuckleMultiTenantBuilder(services); - } - - /// - /// Configure Finbuckle.MultiTenant services for the application. - /// - /// The IServiceCollection instance the extension method applies to. - /// An new instance of MultiTenantBuilder. - public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceCollection services) - where T : class, ITenantInfo, new() - { - return services.AddMultiTenant(_ => { }); - } - - public static bool DecorateService(this IServiceCollection services, params object[] parameters) - { - var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService)); - if (existingService is null) - throw new ArgumentException($"No service of type {typeof(TService).Name} found."); - - ServiceDescriptor? newService; - - if (existingService.ImplementationType is not null) - { - newService = new ServiceDescriptor(existingService.ServiceType, - sp => - { - TService inner = - (TService)ActivatorUtilities.CreateInstance(sp, existingService.ImplementationType); - - if (inner is null) - throw new Exception( - $"Unable to instantiate decorated type via implementation type {existingService.ImplementationType.Name}."); - - var parameters2 = new object[parameters.Length + 1]; - Array.Copy(parameters, 0, parameters2, 1, parameters.Length); - parameters2[0] = inner; - - return ActivatorUtilities.CreateInstance(sp, parameters2)!; - }, - existingService.Lifetime); - } - else if (existingService.ImplementationInstance is not null) - { - newService = new ServiceDescriptor(existingService.ServiceType, - sp => - { - TService inner = (TService)existingService.ImplementationInstance; - return ActivatorUtilities.CreateInstance(sp, inner, parameters)!; - }, - existingService.Lifetime); - } - else if (existingService.ImplementationFactory is not null) - { - newService = new ServiceDescriptor(existingService.ServiceType, - sp => - { - TService inner = (TService)existingService.ImplementationFactory(sp); - if (inner is null) - throw new Exception( - $"Unable to instantiate decorated type via implementation factory."); - - return ActivatorUtilities.CreateInstance(sp, inner, parameters)!; - }, - existingService.Lifetime); - } - else - { - throw new Exception( - $"Unable to instantiate decorated type."); - } - - services.Remove(existingService); - services.Add(newService); - - return true; - } -} \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/Extensions/TypeExtensions.cs b/src/Finbuckle.MultiTenant/Extensions/TypeExtensions.cs index 044fc2eb..34fcef60 100644 --- a/src/Finbuckle.MultiTenant/Extensions/TypeExtensions.cs +++ b/src/Finbuckle.MultiTenant/Extensions/TypeExtensions.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Linq; using System.Reflection; namespace Finbuckle.MultiTenant; diff --git a/src/Finbuckle.MultiTenant/Finbuckle.MultiTenant.csproj b/src/Finbuckle.MultiTenant/Finbuckle.MultiTenant.csproj index ba96ee1b..679fb679 100644 --- a/src/Finbuckle.MultiTenant/Finbuckle.MultiTenant.csproj +++ b/src/Finbuckle.MultiTenant/Finbuckle.MultiTenant.csproj @@ -5,6 +5,7 @@ Finbuckle.MultiTenant Main library package for Finbuckle.MultiTenant. enable + true diff --git a/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs b/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs index ac6e1147..4f65d841 100644 --- a/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs +++ b/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System.Threading; - namespace Finbuckle.MultiTenant.Internal; /// @@ -26,7 +24,8 @@ public IMultiTenantContext? MultiTenantContext } /// - /// TODO move this to the interface? + // TODO move this to the interface? + // TODO should the set throw if "as" returns null? IMultiTenantContext? IMultiTenantContextAccessor.MultiTenantContext { get => MultiTenantContext as IMultiTenantContext; diff --git a/src/Finbuckle.MultiTenant/MultiTenantAttribute.cs b/src/Finbuckle.MultiTenant/MultiTenantAttribute.cs index 72e3e050..fe78857e 100644 --- a/src/Finbuckle.MultiTenant/MultiTenantAttribute.cs +++ b/src/Finbuckle.MultiTenant/MultiTenantAttribute.cs @@ -1,13 +1,11 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; - namespace Finbuckle.MultiTenant; /// -/// Marks a class as multitenant when used with a database context -/// derived from MultiTenantDbContext or MultiTenantIdentityDbContext. +/// Marks a class as multitenant. Currently only used in EFCore support but included here to reduce dependencies where +/// this might be needed. /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class MultiTenantAttribute : Attribute diff --git a/src/Finbuckle.MultiTenant/MultiTenantException.cs b/src/Finbuckle.MultiTenant/MultiTenantException.cs index f286e034..19d80046 100644 --- a/src/Finbuckle.MultiTenant/MultiTenantException.cs +++ b/src/Finbuckle.MultiTenant/MultiTenantException.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; - namespace Finbuckle.MultiTenant; /// diff --git a/src/Finbuckle.MultiTenant/MultiTenantOptions.cs b/src/Finbuckle.MultiTenant/MultiTenantOptions.cs index 5866446d..cd934984 100644 --- a/src/Finbuckle.MultiTenant/MultiTenantOptions.cs +++ b/src/Finbuckle.MultiTenant/MultiTenantOptions.cs @@ -1,7 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System.Collections.Generic; + // TODO move to options folder/namespace on future major release namespace Finbuckle.MultiTenant; diff --git a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptions.cs b/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptions.cs deleted file mode 100644 index e264f89a..00000000 --- a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -namespace Finbuckle.MultiTenant.Options; - -/// -/// Configures options per-tenant. -/// -/// Options type being configured. -/// A type implementing ITenantInfo. -public interface ITenantConfigureNamedOptions - where TOptions : class, new() - where TTenantInfo : class, ITenantInfo, new() -{ - /// - /// Invoked to configure per-tenant options. - /// - /// The name of the option to be configured. - /// The options class instance to be configured. - /// The TTenantInfo instance for the options being configured. - void Configure(string name, TOptions options, TTenantInfo tenantInfo); -} \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs b/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs deleted file mode 100644 index 4215fb70..00000000 --- a/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -using System; - -namespace Finbuckle.MultiTenant.Options; - -[Obsolete] -public interface ITenantConfigureOptions - where TOptions : class, new() - where TTenantInfo : class, ITenantInfo, new() -{ - void Configure(TOptions options, TTenantInfo tenantInfo); -} \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs index bf24f075..392f3825 100644 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs +++ b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs @@ -1,7 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; using System.Collections.Concurrent; using Microsoft.Extensions.Options; @@ -10,26 +9,22 @@ namespace Finbuckle.MultiTenant.Options; /// /// Adds, retrieves, and removes instances of TOptions after adjusting them for the current TenantContext. /// -public class MultiTenantOptionsCache : IOptionsMonitorCache +public class MultiTenantOptionsCache : IOptionsMonitorCache where TOptions : class - where TTenantInfo : class, ITenantInfo, new() { - private readonly IMultiTenantContextAccessor multiTenantContextAccessor; - - // The object is just a dummy because there is no ConcurrentSet class. - //private readonly ConcurrentDictionary> _adjustedOptionsNames = - // new ConcurrentDictionary>(); - - private readonly ConcurrentDictionary> map = new ConcurrentDictionary>(); + private readonly IMultiTenantContextAccessor multiTenantContextAccessor; + + private readonly ConcurrentDictionary> map = new(); /// /// Constructs a new instance of MultiTenantOptionsCache. /// /// /// - public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor) + public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor) { - this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor)); + this.multiTenantContextAccessor = multiTenantContextAccessor ?? + throw new ArgumentNullException(nameof(multiTenantContextAccessor)); } /// @@ -59,7 +54,7 @@ public void Clear(string tenantId) /// public void ClearAll() { - foreach(var cache in map.Values) + foreach (var cache in map.Values) cache.Clear(); } @@ -76,7 +71,7 @@ public TOptions GetOrAdd(string? name, Func createOptions) throw new ArgumentNullException(nameof(createOptions)); } - name = name ?? Microsoft.Extensions.Options.Options.DefaultName; + name ??= Microsoft.Extensions.Options.Options.DefaultName; var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; var cache = map.GetOrAdd(tenantId, new OptionsCache()); diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs deleted file mode 100644 index d73c2c7e..00000000 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -// Portions of this file are derived from the .NET Foundation source file located at: -// https://github.com/dotnet/runtime/blob/5aad989cebe00f0987fcb842ea5b7cbe986c67df/src/libraries/Microsoft.Extensions.Options/src/OptionsFactory.cs - -using System.Collections.Generic; -using Microsoft.Extensions.Options; - -namespace Finbuckle.MultiTenant.Options; - -/// -/// Implementation of IOptionsFactory. -/// -/// The type of options being requested. -/// The type of the tenant info. -public class MultiTenantOptionsFactory : IOptionsFactory - where TOptions : class, new() - where TTenantInfo : class, ITenantInfo, new() -{ - private readonly IConfigureOptions[] _configureOptions; - private readonly IPostConfigureOptions[] _postConfigureOptions; - private readonly IValidateOptions[] _validations; - - private readonly ITenantConfigureOptions[] _tenantConfigureOptions; - private readonly ITenantConfigureNamedOptions[] _tenantConfigureNamedOptions; - private readonly IMultiTenantContextAccessor _multiTenantContextAccessor; - - /// - /// Initializes a new instance with the specified options configurations. - /// - public MultiTenantOptionsFactory(IEnumerable> configureOptions, - IEnumerable> postConfigureOptions, - IEnumerable> validations, - IEnumerable> tenantConfigureOptions, - IEnumerable> tenantConfigureNamedOptions, - IMultiTenantContextAccessor multiTenantContextAccessor) - { - // The default DI container uses arrays under the covers. Take advantage of this knowledge - // by checking for an array and enumerate over that, so we don't need to allocate an enumerator. - // When it isn't already an array, convert it to one, but don't use System.Linq to avoid pulling Linq in to - // small trimmed applications. - - _configureOptions = configureOptions as IConfigureOptions[] ?? - new List>(configureOptions).ToArray(); - _postConfigureOptions = postConfigureOptions as IPostConfigureOptions[] ?? - new List>(postConfigureOptions).ToArray(); - _validations = validations as IValidateOptions[] ?? - new List>(validations).ToArray(); - _tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions[] ?? - new List>(tenantConfigureOptions) - .ToArray(); - _tenantConfigureNamedOptions = - tenantConfigureNamedOptions as ITenantConfigureNamedOptions[] ?? - new List>(tenantConfigureNamedOptions).ToArray(); - _multiTenantContextAccessor = multiTenantContextAccessor; - } - - /// - public TOptions Create(string name) - { - var options = new TOptions(); - foreach (var setup in _configureOptions) - { - if (setup is IConfigureNamedOptions namedSetup) - { - namedSetup.Configure(name, options); - } - else if (name == Microsoft.Extensions.Options.Options.DefaultName) - { - setup.Configure(options); - } - } - - // Configure tenant options. - if (_multiTenantContextAccessor.MultiTenantContext?.HasResolvedTenant ?? false) - { - foreach (var tenantConfigureOption in _tenantConfigureOptions) - tenantConfigureOption.Configure(options, _multiTenantContextAccessor.MultiTenantContext.TenantInfo!); - - // Configure tenant named options. - foreach (var tenantConfigureNamedOption in _tenantConfigureNamedOptions) - tenantConfigureNamedOption.Configure(name, options, - _multiTenantContextAccessor.MultiTenantContext.TenantInfo!); - } - - foreach (var post in _postConfigureOptions) - { - post.PostConfigure(name, options); - } - - // TODO consider per tenant post configure - - if (_validations.Length > 0) - { - var failures = new List(); - foreach (IValidateOptions validate in _validations) - { - ValidateOptionsResult result = validate.Validate(name, options); - if (result is { Failed: true }) - { - failures.AddRange(result.Failures); - } - } - - if (failures.Count > 0) - { - throw new OptionsValidationException(name, typeof(TOptions), failures); - } - } - - return options; - } -} \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs index f7d50035..9a45a498 100644 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs +++ b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs @@ -12,7 +12,7 @@ namespace Finbuckle.MultiTenant.Options; /// Implementation of IOptions and IOptionsSnapshot that uses dependency injection for its private cache. /// /// The type of options being configured. -public class MultiTenantOptionsManager : IOptionsSnapshot where TOptions : class, new() +public class MultiTenantOptionsManager : IOptionsSnapshot where TOptions : class { private readonly IOptionsFactory _factory; private readonly IOptionsMonitorCache _cache; // Note: this is a private cache @@ -34,7 +34,7 @@ public MultiTenantOptionsManager(IOptionsFactory factory, IOptionsMoni /// public TOptions Get(string? name) { - name = name ?? Microsoft.Extensions.Options.Options.DefaultName; + name ??= Microsoft.Extensions.Options.Options.DefaultName; // Store the options in our instance cache. return _cache.GetOrAdd(name, () => _factory.Create(name)); diff --git a/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptions.cs b/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptions.cs deleted file mode 100644 index 820c1551..00000000 --- a/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptions.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -using System; - -namespace Finbuckle.MultiTenant.Options; - -/// -public class TenantConfigureNamedOptions : ITenantConfigureNamedOptions - where TOptions : class, new() - where TTenantInfo : class, ITenantInfo, new() -{ - /// - /// Gets the name of the named option for configuration. - /// - // ReSharper disable once MemberCanBePrivate.Global - public string? Name { get; } - private readonly Action configureOptions; - - /// - /// Constructs a new instance of TenantConfigureNamedOptions. - /// - /// - /// - public TenantConfigureNamedOptions(string? name, Action configureOptions) - { - Name = name; - this.configureOptions = configureOptions; - } - - /// - public void Configure(string name, TOptions options, TTenantInfo tenantInfo) - { - // Null name is used to configure all named options. - if (Name == null || name == Name) - { - configureOptions(options, tenantInfo); - } - } -} \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/Options/TenantConfigureOptions.cs b/src/Finbuckle.MultiTenant/Options/TenantConfigureOptions.cs deleted file mode 100644 index f032ca58..00000000 --- a/src/Finbuckle.MultiTenant/Options/TenantConfigureOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -using System; - -namespace Finbuckle.MultiTenant.Options; - -[Obsolete] -public class TenantConfigureOptions : ITenantConfigureOptions - where TOptions : class, new() - where TTenantInfo : class, ITenantInfo, new() -{ - private readonly Action configureOptions; - - public TenantConfigureOptions(Action configureOptions) - { - this.configureOptions = configureOptions; - } - - public void Configure(TOptions options, TTenantInfo tenantInfo) - { - configureOptions(options, tenantInfo); - } -} \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/StoreInfo.cs b/src/Finbuckle.MultiTenant/StoreInfo.cs index 436f3994..1e91493d 100644 --- a/src/Finbuckle.MultiTenant/StoreInfo.cs +++ b/src/Finbuckle.MultiTenant/StoreInfo.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; - namespace Finbuckle.MultiTenant; public class StoreInfo where TTenantInfo : class, ITenantInfo, new() diff --git a/src/Finbuckle.MultiTenant/Stores/ConfigurationStore/ConfigurationStore.cs b/src/Finbuckle.MultiTenant/Stores/ConfigurationStore/ConfigurationStore.cs index 50e500e4..30cd8e4a 100644 --- a/src/Finbuckle.MultiTenant/Stores/ConfigurationStore/ConfigurationStore.cs +++ b/src/Finbuckle.MultiTenant/Stores/ConfigurationStore/ConfigurationStore.cs @@ -1,11 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; diff --git a/src/Finbuckle.MultiTenant/Stores/DistributedCacheStore/DistributedCacheStore.cs b/src/Finbuckle.MultiTenant/Stores/DistributedCacheStore/DistributedCacheStore.cs index 6b8f5064..c271df01 100644 --- a/src/Finbuckle.MultiTenant/Stores/DistributedCacheStore/DistributedCacheStore.cs +++ b/src/Finbuckle.MultiTenant/Stores/DistributedCacheStore/DistributedCacheStore.cs @@ -2,10 +2,7 @@ // Refer to the solution LICENSE file for more information. using Microsoft.Extensions.Caching.Distributed; -using System; -using System.Collections.Generic; using System.Text.Json; -using System.Threading.Tasks; // ReSharper disable once CheckNamespace namespace Finbuckle.MultiTenant.Stores; diff --git a/src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStore.cs b/src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStore.cs index 658b0895..3ca82232 100644 --- a/src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStore.cs +++ b/src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStore.cs @@ -1,9 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Finbuckle.MultiTenant.Internal; // ReSharper disable once CheckNamespace diff --git a/src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStoreClient.cs b/src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStoreClient.cs index 008239e1..812cd61a 100644 --- a/src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStoreClient.cs +++ b/src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStoreClient.cs @@ -1,10 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Net.Http; using System.Text.Json; -using System.Threading.Tasks; // ReSharper disable once CheckNamespace namespace Finbuckle.MultiTenant.Stores; diff --git a/src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStore.cs b/src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStore.cs index 8d09c665..26d3d73e 100644 --- a/src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStore.cs +++ b/src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStore.cs @@ -1,11 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Options; // ReSharper disable once CheckNamespace diff --git a/src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStoreOptions.cs b/src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStoreOptions.cs index 1191fcb9..acbf63b0 100644 --- a/src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStoreOptions.cs +++ b/src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStoreOptions.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System.Collections.Generic; - namespace Finbuckle.MultiTenant.Stores; public class InMemoryStoreOptions diff --git a/src/Finbuckle.MultiTenant/Stores/MultiTenantStoreWrapper.cs b/src/Finbuckle.MultiTenant/Stores/MultiTenantStoreWrapper.cs index ce1ec90e..a020b07e 100644 --- a/src/Finbuckle.MultiTenant/Stores/MultiTenantStoreWrapper.cs +++ b/src/Finbuckle.MultiTenant/Stores/MultiTenantStoreWrapper.cs @@ -1,9 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Finbuckle.MultiTenant.Stores; diff --git a/src/Finbuckle.MultiTenant/Strategies/DelegateStrategy.cs b/src/Finbuckle.MultiTenant/Strategies/DelegateStrategy.cs index 9901fa76..d14a374f 100644 --- a/src/Finbuckle.MultiTenant/Strategies/DelegateStrategy.cs +++ b/src/Finbuckle.MultiTenant/Strategies/DelegateStrategy.cs @@ -1,9 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Threading.Tasks; - namespace Finbuckle.MultiTenant.Strategies; public class DelegateStrategy : IMultiTenantStrategy diff --git a/src/Finbuckle.MultiTenant/Strategies/MultiTenantStrategyWrapper.cs b/src/Finbuckle.MultiTenant/Strategies/MultiTenantStrategyWrapper.cs index fc2f949c..5175563e 100644 --- a/src/Finbuckle.MultiTenant/Strategies/MultiTenantStrategyWrapper.cs +++ b/src/Finbuckle.MultiTenant/Strategies/MultiTenantStrategyWrapper.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Finbuckle.MultiTenant.Strategies; diff --git a/src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs b/src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs index 88e3f71f..3c099e8a 100644 --- a/src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs +++ b/src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System.Threading.Tasks; - namespace Finbuckle.MultiTenant.Strategies; public class StaticStrategy : IMultiTenantStrategy diff --git a/src/Finbuckle.MultiTenant/StrategyInfo.cs b/src/Finbuckle.MultiTenant/StrategyInfo.cs index 9f18ea7e..dd7f2e6b 100644 --- a/src/Finbuckle.MultiTenant/StrategyInfo.cs +++ b/src/Finbuckle.MultiTenant/StrategyInfo.cs @@ -1,8 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; - namespace Finbuckle.MultiTenant; public class StrategyInfo diff --git a/src/Finbuckle.MultiTenant/TenantResolver.cs b/src/Finbuckle.MultiTenant/TenantResolver.cs index b5adf44c..095adaa8 100644 --- a/src/Finbuckle.MultiTenant/TenantResolver.cs +++ b/src/Finbuckle.MultiTenant/TenantResolver.cs @@ -1,10 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Finbuckle.MultiTenant.Stores; using Finbuckle.MultiTenant.Strategies; using Microsoft.Extensions.Logging; diff --git a/test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantAuthenticationSchemeProviderShould.cs b/test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantAuthenticationSchemeProviderShould.cs index c67353b6..773d2da5 100644 --- a/test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantAuthenticationSchemeProviderShould.cs +++ b/test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantAuthenticationSchemeProviderShould.cs @@ -14,58 +14,59 @@ namespace Finbuckle.MultiTenant.AspNetCore.Test { public class MultiTenantAuthenticationSchemeProviderShould { - private static IWebHostBuilder GetTestHostBuilder() + [Fact] + public async Task ReturnPerTenantAuthenticationOptions() { - return new WebHostBuilder() - .ConfigureServices(services => - { - services.AddAuthentication() - .AddCookie("tenant1Scheme") - .AddCookie("tenant2Scheme"); + // var hostBuilder = GetTestHostBuilder(); + // + // using (var server = new TestServer(hostBuilder)) + // { + // var client = server.CreateClient(); + // var response = await client.GetStringAsync("/tenant1"); + // Assert.Equal("tenant1Scheme", response); + // + // response = await client.GetStringAsync("/tenant2"); + // Assert.Equal("tenant2Scheme", response); + // } - services.AddMultiTenant() - .WithBasePathStrategy() - .WithPerTenantAuthentication() - .WithInMemoryStore() - .WithPerTenantOptions((ao, ti) => - { - ao.DefaultChallengeScheme = ti.Identifier + "Scheme"; - }); + var services = new ServiceCollection(); + services.AddAuthentication() + .AddCookie("tenant1Scheme") + .AddCookie("tenant2Scheme"); - services.AddMvc(); - }) - .Configure(app => - { - app.UseMultiTenant(); - app.Run(async context => - { - if (context.GetMultiTenantContext()?.TenantInfo != null) - { - var schemeProvider = context.RequestServices.GetRequiredService(); - await context.Response.WriteAsync((await schemeProvider.GetDefaultChallengeSchemeAsync())!.Name); - } - }); + services.AddMultiTenant() + .WithPerTenantAuthentication(); - var store = app.ApplicationServices.GetRequiredService>(); - store.TryAddAsync(new TenantInfo { Id = "tenant1", Identifier = "tenant1" }).Wait(); - store.TryAddAsync(new TenantInfo { Id = "tenant2", Identifier = "tenant2" }).Wait(); - }); - } + services.ConfigureAllPerTenant((ao, ti) => + { + ao.DefaultChallengeScheme = ti.Identifier + "Scheme"; + }); - [Fact] - public async Task ReturnPerTenantAuthenticationOptions() - { - var hostBuilder = GetTestHostBuilder(); + var sp = services.BuildServiceProvider(); - using (var server = new TestServer(hostBuilder)) - { - var client = server.CreateClient(); - var response = await client.GetStringAsync("/tenant1"); - Assert.Equal("tenant1Scheme", response); + var tenant1 = new TenantInfo{ + Id = "tenant1", + Identifier = "tenant1" + }; + + var tenant2 = new TenantInfo{ + Id = "tenant2", + Identifier = "tenant2" + }; + + var mtc = new MultiTenantContext(); + var multiTenantContextAccessor = sp.GetRequiredService>(); + multiTenantContextAccessor.MultiTenantContext = mtc; + + mtc.TenantInfo = tenant1; + var schemeProvider = sp.GetRequiredService(); + + var option = schemeProvider.GetDefaultChallengeSchemeAsync().Result; + Assert.Equal("tenant1Scheme", option?.Name); - response = await client.GetStringAsync("/tenant2"); - Assert.Equal("tenant2Scheme", response); - } + mtc.TenantInfo = tenant2; + option = schemeProvider.GetDefaultChallengeSchemeAsync().Result; + Assert.Equal("tenant2Scheme", option?.Name); } } } \ No newline at end of file diff --git a/test/Finbuckle.MultiTenant.Test/Extensions/MultiTenantBuilderExtensionsShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderExtensionsShould.cs similarity index 99% rename from test/Finbuckle.MultiTenant.Test/Extensions/MultiTenantBuilderExtensionsShould.cs rename to test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderExtensionsShould.cs index 943212dc..eba34e89 100644 --- a/test/Finbuckle.MultiTenant.Test/Extensions/MultiTenantBuilderExtensionsShould.cs +++ b/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderExtensionsShould.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Finbuckle.MultiTenant.Test.Extensions +namespace Finbuckle.MultiTenant.Test.DependencyInjection { public class MultiTenantBuilderExtensionsShould { diff --git a/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderShould.cs index 5dba6742..7cea362f 100644 --- a/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderShould.cs +++ b/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderShould.cs @@ -17,8 +17,8 @@ public class MultiTenantBuilderShould { // Used in some tests. // ReSharper disable once UnusedAutoPropertyAccessor.Local - private int TestProperty { get; set; } - + private string? TestProperty { get; set; } + [Theory] [InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped)] @@ -129,44 +129,8 @@ public void ThrowIfNullFactoryAddingCustomStore() { var services = new ServiceCollection(); var builder = new FinbuckleMultiTenantBuilder(services); - Assert.Throws(() => builder.WithStore>(ServiceLifetime.Singleton, factory: null!)); - } - - [Fact] - public void AddPerTenantOptions() - { - var services = new ServiceCollection(); - var accessor = new Mock>(); - accessor.Setup(a => a.MultiTenantContext).Returns((IMultiTenantContext?)null); - services.AddSingleton(accessor.Object); - var builder = new FinbuckleMultiTenantBuilder(services); - // Note: using MultiTenantBuilderShould as our test options class. - builder.WithPerTenantOptions((o, _) => o.TestProperty = 1); - var sp = services.BuildServiceProvider(); - - sp.GetRequiredService>(); - } - - [Fact] - public void AddPerTenantNamedOptions() - { - var services = new ServiceCollection(); - var accessor = new Mock>(); - accessor.Setup(a => a.MultiTenantContext).Returns((IMultiTenantContext?)null); - services.AddSingleton(accessor.Object); - var builder = new FinbuckleMultiTenantBuilder(services); - // Note: using MultiTenantBuilderShould as our test options class. - builder.WithPerTenantNamedOptions("a name", (o, _) => o.TestProperty = 1); - var sp = services.BuildServiceProvider(); - sp.GetRequiredService>(); - } - - [Fact] - public void ThrowIfNullParamAddingPerTenantOptions() - { - var services = new ServiceCollection(); - var builder = new FinbuckleMultiTenantBuilder(services); - Assert.Throws(() => builder.WithPerTenantOptions(null!)); + Assert.Throws(() => + builder.WithStore>(ServiceLifetime.Singleton, factory: null!)); } [Theory] @@ -200,6 +164,8 @@ public void AddCustomStrategyWithDefaultCtorAndLifetime(ServiceLifetime lifetime strategy = scope.ServiceProvider.GetRequiredService(); Assert.NotSame(strategy, strategy2); break; + default: + throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null); } } @@ -276,7 +242,8 @@ public void ThrowIfNullFactoryAddingCustomStrategy() { var services = new ServiceCollection(); var builder = new FinbuckleMultiTenantBuilder(services); - Assert.Throws(() => builder.WithStrategy(ServiceLifetime.Singleton, factory: null!)); + Assert.Throws(() => + builder.WithStrategy(ServiceLifetime.Singleton, factory: null!)); } private class TestStore : IMultiTenantStore diff --git a/test/Finbuckle.MultiTenant.Test/Extensions/ServiceCollectionExtensionsShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionExtensionsShould.cs similarity index 60% rename from test/Finbuckle.MultiTenant.Test/Extensions/ServiceCollectionExtensionsShould.cs rename to test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionExtensionsShould.cs index 81e5ac9a..28b7ab8d 100644 --- a/test/Finbuckle.MultiTenant.Test/Extensions/ServiceCollectionExtensionsShould.cs +++ b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionExtensionsShould.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Finbuckle.MultiTenant.Test.Extensions +namespace Finbuckle.MultiTenant.Test.DependencyInjection { public class ServiceCollectionExtensionsShould { @@ -93,7 +93,8 @@ public void RegisterIMultiTenantContextAccessorGenericInDi() services.AddMultiTenant(); var service = services.SingleOrDefault(s => s.Lifetime == ServiceLifetime.Singleton && - s.ServiceType == typeof(IMultiTenantContextAccessor)); + s.ServiceType == + typeof(IMultiTenantContextAccessor)); Assert.NotNull(service); Assert.Equal(ServiceLifetime.Singleton, service!.Lifetime); @@ -111,5 +112,57 @@ public void RegisterMultiTenantOptionsInDi() Assert.NotNull(service); Assert.Equal(ServiceLifetime.Singleton, service!.Lifetime); } + + public class TestOptions + { + public string? Prop1 { get; set; } + } + + [Fact] + public void RegisterNamedOptionsPerTenant() + { + var services = new ServiceCollection(); + services.AddMultiTenant(); + services.ConfigurePerTenant("name1", + (option, tenant) => option.Prop1 = tenant.Id); + var sp = services.BuildServiceProvider(); + + var configs = sp.GetRequiredService>>(); + var config = configs.Where(config => config is ConfigureNamedOptions> options).ToList(); + + Assert.Single(config); + Assert.Equal("name1", config.Select(c => (ConfigureNamedOptions>)c).Single().Name); + } + + [Fact] + public void RegisterUnnamedOptionsPerTenant() + { + var services = new ServiceCollection(); + services.AddMultiTenant(); + services.ConfigurePerTenant((option, tenant) => option.Prop1 = tenant.Id); + var sp = services.BuildServiceProvider(); + + var configs = sp.GetRequiredService>>(); + var config = configs.Where(config => config is ConfigureNamedOptions> options).ToList(); + + Assert.Single(config); + Assert.Equal(Microsoft.Extensions.Options.Options.DefaultName, + config.Select(c => (ConfigureNamedOptions>)c).Single().Name); + } + + [Fact] + public void RegisterAllOptionsPerTenant() + { + var services = new ServiceCollection(); + services.AddMultiTenant(); + services.ConfigureAllPerTenant((option, tenant) => option.Prop1 = tenant.Id); + var sp = services.BuildServiceProvider(); + + var configs = sp.GetRequiredService>>(); + var config = configs.Where(config => config is ConfigureNamedOptions> options).ToList(); + + Assert.Single(config); + Assert.Null(config.Select(c => (ConfigureNamedOptions>)c).Single().Name); + } } } \ No newline at end of file diff --git a/test/Finbuckle.MultiTenant.Test/Finbuckle.MultiTenant.Test.csproj b/test/Finbuckle.MultiTenant.Test/Finbuckle.MultiTenant.Test.csproj index c72a51bc..fe1582d3 100644 --- a/test/Finbuckle.MultiTenant.Test/Finbuckle.MultiTenant.Test.csproj +++ b/test/Finbuckle.MultiTenant.Test/Finbuckle.MultiTenant.Test.csproj @@ -3,6 +3,7 @@ net8.0;net7.0;net6.0 false enable + true @@ -38,4 +39,8 @@ PreserveNewest + + + + diff --git a/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsCacheShould.cs b/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsCacheShould.cs index ca313294..99141713 100644 --- a/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsCacheShould.cs +++ b/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsCacheShould.cs @@ -1,8 +1,8 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; using System.Collections.Concurrent; +using System.ComponentModel.DataAnnotations; using System.Reflection; using Finbuckle.MultiTenant.Internal; using Finbuckle.MultiTenant.Options; @@ -13,6 +13,12 @@ namespace Finbuckle.MultiTenant.Test.Options { public class MultiTenantOptionsCacheShould { + internal class TestOptions + { + [Required] + public string? DefaultConnectionString { get; set; } + } + [Theory] [InlineData("")] [InlineData(null)] @@ -24,7 +30,7 @@ public void AddNamedOptionsForCurrentTenantOnlyOnAdd(string name) tc.TenantInfo = ti; var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); @@ -46,7 +52,7 @@ public void AddNamedOptionsForCurrentTenantOnlyOnAdd(string name) public void HandleNullMultiTenantContextOnAdd() { var tca = new AsyncLocalMultiTenantContextAccessor(); - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); @@ -59,7 +65,7 @@ public void HandleNullMultiTenantContextOnAdd() public void HandleNullMultiTenantContextOnGetOrAdd() { var tca = new AsyncLocalMultiTenantContextAccessor(); - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); @@ -79,7 +85,7 @@ public void GetOrAddNamedOptionForCurrentTenantOnly(string name) tc.TenantInfo = ti; var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); var options2 = new TestOptions(); @@ -104,7 +110,7 @@ public void ThrowsIfGetOrAddFactoryIsNull() var tc = new MultiTenantContext(); var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); Assert.Throws(() => cache.GetOrAdd("", null!)); } @@ -116,21 +122,21 @@ public void ThrowIfConstructorParamIsNull() var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; - Assert.Throws(() => new MultiTenantOptionsCache(null!)); + Assert.Throws(() => new MultiTenantOptionsCache(null!)); } [Theory] [InlineData("")] [InlineData(null)] [InlineData("name")] - public void RemoveNamedOptionsForCurrentTenantOnly(string name) + public void RemoveNamedOptionsForCurrentTenantOnly(string? name) { var ti = new TenantInfo { Id = "test-id-123" }; var tc = new MultiTenantContext(); tc.TenantInfo = ti; var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); @@ -156,14 +162,14 @@ public void RemoveNamedOptionsForCurrentTenantOnly(string name) .GetValue(tenantCache[ti.Id]); // Assert named options removed and other options on tenant left as-is. - Assert.False(tenantInternalCache!.Keys.Contains(name ?? "")); + Assert.False(tenantInternalCache!.Keys.Contains(name)); Assert.True(tenantInternalCache.Keys.Contains("diffName")); // Assert other tenant not affected. ti.Id = "test-id-123"; tenantInternalCache = tenantCache?[ti.Id].GetType().GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance)? .GetValue(tenantCache[ti.Id]); - Assert.True(tenantInternalCache!.ContainsKey(name ?? "")); + Assert.True(tenantInternalCache!.ContainsKey(name ?? Microsoft.Extensions.Options.Options.DefaultName)); } [Fact] @@ -174,7 +180,7 @@ public void ClearOptionsForCurrentTenantOnly() tc.TenantInfo = ti; var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); @@ -215,7 +221,7 @@ public void ClearOptionsForTenantIdOnly() tc.TenantInfo = ti; var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); @@ -254,7 +260,7 @@ public void ClearAllOptionsForClearAll() tc.TenantInfo = ti; var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; - var cache = new MultiTenantOptionsCache(tca); + var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); diff --git a/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsFactoryShould.cs b/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsFactoryShould.cs deleted file mode 100644 index be86edb5..00000000 --- a/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsFactoryShould.cs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Finbuckle.MultiTenant.Test.Options -{ - public class MultiTenantOptionsFactoryShould - { - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("name")] - public void CreateOptionsWithTenantAction(string name) - { - var services = new ServiceCollection(); - services.AddOptions(); - services.Configure(name, o => o.DefaultConnectionString = $"{name}_begin"); - services.PostConfigure(name, o => o.DefaultConnectionString += "end"); - services.AddMultiTenant() - .WithPerTenantOptions((o, ti) => o.DefaultConnectionString += $"_{ti.Id}_"); - var sp = services.BuildServiceProvider(); - var accessor = sp.GetRequiredService>(); - accessor.MultiTenantContext = new MultiTenantContext - { TenantInfo = new TenantInfo { Id = "test-id-123" } }; - - var options = sp.GetRequiredService>().Get(name); - Assert.Equal($"{name}_begin_{accessor.MultiTenantContext.TenantInfo!.Id}_end", - options.DefaultConnectionString); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("name")] - public void CreateMultipleOptionsWithTenantAction(string name) - { - var services = new ServiceCollection(); - services.AddOptions(); - services.Configure(name, o => o.DefaultConnectionString = $"{name}_begin"); - services.PostConfigure(name, o => o.DefaultConnectionString += "end"); - services.AddMultiTenant() - .WithPerTenantOptions((o, ti) => o.DefaultConnectionString += $"_{ti.Id}") - .WithPerTenantOptions((o, ti) => o.DefaultConnectionString += $"_{ti.Identifier}_"); - var sp = services.BuildServiceProvider(); - var accessor = sp.GetRequiredService>(); - accessor.MultiTenantContext = new MultiTenantContext - { TenantInfo = new TenantInfo { Id = "id", Identifier = "identifier" } }; - - var options = sp.GetRequiredService>().Get(name); - Assert.Equal( - $"{name}_begin_{accessor.MultiTenantContext.TenantInfo!.Id}_{accessor.MultiTenantContext.TenantInfo.Identifier}_end", - options.DefaultConnectionString); - } - - [Theory] - [InlineData("", "name2")] - [InlineData("name1", "name2")] - [InlineData("name1", "")] - public void CreateMultipleNamedOptionsWithTenantAction(string name1, string name2) - { - var services = new ServiceCollection(); - services.AddOptions(); - services.Configure(name1, o => o.DefaultConnectionString = $"{name1}_begin"); - services.Configure(name2, o => o.DefaultConnectionString = $"{name2}_begin"); - services.PostConfigure(name1, o => o.DefaultConnectionString += "end"); - services.PostConfigure(name2, o => o.DefaultConnectionString += "end"); - services.AddMultiTenant() - //configure non-named options - .WithPerTenantOptions((o, ti) => o.DefaultConnectionString += "_noname") - //configure named options - .WithPerTenantNamedOptions(name1, - (o, ti) => o.DefaultConnectionString += $"_{name1}_") - .WithPerTenantNamedOptions(name2, - (o, ti) => o.DefaultConnectionString += $"_{name2}_"); - var sp = services.BuildServiceProvider(); - var accessor = sp.GetRequiredService>(); - accessor.MultiTenantContext = new MultiTenantContext - { TenantInfo = new TenantInfo { Id = "id", Identifier = "identifier" } }; - - var options1 = sp.GetRequiredService>().Get(name1); - var expectedName1 = !string.IsNullOrEmpty(name1) ? name1 : Microsoft.Extensions.Options.Options.DefaultName; - Assert.Equal( - $"{expectedName1}_begin_noname_{expectedName1}_end", - options1.DefaultConnectionString); - - var options2 = sp.GetRequiredService>().Get(name2); - var expectedName2 = !string.IsNullOrEmpty(name2) ? name2 : Microsoft.Extensions.Options.Options.DefaultName; - Assert.Equal( - $"{expectedName2}_begin_noname_{expectedName2}_end", - options2.DefaultConnectionString); - } - - [Fact] - public void IgnoreNullTenantInfo() - { - var services = new ServiceCollection(); - services.Configure(o => o.DefaultConnectionString = "begin"); - services.PostConfigure(o => o.DefaultConnectionString += "End"); - services.AddMultiTenant() - .WithPerTenantOptions((o, ti) => o.DefaultConnectionString += $"_{ti.Id}_"); - var sp = services.BuildServiceProvider(); - var accessor = sp.GetRequiredService>(); - accessor.MultiTenantContext = new MultiTenantContext(); - - var options = sp.GetRequiredService>().Value; - Assert.Equal($"beginEnd", options.DefaultConnectionString); - } - - [Fact] - public void IgnoreNullMultiTenantContext() - { - var services = new ServiceCollection(); - services.Configure(o => o.DefaultConnectionString = "begin"); - services.PostConfigure(o => o.DefaultConnectionString += "End"); - services.AddMultiTenant() - .WithPerTenantOptions((o, ti) => o.DefaultConnectionString += $"_{ti.Id}_"); - var sp = services.BuildServiceProvider(); - var accessor = sp.GetRequiredService>(); - accessor.MultiTenantContext = null; - - var options = sp.GetRequiredService>().Value; - Assert.Equal($"beginEnd", options.DefaultConnectionString); - } - - [Fact] - public void ValidateOptions() - { - var services = new ServiceCollection(); - services.AddOptions() - .Configure(o => o.DefaultConnectionString = "begin") - .ValidateDataAnnotations(); - services.AddMultiTenant() - .WithPerTenantOptions((o, ti) => o.DefaultConnectionString = null); - var sp = services.BuildServiceProvider(); - var accessor = sp.GetRequiredService>(); - accessor.MultiTenantContext = new MultiTenantContext - { TenantInfo = new TenantInfo { Id = "id", Identifier = "identifier" } }; - - Assert.Throws( - () => sp.GetRequiredService>().Value); - } - - [Fact] - public void ValidateNamedOptions() - { - var services = new ServiceCollection(); - services.AddOptions("a name") - .Configure(o => o.DefaultConnectionString = "begin") - .ValidateDataAnnotations(); - services.AddMultiTenant() - .WithPerTenantNamedOptions("a name", (o, ti) => o.DefaultConnectionString = null); - var sp = services.BuildServiceProvider(); - var accessor = sp.GetRequiredService>(); - accessor.MultiTenantContext = new MultiTenantContext - { TenantInfo = new TenantInfo { Id = "id", Identifier = "identifier" } }; - - Assert.Throws( - () => sp.GetRequiredService>().Get("a name")); - } - } -} \ No newline at end of file diff --git a/test/Finbuckle.MultiTenant.Test/Options/TestOptions.cs b/test/Finbuckle.MultiTenant.Test/Options/TestOptions.cs deleted file mode 100644 index a3fdc944..00000000 --- a/test/Finbuckle.MultiTenant.Test/Options/TestOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -using System.ComponentModel.DataAnnotations; - -namespace Finbuckle.MultiTenant.Test.Options -{ - internal class TestOptions - { - [Required] - public string? DefaultConnectionString { get; set; } - } -} \ No newline at end of file