Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Last active May 15, 2023 05:30
Show Gist options
  • Save davidfowl/d350561ea621693176bbc6efd74a1426 to your computer and use it in GitHub Desktop.
Save davidfowl/d350561ea621693176bbc6efd74a1426 to your computer and use it in GitHub Desktop.
Multitenancy
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