Skip to content

Commit

Permalink
feat: add HasResolvedTenant to IMultiTenantContext (#650)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewTriesToCode committed Mar 10, 2023
1 parent 265d26d commit 375add5
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 41 deletions.
4 changes: 4 additions & 0 deletions src/Finbuckle.MultiTenant/Abstractions/IMultiTenantContext.cs
Expand Up @@ -6,13 +6,17 @@ namespace Finbuckle.MultiTenant
public interface IMultiTenantContext
{
ITenantInfo? TenantInfo { get; }
bool HasResolvedTenant => TenantInfo != null;

StrategyInfo? StrategyInfo { get; }
}

public interface IMultiTenantContext<T>
where T : class, ITenantInfo, new()
{
T? TenantInfo { get; set; }
bool HasResolvedTenant => TenantInfo != null;

StrategyInfo? StrategyInfo { get; set; }
StoreInfo<T>? StoreInfo { get; set; }
}
Expand Down
Expand Up @@ -75,6 +75,8 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services)
return this;
}

// TODO consider per tenant AllOptions variation

private static MultiTenantOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp)
where TOptions : class, new()
{
Expand Down
Expand Up @@ -27,7 +27,7 @@ public static FinbuckleMultiTenantBuilder<T> AddMultiTenant<T>(this IServiceColl
services.AddScoped<T>(sp => sp.GetRequiredService<IMultiTenantContextAccessor<T>>().MultiTenantContext?.TenantInfo!);
services.AddScoped<ITenantInfo>(sp => sp.GetService<T>()!);

services.AddSingleton<IMultiTenantContextAccessor<T>, MultiTenantContextAccessor<T>>();
services.AddSingleton<IMultiTenantContextAccessor<T>, AsyncLocalMultiTenantContextAccessor<T>>();
services.AddSingleton<IMultiTenantContextAccessor>(sp => (IMultiTenantContextAccessor)sp.GetRequiredService<IMultiTenantContextAccessor<T>>());

services.Configure<MultiTenantOptions>(config);
Expand Down
Expand Up @@ -5,22 +5,16 @@

namespace Finbuckle.MultiTenant.Core
{
public class MultiTenantContextAccessor<T> : IMultiTenantContextAccessor<T>, IMultiTenantContextAccessor
public class AsyncLocalMultiTenantContextAccessor<T> : IMultiTenantContextAccessor<T>, IMultiTenantContextAccessor
where T : class, ITenantInfo, new()
{
internal static AsyncLocal<IMultiTenantContext<T>?> _asyncLocalContext = new AsyncLocal<IMultiTenantContext<T>?>();
private static readonly AsyncLocal<IMultiTenantContext<T>?> _asyncLocalContext = new();

public IMultiTenantContext<T>? MultiTenantContext
{
get
{
return _asyncLocalContext.Value;
}
get => _asyncLocalContext.Value;

set
{
_asyncLocalContext.Value = value;
}
set => _asyncLocalContext.Value = value;
}

IMultiTenantContext? IMultiTenantContextAccessor.MultiTenantContext
Expand Down
1 change: 1 addition & 0 deletions src/Finbuckle.MultiTenant/MultiTenantContext.cs
Expand Up @@ -10,6 +10,7 @@ public class MultiTenantContext<T> : IMultiTenantContext<T>, IMultiTenantContext
where T : class, ITenantInfo, new()
{
public T? TenantInfo { get; set; }

public StrategyInfo? StrategyInfo { get; set; }
public StoreInfo<T>? StoreInfo { get; set; }

Expand Down
41 changes: 27 additions & 14 deletions src/Finbuckle.MultiTenant/Options/MultiTenantOptionsFactory.cs
Expand Up @@ -29,18 +29,30 @@ public class MultiTenantOptionsFactory<TOptions, TTenantInfo> : IOptionsFactory<
/// <summary>
/// Initializes a new instance with the specified options configurations.
/// </summary>
public MultiTenantOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions, IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions, IEnumerable<IValidateOptions<TOptions>> validations, IEnumerable<ITenantConfigureOptions<TOptions, TTenantInfo>> tenantConfigureOptions, IEnumerable<ITenantConfigureNamedOptions<TOptions, TTenantInfo>> tenantConfigureNamedOptions, IMultiTenantContextAccessor<TTenantInfo> multiTenantContextAccessor)
public MultiTenantOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions,
IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions,
IEnumerable<IValidateOptions<TOptions>> validations,
IEnumerable<ITenantConfigureOptions<TOptions, TTenantInfo>> tenantConfigureOptions,
IEnumerable<ITenantConfigureNamedOptions<TOptions, TTenantInfo>> tenantConfigureNamedOptions,
IMultiTenantContextAccessor<TTenantInfo> 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<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(configureOptions).ToArray();
_postConfigureOptions = postConfigureOptions as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigureOptions).ToArray();
_validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray();
_tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions<TOptions, TTenantInfo>[] ?? new List<ITenantConfigureOptions<TOptions, TTenantInfo>>(tenantConfigureOptions).ToArray();
_tenantConfigureNamedOptions = tenantConfigureNamedOptions as ITenantConfigureNamedOptions<TOptions, TTenantInfo>[] ?? new List<ITenantConfigureNamedOptions<TOptions, TTenantInfo>>(tenantConfigureNamedOptions).ToArray();
_configureOptions = configureOptions as IConfigureOptions<TOptions>[] ??
new List<IConfigureOptions<TOptions>>(configureOptions).ToArray();
_postConfigureOptions = postConfigureOptions as IPostConfigureOptions<TOptions>[] ??
new List<IPostConfigureOptions<TOptions>>(postConfigureOptions).ToArray();
_validations = validations as IValidateOptions<TOptions>[] ??
new List<IValidateOptions<TOptions>>(validations).ToArray();
_tenantConfigureOptions = tenantConfigureOptions as ITenantConfigureOptions<TOptions, TTenantInfo>[] ??
new List<ITenantConfigureOptions<TOptions, TTenantInfo>>(tenantConfigureOptions)
.ToArray();
_tenantConfigureNamedOptions =
tenantConfigureNamedOptions as ITenantConfigureNamedOptions<TOptions, TTenantInfo>[] ??
new List<ITenantConfigureNamedOptions<TOptions, TTenantInfo>>(tenantConfigureNamedOptions).ToArray();
_multiTenantContextAccessor = multiTenantContextAccessor;
}

Expand All @@ -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)
{
Expand All @@ -89,6 +101,7 @@ public TOptions Create(string name)
failures.AddRange(result.Failures);
}
}

if (failures.Count > 0)
{
throw new OptionsValidationException(name, typeof(TOptions), failures);
Expand All @@ -98,4 +111,4 @@ public TOptions Create(string name)
return options;
}
}
}
}
Expand Up @@ -20,7 +20,7 @@ public void GetTenantContextIfExists()
tc.TenantInfo = ti;

var services = new ServiceCollection();
services.AddScoped<IMultiTenantContextAccessor<TenantInfo>>(_ => new MultiTenantContextAccessor<TenantInfo>{ MultiTenantContext = tc });
services.AddScoped<IMultiTenantContextAccessor<TenantInfo>>(_ => new AsyncLocalMultiTenantContextAccessor<TenantInfo>{ MultiTenantContext = tc });
var sp = services.BuildServiceProvider();

var httpContextMock = new Mock<HttpContext>();
Expand All @@ -35,7 +35,7 @@ public void GetTenantContextIfExists()
public void ReturnNullIfNoMultiTenantContext()
{
var services = new ServiceCollection();
services.AddScoped<IMultiTenantContextAccessor<TenantInfo>>(_ => new MultiTenantContextAccessor<TenantInfo>());
services.AddScoped<IMultiTenantContextAccessor<TenantInfo>>(_ => new AsyncLocalMultiTenantContextAccessor<TenantInfo>());
var sp = services.BuildServiceProvider();

var httpContextMock = new Mock<HttpContext>();
Expand Down
40 changes: 40 additions & 0 deletions test/Finbuckle.MultiTenant.Test/MultiTenantContextShould.cs
@@ -0,0 +1,40 @@
using Xunit;

namespace Finbuckle.MultiTenant.Test;

public class MultiTenantContextShould
{
[Fact]
public void ReturnFalseInHasResolvedTenantIfTenantInfoIsNull()
{
IMultiTenantContext<TenantInfo> context = new MultiTenantContext<TenantInfo>();
Assert.False(context.HasResolvedTenant);
}

[Fact]
public void ReturnTrueInHasResolvedTenantIfTenantInfoIsNotNull()
{
IMultiTenantContext<TenantInfo> context = new MultiTenantContext<TenantInfo>();
context.TenantInfo = new TenantInfo();
Assert.True(context.HasResolvedTenant);
}

[Fact]
public void ReturnFalseInHasResolvedTenantIfTenantInfoIsNull_NonGeneric()
{
IMultiTenantContext context = new MultiTenantContext<TenantInfo>();
Assert.False(context.HasResolvedTenant);
}

[Fact]
public void ReturnTrueInHasResolvedTenantIfTenantInfoIsNotNull_NonGeneric()
{
var context = new MultiTenantContext<TenantInfo>
{
TenantInfo = new TenantInfo()
};

IMultiTenantContext iContext = context;
Assert.True(iContext.HasResolvedTenant);
}
}
Expand Up @@ -22,7 +22,7 @@ public void AddNamedOptionsForCurrentTenantOnlyOnAdd(string name)
var ti = new TenantInfo { Id = "test-id-123" };
var tc = new MultiTenantContext<TenantInfo>();
tc.TenantInfo = ti;
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
tca.MultiTenantContext = tc;
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

Expand All @@ -45,7 +45,7 @@ public void AddNamedOptionsForCurrentTenantOnlyOnAdd(string name)
[Fact]
public void HandleNullMultiTenantContextOnAdd()
{
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

var options = new TestOptions();
Expand All @@ -58,7 +58,7 @@ public void HandleNullMultiTenantContextOnAdd()
[Fact]
public void HandleNullMultiTenantContextOnGetOrAdd()
{
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

var options = new TestOptions();
Expand All @@ -77,7 +77,7 @@ public void GetOrAddNamedOptionForCurrentTenantOnly(string name)
var ti = new TenantInfo { Id = "test-id-123"};
var tc = new MultiTenantContext<TenantInfo>();
tc.TenantInfo = ti;
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
tca.MultiTenantContext = tc;
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

Expand All @@ -102,7 +102,7 @@ public void GetOrAddNamedOptionForCurrentTenantOnly(string name)
public void ThrowsIfGetOrAddFactoryIsNull()
{
var tc = new MultiTenantContext<TenantInfo>();
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
tca.MultiTenantContext = tc;
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

Expand All @@ -113,7 +113,7 @@ public void ThrowsIfGetOrAddFactoryIsNull()
public void ThrowIfContructorParamIsNull()
{
var tc = new MultiTenantContext<TenantInfo>();
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
tca.MultiTenantContext = tc;

Assert.Throws<ArgumentNullException>(() => new MultiTenantOptionsCache<TestOptions, TenantInfo>(null!));
Expand All @@ -128,7 +128,7 @@ public void RemoveNamedOptionsForCurrentTenantOnly(string name)
var ti = new TenantInfo { Id = "test-id-123" };
var tc = new MultiTenantContext<TenantInfo>();
tc.TenantInfo = ti;
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
tca.MultiTenantContext = tc;
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

Expand Down Expand Up @@ -172,7 +172,7 @@ public void ClearOptionsForCurrentTenantOnly()
var ti = new TenantInfo { Id = "test-id-123" };
var tc = new MultiTenantContext<TenantInfo>();
tc.TenantInfo = ti;
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
tca.MultiTenantContext = tc;
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

Expand Down Expand Up @@ -213,7 +213,7 @@ public void ClearOptionsForTenantIdOnly()
var ti = new TenantInfo { Id = "test-id-123" };
var tc = new MultiTenantContext<TenantInfo>();
tc.TenantInfo = ti;
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
tca.MultiTenantContext = tc;
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

Expand Down Expand Up @@ -252,7 +252,7 @@ public void ClearAllOptionsForClearAll()
var ti = new TenantInfo { Id = "test-id-123" };
var tc = new MultiTenantContext<TenantInfo>();
tc.TenantInfo = ti;
var tca = new MultiTenantContextAccessor<TenantInfo>();
var tca = new AsyncLocalMultiTenantContextAccessor<TenantInfo>();
tca.MultiTenantContext = tc;
var cache = new MultiTenantOptionsCache<TestOptions, TenantInfo>(tca);

Expand Down
12 changes: 8 additions & 4 deletions test/Finbuckle.MultiTenant.Test/TenantInfoShould.cs
Expand Up @@ -18,10 +18,14 @@ public void ThrowIfIdSetWithLengthAboveTenantIdMaxLength()
// OK
// ReSharper disable once ObjectCreationAsStatement
new TenantInfo { Id = "".PadRight(Constants.TenantIdMaxLength, 'a') };

Assert.Throws<MultiTenantException>(() => new TenantInfo{ Id = "".PadRight(Constants.TenantIdMaxLength + 1, 'a') });
Assert.Throws<MultiTenantException>(() => new TenantInfo{ Id = "".PadRight(Constants.TenantIdMaxLength
+ 999, 'a') });

Assert.Throws<MultiTenantException>(() => new TenantInfo
{ Id = "".PadRight(Constants.TenantIdMaxLength + 1, 'a') });
Assert.Throws<MultiTenantException>(() => new TenantInfo
{
Id = "".PadRight(Constants.TenantIdMaxLength
+ 999, 'a')
});
}
}
}

0 comments on commit 375add5

Please sign in to comment.