Closed glen-84 closed 6 years ago
Use case: You add a "module" (separate assembly) to your web application which contains controllers, views, etc.
Sounds like something that can be fully implemented in a stand alone way and doesn't have to be built in.
We're not trying to be a feature rich DI container so feature like these and other named providers won't be implemented. If you want features like child containers, use a DI container that supports them.
If each module (or assembly or logical set of controllers/views) had its own child container, the instances would be isolated, and each module could have its own set of implementations, which would first be resolved from the child container, and then fall back to the parent container when not found.
There is a single container within the application. If you want support for something like this, it can be built on top of what we provide (make your own service collection and pass that built service provider around).
Orchard does something like this already with Autofac. I'd look there for inspiration.
Why isn't the ActionContext in MVC done this way?
IOW, can we use this approach you suggest to do injection of a specific per-request instance of a type?
I guess, what's really being requested is something like IHttpContextAccessor.
File a bug on MVC then? Currently MVC registers IScopedInstance<T>
to achieve similar things to IHttpContextAccessor
(though they are pretty different).
The problem with implementing it in a stand alone way is for package authors, rather than end-product-developers. As @atrauzzi mentioned in the Announcements thread (wrong spot for us to jump on that before, @davidfowl - sorry for that), it would be atrocious for a package author to require a specific DI framework; we're hoping for hooks that can be tied into by all DI frameworks.
mdekrey/DependencyInjection@04f92ae19311478254f97808297f1006ba747e2d adds Child Container capabilities to the default DI container in a non-breaking way and starts the progress for Ninject and Autofac, though I am not familiar enough with them to complete the job. I'd be happy to learn it and complete the work if that would mean a pull request would be accepted, though.
The problem with implementing it in a stand alone way is for package authors, rather than end-product-developers. As @atrauzzi mentioned in the Announcements thread (wrong spot for us to jump on that before, @davidfowl - sorry for that), it would be atrocious for a package author to require a specific DI framework; we're hoping for hooks that can be tied into by all DI frameworks.
That's a grand claim. This is a brand new DI abstraction and implementation that is only used in ASP.NET as it stands.
If you're thinking about adding this generic capability to the DI abstraction then I'd suggesting doing some more research to see if the most popular DI containers support this in a first class way. ASP.NET 5 doesn't have a way to configure the child container, so even after adding this support to the DI system, ASP.NET 5 won't take advantage of it (btw I'm not a fan of the default parameter, I'd rather it be added in a first class way if we chose to do it). I'm also not a fan of this being on by default. I think the implementation we have today is better and child containers should be something additional but not turned on by default.
I'm not sure what the grand claim is - I'm fairly certain that if it's part of the framework that Microsoft puts out, it'll become a standard that everyone implements. (ProviderBase
sure was.) And I'm REALLY excited that DI is something that is used by the framework by default.
I agree regarding the default parameter - it shouldn't be there; it was to prevent breakage from anyone else who is currently using it, such as MVC 6; even in WIP projects I have the preference to not introduce breaking changes.
Both Autofac and Unity allow adding registrations to child lifetime scopes; Ninject does this through an officially maintained extension. The MVC extensions for both Autofac and Unity have object registrations under the request's scope, and Autofac even registers the HttpContextBase for you.
Unfortunately, I don't think we can "turn on" child containers without at least some additional support for other hooks in the framework. I'd rather see high-level feature requests (such as this one) than low-level ones, myself, but that's just one guy's opinion. (I believe low-level features would be harder to implement in every DI container; this one would need custom scopes and registrations after initial configuration. Ninject does not support the latter as far as I know without making a child scope.)
Again, I want to say, I'm really, really excited that DI is in the Microsoft.Framework
. I'm just hoping that it chooses to provide a more featureful experience.
it would be atrocious for a package author to require a specific DI framework; we're hoping for hooks that can be tied into by all DI frameworks.
That claim. The only real integration point for this that would cause package authors to depend on a specific DI framework would be if they wanted to expose the configuration of the nested container. If a library author just depended on DI features, then configuration the child container is about the "system" creating the child container itself. In the case of an http request, that would be the request itself.
Unfortunately, I don't think we can "turn on" child containers without at least some additional support for other hooks in the framework.
All of the hooks exist already. But we wouldn't design anything around this today. It's a bigger question if we decide that we need to design features around child containers (like exposing ConfigureRequestContainer etc) to which I think the answer is no.
I believe low-level features would be harder to implement in every DI container; this one would need custom scopes and registrations after initial configuration. Ninject does not support the latter as far as I know without making a child scope.
I think the next step in making child containers real would be to flesh out what other DI containers support to see where it would fall over. As an example, we used to have a fallback mechanism that allowed containers to chain GetService calls. That ended up being a disaster to implement in order DI systems and the feature was dropped as a result.
So for child containers now, I suspect it can be implemented using IHttpContextAccessor as the model.
You'd need to design IFooAccessor whose implementation depended on the IHttpContextAccessor to use the HttpContext's Items as the place to read/write the Foo. Somewhere in your pipeline you know to set the right entry in the HttpContext's Items collection, and the anything that needs Foo injects the IFooAccessor.
Sound about right? Or maybe there's a more obvious or easier way I'm missing?
Implementing a child container in the existing framework is not difficult. As a quick proof of concept:
using System;
namespace Microsoft.Framework.DependencyInjection
{
public class ChildServiceProvider : IServiceProvider
{
private readonly IServiceProvider _child;
private readonly IServiceProvider _parent;
public ChildServiceProvider(IServiceProvider parent, IServiceProvider child)
{
_parent = parent;
_child = child;
}
public ChildServiceProvider(IServiceProvider parent, IServiceCollection services)
{
_parent = parent;
_child = services.BuildServiceProvider();
}
public object GetService(Type serviceType)
{
return _child.GetService(serviceType) ?? _parent.GetService(serviceType);
}
}
}
To create the child service provider:
namespace ChildContainer
{
public interface IMyType1 { }
public interface IMyType2 { }
public class MyType1 : IMyType1 { }
public class MyType2 : IMyType2 { }
public class Sample
{
IServiceProvider CreateChildServiceProvider(IServiceProvider parent)
{
var services = new ServiceCollection();
services.AddTransient<IMyType1, MyType1>()
.AddSingleton<IMyType2, MyType2>();
return new ChildServiceProvider(parent, services);
}
}
}
How to use and set the IServiceProvider
in use to the child container is a different issue,
It's unfortunately not that easy, @matthewDDennis, due to the way IServiceProvider
handles IEnumerables
and other Open service resolutions. Thanks for trying to help, though!
Why wouldn't it work?
IServiceProvider
doesn't handle 'IEnumerables
etc. That is done in the ServiceProvider
implementation.
Since my ChildContainer
, using the ServiceCollection.BuildServiceProvider
, is just creating a ServiceProvider
and using it, which already handles everything you are concerned about.
Since it is an IServiceProvider
, all the extension methods would work as well.
I just realized that ServiceProvider
implements IDisposable, so I've added support for disposable IServiceProvider
implementations.
using System;
namespace Microsoft.Framework.DependencyInjection
{
public class ChildServiceProvider : IServiceProvider, IDisposable
{
private readonly IServiceProvider _child;
private readonly IServiceProvider _parent;
public ChildServiceProvider(IServiceProvider parent, IServiceProvider child)
{
_parent = parent;
_child = child;
}
public ChildServiceProvider(IServiceProvider parent, IServiceCollection services)
{
_parent = parent;
_child = services.BuildServiceProvider();
}
public void Dispose()
{
(_child as IDisposable)?.Dispose();
}
public object GetService(Type serviceType)
{
return _child.GetService(serviceType) ?? _parent.GetService(serviceType);
}
}
}
I've been attempting to use this code. You cannot use ChildServiceProvider along side IServiceScopeFactory. I've extended the ChildServiceProvider implementation to have a ChildServiceScopeFactory, etc which works but I have a feeling it's going to fall down at some point or require more reimplementation.
I'd rather have the CreateScope method take an overload of an IServiceCollection that are descriptors just for the child context. The reasoning is that I have an object that applies only the the child scope and doing AddInstance to the parent's service collection at creation would obviously share across all scopes.
I looked at just doing this with a simple PR but the problem is that when I started digging I'd have to change more of ServiceProvider to take not only a parent ServiceProvider but a ServiceCollection and merge the descriptors. Didn't want to think that through at the moment :)
Is this way off base?
@adamhathcock :+1: - the quick implementation above really falls apart upon close inspection, especially with the special handling of IEnumerable<T>
. (It pulls from either the child or the parent, not combining the two as expected by child containers.) I had gotten pretty close, but could not get it to behave in a way that was SOLID, nor performant, since the resolution table was completely internal. I ultimately gave up and created more objects like the IHttpContextAccessor
, which was not elegant, but did work for my problem.
Multitenancy is a case where child containers is needed. SaasKit project searched for a way to implement a per-tenant container, but ended up using StructureMap. @benfoster wrote an interesting blogpost on this.
Orchard project achieved this cloning the parent service collection and injecting instances of Singleton services (code here). It seems to work but it resolves all Singleton services (even if not needed), and there is a known issue with Singleton generics.
On DI Notes wiki page, there is a reference to "chaining containers". Are there plans to implement this ?
Nope.
The proof of concept given here has some issues.
IEnumerable
Same when DI instanciate a new type, it dont call GetService for each of its dependencies, as it tries to be smart about how it activates its constructor params - and it again might miss some registrations from the parent registrations.
This issue was moved to aspnet/Home#2355
Use case: You add a "module" (separate assembly) to your web application which contains controllers, views, etc. The MVC parts make use of an
IStringLocalizer
implementation that loads strings from a JSON file (for example). When you "register" the module, it adds its dependencies to the DI container of the host application, including the string localizer implementation. The host application then sets its own implementation forIStringLocalizer
, which might load strings from a database, overwriting the previous implementation. Now the module's controllers and views will be using the wrong implementation of the string localizer, and the module's message strings will not be loaded.If each module (or assembly or logical set of controllers/views) had its own child container, the instances would be isolated, and each module could have its own set of implementations, which would first be resolved from the child container, and then fall back to the parent container when not found.
This is just one example, and I'm sure there are many more.
/cc @brockallen @mdekrey (from https://github.com/aspnet/Announcements/issues/28)