Skip to content

Commit

Permalink
Merge pull request #30 from woksin-org/tenancy-context
Browse files Browse the repository at this point in the history
Tenancy context
  • Loading branch information
woksin authored Mar 3, 2024
2 parents 826d01d + 633b247 commit b619a59
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 24 deletions.
39 changes: 39 additions & 0 deletions Packages/DotNET/Tenancy/Source/AspNetCore/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extension methods for getting <see cref="ITenantContext"/> from the <see cref="HttpContext"/>.
/// </summary>
public static class HttpContextExtensions
{
public static ITenantContext<TTenant> GetTenantContext<TTenant>(this HttpContext context)
where TTenant : class, ITenantInfo, new()
{
ITenantContext<TTenant> result = TenantContext<TTenant>.Unresolved();
if (context.Items.TryGetValue(TenancyMiddleware.TenantContextItemKey, out var tenantContext) && tenantContext is not null)
{
result = tenantContext as ITenantContext<TTenant> ?? throw new InvalidCastException($"Could not cast tenant context of type {tenantContext.GetType()} to {typeof(ITenantContext<TTenant>)}");
}
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<TTenant> GetTenantContext<TTenant>(this HttpRequest request)
where TTenant : class, ITenantInfo, new() => request.HttpContext.GetTenantContext<TTenant>();

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -26,15 +27,18 @@ public async Task Invoke(HttpContext context)
var logger = context.RequestServices.GetService<ILogger<TenancyMiddleware>>() ?? NullLogger<TenancyMiddleware>.Instance;
LogGettingTenantContext(logger);
var accessor = context.RequestServices.GetRequiredService<ITenantContextAccessor>();
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<IResolveTenant>();
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Options;
using Woksin.Extensions.Tenancy.Context;

namespace Woksin.Extensions.Tenancy;
Expand All @@ -11,15 +12,23 @@ public class ActionInTenantContextPerformer<TTenant> : IPerformActionInTenantCon
{
readonly ITenantContextAccessor<TTenant> _tenantContextAccessor;
readonly ITenantContextAccessor _nonTypedTenantContextAccessor;
readonly IOptionsMonitor<TenancyOptions<TTenant>> _options;

public ActionInTenantContextPerformer(ITenantContextAccessor<TTenant> tenantContextAccessor, ITenantContextAccessor nonTypedTenantContextAccessor)
public ActionInTenantContextPerformer(ITenantContextAccessor<TTenant> tenantContextAccessor, ITenantContextAccessor nonTypedTenantContextAccessor, IOptionsMonitor<TenancyOptions<TTenant>> options)
{
_tenantContextAccessor = tenantContextAccessor;
_nonTypedTenantContextAccessor = nonTypedTenantContextAccessor;
_options = options;
}

void ThrowIfTenantContextDisabled()
{
ITenantContextAccessor<TTenant>.ThrowIfDisabledTenantContext(_tenantContextAccessor, _options.CurrentValue);
}

public async Task<TResult> Perform<TResult>(ITenantContext<TTenant> tenant, Func<ITenantContext<TTenant>, TResult> callback)
{
ThrowIfTenantContextDisabled();
return await Task.Run(() =>
{
_tenantContextAccessor.CurrentTenant = tenant;
Expand All @@ -28,6 +37,7 @@ public async Task<TResult> Perform<TResult>(ITenantContext<TTenant> tenant, Func
}
public async Task<TResult> Perform<TResult>(ITenantContext<TTenant> tenant, Func<ITenantContext<TTenant>, Task<TResult>> callback)
{
ThrowIfTenantContextDisabled();
return await Task.Run(async() =>
{
_tenantContextAccessor.CurrentTenant = tenant;
Expand All @@ -37,6 +47,7 @@ public async Task<TResult> Perform<TResult>(ITenantContext<TTenant> tenant, Func

public async Task Perform<TResult>(ITenantContext<TTenant> tenant, Action<ITenantContext<TTenant>> callback)
{
ThrowIfTenantContextDisabled();
await Task.Run(() =>
{
_tenantContextAccessor.CurrentTenant = tenant;
Expand All @@ -46,6 +57,7 @@ await Task.Run(() =>

public async Task Perform<TResult>(ITenantContext<TTenant> tenant, Func<ITenantContext<TTenant>, Task> callback)
{
ThrowIfTenantContextDisabled();
await Task.Run(async () =>
{
_tenantContextAccessor.CurrentTenant = tenant;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,33 @@ public interface ITenantContextAccessor<TTenant>
/// Gets the current <see cref="ITenantContext"/>.
/// </summary>
ITenantContext<TTenant> CurrentTenant { get; set; }

/// <summary>
/// Throws an exception if the AsyncLocal tenant context is disabled.
/// </summary>
/// <param name="accessor">The registered <see cref="ITenantContextAccessor"/>.</param>
/// <param name="tenancyOptions">The configured <see cref="TenancyOptions{TTenant}"/>.</param>
/// <exception cref="InvalidOperationException">The exception that is thrown.</exception>
public static void ThrowIfDisabledTenantContext(ITenantContextAccessor accessor, TenancyOptions<TTenant> tenancyOptions)
{
if (accessor is StaticTenantContextAccessor<TTenant> && !tenancyOptions.IsUsingStaticTenant(out _))
{
throw new InvalidOperationException("Current tenant context cannot be resolved when AsyncLocal Tenant Context is disabled");
}
}
/// <summary>
/// Throws an exception if the AsyncLocal tenant context is disabled.
/// </summary>
/// <param name="accessor">The registered <see cref="ITenantContextAccessor"/>.</param>
/// <param name="tenancyOptions">The configured <see cref="TenancyOptions{TTenant}"/>.</param>
/// <exception cref="InvalidOperationException">The exception that is thrown.</exception>
public static void ThrowIfDisabledTenantContext(ITenantContextAccessor<TTenant> accessor, TenancyOptions<TTenant> tenancyOptions)
{
if (accessor is StaticTenantContextAccessor<TTenant> && !tenancyOptions.IsUsingStaticTenant(out _))
{
throw new InvalidOperationException("Current tenant context cannot be resolved when AsyncLocal Tenant Context is disabled");
}
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents static implementation of <see cref="ITenantContextAccessor{TTenant}"/> and <see cref="ITenantContextAccessor"/> that will be in use if AsyncLocal tenant context is disabled.
/// </summary>
/// <remarks>Unless <see cref="TenancyOptions{TTenant}.StaticTenantId"/> is configured this implementation will always return the 'unresolved' <see cref="ITenantContext"/>.</remarks>
/// <typeparam name="TTenant">The <see cref="Type"/> of the <see cref="ITenantInfo"/>.</typeparam>
public partial class StaticTenantContextAccessor<TTenant> : ITenantContextAccessor<TTenant>, ITenantContextAccessor
where TTenant : class, ITenantInfo, new()
{
readonly IOptionsMonitor<TenancyOptions<TTenant>> _options;
readonly ILogger<StaticTenantContextAccessor<TTenant>> _logger;

public StaticTenantContextAccessor(IOptionsMonitor<TenancyOptions<TTenant>> options, ILogger<StaticTenantContextAccessor<TTenant>> logger)
{
_options = options;
_logger = logger;
}

/// <inheritdoc />
public ITenantContext<TTenant> CurrentTenant
{
get
{
if (_options.CurrentValue.IsUsingStaticTenant(out var staticTenantId))
{
return TenantContext<TTenant>.Static(new TTenant
{
Id = staticTenantId
});
}
LogReturningUnresolvedContext(_logger);
return TenantContext<TTenant>.Unresolved();
}

// ReSharper disable once ValueParameterNotUsed
set
{
LogCannotSetContext(_logger);
}
}

/// <inheritdoc />
ITenantContext ITenantContextAccessor.CurrentTenant
{
get => CurrentTenant as ITenantContext ?? TenantContext<TTenant>.Unresolved();
set => CurrentTenant = value as ITenantContext<TTenant> ?? 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ public ITenantContext<TTenant> 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<TTenant>.Static(new TTenant()
return TenantContext<TTenant>.Static(new TTenant
{
Id = staticTenantId
});
Expand Down
28 changes: 12 additions & 16 deletions Packages/DotNET/Tenancy/Source/Base/Context/TenantResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,24 @@ public async Task<ITenantContext<TTenant>> Resolve(object context)
return (false, null);
}

bool TryGetTenantContext(TenancyOptions<TTenant> config, string identifier, ITenantResolutionStrategy strategy, [NotNullWhen(true)]out ITenantContext<TTenant>? tenantContext)
bool TryGetTenantContext(TenancyOptions<TTenant> config, string identifier, ITenantResolutionStrategy strategy, out ITenantContext<TTenant> 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<TTenant>.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<TTenant>.Resolved(configuredTenant, new StrategyInfo(strategy.GetType(), strategy));
tenantContext.Resolved(out var tenantInfo, out _);
LogUsingConfiguredTenant(_logger, identifier, tenantInfo!.Name);
}
else
{
LogUsingNonConfiguredTenant(_logger, identifier);
}
return true;
}

Expand Down
34 changes: 30 additions & 4 deletions Packages/DotNET/Tenancy/Source/Base/TenancyBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -12,33 +13,58 @@ public class TenancyBuilder<TTenant>
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<IResolveTenant<TTenant>, TenantResolver<TTenant>>();
_services.TryAddTransient<IResolveTenant>(sp => (IResolveTenant)sp.GetRequiredService<IResolveTenant<TTenant>>());
_services.TryAddScoped<ITenantContext<TTenant>>(sp =>
sp.GetRequiredService<ITenantContextAccessor<TTenant>>().CurrentTenant);
{
var accessor = sp.GetRequiredService<ITenantContextAccessor<TTenant>>();
ITenantContextAccessor<TTenant>.ThrowIfDisabledTenantContext(accessor, sp.GetRequiredService<IOptionsMonitor<TenancyOptions<TTenant>>>().CurrentValue);
return accessor.CurrentTenant;
});

_services.TryAddScoped<TTenant>(sp =>
{
var accessor = sp.GetRequiredService<ITenantContextAccessor<TTenant>>();
if (!accessor.CurrentTenant.Resolved(out var tenantInfo, out _))
var tenantContext = sp.GetRequiredService<ITenantContext<TTenant>>();
if (!tenantContext.Resolved(out var tenantInfo, out _))
{
throw new TenantContextIsNotResolved($"Cannot resolve {typeof(TTenant)}");
}
return tenantInfo;
});
_services.TryAddScoped<ITenantInfo>(sp => sp.GetService<TTenant>()!);
_services.TryAddSingleton<ITenantContextAccessor<TTenant>, TenantContextAccessor<TTenant>>();
if (_disableAsyncLocalTenantContext)
{
_services.TryAddSingleton<ITenantContextAccessor<TTenant>, StaticTenantContextAccessor<TTenant>>();
}
else
{
_services.TryAddSingleton<ITenantContextAccessor<TTenant>, TenantContextAccessor<TTenant>>();
}
_services.TryAddSingleton<ITenantContextAccessor>(sp =>
(ITenantContextAccessor)sp.GetRequiredService<ITenantContextAccessor<TTenant>>());
_services.TryAddTransient<IPerformActionInTenantContext<TTenant>, ActionInTenantContextPerformer<TTenant>>();
_services.TryAddTransient<IPerformActionInTenantContext>(sp =>
(IPerformActionInTenantContext)sp.GetRequiredService<IPerformActionInTenantContext<TTenant>>());
}

/// <summary>
/// Disables the option to use an <see cref="AsyncLocal{T}"/> <see cref="ITenantContext{TTenantInfo}"/> tenant context using the <see cref="TenantContextAccessor{TTenant}"/> implementation of <see cref="ITenantContextAccessor{TTenant}"/>.
/// The <see cref="StaticTenantContextAccessor{TTenant}"/> will now be used instead of <see cref="TenantContextAccessor{TTenant}"/>.
/// </summary>
/// <remarks><see cref="StaticTenantContextAccessor{TTenant}"/> will not care for any <see cref="ITenantContext"/> state and will simply always return the 'unresolved' <see cref="ITenantContext"/>.</remarks>
public TenancyBuilder<TTenant> DisableAsyncLocalTenantContext()
{
_disableAsyncLocalTenantContext = true;
return this;
}

public TenancyBuilder<TTenant> WithTenantInfo(TTenant tenantInfo)
{
_services.Configure((TenancyOptions<TTenant> op) => op.Tenants.Add(tenantInfo));
Expand Down
40 changes: 40 additions & 0 deletions Packages/DotNET/Tenancy/Source/Base/TenancyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,4 +41,43 @@ public bool IsUsingStaticTenant([NotNullWhen(true)]out string? staticTenantId)
staticTenantId = string.IsNullOrEmpty(StaticTenantId) ? null : StaticTenantId;
return !string.IsNullOrEmpty(staticTenantId);
}

/// <summary>
/// Tries to get the resolved <see cref="ITenantContext{TTenant}"/> from the <see cref="TenancyOptions{TTenant}"/>.
/// </summary>
/// <param name="tenantId">The tenant identifier.</param>
/// <param name="strategy">The optional strategy that resolved the tenant identifier.</param>
/// <param name="tenantContext">The outputted <see cref="ITenantContext{TTenant}"/>.</param>
public (bool IsResolved, bool IsResolvedFromConfig) TryGetTenantContext(string tenantId, ITenantResolutionStrategy? strategy, out ITenantContext<TTenant> 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<TTenant>.Resolved(configuredTenant, strategyInfo);
return (true, true);
}

if (Strict)
{
tenantContext = TenantContext<TTenant>.Unresolved();
return (false, false);
}
configuredTenant = new TTenant
{
Id = tenantId
};
tenantContext = TenantContext<TTenant>.Resolved(configuredTenant, strategyInfo);
return (true, false);
}

/// <summary>
/// Tries to get the resolved <see cref="ITenantContext{TTenant}"/> from the <see cref="TenancyOptions{TTenant}"/>.
/// </summary>
/// <param name="tenantId">The tenant identifier.</param>
/// <param name="tenantContext">The outputted <see cref="ITenantContext{TTenant}"/>.</param>
public (bool IsResolved, bool IsResolvedFromConfig) TryGetTenantContext(string tenantId, out ITenantContext<TTenant> tenantContext)
=> TryGetTenantContext(tenantId, null, out tenantContext);
}

0 comments on commit b619a59

Please sign in to comment.