Hi @myquay
Not sure if this project is still active, but in addition to #7, I found that some other services aren't working including Coravel so I took another look. Basically the issue occurs in anything that had a singleton in the root container.
I think the issue is in the MultiTenantServiceScopeFactory:
internal class MultiTenantServiceProviderFactory<T>(
IServiceCollection containerBuilder,
Action<IServiceCollection, T?> tenantServiceConfiguration)
where T : ITenantInfo
{
//Cache compiled providers
private readonly ConcurrentDictionary<string, Lazy<IServiceProvider>> _compiledProviders = new();
public IServiceProvider GetServiceProviderForTenant(T tenant)
{
var sp = _compiledProviders
.GetOrAdd(tenant.Id, _ => new Lazy<IServiceProvider>(() => {
var container = new ServiceCollection();
//Add all root services
foreach (var service in containerBuilder)
{
container.Add(service);
}
//Add tenant specific services
tenantServiceConfiguration(container, tenant);
return container.BuildServiceProvider(); }))
.Value;
return sp;
}
}
The code is copying the service descriptors from the root container but creating entirely new instances when building the tenant container (BuildServiceProvider()). This means:
You lose the existing initialised services from the root container
You potentially create duplicate instances of services that should be shared
You break the scope hierarchy that might be important for things like DbContext lifecycles
I'm not sure how Autofac prevents this (or if it does) but the only solution I can see is take in the IServiceProvider:
public MultiTenantServiceProviderFactory(
IServiceCollection rootServices,
IServiceProvider rootProvider,
Action<IServiceCollection, T?> tenantServiceConfiguration)
{
_rootServices = rootServices;
_rootProvider = rootProvider;
_tenantServiceConfiguration = tenantServiceConfiguration;
}
In the TenantBuilder you'll need to pass in the IServiceProvider:
public TenantBuilder<T> WithTenantedServices(
Action<IServiceCollection, T?> configuration)
{
//Replace the default service provider with a multitenant service provider
if (!options.DisableAutomaticPipelineRegistration)
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<MultiTenantServiceProviderFactory<T>>(sp => {
var rootProvider = sp.GetRequiredService<IServiceProvider>();
return new MultiTenantServiceProviderFactory<T>(
rootServices: Services,
rootProvider: rootProvider,
tenantServiceConfiguration: configuration
);
}
);
return this;
}
Then in the MultiTenantServiceScopeFactory, loop through the services, resolving any that are singletons and add them by ref to the tenant container:
internal class MultiTenantServiceProviderFactory<T> where T : ITenantInfo
{
private readonly IServiceCollection _rootServices;
private readonly Action<IServiceCollection, T?> _tenantServiceConfiguration;
private readonly ConcurrentDictionary<string, Lazy<IServiceProvider>> _tenantProviders = new();
private readonly IServiceProvider _rootProvider;
public MultiTenantServiceProviderFactory(
IServiceCollection rootServices,
IServiceProvider rootProvider,
Action<IServiceCollection, T?> tenantServiceConfiguration)
{
_rootServices = rootServices;
_rootProvider = rootProvider;
_tenantServiceConfiguration = tenantServiceConfiguration;
}
public IServiceProvider GetServiceProviderForTenant(
T tenant)
{
var lazyServiceProvider = _tenantProviders.GetOrAdd(tenant.Id, _ => new Lazy<IServiceProvider>(() => {
var tenantServices = new ServiceCollection();
foreach (var descriptor in _rootServices)
{
if (descriptor.Lifetime == ServiceLifetime.Singleton)
{
// Open generics will throw (all the IOptions, ILogger, etc), so just add their descriptor directly.
// This means that new instances will be created for each tenant.
if (descriptor.ServiceType is { ContainsGenericParameters: true, IsGenericTypeDefinition: true })
{
tenantServices.Add(descriptor);
continue;
}
try
{
// This won't work with open generics
// descriptor.ServiceType.ContainsGenericParameters == true && descriptor.ServiceType.IsGenericTypeDefinition == true
var instance = _rootProvider.GetService(descriptor.ServiceType);
if (instance != null)
{
tenantServices.AddSingleton(descriptor.ServiceType, instance);
continue;
}
}
catch (Exception)
{
// If we can't resolve it, fall back to normal registration. This means that new instances
// will be created for each tenant.
tenantServices.Add(descriptor);
}
}
tenantServices.Add(descriptor);
}
_tenantServiceConfiguration(tenantServices, tenant);
return tenantServices.BuildServiceProvider(
new ServiceProviderOptions
{
ValidateScopes = true,
ValidateOnBuild = true
}
);
}));
return lazyServiceProvider.Value;
}
}
Hi @myquay
Not sure if this project is still active, but in addition to #7, I found that some other services aren't working including Coravel so I took another look. Basically the issue occurs in anything that had a singleton in the root container.
I think the issue is in the MultiTenantServiceScopeFactory:
The code is copying the service descriptors from the root container but creating entirely new instances when building the tenant container (
BuildServiceProvider()). This means:You lose the existing initialised services from the root container
You potentially create duplicate instances of services that should be shared
You break the scope hierarchy that might be important for things like DbContext lifecycles
I'm not sure how Autofac prevents this (or if it does) but the only solution I can see is take in the IServiceProvider:
In the TenantBuilder you'll need to pass in the IServiceProvider:
Then in the MultiTenantServiceScopeFactory, loop through the services, resolving any that are singletons and add them by ref to the tenant container: