From 375add51e7317b03652556d4d1d0eb7ef20b8caf Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 10 Mar 2023 00:42:22 -0700 Subject: [PATCH] feat: add HasResolvedTenant to IMultiTenantContext (#650) --- .../Abstractions/IMultiTenantContext.cs | 4 ++ .../DependencyInjection/MultiTenantBuilder.cs | 2 + .../Extensions/ServiceCollectionExtensions.cs | 2 +- ...> AsyncLocalMultiTenantContextAccessor.cs} | 14 ++----- .../MultiTenantContext.cs | 1 + .../Options/MultiTenantOptionsFactory.cs | 41 ++++++++++++------- .../Extensions/HttpContextExtensionShould.cs | 4 +- .../MultiTenantContextShould.cs | 40 ++++++++++++++++++ .../Options/MultiTenantOptionsCacheShould.cs | 20 ++++----- .../TenantInfoShould.cs | 12 ++++-- 10 files changed, 99 insertions(+), 41 deletions(-) rename src/Finbuckle.MultiTenant/Internal/{MultiTenantContextAccessor.cs => AsyncLocalMultiTenantContextAccessor.cs} (57%) create mode 100644 test/Finbuckle.MultiTenant.Test/MultiTenantContextShould.cs diff --git a/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantContext.cs b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantContext.cs index cda9c9dd..ec4725c5 100644 --- a/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantContext.cs +++ b/src/Finbuckle.MultiTenant/Abstractions/IMultiTenantContext.cs @@ -6,6 +6,8 @@ namespace Finbuckle.MultiTenant public interface IMultiTenantContext { ITenantInfo? TenantInfo { get; } + bool HasResolvedTenant => TenantInfo != null; + StrategyInfo? StrategyInfo { get; } } @@ -13,6 +15,8 @@ public interface IMultiTenantContext where T : class, ITenantInfo, new() { T? TenantInfo { get; set; } + bool HasResolvedTenant => TenantInfo != null; + StrategyInfo? StrategyInfo { get; set; } StoreInfo? StoreInfo { get; set; } } diff --git a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs index 02489aa9..aa2ee6eb 100644 --- a/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs +++ b/src/Finbuckle.MultiTenant/DependencyInjection/MultiTenantBuilder.cs @@ -75,6 +75,8 @@ public FinbuckleMultiTenantBuilder WithPerTenantNamedOptions BuildOptionsManager(IServiceProvider sp) where TOptions : class, new() { diff --git a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs b/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs index bdadbf0d..623c7c58 100644 --- a/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs +++ b/src/Finbuckle.MultiTenant/Extensions/ServiceCollectionExtensions.cs @@ -27,7 +27,7 @@ public static FinbuckleMultiTenantBuilder AddMultiTenant(this IServiceColl services.AddScoped(sp => sp.GetRequiredService>().MultiTenantContext?.TenantInfo!); services.AddScoped(sp => sp.GetService()!); - services.AddSingleton, MultiTenantContextAccessor>(); + services.AddSingleton, AsyncLocalMultiTenantContextAccessor>(); services.AddSingleton(sp => (IMultiTenantContextAccessor)sp.GetRequiredService>()); services.Configure(config); diff --git a/src/Finbuckle.MultiTenant/Internal/MultiTenantContextAccessor.cs b/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs similarity index 57% rename from src/Finbuckle.MultiTenant/Internal/MultiTenantContextAccessor.cs rename to src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs index e2f8098b..de2c555d 100644 --- a/src/Finbuckle.MultiTenant/Internal/MultiTenantContextAccessor.cs +++ b/src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs @@ -5,22 +5,16 @@ namespace Finbuckle.MultiTenant.Core { - public class MultiTenantContextAccessor : IMultiTenantContextAccessor, IMultiTenantContextAccessor + public class AsyncLocalMultiTenantContextAccessor : IMultiTenantContextAccessor, IMultiTenantContextAccessor where T : class, ITenantInfo, new() { - internal static AsyncLocal?> _asyncLocalContext = new AsyncLocal?>(); + private static readonly AsyncLocal?> _asyncLocalContext = new(); public IMultiTenantContext? MultiTenantContext { - get - { - return _asyncLocalContext.Value; - } + get => _asyncLocalContext.Value; - set - { - _asyncLocalContext.Value = value; - } + set => _asyncLocalContext.Value = value; } IMultiTenantContext? IMultiTenantContextAccessor.MultiTenantContext diff --git a/src/Finbuckle.MultiTenant/MultiTenantContext.cs b/src/Finbuckle.MultiTenant/MultiTenantContext.cs index 97c230d4..624a9f36 100644 --- a/src/Finbuckle.MultiTenant/MultiTenantContext.cs +++ b/src/Finbuckle.MultiTenant/MultiTenantContext.cs @@ -10,6 +10,7 @@ public class MultiTenantContext : IMultiTenantContext, IMultiTenantContext where T : class, ITenantInfo, new() { public T? TenantInfo { get; set; } + public StrategyInfo? StrategyInfo { get; set; } public StoreInfo? StoreInfo { get; set; } diff --git a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs index 22c24df4..dae3dd89 100644 --- a/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs +++ b/src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs @@ -29,18 +29,30 @@ public class MultiTenantOptionsFactory : IOptionsFactory< /// /// Initializes a new instance with the specified options configurations. /// - public MultiTenantOptionsFactory(IEnumerable> configureOptions, IEnumerable> postConfigureOptions, IEnumerable> validations, IEnumerable> tenantConfigureOptions, IEnumerable> tenantConfigureNamedOptions, IMultiTenantContextAccessor multiTenantContextAccessor) + public MultiTenantOptionsFactory(IEnumerable> configureOptions, + IEnumerable> postConfigureOptions, + IEnumerable> validations, + IEnumerable> tenantConfigureOptions, + IEnumerable> tenantConfigureNamedOptions, + IMultiTenantContextAccessor multiTenantContextAccessor) { // The default DI container uses arrays under the covers. Take advantage of this knowledge // by checking for an array and enumerate over that, so we don't need to allocate an enumerator. // When it isn't already an array, convert it to one, but don't use System.Linq to avoid pulling Linq in to // small trimmed applications. - _configureOptions = configureOptions as IConfigureOptions[] ?? new List>(configureOptions).ToArray(); - _postConfigureOptions = postConfigureOptions as IPostConfigureOptions[] ?? new List>(postConfigureOptions).ToArray(); - _validations = validations as IValidateOptions[] ?? new List>(validations).ToArray(); - _tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions[] ?? new List>(tenantConfigureOptions).ToArray(); - _tenantConfigureNamedOptions = tenantConfigureNamedOptions as ITenantConfigureNamedOptions[] ?? new List>(tenantConfigureNamedOptions).ToArray(); + _configureOptions = configureOptions as IConfigureOptions[] ?? + new List>(configureOptions).ToArray(); + _postConfigureOptions = postConfigureOptions as IPostConfigureOptions[] ?? + new List>(postConfigureOptions).ToArray(); + _validations = validations as IValidateOptions[] ?? + new List>(validations).ToArray(); + _tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions[] ?? + new List>(tenantConfigureOptions) + .ToArray(); + _tenantConfigureNamedOptions = + tenantConfigureNamedOptions as ITenantConfigureNamedOptions[] ?? + new List>(tenantConfigureNamedOptions).ToArray(); _multiTenantContextAccessor = multiTenantContextAccessor; } @@ -60,23 +72,23 @@ public TOptions Create(string name) } // Configure tenant options. - if (_multiTenantContextAccessor?.MultiTenantContext?.TenantInfo != null) + if (_multiTenantContextAccessor?.MultiTenantContext?.HasResolvedTenant ?? false) { foreach (var tenantConfigureOption in _tenantConfigureOptions) - tenantConfigureOption.Configure(options, _multiTenantContextAccessor.MultiTenantContext.TenantInfo); - } + tenantConfigureOption.Configure(options, _multiTenantContextAccessor.MultiTenantContext.TenantInfo!); - // Configure tenant named options. - if (_multiTenantContextAccessor?.MultiTenantContext?.TenantInfo != null) - { + // Configure tenant named options. foreach (var tenantConfigureNamedOption in _tenantConfigureNamedOptions) - tenantConfigureNamedOption.Configure(name, options, _multiTenantContextAccessor.MultiTenantContext.TenantInfo); + 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) { @@ -89,6 +101,7 @@ public TOptions Create(string name) failures.AddRange(result.Failures); } } + if (failures.Count > 0) { throw new OptionsValidationException(name, typeof(TOptions), failures); @@ -98,4 +111,4 @@ public TOptions Create(string name) return options; } } -} +} \ No newline at end of file diff --git a/test/Finbuckle.MultiTenant.AspNetCore.Test/Extensions/HttpContextExtensionShould.cs b/test/Finbuckle.MultiTenant.AspNetCore.Test/Extensions/HttpContextExtensionShould.cs index 35cdfb78..24ad12a7 100644 --- a/test/Finbuckle.MultiTenant.AspNetCore.Test/Extensions/HttpContextExtensionShould.cs +++ b/test/Finbuckle.MultiTenant.AspNetCore.Test/Extensions/HttpContextExtensionShould.cs @@ -20,7 +20,7 @@ public void GetTenantContextIfExists() tc.TenantInfo = ti; var services = new ServiceCollection(); - services.AddScoped>(_ => new MultiTenantContextAccessor{ MultiTenantContext = tc }); + services.AddScoped>(_ => new AsyncLocalMultiTenantContextAccessor{ MultiTenantContext = tc }); var sp = services.BuildServiceProvider(); var httpContextMock = new Mock(); @@ -35,7 +35,7 @@ public void GetTenantContextIfExists() public void ReturnNullIfNoMultiTenantContext() { var services = new ServiceCollection(); - services.AddScoped>(_ => new MultiTenantContextAccessor()); + services.AddScoped>(_ => new AsyncLocalMultiTenantContextAccessor()); var sp = services.BuildServiceProvider(); var httpContextMock = new Mock(); diff --git a/test/Finbuckle.MultiTenant.Test/MultiTenantContextShould.cs b/test/Finbuckle.MultiTenant.Test/MultiTenantContextShould.cs new file mode 100644 index 00000000..46d47402 --- /dev/null +++ b/test/Finbuckle.MultiTenant.Test/MultiTenantContextShould.cs @@ -0,0 +1,40 @@ +using Xunit; + +namespace Finbuckle.MultiTenant.Test; + +public class MultiTenantContextShould +{ + [Fact] + public void ReturnFalseInHasResolvedTenantIfTenantInfoIsNull() + { + IMultiTenantContext context = new MultiTenantContext(); + Assert.False(context.HasResolvedTenant); + } + + [Fact] + public void ReturnTrueInHasResolvedTenantIfTenantInfoIsNotNull() + { + IMultiTenantContext context = new MultiTenantContext(); + context.TenantInfo = new TenantInfo(); + Assert.True(context.HasResolvedTenant); + } + + [Fact] + public void ReturnFalseInHasResolvedTenantIfTenantInfoIsNull_NonGeneric() + { + IMultiTenantContext context = new MultiTenantContext(); + Assert.False(context.HasResolvedTenant); + } + + [Fact] + public void ReturnTrueInHasResolvedTenantIfTenantInfoIsNotNull_NonGeneric() + { + var context = new MultiTenantContext + { + TenantInfo = new TenantInfo() + }; + + IMultiTenantContext iContext = context; + Assert.True(iContext.HasResolvedTenant); + } +} \ 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 d6edb2a5..11fbf0ad 100644 --- a/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsCacheShould.cs +++ b/test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsCacheShould.cs @@ -22,7 +22,7 @@ public void AddNamedOptionsForCurrentTenantOnlyOnAdd(string name) var ti = new TenantInfo { Id = "test-id-123" }; var tc = new MultiTenantContext(); tc.TenantInfo = ti; - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; var cache = new MultiTenantOptionsCache(tca); @@ -45,7 +45,7 @@ public void AddNamedOptionsForCurrentTenantOnlyOnAdd(string name) [Fact] public void HandleNullMultiTenantContextOnAdd() { - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); @@ -58,7 +58,7 @@ public void HandleNullMultiTenantContextOnAdd() [Fact] public void HandleNullMultiTenantContextOnGetOrAdd() { - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); var cache = new MultiTenantOptionsCache(tca); var options = new TestOptions(); @@ -77,7 +77,7 @@ public void GetOrAddNamedOptionForCurrentTenantOnly(string name) var ti = new TenantInfo { Id = "test-id-123"}; var tc = new MultiTenantContext(); tc.TenantInfo = ti; - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; var cache = new MultiTenantOptionsCache(tca); @@ -102,7 +102,7 @@ public void GetOrAddNamedOptionForCurrentTenantOnly(string name) public void ThrowsIfGetOrAddFactoryIsNull() { var tc = new MultiTenantContext(); - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; var cache = new MultiTenantOptionsCache(tca); @@ -113,7 +113,7 @@ public void ThrowsIfGetOrAddFactoryIsNull() public void ThrowIfContructorParamIsNull() { var tc = new MultiTenantContext(); - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; Assert.Throws(() => new MultiTenantOptionsCache(null!)); @@ -128,7 +128,7 @@ public void RemoveNamedOptionsForCurrentTenantOnly(string name) var ti = new TenantInfo { Id = "test-id-123" }; var tc = new MultiTenantContext(); tc.TenantInfo = ti; - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; var cache = new MultiTenantOptionsCache(tca); @@ -172,7 +172,7 @@ public void ClearOptionsForCurrentTenantOnly() var ti = new TenantInfo { Id = "test-id-123" }; var tc = new MultiTenantContext(); tc.TenantInfo = ti; - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; var cache = new MultiTenantOptionsCache(tca); @@ -213,7 +213,7 @@ public void ClearOptionsForTenantIdOnly() var ti = new TenantInfo { Id = "test-id-123" }; var tc = new MultiTenantContext(); tc.TenantInfo = ti; - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; var cache = new MultiTenantOptionsCache(tca); @@ -252,7 +252,7 @@ public void ClearAllOptionsForClearAll() var ti = new TenantInfo { Id = "test-id-123" }; var tc = new MultiTenantContext(); tc.TenantInfo = ti; - var tca = new MultiTenantContextAccessor(); + var tca = new AsyncLocalMultiTenantContextAccessor(); tca.MultiTenantContext = tc; var cache = new MultiTenantOptionsCache(tca); diff --git a/test/Finbuckle.MultiTenant.Test/TenantInfoShould.cs b/test/Finbuckle.MultiTenant.Test/TenantInfoShould.cs index 7dfc1d76..5bcefff3 100644 --- a/test/Finbuckle.MultiTenant.Test/TenantInfoShould.cs +++ b/test/Finbuckle.MultiTenant.Test/TenantInfoShould.cs @@ -18,10 +18,14 @@ public void ThrowIfIdSetWithLengthAboveTenantIdMaxLength() // OK // ReSharper disable once ObjectCreationAsStatement new TenantInfo { Id = "".PadRight(Constants.TenantIdMaxLength, 'a') }; - - Assert.Throws(() => new TenantInfo{ Id = "".PadRight(Constants.TenantIdMaxLength + 1, 'a') }); - Assert.Throws(() => new TenantInfo{ Id = "".PadRight(Constants.TenantIdMaxLength - + 999, 'a') }); + + Assert.Throws(() => new TenantInfo + { Id = "".PadRight(Constants.TenantIdMaxLength + 1, 'a') }); + Assert.Throws(() => new TenantInfo + { + Id = "".PadRight(Constants.TenantIdMaxLength + + 999, 'a') + }); } } } \ No newline at end of file