This repository was archived by the owner on Nov 2, 2018. It is now read-only.
This repository was archived by the owner on Nov 2, 2018. It is now read-only.
Changes to DependencyInjection requirements #433
Closed
Description
Moving the discussion from #416
The tricky part with supporting DI is that not all of the things can be codified into the interface definition. That would make it easier for sure but some of it is just impossible.
Right now we're researching a couple of things:
- We're looking to see which components depend on specific features of the DI container (via some automation) and as part of it, we want to see if we can automate any of it.
- We're looking at @tillig's suggestions (like writing a chaos container, one of our devs already wrote a second one)
- We're trying to see if we build ordering on top via another service type (playing around with
IOrdered<T>
) - We're looking at the implementations of other adapters to see what things are impossible to implement (We could use everyone's help/feedback on this one).
- As a result of the previous exercise, we're also looking to see what other requirements we can relax (last registration wins, closed generics falling back to open generics etc)
As for the a specific list of things we think can be candidates for potential removal:
- Ordering - We're trying to build a service on top with some that can potentially handle this.
- Last registration wins - We'll likely change the API around service collection to make sure that if multiple registrations make it into the container, resolving it as a single service fails.
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
davidfowl commentedon Jul 20, 2016
From @dotnetjunkie
There is a lot of implicit behavior in the adapter that will have to be explicitly unspecified. In other words, the contract should explicitly state that in certain conditions an adapter is free to behave as it chooses. For instance, an adapter is allowed to throw an exception.
When considering the current version of Simple Injector, the behavior as specified by the abstraction should be undetermined in the following scenarios:
IDisposable
is registered as transient.ApplicationServices
. The built-in container seems to resolve transient components as singleton, when requested from theApplicationServices
property. Simple Injector would resolve them as transient.ApplicationServices
. Simple Injector does not allow resolving scoped instances outside the context of an active scope. It throws an exception instead.Besides the above list of behavior that has to become explicitly unspecified, there are other behaviors of the abstraction that I’m currently unsure of how it should be specified for the current version of Simple Injector to conform. These included:
This is what I’ve been able to come up with at this point and that holds for the current version of Simple Injector. As already noted above, there is already a plan to change some behavior of Simple Injector in the next major version. I will likely have missed some things, so I will update this comment as I discover more items.
davidfowl commentedon Jul 20, 2016
From @slaneyrw
@dotnetjunkie Looking at the perspective of a Unity user I echo most of your points, and explicitly call out the Cardinality problem.
I have been a user of the Unity container for many years and i have a fork of the GitHub that I'm using to convert it to netstandard1.6. When i was trying to build an Adapter for IServiceCollection/IServiceProvider I immediately came across the issue of how to distinguish between a replacement registration and a multiple registration.
TryAdd
is easy as you check for an existing registration and add if not presentAdd
becomes complicated... Do I replace the existing registration or add another one. Without understanding of the intent of a registration at the time of registration the container will have to hedge both bets.It become vastly more complicated with IServiceScope. It doesn't make sense to me have registrations for any type that is designed for a single instance when resolved to have both a Scope lifetime and singleton/transient lifetime. In Unity a Scoped lifetime would be implemented using the HierarchicalLifetimeManager. If a type was resolved from the composition root ( i.e. IServiceProvider ) or from the scope (IServiceScope ? ) then the same build plan would be used but the instances will be stored in their respective owner container. I would not be possible to store two separate registrations for two different implementation without creating a new registration in the scope container when it is created ( or black magic to "migrate" a parent registration marked as "Scope" ). Once again, having this distinction for a single instance resolve does not make sense to me.
Without any cadinality metadata at registration I will be forced to maintain two sets of registrations. If
Add
is used then I will have to add a default registration AND a named registration. IfAdd
is used multiple times for the same interface/concrete then last in wins for the default registration.Having to maintain a specific order for resolved
IEnumerable<T>
should be easy to accomplish by adding a post resolve build policy for those interfaces what have Order metadata, likeIOptions<T>
.However is there anything in the spec that indicates whether the order should be evaluated across the entire resolve set... i.e.
IEnumerable<T>
from IServiceScope should ignore composition rootdavidfowl commentedon Jul 20, 2016
The problem with having cardinality defined up front for everything is that you lose the ability to distribute where registrations come from. There might be some default implementations in the box and the user might provide some. I'm not sure how that would work if you were forced to declare everything up front in a single call. I'm not even sure why that matters to be honest.
We're currently going to change things so that an
IServiceCollection
with multiple entries of the same service type indicate the service isIEnumerable<T>
. There's some gotcha's though:IEnumerable<T>
and check for empty as a pattern. We didn't plan to break that but that will conflict with features like this:We'd need to take a look at places where we do this and see if we can break that pattern.
@slaneyrw about unity
Always preserve registration order (currently) using what. As I said before, we're looking at relaxing that requirement. You need to look at both the "composition root" and the "IServiceScope" depending on the lifetimes of the resolved services.
mattnischan commentedon Jul 20, 2016
That's crazy evil. Collection resolution is pretty important, especially when you have strategy pattern type services like something resembling an
IEnumerable<IFilter>
orIEnumerable<IRule>
. You might want all of them to throw your data at.Optional services should still be able to be injected by either expanding the expression generator to mock up a default class for that service interface or by injecting
default(T)
. That would make it more compatible with most other containers.dotnetjunkie commentedon Jul 20, 2016
Think again. Most containers disallow injecting
null
references into constructors. Constructor arguments should never be optional.mattnischan commentedon Jul 20, 2016
Not a huge fan of optional constructor injection either, honestly. Just looking for the most easily cross container compatible answer without resorting to something like
IOptional<T>
and a staticOptional<T>.Empty
.slaneyrw commentedon Jul 25, 2016
Almost finished a ServiceCollection adapter for Unity and came across a few nasties.
BuildServiceProvider()
extension. However your implementation uses hardcodes a ServiceProvider instance passing all the ServiceDescriptors, instead of resolving it or asking ServiceCollection. Rounding tripped registrations via ServiceDescriptor means I'll lose information.IsRegistered ( ServiceDescriptor ) => bool
I think both of the aspects should be on the ServiceCollection interface so implementors can override the behaviour
davidfowl commentedon Jul 25, 2016
slaneyrw commentedon Jul 25, 2016
Yes, I am creating my own service provider, but NOT over the original ServiceDescriptors. The ServiceDescriptor is too simplistic for all but the basic mapping type to implementation. What about property injection, method invocation, interface and virtual method interception. These are all tools in the arsenal that are not supported using ServiceDescriptor.
Asking the ServiceCollection for all the descriptors and giving it to a ServiceProvider feels wrong to me. It believe should be the responsibility of the ServiceCollection to initialise the ServiceProvider.
So at the moment I have to return an IServiceProvider from the ConfigureServices method instead of relying on MVC to create it. I hope you don't remove that capability! Maybe the default MVC template should be changed to make it more explicit and return IServiceProvider from ConfigureServices.
davidfowl commentedon Jul 28, 2016
I don't know why that affects any of those features.
The service collection is just metadata for your actual container implementation. The service collection isn't your container registration API. So its the container implementors job to take the "metadata" and turn it into container specific API calls. The container implementor also needs to return an
IServiceProvider
that calls through the container implementation.That's how it works and will continue to work.
We landed on leaving it implicit for the default case.
davidfowl commentedon Jul 28, 2016
We've started on some of the proposed changes here:
#437
IOrdered<T>
to get a list of ordered things described viaservices.AddOrdered<T>
calls. Specific containers can choose to override the default implementation and do something more efficient.IEnumerable<T>
services - If you ask forIEnumerable<T>
without saying the service isIEnumerable<T>
it will fail.ServiceDescriptor
is the base class and there's derivedServiceDescriptor
(s) for each of the service types (Instance, Type, Factory, Enumerable etc)./cc @tillig @alexmg @nblumhardt @seesharper @jeremydmiller @khellang
tillig commentedon Jul 28, 2016
I'm a bit torn on the explicit
IEnumerable<T>
services. I can see enforcing that adding multiple services of the same type to aServiceCollection
may fail unless you specify that you're adding to a member of a collection (intentionally), but the resolution of theIEnumerable<T>
may need to be unspecified.For example, Autofac intentionally returns an empty
IEnumerable<T>
if you don't have any members registered so things like message processors that want a list of handlers won't just fail out of hand - you won't have to check for null-or-empty, just empty. And if you only register one thing, great, you get an enumerable of one.We couldn't even really add anything to the service provider to track which explicit
IEnumerable<T>
contributors have been registered because we'd need to push the tracking down into Autofac (so things registered by assembly scanning or whatever are also tracked)... and then it'd break out of the box behavior.Long story too long - I think it's cool to enforce the different registration mechanism as part of
ServiceCollection
but not so much for the actual backingIServiceProvider
doing the resolving.38 remaining items
slaneyrw commentedon Mar 11, 2017
Is this still to be done? I cannot find any subclasses of ServiceDescriptor on the Dev branch
ErikSchierboom commentedon Mar 31, 2017
This looks great. I would love to have improved integration when using SimpleInjector, preferrably such that SimpleInjector would be able to inject dependencies as action parameters (which is a brilliant feature).
By the way, SimpleInjector 4 has just been released. Don't know if that helps.
slaneyrw commentedon Aug 14, 2017
Just looking at the API for v2, looks like none of the service descriptor differentiation still hasn't surfaced
Also new issues with bad constructor discovery
Func<string, LogLevel, bool>
for the first argument, which is not injectable.davidfowl commentedon Aug 15, 2017
Realistically, we can't change anything here. The best bet for making things like unity, castle and other containers work would be to go to @dotnetjunkie approach outlined here aspnet/Mvc#5403.
TL;DR if the container implementation can't conform then there's another approach that includes adapting various composition roots. I think this is the best path forward to making containers with completely incompatible semantics work. It does mean that both containers need to be bridged but that's basically how things used to work in the older days (pre-ASP.NET Core).
slaneyrw commentedon Aug 16, 2017
Just had a look at @dotnetjunkie 's repo and I think there are major problems trying to run both side-by-side. I prefer that the container OWNS the composition root, and that is how I've got unity to work
Using the
IServiceProviderFactory
approach make the most architectural sense to me. I ended up creating a subclass of ServiceDescriptor where I can add all the Unity specific registration metadata and handle the registration in theCreateBuilder
phase. I've created extensions off IServiceCollection to build the new descriptor, now I can support all the more complicated DI aspects, like Property and Method injection, parallel dependency graphs, as well as interception mechanisms ( transparent caching )I've solved the issues with the constructor selection by switching the constructor selector policy to match the default DI's behaviour, although I can't work out how it makes a distinction between LoggerFactory's 2 constructors - I think there is some undocumented behaviour in there.
Cardinality hints ( or lack there of ) is still an issue, I was really hoping the v2 API was going to support this, but I can work around it.
aspnet-hello commentedon Jan 1, 2018
This issue was moved to dotnet/aspnetcore#2346