Skip to content
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

Add a service configuration pattern, similar to UseStartup<> from IWebHostBuilder #42364

Closed
EccentricVamp opened this issue Sep 17, 2020 · 44 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-Hosting
Milestone

Comments

@EccentricVamp
Copy link

EccentricVamp commented Sep 17, 2020

Most of the following ideas and syntax are copied from suggestions here: #42258, with modifications taken from the discussion below

Background and Motivation

IWebHostBuilder has carried over a convenient pattern to separate HostBuilder boilerplate from it's DI service and HTTP pipeline configuration.

According to Tratcher

Startup originated back with Microsoft.Owin and only had a Configure method for setting up an http request pipeline. AspNetCore expanded on that to add ConfigureServices for setting up DI, but the http pipeline was still the primary focus.

With IHostBuilder, ConfigureServices has moved directly to to the host builder and Configure moved to a web app specific area. The web app specific area maintained the Startup pattern for backwards compatibility, but now it's only a bridged over these two new locations.

The key takeaway here is that .UseStartup<TStartup> has functionality that is now useful outside of web hosting, but it's usage is still restricted to IWebHostBuilder

Proposed API

If we extrapolate from Tratcher's reply here, with modifications from eerhardt's reply here, then I think the signature might look like:

namespace Microsoft.Extensions.Hosting
{
+   public interface IServiceConfigurer
+   {
+       IConfiguration Configuration { get; set; }
+       void ConfigureServices(IServiceCollection services);
+   }

    public static class HostBuilderExtensions
    {
+       public static IHostBuilder ConfigureServices<T>(this IHostBuilder hostBuilder)
+           where T : IServiceConfigurer, new();
    }
}

Usage Examples

Program entry and hosting boilerplate (Program.cs)

using Microsoft.Extensions.Hosting;

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
        await host.RunAsync();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices<Services>();
}

Service configuration (Services.cs)

using Microsoft.Extensions.Hosting;

public class Services : IServiceConfigurer
{
    public IConfiguration Configuration { get; set;}

    public Services(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // Configure your services here
    }
}

Alternative Designs

A signature identical to IWebHostBuilder's was also considered:

namespace Microsoft.Extensions.Hosting
{
    public static class HostBuilderExtensions
    {
+        public static IHostBuilder UseStartup<T>(this IHostBuilder hostBuilder) where T : class;
    }
}

However, according to Tratcher

This one concerns me because we'd end up with two UseStartup APIs that worked very differently. If you called this new one from a web app then it wouldn't call Configure to build the request pipeline and your app would 404 (or fail to start?).

Risks

Other than the above concern (in Alternative Designs), I can't think of any.

@EccentricVamp EccentricVamp added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Sep 17, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Extensions-Hosting untriaged New issue has not been triaged by the area owner labels Sep 17, 2020
@ghost
Copy link

ghost commented Sep 17, 2020

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

@Tratcher
Copy link
Member

Clarification: The new API should be an extension method on IHostBuilder.

@EccentricVamp
Copy link
Author

Thanks for the clarification. I wasn't sure exactly what "Adding" meant. I've updated the proposal.

@Tratcher
Copy link
Member

We should encourage a new naming convention while we're at it to help distinguish this from the old Startup pattern. How about Services?

hostBuilder.ConfigureServices<Services>()

public class Services
{
    public IConfiguration Configuration { get; }

    public Services(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // Configure your services here
    }
}

@eerhardt
Copy link
Member

I've always wondered why we don't have an interface that declares the correct shape of the Configure/ConfigureServices method. That seems like the more typical way this is done in .NET:

// in the Hosting library:
public interface IServiceConfigurer
{
    void ConfigureServices(IServiceCollection services);
}

// in the user's application:
public class Services : IServiceConfigurer
{
    public IConfiguration Configuration { get; }

    public Services(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // Configure your services here
    }
}

If you wanted to take it one step further, and eliminate the need for reflection all together, you could make the generic have a new() constraint on it, and make the Configuration a settable property.

public interface IServiceConfigurer
{
    IConfiguration Configuration { get; set; }
    void ConfigureServices(IServiceCollection services);
}

public static class HostBuilderExtensions
{
    public static IHostBuilder ConfigureServices<T>(this IHostBuilder hostBuilder) where T : IServiceConfigurer, new();
}

@eerhardt eerhardt removed the untriaged New issue has not been triaged by the area owner label Sep 21, 2020
@eerhardt eerhardt added this to the Future milestone Sep 21, 2020
@davidfowl
Copy link
Member

Read this epic discussion dotnet/aspnetcore#11050

@eerhardt
Copy link
Member

The "convention based" pattern using Reflection is not very linker or AOT friendly. I would think we would want to move away from using Reflection, when possible.

@davidfowl
Copy link
Member

davidfowl commented Sep 21, 2020

That's not the important bit. The thing to take away from the discussion is that the reason it uses reflection (at least today) is because the signature isn't fixed.

@eerhardt
Copy link
Member

the signature isn't fixed

With the generic host - what isn't fixed? There's only 1 thing being proposed: public void ConfigureServices(IServiceCollection services).

And in the future, if we want to add more methods, we can add more optional interfaces.

@davidfowl
Copy link
Member

ConfigureServices is fixed, Configure isn't.

@eerhardt
Copy link
Member

Ok, so that's why it hasn't been done traditionally in ASP.NET.

From @Tratcher's #42258 (comment)

With IHostBuilder, ConfigureServices has moved directly to to the host builder and Configure moved to a web app specific area.

If we are only concerning ourselves with the generic Host here (since this is proposing an API for Extensions.Hosting), do we need to continue the convention based design going forward in the generic Host? Or can we make it a "contract-based" design?

@EccentricVamp
Copy link
Author

EccentricVamp commented Sep 22, 2020

If we are only concerning ourselves with the generic Host here (since this is proposing an API for Extensions.Hosting), do we need to continue the convention based design going forward in the generic Host? Or can we make it a "contract-based" design?

Not sure if this question is directed at me or not, but my original reason for joining this conversation was my desire for a standardized/elegant pattern for separating DI injection from HostBuilder boilerplate. In doing so, I'd like to avoid creating as few demons as possible. So my instinct is to lean towards a contract-based design, in order to avoid creating yet another Startup.

That said, the convention-based and contract-based designs both meet my needs, but the above proposal and usage example do not cover Configure's flexible signature, nor it's role in host configuration/testing. Someone else will have to provide input for that use-case and what kind of API would satisfy that need.

@davidfowl
Copy link
Member

It's fine I just don't see the benefit in having a Startup class with just ConfigureServices, if others do I'm willing to be flexible and have that discussion. I'd rather not call it UseStartup because it'll cause confusion with the web host's startup.

@EccentricVamp
Copy link
Author

EccentricVamp commented Sep 22, 2020

I'd rather not call it UseStartup

We're agreed on that.

I just don't see the benefit in having a Startup class with just ConfigureServices

I have to assume I'm in the small subset of people with a project that doesn't need to inject dependencies into Configure. But I'd assume the majority of people will need some sort of Configure method in order to reference resolved services.

@KyleMit and @sonicmouse may be able to provide more input here, as they originally hunted and posted, respectively, the bounty for the extension that became this proposal. And Andy (sonicmouse) himself posted the original issue.

@davidfowl
Copy link
Member

I have to assume I'm in the small subset of people with a project that doesn't need to inject dependencies into Configure. But I'd assume the majority of people will need some sort of Configure method in order to reference resolved services.

What would the signature of Configure be?

@davidfowl
Copy link
Member

Some additional questions:

  • When you register multiple "Startup" classes, do they all run?
  • There's a concept of an IStartupFilter in the webhost (though they are only for the Configure phase), would we try to make those work in this scenario?
  • Async support, we're looking at supporting async Configure (maybe ConfigureServices), so that should be taken into account as well.

@ronwarner

This comment has been minimized.

@Tratcher
Copy link
Member

@ronwarner that's a well known concern, but not what we're trying to address in this issue.

@ronwarner
Copy link

@Tratcher - Is there a pertinent open issue for that?

@Tratcher
Copy link
Member

@ronwarner see dotnet/aspnetcore#9337 and related issues linked from there.

@Tratcher
Copy link
Member

"Configure" is web app specific and out of scope here. Please take that conversation back to dotnet/aspnetcore#11050.

@EccentricVamp
Copy link
Author

EccentricVamp commented Sep 23, 2020

"Configure" is web app specific and out of scope here

I'm not totally familiar with how related issues are discussed around here, but if Configure is out of scope, then I'd say this discussion concluded.

@eerhardt, we should go with the contract-based design, since we no longer need to consider the flexible signature of Configure. I'll update the issue to match your suggestion for eliminating reflection.

@davidfowl, to answer your remaining (in-scope) questions:

  • According to @eerhardt's suggested design, I think all registered IServicesConfigurer classes would run
  • We could replicate IStartupFilter functionality for ConfigureService, but that seems out-of-scope for this discussion?
  • Async support seems logical, and relatively straightforward now that we don't have to worry about reflection. Just not sure what the best practice / convention would be. Maybe a separate IServiceConfigurerAsync? But that smells bad to me.

@davidfowl
Copy link
Member

I'm not totally familiar with how related issues are discussed around here, but if Configure is out of scope, then I'd say this discussion concluded.

I don't think it's out of scope. We can discuss it as part of this issue. Configure is scoped to web today but I'm curious what others think it would look like if it was also available as part of the general contract. If nothing else but to explore what the possibilities are.

We could replicate IStartupFilter functionality for ConfigureService, but that seems out-of-scope for this discussion?

Maybe but I think it warrants a discussion especially if we try to bring back something like configure.

Async support seems logical, and relatively straightforward now that we don't have to worry about reflection. Just not sure what the best practice / convention would be. Maybe a separate IServiceConfigurerAsync? But that smells bad to me.

That's one of the benefits of the late bound loose contract model. Varying signatures over time allows us to widen support for things like async or ValueTask without having an interface with a strict contract.

@maryamariyan maryamariyan modified the milestones: Future, 6.0.0 Oct 1, 2020
@dazinator
Copy link

dazinator commented Jul 9, 2021

@davidfowl How about a startup pattern where you would remove the flexible "Configure()` method, and instead, you would register a service with the host that can configure the pipeline for web hosts. For non web hosts, this service (if accidentally registered) wouldn't be utilised, as I am guessing its only the web host that is interested in being able to build a pipeline!

HostBuilder.ConfigureServices(s=>s.AddConfigureMiddleware<ConfigureMiddleware>());

and

public class ConfigureMiddleware: IConfigureMiddleware
{

  public ConfigureMiddleware(WhateverDependency youLike)
  {
  }
  public void Configure(IApplicationBuilder appBuilder)
  {
  }
}

public interface IConfigureMiddleware
{
   void Configure(IApplicationBuilder appBuilder)
}

A delegate based implementation could be supplied out of the box with an extension method to utilise it, so users don't have to implement the interface they could supply a delegate on the fly:

HostBuilder.ConfigureServices(s=>s.AddConfigureMiddlewareDelegate((appBuilder)=>{     }));

Or for those wishing to tie in existing startup classes still (i.e which would still use the reflection based approach)

HostBuilder.ConfigureServices(s=>s.AddConfigureMiddlewareWithStartup<TStartup>());

The web host could check for this service when it needs to build the pipeline, and invoke it if registered. If not registered, it could fallback to using the Configure() on the startup class as before to avoid breaking changes. This would allow those wishing to use a contract based approach and avoid reflection, to plug something in.

@Tratcher
Copy link
Member

Tratcher commented Jul 9, 2021

@dazinator isn't that pretty much what IStartupFilter already allows you to do?

@dazinator
Copy link

dazinator commented Jul 9, 2021

@Tratcher good point! Quick thought: Does that mean that the portion of UseStartup<> concerned with configuring the pipeline, works (or should work?) by registering an IStartupFilter implementation that uses reflection to find and invoke a configure method on the target startup type then?

@davidfowl
Copy link
Member

There's no support for more than one UseStartup call. The application calls UseStartup and "modules" can add IStartupFilters

@Tratcher
Copy link
Member

Tratcher commented Jul 9, 2021

UseStartup is a special case that's used as the base and IStartupFitlers are optional layers on top of that. I assume we have a check somewhere that you've called Configure or UseStartup to set up the app with that base implementation.

@dazinator
Copy link

dazinator commented Jul 9, 2021

I assume we have a check somewhere that you've called Configure or UseStartup to set up the app with that base implementation.

@Tratcher So you'd expect a WebHost that's configured purely by IStartupFilter's and doesn't have any Configure() or UseStartup (aka "base call") to be invalid and throw an exception when started? If that is not the desired behaviour then there could be an argument for refactoring UseStartup() and Configure() to add IStartupFilters so that the only thing that ever takes part in configuring a pipeline are IStartupFilters irrespective of how you do it?

@Tratcher
Copy link
Member

Tratcher commented Jul 9, 2021

@Tratcher So you'd expect a WebHost that's configured purely by IStartupFilter's and doesn't have any Configure() or UseStartup (aka "base call") to be invalid and throw an exception when started?

I think that's how it's set up today to prevent people from making easy mistakes like not calling UseStartup and getting 404s for all requests. IStartupFilters are much less common right now.

With 6.0's new minimal Program.cs we no longer need Startup and UseStartup in the common case.

@eerhardt
Copy link
Member

Moving to Future, as 6.0 has hit feature complete and we won't be adding new features to 6.0.

I wonder if we would ever do this in the future, and if we should just close this issue.

@eerhardt eerhardt modified the milestones: 6.0.0, Future Jul 22, 2021
@EccentricVamp
Copy link
Author

EccentricVamp commented Jul 24, 2021

@Tratcher, does the minimal Progam.cs you mentioned exist for HostBuilder? From examples I've seen (ASP.NET Blog, dotnetthoughts), the new minimal API's are directly tied to ASP.NET Core and IWebHostBuilder, rather than being generalized for all hosts.

As I said in original Background and Motivation section for this issue:

The key takeaway here is that .UseStartup<TStartup> has functionality that is now useful outside of web hosting, but it's usage is still restricted to IWebHostBuilder

Do I need to open an issue identical to this one, but for minimal API's this time?

@davidfowl
Copy link
Member

We've discussed a WorkerApplication similar to ASP.NET Core WebApplication but I'm not sure what's being suggested here. We're moving away from the startup pattern by default in .NET 6

@EccentricVamp
Copy link
Author

EccentricVamp commented Jul 24, 2021

Ah, that makes more sense now. I couldn't figure out from the examples whether the "minimal API" was being provided by Microsoft.AspNetCore.Builder or Microsoft.Extensions.Hosting. I wasn't really trying to make a suggestion, just trying to get more information and figure out if I need to open another issue.

If I'm understanding the ASP.NET Blog post correctly, the minimal API for IWebHostBuilder is a consolidation of several steps (host boilerplate, DI service config, and HTTP pipeline config) that used to be segmented across several different files and methods.

If the plan is to implement an IWorkerHostBuilder with its own minimal API, then I guess this issue is closed. My primary motivation for opening this issue was to generalize the startup pattern for use by non-web hosts.

I'm guessing that generalization or duplication of IWebHostBuilder's minimal API for non-web hosts isn't on the roadmap yet? If so, should I open a new API suggestion?

@YarekTyshchenko
Copy link

YarekTyshchenko commented Aug 18, 2021

Using all the suggestions in both issues, this is the best workaround I could come up with:

namespace Example
{
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder<Services>(args).Build().Run();
        }

        private static IHostBuilder CreateHostBuilder<T>(string[] args)
            where T : IServiceConfigurer, new() =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices<T>();
    }

    public interface IServiceConfigurer
    {
        void ConfigureServices(
            IConfiguration configuration,
            IServiceCollection services);
    }

    public static class HostBuilderExtensions
    {
        public static IHostBuilder ConfigureServices<T>(this IHostBuilder hostBuilder)
            where T : IServiceConfigurer, new() =>
            hostBuilder.ConfigureServices((context, services) =>
                new T().ConfigureServices(context.Configuration, services));
    }

    public class Services : IServiceConfigurer
    {
        public void ConfigureServices(
            IConfiguration configuration,
            IServiceCollection services)
        {
            // Add services dependencies
        }
    }
}

Primary requirement of this is to inject substitute dependencies for integration testing of the host stack. There are a lot of middleware that can't be tested in isolation.

@loic-sharma
Copy link
Contributor

Is there a place we can learn more about WorkerApplication's potential design?

@julealgon
Copy link

We're moving away from the startup pattern by default in .NET 6

@davidfowl could you provide a link to a sample on how this looks like in .NET 6? I came to this thread in hopes of unifying how we configured services across our web apps and worker services by potentially using Startup classes on both, but it feels like I should be moving in the opposite path and dropping Startup on all of them.

Since we are in the middle of a large scale migration to NET Core (from NET 4.8) this could end up impacting some of our migrated code in hopes to generate as few diffs as possible when moving from NET5 to NET6.

@eerhardt
Copy link
Member

I'm going to close this issue as "not planned". In .NET 7, we've added Host.CreateApplicationBuilder (see #61634 (comment)) which is designed like WebApplication.CreateBuilder. You can unify how you configure services across web apps and worker services by calling a method from Main, passing in the ServiceCollection you get from each builder.Services.

could you provide a link to a sample on how this looks like in .NET 6?

Check out the docs here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-6.0

@eerhardt eerhardt closed this as not planned Won't fix, can't repro, duplicate, stale Jul 15, 2022
@ronwarner
Copy link

I'm going to close this issue as "not planned". In .NET 7, we've added Host.CreateApplicationBuilder (see #61634 (comment)) which is designed like WebApplication.CreateBuilder.

Will this provide a technique for (or a replacement for) two-phase DI? We built a framework for our web API microservices in .NET Core 2.x/3.x where we must register a select few of our services early. These services provide access to configuration settings, some Graph calls and various security details which the other services must access during their own registrations. Previously this was done using the two-phase DI offered by Program.cs and Startup.cs. Of course, we ran into issues with this once we start making headless jobs using BackgroundService and the Generic Host which did not offer this two-phase container construction.

So what is the guidance going forward if you have applications that must first register cross-cutting services, and then use them to access information needed while registering the rest of the services?

@eerhardt
Copy link
Member

Because all the code happens in Main, and it executes directly instead of during callbacks, the app developer is in control of how they want their application to run/startup. If you need a two-phase DI, you can just make one - create a ServiceCollection, add services to it, build it into an IServiceProvider, and then pass that IServiceProvider to the rest of your service registration logic.

The new pattern has a ConfigurationManager class that acts as both an IConfigurationBuilder and an IConfiguration. So you can add a config source to it, and get config values out of it immediately without waiting for some "build the configuration" step.

Try it out and open a new issue for things that aren't working for you.

@davidfowl
Copy link
Member

2 phase DI is why we moved away from this pattern in the first place. Initially we had the web host builder which was real 2 phase DI then we got rid of that and only made it so you could inject the hosing environment. Now to this.

@ronwarner
Copy link

2 phase DI is why we moved away from this pattern in the first place. Initially we had the web host builder which was real 2 phase DI then we got rid of that and only made it so you could inject the hosing environment. Now to this.

@davidfowl @eerhardt - Thanks for the responses. I've gleaned that your team is not wild about creating two containers, I'm just not sure there's another way to register services and IOptions and then use them in registering the rest of the services, though I had held out upgrading in hopes you were coming up with something magical. If Eric's method is the way to go about it, I'll give that a shot. It's something we've kept coming back to as our high-level to-do list (which we try to get to when the daily problems are less) has looked something like this:

  • Upgrade to .NET 5 Wait for .NET 6
  • Upgrade to .NET 6 Wait for .NET 7

Obviously we don't want that pattern to go on indefinitely!

@julealgon
Copy link

I'm just not sure there's another way to register services and IOptions and then use them in registering the rest of the services

@ronwarner wouldn't a factory resolve the issue in your case? The factory could take the IOptions instance and return the objects you need.

You can also abstract away the factory completely by introducing an indirect wrapper for your main interface that leverages the factory internally, so consumers would still work with the same interface as they are today.

@dazinator
Copy link

dazinator commented Jul 18, 2022

@ronwarner
Bootstrapping.. a fun issue!
I explored this territory a while ago and came to a conclusion..you start off as in your case needing to bootstrap just a DI container and maybe IConfiguration... and what if your bootstrapping code has an issue? of course you need logging for this bootstrap phase as well.. maybe seperate from your "fully bootstrapped app logging"

I found the most natural way to deal with this was to build an initial and seperate IHost and call that the bootstrap environment / host. In .net core an IHost is typically all about containing this life cycle.. so rather than just creating a bootstrap DI container off to one side and then discovering you need xyz as well later on - by creating a IHost you can use all standard capabilities in that bootstrap environment such as logging, configuration, DI, background services etc etc in a familiar way consistent with your "proper" app IHost. I did eventually expand this into a concept of parent / child hosts, where IHosts live in a tree structure and can be dynamically added / removed / restarted at runtime - but that's a different use case and probably a step beyond what you (99% of others) need :-)

@ghost ghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-Hosting
Projects
None yet
Development

No branches or pull requests