Skip to content
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
@davidfowl

Description

@davidfowl

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.

Activity

davidfowl

davidfowl commented on Jul 20, 2016

@davidfowl
MemberAuthor

From @dotnetjunkie

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).

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:

  • A captive dependency is registered. Simple Injector will throw an exception when an object graph contains captive dependencies.
  • A disposable transient component is registered. Simple Injector throws an exception during verification when a component that implements IDisposable is registered as transient.
  • A component with ambiguous lifestyles is registered. Simple Injector throws an exception during verification when it finds multiple registrations for the same components (e.g. using different interfaces) that are registered with different lifestyles.
  • Multiple registrations for the same component and same lifestyle, but with different interfaces. The current version of Simple Injector throws an exception during verification when there are multiple registrations for the same component (e.g. using different interfaces) for the same lifestyle, when the registrations will result in having multiple instances of that component within the same scope. In a future version, we are likely to automatically merge these registrations into one to prevent this (much like Unity already does), but this means that it should be undetermined how the system behaves when this happens. i.e. the options are: registrations could be merged, the container can resolve multiple instances per scope, or an exception can be thrown.
  • Registration of types with multiple constructors. Simple Injector throws an exception when a type with multiple constructors is registered.
  • Registration of open generic types that overlap with closed generic registrations. Simple Injector does not treat open generic types as fallback, but will instead throws an exception where registrations overlap.
  • Resolving transient components from ApplicationServices. The built-in container seems to resolve transient components as singleton, when requested from the ApplicationServices property. Simple Injector would resolve them as transient.
  • Resolving scoped components from 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:

  • Registration and resolving of collections. In its API, Simple Injector explicitly separates the registration of collections from one-to-one mappings. Letting an adapter to Simple Injector register everything as collection (with possibly one element) makes users lose out on one of Simple Injector verification capabilities. Disallowing a single ‘default’ to be resolved when there are multiple registrations (as proposed earlier in this thread) doesn’t change this fact.
  • Scopes are explicit. In Simple Injector, scopes are ambient (just like TransactionScope) and the services are always resolves from the container; never directly from a scope instance.

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

davidfowl commented on Jul 20, 2016

@davidfowl
MemberAuthor

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 present
Add 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. If Add 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, like IOptions<T>.

However is there anything in the spec that indicates whether the order should be evaluated across the entire resolve set... i.e.

  1. Resolve both composition root AND scope, then ordering
  2. Resolving and ordering from scope and composition root respectively, in isolation, then concatentating, and in what order
  3. Resolving IEnumerable<T> from IServiceScope should ignore composition root
davidfowl

davidfowl commented on Jul 20, 2016

@davidfowl
MemberAuthor

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 is IEnumerable<T>. There's some gotcha's though:

  • To support optional services in constructor injection some components use IEnumerable<T> and check for empty as a pattern. We didn't plan to break that but that will conflict with features like this:

Registration and resolving of collections. In its API, Simple Injector explicitly separates the registration of collections from one-to-one mappings. Letting an adapter to Simple Injector register everything as collection (with possibly one element) makes users lose out on one of Simple Injector verification capabilities. Disallowing a single ‘default’ to be resolved when there are multiple registrations (as proposed earlier in this thread) doesn’t change this fact.

We'd need to take a look at places where we do this and see if we can break that pattern.

@slaneyrw about unity

  1. Resolve both composition root AND scope, then ordering
    1. Resolving and ordering from scope and composition root respectively, in isolation, then concatentating, and in what order
    2. Resolving IEnumerable from IServiceScope should ignore composition root

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

mattnischan commented on Jul 20, 2016

@mattnischan

To support optional services in constructor injection some components use IEnumerable and check for empty as a pattern.

That's crazy evil. Collection resolution is pretty important, especially when you have strategy pattern type services like something resembling an IEnumerable<IFilter> or IEnumerable<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

dotnetjunkie commented on Jul 20, 2016

@dotnetjunkie

or by injecting default(T). That would make it more compatible with most other containers.

Think again. Most containers disallow injecting null references into constructors. Constructor arguments should never be optional.

mattnischan

mattnischan commented on Jul 20, 2016

@mattnischan

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 static Optional<T>.Empty.

slaneyrw

slaneyrw commented on Jul 25, 2016

@slaneyrw

Almost finished a ServiceCollection adapter for Unity and came across a few nasties.

  1. There is no way of getting a IServiceProvider from IServiceCollection, so I tried using your 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.
  2. TryAdd extension iterates through all the ServiceDescriptor in the ServiceCollection. Maybe IServiceCollection should have an IsRegistered ( ServiceDescriptor ) => bool

I think both of the aspects should be on the ServiceCollection interface so implementors can override the behaviour

davidfowl

davidfowl commented on Jul 25, 2016

@davidfowl
MemberAuthor
  1. You're supposed to create your own service provider implementation over the list of service descriptors. Look at the other adapters for an example.
  2. Why? Also changing the interface is breaking so that's not going to be happening in the short term. We can look in a future release.
slaneyrw

slaneyrw commented on Jul 25, 2016

@slaneyrw

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

davidfowl commented on Jul 28, 2016

@davidfowl
MemberAuthor

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.

I don't know why that affects any of those features.

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.

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.

So at the moment I have to return an IServiceProvider from the ConfigureServices method instead of relying on MVC to create it.

That's how it works and will continue to work.

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.

We landed on leaving it implicit for the default case.

davidfowl

davidfowl commented on Jul 28, 2016

@davidfowl
MemberAuthor

We've started on some of the proposed changes here:

#437

  • Preserving ordering is no longer a requirement - There are new APIs and a new type IOrdered<T> to get a list of ordered things described via services.AddOrdered<T> calls. Specific containers can choose to override the default implementation and do something more efficient.
  • Explicit IEnumerable<T> services - If you ask for IEnumerable<T> without saying the service is IEnumerable<T> it will fail.
  • New service descriptor (breaking change) implementation - ServiceDescriptor is the base class and there's derived ServiceDescriptor(s) for each of the service types (Instance, Type, Factory, Enumerable etc).

/cc @tillig @alexmg @nblumhardt @seesharper @jeremydmiller @khellang

tillig

tillig commented on Jul 28, 2016

@tillig
Contributor

I'm a bit torn on the explicit IEnumerable<T> services. I can see enforcing that adding multiple services of the same type to a ServiceCollection may fail unless you specify that you're adding to a member of a collection (intentionally), but the resolution of the IEnumerable<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 backing IServiceProvider doing the resolving.

38 remaining items

removed this from the 2.0.0 milestone on Mar 3, 2017
slaneyrw

slaneyrw commented on Mar 11, 2017

@slaneyrw

Is this still to be done? I cannot find any subclasses of ServiceDescriptor on the Dev branch

ErikSchierboom

ErikSchierboom commented on Mar 31, 2017

@ErikSchierboom

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

slaneyrw commented on Aug 14, 2017

@slaneyrw

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

  1. LoggerFactory now has 2 constructors of length 2 and Unity is throwing an ambigious constructor exception.
  2. ConsoleLoggerProvider's longest constructor takes a Func<string, LogLevel, bool> for the first argument, which is not injectable.
  3. DebuggerLoggerProvider has the same problem
  4. HttpContextFactory longest constructor takes an IHttpContextAccessor, which is not registered in the default service collection. AppInsights also has this pattern.
davidfowl

davidfowl commented on Aug 15, 2017

@davidfowl
MemberAuthor

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

slaneyrw commented on Aug 16, 2017

@slaneyrw

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 the CreateBuilder 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

aspnet-hello commented on Jan 1, 2018

@aspnet-hello

This issue was moved to dotnet/aspnetcore#2346

locked and limited conversation to collaborators on Jan 1, 2018
removed this from the Backlog milestone on Jan 1, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @davidfowl@ErikSchierboom@nvivo@Eilon@khellang

      Issue actions

        Changes to DependencyInjection requirements · Issue #433 · aspnet/DependencyInjection