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); }