-
Notifications
You must be signed in to change notification settings - Fork 9.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
[Announcement] Generic Host restricts Startup constructor injection #9337
Comments
Your mitigation does not work for me: In my app the entire asp-net-core block itself is just a little part of the entire app. It inherits outer services by the use of nested Autofac lifetime scopes and a bunch of delegates are being collected in earlier stages which configure aspnetcore services and routes -- all of that getting passed to the Startup class. |
Constructor injected services were only needed for use in Startup.ConfigureServices. Can you give specific examples of what you're doing in ConfigureServices that consumes services? It's often possible to set up delayed actions that only resolve services when your service is resolved. |
While this helps in many cases, here it does not, because aspnetcore is just one of many (optional) services in the app:
Having this written now, I wonder if |
Indeed, Startup is not a required construct, it's a holdover from the OWIN days when there might not have been a Program.Main. It can be still useful for organizing code. Are you able to do everything you need to with I(Web)HostBuilder? |
Since the AspNetCoreInitializer class I was speaking of already contains the code which calls |
Hmm. In my public IServiceProvider ConfigureServices(IServiceCollection services) which returns the DI container to use. How would I do this with IWebHostBuilder? |
That does not work either. It doesn't compose with other methods on the You can now plug in a DI container using the public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseServiceProviderFactory(new AutofacServiceProviderFactory()); |
And what about WebHostBuilderContext why isn't that injectable as its constituent parts are? |
@NinoFloris why would you want the WebHostBuilderContext? As you say, all of its fields are available individually. |
No reason in particular, separate is fine. I don't know, it may have felt a bit inconsistent as WebHostBuilder ConfigureServices does pass it. |
Using the generic host builder, how do you log? Within the ConfigureServices method how do you write a log based on the configured logging? How can you reference ILogger or ILoggerFactory? Logging is important and should be easily accessible after the ConfigureLogging method is called. |
@grecosoft logging is built on dependency injection, and ConfigureLogging is just a wrapper for ConfigureServices. It's not possible to log until after all of the services have been registered and the container has been built. |
Then how would you write logs. For example, if you wanted to write a log as to which service type was register? I understand you can access after the container is created. But how do you write logs as the application is being built? |
@grecosoft it's not supported in this model, or at least not until after ConfigureSerivices. Startup.Configure is one of the earliest phases where DI services like logging are available. |
Ok. Seems to be limiting. Just seems that logging is so important that it should be created up front and allowed to be used anywhere. Is it possible to create a logger to be used during configuration? |
No, the logger depends on configuration too. What your asking for requires a logger that is configured directly in code and doesn't use DI, Config, Options, etc.. That kind of logger is possible, but of very limited usefulness. |
Tratcher - I understand. Not an issue just need to make a small refactor to our code. Thanks for the answering my questions and for such a fast response. |
This is a real disappointment. By using the WebHost container, I was able to register a number of classes as "Binding Services" or "Startup Services" via attributes. Then I could use public class Startup {
private ILogger<Startup> _logger { get; set; }
private IEnumerable<IStartupBinder> _binders { get; }
public Startup(IConfiguration config, ILogger<Startup> logger, IEnumerable<IStartupBinder> binders) {
_logger = logger;
_binders = binders;
}
public void ConfigureServices(IServiceCollection services) {
foreach (var startupBinder in _binders) {
_logger.LogInformation($"Starting binder: {startupBinder.GetType().Name}");
startupBinder.Bind(services);
}
}
public void Configure(IApplicationBuilder app, IEnumerable<IStartupService> startupServices) {
foreach (var startupService in startupServices) {
_logger.LogInformation($"Starting service: {startupService.GetType().Name}");
startupService.Startup(app);
}
}
} Which our team found to be a very clean design. We had a chuckle at the response that logging is simply "not supported in this model". Please reconsider whether this is the ideal way forward. We certainly were not confused by the WebHost having a separate container. Maybe documentation was the answer instead of this backwards-incompatible change... |
Just saying don't log until Configure feels wrong. I was previously injecting IOptions and ILoggers in to ConfigureServices that were known to be already fully configured during the WebHost build process. This worked well. FWIW the docs for 3.0 Startup still say you can inject a LoggerFactory. |
@wpbrown if you use Serilog, you can initialize it before building the host, and use it directly until it's wired into the Microsoft.Extensions.Logging subsystem during start-up. This does mean using configuration directly from the files or environment, of course, since the configuration subsystem won't have been initialized, yet; e.g. |
Workaround for a few Startup logs. This should not be used to create a logger that lives longer than the Startup Configuration. USE AT OWN RISK!!! using (var scope = services.BuildServiceProvider().CreateScope())
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Startup>>();
logger.LogInformation("Application is starting up");
} Important notes:
You can also create a method to make its usage cleaner: private static void WriteLog(IServiceCollection services, Action<ILogger> action)
{
using (var scope = services.BuildServiceProvider().CreateScope())
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Startup>>();
action(logger);
}
}
public void ConfigureServices(IServiceCollection services)
{
...
WriteLog(services, logger => logger.LogInformation("Application starting up"));
...
} |
I'm not suggesting building the container twice either, I am suggesting passing the additional services to enable DI via an overload of
|
That might be reasonable. To pass a separate collection or bag of values to Startup. |
Yes please. These instances would then be available as singleton-scoped during entire lifetime? |
I'm not convinced that this overload should automatically register these instances passed in this way for DI as singletons. You may not want them registered with DI at all, or you may want to use a different scope like transient etc. This method would just use them when activating the startup type in addition to the inbuilt instances (IHostEnvironment etc) that are already supported. If you wanted these objects to also be registered as singletons with DI you can do that fairly easily by registering them yourself, in addition to passing them in this manner. I'e using the existing mechanisms for configuring services (ConfigureServices method in Startup class, or within |
Perhaps a simple solution here might be to allow the Startup class to be instantiated via a Lambda, so you can pass whatever you want to the constructor of your own Startup class? Something like: // Get whatever logger you want
var myLogger = new SomeLogger();
// Create the host
var host = Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webHostBuilder =>
{
// Configure the startup, but with a lambda
webHostBuilder
.UseStartup((hostingEnvironment, configuration) => new Startup(hostingEnvironment, myLogger))
.UseContentRoot(Directory.GetCurrentDirectory());
}) Arguments to the lambda could be the things available to .NET at that time, i.e. hosting environment and configuration, then you add your own things. |
@alistairjevans at that point why not just use the IWebHostBuilder Configure and ConfigureServices APIs directly and capture what you need? Startup doesn't offer much beyond those. |
Some further background info about how I'd plan to use this featureDuring web app startup I commonly find I need to repeat certain tasks. For example:
My plan to stop from repeating myself in all apps, is to ship a nuget package that has an extension method for This extension method would do all of the above opinionated plumbing, and then internally call Now I can do all of this so far, already. The thing I cant currently do, is, as a consumer of this nuget package, when calling
Then in
This also explains why the Lambda idea doesn't help me in this case - The Extension method doesn't know how to construct the Startup class passed by the caller - and the caller is free to change their Startup class constructor. Therefore I need to activate the class - I could build my own activation logic of course, but I'd prefer if this useful feature was inbuilt for me to leverage, that way if asp.net core chooses to change the services that can be injected into startup classes by default, I wont have any logic to mimic. |
@dazinator If I'm following you, it seems you could simply skip the use of As for the logging issue, I'm starting to hear from people who are realizing there is a problem. We have a lot of headless jobs that rely solely on logging for progress and failure reporting. I've seen a few cobbled-together work-arounds. People need this so they will try to find an answer, which may be worse than the problem that was meant to be solved. Building early and breaking DI is not a solution. I'm trying to devise an in-house standard along the lines of caching messages until the builder runs. Exceptions actually run a new logger-only builder (which hopefully doesn't fail itself) merely to dump the cache and the exception ... but it feels like a hack, and of course, this lightweight pseudo-logger lacks the many important features of a real modern logger. +1 for the observations that logging is a special-case / cross-cutting and if it needs special handling, so be it. This is non-trivial. |
I spent the day cleaning up my implementation of reliable basic logging during Host Builder configuration. I still need to get it packaged for public use, but maybe somebody will find it useful. |
How do you pass command line args to Startup? |
@MichaelPuckett2 inject IConfiguration, that will contain your command line args if you provided them to the host builder. |
I also run into issues with this. We are setting a custom
|
@razzemans does this help?
https://github.com/dotnet/extensions/blob/cc317e6112479d36ba77a57e1eeea5d88a164a6c/src/Options/Options/src/OptionsServiceCollectionExtensions.cs#L213 https://github.com/dotnet/extensions/blob/cc317e6112479d36ba77a57e1eeea5d88a164a6c/src/Options/Options/src/OptionsBuilder.cs#L73-L74 We're missing some overloads of AddCookie or Configure that would make this scenario easier.
Let me know which of these look useful and I'll file an issue. [Edit] Filed #18772 |
Thanks a lot @Tratcher - that indeed works. I think I would prefer your first proposal - it is also consistent with the factory methods overloads of the |
I'm with the other people in this thread calling for a special-case exemption for logging: it's absolutely necessary. The fact that this apparently wasn't considered at all during Core 3.0 dev is absolutely bizarre, in my opinion. I understand the rationale behind not doing it, and the hoops that @MV10 jumped through to make a usable workaround that doesn't break everything shows how difficult it would be to do without the "fake" Please strongly consider adding this capability back as soon as possible. Fobbing people off with half-a**ed workarounds, or requiring them to install third-party NuGet packages to get this basic functionality back, is not an acceptable solution in any way shape or form. Usability needs to trump ideology here. |
To echo @IanKemp, and add some info, there is nothing stopping us right now from initialising logging early (creating an ILoggerProvider instance, be that console, serilog etc) immediately in program main, and using that to create an All good so far. A few issues remain for the framework to address though in my view:
To address that part we'd just need the ability to supply additional constructor parameter values for activating startup classes, so we could do:
|
Sounds like you're suggesting the templates change to do this.
This requires some more thought but it might work if we do something hacky like try to detect an existing ILoggerProvider. There are still 2 DI containers in this universe so for example, you'd need to make sure to configure options for logging in the right place as the streams don't cross.
This sounds reasonable and it's something we do today for middleware (see UseMiddleware). @danzinator Can you file 2 separate issues for these items? No promises of course but having 2 concrete proposals would help the discussion. |
@davidfowl ok, have created #19807 and #19809 |
Thank you for contacting us. Due to a lack of activity on this discussion issue we're closing it in an effort to keep our backlog clean. If you believe there is a concern related to the ASP.NET Core framework, which hasn't been addressed yet, please file a new issue. This issue will be locked after 30 more days of inactivity. If you still wish to discuss this subject after then, please create a new issue! |
TLDR: The only types the generic Host supports for Startup constructor injection are
IHostEnvironment
,IWebHostEnvironment
, andIConfiguration
. Applications using WebHost are unaffected.In 3.0 we've re-platformed the web stack onto the generic host library. You can see the change in Program.cs in the templates:
2.x:
https://github.com/aspnet/AspNetCore/blob/5cb615fcbe8559e49042e93394008077e30454c0/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.cs#L20-L22
3.0:
https://github.com/aspnet/AspNetCore/blob/b1ca2c1155da3920f0df5108b9fedbe82efaa11c/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/Program.cs#L19-L24
One key behavior change here is that Host only uses one dependency injection container to build the application, as opposed to WebHost that used one for the host and one for the app. As a result the Startup constructor no longer supports custom service injection, only
IHostEnvironment
,IWebHostEnvironment
, andIConfiguration
can be injected. This change was made to avoid DI issues such as duplicate singleton services getting created.Mitigations:
Inject services into Startup.Configure:
public void Configure(IApplicationBuilder app, IOptions<MyOptions> options)
[We'll add more based on requests for specific scenarios.]
The text was updated successfully, but these errors were encountered: