Skip to content

Latest commit

 

History

History
278 lines (227 loc) · 8.67 KB

1.md

File metadata and controls

278 lines (227 loc) · 8.67 KB

Table of contents

Generics types as a factory

This pattern is used in Microsoft.Extensions.* and in Microsoft.AspNetCore.*. The idea is that you can use a generic type as a factory instead of a function. The type argument is the type you want to instantiate. Consider the example below where we have an IServiceFactory<TService> that can resolve the TService from the DI container or creates an instance if it's not in the container.

public interface IServiceFactory<TService>
{
    TService Service { get; }
}

public class ServiceFactory<TService> : IServiceFactory<TService>
{
    public ServiceFactory(IServiceProvider service)
    {
        Service = (TService)service.GetService(typeof(TService)) ?? ActivatorUtilities.CreateInstance<TService>(service);
    }

    public TService Service { get; }
}

The constructor has access to any service and the generic type being requested. These open generic services are registered like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient(typeof(IServiceFactory<>), typeof(ServiceFactory<>));
}

Lazy initialization of services

Sometimes it's necessary to create services later than a constructor. The usual workaround for this is to inject an IServiceProvider into the constructor of the service that needs to lazily create the instance.

public class Service
{
    private readonly IServiceProvider _serviceProvider;
    public Service(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public IFoo CreateFoo() => _serviceProvider.GetRequiredService<IFoo>();
    public IBar CreateBar() => _serviceProvider.GetRequiredService<IBar>();
}

If the types are known ahead of time, we can build a custom service provider lazy type to encapsulate the service locator pattern:

public interface ILazy<T>
{
    T Value { get; }
}

public class LazyFactory<T> : ILazy<T>
{
    private readonly Lazy<T> _lazy;

    public LazyFactory(IServiceProvider service)
    {
        _lazy = new Lazy<T>(() => service.GetRequiredService<T>());
    }

    public T Value => _lazy.Value;
}

public class Service
{
    private readonly ILazy<IFoo> _foo;
    private readonly ILazy<IBar> _bar;
    public Service(ILazy<IFoo> foo, ILazy<IBar> bar)
    {
        _foo = foo;
        _bar = bar;
    }
    
    public IFoo CreateFoo() => _foo.Value;
    public IBar CreateBar() => _bar.Value;
}

Registered like this:

public void ConfigureServices(IServiceCollection services)
{
    // We register this as transient so it handles both singleton and scoped services
    // as the IServiceProvider injected will have the relevant lifetime.
    services.AddTransient(typeof(ILazy<>), typeof(LazyFactory<>));
}

Lazy<T> could also be used as-is if added with a concrete type at service registration time:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<Lazy<IFoo>>(sp => new Lazy<IFoo>(() => sp.GetRequiredService<IFoo>());
    services.AddTransient<Lazy<IBar>>(sp => new Lazy<IBar>(() => sp.GetRequiredService<IBar>());
}

Single implementation multiple interfaces

Let's say you had a type that implemented multiple interfaces and you wanted to expose it using the DI container. The built-in IServiceCollection type doesn't natively support this but it's easy to emulate using the following pattern.

public class FooAndBar : IFoo, IBar
{
   // Imagine a useful implementation
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<FooAndBar>();
    services.AddSingleton<IFoo>(sp => sp.GetRequiredService<FooAndBar>());
    services.AddSingleton<IBar>(sp => sp.GetRequiredService<FooAndBar>());
}

This will let me resolve FooAndBar, IFoo and IBar and it will give me the same instance.

Creating instances of types from an IServiceProvider

Usually you need to register a type in order to instantiate instances of a type from the DI container, somebody needs to call IServiceProvider.GetService. This means that the service needs to be registered in the container. This may not be desirable if these types are discovered dynamically (like controllers in MVC). There's a useful utility called ActivatorUtilities that can be used as a factory for types that haven't been registered, but have dependencies that are registered in the DI container.

public class MyDependency
{
    public MyDependency(ILogger logger)
    {
    }
}
public class MyDependencyFactory
{
    private readonly IServiceProvider _serviceProvider;
    public MyDependencyFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public MyDependency GetInstance()
    {
        return ActivatorUtilities.CreateInstance<MyDependency>(_serviceProvider);
    }
}

You can build a more optimized version of this uses the CreateFactory. This will pre-calculate the constructor based on the types passed and build a factory for it.

public class MyDependencyFactory
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ObjectFactory _factory;
    public MyDependencyFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _factory = ActivatorUtilities.CreateFactory(typeof(MyDependency), Type.EmptyTypes);
    }
    public MyDependency GetInstance()
    {
        return (MyDependency) _factory(_serviceProvider, null);
    }
}

NOTE: Disposable instances created with this API will not be disposed by the DI container.

Caching singletons in generic types

If you need to cache an instance of something based on type, then you can store it on a static field of a generic type.

public class Factory
{
    public T Create<T>()
        where T : new()
    {
        return Cache<T>.Instance;
    }
    
    private static class Cache<T>
        where T : new()
    {
        public static readonly T Instance = new();
    }
}

You can use the JIT to cache instances on your behalf instead of a slower ConcurrentDictionary<Type, T>. It can also be used to cache the expensive object creation process:

public class Factory
{
    public T Create<T>()
        where T : new()
    {
        return Cache<T>.Instance;
    }
    
    private static class Cache<T>
        where T : new()
    {
        public static readonly T Instance = CallExpensiveMethodToBuildANewInstance();
        
        private static T CallExpensiveMethodToBuildANewInstance()
        {
            // Imagine a really complex process to construct T
            return instance.
        }
    }
}

Resolving services when using IOptions<T>

The options pattern is used in Microsoft.Extensions.* and in Microsoft.AspNetCore.*. It allows anyone to register a callback to mutate a POCO used to configure a library. Sometimes you'd like access to services when configuring these options. There are multiple ways to do this:

public class LibraryOptions
{
    public int Setting { get; set; }
}

public class MyConfigureOptions : IConfigureOptions<LibraryOptions>
{
    private readonly ISomeService _service;
    public MyConfigureOptions(ISomeService service)
    {
        _service = service;
    }
    
    public void Configure(LibraryOptions options)
    {
        options.Setting = _service.ComputeSetting();
    }
}

Followed by the registration.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IConfigureOptions<LibraryOptions>, MyConfigureOptions>();
}

That's a bit verbose, so we added some helpers to make it easier.

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<LibraryOptions>()
            .Configure<ISomeService>((options, service) =>
            {
                options.Setting = service.ComputeSetting();
            });
}