Laden...

ASP.NET Core DatabaseContext injection mit MediatR?

Erstellt von Olii vor 5 Jahren Letzter Beitrag vor 5 Jahren 2.244 Views
O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 5 Jahren
ASP.NET Core DatabaseContext injection mit MediatR?

Hallo liebe Forum User,

ich versuche mich gerade an CQRS und verwende dazu MediatR als Hilfe. Nun habe ich aber das Problem das ich es nicht schaffe meinen DBContext in die Handler zu injecten.

Als ich Erfahrungen gesamelt habe mit dem Repository Pattern, habe ich es so gemacht:

var connection = @"Server=127.0.0.1;Port=5433;Database=PostgreSQL 11;UserId=postgres;Password=admin;";
            services.AddTransient<IEmployeeRepository, EmployeeRepository>(parameter => new EmployeeRepository(connection));

(Ist nur eine lokale Datenbank, deswegen poste ich einfach mal den ganzen ConnectionString)

So funktioniert das leider nicht da MediatR zumal die ganze Struktur übernimmt.

Von einem User hier aus dem Forum habe ich noch aus meinem letzten Thread sein git Repository zugeschickt bekommen (ich weiß nicht ob ich hier Namen nennen darf), dort wurde es wie folgt gemacht:

services.AddSingleton<IDataContext>(_ => new InMemoryDataContext(
                authorsCount: 25, booksCount: 200, booksPerAuthorMin: 3, booksPerAuthorMax: 12));

In meinem Fall habe ich keine Klasse und um ehrlich zu sein weiß ich nicht zu 100% wie genau das funktioniert. Ich wollte erst mit einer PostgreSQL Datenbank arbeiten, also quasi in den Handlern sofort query dahingehend ausführen etc. später würde ich allerdings auch mal versuchen Redis einzubauen um das cachen mal auszuprobieren.

Aber momentan schaffe ich es nicht Injection und MediatR in Kombination zu verwenden.

2.078 Beiträge seit 2012
vor 5 Jahren

Ich galube, Du wirfst hier einiges durcheinander 😉

MediatR bietet erst einmal nur die Infrastruktur zum vermitteln (Mediator-Pattern) der Requests an die passenden Handler.
Das Injecten weiterer Abhängigkeiten der Handler hat nichts mit MediatR zu tun, sondern mit dem IoC-Container.

Du musst also folgendes tun:

  • einen IoC-Container erstellen (Bei der Variante von ASP.NET ist das die ServiceCollection)
  • die Klasse registrieren, die für das Aufbauen der DB-Verbindung zuständig ist. Für EFCore gibt's für die ServiceCollection passende Methoden
  • wenn Du weitere Klassen wie z.B. Repositories hast, musst Du die auch registrieren
  • alles Nötige von MediatR registrieren (Müsste aus den Beispielen im GitHub-Repository ersichtlich sein, wie das geht)
  • deine Handler registrieren

Das Registrieren läuft dabei immer nach dem selben Prinzip ab:

services.AddEntityFrameworkSqlServer();
services.AddTransient<IMyRepository, MyRepository>();
// Weiteres für MediatR und die Handler

Nach diesem Prinzip registrierst Du alles, Du erstellst keine Abhängigkeit mehr selber, Du registrierst sie und der IoC-Container setzt alles zusammen. Oder Du verwendest die Klasse "ActivatorUtilities", die kann das auch.

Die Parameter musst Du nicht übergeben! Das übernimmt das Framework für dich, wenn es die Parameterwerte in den registrierten Services finden kann. Bei sowas wie z.B. String-Parametern musst Du dann konkreter arbeiten, aber ich gehe da lieber den Umweg über das FactoryPattern:

services.AddTransient<IMyServiceFactory, MyServiceFactory>();
services.AddTransient(c => c.GetService<IMyServiceFactory>().Create());

So könntest Du dann auch deine Datenbankverbindung unterbringen.

Das EntityFramework bietet dafür aber ein eigenes Konzept, es hat nämlich ein paar Erweiterungsmethoden, wie z.B. AddDbContext, worüber Du dann den DbContext konfigurieren kannst. Wenn Du eine Klasse registrierst, die den DbContext als Parameter braucht, wird der automatisch injected.

Ach ja:
Die Datenbankverbindung sollte nicht als Singleton registriert werden!
Ich würde die als Scoped registrieren, dann bleibt sie nur so lange offen, bis der Scope disposed wird.

O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 5 Jahren

Danke erstmal für die Hilfe, sehr freundlich 😃

ich habe mal versucht das umzusetzen, aber ich glaube das ist nicht richtig was ich mache (funktioniert nämlich nicht).

Ich verwende übrigends Dapper anstatt EF da ich das damit auch mal ausprobieren wollte.

Erstmal habe ich einfach eine ganz einfache Klasse erstellt + Interface damit ich das ganze hier ausprobieren kann.


public class DBContext : IDBContext
{
    private readonly string _connectionString;

    public DBContext()
    {
        _connectionString = "Server=127.0.0.1;Port=5433;Database=PostgreSQL 11;UserId=postgres;Password=admin;";
    }

}

public interface IDBContext
{

}

Dann habe ich das in der Startup.cs so registriert:


services.AddScoped<IDBContext, DBContext>();
            services.AddScoped(c => c.GetService<IDBContext>());
            services.AddScoped<IRequestHandler<CreateUserQuery,UserViewModel>, Handler>();

Mein Handler sieht so aus:


using System.Threading;
using System.Threading.Tasks;
using MediatR;

public class Handler : IRequestHandler<CreateUserQuery,UserViewModel>
{
    public readonly DBContext _dbcon;

    public Handler (DBContext dbcon)
    {
        _dbcon = dbcon;
    }
    public async Task<UserViewModel> Handle(CreateUserQuery request, CancellationToken cancellationToken)
    {
        return new UserViewModel()
        {
            Id = 1,
            FirstName = request.FirstName
        };
    }

}

IRequestHandler

IRequestHandler kommt von MediatR

Wenn ich das ganze so versuche bekomme ich nur die Fehlermeldung:

InvalidOperationException: Unable to resolve service for type 'DBContext' while attempting to activate 'Handler'.

InvalidOperationException: Error constructing handler for request of type MediatR.IRequestHandler`2[CreateUserQuery,UserViewModel]. Register your handlers with the container.

Ich dachte ich hätte mein Handler damit schon registriert aber anscheinend nicht.

Ich bin in der ganzen Thematik neu und lese viele Artikel und Dokus aber ich glaube ich bin total durcheinander geraten...

4.931 Beiträge seit 2008
vor 5 Jahren

Da du IDBContext registriert hast, mußt du dann auch diesen als Parameter angeben:


public readonly IDBContext _dbcon;

public Handler (IDBContext dbcon)
{
    _dbcon = dbcon;
}

2.078 Beiträge seit 2012
vor 5 Jahren

Das könnte (muss nicht) auch problematisch sein:

services.AddScoped<IDBContext, DBContext>();
services.AddScoped(c => c.GetService<IDBContext>());

Du registrierst hier IDBContext zwei Mal, beim zweiten Mal rufst Du die Instanz vom ersten Mal ab, also zwei Mal die selbe Instanz.

Ich meine stattdessen, dass Du eine IDBContextFactory schreibst:

public class DBContext : IDBContext
{
    private readonly string _connectionString;

    public DBContext(string connectionString)
    {
        _connectionString = connectionString;
    }
}
public interface IDBContext
{

}
public interface IDBContextFactory
{
    IDBContext CreateContext();
}
public interface DBContextFactory
{
    public IDBContext CreateContext()
    {
        var connectionString = GetConnectionStringFromConfig();
        return new DBContext(connectionString);
    }
}

services.AddScoped<IDBContextFactory, DBContextFactory>();
services.AddScoped(c => c.GetServiceRequired<IDBContextFactory>().CreateContext());

Deine Variante reicht aber auch, dann lässt Du einfach die zweite, überflüssige Registrierung weg.
Das musst dann Du entscheiden, ob eine Factory für dich Sinn macht oder nicht, ich persönlich hab nur ungern die Verbindungsinformationen im DBContext bzw. hole sie dort aus z.B. der Config.

16.807 Beiträge seit 2008
vor 5 Jahren

Ich würde keinen Factory Pattern hier verwenden; lohnt sich nicht - und die Pipe kann bei einer Factory eine Verbindung nicht automatisch disposen.

Darüber hinaus würde ich den Kontext in diesem Fall nicht als Scoped, sondern als Transient verwenden.
Scoped = ein Context pro Request(thread).
Sprich mehrere Handler würden sich einen Kontext teilen - bin ich jetzt nicht so nen riesen Fan von.
Ich würde jedem Handler nen eigenen Kontext verpassen (=> Transient)

O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 5 Jahren

Danke erstmal für die Antworten. Sehr Hilfreiche Informationen.

Hier die vorgenommen Änderungen zur Lösung Falls jemand das selbe Problem hat:

Th69 hatte recht, ich hatte mich vertan und nicht das Interface genommen weil ich dachte die Klasse müsste verwendet werden.

Und Palladin007 hat ebenso recht das diese Zeile nicht gestimmt hat:


services.AddScoped(c => c.GetService<IDBContext>());

Diese Zeile hat den Fehler verursacht, aber ebenso war diese auch wie es aussieht nicht nötig:


services.AddScoped<IRequestHandler<CreateUserQuery,UserViewModel>, Handler>();

Den ohne diese Zeile funktioniert alles wie es soll.

Und danke Abt an die lehrreichen Informationen!

16.807 Beiträge seit 2008
vor 5 Jahren

Es ist nicht notwendig jeden Handler zu registrieren.
Das passiert automatisch, wenn Du AddMediatR verwendest.
Dabei musst Du nur die Assembly angeben, in der sich die Handler befinden (siehe mein Sample, daher die Zeile Assembly engineAssembly = typeof(CommonEngine).Assembly;

2.078 Beiträge seit 2012
vor 5 Jahren

Ich würde keinen Factory Pattern hier verwenden; lohnt sich nicht - und die Pipe kann bei einer Factory eine Verbindung nicht automatisch disposen.

Das stimmt so nicht ganz.
Solange man es so registriert, wie ich das gezeigt habe, kann es disposed werden.
Getestet mit Microsoft.Extensions.DependencyInjection und Autofac; mit Scope und ohne.
Bei Autofac kann man das Verhalten auch abstellen, bei der Variante von Microsoft hab ich keine passende Option gefunden.

Dennoch sollte man sich natürlich überlegen, ob sich eine Factory lohnt oder nicht.
Pauschal verneinen würde ich es aber nicht.