dazinator / Dotnettency

Mutlitenancy for dotnet applications
MIT License
111 stars 23 forks source link

Fix issue when empty ServiceCollections are used. #38

Closed dazinator closed 5 years ago

dazinator commented 6 years ago

As per https://github.com/aspnet/Mvc/issues/8340

Need to re-think design of creating new Empty ServiceCollection's per tenant, when gathering services registrations.

The current approach is:

  1. Use ServiceCollection at ApplicationServices level to populate an autofac or structuremap container which is the "root" container, for which an IServiceProvider is returned to asp.net core stack.
  2. Create a child container for each tenant (derived from the root container).
  3. Allow child container to configured with additional services / overrides. I do this, by currently, by creating a new Empty ServiceCollection and allowing the tenants services to be added to it.
  4. Use the tenant's ServiceCollection , to populate the newly created child container for that tenant with those additional registrations.

Basically this approach let's you configure the child container by adding services to a new ServiceCollection for that tenant, which starts of empty.

The problem comes, because when AddMvc is called (and possibly other Extension Methods) they expect to find certain services allready registered in the ServiceCollection - like IHostingEnvironment. In my scenario, because when configuring a tenant's services, the ServiceCollection is only to collect delta registrations, it starts of empty, and therefore the AddMvc configuration method can't find expected services and ends up configuring MVC wrong at the tenant level.

I am not sure of the best way to resolve this just yet.

kellypleahy commented 5 years ago

Could you make a decorator for IServiceCollection that adds to the "child" service collection, but when enumerated or when requesting any items from the collection, it checks for items in both the root collection and the "child" collection?

dazinator commented 5 years ago

It's an interesting idea, thanks. This could work. The child collection wouldn't see services added to the root container through other means (i.e if you added structure map, or autofac registratons directly) but it would see the services from the root IServiceCollection which would be all we need, so I think it would be workable.

One for the backlog ;-)

dazinator commented 5 years ago

I've done a little bit more work on this recently.

I'm allowing you to pass an IServiceCollection that contains the "default" services that you want to include in the tenant's IServiceCollection before the per tenant services are then registered using AddMvc and the like.

        public void ConfigureServices(IServiceCollection services)
        {      
            // take a clone of the default "hosting" level services provided by the platform.
            // We'll give these to dotnettency later so they can be also included in per tenant services.
            var defaultServices = services.Clone();            

            services.AddMultiTenancy<Tenant>((builder) =>
             {
                 builder.IdentifyTenantsWithRequestAuthorityUri()
                        .InitialiseTenant<TenantShellFactory>()
                        .AddAspNetCore() // I now also provide AddOwin()
                        .ConfigureTenantContainers((containerOptions) =>
                        {
                            containerOptions
                            .SetDefaultServices(defaultServices) // here it is.. just means these services will be in tenantServices already before below delegate is executed..
                            .Autofac((tenant, tenantServices) =>
                            {
                                tenantServices.AddRazorPages()
                                        .AddNewtonsoftJson();
                            });                          
                        })
                        .ConfigureTenantMiddleware((tenantOptions) =>
                        {
                            // I now also provide OwinPipeline()                        
                            tenantOptions.AspNetCorePipeline((context, tenantAppBuilder) =>
                            {
                                tenantAppBuilder.Use(async (c, next) =>
                                {
                                    Console.WriteLine("Entering tenant pipeline: " + context.Tenant?.Name);
                                    await next.Invoke();
                                });
                                tenantAppBuilder.UseRouting();
                                tenantAppBuilder.UseAuthorization();
                                tenantAppBuilder.UseEndpoints(endpoints =>
                                {                                  
                                        endpoints.MapRazorPages();                                    
                                });
                            });
                        });
               });
        }
dazinator commented 5 years ago

I'll address this issue in the form of a working razor pages example.

51