2016-08-19 3 views
15

In ASP.NET Core eines der Dinge, die Sie mit Microsofts Dependency Injection-Framework is bind "open generics" (generische Typen ungebunden auf einen konkreten Typ) tun können, etwa so:Fabrik-Muster mit Öffnen Generics

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

Sie können auch beschäftigen the factory pattern to hydrate dependencies. Hier ist ein konstruiertes Beispiel:

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() 
    ); 
} 

Allerdings habe ich nicht in der Lage gewesen, um herauszufinden, wie die beiden Konzepte miteinander zu kombinieren. Es scheint, als würde es mit so etwas beginnen, aber ich brauche den konkreten Typ, der verwendet wird, um eine Instanz von IRepository<> zu hydratisieren.

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(); 
     }   
    ); 
} 

Wenn ich versuche, den Func<IServiceProvider, object> Funktor mit einem offenen Generika zu verwenden, erhalte ich this ArgumentException mit der Nachricht Open generic service type 'IRepository<T>' requires registering an open generic implementation type. von der Dotnet CLI. Es kommt nicht einmal zum Lambda.

Ist diese Art der Bindung mit dem Microsoft-Framework für Abhängigkeitsinjektionen möglich?

+0

Was ist der Vorteil von registerin ist g ein Lambda, das eine Fabrik auflöst, die den erforderlichen Service löst? – Steven

+0

Gute Frage. Es leitet die Komplexität für die bedingte Hydratation um. Sie benötigen keine explizite Factory, da das Lambda als eins fungiert (seine Variable wird sogar als "implementationFactory" bezeichnet), aber wenn Sie mehrere Services benötigen, um eine Entscheidung darüber zu treffen, welche Instanz Sie hydratisieren möchten, werden Sie dies tun ein Lambda, das komplex und schwer zu testen ist. Der Blogbeitrag, den ich oben verlinkt habe, hat ein gutes Beispiel: http://dotnetliberty.com/index.php/2016/05/09/asp-net-core-factory-penden-dependency-injection/ – Technetium

+0

hast du jemals ein gutes gefunden dafür antworten? Ich habe das gleiche Problem, aber keine der Antworten hier scheint eine gute Lösung für das Problem zu sein –

Antwort

1

Ich verstehe auch nicht den Punkt Ihres Lambda-Ausdrucks, also erkläre ich Ihnen meine Art, es zu tun.

Ich nehme an, was Sie wollen, ist zu erreichen, was in dem Artikel erklären Sie

geteilt

Das ist mir die eingehende Anforderung zu inspizieren, bevor eine Abhängigkeit in das ASP.NET-Core Dependency Injection-System

Versorgung

Ich musste einen benutzerdefinierten Header in der HTTP-Anfrage untersuchen, um festzustellen, welcher Kunde meine API anfordert. Ich könnte dann etwas später in der Pipeline entscheiden, welche Implementierung meines IDatabaseRepository (Dateisystem oder Entitätsframework verknüpft mit einer SQL-Datenbank) für diese eindeutige Anfrage bereitstellen soll.

So beginne ich durch eine Middleware-Schreiben

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); 
    } 
} 

Mein SettingsProvider nur ein Singleton ist, die mir die entsprechenden Kundenobjekt zur Verfügung stellt.

Um unsere Middleware-Zugang zu lassen diese ContextSettings wir es zuerst in ConfigureServices in Startup.cs registrieren müssen

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

Und in der Configure Methode, die wir unsere Middleware registrieren

app.UseMiddleware<ContextSettingsMiddleware>(); 

Jetzt, wo unsere Kunden Ist von anderswo zugänglich, lass uns unsere Factory schreiben.

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; 
     } 
    } 
} 

Um serviceProvider.GetService<>() Methode verwenden Sie die folgende Verwendung in der CS-Datei

using Microsoft.Extensions.DependencyInjection; 

Schließlich enthalten müssen wir unsere Fabrik in ConfigureServices Methode können

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

Also jedes Single HTTP-Anfrage mein DatabaseRepository wird möglicherweise abhängig von mehreren Parametern. Ich könnte ein Dateisystem oder eine SQL-Datenbank verwenden und ich kann die richtige Datenbank für meinen Kunden erhalten. (Ja, ich habe mehrere Datenbanken pro Kunde, versuchen Sie nicht zu verstehen, warum)

Ich vereinfachte es wie möglich, mein Code ist in Wirklichkeit komplexer, aber Sie bekommen die Idee (ich hoffe). Jetzt können Sie dies Ihren Bedürfnissen anpassen.

+0

Dies scheint ein bisschen anders zu sein als mein Problem, obwohl es ein sehr gutes Beispiel dafür ist, Definitionen für IServiceCollection auf einem Anfragebasis. Sie verschieben die Entscheidung, welche Implementierung eines bekannten Typs (IDatabaseRepository) zu verwenden ist. In meiner Situation ist 'IRepository <>' offen/unbekannt, weil ich nicht die Typinformation habe, die notwendig ist, um es im 'implementationFactory' Lambda für' ServiceProvider' zu schließen. – Technetium

+0

Nur um einige Dinge zu klären. Sie können IMMER Lambda-Ausdrücke loswerden.Sie dienen lediglich dazu, das Leben der Entwickler zu vereinfachen und ermöglichen uns, schneller zu programmieren. Aber in diesem Fall hatte ich nicht gerne diesen großen Code in meiner Startup.cs-Datei. Deshalb habe ich beschlossen, es loszuwerden und meine Logik in einer 'DatabaseRepositoryFactory' zu implementieren. Sie können auch dasselbe tun, wenn Sie möchten. –

+0

Auch meine Datenbankrepositorys implementieren 'IDatabaseRepository ', Schnittstelle, die tatsächlich von' IRepository 'erbt. Also meine wirkliche Abhängigkeitsinjektion ist 'services.AddScoped > (databaseRepositoryFactory.DatabaseRepository)'. Ich dachte, es wäre eine gute Idee, einfach das in meinem Beispiel, aber vielleicht benötigten Sie die volle Probe ... Es ist egal, dass ich nicht wähle, welche Implementierung von 'IDatabase' ich verwende. Mein Repository muss nur seinen Vertrag respektieren und eine "IDatabase" akzeptieren oder zurückgeben. –

4

Die net.core-Abhängigkeit erlaubt Ihnen nicht, eine Factory-Methode beim Registrieren eines offenen generischen Typs anzugeben, aber Sie können dies umgehen, indem Sie einen Typ bereitstellen, der die angeforderte Schnittstelle implementiert, aber intern als Factory fungiert . Eine Fabrik in der Verkleidung:

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 
} 

Wenn der Behälter die MongoCollectionFactory ti wissen löst, welche Art T und wird die Sammlung korrekt erstellen. Dann nehmen wir diese erstellte Sammlung intern ab und delegieren alle Aufrufe an sie. (Wir ahmen this=factory.Create(), die nicht in csharp erlaubt ist :).)

Update: Wie von Kristian Hellang das gleiche Muster von ASP.NET Logging verwendet wird

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

Original Diskussion hier:

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