Most of the following ideas and syntax are copied from suggestions here: #42258, with modifications taken from the discussion below
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
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();
}
}
Program entry and hosting boilerplate (Program.cs)
```c#
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)
```c#
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
}
}
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?).
Other than the above concern (in Alternative Designs), I can't think of any.
Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.
Clarification: The new API should be an extension method on IHostBuilder.
Thanks for the clarification. I wasn't sure exactly what "Adding" meant. I've updated the proposal.
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>()
```C#
public class Services
{
public IConfiguration Configuration { get; }
public Services(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
// Configure your services here
}
}
```
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:
```C#
// 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.
```C#
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();
}
Read this epic discussion https://github.com/dotnet/aspnetcore/issues/11050
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.
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.
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.
ConfigureServices is fixed, Configure isn't.
Ok, so that's why it hasn't been done traditionally in ASP.NET.
From @Tratcher's https://github.com/dotnet/runtime/issues/42258#issuecomment-692833683
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?
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.
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.
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.
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?
Some additional questions:
My organization and I have a lot of custom code that relies on being able to register "early" the dependencies that will be used in Startup. These early registrations are done as part of the host creation. Then, we can inject all of them into the Startup constructor so that they can be used to aid the registration and configuration of everything else. At this point, this is an indispensable feature of most of our apps, which compose a framework of microservices.
As we've begun to implement headless jobs to complement this framework, we've turned to creating them as IHostedService apps, implementing Microsoft.Extensions.Hosting.BackgroundService. It has become necessary to use the same two-step DI pattern in these apps as we do in our web apps. This is missing, obviously, from the non-web host builder scenario, hence this open issue. I've spent hours in recent weeks combing the web for the various workarounds people have come up with. Until something official exists in .NET Core (or until we can completely rewrite our framework!), I'm going to be forced to cobble something together out of these workarounds.
I'm commenting basically to say that the need for this is very real, and I can't be the only one anxiously awaiting a way to create BackgroundService apps with dependencies registered early for assisting in registering the rest of the dependencies.
@ronwarner that's a well known concern, but not what we're trying to address in this issue.
@Tratcher - Is there a pertinent open issue for that?
@ronwarner see https://github.com/dotnet/aspnetcore/issues/9337 and related issues linked from there.
"Configure" is web app specific and out of scope here. Please take that conversation back to https://github.com/dotnet/aspnetcore/issues/11050.
"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:
IServicesConfigurer classes would runIStartupFilter functionality for ConfigureService, but that seems out-of-scope for this discussion?IServiceConfigurerAsync? But that smells bad to me.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.