54 votes

Factory Pattern avec Open Generics

Dans ASP.NET Core, le cadre d'injection de dépendances de Microsoft vous permet notamment de est lié aux "génériques ouverts". (types génériques non liés à un type concret) comme suit :

public void ConfigureServices(IServiceCollection services) {
    services.AddSingleton(typeof(IRepository<>), typeof(Repository<>))
}

Vous pouvez également employer le modèle d'usine pour hydrater les dépendances . Voici un exemple artificiel :

public interface IFactory<out T> {
    T Provide();
}

public void ConfigureServices(IServiceCollection services) {
    services.AddTransient(typeof(IFactory<>), typeof(Factory<>));

    services.AddSingleton(
        typeof(IRepository<Foo>), 
        p => p.GetRequiredService<IFactory<IRepository<Foo>>().Provide()
    ); 
}

Cependant, je n'ai pas réussi à trouver le moyen de combiner les deux concepts. Il semble que cela pourrait commencer par quelque chose comme ceci, mais j'ai besoin du type concret qui est utilisé pour hydrater une instance de IRepository<> .

public void ConfigureServices(IServiceCollection services) {
    services.AddTransient(typeof(IFactory<>), typeof(Factory<>));

    services.AddSingleton(
        typeof(IRepository<>), 
        provider => {
            // Say the IServiceProvider is trying to hydrate 
            // IRepository<Foo> when this lambda is invoked. 
            // In that case, I need access to a System.Type 
            // object which is IRepository<Foo>. 
            // i.e.: repositoryType = typeof(IRepository<Foo>);

            // If I had that, I could snag the generic argument
            // from IRepository<Foo> and hydrate the factory, like so:

            var modelType = repositoryType.GetGenericArguments()[0];
            var factoryType = typeof(IFactory<IRepository<>>).MakeGenericType(modelType);
            var factory = (IFactory<object>)p.GetRequiredService(factoryType);

            return factory.Provide();
        }           
    ); 
}

Si j'essaie d'utiliser le Func<IServiceProvider, object> avec un générique ouvert, j'obtiens ce ArgumentException avec le message Open generic service type 'IRepository<T>' requires registering an open generic implementation type. à partir de l'interface CLI de Dotnet. Il n'arrive même pas à la lambda.

Ce type de liaison est-il possible avec le cadre d'injection de dépendances de Microsoft ?

27voto

nohwnd Points 489

La dépendance net.core ne vous permet pas de fournir une méthode de fabrique lors de l'enregistrement d'un type générique ouvert, mais vous pouvez contourner ce problème en fournissant un type qui implémentera l'interface demandée, mais qui agira en interne comme une fabrique. Une usine déguisée :

services.AddSingleton(typeof(IMongoCollection<>), typeof(MongoCollectionFactory<>)); //this is the important part
services.AddSingleton(typeof(IRepository<>), typeof(Repository<>))

public class Repository : IRepository {
    private readonly IMongoCollection _collection;
    public Repository(IMongoCollection collection)
    {
        _collection = collection;
    }

    // .. rest of the implementation
}

//and this is important as well
public class MongoCollectionFactory<T> : IMongoCollection<T> {
    private readonly _collection;

    public RepositoryFactoryAdapter(IMongoDatabase database) {
        // do the factory work here
        _collection = database.GetCollection<T>(typeof(T).Name.ToLowerInvariant())
    }

    public T Find(string id) 
    {
        return collection.Find(id);
    }   
    // ... etc. all the remaining members of the IMongoCollection<T>, 
    // you can generate this easily with ReSharper, by running 
    // delegate implementation to a new field refactoring
}

Lorsque le conteneur résout le MongoCollectionFactory ti saura quel est le type de T et créera la collection correctement. Ensuite, nous prenons cette collection créée, la sauvegardons en interne et déléguons tous les appels à cette collection. (Nous imitons this=factory.Create() ce qui n'est pas autorisé dans csharp. :))

Mise à jour : Comme indiqué par Kristian Hellang, le même modèle est utilisé par ASP.NET Logging.

public class Logger<T> : ILogger<T>
{
    private readonly ILogger _logger;

    public Logger(ILoggerFactory factory)
    {
        _logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T)));
    }

    void ILogger.Log<TState>(...)
    {
        _logger.Log(logLevel, eventId, state, exception, formatter);
    }
}

https://github.com/aspnet/Logging/blob/dev/src/Microsoft.Extensions.Logging.Abstractions/LoggerOfT.cs#L29

discussion originale ici :

https://twitter.com/khellang/status/839120286222012416

1voto

Jérôme MEVEL Points 1458

Je ne comprends pas non plus l'intérêt de votre expression lambda, je vais donc vous expliquer ma façon de faire.

Je suppose que ce que vous souhaitez est d'atteindre ce qui est expliqué dans l'article que vous avez partagé.

Cela m'a permis d'inspecter la requête entrante avant de fournir une dépendance dans le système d'injection de dépendances d'ASP.NET Core.

Mon besoin était d'inspecter un en-tête personnalisé dans la requête HTTP pour déterminer quel client demande mon API. Je pourrais alors, un peu plus tard dans le pipeline, décider quelle implémentation de mon IDatabaseRepository (Système de fichiers ou Entity Framework lié à une base de données SQL) pour répondre à cette demande unique.

Je commence donc par écrire un middleware

public class ContextSettingsMiddleware
{
    private readonly RequestDelegate _next;

    public ContextSettingsMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IServiceProvider serviceProvider, IHostingEnvironment env, IContextSettings contextSettings)
    {
        var customerName = context.Request.Headers["customer"];
        var customer = SettingsProvider.Instance.Settings.Customers.FirstOrDefault(c => c.Name == customerName);
        contextSettings.SetCurrentCustomer(customer);

        await _next.Invoke(context);
    }
}

Mi SettingsProvider est juste un singleton qui me fournit l'objet client correspondant.

Pour permettre à notre intergiciel d'accéder à ce ContextSettings nous devons d'abord l'enregistrer dans ConfigureServices dans Startup.cs

var contextSettings = new ContextSettings();
services.AddSingleton<IContextSettings>(contextSettings);

Et dans le Configure nous enregistrons notre intergiciel

app.UseMiddleware<ContextSettingsMiddleware>();

Maintenant que notre client est accessible d'ailleurs, écrivons notre Factory.

public class DatabaseRepositoryFactory
{
    private IHostingEnvironment _env { get; set; }

    public Func<IServiceProvider, IDatabaseRepository> DatabaseRepository { get; private set; }

    public DatabaseRepositoryFactory(IHostingEnvironment env)
    {
        _env = env;
        DatabaseRepository = GetDatabaseRepository;
    }

    private IDatabaseRepository GetDatabaseRepository(IServiceProvider serviceProvider)
    {
        var contextSettings = serviceProvider.GetService<IContextSettings>();
        var currentCustomer = contextSettings.GetCurrentCustomer();

        if(SOME CHECK)
        {
            var currentDatabase = currentCustomer.CurrentDatabase as FileSystemDatabase;
            var databaseRepository = new FileSystemDatabaseRepository(currentDatabase.Path);
            return databaseRepository;
        }
        else
        {
            var currentDatabase = currentCustomer.CurrentDatabase as EntityDatabase;
            var dbContext = new CustomDbContext(currentDatabase.ConnectionString, _env.EnvironmentName);
            var databaseRepository = new EntityFrameworkDatabaseRepository(dbContext);
            return databaseRepository;
        }
    }
}

Afin d'utiliser serviceProvider.GetService<>() vous devrez inclure l'utilisation suivante dans votre fichier CS

using Microsoft.Extensions.DependencyInjection;

Enfin, nous pouvons utiliser notre usine dans ConfigureServices méthode

var databaseRepositoryFactory = new DatabaseRepositoryFactory(_env);
services.AddScoped<IDatabaseRepository>(databaseRepositoryFactory.DatabaseRepository);

Donc chaque requête HTTP que mon DatabaseRepository peut être différente en fonction de plusieurs paramètres. Je pourrais utiliser un système de fichiers ou une base de données SQL et je peux obtenir la base de données appropriée correspondant à mon client. (Oui j'ai plusieurs bases de données par client, n'essayez pas de comprendre pourquoi)

Je l'ai simplifié au maximum, mon code est en réalité plus complexe mais vous comprenez l'idée (j'espère). Vous pouvez maintenant le modifier pour l'adapter à vos besoins.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X