Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,69 +16,65 @@ namespace CleanArchitecture.Extensions.Multitenancy.AspNetCore.Middleware;
public sealed class TenantResolutionMiddleware
{
private readonly RequestDelegate _next;
private readonly ITenantResolver _resolver;
private readonly ITenantAccessor _accessor;
private readonly ITenantResolutionContextFactory _contextFactory;
private readonly AspNetCoreMultitenancyOptions _aspNetCoreOptions;
private readonly MultitenancyOptions _options;
private readonly ILogger<TenantResolutionMiddleware> _logger;
private readonly ITenantCorrelationScopeAccessor _scopeAccessor;

/// <summary>
/// Initializes a new instance of the <see cref="TenantResolutionMiddleware"/> class.
/// </summary>
/// <param name="next">Request delegate.</param>
public TenantResolutionMiddleware(RequestDelegate next)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
}

/// <summary>
/// Executes the middleware.
/// </summary>
/// <param name="httpContext">HTTP context.</param>
/// <param name="resolver">Tenant resolver.</param>
/// <param name="accessor">Tenant accessor.</param>
/// <param name="contextFactory">Resolution context factory.</param>
/// <param name="aspNetCoreOptions">ASP.NET Core multitenancy options.</param>
/// <param name="options">Core multitenancy options.</param>
/// <param name="logger">Logger.</param>
/// <param name="scopeAccessor">Correlation scope accessor.</param>
public TenantResolutionMiddleware(
RequestDelegate next,
public async Task InvokeAsync(
HttpContext httpContext,
ITenantResolver resolver,
ITenantAccessor accessor,
ITenantResolutionContextFactory contextFactory,
IOptions<AspNetCoreMultitenancyOptions> aspNetCoreOptions,
IOptions<MultitenancyOptions> options,
ILogger<TenantResolutionMiddleware> logger,
ITenantCorrelationScopeAccessor scopeAccessor)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
_accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
_contextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
_aspNetCoreOptions = aspNetCoreOptions?.Value ?? throw new ArgumentNullException(nameof(aspNetCoreOptions));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor));
}

/// <summary>
/// Executes the middleware.
/// </summary>
/// <param name="httpContext">HTTP context.</param>
public async Task InvokeAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(resolver);
ArgumentNullException.ThrowIfNull(accessor);
ArgumentNullException.ThrowIfNull(contextFactory);
ArgumentNullException.ThrowIfNull(aspNetCoreOptions);
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(scopeAccessor);

var resolutionContext = _contextFactory.Create(httpContext);
var tenantContext = await _resolver.ResolveAsync(resolutionContext, httpContext.RequestAborted).ConfigureAwait(false);
var aspNetCoreOptionsValue = aspNetCoreOptions.Value ?? throw new ArgumentNullException(nameof(aspNetCoreOptions));
var optionsValue = options.Value ?? throw new ArgumentNullException(nameof(options));

if (_aspNetCoreOptions.StoreTenantInHttpContextItems)
var resolutionContext = contextFactory.Create(httpContext);
var tenantContext = await resolver.ResolveAsync(resolutionContext, httpContext.RequestAborted).ConfigureAwait(false);

if (aspNetCoreOptionsValue.StoreTenantInHttpContextItems)
{
httpContext.Items[_aspNetCoreOptions.HttpContextItemKey] = tenantContext;
httpContext.Items[aspNetCoreOptionsValue.HttpContextItemKey] = tenantContext;
}

using var tenantScope = _accessor.BeginScope(tenantContext);
using var tenantScope = accessor.BeginScope(tenantContext);

IDisposable? logScope = null;
var shouldClearScope = _scopeAccessor.CurrentScope is null;
var shouldClearScope = scopeAccessor.CurrentScope is null;
if (shouldClearScope)
{
logScope = BeginLoggingScope(tenantContext?.TenantId);
_scopeAccessor.SetScope(logScope, owned: false);
logScope = BeginLoggingScope(logger, optionsValue, tenantContext?.TenantId);
scopeAccessor.SetScope(logScope, owned: false);
}

try
Expand All @@ -89,9 +85,9 @@ public async Task InvokeAsync(HttpContext httpContext)
{
if (shouldClearScope)
{
if (ReferenceEquals(_scopeAccessor.CurrentScope, logScope))
if (ReferenceEquals(scopeAccessor.CurrentScope, logScope))
{
_scopeAccessor.ClearScope()?.Dispose();
scopeAccessor.ClearScope()?.Dispose();
}
else
{
Expand All @@ -101,13 +97,16 @@ public async Task InvokeAsync(HttpContext httpContext)
}
}

private IDisposable? BeginLoggingScope(string? tenantId)
private static IDisposable? BeginLoggingScope(
ILogger<TenantResolutionMiddleware> logger,
MultitenancyOptions options,
string? tenantId)
{
var scopeKey = string.IsNullOrWhiteSpace(_options.LogScopeKey)
var scopeKey = string.IsNullOrWhiteSpace(options.LogScopeKey)
? "tenant_id"
: _options.LogScopeKey;
: options.LogScopeKey;

if (_options.AddTenantToActivity)
if (options.AddTenantToActivity)
{
var activity = Activity.Current;
if (activity is not null)
Expand All @@ -117,11 +116,11 @@ public async Task InvokeAsync(HttpContext httpContext)
}
}

if (!_options.AddTenantToLogScope)
if (!options.AddTenantToLogScope)
{
return null;
}

return _logger.BeginScope(new Dictionary<string, object?> { [scopeKey] = tenantId });
return logger.BeginScope(new Dictionary<string, object?> { [scopeKey] = tenantId });
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using CleanArchitecture.Extensions.Multitenancy;
using CleanArchitecture.Extensions.Multitenancy.Abstractions;
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Context;
using CleanArchitecture.Extensions.Multitenancy.AspNetCore;
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Middleware;
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Options;
using CleanArchitecture.Extensions.Multitenancy.Behaviors;
using CleanArchitecture.Extensions.Multitenancy.Configuration;
using CleanArchitecture.Extensions.Multitenancy.Context;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using OptionsFactory = Microsoft.Extensions.Options.Options;

Expand Down Expand Up @@ -34,8 +37,13 @@ public async Task InvokeAsync_sets_current_tenant_and_http_context_items()
return Task.CompletedTask;
};

var middleware = new TenantResolutionMiddleware(
next,
var middleware = new TenantResolutionMiddleware(next);

var httpContext = new DefaultHttpContext();
httpContext.Request.Headers["X-Tenant-ID"] = "tenant-1";

await middleware.InvokeAsync(
httpContext,
resolver,
accessor,
factory,
Expand All @@ -44,16 +52,31 @@ public async Task InvokeAsync_sets_current_tenant_and_http_context_items()
logger,
new TenantCorrelationScopeAccessor());

var httpContext = new DefaultHttpContext();
httpContext.Request.Headers["X-Tenant-ID"] = "tenant-1";

await middleware.InvokeAsync(httpContext);

Assert.Equal("tenant-1", observedTenantId);
Assert.NotNull(observedContext);
Assert.Null(accessor.TenantId);
}

[Fact]
public void UseCleanArchitectureMultitenancy_builds_pipeline_with_validated_scopes()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddCleanArchitectureMultitenancyAspNetCore();

using var provider = services.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = true
});

var app = new ApplicationBuilder(provider);
app.UseCleanArchitectureMultitenancy();
app.Run(_ => Task.CompletedTask);

var exception = Record.Exception(() => app.Build());
Assert.Null(exception);
}

private sealed class CapturingTenantResolver : ITenantResolver
{
public TenantResolutionContext? CapturedContext { get; private set; }
Expand Down
Loading