Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for .net 6 LTS #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 @@ -9,11 +9,18 @@ namespace MultiTenant.AspNetCore.Infrastructure.DependencyInjection
/// Factory for creating tenant specific service providers
/// </summary>
/// <typeparam name="T"></typeparam>
internal class MultiTenantServiceProviderFactory<T>(IServiceCollection containerBuilder, Action<IServiceCollection, T?> tenantServiceConfiguration) where T : ITenantInfo
internal class MultiTenantServiceProviderFactory<T> where T : ITenantInfo
{


public MultiTenantServiceProviderFactory(IServiceCollection containerBuilder, Action<IServiceCollection, T?> tenantServiceConfiguration)
{
this.containerBuilder = containerBuilder;
this.tenantServiceConfiguration = tenantServiceConfiguration;
}
//Cache compiled providers
private readonly ConcurrentDictionary<string, Lazy<IServiceProvider>> CompiledProviders = new();
private readonly IServiceCollection containerBuilder;
private readonly Action<IServiceCollection, T?> tenantServiceConfiguration;

public IServiceProvider GetServiceProviderForTenant(T tenant)
{
Expand All @@ -36,17 +43,24 @@ public IServiceProvider GetServiceProviderForTenant(T tenant)
/// Factory wrapper for creating service scopes
/// </summary>
/// <param name="serviceProvider"></param>
internal class MultiTenantServiceScopeFactory<T>(MultiTenantServiceProviderFactory<T> ServiceProviderFactory, IMultiTenantContextAccessor<T> multiTenantContextAccessor) : IMultiTenantServiceScopeFactory where T : ITenantInfo
internal class MultiTenantServiceScopeFactory<T> : IMultiTenantServiceScopeFactory where T : ITenantInfo
{
private readonly MultiTenantServiceProviderFactory<T> serviceProviderFactory;
private readonly IMultiTenantContextAccessor<T> multiTenantContextAccessor;

public MultiTenantServiceScopeFactory(MultiTenantServiceProviderFactory<T> ServiceProviderFactory, IMultiTenantContextAccessor<T> multiTenantContextAccessor)
{
serviceProviderFactory = ServiceProviderFactory;
this.multiTenantContextAccessor = multiTenantContextAccessor;
}
/// <summary>
/// Create scope
/// </summary>
/// <returns></returns>
public IServiceScope CreateScope()
{
var tenant = multiTenantContextAccessor.TenantInfo ?? throw new InvalidOperationException("Tenant context is not available");
return ServiceProviderFactory.GetServiceProviderForTenant(tenant).CreateScope();
return serviceProviderFactory.GetServiceProviderForTenant(tenant).CreateScope();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace MultiTenant.AspNetCore.Infrastructure.Middleware
/// Register the multitenant context accessor middleware with the app pipeline.
/// </summary>
/// <seealso cref="IStartupFilter" />
internal class MultiTenantContextAccessorStartupFilter<T>() : IStartupFilter where T : ITenantInfo
internal class MultiTenantContextAccessorStartupFilter<T> : IStartupFilter where T : ITenantInfo
{
/// <summary>
/// Adds the multitenant request services middleware to the app pipeline.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,30 @@ namespace MultiTenant.AspNetCore.Infrastructure.Middleware
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tenantServicesConfiguration"></param>
internal class MultiTenantContextAccessorMiddleware<T>(
internal class MultiTenantContextAccessorMiddleware<T> where T : ITenantInfo
{
private readonly RequestDelegate next;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IMultiTenantContextAccessor<T> tenantAccessor;
private readonly ITenantLookupService<T> tenantResolver;
private readonly ITenantResolutionStrategy tenantResolutionStrategy;
private readonly IOptions<MultiTenantOptions<T>> options;

public MultiTenantContextAccessorMiddleware(
RequestDelegate next,
IHttpContextAccessor httpContextAccessor,
IMultiTenantContextAccessor<T> TenantAccessor,
ITenantLookupService<T> TenantResolver,
ITenantResolutionStrategy TenantResolutionStrategy,
IOptions<MultiTenantOptions<T>> Options) where T : ITenantInfo
{
IOptions<MultiTenantOptions<T>> Options)
{
this.next = next;
this.httpContextAccessor = httpContextAccessor;
tenantAccessor = TenantAccessor;
tenantResolver = TenantResolver;
tenantResolutionStrategy = TenantResolutionStrategy;
options = Options;
}

/// <summary>
/// Set the services for the tenant to be our specific tenant services
Expand All @@ -24,13 +40,13 @@ internal class MultiTenantContextAccessorMiddleware<T>(
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
var options = Options.Value!;
var options = this.options.Value!;

//Set context if missing so it can be used by the tenant services to resolve the tenant
httpContextAccessor.HttpContext ??= context;

//Get the tenant identifier
var identifier = await TenantResolutionStrategy.GetTenantIdentifierAsync();
var identifier = await tenantResolutionStrategy.GetTenantIdentifierAsync();
if(identifier == null && options.MissingTenantBehavior == MissingTenantBehavior.ThrowException)
throw new InvalidOperationException("Tenant identifier could not be resolved using configured strategy");
if(identifier == null && options.MissingTenantBehavior == MissingTenantBehavior.UseDefault)
Expand All @@ -39,13 +55,13 @@ public async Task Invoke(HttpContext context)
//Set the tenant context
if (identifier != null)
{
var tenant = await TenantResolver.GetTenantAsync(identifier);
var tenant = await tenantResolver.GetTenantAsync(identifier);
if(tenant == null && options.MissingTenantBehavior == MissingTenantBehavior.ThrowException)
throw new InvalidOperationException($"No tenant found matching '{identifier}'");
if(tenant == null && options.MissingTenantBehavior == MissingTenantBehavior.UseDefault)
tenant = options.DefaultTenant;

TenantAccessor.TenantInfo ??= tenant;
tenantAccessor.TenantInfo ??= tenant;
}

await next.Invoke(context);
Expand Down
11 changes: 10 additions & 1 deletion src/Infrastructure/Middleware/MultiTenantMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@ namespace MultiTenant.AspNetCore.Infrastructure.Middleware
/// <typeparam name="T"></typeparam>
/// <param name="next"></param>
/// <param name="configurePipeline"></param>
internal class MultiTenantMiddleware<T>(RequestDelegate next, IApplicationBuilder builder, Action<T, IApplicationBuilder> configurePipeline)
internal class MultiTenantMiddleware<T>
where T : ITenantInfo
{
public MultiTenantMiddleware(RequestDelegate next, IApplicationBuilder builder, Action<T, IApplicationBuilder> configurePipeline)
{
this.next = next;
this.builder = builder;
this.configurePipeline = configurePipeline;
}
//Cache compiled pipelines
private readonly ConcurrentDictionary<string, Lazy<RequestDelegate>> _pipelinesCache = new();
private readonly RequestDelegate next;
private readonly IApplicationBuilder builder;
private readonly Action<T, IApplicationBuilder> configurePipeline;

/// <summary>
/// Set the services for the tenant to be our specific tenant services
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace MultiTenant.AspNetCore.Infrastructure.Middleware
/// </summary>
/// <param name="tenantServicesConfiguration">The tenant specific tenant services configuration.</param>
/// <seealso cref="IStartupFilter" />
internal class MultitenantRequestServicesStartupFilter<T>() : IStartupFilter where T : ITenantInfo
internal class MultitenantRequestServicesStartupFilter<T> : IStartupFilter where T : ITenantInfo
{
/// <summary>
/// Adds the multitenant request services middleware to the app pipeline.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@ namespace MultiTenant.AspNetCore.Infrastructure.Middleware
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tenantServicesConfiguration"></param>
internal class MultiTenantRequestServicesMiddleware<T>(
RequestDelegate next,
IMultiTenantServiceScopeFactory multiTenantServiceProviderScopeFactory,
IHttpContextAccessor httpContextAccessor) where T : ITenantInfo
internal class MultiTenantRequestServicesMiddleware<T> where T : ITenantInfo
{
private readonly RequestDelegate next;
private readonly IMultiTenantServiceScopeFactory multiTenantServiceProviderScopeFactory;
private readonly IHttpContextAccessor httpContextAccessor;

public MultiTenantRequestServicesMiddleware(
RequestDelegate next,
IMultiTenantServiceScopeFactory multiTenantServiceProviderScopeFactory,
IHttpContextAccessor httpContextAccessor)
{
this.next = next;
this.multiTenantServiceProviderScopeFactory = multiTenantServiceProviderScopeFactory;
this.httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// Set the services for the tenant to be our specific tenant services
/// </summary>
Expand Down
9 changes: 6 additions & 3 deletions src/Infrastructure/Options/MultiTenantOptionsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ namespace MultiTenant.AspNetCore.Infrastructure.Options
/// </summary>
/// <param name="multiTenantContextAccessor"></param>
/// <exception cref="ArgumentNullException"></exception>
internal class MultiTenantOptionsCache<TOptions, T>(IMultiTenantContextAccessor<T> multiTenantContextAccessor) : IOptionsMonitorCache<TOptions>
internal class MultiTenantOptionsCache<TOptions, T> : IOptionsMonitorCache<TOptions>
where TOptions : class where T : ITenantInfo
{

private readonly IMultiTenantContextAccessor<T> multiTenantContextAccessor = multiTenantContextAccessor ??
public MultiTenantOptionsCache(IMultiTenantContextAccessor<T> multiTenantContextAccessor)
{
this.multiTenantContextAccessor = multiTenantContextAccessor ??
throw new ArgumentNullException(nameof(multiTenantContextAccessor));
}
private readonly IMultiTenantContextAccessor<T> multiTenantContextAccessor;
private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> tenantCaches = new();

/// <summary>
Expand Down
10 changes: 9 additions & 1 deletion src/Infrastructure/Options/MultiTenantOptionsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

namespace MultiTenant.AspNetCore.Infrastructure.Options
{
internal class MultiTenantOptionsManager<TOptions>(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> cache) : IOptionsSnapshot<TOptions> where TOptions : class
internal class MultiTenantOptionsManager<TOptions> : IOptionsSnapshot<TOptions> where TOptions : class
{
private readonly IOptionsFactory<TOptions> factory;
private readonly IOptionsMonitorCache<TOptions> cache;

public MultiTenantOptionsManager(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> cache)
{
this.factory = factory;
this.cache = cache;
}
public TOptions Value => Get(Microsoft.Extensions.Options.Options.DefaultName);

public TOptions Get(string? name)
Expand Down
8 changes: 6 additions & 2 deletions src/Infrastructure/Strategies/HostResolutionStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ namespace MultiTenant.AspNetCore.Infrastructure.Strategies
/// <summary>
/// Resolve the host to a tenant identifier
/// </summary>
internal class HostResolutionStrategy(IHttpContextAccessor httpContextAccessor) : ITenantResolutionStrategy
internal class HostResolutionStrategy : ITenantResolutionStrategy
{
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
public HostResolutionStrategy(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
private readonly IHttpContextAccessor _httpContextAccessor;

/// <summary>
/// Get the tenant identifier
Expand Down
2 changes: 1 addition & 1 deletion src/MultiTenant.AspNetCore.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Title>MultiTenant.AspNetCore</Title>
Expand Down
34 changes: 21 additions & 13 deletions src/Registration/TenantBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ namespace MultiTenant.AspNetCore.Builder
/// Tenant builder
/// </summary>
/// <param name="services"></param>
public class TenantBuilder<T>(IServiceCollection Services, MultiTenantOptions<T> options) where T : ITenantInfo
public class TenantBuilder<T> where T : ITenantInfo
{
private readonly IServiceCollection services;
private readonly MultiTenantOptions<T> options;

public TenantBuilder(IServiceCollection Services, MultiTenantOptions<T> options)
{
services = Services;
this.options = options;
}
/// <summary>
/// Register the tenant resolver implementation
/// </summary>
Expand All @@ -25,8 +33,8 @@ public class TenantBuilder<T>(IServiceCollection Services, MultiTenantOptions<T>
/// <returns></returns>
public TenantBuilder<T> WithResolutionStrategy<V>() where V : class, ITenantResolutionStrategy
{
Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Services.TryAddSingleton(typeof(ITenantResolutionStrategy), typeof(V));
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton(typeof(ITenantResolutionStrategy), typeof(V));
return this;
}

Expand All @@ -47,7 +55,7 @@ public TenantBuilder<T> WithHostResolutionStrategy()
/// <returns></returns>
public TenantBuilder<T> WithTenantLookupService<V>() where V : class, ITenantLookupService<T>
{
Services.TryAddSingleton<ITenantLookupService<T>, V>();
services.TryAddSingleton<ITenantLookupService<T>, V>();
return this;
}

Expand All @@ -60,7 +68,7 @@ public TenantBuilder<T> WithTenantLookupService<V>() where V : class, ITenantLoo
public TenantBuilder<T> WithInMemoryTenantLookupService(IEnumerable<T> tenants)
{
var service = new InMemoryLookupService<T>(tenants);
Services.TryAddSingleton<ITenantLookupService<T>>(service);
services.TryAddSingleton<ITenantLookupService<T>>(service);
return this;
}

Expand All @@ -74,11 +82,11 @@ public TenantBuilder<T> WithTenantedServices(Action<IServiceCollection, T?> conf
{
//Replace the default service provider with a multitenant service provider
if (!options.DisableAutomaticPipelineRegistration)
Services.Insert(0, ServiceDescriptor.Transient<IStartupFilter>(provider => new MultitenantRequestServicesStartupFilter<T>()));
services.Insert(0, ServiceDescriptor.Transient<IStartupFilter>(provider => new MultitenantRequestServicesStartupFilter<T>()));

//Register the multi-tenant service provider
Services.AddSingleton<IMultiTenantServiceScopeFactory, MultiTenantServiceScopeFactory<T>>();
Services.AddSingleton(new MultiTenantServiceProviderFactory<T>(Services, configuration));
services.AddSingleton<IMultiTenantServiceScopeFactory, MultiTenantServiceScopeFactory<T>>();
services.AddSingleton(new MultiTenantServiceProviderFactory<T>(services, configuration));

return this;
}
Expand All @@ -91,19 +99,19 @@ public TenantBuilder<T> WithTenantedServices(Action<IServiceCollection, T?> conf
/// <returns></returns>
public TenantBuilder<T> WithTenantedConfigure<TOptions>(Action<TOptions, T?> tenantOptionsConfiguration) where TOptions : class
{
Services.AddOptions();
services.AddOptions();

Services.TryAddSingleton<IOptionsMonitorCache<TOptions>, MultiTenantOptionsCache<TOptions, T>>();
Services.TryAddScoped<IOptionsSnapshot<TOptions>>((sp) =>
services.TryAddSingleton<IOptionsMonitorCache<TOptions>, MultiTenantOptionsCache<TOptions, T>>();
services.TryAddScoped<IOptionsSnapshot<TOptions>>((sp) =>
{
return new MultiTenantOptionsManager<TOptions>(sp.GetRequiredService<IOptionsFactory<TOptions>>(), sp.GetRequiredService<IOptionsMonitorCache<TOptions>>());
});
Services.TryAddSingleton<IOptions<TOptions>>((sp) =>
services.TryAddSingleton<IOptions<TOptions>>((sp) =>
{
return new MultiTenantOptionsManager<TOptions>(sp.GetRequiredService<IOptionsFactory<TOptions>>(), sp.GetRequiredService<IOptionsMonitorCache<TOptions>>());
});

Services.AddSingleton<IConfigureOptions<TOptions>, ConfigureOptions<TOptions>>((IServiceProvider sp) =>
services.AddSingleton<IConfigureOptions<TOptions>, ConfigureOptions<TOptions>>((IServiceProvider sp) =>
{
var tenantAccessor = sp.GetRequiredService<IMultiTenantContextAccessor<T>>();
return new ConfigureOptions<TOptions>((options) => tenantOptionsConfiguration(options, tenantAccessor.TenantInfo));
Expand Down
2 changes: 1 addition & 1 deletion src/Services/AsyncLocalMultiTenantContextAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public W? TenantInfo
/// <remarks>
/// https://github.com/aspnet/HttpAbstractions/pull/1066
/// </remarks>
private class TenantInfoHolder()
private class TenantInfoHolder
{
public W? Context;
}
Expand Down
10 changes: 8 additions & 2 deletions src/Services/InMemoryLookupService.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
namespace MultiTenant.AspNetCore.Services
{
internal class InMemoryLookupService<T>(IEnumerable<T> Tenants) : ITenantLookupService<T> where T : ITenantInfo
internal class InMemoryLookupService<T> : ITenantLookupService<T> where T : ITenantInfo
{
private readonly IEnumerable<T> tenants;

public InMemoryLookupService(IEnumerable<T> Tenants)
{
tenants = Tenants;
}
public Task<T?> GetTenantAsync(string identifier)
{
return Task.FromResult(Tenants.SingleOrDefault(t => t.Identifier == identifier));
return Task.FromResult(tenants.SingleOrDefault(t => t.Identifier == identifier));
}
}
}
6 changes: 3 additions & 3 deletions test/MultiTenant.AspNetCore.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand All @@ -10,7 +10,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.29" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="xunit" Version="2.5.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1">
Expand Down
10 changes: 7 additions & 3 deletions test/TestTenant.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
namespace MultiTenant.AspNetCore.Tests
using System.ComponentModel.DataAnnotations;

namespace MultiTenant.AspNetCore.Tests
{
internal class TestTenant : ITenantInfo
{
public required string Id { get; set; }
public required string Identifier { get; set; }
[Required]
public string Id { get; set; }
[Required]
public string Identifier { get; set; }
}
}