diff --git a/Packages/DotNET/Tenancy/Source/AspNetCore/HttpContextExtensions.cs b/Packages/DotNET/Tenancy/Source/AspNetCore/HttpContextExtensions.cs
new file mode 100644
index 0000000..b674365
--- /dev/null
+++ b/Packages/DotNET/Tenancy/Source/AspNetCore/HttpContextExtensions.cs
@@ -0,0 +1,39 @@
+// Copyright (c) woksin-org. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.AspNetCore.Http;
+using Woksin.Extensions.Tenancy.Context;
+
+namespace Woksin.Extensions.Tenancy.AspNetCore;
+
+///
+/// Extension methods for getting from the .
+///
+public static class HttpContextExtensions
+{
+ public static ITenantContext GetTenantContext(this HttpContext context)
+ where TTenant : class, ITenantInfo, new()
+ {
+ ITenantContext result = TenantContext.Unresolved();
+ if (context.Items.TryGetValue(TenancyMiddleware.TenantContextItemKey, out var tenantContext) && tenantContext is not null)
+ {
+ result = tenantContext as ITenantContext ?? throw new InvalidCastException($"Could not cast tenant context of type {tenantContext.GetType()} to {typeof(ITenantContext)}");
+ }
+ return result;
+ }
+
+ public static ITenantContext GetTenantContext(this HttpContext context)
+ {
+ ITenantContext result = TenantContext.Unresolved();
+ if (context.Items.TryGetValue(TenancyMiddleware.TenantContextItemKey, out var tenantContext) && tenantContext is not null)
+ {
+ result = tenantContext as ITenantContext ?? throw new InvalidCastException($"Could not cast tenant context of type {tenantContext.GetType()} to {typeof(ITenantContext)}");
+ }
+ return result;
+ }
+
+ public static ITenantContext GetTenantContext(this HttpRequest request) => request.HttpContext.GetTenantContext();
+ public static ITenantContext GetTenantContext(this HttpRequest request)
+ where TTenant : class, ITenantInfo, new() => request.HttpContext.GetTenantContext();
+
+}
diff --git a/Packages/DotNET/Tenancy/Source/AspNetCore/TenancyMiddleware.cs b/Packages/DotNET/Tenancy/Source/AspNetCore/TenancyMiddleware.cs
index e905025..8b38f0a 100644
--- a/Packages/DotNET/Tenancy/Source/AspNetCore/TenancyMiddleware.cs
+++ b/Packages/DotNET/Tenancy/Source/AspNetCore/TenancyMiddleware.cs
@@ -15,6 +15,7 @@ namespace Woksin.Extensions.Tenancy.AspNetCore;
public partial class TenancyMiddleware
{
readonly RequestDelegate _next;
+ public static readonly object TenantContextItemKey = new();
public TenancyMiddleware(RequestDelegate next)
{
@@ -26,15 +27,18 @@ public async Task Invoke(HttpContext context)
var logger = context.RequestServices.GetService>() ?? NullLogger.Instance;
LogGettingTenantContext(logger);
var accessor = context.RequestServices.GetRequiredService();
- if (!accessor.CurrentTenant.Resolved(out var tenantInfo, out _))
+ if (!accessor.CurrentTenant.Resolved(out var tenantInfo, out var currentTenantContext))
{
LogResolvingTenantContext(logger);
var resolver = context.RequestServices.GetRequiredService();
- accessor.CurrentTenant = await resolver.Resolve(context);
+ var resolvedTenant = await resolver.Resolve(context);
+ accessor.CurrentTenant = resolvedTenant;
+ context.Items[TenantContextItemKey] = resolvedTenant;
}
else
{
LogTenantContextExists(logger, tenantInfo.Id, tenantInfo.Name);
+ context.Items[TenantContextItemKey] = currentTenantContext;
}
await _next(context);
diff --git a/Packages/DotNET/Tenancy/Source/Base/ActionInTenantContextPerformer.cs b/Packages/DotNET/Tenancy/Source/Base/ActionInTenantContextPerformer.cs
index 2be0195..6a689b3 100644
--- a/Packages/DotNET/Tenancy/Source/Base/ActionInTenantContextPerformer.cs
+++ b/Packages/DotNET/Tenancy/Source/Base/ActionInTenantContextPerformer.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.Options;
using Woksin.Extensions.Tenancy.Context;
namespace Woksin.Extensions.Tenancy;
@@ -11,15 +12,23 @@ public class ActionInTenantContextPerformer : IPerformActionInTenantCon
{
readonly ITenantContextAccessor _tenantContextAccessor;
readonly ITenantContextAccessor _nonTypedTenantContextAccessor;
+ readonly IOptionsMonitor> _options;
- public ActionInTenantContextPerformer(ITenantContextAccessor tenantContextAccessor, ITenantContextAccessor nonTypedTenantContextAccessor)
+ public ActionInTenantContextPerformer(ITenantContextAccessor tenantContextAccessor, ITenantContextAccessor nonTypedTenantContextAccessor, IOptionsMonitor> options)
{
_tenantContextAccessor = tenantContextAccessor;
_nonTypedTenantContextAccessor = nonTypedTenantContextAccessor;
+ _options = options;
}
+ void ThrowIfTenantContextDisabled()
+ {
+ ITenantContextAccessor.ThrowIfDisabledTenantContext(_tenantContextAccessor, _options.CurrentValue);
+ }
+
public async Task Perform(ITenantContext tenant, Func, TResult> callback)
{
+ ThrowIfTenantContextDisabled();
return await Task.Run(() =>
{
_tenantContextAccessor.CurrentTenant = tenant;
@@ -28,6 +37,7 @@ public async Task Perform(ITenantContext tenant, Func
}
public async Task Perform(ITenantContext tenant, Func, Task> callback)
{
+ ThrowIfTenantContextDisabled();
return await Task.Run(async() =>
{
_tenantContextAccessor.CurrentTenant = tenant;
@@ -37,6 +47,7 @@ public async Task Perform(ITenantContext tenant, Func
public async Task Perform(ITenantContext tenant, Action> callback)
{
+ ThrowIfTenantContextDisabled();
await Task.Run(() =>
{
_tenantContextAccessor.CurrentTenant = tenant;
@@ -46,6 +57,7 @@ await Task.Run(() =>
public async Task Perform(ITenantContext tenant, Func, Task> callback)
{
+ ThrowIfTenantContextDisabled();
await Task.Run(async () =>
{
_tenantContextAccessor.CurrentTenant = tenant;
diff --git a/Packages/DotNET/Tenancy/Source/Base/Context/ITenantContextAccessor.cs b/Packages/DotNET/Tenancy/Source/Base/Context/ITenantContextAccessor.cs
index e136753..a4668c9 100644
--- a/Packages/DotNET/Tenancy/Source/Base/Context/ITenantContextAccessor.cs
+++ b/Packages/DotNET/Tenancy/Source/Base/Context/ITenantContextAccessor.cs
@@ -14,6 +14,33 @@ public interface ITenantContextAccessor
/// Gets the current .
///
ITenantContext CurrentTenant { get; set; }
+
+ ///
+ /// Throws an exception if the AsyncLocal tenant context is disabled.
+ ///
+ /// The registered .
+ /// The configured .
+ /// The exception that is thrown.
+ public static void ThrowIfDisabledTenantContext(ITenantContextAccessor accessor, TenancyOptions tenancyOptions)
+ {
+ if (accessor is StaticTenantContextAccessor && !tenancyOptions.IsUsingStaticTenant(out _))
+ {
+ throw new InvalidOperationException("Current tenant context cannot be resolved when AsyncLocal Tenant Context is disabled");
+ }
+ }
+ ///
+ /// Throws an exception if the AsyncLocal tenant context is disabled.
+ ///
+ /// The registered .
+ /// The configured .
+ /// The exception that is thrown.
+ public static void ThrowIfDisabledTenantContext(ITenantContextAccessor accessor, TenancyOptions tenancyOptions)
+ {
+ if (accessor is StaticTenantContextAccessor && !tenancyOptions.IsUsingStaticTenant(out _))
+ {
+ throw new InvalidOperationException("Current tenant context cannot be resolved when AsyncLocal Tenant Context is disabled");
+ }
+ }
}
///
diff --git a/Packages/DotNET/Tenancy/Source/Base/Context/StaticTenantContextAccessor.cs b/Packages/DotNET/Tenancy/Source/Base/Context/StaticTenantContextAccessor.cs
new file mode 100644
index 0000000..819adfa
--- /dev/null
+++ b/Packages/DotNET/Tenancy/Source/Base/Context/StaticTenantContextAccessor.cs
@@ -0,0 +1,61 @@
+// Copyright (c) woksin-org. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Woksin.Extensions.Tenancy.Context;
+
+///
+/// Represents static implementation of and that will be in use if AsyncLocal tenant context is disabled.
+///
+/// Unless is configured this implementation will always return the 'unresolved' .
+/// The of the .
+public partial class StaticTenantContextAccessor : ITenantContextAccessor, ITenantContextAccessor
+ where TTenant : class, ITenantInfo, new()
+{
+ readonly IOptionsMonitor> _options;
+ readonly ILogger> _logger;
+
+ public StaticTenantContextAccessor(IOptionsMonitor> options, ILogger> logger)
+ {
+ _options = options;
+ _logger = logger;
+ }
+
+ ///
+ public ITenantContext CurrentTenant
+ {
+ get
+ {
+ if (_options.CurrentValue.IsUsingStaticTenant(out var staticTenantId))
+ {
+ return TenantContext.Static(new TTenant
+ {
+ Id = staticTenantId
+ });
+ }
+ LogReturningUnresolvedContext(_logger);
+ return TenantContext.Unresolved();
+ }
+
+ // ReSharper disable once ValueParameterNotUsed
+ set
+ {
+ LogCannotSetContext(_logger);
+ }
+ }
+
+ ///
+ ITenantContext ITenantContextAccessor.CurrentTenant
+ {
+ get => CurrentTenant as ITenantContext ?? TenantContext.Unresolved();
+ set => CurrentTenant = value as ITenantContext ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ [LoggerMessage(LogLevel.Warning, "When tenant context is disabled it is not possible to set the tenant context")]
+ static partial void LogCannotSetContext(ILogger logger);
+
+ [LoggerMessage(LogLevel.Warning, "When tenant context is disabled and static tenant is not configured the tenant context will always be unresolved")]
+ static partial void LogReturningUnresolvedContext(ILogger logger);
+}
diff --git a/Packages/DotNET/Tenancy/Source/Base/Context/TenantContextAccessor.cs b/Packages/DotNET/Tenancy/Source/Base/Context/TenantContextAccessor.cs
index 9b6709a..b334b23 100644
--- a/Packages/DotNET/Tenancy/Source/Base/Context/TenantContextAccessor.cs
+++ b/Packages/DotNET/Tenancy/Source/Base/Context/TenantContextAccessor.cs
@@ -26,9 +26,10 @@ public ITenantContext CurrentTenant
{
get
{
+ // This is simply a minor performance optimization to avoid accessing the AsyncLocal when not necessary.
if (_options.CurrentValue.IsUsingStaticTenant(out var staticTenantId))
{
- return TenantContext.Static(new TTenant()
+ return TenantContext.Static(new TTenant
{
Id = staticTenantId
});
diff --git a/Packages/DotNET/Tenancy/Source/Base/Context/TenantResolver.cs b/Packages/DotNET/Tenancy/Source/Base/Context/TenantResolver.cs
index 5782748..508ae2b 100644
--- a/Packages/DotNET/Tenancy/Source/Base/Context/TenantResolver.cs
+++ b/Packages/DotNET/Tenancy/Source/Base/Context/TenantResolver.cs
@@ -73,28 +73,24 @@ public async Task> Resolve(object context)
return (false, null);
}
- bool TryGetTenantContext(TenancyOptions config, string identifier, ITenantResolutionStrategy strategy, [NotNullWhen(true)]out ITenantContext? tenantContext)
+ bool TryGetTenantContext(TenancyOptions config, string identifier, ITenantResolutionStrategy strategy, out ITenantContext tenantContext)
{
- tenantContext = null;
- var configuredTenant = config.Tenants.FirstOrDefault(tenant => tenant.Id.Equals(identifier, StringComparison.OrdinalIgnoreCase));
- if (configuredTenant is not null)
- {
- LogUsingConfiguredTenant(_logger, identifier, configuredTenant.Name);
- tenantContext = TenantContext.Resolved(configuredTenant, new StrategyInfo(strategy.GetType(), strategy));
- return true;
- }
-
- if (config.Strict)
+ var (isResolved, isResolvedFromConfig) = config.TryGetTenantContext(identifier, strategy, out tenantContext);
+ if (!isResolved)
{
LogTenantNotConfigured(_logger, identifier);
return false;
}
- LogUsingNonConfiguredTenant(_logger, identifier);
- configuredTenant = new TTenant
+
+ if (isResolvedFromConfig)
{
- Id = identifier
- };
- tenantContext = TenantContext.Resolved(configuredTenant, new StrategyInfo(strategy.GetType(), strategy));
+ tenantContext.Resolved(out var tenantInfo, out _);
+ LogUsingConfiguredTenant(_logger, identifier, tenantInfo!.Name);
+ }
+ else
+ {
+ LogUsingNonConfiguredTenant(_logger, identifier);
+ }
return true;
}
diff --git a/Packages/DotNET/Tenancy/Source/Base/TenancyBuilder.cs b/Packages/DotNET/Tenancy/Source/Base/TenancyBuilder.cs
index 1e5fbe1..6842d2b 100644
--- a/Packages/DotNET/Tenancy/Source/Base/TenancyBuilder.cs
+++ b/Packages/DotNET/Tenancy/Source/Base/TenancyBuilder.cs
@@ -3,6 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
using Woksin.Extensions.Tenancy.Context;
using Woksin.Extensions.Tenancy.Strategies;
@@ -12,26 +13,40 @@ public class TenancyBuilder
where TTenant : class, ITenantInfo, new()
{
readonly IServiceCollection _services;
+ bool _disableAsyncLocalTenantContext;
public TenancyBuilder(IServiceCollection services)
{
_services = services;
+ // Simply configure the TenancyOptions so that it is at least registered;
+ Configure(_ => { });
_services.TryAddTransient, TenantResolver>();
_services.TryAddTransient(sp => (IResolveTenant)sp.GetRequiredService>());
_services.TryAddScoped>(sp =>
- sp.GetRequiredService>().CurrentTenant);
+ {
+ var accessor = sp.GetRequiredService>();
+ ITenantContextAccessor.ThrowIfDisabledTenantContext(accessor, sp.GetRequiredService>>().CurrentValue);
+ return accessor.CurrentTenant;
+ });
_services.TryAddScoped(sp =>
{
- var accessor = sp.GetRequiredService>();
- if (!accessor.CurrentTenant.Resolved(out var tenantInfo, out _))
+ var tenantContext = sp.GetRequiredService>();
+ if (!tenantContext.Resolved(out var tenantInfo, out _))
{
throw new TenantContextIsNotResolved($"Cannot resolve {typeof(TTenant)}");
}
return tenantInfo;
});
_services.TryAddScoped(sp => sp.GetService()!);
- _services.TryAddSingleton, TenantContextAccessor>();
+ if (_disableAsyncLocalTenantContext)
+ {
+ _services.TryAddSingleton, StaticTenantContextAccessor>();
+ }
+ else
+ {
+ _services.TryAddSingleton, TenantContextAccessor>();
+ }
_services.TryAddSingleton(sp =>
(ITenantContextAccessor)sp.GetRequiredService>());
_services.TryAddTransient, ActionInTenantContextPerformer>();
@@ -39,6 +54,17 @@ public TenancyBuilder(IServiceCollection services)
(IPerformActionInTenantContext)sp.GetRequiredService>());
}
+ ///
+ /// Disables the option to use an tenant context using the implementation of .
+ /// The will now be used instead of .
+ ///
+ /// will not care for any state and will simply always return the 'unresolved' .
+ public TenancyBuilder DisableAsyncLocalTenantContext()
+ {
+ _disableAsyncLocalTenantContext = true;
+ return this;
+ }
+
public TenancyBuilder WithTenantInfo(TTenant tenantInfo)
{
_services.Configure((TenancyOptions op) => op.Tenants.Add(tenantInfo));
diff --git a/Packages/DotNET/Tenancy/Source/Base/TenancyOptions.cs b/Packages/DotNET/Tenancy/Source/Base/TenancyOptions.cs
index d9ad537..5d59ea4 100644
--- a/Packages/DotNET/Tenancy/Source/Base/TenancyOptions.cs
+++ b/Packages/DotNET/Tenancy/Source/Base/TenancyOptions.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Diagnostics.CodeAnalysis;
+using Woksin.Extensions.Tenancy.Context;
using Woksin.Extensions.Tenancy.Strategies;
namespace Woksin.Extensions.Tenancy;
@@ -40,4 +41,43 @@ public bool IsUsingStaticTenant([NotNullWhen(true)]out string? staticTenantId)
staticTenantId = string.IsNullOrEmpty(StaticTenantId) ? null : StaticTenantId;
return !string.IsNullOrEmpty(staticTenantId);
}
+
+ ///
+ /// Tries to get the resolved from the .
+ ///
+ /// The tenant identifier.
+ /// The optional strategy that resolved the tenant identifier.
+ /// The outputted .
+ public (bool IsResolved, bool IsResolvedFromConfig) TryGetTenantContext(string tenantId, ITenantResolutionStrategy? strategy, out ITenantContext tenantContext)
+ {
+ var configuredTenant = Tenants.FirstOrDefault(tenant => tenant.Id.Equals(tenantId, StringComparison.OrdinalIgnoreCase));
+ var strategyInfo = strategy is not null
+ ? new StrategyInfo(strategy.GetType(), strategy)
+ : null;
+ if (configuredTenant is not null)
+ {
+ tenantContext = TenantContext.Resolved(configuredTenant, strategyInfo);
+ return (true, true);
+ }
+
+ if (Strict)
+ {
+ tenantContext = TenantContext.Unresolved();
+ return (false, false);
+ }
+ configuredTenant = new TTenant
+ {
+ Id = tenantId
+ };
+ tenantContext = TenantContext.Resolved(configuredTenant, strategyInfo);
+ return (true, false);
+ }
+
+ ///
+ /// Tries to get the resolved from the .
+ ///
+ /// The tenant identifier.
+ /// The outputted .
+ public (bool IsResolved, bool IsResolvedFromConfig) TryGetTenantContext(string tenantId, out ITenantContext tenantContext)
+ => TryGetTenantContext(tenantId, null, out tenantContext);
}