From 7e8c0a064080c3ad3ced49186634d805b06c4669 Mon Sep 17 00:00:00 2001 From: Sam G Date: Sat, 12 Aug 2023 14:54:32 -0400 Subject: [PATCH 01/10] Options builder support (#672) * Implement support for OptionsBuilder * Reverted changes to TenantConfigureNamedOptions * Restored commented test * Removed dependencies --------- Co-authored-by: Sam Goldmann --- .../DependencyInjection/MultiTenantBuilder.cs | 26 +---- .../Extensions/ServiceCollectionExtensions.cs | 49 ++++++++ .../ITenantConfigureNamedOptionsWrapper.cs | 11 ++ .../Options/ITenantConfigureOptions.cs | 3 +- .../Options/MultiTenantOptionsCache.cs | 107 +++++++++++++++++- .../Options/MultiTenantOptionsFactory.cs | 95 +++++++++++++++- .../TenantConfigureNamedOptionsWrapper.cs | 55 +++++++++ .../ServiceCollectionShould.cs | 35 ++++++ 8 files changed, 357 insertions(+), 24 deletions(-) create mode 100644 src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs create mode 100644 src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs create mode 100644 test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs index 6dc6e1c2..8637e9f0 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs @@ -40,7 +40,7 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) Action tenantConfigureOptions) where TOptions : class, new() { // 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); } /// @@ -58,32 +58,16 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) 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); + Services.AddPerTenantOptionsCore(); + Services.TryAddEnumerable(ServiceDescriptor.Scoped, TenantConfigureNamedOptionsWrapper>()); + Services.AddScoped>(sp => new TenantConfigureNamedOptions(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. diff --git a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs b/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs index 62b1c35a..a8db44ad 100644 --- a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs +++ b/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,9 @@ using System.Linq; 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; @@ -55,6 +58,29 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceColl return services.AddMultiTenant(_ => { }); } + /// + /// Gets an options builder that forwards Configure calls for the same named per-tenant to the underlying service collection. + /// + /// The options type to be configured. + /// The to add the services to. + /// The name of the options instance. + /// The so that configure calls can be chained in it. + public static OptionsBuilder AddPerTenantOptions(this IServiceCollection services, string? name) where TOptions : class, new() + { + + services.AddPerTenantOptionsCore(); + return new OptionsBuilder(services, name); + } + + /// + /// Gets an options builder that forwards Configure calls for the same per-tenant to the underlying service collection. + /// + /// The options type to be configured. + /// The to add the services to. + /// The so that configure calls can be chained in it. + public static OptionsBuilder AddPerTenantOptions(this IServiceCollection services) where TOptions : class, new() => + services.AddPerTenantOptions(Options.Options.DefaultName); + public static bool DecorateService(this IServiceCollection services, params object[] parameters) { var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService)); @@ -118,4 +144,27 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceColl return true; } + + internal static void AddPerTenantOptionsCore(this IServiceCollection services) where TOptions : class, new() + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + // Handles multiplexing cached options. + services.TryAddSingleton, MultiTenantOptionsCache>(); + services.TryAddTransient, MultiTenantOptionsFactory>(); + services.TryAddScoped>(BuildOptionsManager); + services.TryAddSingleton>(BuildOptionsManager); + } + + 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); + } } \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs b/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs new file mode 100644 index 00000000..d5749a11 --- /dev/null +++ b/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs @@ -0,0 +1,11 @@ +// Copyright Finbuckle LLC, Andrew White, and Contributors. +// Refer to the solution LICENSE file for more information. + +using Microsoft.Extensions.Options; + +namespace Finbuckle.MultiTenant.Options; + +interface ITenantConfigureNamedOptionsWrapper : IConfigureNamedOptions + where TOptions : class, new() +{ +} diff --git a/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs b/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs index 4215fb70..04f684d9 100644 --- a/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs +++ b/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs @@ -1,6 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. +using Microsoft.Extensions.Options; using System; namespace Finbuckle.MultiTenant.Options; @@ -11,4 +12,4 @@ public interface ITenantConfigureOptions 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..66daa3e8 100644 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs +++ b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs @@ -7,6 +7,111 @@ namespace Finbuckle.MultiTenant.Options; +/// +/// Adds, retrieves, and removes instances of TOptions after adjusting them for the current TenantContext. +/// +public class MultiTenantOptionsCache : IOptionsMonitorCache + where TOptions : class +{ + 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>(); + + /// + /// Constructs a new instance of MultiTenantOptionsCache. + /// + /// + /// + public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor) + { + this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor)); + } + + /// + /// Clears all cached options for the current tenant. + /// + public void Clear() + { + var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; + var cache = map.GetOrAdd(tenantId, new OptionsCache()); + + cache.Clear(); + } + + /// + /// Clears all cached options for the given tenant. + /// + /// The Id of the tenant which will have its options cleared. + public void Clear(string tenantId) + { + var cache = map.GetOrAdd(tenantId, new OptionsCache()); + + cache.Clear(); + } + + /// + /// Clears all cached options for all tenants and no tenant. + /// + public void ClearAll() + { + foreach (var cache in map.Values) + cache.Clear(); + } + + /// + /// Gets a named options instance for the current tenant, or adds a new instance created with createOptions. + /// + /// The options name. + /// The factory function for creating the options instance. + /// The existing or new options instance. + public TOptions GetOrAdd(string? name, Func createOptions) + { + if (createOptions == null) + { + throw new ArgumentNullException(nameof(createOptions)); + } + + name = name ?? Microsoft.Extensions.Options.Options.DefaultName; + var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; + var cache = map.GetOrAdd(tenantId, new OptionsCache()); + + return cache.GetOrAdd(name, createOptions); + } + + /// + /// Tries to adds a new option to the cache for the current tenant. + /// + /// The options name. + /// The options instance. + /// True if the options was added to the cache for the current tenant. + public bool TryAdd(string? name, TOptions options) + { + name = name ?? Microsoft.Extensions.Options.Options.DefaultName; + var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; + var cache = map.GetOrAdd(tenantId, new OptionsCache()); + + return cache.TryAdd(name, options); + } + + /// + /// Try to remove an options instance for the current tenant. + /// + /// The options name. + /// True if the options was removed from the cache for the current tenant. + public bool TryRemove(string? name) + { + name = name ?? Microsoft.Extensions.Options.Options.DefaultName; + var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; + var cache = map.GetOrAdd(tenantId, new OptionsCache()); + + return cache.TryRemove(name); + } +} + /// /// Adds, retrieves, and removes instances of TOptions after adjusting them for the current TenantContext. /// @@ -59,7 +164,7 @@ public void Clear(string tenantId) /// public void ClearAll() { - foreach(var cache in map.Values) + foreach (var cache in map.Values) cache.Clear(); } diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs index d73c2c7e..042ab843 100644 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs +++ b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs @@ -4,16 +4,103 @@ // 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; using System.Collections.Generic; using Microsoft.Extensions.Options; namespace Finbuckle.MultiTenant.Options; +/// +/// Implementation of IOptionsFactory. +/// +/// The type of options being requested. +public class MultiTenantOptionsFactory : IOptionsFactory + where TOptions : class, new() +{ + private readonly IConfigureOptions[] _configureOptions; + private readonly IPostConfigureOptions[] _postConfigureOptions; + private readonly IValidateOptions[] _validations; + + /// + /// Initializes a new instance with the specified options configurations. + /// + public MultiTenantOptionsFactory( + IEnumerable> configureOptions, + IEnumerable> postConfigureOptions, + IEnumerable> validations) + { + // 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(); + } + + /// + public TOptions Create(string name) + { + ITenantConfigureNamedOptionsWrapper? tenantConfigureNamedOptionsWrapper = null; + var options = new TOptions(); + foreach (var setup in _configureOptions) + { + // consider directly injecting this to avoid a conditional. + if (setup is ITenantConfigureNamedOptionsWrapper wrapper) + { + tenantConfigureNamedOptionsWrapper = wrapper; + } + else if (setup is IConfigureNamedOptions namedSetup) + { + namedSetup.Configure(name, options); + } + else if (name == Microsoft.Extensions.Options.Options.DefaultName) + { + setup.Configure(options); + } + } + + tenantConfigureNamedOptionsWrapper?.Configure(name, options); + + 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; + } +} + /// /// Implementation of IOptionsFactory. /// /// The type of options being requested. /// The type of the tenant info. +[Obsolete] public class MultiTenantOptionsFactory : IOptionsFactory where TOptions : class, new() where TTenantInfo : class, ITenantInfo, new() @@ -22,7 +109,9 @@ public class MultiTenantOptionsFactory : IOptionsFactory< private readonly IPostConfigureOptions[] _postConfigureOptions; private readonly IValidateOptions[] _validations; +#pragma warning disable CS0612 // Type or member is obsolete private readonly ITenantConfigureOptions[] _tenantConfigureOptions; +#pragma warning restore CS0612 // Type or member is obsolete private readonly ITenantConfigureNamedOptions[] _tenantConfigureNamedOptions; private readonly IMultiTenantContextAccessor _multiTenantContextAccessor; @@ -32,7 +121,9 @@ public class MultiTenantOptionsFactory : IOptionsFactory< public MultiTenantOptionsFactory(IEnumerable> configureOptions, IEnumerable> postConfigureOptions, IEnumerable> validations, +#pragma warning disable CS0612 // Type or member is obsolete IEnumerable> tenantConfigureOptions, +#pragma warning restore CS0612 // Type or member is obsolete IEnumerable> tenantConfigureNamedOptions, IMultiTenantContextAccessor multiTenantContextAccessor) { @@ -47,9 +138,11 @@ public class MultiTenantOptionsFactory : IOptionsFactory< new List>(postConfigureOptions).ToArray(); _validations = validations as IValidateOptions[] ?? new List>(validations).ToArray(); +#pragma warning disable CS0612 // Type or member is obsolete _tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions[] ?? new List>(tenantConfigureOptions) .ToArray(); +#pragma warning restore CS0612 // Type or member is obsolete _tenantConfigureNamedOptions = tenantConfigureNamedOptions as ITenantConfigureNamedOptions[] ?? new List>(tenantConfigureNamedOptions).ToArray(); @@ -88,7 +181,7 @@ public TOptions Create(string name) { post.PostConfigure(name, options); } - + // TODO consider per tenant post configure if (_validations.Length > 0) diff --git a/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs b/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs new file mode 100644 index 00000000..baba7df2 --- /dev/null +++ b/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs @@ -0,0 +1,55 @@ +// Copyright Finbuckle LLC, Andrew White, and Contributors. +// Refer to the solution LICENSE file for more information. + +using System.Collections.Generic; + +namespace Finbuckle.MultiTenant.Options; + +class TenantConfigureNamedOptionsWrapper : ITenantConfigureNamedOptionsWrapper + where TOptions : class, new() + where TTenantInfo : class, ITenantInfo, new() +{ + private readonly IMultiTenantContextAccessor multiTenantContextAccessor; + private readonly ITenantConfigureOptions[] tenantConfigureOptions; + private readonly ITenantConfigureNamedOptions[] tenantConfigureNamedOptions; + + public TenantConfigureNamedOptionsWrapper( + IMultiTenantContextAccessor multiTenantContextAccessor, + IEnumerable> tenantConfigureOptions, + IEnumerable> tenantConfigureNamedOptions) + { + this.multiTenantContextAccessor = multiTenantContextAccessor; + this.tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions[] ?? + new List>(tenantConfigureOptions).ToArray(); + this.tenantConfigureNamedOptions = tenantConfigureNamedOptions as ITenantConfigureNamedOptions[] ?? + new List>(tenantConfigureNamedOptions).ToArray(); + } + + public void Configure(string name, TOptions 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!); + } + } + + public void Configure(TOptions 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(Microsoft.Extensions.Options.Options.DefaultName, options, + multiTenantContextAccessor.MultiTenantContext.TenantInfo!); + } + } +} diff --git a/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs new file mode 100644 index 00000000..9f4b7606 --- /dev/null +++ b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs @@ -0,0 +1,35 @@ +// Copyright Finbuckle LLC, Andrew White, and Co ntributors. +// Refer to the solution LICENSE file for more information. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Finbuckle.MultiTenant.Test.DependencyInjection +{ + public class ServiceCollectionShould + { + // Used in some tests. + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private string? TestProperty { get; set; } + + [Fact] + public void AddPerTenantOptions() + { + var services = new ServiceCollection(); + _ = services.AddMultiTenant(); + + services.AddPerTenantOptions() + .Configure((o, ti) => o.TestProperty = ti.Id); + + var sp = services.BuildServiceProvider(); + var multiTenantContextAccessor = sp.GetRequiredService>(); + multiTenantContextAccessor.MultiTenantContext = new MultiTenantContext{ TenantInfo = new TenantInfo { Id = "initech" } }; + + using var scope = sp.CreateScope(); + var options = scope.ServiceProvider.GetRequiredService>(); + + Assert.Equal("initech", options.Value.TestProperty); + } + } +} \ No newline at end of file From 975f7a748151962a9a4a3b0de833454986d75f2f Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 14 Nov 2023 12:18:09 -0700 Subject: [PATCH 02/10] partial --- .../DependencyInjection/MultiTenantBuilder.cs | 12 ++-- .../Extensions/ServiceCollectionExtensions.cs | 59 +++++++++---------- .../Options/ITenantConfigureNamedOptions.cs | 2 +- .../ITenantConfigureNamedOptionsWrapper.cs | 4 +- .../Options/ITenantConfigureOptions.cs | 2 +- .../Options/MultiTenantOptionsFactory.cs | 4 +- .../Options/MultiTenantOptionsManager.cs | 2 +- .../Options/TenantConfigureNamedOptions.cs | 2 +- .../TenantConfigureNamedOptionsWrapper.cs | 2 +- .../MultiTenantBuilderShould.cs | 31 +++++++--- .../ServiceCollectionShould.cs | 36 +++++------ 11 files changed, 88 insertions(+), 68 deletions(-) diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs index 8637e9f0..ec5dc5ae 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs @@ -57,17 +57,19 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) { throw new ArgumentNullException(nameof(tenantConfigureNamedOptions)); } - - Services.AddPerTenantOptionsCore(); - Services.TryAddEnumerable(ServiceDescriptor.Scoped, TenantConfigureNamedOptionsWrapper>()); - Services.AddScoped>(sp => new TenantConfigureNamedOptions(name, tenantConfigureNamedOptions)); + + // Services.AddOptionsCore(); + Services.TryAddEnumerable(ServiceDescriptor + .Scoped, TenantConfigureNamedOptionsWrapper>()); + Services.AddScoped>(sp => + new TenantConfigureNamedOptions(name, tenantConfigureNamedOptions)); return this; } // TODO consider per tenant AllOptions variation // TODO consider per-tenant post options - + /// /// Adds and configures an IMultiTenantStore to the application using default dependency injection. diff --git a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs b/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs index a8db44ad..47e14fb2 100644 --- a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs +++ b/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs @@ -38,6 +38,7 @@ public static class FinbuckleServiceCollectionExtensions sp.GetRequiredService>().MultiTenantContext?.TenantInfo!); services.AddScoped(sp => sp.GetService()!); + // TODO this might require instance services.AddSingleton, AsyncLocalMultiTenantContextAccessor>(); services.AddSingleton(sp => (IMultiTenantContextAccessor)sp.GetRequiredService>()); @@ -58,29 +59,6 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceColl return services.AddMultiTenant(_ => { }); } - /// - /// Gets an options builder that forwards Configure calls for the same named per-tenant to the underlying service collection. - /// - /// The options type to be configured. - /// The to add the services to. - /// The name of the options instance. - /// The so that configure calls can be chained in it. - public static OptionsBuilder AddPerTenantOptions(this IServiceCollection services, string? name) where TOptions : class, new() - { - - services.AddPerTenantOptionsCore(); - return new OptionsBuilder(services, name); - } - - /// - /// Gets an options builder that forwards Configure calls for the same per-tenant to the underlying service collection. - /// - /// The options type to be configured. - /// The to add the services to. - /// The so that configure calls can be chained in it. - public static OptionsBuilder AddPerTenantOptions(this IServiceCollection services) where TOptions : class, new() => - services.AddPerTenantOptions(Options.Options.DefaultName); - public static bool DecorateService(this IServiceCollection services, params object[] parameters) { var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService)); @@ -145,22 +123,43 @@ public static OptionsBuilder AddPerTenantOptions(this IServi return true; } - internal static void AddPerTenantOptionsCore(this IServiceCollection services) where TOptions : class, new() + /// + /// Registers an action used to configure a particular type of options. + /// Note: These are run before all . + /// + /// The options type to be configured. + /// 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 config) where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + // Required infrastructure. + services.TryAddSingleton, MultiTenantOptionsCache>(); + services.TryAddTransient, MultiTenantOptionsFactory>(); + services.TryAddScoped>(BuildOptionsManager); + services.TryAddSingleton>(BuildOptionsManager); + services.ConfigureAll() + services.AddSingleton>(sp => + ActivatorUtilities.CreateInstance>(sp)); + + return services; + } + + private static void AddOptionsPerTenantCore(this IServiceCollection services) where TOptions : class { if (services == null) { throw new ArgumentNullException(nameof(services)); } - // Handles multiplexing cached options. - services.TryAddSingleton, MultiTenantOptionsCache>(); - services.TryAddTransient, MultiTenantOptionsFactory>(); - services.TryAddScoped>(BuildOptionsManager); - services.TryAddSingleton>(BuildOptionsManager); + } private static MultiTenantOptionsManager BuildOptionsManager(IServiceProvider sp) - where TOptions : class, new() + where TOptions : class { var cache = (IOptionsMonitorCache)ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsCache)); diff --git a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptions.cs b/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptions.cs index e264f89a..21aea397 100644 --- a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptions.cs +++ b/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptions.cs @@ -9,7 +9,7 @@ namespace Finbuckle.MultiTenant.Options; /// Options type being configured. /// A type implementing ITenantInfo. public interface ITenantConfigureNamedOptions - where TOptions : class, new() + where TOptions : class where TTenantInfo : class, ITenantInfo, new() { /// diff --git a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs b/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs index d5749a11..f0eb4723 100644 --- a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs +++ b/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs @@ -1,11 +1,13 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. +using System; using Microsoft.Extensions.Options; namespace Finbuckle.MultiTenant.Options; +[Obsolete] interface ITenantConfigureNamedOptionsWrapper : IConfigureNamedOptions - where TOptions : class, new() + where TOptions : class { } diff --git a/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs b/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs index 04f684d9..17749838 100644 --- a/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs +++ b/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs @@ -8,7 +8,7 @@ namespace Finbuckle.MultiTenant.Options; [Obsolete] public interface ITenantConfigureOptions - where TOptions : class, new() + where TOptions : class where TTenantInfo : class, ITenantInfo, new() { void Configure(TOptions options, TTenantInfo tenantInfo); diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs index 042ab843..7bdd861a 100644 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs +++ b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs @@ -15,7 +15,7 @@ namespace Finbuckle.MultiTenant.Options; /// /// The type of options being requested. public class MultiTenantOptionsFactory : IOptionsFactory - where TOptions : class, new() + where TOptions : class { private readonly IConfigureOptions[] _configureOptions; private readonly IPostConfigureOptions[] _postConfigureOptions; @@ -46,7 +46,7 @@ public class MultiTenantOptionsFactory : IOptionsFactory public TOptions Create(string name) { ITenantConfigureNamedOptionsWrapper? tenantConfigureNamedOptionsWrapper = null; - var options = new TOptions(); + var options = Activator.CreateInstance(); foreach (var setup in _configureOptions) { // consider directly injecting this to avoid a conditional. diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs index f7d50035..53b18619 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 diff --git a/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptions.cs b/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptions.cs index 820c1551..912c94b8 100644 --- a/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptions.cs +++ b/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptions.cs @@ -7,7 +7,7 @@ namespace Finbuckle.MultiTenant.Options; /// public class TenantConfigureNamedOptions : ITenantConfigureNamedOptions - where TOptions : class, new() + where TOptions : class where TTenantInfo : class, ITenantInfo, new() { /// diff --git a/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs b/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs index baba7df2..ba36b63f 100644 --- a/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs +++ b/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs @@ -6,7 +6,7 @@ namespace Finbuckle.MultiTenant.Options; class TenantConfigureNamedOptionsWrapper : ITenantConfigureNamedOptionsWrapper - where TOptions : class, new() + where TOptions : class where TTenantInfo : class, ITenantInfo, new() { private readonly IMultiTenantContextAccessor multiTenantContextAccessor; diff --git a/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderShould.cs index 5dba6742..a679bc29 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,7 +129,8 @@ public void ThrowIfNullFactoryAddingCustomStore() { var services = new ServiceCollection(); var builder = new FinbuckleMultiTenantBuilder(services); - Assert.Throws(() => builder.WithStore>(ServiceLifetime.Singleton, factory: null!)); + Assert.Throws(() => + builder.WithStore>(ServiceLifetime.Singleton, factory: null!)); } [Fact] @@ -141,12 +142,27 @@ public void AddPerTenantOptions() services.AddSingleton(accessor.Object); var builder = new FinbuckleMultiTenantBuilder(services); // Note: using MultiTenantBuilderShould as our test options class. - builder.WithPerTenantOptions((o, _) => o.TestProperty = 1); + builder.WithPerTenantOptions((o, _) => o.TestProperty = "1"); var sp = services.BuildServiceProvider(); sp.GetRequiredService>(); } - + + [Fact] + public void AddConfigurePerTenant() + { + var services = new ServiceCollection(); + services.ConfigurePerTenant("", + (options, tenant) => options.TestProperty = tenant?.Id); + + Assert.Contains(services, service => + service.ServiceType == typeof(IOptionsMonitorCache) && + service.ImplementationType == typeof(MultiTenantOptionsCache) && + service.Lifetime == ServiceLifetime.Singleton); + + var sp = services.BuildServiceProvider(); + } + [Fact] public void AddPerTenantNamedOptions() { @@ -156,7 +172,7 @@ public void AddPerTenantNamedOptions() 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); + builder.WithPerTenantNamedOptions("a name", (o, _) => o.TestProperty = "1"); var sp = services.BuildServiceProvider(); sp.GetRequiredService>(); } @@ -276,7 +292,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/DependencyInjection/ServiceCollectionShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs index 9f4b7606..3e36897c 100644 --- a/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs +++ b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs @@ -13,23 +13,23 @@ public class ServiceCollectionShould // ReSharper disable once UnusedAutoPropertyAccessor.Local private string? TestProperty { get; set; } - [Fact] - public void AddPerTenantOptions() - { - var services = new ServiceCollection(); - _ = services.AddMultiTenant(); - - services.AddPerTenantOptions() - .Configure((o, ti) => o.TestProperty = ti.Id); - - var sp = services.BuildServiceProvider(); - var multiTenantContextAccessor = sp.GetRequiredService>(); - multiTenantContextAccessor.MultiTenantContext = new MultiTenantContext{ TenantInfo = new TenantInfo { Id = "initech" } }; - - using var scope = sp.CreateScope(); - var options = scope.ServiceProvider.GetRequiredService>(); - - Assert.Equal("initech", options.Value.TestProperty); - } + // [Fact] + // public void AddPerTenantOptions() + // { + // var services = new ServiceCollection(); + // _ = services.AddMultiTenant(); + // + // services.AddPerTenantOptions() + // .Configure((o, ti) => o.TestProperty = ti.Id); + // + // var sp = services.BuildServiceProvider(); + // var multiTenantContextAccessor = sp.GetRequiredService>(); + // multiTenantContextAccessor.MultiTenantContext = new MultiTenantContext{ TenantInfo = new TenantInfo { Id = "initech" } }; + // + // using var scope = sp.CreateScope(); + // var options = scope.ServiceProvider.GetRequiredService>(); + // + // Assert.Equal("initech", options.Value.TestProperty); + // } } } \ No newline at end of file From 71e3ca0c9e217cf728952732f7e5e0ede3e10b11 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 15 Nov 2023 21:51:34 -0700 Subject: [PATCH 03/10] completed ConfigurePerTenant for named and unnamed options. --- .../IMultiTenantConfigureNamedOptions.cs} | 7 +- .../Abstractions/IMultiTenantStore.cs | 3 +- .../Abstractions/IMultiTenantStrategy.cs | 2 +- .../Abstractions/ITenantResolver.cs | 3 +- .../FinbuckleMultiTenantBuilderExtensions.cs | 2 - .../DependencyInjection/MultiTenantBuilder.cs | 27 ++- .../ServiceCollectionExtensions.cs | 39 +++- .../Events/MultiTenantEvents.cs | 3 +- .../Events/TenantResolvedContext.cs | 2 +- .../Extensions/TypeExtensions.cs | 2 - .../Finbuckle.MultiTenant.csproj | 1 + .../AsyncLocalMultiTenantContextAccessor.cs | 2 - .../MultiTenantAttribute.cs | 6 +- .../MultiTenantException.cs | 2 - .../MultiTenantOptions.cs | 2 +- .../Options/ITenantConfigureNamedOptions.cs | 22 -- .../Options/ITenantConfigureOptions.cs | 15 -- .../Options/MultiTenantOptionsCache.cs | 3 +- .../Options/MultiTenantOptionsFactory.cs | 207 ------------------ .../Options/MultiTenantOptionsManager.cs | 2 +- .../Options/TenantConfigureNamedOptions.cs | 40 ---- .../TenantConfigureNamedOptionsWrapper.cs | 55 ----- .../Options/TenantConfigureOptions.cs | 24 -- src/Finbuckle.MultiTenant/StoreInfo.cs | 2 - .../ConfigurationStore/ConfigurationStore.cs | 4 - .../DistributedCacheStore.cs | 3 - .../Stores/HttpRemoteStore/HttpRemoteStore.cs | 3 - .../HttpRemoteStore/HttpRemoteStoreClient.cs | 3 - .../Stores/InMemoryStore/InMemoryStore.cs | 4 - .../InMemoryStore/InMemoryStoreOptions.cs | 2 - .../Stores/MultiTenantStoreWrapper.cs | 3 - .../Strategies/DelegateStrategy.cs | 3 - .../Strategies/MultiTenantStrategyWrapper.cs | 2 - .../Strategies/StaticStrategy.cs | 2 - src/Finbuckle.MultiTenant/StrategyInfo.cs | 2 - src/Finbuckle.MultiTenant/TenantResolver.cs | 4 - .../MultiTenantBuilderExtensionsShould.cs | 2 +- .../MultiTenantBuilderShould.cs | 54 +---- .../ServiceCollectionExtensionsShould.cs | 57 ++++- .../ServiceCollectionShould.cs | 35 --- .../Finbuckle.MultiTenant.Test.csproj | 5 + .../MultiTenantOptionsFactoryShould.cs | 164 -------------- 42 files changed, 123 insertions(+), 702 deletions(-) rename src/Finbuckle.MultiTenant/{Options/ITenantConfigureNamedOptionsWrapper.cs => Abstractions/IMultiTenantConfigureNamedOptions.cs} (50%) rename src/Finbuckle.MultiTenant/{Extensions => DependencyInjection}/FinbuckleMultiTenantBuilderExtensions.cs (99%) rename src/Finbuckle.MultiTenant/{Extensions => DependencyInjection}/ServiceCollectionExtensions.cs (80%) delete mode 100644 src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptions.cs delete mode 100644 src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs delete mode 100644 src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs delete mode 100644 src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptions.cs delete mode 100644 src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs delete mode 100644 src/Finbuckle.MultiTenant/Options/TenantConfigureOptions.cs rename test/Finbuckle.MultiTenant.Test/{Extensions => DependencyInjection}/MultiTenantBuilderExtensionsShould.cs (99%) rename test/Finbuckle.MultiTenant.Test/{Extensions => DependencyInjection}/ServiceCollectionExtensionsShould.cs (62%) delete mode 100644 test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs delete mode 100644 test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsFactoryShould.cs diff --git a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantConfigureNamedOptions.cs similarity index 50% rename from src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs rename to src/Finbuckle.MultiTenant/Abstractions/IMultiTenantConfigureNamedOptions.cs index f0eb4723..1eb29bf5 100644 --- a/src/Finbuckle.MultiTenant/Options/ITenantConfigureNamedOptionsWrapper.cs +++ b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantConfigureNamedOptions.cs @@ -1,13 +1,12 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; using Microsoft.Extensions.Options; -namespace Finbuckle.MultiTenant.Options; +namespace Finbuckle.MultiTenant.Abstractions; -[Obsolete] -interface ITenantConfigureNamedOptionsWrapper : IConfigureNamedOptions +// 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 ec5dc5ae..9f0febef 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs @@ -1,15 +1,12 @@ // 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; +// TODO refactor methods into extensions /// /// Builder class for Finbuckle.MultiTenant configuration. /// @@ -39,6 +36,7 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) 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); } @@ -53,16 +51,17 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) public FinbuckleMultiTenantBuilder WithPerTenantNamedOptions(string? name, Action tenantConfigureNamedOptions) where TOptions : class, new() { - if (tenantConfigureNamedOptions == null) - { - throw new ArgumentNullException(nameof(tenantConfigureNamedOptions)); - } - - // Services.AddOptionsCore(); - Services.TryAddEnumerable(ServiceDescriptor - .Scoped, TenantConfigureNamedOptionsWrapper>()); - Services.AddScoped>(sp => - new TenantConfigureNamedOptions(name, tenantConfigureNamedOptions)); + // 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; } diff --git a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs similarity index 80% rename from src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs rename to src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs index 47e14fb2..4de7ea5e 100644 --- a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.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 Finbuckle.MultiTenant; using Finbuckle.MultiTenant.Internal; using Finbuckle.MultiTenant.Options; @@ -123,6 +121,7 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceColl return true; } + // TODO adjust summary /// /// Registers an action used to configure a particular type of options. /// Note: These are run before all . @@ -133,20 +132,48 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceColl /// 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 config) where TOptions : class + string? name, Action action) where TOptions : class where TTenantInfo : class, ITenantInfo, new() { // Required infrastructure. + services.AddOptions(); services.TryAddSingleton, MultiTenantOptionsCache>(); - services.TryAddTransient, MultiTenantOptionsFactory>(); services.TryAddScoped>(BuildOptionsManager); services.TryAddSingleton>(BuildOptionsManager); - services.ConfigureAll() + services.AddSingleton>(sp => - ActivatorUtilities.CreateInstance>(sp)); + { + var multiTenantContextAccessor = sp.GetRequiredService>(); + + void ConfigureAction(TOptions options) + { + var multiTenantContext = multiTenantContextAccessor.MultiTenantContext; + if (multiTenantContext.HasResolvedTenant) + action(options, multiTenantContext.TenantInfo); + } + + return new ConfigureNamedOptions(name, ConfigureAction); + }); return services; } + + // TODO adjust summary + /// + /// Registers an action used to configure a particular type of options. + /// Note: These are run before all . + /// + /// The options type to be configured. + /// 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, + Action action) where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + return services.ConfigurePerTenant(Options.Options.DefaultName, action); + } private static void AddOptionsPerTenantCore(this IServiceCollection services) where TOptions : class { 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/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..522e9bfd 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; /// 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 21aea397..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 - 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 17749838..00000000 --- a/src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -using Microsoft.Extensions.Options; -using System; - -namespace Finbuckle.MultiTenant.Options; - -[Obsolete] -public interface ITenantConfigureOptions - where TOptions : class - where TTenantInfo : class, ITenantInfo, new() -{ - void Configure(TOptions options, TTenantInfo tenantInfo); -} diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs index 66daa3e8..79150d33 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; @@ -75,7 +74,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 7bdd861a..00000000 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs +++ /dev/null @@ -1,207 +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; -using System.Collections.Generic; -using Microsoft.Extensions.Options; - -namespace Finbuckle.MultiTenant.Options; - -/// -/// Implementation of IOptionsFactory. -/// -/// The type of options being requested. -public class MultiTenantOptionsFactory : IOptionsFactory - where TOptions : class -{ - private readonly IConfigureOptions[] _configureOptions; - private readonly IPostConfigureOptions[] _postConfigureOptions; - private readonly IValidateOptions[] _validations; - - /// - /// Initializes a new instance with the specified options configurations. - /// - public MultiTenantOptionsFactory( - IEnumerable> configureOptions, - IEnumerable> postConfigureOptions, - IEnumerable> validations) - { - // 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(); - } - - /// - public TOptions Create(string name) - { - ITenantConfigureNamedOptionsWrapper? tenantConfigureNamedOptionsWrapper = null; - var options = Activator.CreateInstance(); - foreach (var setup in _configureOptions) - { - // consider directly injecting this to avoid a conditional. - if (setup is ITenantConfigureNamedOptionsWrapper wrapper) - { - tenantConfigureNamedOptionsWrapper = wrapper; - } - else if (setup is IConfigureNamedOptions namedSetup) - { - namedSetup.Configure(name, options); - } - else if (name == Microsoft.Extensions.Options.Options.DefaultName) - { - setup.Configure(options); - } - } - - tenantConfigureNamedOptionsWrapper?.Configure(name, options); - - 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; - } -} - -/// -/// Implementation of IOptionsFactory. -/// -/// The type of options being requested. -/// The type of the tenant info. -[Obsolete] -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; - -#pragma warning disable CS0612 // Type or member is obsolete - private readonly ITenantConfigureOptions[] _tenantConfigureOptions; -#pragma warning restore CS0612 // Type or member is obsolete - 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, -#pragma warning disable CS0612 // Type or member is obsolete - IEnumerable> tenantConfigureOptions, -#pragma warning restore CS0612 // Type or member is obsolete - 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(); -#pragma warning disable CS0612 // Type or member is obsolete - _tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions[] ?? - new List>(tenantConfigureOptions) - .ToArray(); -#pragma warning restore CS0612 // Type or member is obsolete - _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 53b18619..9a45a498 100644 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs +++ b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs @@ -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 912c94b8..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 - 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/TenantConfigureNamedOptionsWrapper.cs b/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs deleted file mode 100644 index ba36b63f..00000000 --- a/src/Finbuckle.MultiTenant/Options/TenantConfigureNamedOptionsWrapper.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Contributors. -// Refer to the solution LICENSE file for more information. - -using System.Collections.Generic; - -namespace Finbuckle.MultiTenant.Options; - -class TenantConfigureNamedOptionsWrapper : ITenantConfigureNamedOptionsWrapper - where TOptions : class - where TTenantInfo : class, ITenantInfo, new() -{ - private readonly IMultiTenantContextAccessor multiTenantContextAccessor; - private readonly ITenantConfigureOptions[] tenantConfigureOptions; - private readonly ITenantConfigureNamedOptions[] tenantConfigureNamedOptions; - - public TenantConfigureNamedOptionsWrapper( - IMultiTenantContextAccessor multiTenantContextAccessor, - IEnumerable> tenantConfigureOptions, - IEnumerable> tenantConfigureNamedOptions) - { - this.multiTenantContextAccessor = multiTenantContextAccessor; - this.tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions[] ?? - new List>(tenantConfigureOptions).ToArray(); - this.tenantConfigureNamedOptions = tenantConfigureNamedOptions as ITenantConfigureNamedOptions[] ?? - new List>(tenantConfigureNamedOptions).ToArray(); - } - - public void Configure(string name, TOptions 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!); - } - } - - public void Configure(TOptions 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(Microsoft.Extensions.Options.Options.DefaultName, options, - multiTenantContextAccessor.MultiTenantContext.TenantInfo!); - } - } -} 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.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 a679bc29..7cea362f 100644 --- a/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderShould.cs +++ b/test/Finbuckle.MultiTenant.Test/DependencyInjection/MultiTenantBuilderShould.cs @@ -133,58 +133,6 @@ public void ThrowIfNullFactoryAddingCustomStore() 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 AddConfigurePerTenant() - { - var services = new ServiceCollection(); - services.ConfigurePerTenant("", - (options, tenant) => options.TestProperty = tenant?.Id); - - Assert.Contains(services, service => - service.ServiceType == typeof(IOptionsMonitorCache) && - service.ImplementationType == typeof(MultiTenantOptionsCache) && - service.Lifetime == ServiceLifetime.Singleton); - - var sp = services.BuildServiceProvider(); - } - - [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!)); - } - [Theory] [InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped)] @@ -216,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); } } diff --git a/test/Finbuckle.MultiTenant.Test/Extensions/ServiceCollectionExtensionsShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionExtensionsShould.cs similarity index 62% rename from test/Finbuckle.MultiTenant.Test/Extensions/ServiceCollectionExtensionsShould.cs rename to test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionExtensionsShould.cs index 81e5ac9a..0b5eeae6 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,23 @@ 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); + } + + [Fact] + public void PreInitMultiTenantContextAccessorInDi() + { + var services = new ServiceCollection(); + services.AddMultiTenant(); + + var service = services.SingleOrDefault(s => s.Lifetime == ServiceLifetime.Singleton && + s.ServiceType == + typeof(IMultiTenantContextAccessor) && + s.ImplementationInstance is not null); Assert.NotNull(service); Assert.Equal(ServiceLifetime.Singleton, service!.Lifetime); @@ -111,5 +127,42 @@ 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); + } } } \ No newline at end of file diff --git a/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs deleted file mode 100644 index 3e36897c..00000000 --- a/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionShould.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright Finbuckle LLC, Andrew White, and Co ntributors. -// Refer to the solution LICENSE file for more information. - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Finbuckle.MultiTenant.Test.DependencyInjection -{ - public class ServiceCollectionShould - { - // Used in some tests. - // ReSharper disable once UnusedAutoPropertyAccessor.Local - private string? TestProperty { get; set; } - - // [Fact] - // public void AddPerTenantOptions() - // { - // var services = new ServiceCollection(); - // _ = services.AddMultiTenant(); - // - // services.AddPerTenantOptions() - // .Configure((o, ti) => o.TestProperty = ti.Id); - // - // var sp = services.BuildServiceProvider(); - // var multiTenantContextAccessor = sp.GetRequiredService>(); - // multiTenantContextAccessor.MultiTenantContext = new MultiTenantContext{ TenantInfo = new TenantInfo { Id = "initech" } }; - // - // using var scope = sp.CreateScope(); - // var options = scope.ServiceProvider.GetRequiredService>(); - // - // Assert.Equal("initech", options.Value.TestProperty); - // } - } -} \ 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/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 From 02878fe41d013dd30fddc6322ad4433747de5d6c Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 9 Dec 2023 12:03:59 -0700 Subject: [PATCH 04/10] - implementation for PostConfigurePerTenant, ConfigureAllPerTenant, and PostConfigureAllPerTenant added. - various cleanups --- .../Extensions/ApplicationBuilderExtensions.cs | 2 +- .../Internal/AsyncLocalMultiTenantContextAccessor.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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/Internal/AsyncLocalMultiTenantContextAccessor.cs b/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs index 522e9bfd..4f65d841 100644 --- a/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs +++ b/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs @@ -24,7 +24,8 @@ public class AsyncLocalMultiTenantContextAccessor : IMultiTenantContextAccess } /// - /// 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; From 488cffd9be2e2979d34a08066981b262553e68fd Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 22 Dec 2023 15:31:54 -0700 Subject: [PATCH 05/10] Add `ConfigurePerTenant` `OptionsBuilder` extensions supporting DI. --- .../DependencyInjection/MultiTenantBuilder.cs | 32 +- .../OptionsBuilderExtensions.cs | 732 ++++++++++++++++++ .../ServiceCollectionExtensions.cs | 198 +++-- 3 files changed, 887 insertions(+), 75 deletions(-) create mode 100644 src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs index 9f0febef..3390defd 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs @@ -10,8 +10,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. @@ -33,8 +33,9 @@ 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. @@ -48,8 +49,9 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) /// 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() { // TODO remove this method // if (tenantConfigureNamedOptions == null) @@ -66,19 +68,15 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) return this; } - // TODO consider per tenant AllOptions variation - // TODO consider per-tenant post options - - /// /// 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)); /// @@ -88,9 +86,9 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) /// 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) { @@ -99,7 +97,7 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) // 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; } @@ -110,7 +108,7 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) /// 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)); @@ -121,7 +119,7 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services) /// 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..bee58ab2 --- /dev/null +++ b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs @@ -0,0 +1,732 @@ +// 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.DependencyInjection; +using Microsoft.Extensions.Options; + +// ReSharper disable once CheckNamespace +namespace Finbuckle.Extensions.DependencyInjection; + +public static class OptionsBuilderExtensions +{ + public static OptionsBuilder ConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions( + optionsBuilder.Name, + sp.GetService(), + (options, dep) => + { + if (dep is not null) + configureOptions(options, dep); + })); + + return optionsBuilder; + } + + public static OptionsBuilder ConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TDep : class + where TTenantInfo : class, ITenantInfo, new() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetService(), + (options, dep, 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() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetService(), + (options, dep1, dep2, 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() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetService(), + (options, dep1, dep2, dep3, 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() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new ConfigureNamedOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetService(), + (options, dep1, dep2, dep3, dep4, tenantInfo) => + { + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, dep3, dep4, 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 TDep5 : class + where TTenantInfo : class, ITenantInfo, new() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + { + var tenantInfo = sp.GetService(); + return new ConfigureNamedOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + (options, dep1, dep2, dep3, dep4, dep5) => + { + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, dep3, dep4, dep5, tenantInfo); + }); + }); + + return optionsBuilder; + } +} +// +// /// +// /// Used to configure instances per-tenant. +// /// +// /// The type of options being requested. +// /// The ITenantInfo implementation type. +// public class PerTenantOptionsBuilder +// where TOptions : class +// where TTenantInfo : class, ITenantInfo, new() +// { +// private const string DefaultValidationFailureMessage = "A validation error has occurred."; +// +// /// +// /// The default name of the instance. +// /// +// public string Name { get; } +// +// /// +// /// The for the options being configured. +// /// +// public IServiceCollection Services { get; } +// +// /// +// /// Constructor. +// /// +// /// The for the options being configured. +// /// The default name of the instance, if null is used. +// public PerTenantOptionsBuilder(IServiceCollection services, string? name) +// { +// Services = services ?? throw new ArgumentNullException(nameof(services)); +// Name = name ?? Options.DefaultName; +// } +// +// /// +// /// Registers an action used to configure a particular type of options. +// /// Note: These are run before all . +// /// +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder Configure( +// Action configureOptions) +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// Services.ConfigurePerTenant(Name, configureOptions); +// return this; +// } +// +// /// +// /// Registers an action used to configure a particular type of options. +// /// Note: These are run before all . +// /// +// /// A dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder Configure( +// Action configureOptions) +// where TDep : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>(sp => +// { +// var multiTenantContextAccessor = sp.GetRequiredService>(); +// +// return new ConfigureNamedOptions(Name, sp.GetRequiredService(), (options, dep1) => +// { +// var multiTenantContext = multiTenantContextAccessor.MultiTenantContext; +// if (multiTenantContext.HasResolvedTenant) +// configureOptions(options, dep1, multiTenantContext.TenantInfo); +// }); +// }); +// +// return this; +// } +// +// /// +// /// Registers an action used to configure a particular type of options. +// /// Note: These are run before all . +// /// +// /// The first dependency used by the action. +// /// The second dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder Configure( +// Action configureOptions) +// where TDep1 : class +// where TDep2 : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>(sp => +// new ConfigureNamedOptions(Name, sp.GetRequiredService(), +// sp.GetRequiredService(), configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to configure a particular type of options. +// /// Note: These are run before all . +// /// +// /// The first dependency used by the action. +// /// The second dependency used by the action. +// /// The third dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder Configure( +// Action configureOptions) +// where TDep1 : class +// where TDep2 : class +// where TDep3 : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>( +// sp => new ConfigureNamedOptions( +// Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to configure a particular type of options. +// /// Note: These are run before all . +// /// +// /// The first dependency used by the action. +// /// The second dependency used by the action. +// /// The third dependency used by the action. +// /// The fourth dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder Configure( +// Action configureOptions) +// where TDep1 : class +// where TDep2 : class +// where TDep3 : class +// where TDep4 : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>( +// sp => new ConfigureNamedOptions( +// Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to configure a particular type of options. +// /// Note: These are run before all . +// /// +// /// The first dependency used by the action. +// /// The second dependency used by the action. +// /// The third dependency used by the action. +// /// The fourth dependency used by the action. +// /// The fifth dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder Configure( +// Action configureOptions) +// where TDep1 : class +// where TDep2 : class +// where TDep3 : class +// where TDep4 : class +// where TDep5 : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>( +// sp => new ConfigureNamedOptions( +// Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to configure a particular type of options. +// /// Note: These are run after all . +// /// +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder PostConfigure(Action configureOptions) +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddSingleton>( +// new PostConfigureOptions(Name, configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to post configure a particular type of options. +// /// Note: These are run after all . +// /// +// /// The dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder PostConfigure( +// Action configureOptions) +// where TDep : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>(sp => +// new PostConfigureOptions(Name, sp.GetRequiredService(), configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to post configure a particular type of options. +// /// Note: These are run after all . +// /// +// /// The first dependency used by the action. +// /// The second dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder PostConfigure( +// Action configureOptions) +// where TDep1 : class +// where TDep2 : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>(sp => +// new PostConfigureOptions(Name, sp.GetRequiredService(), +// sp.GetRequiredService(), configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to post configure a particular type of options. +// /// Note: These are run after all . +// /// +// /// The first dependency used by the action. +// /// The second dependency used by the action. +// /// The third dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder PostConfigure( +// Action configureOptions) +// where TDep1 : class +// where TDep2 : class +// where TDep3 : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>( +// sp => new PostConfigureOptions( +// Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to post configure a particular type of options. +// /// Note: These are run after all . +// /// +// /// The first dependency used by the action. +// /// The second dependency used by the action. +// /// The third dependency used by the action. +// /// The fourth dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder PostConfigure( +// Action configureOptions) +// where TDep1 : class +// where TDep2 : class +// where TDep3 : class +// where TDep4 : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>( +// sp => new PostConfigureOptions( +// Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// configureOptions)); +// return this; +// } +// +// /// +// /// Registers an action used to post configure a particular type of options. +// /// Note: These are run after all . +// /// +// /// The first dependency used by the action. +// /// The second dependency used by the action. +// /// The third dependency used by the action. +// /// The fourth dependency used by the action. +// /// The fifth dependency used by the action. +// /// The action used to configure the options. +// /// The current . +// public virtual PerTenantOptionsBuilder PostConfigure( +// Action configureOptions) +// where TDep1 : class +// where TDep2 : class +// where TDep3 : class +// where TDep4 : class +// where TDep5 : class +// { +// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); +// +// Services.AddTransient>( +// sp => new PostConfigureOptions( +// Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// configureOptions)); +// return this; +// } +// +// /// +// /// Register a validation action for an options type using a default failure message. +// /// +// /// The validation function. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate(Func validation) +// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); +// +// /// +// /// Register a validation action for an options type. +// /// +// /// The validation function. +// /// The failure message to use when validation fails. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate(Func validation, +// string failureMessage) +// { +// if (validation == null) throw new ArgumentNullException(nameof(validation)); +// +// Services.AddSingleton>( +// new ValidateOptions(Name, validation, failureMessage)); +// return this; +// } +// +// /// +// /// Register a validation action for an options type using a default failure message. +// /// +// /// The dependency used by the validation function. +// /// The validation function. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate(Func validation) +// where TDep : notnull +// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); +// +// /// +// /// Register a validation action for an options type. +// /// +// /// The dependency used by the validation function. +// /// The validation function. +// /// The failure message to use when validation fails. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate(Func validation, +// string failureMessage) +// where TDep : notnull +// { +// if (validation == null) throw new ArgumentNullException(nameof(validation)); +// +// Services.AddTransient>(sp => +// new ValidateOptions(Name, sp.GetRequiredService(), validation, failureMessage)); +// return this; +// } +// +// /// +// /// Register a validation action for an options type using a default failure message. +// /// +// /// The first dependency used by the validation function. +// /// The second dependency used by the validation function. +// /// The validation function. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate( +// Func validation) +// where TDep1 : notnull +// where TDep2 : notnull +// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); +// +// /// +// /// Register a validation action for an options type. +// /// +// /// The first dependency used by the validation function. +// /// The second dependency used by the validation function. +// /// The validation function. +// /// The failure message to use when validation fails. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate( +// Func validation, +// string failureMessage) +// where TDep1 : notnull +// where TDep2 : notnull +// { +// if (validation == null) throw new ArgumentNullException(nameof(validation)); +// +// +// Services.AddTransient>(sp => +// new ValidateOptions(Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// validation, +// failureMessage)); +// return this; +// } +// +// /// +// /// Register a validation action for an options type using a default failure message. +// /// +// /// The first dependency used by the validation function. +// /// The second dependency used by the validation function. +// /// The third dependency used by the validation function. +// /// The validation function. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate( +// Func validation) +// where TDep1 : notnull +// where TDep2 : notnull +// where TDep3 : notnull +// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); +// +// /// +// /// Register a validation action for an options type. +// /// +// /// The first dependency used by the validation function. +// /// The second dependency used by the validation function. +// /// The third dependency used by the validation function. +// /// The validation function. +// /// The failure message to use when validation fails. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate( +// Func validation, string failureMessage) +// where TDep1 : notnull +// where TDep2 : notnull +// where TDep3 : notnull +// { +// if (validation == null) throw new ArgumentNullException(nameof(validation)); +// +// Services.AddTransient>(sp => +// new ValidateOptions(Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// validation, +// failureMessage)); +// return this; +// } +// +// /// +// /// Register a validation action for an options type using a default failure message. +// /// +// /// The first dependency used by the validation function. +// /// The second dependency used by the validation function. +// /// The third dependency used by the validation function. +// /// The fourth dependency used by the validation function. +// /// The validation function. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate( +// Func validation) +// where TDep1 : notnull +// where TDep2 : notnull +// where TDep3 : notnull +// where TDep4 : notnull +// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); +// +// /// +// /// Register a validation action for an options type. +// /// +// /// The first dependency used by the validation function. +// /// The second dependency used by the validation function. +// /// The third dependency used by the validation function. +// /// The fourth dependency used by the validation function. +// /// The validation function. +// /// The failure message to use when validation fails. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate( +// Func validation, string failureMessage) +// where TDep1 : notnull +// where TDep2 : notnull +// where TDep3 : notnull +// where TDep4 : notnull +// { +// if (validation == null) throw new ArgumentNullException(nameof(validation)); +// +// Services.AddTransient>(sp => +// new ValidateOptions(Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// validation, +// failureMessage)); +// return this; +// } +// +// /// +// /// Register a validation action for an options type using a default failure message. +// /// +// /// The first dependency used by the validation function. +// /// The second dependency used by the validation function. +// /// The third dependency used by the validation function. +// /// The fourth dependency used by the validation function. +// /// The fifth dependency used by the validation function. +// /// The validation function. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate( +// Func validation) +// where TDep1 : notnull +// where TDep2 : notnull +// where TDep3 : notnull +// where TDep4 : notnull +// where TDep5 : notnull +// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); +// +// /// +// /// Register a validation action for an options type. +// /// +// /// The first dependency used by the validation function. +// /// The second dependency used by the validation function. +// /// The third dependency used by the validation function. +// /// The fourth dependency used by the validation function. +// /// The fifth dependency used by the validation function. +// /// The validation function. +// /// The failure message to use when validation fails. +// /// The current . +// public virtual PerTenantOptionsBuilder Validate( +// Func validation, string failureMessage) +// where TDep1 : notnull +// where TDep2 : notnull +// where TDep3 : notnull +// where TDep4 : notnull +// where TDep5 : notnull +// { +// if (validation == null) throw new ArgumentNullException(nameof(validation)); +// +// Services.AddTransient>(sp => +// new ValidateOptions(Name, +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// sp.GetRequiredService(), +// validation, +// failureMessage)); +// return this; +// } +// } \ No newline at end of file diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs index 4de7ea5e..178de48a 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs @@ -18,45 +18,49 @@ public static class FinbuckleServiceCollectionExtensions /// /// Configure Finbuckle.MultiTenant services for the application. /// - /// The IServiceCollection instance the extension method applies to. + /// 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, + public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceCollection services, Action config) - where T : class, ITenantInfo, new() + where TTenantInfo : class, ITenantInfo, new() { - services.AddScoped, TenantResolver>(); - services.AddScoped(sp => (ITenantResolver)sp.GetRequiredService>()); + services.AddScoped, TenantResolver>(); + services.AddScoped( + sp => (ITenantResolver)sp.GetRequiredService>()); - services.AddScoped>(sp => - sp.GetRequiredService>().MultiTenantContext!); + services.AddScoped>(sp => + sp.GetRequiredService>().MultiTenantContext!); - services.AddScoped(sp => - sp.GetRequiredService>().MultiTenantContext?.TenantInfo!); - services.AddScoped(sp => sp.GetService()!); + services.AddScoped(sp => + sp.GetRequiredService>().MultiTenantContext?.TenantInfo!); + services.AddScoped(sp => sp.GetService()!); - // TODO this might require instance - services.AddSingleton, AsyncLocalMultiTenantContextAccessor>(); + // TODO this might require instance to ensure it already exists when needed + services + .AddSingleton, + AsyncLocalMultiTenantContextAccessor>(); services.AddSingleton(sp => - (IMultiTenantContextAccessor)sp.GetRequiredService>()); + (IMultiTenantContextAccessor)sp.GetRequiredService>()); services.Configure(config); - return new FinbuckleMultiTenantBuilder(services); + return new FinbuckleMultiTenantBuilder(services); } /// /// Configure Finbuckle.MultiTenant services for the application. /// - /// The IServiceCollection instance the extension method applies to. + /// 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() + public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceCollection services) + where TTenantInfo : class, ITenantInfo, new() { - return services.AddMultiTenant(_ => { }); + 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)); @@ -121,76 +125,154 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceColl return true; } - // TODO adjust summary /// - /// Registers an action used to configure a particular type of options. - /// Note: These are run before all . + /// 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 action) where TOptions : class + public static IServiceCollection ConfigurePerTenant( + this IServiceCollection services, + string? name, Action configureOptions) + where TOptions : class where TTenantInfo : class, ITenantInfo, new() { - // Required infrastructure. - services.AddOptions(); - services.TryAddSingleton, MultiTenantOptionsCache>(); - services.TryAddScoped>(BuildOptionsManager); - services.TryAddSingleton>(BuildOptionsManager); - - services.AddSingleton>(sp => - { - var multiTenantContextAccessor = sp.GetRequiredService>(); - - void ConfigureAction(TOptions options) - { - var multiTenantContext = multiTenantContextAccessor.MultiTenantContext; - if (multiTenantContext.HasResolvedTenant) - action(options, multiTenantContext.TenantInfo); - } + ConfigurePerTenantReqs(services); - return new ConfigureNamedOptions(name, ConfigureAction); - }); + services.AddTransient>(sp => + new ConfigureNamedOptions( + name, + sp.GetService(), + (options, tenantInfo) => + { + if (tenantInfo is not null) + configureOptions(options, tenantInfo); + })); return services; } - - // TODO adjust summary + /// /// Registers an action used to configure a particular type of options. - /// Note: These are run before all . /// /// 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 ConfigurePerTenant(this IServiceCollection services, - Action action) where TOptions : class + public static IServiceCollection PostConfigurePerTenant( + this IServiceCollection services, + string? name, Action configureOptions) + where TOptions : class where TTenantInfo : class, ITenantInfo, new() { - return services.ConfigurePerTenant(Options.Options.DefaultName, action); + ConfigurePerTenantReqs(services); + + services.AddTransient>(sp => + new PostConfigureOptions( + name, + sp.GetService(), + (options, tenantInfo) => + { + if (tenantInfo is not null) + configureOptions(options, tenantInfo); + })); + + return services; } - private static void AddOptionsPerTenantCore(this IServiceCollection services) where TOptions : class + /// + /// 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() { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } + 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); } - private static MultiTenantOptionsManager BuildOptionsManager(IServiceProvider sp) + internal static void ConfigurePerTenantReqs(IServiceCollection services) where TOptions : class { - var cache = (IOptionsMonitorCache)ActivatorUtilities.CreateInstance(sp, - typeof(MultiTenantOptionsCache)); - return (MultiTenantOptionsManager) - ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager), cache); + 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 From 6d4527c2509fddd929c916c63cedbc87412338e8 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 22 Dec 2023 15:36:54 -0700 Subject: [PATCH 06/10] Add post configure variants to options builder extensions. --- .../OptionsBuilderExtensions.cs | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs index bee58ab2..13fd5950 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs @@ -167,6 +167,161 @@ public static class OptionsBuilderExtensions return optionsBuilder; } + + public static OptionsBuilder PostConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TTenantInfo : class, ITenantInfo, new() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions( + optionsBuilder.Name, + sp.GetService(), + (options, dep) => + { + if (dep is not null) + configureOptions(options, dep); + })); + + return optionsBuilder; + } + + public static OptionsBuilder PostConfigurePerTenant( + this OptionsBuilder optionsBuilder, Action configureOptions) + where TOptions : class + where TDep : class + where TTenantInfo : class, ITenantInfo, new() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetService(), + (options, dep, 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() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetService(), + (options, dep1, dep2, 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() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetService(), + (options, dep1, dep2, dep3, 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() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + new PostConfigureOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetService(), + (options, dep1, dep2, dep3, dep4, tenantInfo) => + { + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, dep3, dep4, 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 TDep5 : class + where TTenantInfo : class, ITenantInfo, new() + { + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); + + optionsBuilder.Services.AddTransient>(sp => + { + var tenantInfo = sp.GetService(); + return new PostConfigureOptions( + optionsBuilder.Name, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + (options, dep1, dep2, dep3, dep4, dep5) => + { + if (tenantInfo is not null) + configureOptions(options, dep1, dep2, dep3, dep4, dep5, tenantInfo); + }); + }); + + return optionsBuilder; + } } // // /// From 57021ee9a0fd7e68314d951d7d94cb7e29651fea Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 22 Dec 2023 15:41:23 -0700 Subject: [PATCH 07/10] Add check for null parameters to extension methods. --- .../OptionsBuilderExtensions.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs index 13fd5950..423640ca 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs @@ -18,6 +18,8 @@ public static class OptionsBuilderExtensions where TOptions : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -39,6 +41,8 @@ public static class OptionsBuilderExtensions where TDep : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -62,6 +66,8 @@ public static class OptionsBuilderExtensions where TDep2 : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -88,6 +94,8 @@ public static class OptionsBuilderExtensions where TDep3 : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -116,6 +124,8 @@ public static class OptionsBuilderExtensions where TDep4 : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -146,6 +156,8 @@ public static class OptionsBuilderExtensions where TDep5 : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -173,6 +185,8 @@ public static class OptionsBuilderExtensions where TOptions : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -194,6 +208,8 @@ public static class OptionsBuilderExtensions where TDep : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -217,6 +233,8 @@ public static class OptionsBuilderExtensions where TDep2 : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -243,6 +261,8 @@ public static class OptionsBuilderExtensions where TDep3 : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -271,6 +291,8 @@ public static class OptionsBuilderExtensions where TDep4 : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => @@ -301,6 +323,8 @@ public static class OptionsBuilderExtensions where TDep5 : class where TTenantInfo : class, ITenantInfo, new() { + if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => From f029972dfab93f6c88cb9ebe93a12bfee4681644 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Mon, 5 Feb 2024 21:31:45 -0700 Subject: [PATCH 08/10] fixed all broken unit tests and additional tweaks --- .../FinbuckleMultiTenantBuilderExtensions.cs | 8 +- ...MultiTenantAuthenticationSchemeProvider.cs | 1 + .../DependencyInjection/MultiTenantBuilder.cs | 1 - .../OptionsBuilderExtensions.cs | 810 +++--------------- .../ServiceCollectionExtensions.cs | 14 +- .../Options/MultiTenantOptionsCache.cs | 117 +-- ...enantAuthenticationSchemeProviderShould.cs | 91 +- .../ServiceCollectionExtensionsShould.cs | 38 +- .../Options/MultiTenantOptionsCacheShould.cs | 34 +- .../Options/TestOptions.cs | 13 - 10 files changed, 233 insertions(+), 894 deletions(-) delete mode 100644 test/Finbuckle.MultiTenant.Test/Options/TestOptions.cs 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 class FinbuckleMultiTenantBuilderExtensions }); // 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 class FinbuckleMultiTenantBuilderExtensions }); // 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 class FinbuckleMultiTenantBuilderExtensions 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 class FinbuckleMultiTenantBuilderExtensions // 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/DependencyInjection/MultiTenantBuilder.cs b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs index 3390defd..6ad5bdc0 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs @@ -6,7 +6,6 @@ // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; -// TODO refactor methods into extensions /// /// Builder class for Finbuckle.MultiTenant configuration. /// diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs index 423640ca..6921cd68 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/OptionsBuilderExtensions.cs @@ -5,11 +5,10 @@ // https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs using Finbuckle.MultiTenant; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; // ReSharper disable once CheckNamespace -namespace Finbuckle.Extensions.DependencyInjection; +namespace Microsoft.Extensions.DependencyInjection; public static class OptionsBuilderExtensions { @@ -19,19 +18,20 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new ConfigureNamedOptions( + new ConfigureNamedOptions>( optionsBuilder.Name, - sp.GetService(), - (options, dep) => + sp.GetRequiredService>(), + (options, mtcAccessor) => { - if (dep is not null) - configureOptions(options, dep); + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, tenantInfo); })); - + return optionsBuilder; } @@ -42,16 +42,17 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new ConfigureNamedOptions( + new ConfigureNamedOptions>( optionsBuilder.Name, sp.GetRequiredService(), - sp.GetService(), - (options, dep, tenantInfo) => + sp.GetRequiredService>(), + (options, dep, mtcAccessor) => { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; if (tenantInfo is not null) configureOptions(options, dep, tenantInfo); })); @@ -67,17 +68,18 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new ConfigureNamedOptions( + new ConfigureNamedOptions>( optionsBuilder.Name, sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetService(), - (options, dep1, dep2, tenantInfo) => + sp.GetRequiredService>(), + (options, dep1, dep2, mtcAccessor) => { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; if (tenantInfo is not null) configureOptions(options, dep1, dep2, tenantInfo); })); @@ -95,18 +97,19 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new ConfigureNamedOptions( + new ConfigureNamedOptions>( optionsBuilder.Name, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetService(), - (options, dep1, dep2, dep3, tenantInfo) => + sp.GetRequiredService>(), + (options, dep1, dep2, dep3, mtcAccessor) => { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; if (tenantInfo is not null) configureOptions(options, dep1, dep2, dep3, tenantInfo); })); @@ -125,19 +128,20 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new ConfigureNamedOptions( + new ConfigureNamedOptions>( optionsBuilder.Name, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetService(), - (options, dep1, dep2, dep3, dep4, tenantInfo) => + 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); })); @@ -145,60 +149,63 @@ public static class OptionsBuilderExtensions 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 TDep5 : class - where TTenantInfo : class, ITenantInfo, new() - { - if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - - FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); - - optionsBuilder.Services.AddTransient>(sp => - { - var tenantInfo = sp.GetService(); - return new ConfigureNamedOptions( - optionsBuilder.Name, - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - (options, dep1, dep2, dep3, dep4, dep5) => - { - if (tenantInfo is not null) - configureOptions(options, dep1, dep2, dep3, dep4, dep5, tenantInfo); - }); - }); + // 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; + // } - 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( + new PostConfigureOptions>( optionsBuilder.Name, - sp.GetService(), - (options, dep) => + sp.GetRequiredService>(), + (options, mtcAccessor) => { - if (dep is not null) - configureOptions(options, dep); + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; + if (tenantInfo is not null) + configureOptions(options, tenantInfo); })); - + return optionsBuilder; } @@ -209,16 +216,17 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new PostConfigureOptions( + new PostConfigureOptions>( optionsBuilder.Name, sp.GetRequiredService(), - sp.GetService(), - (options, dep, tenantInfo) => + sp.GetRequiredService>(), + (options, dep, mtcAccessor) => { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; if (tenantInfo is not null) configureOptions(options, dep, tenantInfo); })); @@ -234,17 +242,18 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new PostConfigureOptions( + new PostConfigureOptions>( optionsBuilder.Name, sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetService(), - (options, dep1, dep2, tenantInfo) => + sp.GetRequiredService>(), + (options, dep1, dep2, mtcAccessor) => { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; if (tenantInfo is not null) configureOptions(options, dep1, dep2, tenantInfo); })); @@ -262,18 +271,19 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new PostConfigureOptions( + new PostConfigureOptions>( optionsBuilder.Name, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetService(), - (options, dep1, dep2, dep3, tenantInfo) => + sp.GetRequiredService>(), + (options, dep1, dep2, dep3, mtcAccessor) => { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; if (tenantInfo is not null) configureOptions(options, dep1, dep2, dep3, tenantInfo); })); @@ -292,19 +302,20 @@ public static class OptionsBuilderExtensions where TTenantInfo : class, ITenantInfo, new() { if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - + FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); optionsBuilder.Services.AddTransient>(sp => - new PostConfigureOptions( + new PostConfigureOptions>( optionsBuilder.Name, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetService(), - (options, dep1, dep2, dep3, dep4, tenantInfo) => + 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); })); @@ -312,600 +323,41 @@ public static class OptionsBuilderExtensions 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 TDep5 : class - where TTenantInfo : class, ITenantInfo, new() - { - if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); - - FinbuckleServiceCollectionExtensions.ConfigurePerTenantReqs(optionsBuilder.Services); - - optionsBuilder.Services.AddTransient>(sp => - { - var tenantInfo = sp.GetService(); - return new PostConfigureOptions( - optionsBuilder.Name, - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - (options, dep1, dep2, dep3, dep4, dep5) => - { - if (tenantInfo is not null) - configureOptions(options, dep1, dep2, dep3, dep4, dep5, tenantInfo); - }); - }); - - return optionsBuilder; - } -} -// -// /// -// /// Used to configure instances per-tenant. -// /// -// /// The type of options being requested. -// /// The ITenantInfo implementation type. -// public class PerTenantOptionsBuilder -// where TOptions : class -// where TTenantInfo : class, ITenantInfo, new() -// { -// private const string DefaultValidationFailureMessage = "A validation error has occurred."; -// -// /// -// /// The default name of the instance. -// /// -// public string Name { get; } -// -// /// -// /// The for the options being configured. -// /// -// public IServiceCollection Services { get; } -// -// /// -// /// Constructor. -// /// -// /// The for the options being configured. -// /// The default name of the instance, if null is used. -// public PerTenantOptionsBuilder(IServiceCollection services, string? name) -// { -// Services = services ?? throw new ArgumentNullException(nameof(services)); -// Name = name ?? Options.DefaultName; -// } -// -// /// -// /// Registers an action used to configure a particular type of options. -// /// Note: These are run before all . -// /// -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder Configure( -// Action configureOptions) -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// Services.ConfigurePerTenant(Name, configureOptions); -// return this; -// } -// -// /// -// /// Registers an action used to configure a particular type of options. -// /// Note: These are run before all . -// /// -// /// A dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder Configure( -// Action configureOptions) -// where TDep : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>(sp => -// { -// var multiTenantContextAccessor = sp.GetRequiredService>(); -// -// return new ConfigureNamedOptions(Name, sp.GetRequiredService(), (options, dep1) => -// { -// var multiTenantContext = multiTenantContextAccessor.MultiTenantContext; -// if (multiTenantContext.HasResolvedTenant) -// configureOptions(options, dep1, multiTenantContext.TenantInfo); -// }); -// }); -// -// return this; -// } -// -// /// -// /// Registers an action used to configure a particular type of options. -// /// Note: These are run before all . -// /// -// /// The first dependency used by the action. -// /// The second dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder Configure( -// Action configureOptions) -// where TDep1 : class -// where TDep2 : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>(sp => -// new ConfigureNamedOptions(Name, sp.GetRequiredService(), -// sp.GetRequiredService(), configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to configure a particular type of options. -// /// Note: These are run before all . -// /// -// /// The first dependency used by the action. -// /// The second dependency used by the action. -// /// The third dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder Configure( -// Action configureOptions) -// where TDep1 : class -// where TDep2 : class -// where TDep3 : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>( -// sp => new ConfigureNamedOptions( -// Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to configure a particular type of options. -// /// Note: These are run before all . -// /// -// /// The first dependency used by the action. -// /// The second dependency used by the action. -// /// The third dependency used by the action. -// /// The fourth dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder Configure( -// Action configureOptions) -// where TDep1 : class -// where TDep2 : class -// where TDep3 : class -// where TDep4 : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>( -// sp => new ConfigureNamedOptions( -// Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to configure a particular type of options. -// /// Note: These are run before all . -// /// -// /// The first dependency used by the action. -// /// The second dependency used by the action. -// /// The third dependency used by the action. -// /// The fourth dependency used by the action. -// /// The fifth dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder Configure( -// Action configureOptions) -// where TDep1 : class -// where TDep2 : class -// where TDep3 : class -// where TDep4 : class -// where TDep5 : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>( -// sp => new ConfigureNamedOptions( -// Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to configure a particular type of options. -// /// Note: These are run after all . -// /// -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder PostConfigure(Action configureOptions) -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddSingleton>( -// new PostConfigureOptions(Name, configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to post configure a particular type of options. -// /// Note: These are run after all . -// /// -// /// The dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder PostConfigure( -// Action configureOptions) -// where TDep : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>(sp => -// new PostConfigureOptions(Name, sp.GetRequiredService(), configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to post configure a particular type of options. -// /// Note: These are run after all . -// /// -// /// The first dependency used by the action. -// /// The second dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder PostConfigure( -// Action configureOptions) -// where TDep1 : class -// where TDep2 : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>(sp => -// new PostConfigureOptions(Name, sp.GetRequiredService(), -// sp.GetRequiredService(), configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to post configure a particular type of options. -// /// Note: These are run after all . -// /// -// /// The first dependency used by the action. -// /// The second dependency used by the action. -// /// The third dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder PostConfigure( -// Action configureOptions) -// where TDep1 : class -// where TDep2 : class -// where TDep3 : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>( -// sp => new PostConfigureOptions( -// Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to post configure a particular type of options. -// /// Note: These are run after all . -// /// -// /// The first dependency used by the action. -// /// The second dependency used by the action. -// /// The third dependency used by the action. -// /// The fourth dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder PostConfigure( -// Action configureOptions) -// where TDep1 : class -// where TDep2 : class -// where TDep3 : class -// where TDep4 : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>( -// sp => new PostConfigureOptions( -// Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// configureOptions)); -// return this; -// } -// -// /// -// /// Registers an action used to post configure a particular type of options. -// /// Note: These are run after all . -// /// -// /// The first dependency used by the action. -// /// The second dependency used by the action. -// /// The third dependency used by the action. -// /// The fourth dependency used by the action. -// /// The fifth dependency used by the action. -// /// The action used to configure the options. -// /// The current . -// public virtual PerTenantOptionsBuilder PostConfigure( -// Action configureOptions) -// where TDep1 : class -// where TDep2 : class -// where TDep3 : class -// where TDep4 : class -// where TDep5 : class -// { -// if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); -// -// Services.AddTransient>( -// sp => new PostConfigureOptions( -// Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// configureOptions)); -// return this; -// } -// -// /// -// /// Register a validation action for an options type using a default failure message. -// /// -// /// The validation function. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate(Func validation) -// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); -// -// /// -// /// Register a validation action for an options type. -// /// -// /// The validation function. -// /// The failure message to use when validation fails. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate(Func validation, -// string failureMessage) -// { -// if (validation == null) throw new ArgumentNullException(nameof(validation)); -// -// Services.AddSingleton>( -// new ValidateOptions(Name, validation, failureMessage)); -// return this; -// } -// -// /// -// /// Register a validation action for an options type using a default failure message. -// /// -// /// The dependency used by the validation function. -// /// The validation function. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate(Func validation) -// where TDep : notnull -// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); -// -// /// -// /// Register a validation action for an options type. -// /// -// /// The dependency used by the validation function. -// /// The validation function. -// /// The failure message to use when validation fails. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate(Func validation, -// string failureMessage) -// where TDep : notnull -// { -// if (validation == null) throw new ArgumentNullException(nameof(validation)); -// -// Services.AddTransient>(sp => -// new ValidateOptions(Name, sp.GetRequiredService(), validation, failureMessage)); -// return this; -// } -// -// /// -// /// Register a validation action for an options type using a default failure message. -// /// -// /// The first dependency used by the validation function. -// /// The second dependency used by the validation function. -// /// The validation function. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate( -// Func validation) -// where TDep1 : notnull -// where TDep2 : notnull -// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); -// -// /// -// /// Register a validation action for an options type. -// /// -// /// The first dependency used by the validation function. -// /// The second dependency used by the validation function. -// /// The validation function. -// /// The failure message to use when validation fails. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate( -// Func validation, -// string failureMessage) -// where TDep1 : notnull -// where TDep2 : notnull -// { -// if (validation == null) throw new ArgumentNullException(nameof(validation)); -// -// -// Services.AddTransient>(sp => -// new ValidateOptions(Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// validation, -// failureMessage)); -// return this; -// } -// -// /// -// /// Register a validation action for an options type using a default failure message. -// /// -// /// The first dependency used by the validation function. -// /// The second dependency used by the validation function. -// /// The third dependency used by the validation function. -// /// The validation function. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate( -// Func validation) -// where TDep1 : notnull -// where TDep2 : notnull -// where TDep3 : notnull -// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); -// -// /// -// /// Register a validation action for an options type. -// /// -// /// The first dependency used by the validation function. -// /// The second dependency used by the validation function. -// /// The third dependency used by the validation function. -// /// The validation function. -// /// The failure message to use when validation fails. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate( -// Func validation, string failureMessage) -// where TDep1 : notnull -// where TDep2 : notnull -// where TDep3 : notnull -// { -// if (validation == null) throw new ArgumentNullException(nameof(validation)); -// -// Services.AddTransient>(sp => -// new ValidateOptions(Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// validation, -// failureMessage)); -// return this; -// } -// -// /// -// /// Register a validation action for an options type using a default failure message. -// /// -// /// The first dependency used by the validation function. -// /// The second dependency used by the validation function. -// /// The third dependency used by the validation function. -// /// The fourth dependency used by the validation function. -// /// The validation function. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate( -// Func validation) -// where TDep1 : notnull -// where TDep2 : notnull -// where TDep3 : notnull -// where TDep4 : notnull -// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); -// -// /// -// /// Register a validation action for an options type. -// /// -// /// The first dependency used by the validation function. -// /// The second dependency used by the validation function. -// /// The third dependency used by the validation function. -// /// The fourth dependency used by the validation function. -// /// The validation function. -// /// The failure message to use when validation fails. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate( -// Func validation, string failureMessage) -// where TDep1 : notnull -// where TDep2 : notnull -// where TDep3 : notnull -// where TDep4 : notnull -// { -// if (validation == null) throw new ArgumentNullException(nameof(validation)); -// -// Services.AddTransient>(sp => -// new ValidateOptions(Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// validation, -// failureMessage)); -// return this; -// } -// -// /// -// /// Register a validation action for an options type using a default failure message. -// /// -// /// The first dependency used by the validation function. -// /// The second dependency used by the validation function. -// /// The third dependency used by the validation function. -// /// The fourth dependency used by the validation function. -// /// The fifth dependency used by the validation function. -// /// The validation function. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate( -// Func validation) -// where TDep1 : notnull -// where TDep2 : notnull -// where TDep3 : notnull -// where TDep4 : notnull -// where TDep5 : notnull -// => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); -// -// /// -// /// Register a validation action for an options type. -// /// -// /// The first dependency used by the validation function. -// /// The second dependency used by the validation function. -// /// The third dependency used by the validation function. -// /// The fourth dependency used by the validation function. -// /// The fifth dependency used by the validation function. -// /// The validation function. -// /// The failure message to use when validation fails. -// /// The current . -// public virtual PerTenantOptionsBuilder Validate( -// Func validation, string failureMessage) -// where TDep1 : notnull -// where TDep2 : notnull -// where TDep3 : notnull -// where TDep4 : notnull -// where TDep5 : notnull -// { -// if (validation == null) throw new ArgumentNullException(nameof(validation)); -// -// Services.AddTransient>(sp => -// new ValidateOptions(Name, -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// sp.GetRequiredService(), -// validation, -// failureMessage)); -// return this; -// } -// } \ No newline at end of file + // 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 index 178de48a..66f48e83 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/ServiceCollectionExtensions.cs @@ -143,11 +143,12 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(services); services.AddTransient>(sp => - new ConfigureNamedOptions( + new ConfigureNamedOptions>( name, - sp.GetService(), - (options, tenantInfo) => + sp.GetRequiredService>(), + (options, mtcAccessor) => { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; if (tenantInfo is not null) configureOptions(options, tenantInfo); })); @@ -207,11 +208,12 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(services); services.AddTransient>(sp => - new PostConfigureOptions( + new PostConfigureOptions>( name, - sp.GetService(), - (options, tenantInfo) => + sp.GetRequiredService>(), + (options, mtcAccessor) => { + var tenantInfo = mtcAccessor.MultiTenantContext?.TenantInfo; if (tenantInfo is not null) configureOptions(options, tenantInfo); })); diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs index 79150d33..392f3825 100644 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs +++ b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs @@ -13,12 +13,8 @@ public class MultiTenantOptionsCache : IOptionsMonitorCache where TOptions : class { 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 ConcurrentDictionary> map = new(); /// /// Constructs a new instance of MultiTenantOptionsCache. @@ -27,7 +23,8 @@ public class MultiTenantOptionsCache : IOptionsMonitorCache /// public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor) { - this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor)); + this.multiTenantContextAccessor = multiTenantContextAccessor ?? + throw new ArgumentNullException(nameof(multiTenantContextAccessor)); } /// @@ -96,112 +93,6 @@ public bool TryAdd(string? name, TOptions options) return cache.TryAdd(name, options); } - /// - /// Try to remove an options instance for the current tenant. - /// - /// The options name. - /// True if the options was removed from the cache for the current tenant. - public bool TryRemove(string? name) - { - name = name ?? Microsoft.Extensions.Options.Options.DefaultName; - var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; - var cache = map.GetOrAdd(tenantId, new OptionsCache()); - - return cache.TryRemove(name); - } -} - -/// -/// Adds, retrieves, and removes instances of TOptions after adjusting them for the current TenantContext. -/// -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>(); - - /// - /// Constructs a new instance of MultiTenantOptionsCache. - /// - /// - /// - public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor) - { - this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor)); - } - - /// - /// Clears all cached options for the current tenant. - /// - public void Clear() - { - var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; - var cache = map.GetOrAdd(tenantId, new OptionsCache()); - - cache.Clear(); - } - - /// - /// Clears all cached options for the given tenant. - /// - /// The Id of the tenant which will have its options cleared. - public void Clear(string tenantId) - { - var cache = map.GetOrAdd(tenantId, new OptionsCache()); - - cache.Clear(); - } - - /// - /// Clears all cached options for all tenants and no tenant. - /// - public void ClearAll() - { - foreach (var cache in map.Values) - cache.Clear(); - } - - /// - /// Gets a named options instance for the current tenant, or adds a new instance created with createOptions. - /// - /// The options name. - /// The factory function for creating the options instance. - /// The existing or new options instance. - public TOptions GetOrAdd(string? name, Func createOptions) - { - if (createOptions == null) - { - throw new ArgumentNullException(nameof(createOptions)); - } - - name = name ?? Microsoft.Extensions.Options.Options.DefaultName; - var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; - var cache = map.GetOrAdd(tenantId, new OptionsCache()); - - return cache.GetOrAdd(name, createOptions); - } - - /// - /// Tries to adds a new option to the cache for the current tenant. - /// - /// The options name. - /// The options instance. - /// True if the options was added to the cache for the current tenant. - public bool TryAdd(string? name, TOptions options) - { - name = name ?? Microsoft.Extensions.Options.Options.DefaultName; - var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; - var cache = map.GetOrAdd(tenantId, new OptionsCache()); - - return cache.TryAdd(name, options); - } - /// /// Try to remove an options instance for the current tenant. /// 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/DependencyInjection/ServiceCollectionExtensionsShould.cs b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionExtensionsShould.cs index 0b5eeae6..28b7ab8d 100644 --- a/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionExtensionsShould.cs +++ b/test/Finbuckle.MultiTenant.Test/DependencyInjection/ServiceCollectionExtensionsShould.cs @@ -100,21 +100,6 @@ public void RegisterIMultiTenantContextAccessorGenericInDi() Assert.Equal(ServiceLifetime.Singleton, service!.Lifetime); } - [Fact] - public void PreInitMultiTenantContextAccessorInDi() - { - var services = new ServiceCollection(); - services.AddMultiTenant(); - - var service = services.SingleOrDefault(s => s.Lifetime == ServiceLifetime.Singleton && - s.ServiceType == - typeof(IMultiTenantContextAccessor) && - s.ImplementationInstance is not null); - - Assert.NotNull(service); - Assert.Equal(ServiceLifetime.Singleton, service!.Lifetime); - } - [Fact] public void RegisterMultiTenantOptionsInDi() { @@ -143,10 +128,10 @@ public void RegisterNamedOptionsPerTenant() var sp = services.BuildServiceProvider(); var configs = sp.GetRequiredService>>(); - var config = configs.Where(config => config is ConfigureNamedOptions options).ToList(); + var config = configs.Where(config => config is ConfigureNamedOptions> options).ToList(); Assert.Single(config); - Assert.Equal("name1", config.Select(c => (ConfigureNamedOptions)c).Single().Name); + Assert.Equal("name1", config.Select(c => (ConfigureNamedOptions>)c).Single().Name); } [Fact] @@ -158,11 +143,26 @@ public void RegisterUnnamedOptionsPerTenant() var sp = services.BuildServiceProvider(); var configs = sp.GetRequiredService>>(); - var config = configs.Where(config => config is ConfigureNamedOptions options).ToList(); + 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); + 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/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/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 From 20500403a21262ee76bdb76854d91d051d977fb9 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 27 Feb 2024 22:25:02 -0700 Subject: [PATCH 09/10] partial update of the docs --- docs/Options.md | 100 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/docs/Options.md b/docs/Options.md index 395484cb..7ac7ad34 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -1,17 +1,23 @@ # 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 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. + +The current tenant determines which options are retrieved via +the `IOptions`, `IOptionsSnapshot`, or `IOptionsMonitor` instances' `Value` property and +`Get(string name)` method. A specialized variation of this is [per-tenant authentication](Authentication). 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. There is a caveat to be aware of: some +code, such as certain classes in ASP.NET Core or other libraries, may internally cache options resulting in only the +values from the first tenant being used despite the current tenant. + +## Options Basics Consider a typical scenario in ASP.Net Core, starting with a simple class: @@ -23,25 +29,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 +57,65 @@ public MyController : Controller _myOptions = optionsAccessor.Value; } } + +// or with a service provider +httpContext.RequestServices.GetServices(); ``` +At this point the options would be the same for all tenants. + ## Customizing Options Per Tenant -This sections assumes Finbuckle.MultiTenant is installed and configured. See [Getting Started](GettingStarted) for -details. +This sections assumes Finbuckle.MultiTenant is installed and 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) => +var builder = WebApplication.CreateBuilder(args); + +// configure options per tenant +builder.Services.ConfigurePerTenant((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) -. +// or configure named options per tenant +builder.Services.ConfigurePerTenant("scheme2", (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. +// ConfigureAll options variant +builder.Services.ConfigureAllPerTenant((options, tenantInfo) => + { + options.MyOption1 = tenantInfo.Option1Value; + options.MyOption2 = tenantInfo.Option2Value; + }); + +// 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; + }); -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.PostConfigurePerTenant("scheme2", (options, tenantInfo) => + { + options.MyOption1 = tenantInfo.Option1Value; + options.MyOption2 = tenantInfo.Option2Value; + }); + +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: From 734db0f7f585137db1db3d9861b2ba6634b8a435 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 16 Mar 2024 15:05:04 -0600 Subject: [PATCH 10/10] chore: update docs --- .gitignore | 2 ++ docs/Options.md | 84 ++++++++++++++++++++++++------------------------- 2 files changed, 44 insertions(+), 42 deletions(-) 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 7ac7ad34..5824ff66 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -1,21 +1,37 @@ # Per-Tenant Options +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. + +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. -A specialized variation of this is [per-tenant authentication](Authentication). - 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 a caveat to be aware of: some -code, such as certain classes in ASP.NET Core or other libraries, may internally cache options resulting in only the -values from the first tenant being used despite the current tenant. +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 @@ -62,11 +78,12 @@ public MyController : Controller httpContext.RequestServices.GetServices(); ``` -At this point the options would be the same for all tenants. +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 with a `TTenantInfo` type of `TenantInfo`. +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. To configure options per tenant, the standard `Configure` method variants on the service collection now all @@ -74,8 +91,6 @@ have `PerTenant` equivalents which accept a `Action` dele runtime the delegate will be called with the current tenant details. ```cs -var builder = WebApplication.CreateBuilder(args); - // configure options per tenant builder.Services.ConfigurePerTenant((options, tenantInfo) => { @@ -132,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.