-
Notifications
You must be signed in to change notification settings - Fork 312
Allow external container to resolve IStartup #1321
Conversation
@@ -188,7 +195,7 @@ private void EnsureStartup() | |||
return; | |||
} | |||
|
|||
_startup = _hostingServiceProvider.GetService<IStartup>(); | |||
_startup = (_applicationServices ?? _hostingServiceProvider).GetService<IStartup>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if resolved IStartup instance should be registered with _hostingServiceProvider as well. If it does, let me know. I'll add it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you resolving the IStartup from the application services? That's just broken. The hosting service provider is responsible for building the IStartup. There are 2 separate phases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I need to resolve it from Unity when it is provided as instance. Did you run the test I submitted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point in time _applicationServices
holds reference to externally supplied IServiceProvider
or null
in which case normal flow kicks in and IStartup
is resolved from _hostingServiceProvider
.
@@ -176,8 +176,15 @@ private void EnsureApplicationServices() | |||
{ | |||
if (_applicationServices == null) | |||
{ | |||
_applicationServices = (IServiceProvider)_applicationServiceCollection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Doesn't this code just not call _startup.ConfigureServices(...) if an IServiceProvider is specified?
- Any code that looks at service registrations is usually assuming too much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It just skips calling provider factory if instance is provided. The rest is the same as usual.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this code grab the IServiceProvider out of the IServiceCollection
(which itself is an anti-pattern) and then skips calling _startup.ConfigureServices if it's an instance?
Generally I don't like the approach and I think #550 cannot be fixed without changing the overall design of the WebHostBuilder. It has to be the owner of the DI container as things are designed today. This PR makes an attempt to make that not the case but it actually doesn't work well as it basically skips calling into ConfigureServices in the startup class. |
If you run the test I've provided you will see that with exception of ServiceProviderFactory everything else is called properly. Calling Factory methods in this scenario is pointless when service provider already instantiated. |
Just checked it again and every single configure method is being called. |
I don’t see how any of this can work. If you pass a concrete service provider to the WebHostBuilder, what’s the contract? What service provider is used for each of the scenarios? Part of the confusion is that you claim this fixes #500, but it really doesn’t. |
It fixes #550 (Allow acceptance of pre-created IServiceProvider instance). I'll refactor the test so it is more obvious. |
_applicationServices = (IServiceProvider)_applicationServiceCollection | ||
.LastOrDefault(d => d.ServiceType == typeof(IServiceProvider) && | ||
d.ImplementationInstance != null)? | ||
.ImplementationInstance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can not do this: _hostingServiceProvider.GetService<IServiceProvider>();
because it will return _hostingServiceProvider. We need to look it up in the list.
Perhaps you know better way of doing the lookup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the entire feature is undesirable really and this is a hack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that there are 3 service providers and the service provider you get when you build is different to the one added to the IServiceCollection. It's using the IServiceCollection like a property bag to shuttle the information to the WebHost
I fundamentally have an aversion to this change because IMO it doesn't make much sense to add an IServiceProvider to the IServiceCollection. There are 2 service providers:
There's no physical way to replace the application service provider because hosting fundamentally needs to call into user code to then build the "final" service provider. Passing in an already constructed one will never work. This is what #550 is asking for. Replacing the hosting service provider is achievable and it's basically what you have to do to achieve the "use a third party container to resolve IStartup". I believe the cleanest way to achieve this is to use the IServiceProviderFactory outside of any dependency injection (so removing it from the hosting container) and using it on both the hosting services as well as the application services. Today it's only used on the application services. This PR attempts to shuttle an IServiceProvider implementation through the hosting services (which is a clever "hack") and then resolve the "last" IServiceProvider instance to resolve the IStartup. This feels extremely fragile and can break if the default dependency injection implementation changes to disallow overriding the internal IServiceProvider implementation. On top of that, to make things work well, you really need to preserve the existing hosting services when resolving the IStartup or things may fall apart. It means something like this would just fail: public static IWebHost BuildWebHost(string[] args)
{
var hostingServices = new ServiceCollection();
return WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.AddSingleton<IServiceProvider>(hostingServices.BuildServiceProvider());
})
.Build();
} If you want to go this route, then we need to define a separate interface that hosting can use to replace the hosting services independently from application services. The WebHostBuilder does something like this: public interface IHostingServiceProviderFactory
{
IServiceProvider BuildServiceProvider(IServiceCollection services);
} Internally, hosting would resolve this from the container (we might have to build another container) and call BuildServiceProvider passing in the initial collection it creates internally to get the final hosting IServiceProvider. |
Your reservations are based on the notion that DI container has to be built before it could be used. Unity and some other containers do not require Build step. Registrations could be adjusted on the fly. Added, removed, replaced.
The behavior of the DI container this solution is based on is Required by the Microsoft.Extensions.DependencyInjection specification and will not change unless specification itself changes. At which point this whole implementation will stop working anyway. In normal flow events happen in this sequence:
In proposed PR flow is as follows:
The only difference is where IStartup is resolved from.During resolution of IStartup only types required are these used in constructor of implementing type. Nothing from Hosting namespace is required at that moment. If you run the test and check it in debugger you will see that all your concerns are addressed. All required services are registered and resolve properly including server, logging, etc.. |
As long as services are in service collection they will be registered with the container and available during resolution. |
Only if you use the service collection provided by ConfigureServices which is required. |
Yes, but this solution does not require collection replacement. It uses provided by the framework collection. It other words: It always uses collection provided by ConfigureServices |
@ENikS you say "it" but thats not expressed in any contract. The example I show is completely broken. So you can't just provide any service provider and you have to understand this hidden contract to make any of this work. |
What are you talking about, what contract? What am I missing? |
I sent you another PR which solves both of these problems. |
I am withdrawing this PR because #1322 provides more complete solution |
Fixed #1309