Dies ist eine Probe -Blazor -Anwendung, die CRUD von Personen ermöglicht. Dies zeigt den folgenden Stack:
Siehe die laufende Anwendung: Blazor-Wasm-Crud Dot Fly Dot Dev 
dotnet ef (> = v7.0.13) installieren dotnet tool install -- global dotnet - ef -- version 7.0 . 13Erstellen und migrieren Sie die SQLite -Datenbank und starten Sie den Server.
.local.ps1 run Wenn dies das erste Mal ausführt, dass die Anwendung ausgeführt wird, wird dies erstellt .srcPeople.BlazorWasmServerpeople.db .
Sehen Sie sich die laufende Anwendung unter https: // localhost: 7102 an

Führen Sie die Tests durch
.local.ps1 test.local.ps1 migrate Wenn Sie Änderungen an den Datenbankmodellen vornehmen, müssen Sie eine neue Migration erstellen. Dadurch wird eine neue Migrationsdatei in .srcPeople.InfrastructureMigrations erstellt.
.local.ps1 migration MyNewMigration Diese Anwendung wird mit dem WASM -Hosting -Modell erstellt, mit der Idee, dass der Client HTTP -Anrufe an eine REST -API tätigen sollte. Ich bin mir nicht sicher, ob dieses Modell der Verwendung einer WASM -Version von .NETs HttpClient besser oder schlechter ist als ein auf JavaScript -basierter Client, der fetch verwendet, aber es ermöglicht sicherlich die Freigabe von Anforderungs-/Antwortmodellen zwischen Client und Server.
Ein Schluckauf, den ich begegnet bin, ist, dass es nicht trivial ist, Argumente an Event -Handler weiterzugeben. Wenn Sie beispielsweise auf die Schaltfläche Löschen klicken, um eine Person zu löschen, möchte ich die ID der Person an den Event -Handler weitergeben. Am Ende habe ich einen Lambda -Ausdruck verwendet:
< button @onclick =" @((e) => DeletePerson(person.Id)) " > Delete </ button >Dies funktioniert, ist aber mit der Warnung verbunden:
Eine große Anzahl von Ereignisdelegierten in einer Schleife kann zu einer schlechten Rendering -Leistung führen. Weitere Informationen finden Sie in Best Practices ASP.NET Core Blazor Performance.
Ich habe die globale Fehlerbehebung der Fehlerbehandlung beibehalten, möchte aber diese Erfahrung verbessern. Dies ist insbesondere bei der Überprüfung der API -Antwortstatus -Codes mit dem API -Antwort -Status mit
response . EnsureSuccessStatusCode ( ) ; ... aber haben derzeit keinen Weg, dem Benutzer eine nützliche Nachricht anzuzeigen, ohne einen try/catch -Block vorzustellen.
Die Hauptsache, die hier zu beachten ist, ist, dass der Controller zwei Dinge tut:
IPeopleService anDas Grundmuster ist:
[ HttpPost ]
public IResult HandleSomePost ( [ FromBody ] SomeRequest request )
{
var result = _peopleService . DoSomething ( request ) ;
return result . Match (
( error ) => Results . BadRequest ( new SomeResponse ( error . Value ) ) ) ,
( success ) => Results . Ok ( new SomeResponse ( success . Value ) ) ) ;
} Die Antwort für 400 und 200, die die gleiche Form ( SomeResponse ) haben, vereinfacht dem Client, der die API aufruft, da sie die Antwort immer in denselben Typ deserialisieren kann, und kann entweder (1) auf .error === true , um festzustellen, ob die Antwort ein Fehler ist oder nicht, oder (2) den HTTP -Statuscode. Die Form dieses zurückgegebenen Objekts sieht aus wie:
{
"error" : false ,
"errors" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} Dies ermöglicht die Erweiterung der Antwort in Zukunft bei Bedarf. Wenn wir beispielsweise ein warnings hinzufügen wollten, könnten wir dies tun, ohne dass es für den Kunden eine brichtliche Veränderung ist.
{
"error" : false ,
"errors" : [],
"warnings" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} Der IPeopleService und auch die Person antworten mit OneOf Typ. Zum Beispiel:
interface IPeopleService {
Result < PersonNotFound , Person > FindPerson ( Guid id ) ;
}Und
class Person {
private Person ( ) { }
public static Result < PersonInvalid , Person > Create ( PersonRequest request ) {
if ( /* request is invalid */ ) {
return new PersonInvalid ( ) ;
}
return new Person ( ) {
// ...
} ;
}
}Dadurch wird die Domain und die Anwendung explizit darüber, was schief gehen kann. Dies zwingt die Anrufer (Anwendung und Controller), alle Rückgabeszenarien zu verarbeiten. Es verhindert auch, dass Ausnahmen zur Validierung geworfen werden, die ich sehr oft sehe:
class Person {
public Person ( string name ) {
if ( string . IsNullOrWhiteSpace ( name ) ) {
// bad!
throw new PersonInvalidException ( ) ;
}
Name = name ;
}
} Ausnahmen sollten außergewöhnlich sein. Wenn wir davon ausgehen, dass eine Person ungültig ist, sollten wir den Anrufer als solche informieren. Auf diese Weise müssen Sie das new Schlüsselwort nicht verwenden, sondern eine statische Fabrikmethode, aber es ist ein Kompromiss, den ich denke, der es wert ist.
Oneof hat einige grundlegende Rückgabetypen, aber ich ziehe es vor, meine eigenen Rückgabetypen mit C# -Datensätzen zu erstellen, da sie normalerweise nur ein Liner sind und ausdrucksvoller sind als Success von einem OFOF / NotFound .
public record PersonNotFound ( Guid Id ) {
public string Message => $ "Person with ID { Id } not found" ;
}
public record PersonBirthCannotBeInFuture ( ) ;
public record PersonInvalid ( FieldErrors Errors ) ; Ohne eins müsste der PeopleService .IsT1 und verwenden .AsT0 und .AsT1 .
public OneOf < PersonAddError , PersonResponse > AddPerson ( PersonAddEditRequest request )
{
var result = Person . Add ( _clock , request ) ;
if ( result . IsT0 )
{
return new PersonAddError ( result . AsT0 ) ;
}
var person = result . AsT1 ;
_repository . InsertPerson ( person ) ;
return person . ToPersonResponse ( ) ;
} Bei Oneof.Monads erhalten wir ein Result<Error, Success> Typ, der .IsT0 mit einem ausdrucksstärkeren .IsError() :
public Result < PersonAddError , PersonResponse > AddPerson ( PersonAddEditRequest request )
{
var result = Person . Add ( _clock , request ) ;
if ( result . IsError ( ) )
{
return new PersonAddError ( result . ErrorValue ( ) ) ;
}
var person = result . SuccessValue ( ) ;
_repository . InsertPerson ( person ) ;
return person . ToPersonResponse ( ) ;
} Es gibt viele Diskussionen darüber, einen DbContext direkt als Repository zu verwenden. Dies erschwert die Unit -Tests, da Sie erforderlich sind, um Schein-/gefälschte DbSet zu erstellen. Für diese Anwendung habe ich es vorgezogen, mein eigenes Repository zu verwenden, mit dem ich einfach ein gefälschtes Repository für Unit -Tests erstellen kann.
interface IPeopleRepository {
void Insert ( Person person ) ;
// ...
}
class PeopleEfRepository : IPeopleRepository {
public PeopleEfRepository ( PeopleDbContext db ) {
_db = db ;
}
public void Insert ( Person person ) {
_db . People . Add ( person ) ;
_db . SaveChanges ( ) ;
}
}
class FakePeopleRepository : IPeopleRepository {
public List < Person > People { get ; } = new ( ) ;
public void Insert ( Person person ) {
People . Add ( person ) ;
}
}Mit SQLITE können wir schnell zum Laufen kommen, ohne einen Datenbankserver installieren zu müssen. Außerdem eignet sich die In-Memory-Version für Integrationstests hervorragend, da wir es uns ermöglichen, noch den "Real" -Datenbankanbieter zu verwenden, ohne sich jedoch Sorgen um die Reinigung der Datenbank nach jedem Test zu machen. Oft wechseln Integrationstests in eine in Speicher -EF -Kerndatenbank, sodass Sie keine Datenbankspezifikationsfunktionen verwenden und den Sicherungsdatenspeichermechanismus ändern müssen. Wenn Sie also Integrationstests auf diese Weise ausführen, testen Sie nicht wirklich denselben Datenspeicher. Wenn ich eine Datenbank wie MySQL oder Postgres verwenden würde, würde ich eine Docker -Komponierungsanwendung erstellen, um eine Testdatenbank zu hosten.
Ich habe mich für die Integrationstests mit ITestOutputHelper mit meziantou.extensions.logging.xunit verkabelt. Auf diese Weise können die Protokolle vom Server beim Ausführen der WebApplicationFactory -Integrationstests angezeigt werden.
Die empfohlene Praxis hier besteht darin, Ihre Xunit [Fact] in .razor -Dateien zu schreiben. Dies fühlt sich zunächst seltsam an, ermöglicht aber die Unterstützung der reichhaltigen Redakteur der Blazor -Komponenten.
Das Ausführen der dotnet -Befehle für Migrationen und das Ausführen der Anwendung kann anstrengend werden, zu kopieren/einzufügen. Für die meisten Projekte erstelle ich ein PowerShell -Skript "Makefile" -Stil, um gemeinsame lokale Entwickleraufgaben auszuführen. Ich habe hier ausführlich darüber gebloggt: https://kevinareed.com/2021/04/14/creeating-a-command-basierte-cli-in-powersshell/.
