Skip to content

Issue with Root Singleton services #8

Description

@brettwinters

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;
        }
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions