-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Comments
Tagging subscribers to this area: @eerhardt, @maryamariyan |
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?
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 // 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 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 dotnet/aspnetcore#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. |
With the generic host - what isn't fixed? There's only 1 thing being proposed: 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 #42258 (comment)
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 |
We're agreed on that.
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. |
What would the signature of Configure be? |
Some additional questions:
|
This comment has been minimized.
This comment has been minimized.
@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 dotnet/aspnetcore#9337 and related issues linked from there. |
"Configure" is web app specific and out of scope here. Please take that conversation back to dotnet/aspnetcore#11050. |
I'm not totally familiar with how related issues are discussed around here, but if @eerhardt, we should go with the contract-based design, since we no longer need to consider the flexible signature of @davidfowl, to answer your remaining (in-scope) questions:
|
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.
Maybe but I think it warrants a discussion especially if we try to bring back something like configure.
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. |
@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:
Or for those wishing to tie in existing startup classes still (i.e which would still use the reflection based approach)
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 |
@dazinator isn't that pretty much what IStartupFilter already allows you to do? |
@Tratcher good point! Quick thought: Does that mean that the portion of |
There's no support for more than one UseStartup call. The application calls UseStartup and "modules" can add IStartupFilters |
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. |
@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? |
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. |
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. |
@Tratcher, does the minimal Progam.cs you mentioned exist for As I said in original Background and Motivation section for this issue:
Do I need to open an issue identical to this one, but for minimal API's this time? |
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 |
Ah, that makes more sense now. I couldn't figure out from the examples whether the "minimal API" was being provided by If I'm understanding the ASP.NET Blog post correctly, the minimal API for If the plan is to implement an I'm guessing that generalization or duplication of |
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. |
Is there a place we can learn more about |
@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 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. |
I'm going to close this issue as "not planned". In .NET 7, we've added
Check out the docs here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-6.0 |
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? |
Because all the code happens in 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. |
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:
Obviously we don't want that pattern to go on indefinitely! |
@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. |
@ronwarner 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 :-) |
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 separateHostBuilder
boilerplate from it's DI service and HTTP pipeline configuration.According to Tratcher
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 toIWebHostBuilder
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:
Usage Examples
Program entry and hosting boilerplate (Program.cs)
Service configuration (Services.cs)
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
Risks
Other than the above concern (in Alternative Designs), I can't think of any.
The text was updated successfully, but these errors were encountered: