简体   繁体   中英

Using Factory Pattern with ASP.NET Core Dependency Injection

I need the ASP.Net Core dependency injection to pass some parameters to the constructor of my GlobalRepository class which implements the ICardPaymentRepository interface.

The parameters are for configuration and come from the config file and the database, and I don't want my class to go and reference the database and config itself.

I think the factory pattern is the best way to do this but I can't figure out the best way to use a factory class which itself has dependencies on config and database.

My startup looks like this currently:

public class Startup
{
    public IConfiguration _configuration { get; }
    public IHostingEnvironment _environment { get; }

    public Startup(IConfiguration configuration, IHostingEnvironment environment)
    {
        _configuration = configuration;
        _environment = environment;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IDbRepository, DbRepository>();
        var connection = _configuration.GetConnectionString("DbConnection");
        services.Configure<ConnectionStrings>(_configuration.GetSection("ConnectionStrings"));
        services.AddDbContext<DbContext>(options => options.UseSqlServer(connection));
        services.AddScoped<ICardPaymentRepository, GlobalRepository>();
        ...
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IRFDbRepository rFDbRepository)
    {
     ...
    }
}

The GlobalRepository constructor looks like this:

public GlobalRepository(string mode, string apiKey)
{
}

How do I now pass the mode from configuration and the apiKey from the DbRepository into the constructor from Startup?

Use the factory delegate overload when registering the repository

//...

string mode = "get value from config";

services.AddScoped<ICardPaymentRepository, GlobalRepository>(sp => {        
    IDbRepository repo = sp.GetRequiredService<IDbRepository>();
    string apiKey = repo.GetApiKeyMethodHere();

    return new GlobalRepository(mode, apiKey);
});

//...

Alternative using ActivatorUtilities.CreateInstance

//...

string mode = "get value from config";

services.AddScoped<ICardPaymentRepository>(sp => {        
    IDbRepository repo = sp.GetRequiredService<IDbRepository>();
    string apiKey = repo.GetApiKeyMethodHere();

    return ActivatorUtilities.CreateInstance<GlobalRepository>(sp, mode, apiKey);
});

//...

You might want to also check these links...

https://github.com/Microsoft/AspNetCoreInjection.TypedFactories

https://espressocoder.com/2018/10/08/injecting-a-factory-service-in-asp-net-core/

With regard to the last link the code is basically:

public class Factory<T> : IFactory<T>
{
    private readonly Func<T> _initFunc;

    public Factory(Func<T> initFunc)
    {
        _initFunc = initFunc;
    }

    public T Create()
    {
        return _initFunc();
    }
}

public static class ServiceCollectionExtensions
{
    public static void AddFactory<TService, TImplementation>(this IServiceCollection services) 
    where TService : class
    where TImplementation : class, TService
    {
        services.AddTransient<TService, TImplementation>();
        services.AddSingleton<Func<TService>>(x => () => x.GetService<TService>());
        services.AddSingleton<IFactory<TService>, Factory<TService>>();
    }
}

I think castle windsor's typed factories dispose of all they created when they themselves are disposed (which may not be always the best idea), with these links you would probably have to consider if you are still expecting that behaviour. When I reconsidered why I wanted a factory I ended up just creating a simple factory wrapping new, such as:

public class DefaultFooFactory: IFooFactory{
  public IFoo create(){return new DefaultFoo();}
}

I've been running up against the same issue and solved this by registering a set of open generics for IFactory<TService> , IFactory<T, TService> , IFactory<T1, T2, TService> etc. A single call on startup to add this facility then allows any IFactory<...> to be injected / resolved, which will instantiate an instance of TService for a given set of argument types, provided a constuctor exists whose last parameters match the T* types of the factory generic. Source code, NuGet package and explanatory blog article below:

https://github.com/jmg48/useful

https://www.nuget.org/packages/Ariadne.Extensions.ServiceCollection/

https://jon-glass.medium.com/abstract-factory-support-for-microsoft-net-dependency-injection-3c3834894c19

An alternative to the other answers. Follow the options pattern .

First introduce a strong type for your configuration;

public class RespositoryOptions {
    public string Mode { get; set; }
    public string ApiKey { get; set; }
}

public GlobalRepository(IOptions<RespositoryOptions> options) {
    // use options.Value;
}

You could still use a service factory method to unwrap the IOptions<RespositoryOptions> if you prefer. But then you lose the ability to verify that your service dependencies have all been met.

Then you can seed your options from configuration;

public void ConfigureServices(IServiceCollection services) {
    ...
    services.Configure<RespositoryOptions>(_configuration.GetSection(name));
    ...
}

And write another service to update that options instance from other services, like a database;

public class ConfigureRespositoryOptions : IConfigureOptions<RespositoryOptions> {
    private readonly IDbRepository repo;
    public ConfigureRespositoryOptions(IDbRepository repo) {
        this.repo = repo;
    }
    public void Configure(RespositoryOptions config) {
        string apiKey = repo.GetApiKeyMethodHere();
    }
}

I'll show the minimal example for the factory that resolves ITalk implementation by a string key. The solution can be easily extended to a generic factory with any key and entity type.

For the sake of example let's define the interface ITalk and two implementations Cat and Dog :

public interface ITalk
{
    string Talk();
}

public class Cat : ITalk
{
    public string Talk() => "Meow!";
}

public class Dog : ITalk
{
    public string Talk() => "Woof!";
}

Now define the TalkFactoryOptions and TalkFactory :

public class TalkFactoryOptions
{
    public IDictionary<string, Type> Types { get; } = new Dictionary<string, Type>();

    public void Register<T>(string name) where T : ITalk
    {
        Types.Add(name, typeof(T));
    }
}

public class TalkFactory
{
    private readonly IServiceProvider _provider;
    private readonly IDictionary<string, Type> _types;

    public TalkFactory(IServiceProvider provider, IOptions<TalkFactoryOptions> options)
    {
        _provider = provider;
        _types = options.Value.Types;
    }

    public ITalk Resolve(string name)
    {
        if (_types.TryGetValue(name, out var type))
        {
            return (ITalk)_provider.GetRequiredService(type);
        }

        throw new ArgumentOutOfRangeException(nameof(name));
    }
}

Add extension method for simple implementations registration:

public static class FactoryDiExtensions
{
    public static IServiceCollection RegisterTransientSpeaker<TImplementation>(this IServiceCollection services, string name)
        where TImplementation : class, ITalk
    {
        services.TryAddTransient<TalkFactory>();
        services.TryAddTransient<TImplementation>();
        services.Configure<TalkFactoryOptions>(options => options.Register<TImplementation>(name));
        return services;
    }
}

And register the Cat and Dog implementations:

services
  .RegisterTransientSpeaker<Cat>("cat")
  .RegisterTransientSpeaker<Dog>("dog");

Now you can inject the TalkFactory and resolve the implementation by the name:

var speaker = _factory.Resolve("cat");
var speech = speaker.Talk();

The trick here is Configure<TOptions() . This method is additive, which means you can call it multiple times to configure the same instance of TalkFactoryOptions .
As I said this example can be converted into a generic factory and add the ability to register factory delegate instead of a concrete type. But the code will be too long for SO.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM