Laden...

Bearbeitungszyklus mit DbContext implementieren: Best-Practice

Erstellt von sugar76 vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.588 Views
S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren
Bearbeitungszyklus mit DbContext implementieren: Best-Practice

Hallo allerseits,

ich arbeite aktuell an einer Desktop-Anwendung mit WPF und EF6.

In einem vorherigen Beitrag ist die Frage aufgetaucht, wie der Bearbeitungszyklus am besten zu implementieren ist. Damit meine ich:

  1. Benutzer öffnet einen Datensatz zur Bearbeitung
  2. Benutzer nimmt Eingaben vor
  3. Benutzer speichert seine Eingaben

Ich habe es bisher so gemacht, dass ich mit Objekten im detached-State arbeite: erst beim Speichern der Änderungen wird ein neuer DbContext erzeugt und dann gespeichert. Das führt zu Problemen (siehe oben genannter Thread).

Die Alternative ist ja, ein UnitOfWork zu verwenden:

  1. Benutzer öffnet einen Datensatz zur Bearbeitung (Kontext wird erzeugt)
  2. Benutzer nimmt Eingaben vor
  3. Benutzer speichert seine Änderungen (Kontext wird committed).

Wie genau man den UnitOfWork implementiert (DbContext als UoW oder abstrahiert), ist hier erstmal nicht so wichtig.

**Meine Frage: **ist dieses Verfahren auch in einer Desktop-Anwendung zu empfehlen? Was ist hier der Best-Practice? Was, wenn der Benutzer zwischen Schritt 2) und 3) einen Kaffee trinken geht? Dann bleibt der Kontext ja über mehrere Minuten geöffnet ...

Gruß

6.911 Beiträge seit 2009
vor 5 Jahren

Hallo sugar76,

Die Alternative ist ja, ein UnitOfWork zu verwenden:
...
ist dieses Verfahren auch in einer Desktop-Anwendung zu empfehlen?

Ja.
Genau für solche Zwecke gibt es die UoW auch.

Was, wenn der Benutzer zwischen Schritt 2) und 3) einen Kaffee trinken geht?

Wenn nicht jemand anders an den gleichen Daten in der Zwischenzeit etwas ändert, so passiert nichts anderes als wenn der Benutzer keinen Kaffee trinken ginge.
Hat jemand anders in dieser Zeit an den gleichen Daten etwas geändert, so kommt beim Update von der DB eine ConcurrencyException und diese kann entsprechend behandelt werden. Z.B. können die neuen Daten von der DB geladen werden um den lokalen Context aufzufrischen und dann mit den lokalen Änderungen erneut füttern -> geht es dann, gut, sonst dem Benutzer eine Mitteilung anzeigen.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

Danke für die eindeutige Antwort.

Dann stelle ich jetzt doch mal die Frage nach der Implementierung 😉
Ich sehe zwei Möglichkeiten:

  1. direkt den DbContext als UoW zu verwenden
  2. vom DbContext mittels Repositories zu abstrahieren und diese dann in ein eigenes UoW packen:
public class UnitOfWork : IDisposable
{
    protected MyDbContext _context = new MyDbContext();

    private AufgabeRepository _aufgabeRepository;
    public AufgabeRepository AufgabeRepository
    {
        get
        {
            if (_aufgabeRepository == null) _aufgabeRepository = new AufgabeRepository(_context);
            return _aufgabeRepository;
        }
    }

    private PersonRepository _personRepository;
    public PersonRepository PersonRepository
    {
        get
        {
            if (_personRepository == null) _personRepository = new PersonRepository(_context);
            return _personRepository;
        }
    }

    // alle anderen Repositories (eines pro DB-Tabelle)

    public void Commit()
    {
        _context.SaveChanges();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

Variante 1) ist schön einfach. Bei Variante 2) habe ich den Vorteil, dass ich wiederverwendbare Queries im Repository implementieren kann.

Was meint Ihr ...?

Gruß

6.911 Beiträge seit 2009
vor 5 Jahren

Hallo sugar76,

ich bin für Variante 1, denn der DbContext ist schon eine UoW, daher braucht das nicht neu / doppelt implementiert werden.

wiederverwendbare Queries im Repository implementieren kann.

Wenn du den CodeFirst-Ansatz bei EF (Core) verwendest, so kannst du die DbContext-Klasse auch erweitern und dort diese Queries einbauen.
Bei DB-First kannst du die partielle Klasse erweitern.

Allerdings koppelst du dich damit an EF (Core). Wenn das kein Nachteil ist, so passt dieser Weg. Willst du keine Kopplung an EF (Core) haben, so wäre Variante 2 od. eine andere Abstraktion besser.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

16.835 Beiträge seit 2008
vor 5 Jahren

Wenn Du Dich für die Abstraktion entscheidest, so setze das am Besten mit entsprechender Dependency Injection um; Instanzen sollten nie manuell erstellt werden.
Und verwende Interfaces, damit Du Deinen Code auch testen kannst.
[Artikel] Unit-Tests: Einführung in das Unit-Testing mit VisualStudio

Deine UnitOfWork Implementierung würde auch dazu führen, dass Du Repositories instanziierst, die Du gar nicht brauchst.

PS: ich bin gar kein Fan mehr von einem solchen Repository Aufbau.
Moderne Pattern (werfe erneut MediatR in den Raum) machen das alles viel eleganter und übersichtlicher.

PPS: auch ohne die zusätzliche Abstraktion kannst Du Queries wiederverwenden.
Das ist ja nur ne OOP Struktur.

S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

Deine UnitOfWork Implementierung würde auch dazu führen, dass Du Repositories instanziierst, die Du gar nicht brauchst.

Moderne Pattern (werfe erneut MediatR in den Raum) machen das alles viel eleganter und übersichtlicher.

Den ersten Punkt verstehe ich nicht - das Repository wird doch nur instanziiert, wenn tatsächlich auf die Property zugegriffen wird:

private AufgabeRepository _aufgabeRepository;
public AufgabeRepository AufgabeRepository
{
    get
    {
        if (_aufgabeRepository == null) _aufgabeRepository = new AufgabeRepository(_context);
        return _aufgabeRepository;
    }
}

Ich gebe zu, dass ich ich das Konzept hinter MediatR & Co. nicht ganz kapiert habe. Gefunden habe ich dieses Beispiel.

public class LoginViewModel : BaseViewModel
{
    private readonly IMediator _mediator;

    // ...
    public IMvxAsyncCommand LoginCommand => new MvxAsyncCommand(LoginAsync, () => CanLogin);

    // ...

    public bool CanLogin => !string.IsNullOrEmpty(Username) && Username.Length >= 5 &&
                                   !string.IsNullOrEmpty(Password) && Password.Length >= 4;
    
    private async Task LoginAsync()
    {
        var result = await _mediator.Send(LoginRequest.WithCredentials(Username, Password))
                                    .ConfigureAwait(false);

        if (result.Succes)
            await NavigationService.Navigate<StartViewModel>()
                                   .ConfigureAwait(false);
        else
            ErrorMessage = result.Message;
    }
}

Klassischerweise würde man das doch so machen:

private async Task LoginAsync()
    {
        bool success = LoginService.Login(Username, Password);

        if (success)
            NavigationService.Navigate<StartViewModel>()
                                   .ConfigureAwait(false);
        else
            ErrorMessage = result.Message;
    }

Wenn ich das richtig verstehe, muss ich bei Verwendung des Mediators für jede Benutzerinteraktion einen Request und Response erstellen. Ist das nicht ein großer Overhead?

Gruß

16.835 Beiträge seit 2008
vor 5 Jahren

MediatR ist nichts anderes als ein Dependency Resolver. Es gibt technisch kein Request/Response. Das sind nur Bezeichner des Patterns, der hier verwendet wird: CQRS.
Es ist ein Event-gesteuertes, asynchrones System, das Abhängigkeiten reduziert - mit all den netten Vorteilen dieses Patterns aber ohne physikalische Queue, über die wirklich ein Senden stattfindet.
Schau es Dir einfach mal 15 Minuten an.. das würde viele Fragen auflösen 😉

Statt Business Logik (wie Dein LoginService) oder Repository-Inhalte in große Klassen zu schreiben, schreibt man Queries und QueryHandler bzw. Commands und CommandHandler.
Das Send ist nur ein Name, damit aus einem Query eben ein QueryHandler ausgeführt wird.
Der Overhead ist minimal - sicherlich nicht größer als das klassische Beispiel hier; der Benefit enorm - die Abhängigkeiten sinken auf ein Minimum und die Wiederverwendbarkeit wird enorm erhöht.

Ich nutzen MediatR quasi in all meinen/unseren aktuellen Projekten und verwende es auch in meinen Talks; zB https://github.com/BenjaminAbt/2018-Talks-ModernApiDevelopment/tree/master/src/Common.Engine

Dein klassisches Beispiel ist eigentlich genau das, was man vermeiden will:

  • blockierenden Code durch fehlende asynchrone Schnittstellen
  • die Abhängigkeit auf einen Service (hier LoginService); selbst der NavigationService sollte im MediatR Beispiel entfernt werden.
S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

Habe mir das mal angesehen. Der Vorteil ist ja, dass ich damit oft benutzte Queries wiederwenden kann.

Durch das Schreiben der Request/ResponseHandler (oder Query oder wie immer man das jetzt nennt) gibt es allerdings auch einen Mehraufwand bei der Implementierung.

Nicht klar ist mir, wie das Konzept UnitOfWork bzw. DbContext zusammen mit dem Mediator verwendet werden kann.

Gruß

6.911 Beiträge seit 2009
vor 5 Jahren

Hallo sugar76,

gibt es allerdings auch einen Mehraufwand bei der Implementierung.

Es hat halt alles Vor- und Nachteile. Kurzfristig mag es Mehraufwand sein, aber langfristig (mag) es sicher eine vorteilhafte Lösung sein.

Nicht klar ist mir, wie das Konzept UnitOfWork bzw. DbContext...

In Abts Demo-Projekt im Bereich InMemoryQueryHandlers gehört das angesiedelt. Er hat dort IAuthorsRepository injiziert (per Dependency Injection), du würdest hier den DbContext injizieren.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"