dazinator/Dazinator.Extensions.DependencyInjection

Child Service Provider

Closed this issue · 4 comments

Child containers are not directly supported in Microsoft.Extensions.DependencyInjection
But through the magic of code we should be able to make it happen here

Related #7

Test coverage needed:

Scenario:

Register services at different levels:
- Just in parent
- Just in child
- In Parent and in Child

Services registered just in parent

  • can be resolved through parent or child service provider.
  • are created in the correct container:
    • transient or scoped services, should be created by the container they are resolved through.
      • and therefore, tracked / disposed by that container if they are IDisposable
      • can have dependencies satisfied by that container (e.g can register a dependency in the child container which overrides the dependency (or lack of) in the parent container when resolving the service through child container)
    • singleton services should be created in parent container. We don't want child container to create seperate instances (the user would have to register the service as a singleton in the child container to configure that explicitly)

Services registered just in child

  • Can not be resolved from parent
  • Can have dependencies resolved from parent registrations
    • If the dependency is scoped or transient, it will be owned by the container its resolved in (so child in this case)
    • if the dependency is singleton, it will remain owned by the parent container (or user if its a registered instance)

Services registered in parent and in child

  • parent resolution should get service registered with parent.
  • child resolution should get service registered with child and not the service registered with parent (it overrides parent registration)
    • This means you always get two different instances, unless the user has registered a singleton instance, or is registering factory functions that capture shared instances.
  • IEnumerable` resolution in parent should only return services from parent registrations.
  • IEnumerable` resolution in child should return services from parent and child registrations (concatenated)
    • This does mean its not possible to directly "replace" a service registered in the parent, by registering it again in the child, when resolving IEnumerable() - as both will be included in the IEnumerable. Registering a service in a child container only overrides the parent registration when resolving the type, not an IEnumerable of that type.

Notes on Overriding services

If you register a service in the parent container, and in the child container, and then resolve it in the child container - you get the child instance as you'd expect.

If you instead, resolve IEnumerable<TService> you get both (i.e the registrations in the parent for TService, and concatenated with the registrations in the child, and the IEnumerable therefore enumerates all instances).

Therefore if you want to "replace" or "delete" a service that has been registered at parent level so that it isn't included in the IEnumerable<TService> that is resolved from the child container, you'll have to add your own work around in application code using something like this to filter the IEnumerable as you see fit.

public class MyService 
{

public MyService(IEnumerable<Service> services)
{

// e.g filter the IEnumerable yourself to remove or replace services sourced from parent container registrations.
this.Services = services.Where(a=>a.Type.Name=="Foo").ToList();
}

}

This was a design decision - it's better to potentially include "too many" services and allow you to filter them yourself, that in is to autoamtically assume child registrations are not "additive" to parent ones, and perform auto substitution / replacement.

TODO: More to add

wvpm commented

@dazinator forgive me from abusing this issue just to contact you, I don't know how to PM in github.

I actually work(ed) on a similar construct build upon MS DI. My work is focused on encapsulation instead of parent-child relation though.
Our goals partially overlap, we both want to be able to register a different variant for a 'child' container. I also want to make it explicit which (optional) dependencies a container has. I chose the term DependencyInjectionModule over container for this purpose.

In my system you have a root container, which can explicitly expose services to a DependencyInjectionModule, when it imports it. A DependencyInjectionModule has to specify which services it will export (to the container importing it).

I hope this makes sense.

I've attached a zip with the project, hopefully it can help you in your work with child containers.
Below is an example of how I use it to optionally configure a HttpClientProvider. The IHttpRequestSender is a public interface that will be exported.

	public sealed class DependencyInjectionModule : BaseDependencyInjectionModule<DependencyInjectionConfiguration> {
		public DependencyInjectionModule() : base(false) { }

		protected override void RegisterInternals(IServiceCollection internalServiceCollection) {
			internalServiceCollection.AddSingleton<IHttpRequestSender, HttpRequestSender>();
		}

		protected override void RegisterFromConfiguration(
			IServiceCollection internalServiceCollection,
			IServiceProvider publicServiceProvider,
			DependencyInjectionConfiguration dependencyInjectionConfiguration
		) {
			internalServiceCollection.AddSingleton(internalServiceProvider => dependencyInjectionConfiguration.HttpClientProvider(publicServiceProvider));
		}
	}

	public sealed class DependencyInjectionConfiguration : IDependencyInjectionConfiguration {
		[NotNull]
		public Func<IServiceProvider, Func<HttpClient>> HttpClientProvider { get; set; } = provider => () => new HttpClient(
			new SocketsHttpHandler {AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.All, UseCookies = false},
			true
		);
	}

WVPM.DependencyInjection.zip

@wvpm Hi - thanks for the zip (and the introduction) - I'll take a look!
I've done something similar with this "modules" concept in a different project. I think it's a worthy goal to help make applications more modular and in that sense compliments child container concept!

(Didn't mean to reopen this issue, hit the wrong button!l