Skip to content

Support nested registration of Startups #242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
MartinJohns opened this issue Dec 19, 2014 · 11 comments
Closed

Support nested registration of Startups #242

MartinJohns opened this issue Dec 19, 2014 · 11 comments

Comments

@MartinJohns
Copy link
Contributor

In my old projects I used to have a solution with a web-project that renders a SPA frontend (using webpages) and a service-project that has a RESTlike API (using Web API). The service was hosted within the web-project out of cost issues, but I always kept the projects strictly separated so I could always host the service and the web project on each own.

With ASP.NET 5 and the merging of MVC, WebPages and Web API into one-MVC framework and with the global dependency injection this becomes more difficult. You can just have one single service collection that is used by all projects and registered in one Startup. You can't easily create sub-startups with their own service collection that is hosted in the same process.

With the help of PinPoint in the Jabbr chat I created a test project which allows this behavior to demonstrate my use case, but it's not nicely done.

https://github.com/MartinJohns/NestedStartupTesting/

In here I added an extension method ApplicationBuilderExtensions.IsolatedMap. It's basically a glorified app.Map("/api") call that creates an isolated new application builder and hooks up a MapMiddleware in it. For this to work properly I need to create a copy of the IServiceCollection within my web-startup and pass it to my IsolatedMap-call. This is the ugly part that I can't find a good solution for.

This approach allows me to call service.AddMvc() and app.UseMvc both in the web- and in the service-project distinctly. Both projects can add their own services and they don't mix up. This also allows me to either host my web-project together with my service-project, or host the service-project on it's own without doing any change. It has it's own Startup that is recognized and run by the environment. I could also create a hosting project and make it configurable (via IConfiguraton) which other projects are hosted within it, which would be great for a system where you can just add plugins that each work on their own.

My solution may work, but I consider it ugly. I have two working points, once in ConfigureServices where I need to clone the incoming service collection, and once in Configure where I actually register the sub-app. I need to copy the service collection so the sub-app has an equal starting point, the same pre-requisites as the outer-app (e.g. IHostingEnvironment).

Therefor I propose a new functionality that would make this more easy possible, but would require changes in (I think) the hosting environment:

public class Startup
{
    public Startup(IStartupMagic conf)
    {
        // Sub-app is registered in constructor

        // Startup of ServiceProject is registered and hooked under the mapped path "/api".
        // It will use the same discovery of the ConfigureServices and Configure methods,
        // and will pass the same services as it will in this Startup.
        conf.RegisterSubApp<ServiceProject.Startup>("/api");

        // A theoretically overload that will inject additional services into the sub-app
        conf.RegisterSubApp<ServiceProject.Startup>(
            "/api",
            services =>
            {
                services.AddTransient<...>(...);
            });
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // Do stuff
    }

    public void Configure(IApplicationBuilder app)
    {
        // Do stuff
    }
}
@Eilon
Copy link
Member

Eilon commented Dec 20, 2014

Thanks for the great write-up! We have yet to have a serious discussion in the team about multi-tenancy (of which this is one aspect), but your notes are definitely helpful in explaining one of the scenarios.

@MartinJohns
Copy link
Contributor Author

Any news to this? Did you have a serious discussion about this very important subject?

@Eilon
Copy link
Member

Eilon commented May 24, 2015

@MartinJohns sorry, we have not had this discussion. To be honest it hasn't come up much outside of this issue. I of course don't doubt the scenario is important, but it hasn't bubbled up close enough to the top of the priority list just yet.

One thing that I think is theoretically possible right now, but perhaps a bit more difficult than it ought to be, is that if you fully use .Map(...) and "fork" the middleware pipeline, is that you might be able to use the RequestServicesContainerMiddleware in each of the forks, and thus have each fork get its own set of services by passing in those services to the middleware). Now, mind you, this is completely theoretical, and I haven't tried it, and I have no idea if it'll work, but I believe that's kind of sort of maybe possibly the intention of that code. We certainly didn't wrap it up in any convenient API or anything, though...

So, if you're feeling especially brave, it could be worth peeking into that corner of the system to see what might be possible!

@glen-84
Copy link
Contributor

glen-84 commented Jun 8, 2015

This is an interesting idea. I'm trying to figure out the best way to create a module/plug-in system so that you can drop in an assembly (via NuGet most likely) and it will have its own controllers, views, etc.

I was initially thinking of having something like a ModuleStartup class in each module. The modules would be "registered" with the host application, which would then invoke a method on each, passing it the IServiceCollection and allowing it to register additional "services".

Ideally, each module would be able to register its services into a child container to provide some isolation, and allow each module to have its own set of implementations (f.e. its own string localizer).

One thing that I wasn't sure about was how to add items such as routes and DB contexts to the host application, as these things seem to usually run off calls to methods like AddEntityFramework and UseMvc, and I'm not yet sure if there are side-effects to calling them more than once.

Using the existing Startup class might bring some additional benefits, such as being able to run the module directly (without a host application), and being able to customize configuration (f.e. MVC options) on a per-request/path basis.

I also like the more explicit way of finding controllers per-assembly, instead of looking for controllers "everywhere".

Unless I'm misunderstanding something, there is one major flaw: If you register a module (f.e. a "Blogs" module) based on a path ("/blogs"), then the services will only be registered/configured under that specific path, so pages in the host application would be unaware of the new configuration, meaning that it would not be possible to reference a route in the module (in order to link to it). I may be missing the whole idea here, so my apologies if that is the case.

@Eilon
Copy link
Member

Eilon commented Jun 8, 2015

@glen-84 solving multi-tenancy and modular applications is... a huge task 😄 In the existing universe of ASP.NET applications that's where platforms such as Orchard and DNN came in and created a whole system for these types of modular applications.

@glen-84
Copy link
Contributor

glen-84 commented Jun 8, 2015

@Eilon,

I know that it's not simple, but if the following was required for this to work (as an example):

  1. Custom assembly locator/provider.
  2. Custom controller locator/provider.
  3. Custom router with strongly-typed routes. (link)
  4. Custom view engine with strongly-typed views. (link)
  5. Custom work to support EF7/migrations/etc. (link 1, link 2)
  6. Possibly even a custom hosting/startup system.
  7. etc.

... then ASP.NET/MVC wouldn't really be offering much, and it would probably make more sense to just create an entirely new framework. And before you suggest just that (:smile:), I don't have that amount of free time (or expertise, most likely).

As for things like Orchard, I have looked at that to a certain degree, but I just wanted something relatively simple and I have no idea when they might have a solution for ASP.NET 5, and whether or not it would suit my needs.

Anyway, I just wanted to indicate my interest in this type of architecture, as I think it would be extremely useful.

@adamvoss
Copy link

As a voice of support, I am encountering this as I suspect are people looking for aspnet/DependencyInjection#246.

I agree a universal solution may not be available; however, with DI throughout I do think it should be easier than it is. I had hoped that with Map a different service provider could be used within the context of the mapped route. Unfortunately, it seems that assigning ApplicationServices affects all ApplicationBuilder with the default implementation anyway.

Here is what I attempted (based on the Web API ASP.NET 5 Preview Template):

app.Map(
    "/comp",
    c =>
        {
            var services = new ServiceCollection();
            foreach (var service in this._services)
            {
                services.Add(service);
            }
            services.AddMvc().WithControllersAsServices(new[] { typeof(ValuesController) });
            c.ApplicationServices = services.BuildServiceProvider();

            c.UseMvc();
        });

app.Map(
    "/comp2",
    c =>
        {
            var services = new ServiceCollection();
            foreach (var service in this._services)
            {
                services.Add(service);
            }
            services.AddMvc().WithControllersAsServices(new Type[0]);
            c.ApplicationServices = services.BuildServiceProvider();

            c.UseMvc();
        });

My hope/expectation was that ValuesController would be route-able under comp while (in this case) nothing would be under comp2. Unfortunately, the last-one-wins so in this case the result is nothing is available under either.

@Eilon
Copy link
Member

Eilon commented Jun 26, 2015

@rynowak for your FYI information re: multi-tenancy.

@MartinJohns
Copy link
Contributor Author

I think it's a pity that such an important feature is not considered for the initial release. I understand that this might be a bit more exotic, but it is something that likely can't easily be added on top of the existing frameworks, but instead needs to be tackled very far down.

I tried updating my test-project to beta6, but unfortunately it does not work anymore. The inner "Service" MVC pipeline is not triggered.
https://github.com/MartinJohns/NestedStartupTesting/

When navigation to /api/Sample you get the 404 error I return in the Service-Startup class. But when navigation to /api/Home it strangely uses the HomeController from my Web-assembly, while here it actually should use the Service-assembly.

I have two distinct IApplicationBuilder and two distinct ServiceCollections, so I don't understand how this mixup is happening.

@davidfowl
Copy link
Member

@MartinJohns You probably need to set RequestServices to a child container once it enters your IsolatedMap

@aspnet-hello
Copy link

This issue is being closed because it has not been updated in 3 months.

We apologize if this causes any inconvenience. We ask that if you are still encountering this issue, please log a new issue with updated information and we will investigate.

natemcmaster pushed a commit that referenced this issue Oct 17, 2018
natemcmaster pushed a commit that referenced this issue Nov 13, 2018
Implicit imports prevents using <Import> on a project file that has the Sdk attribute. This change instead generates a file in the MSBuildProjectExtensionsPath to inject targets require to find the UserSecretsId property in a project.

Resolves #242
natemcmaster pushed a commit that referenced this issue Nov 30, 2018
@ghost ghost locked as resolved and limited conversation to collaborators Dec 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants