Laden...

Error handling im CQRS Pattern (MediatR) (asp.net core)

Erstellt von Olii vor 5 Jahren Letzter Beitrag vor 5 Jahren 2.480 Views
O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 5 Jahren
Error handling im CQRS Pattern (MediatR) (asp.net core)

Hallo liebe User,

bislang habe ich mich immer vor diesem Thema gedrückt, aber nun will ich da mal richtig ran. Einen Error zu handeln ist meist ja gar nicht das Problem, sonder einen Error richtig zu handeln oder diese entsprechend abzufangen.

Was wäre der korrekte weg in dem SQRS Pattern im handle Fehler richtig zu behandeln?

Ein kleines Beispiel:

Eine Funktion zum registrieren eines Nutzers. In dieser Funktion wird geprüft ob es einen user mit dem selben Namen schon einmal gibt. Wenn nein, dann registriere den Nutzer und gib seine ID zurück, wenn ja, dann gib einen leeren Guid zurück (alle ID's sind guids).

Ein leerer Guid ist aber ein valider Guid. Wie könnte man in solchen Fällen am besten vorgehen?

public async Task<Guid> Handle(CommandRegister command, CancellationToken cancellationToken)
    {
        List<Condition> ConditionList = new List<Condition>();
        ConditionList.Add (new Condition("username","=",command.username, DbType.String));
        ConditionList.Add (new Condition("email","=",command.email, DbType.String));
        if (_baseCommandRepository.GetSingle<int>("Count(*)","applicationuser",ConditionList) > 0)
        {
            return Guid.Empty; //ToDo leerer guid ist trotzdem ein valider guid
        }

        return await Task.Run(() => _baseCommandRepository.InsertReturnGuid<CommandRegister>("applicationuser",command, "iduser"));
    }

Ich habe mir ein paar Möglichkeiten überlegt:

  1. Ich prüfe in der Funktion darüber ab ob der return value == Guid.Empty

  2. Ich splitte die Funktion in zwei und gebe einen bool zurück bei der Überprüfung ob ein User existiert. Und anhand dessen kann ich dann ein z.B. BadRequest() an den Client senden. Müsste aber die commands etc. kopieren.

  3. oder ich prüfe auf den string "0000-0000-0000-0000" ab, was aber eigentlich wie Möglichkeit Nr. 1 ist

  4. Man könnte den User aber auch im ungewissen lassen und ihm bei Fehlern gar nicht bescheid geben

Aber keiner dieser Möglichkeiten scheint mir elegant genug.
Ich möchte nicht einfach programmieren lernen... Ich will es richtig lernen und es gut machen.

Falls jemand Vorschläge hat, würde ich mich sehr freuen oder auch auf eine rege Diskussion 😃

16.806 Beiträge seit 2008
vor 5 Jahren

Errorhandling allgemein:
CQRS wird Dir diese Frage an für sich nicht beantworten, sondern die Implementierung.
Der CQRS Pattern selbst kennt nur eine Kommunikationsrichtung; und in diese gehen auch Exceptions. Im Falle von IMediatR, also einer sehr vereinfachten In-Process Implementerung, kannst Du ganz normal mit Exceptions und Rückgaben arbeiten.
Du hast hier aber kein wirkliches Errorhandling im Sinne von Exceptions - sondern nur Logik.

Was ist denn das für eine Message? CreateUserMessage? Oder nur Exists-Prüfung?

PS: Der Code ist wirklich sehr unsauber.
Eine riesige Menge an Magic Strings -> unbedingt vermeiden; suboptimales async/await..
Vermutlich wäre bei einer korrekten Implementierung von DB und Logik der Fall gar nicht vorhanden.
Was für eine DB ist das denn? Wieso verwendest Du nicht ADO.NET? Oder ist das ein eigener Wrapper?

Punkt 4 darf niemals passieren.

Ich möchte nicht einfach programmieren lernen... Ich will es richtig lernen und es gut machen.

Es wird fast nie nur eine Lösung geben.

PPS: es ist eine Methode, keine Funktion.

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

Also diese Magic Strings habe ich gemacht weil ich versucht habe sowas wie ein SQL-Builder zu machen. Ich gebe Parameter rein und daraus wird mir ein SQL generiert und gegen die Datenbank gefeuert.

Der Grund wieso ich das machen wollte ist, dass ich nicht in jeden Handler den DBContext übergeben wollte sonder nur in diesen SQLBuilder. Ich dachte mir ads es ne gute Sache sei aber bin mir mitlerweile auch nicht mehr ganz so sicher weil es immer unleserlicher und relativ unflexible ist. Aber was anderes auser Magic Strings zu bauen ist mir nicht eingefallen.

Ich benutze kein ADO.NET weil ich DAPPER schon im Projekte hatte und dachte das DAPPER nicht schlecht ist. Also habe ich den benutzt. Oder ist Dapper eher ungeeignet?

Und als Datenbank verwende ich PostgreSQL.

Dieser Handler sollte überprüfen ob ein User in der Datenbank vorhanden ist. Wenn nein, hat er den User angelegt. An sich funktioniert das auch, aber ob die umsetzung schön ist ist eine andere Sache 😄

Um ehrlich zu sein habe ich mich mit Asyn noch garnicht beschäftigt. Habe ich nur so schon mal eingebaut damit kein Fehler kommt.

16.806 Beiträge seit 2008
vor 5 Jahren

Ich gebe Parameter rein und daraus wird mir ein SQL generiert und gegen die Datenbank gefeuert.

Das geht auch problemlos und typsicher mit Linq (oder anderen Varianten, die auf Expression basieren).

Der Grund wieso ich das machen wollte ist, dass ich nicht in jeden Handler den DBContext übergeben wollte sonder nur in diesen SQLBuilder.

Damit raubst Du Dir Flexibilität.

Ich benutze kein ADO.NET weil ich DAPPER schon im Projekte hatte und dachte das DAPPER nicht schlecht ist. Also habe ich den benutzt. Oder ist Dapper eher ungeeignet?

Ich persönlich bin ein absoluter Fan von Dapper, wenn es um relationale Datenbanken geht.
Daher bleib ruhig bei Dapper!
Ich würde aber ein Repository Pattern davor setzen, um die Typsicherheit zu gewährleisten. Damit wärst Du auch effizienter in Sachen Wiederverwendbarkeit als diese SQLBuilder-Konstruktion.

Deine Handler bekommen dann ein Repository injiziert.

Dieser Handler sollte überprüfen ob ein User in der Datenbank vorhanden ist. Wenn nein, hat er den User angelegt. An sich funktioniert das auch, aber ob die umsetzung schön ist ist eine andere Sache 😄

Dann würde ich hier keine Überprüfung im Code machen (Gefahr von Race Conditions), sondern einfach versuchen den User anzulegen.
Wenn der Context dann eine Exception wirft, dass zB die E-Mail bereits existiert, dann hast Du Race-Condition-Safe Deine Exist-Prüfung bereits.

Eine UserAlreadyExistException kannst Du anschließend in der Fehlerbehandlung des Handlers werfen.

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

Ich würde aber ein Repository Pattern davor setzen, um die Typsicherheit zu gewährleisten. Damit wärst Du auch effizienter in Sachen Wiederverwendbarkeit als diese SQLBuilder-Konstruktion.

Also quasi verschachteln von beiden Pattern. Das hört sich interessant an. Das versuche mal.

Danke für die Tips Abt. Ich probiere mal die Dinge umzusetzen und gebe hier dann nochmal ein Kommentar dazu für Leute die es vielleicht interessiert.

16.806 Beiträge seit 2008
vor 5 Jahren

Eine Software besteht i.d.R. immer aus einer Vielzahl von Pattern.

Im Prinzip kannst Du auch alles mit Handler abbilden und quasi die Handler kleiner schneiden - kann auch sinn machen.
Sodass Handler "CreateUser" eben ein "AddUserToDatabaseMessage" aufruft statt ein UserRepository.Add()

Müsste man die Gesamtsituation kennen. Tendiere aber zum Repositoryweg: der ist einfach simpler und erfüllt den Zweck genauso.

Aber kannst Du auch später ändern, sollte das notwendig sein.
Eine Architektur entwickelt sich wie alle anderen Teile einer Software schließlich auch weiter.