Last active
May 15, 2023 05:30
-
-
Save davidfowl/d350561ea621693176bbc6efd74a1426 to your computer and use it in GitHub Desktop.
Multitenancy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Threading; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.Extensions.DependencyInjection; | |
/* | |
- Don't need to support different services per tenant. | |
- Tenant services lifetime outlasts the request. | |
- Need singletons per tenant and true application singletons to be separate. | |
*/ | |
namespace Multitenancy | |
{ | |
public class Program | |
{ | |
public static void Main(string[] args) | |
{ | |
var services = new ServiceCollection(); | |
// Tenant scoped services | |
services.AddTenantService<TenantScopedService>(); | |
// Regular services | |
services.AddTransient<HomeController>(); | |
services.AddScoped<ScopedService>(); | |
services.AddSingleton<SingletonService>(); | |
services.AddTransient<TransientService>(); | |
// Tenant scoped Infrastrucure | |
services.AddSingleton<ITenantServiceScopeProvider, TenantServiceScopeProvider>(); | |
services.AddSingleton(typeof(ITenantService<>), typeof(TenantService<>)); | |
var tentantProvider = new TenantIdProvider(); | |
services.AddSingleton<ITenantIdProvider>(tentantProvider); | |
// Make sure we validate scopes | |
var serviceProvider = services.BuildServiceProvider(validateScopes: true); | |
// EXAMPLE: This is a sample showing the usage of tenant scoped services interleaved with the request scope | |
HomeController homeController1 = null; | |
HomeController homeController2 = null; | |
// Set the current tenant id | |
tentantProvider.TenantId = Guid.NewGuid(); | |
// Request 1 | |
using (var scope = serviceProvider.CreateScope()) | |
{ | |
homeController1 = scope.ServiceProvider.GetRequiredService<HomeController>(); | |
var result = homeController1.Index(); | |
} | |
// Request 2 | |
using (var scope = serviceProvider.CreateScope()) | |
{ | |
homeController2 = scope.ServiceProvider.GetRequiredService<HomeController>(); | |
var result = homeController2.Index(); | |
} | |
Console.WriteLine("Tenant instances across requests are equal = " + ReferenceEquals(homeController1.TenantService, homeController2.TenantService)); | |
Console.WriteLine("True singletons are the same between tenants and other scopes = " + ReferenceEquals(homeController1.SingletonService, homeController1.TenantService.Singleton)); | |
Console.WriteLine("=============================================================="); | |
Console.WriteLine("Scoped service resolved from controller disposed = " + homeController1.ScopedService.Disposed); | |
Console.WriteLine("Transient service resolved from controller disposed = " + homeController1.TransientService.Disposed); | |
Console.WriteLine("=============================================================="); | |
Console.WriteLine("Scoped service resolved from tenant disposed = " + homeController1.TenantService.Scoped.Disposed); | |
Console.WriteLine("Transient service resolved from tenant disposed = " + homeController1.TenantService.Transient.Disposed); | |
Console.WriteLine("=============================================================="); | |
// This instance should be the same | |
Console.WriteLine("Tenant scoped instance disposed after request scope = " + homeController1.TenantService.Disposed); | |
// Dispose the tenant provider | |
var provider = serviceProvider.GetRequiredService<ITenantServiceScopeProvider>(); | |
provider.DisposeServiceScope(tentantProvider.TenantId); | |
Console.WriteLine("=============================================================="); | |
Console.WriteLine("Disposed tenant provider"); | |
Console.WriteLine("=============================================================="); | |
Console.WriteLine("Tenant instance disposed = " + homeController1.TenantService.Disposed); | |
Console.WriteLine("Scoped service resolved from tenant disposed = " + homeController1.TenantService.Scoped.Disposed); | |
Console.WriteLine("Transient service resolved from tenant disposed = " + homeController1.TenantService.Transient.Disposed); | |
} | |
} | |
public static class TenantServiceProviderExtensions | |
{ | |
// These are just helpers to make sure TenantServices look different from transient | |
// This will fail if scoped services are injected into them | |
public static IServiceCollection AddTenantService<TService>(this IServiceCollection services) where TService : class, ITenantScoped | |
{ | |
return services.AddTenantService<TService, TService>(); | |
} | |
public static IServiceCollection AddTenantService<TService, TImplementation>(this IServiceCollection services) | |
where TService : class, ITenantScoped | |
where TImplementation : class, TService | |
{ | |
return services.AddScoped<TService, TImplementation>(); | |
} | |
} | |
// Marker interface to use as a generic constraint for tenant scoped services | |
public interface ITenantScoped | |
{ | |
} | |
// Factory for tenant scoped services | |
public interface ITenantService<T> where T : ITenantScoped | |
{ | |
// This is the instance scoped to the current tenant | |
T Value { get; } | |
} | |
// Returns the current tenant ID | |
public interface ITenantIdProvider | |
{ | |
// The current tenant id | |
Guid TenantId { get; } | |
} | |
// A service to maintain tenant specific scopes | |
public interface ITenantServiceScopeProvider : IDisposable | |
{ | |
TService GetService<TService>(Guid tenantId); | |
IServiceScope GetServiceScope(Guid tenantId); | |
void DisposeServiceScope(Guid tenantId); | |
} | |
public class TenantServiceScopeProvider : ITenantServiceScopeProvider | |
{ | |
private readonly Dictionary<Guid, IServiceScope> _scopes = new Dictionary<Guid, IServiceScope>(); | |
private readonly IServiceProvider _serviceProvider; | |
public TenantServiceScopeProvider(IServiceProvider serviceProvider) | |
{ | |
_serviceProvider = serviceProvider; | |
} | |
public TService GetService<TService>(Guid tenantId) | |
{ | |
lock (_scopes) | |
{ | |
if (!_scopes.TryGetValue(tenantId, out var scope)) | |
{ | |
scope = _serviceProvider.CreateScope(); | |
_scopes[tenantId] = scope; | |
} | |
return scope.ServiceProvider.GetRequiredService<TService>(); | |
} | |
} | |
public IServiceScope GetServiceScope(Guid tenantId) | |
{ | |
lock (_scopes) | |
{ | |
if (!_scopes.TryGetValue(tenantId, out var scope)) | |
{ | |
scope = _serviceProvider.CreateScope(); | |
_scopes[tenantId] = scope; | |
} | |
return scope; | |
} | |
} | |
public void DisposeServiceScope(Guid tenantId) | |
{ | |
lock (_scopes) | |
{ | |
if (_scopes.TryGetValue(tenantId, out var scope)) | |
{ | |
scope.Dispose(); | |
_scopes.Remove(tenantId); | |
} | |
} | |
} | |
public void Dispose() | |
{ | |
lock (_scopes) | |
{ | |
foreach (var pair in _scopes) | |
{ | |
pair.Value.Dispose(); | |
} | |
_scopes.Clear(); | |
} | |
} | |
} | |
// Implementation of the factory, requesting the Value will get the | |
// create and cache the service | |
public class TenantService<T> : ITenantService<T> where T : ITenantScoped | |
{ | |
private readonly ITenantIdProvider _accessor; | |
private readonly ITenantServiceScopeProvider _serviceScopeProvider; | |
public TenantService(ITenantIdProvider accessor, ITenantServiceScopeProvider serviceScopeProvider) | |
{ | |
_accessor = accessor; | |
_serviceScopeProvider = serviceScopeProvider; | |
} | |
public T Value => _serviceScopeProvider.GetService<T>(_accessor.TenantId); | |
} | |
// Usually this would access the IHttpContextAccessor to get the tentant id from the request information | |
public class TenantIdProvider : ITenantIdProvider | |
{ | |
private AsyncLocal<Guid> _tenant = new AsyncLocal<Guid>(); | |
public Guid TenantId | |
{ | |
get => _tenant.Value; | |
set => _tenant.Value = value; | |
} | |
} | |
// An example of a tenant scoped service | |
public class TenantScopedService : ITenantScoped, IDisposable | |
{ | |
public bool Disposed { get; set; } | |
// This will be tenant scoped as well even though it's a scoped service | |
public ScopedService Scoped { get; } | |
public SingletonService Singleton { get; } | |
public TransientService Transient { get; } | |
public TenantScopedService(ScopedService scoped, SingletonService singleton, TransientService transient) | |
{ | |
Scoped = scoped; | |
Singleton = singleton; | |
Transient = transient; | |
} | |
public void Dispose() | |
{ | |
// This should be cleaned up appropriately | |
Disposed = true; | |
} | |
} | |
// Transient service | |
public class TransientService : IDisposable | |
{ | |
public bool Disposed { get; set; } | |
public void Dispose() | |
{ | |
Disposed = true; | |
} | |
} | |
// A scoped service | |
public class ScopedService : IDisposable | |
{ | |
public bool Disposed { get; set; } | |
public void Dispose() | |
{ | |
Disposed = true; | |
} | |
} | |
// Container wide singleton | |
public class SingletonService : IDisposable | |
{ | |
public bool Disposed { get; set; } | |
public void Dispose() | |
{ | |
Disposed = true; | |
} | |
} | |
public class HomeController | |
{ | |
// ITenantService<T> is the bridge into the current tenant scope from the request scope | |
// There are 2 DI scopes at play here that interact nicely with each other | |
// 1. The HomeController is resolved in the request scope | |
// 2. The tenantService is resolved in the current tenant's scope, in essense, it acts like a composition root | |
// for the tenant scoped services | |
public HomeController(ITenantService<TenantScopedService> tenantService, | |
SingletonService singleton, | |
ScopedService scoped, | |
TransientService transientService) | |
{ | |
TenantService = tenantService.Value; | |
SingletonService = singleton; | |
ScopedService = scoped; | |
TransientService = transientService; | |
} | |
public TenantScopedService TenantService { get; } | |
public TransientService TransientService { get; } | |
public SingletonService SingletonService { get; } | |
public ScopedService ScopedService { get; } | |
public IActionResult Index() | |
{ | |
return new StatusCodeResult(404); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment