Il s'agit d'un exemple d'application de blazor qui permet de crud de personnes. Cela présente la pile suivante:
Voir l'application en cours d'exécution: Blazor-Wasm-crud Dot Fly Dot Dev 
dotnet ef (> = v7.0.13) dotnet tool install -- global dotnet - ef -- version 7.0 . 13Créez et migrez la base de données SQLite et démarrez le serveur.
.local.ps1 run Si c'est la première fois exécute l'application, cela créera .srcPeople.BlazorWasmServerpeople.db .
Afficher l'application en cours d'exécution sur https: // localhost: 7102

Exécutez les tests
.local.ps1 test.local.ps1 migrate Si vous apportez des modifications aux modèles de base de données, vous devrez créer une nouvelle migration. Cela créera un nouveau fichier de migration dans .srcPeople.InfrastructureMigrations .
.local.ps1 migration MyNewMigration Cette application est construite à l'aide du modèle d'hébergement WASM avec l'idée que le client devrait passer des appels HTTP vers une API REST. Je ne suis pas sûr que ce modèle d'utilisation d'une version WASM de HttpClient de .NET soit meilleur ou pire qu'un client basé sur JavaScript qui utilise fetch , mais il permet certainement de partager des modèles de demande / réponse entre le client et le serveur.
Un hoquet que j'ai rencontré est qu'il n'est pas trivial de passer des arguments aux gestionnaires d'événements. Par exemple, lorsque vous cliquez sur le bouton Supprimer pour supprimer une personne, je souhaite transmettre l'ID de la personne au gestionnaire d'événements. J'ai fini par utiliser une expression de lambda:
< button @onclick =" @((e) => DeletePerson(person.Id)) " > Delete </ button >Cela fonctionne, mais vient avec l'avertissement:
La création d'un grand nombre de délégués d'événements dans une boucle peut entraîner de mauvaises performances de rendu. Pour plus d'informations, consultez les meilleures pratiques ASP.NET Core Blazor Best Practices.
J'ai gardé l'interface utilisateur mondiale des erreurs, mais je voudrais améliorer cette expérience. Ceci est particulièrement visible lorsque nous vérifions les codes d'état de la réponse API avec
response . EnsureSuccessStatusCode ( ) ; ... mais je n'ai pas de moyen actuellement d'afficher un message utile à l'utilisateur sans introduire un bloc try/catch .
La principale chose à noter ici est que le contrôleur fait 2 choses:
IPeopleServiceLe modèle de base est:
[ 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 ) ) ) ;
} La réponse pour 400 et 200 étant de la même forme ( SomeResponse ) simplifie et ajoute de la flexibilité au client qui appelle l'API, car il peut toujours désérialiser la réponse dans le même type, puis peut soit (1) vérifier .error === true pour déterminer si la réponse est une erreur ou non, ou (2) vérifier le code d'état HTTP. La forme de cet objet retourné ressemble:
{
"error" : false ,
"errors" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} Cela permet une extension de la réponse à l'avenir si nécessaire. Par exemple, si nous voulions ajouter un tableau warnings , nous pourrions le faire sans que ce soit un changement de rupture pour le client.
{
"error" : false ,
"errors" : [],
"warnings" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} L' IPeopleService et la Person répondent également par OneOf types. Par exemple:
interface IPeopleService {
Result < PersonNotFound , Person > FindPerson ( Guid id ) ;
}et
class Person {
private Person ( ) { }
public static Result < PersonInvalid , Person > Create ( PersonRequest request ) {
if ( /* request is invalid */ ) {
return new PersonInvalid ( ) ;
}
return new Person ( ) {
// ...
} ;
}
}Cela rend le domaine et l'application explicites sur ce qui peut mal tourner. Cela force les appelants (application et contrôleur) pour gérer tous les scénarios de retour. Il empêche également de lancer des exceptions pour la validation, que je vois très souvent:
class Person {
public Person ( string name ) {
if ( string . IsNullOrWhiteSpace ( name ) ) {
// bad!
throw new PersonInvalidException ( ) ;
}
Name = name ;
}
} Les exceptions doivent être exceptionnelles. Si nous nous attendons à ce qu'une Person puisse être invalide, nous devons informer l'appelant en tant que tel. Cela vous oblige à ne pas utiliser le new mot-clé, mais à la place utilisez une méthode d'usine statique, mais c'est un compromis qui, je pense, en vaut la peine.
OneOf a des types de retour de base, mais je préfère créer mes propres types de retour avec C # Records, car ils ne sont généralement qu'une seule doublure et sont plus expressifs que Success de Oneof / NotFound / etc.
public record PersonNotFound ( Guid Id ) {
public string Message => $ "Person with ID { Id } not found" ;
}
public record PersonBirthCannotBeInFuture ( ) ;
public record PersonInvalid ( FieldErrors Errors ) ; Sans OneOf.Monads, le PeopleService devrait vérifier .IsT1 et utiliser .AsT0 et .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 ( ) ;
} Avec OneOf.Monads, nous obtenons un Result<Error, Success> Type qui s'enroule .IsT0 avec un .IsError() plus expressif:
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 ( ) ;
} Il y a beaucoup de discussions sur l'utilisation d'un DbContext directement comme référentiel, mais cela rend les tests unitaires plus difficiles car il vous oblige à créer DbSet simulé / faux. Pour cette application, j'ai préféré utiliser mon propre référentiel qui me permet de créer facilement un faux référentiel pour les tests unitaires.
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 ) ;
}
}SQLITE nous permet de se mettre en service rapidement sans avoir à installer un serveur de base de données. De plus, la version en mémoire est idéale pour les tests d'intégration, car il nous permet de toujours utiliser le fournisseur de base de données "réel", mais sans avoir à nous soucier de nettoyer la base de données après chaque test. Souvent, les tests d'intégration passeront à une base de données de base EF en mémoire, ce qui vous oblige à ne pas utiliser de fonctionnalités spécifiques à la base de données et modifie le mécanisme de stockage des données de support, donc lorsque vous exécutez les tests d'intégration de cette manière, vous ne testez pas vraiment le même magasin de données. Si vous utilisez une base de données comme MySQL ou Postgres, je créerais une application Docker Compose pour héberger une base de données de test.
J'ai câblé la journalisation pour les tests d'intégration avec ITestOutputHelper avec meziantou.extensions.logging.xunit. Cela permet de voir les journaux du serveur lors de l'exécution des tests d'intégration WebApplicationFactory .
La pratique recommandée ici est d'écrire votre Xunit [Fact] S dans des fichiers .razor . Cela semble étrange au début, mais permet une prise en charge de l'éditeur riche des composants Blazor.
L'exécution des commandes dotnet pour les migrations et l'exécution de l'application peut devenir fatigant de copier / coller, donc pour la plupart des projets, je crée un script de style "MakeFile" PowerShell pour exécuter des tâches de développement locales communes. J'ai blogué à ce sujet en détail ici: https://kevinareed.com/2021/04/14/creating-a-command à base de--in-in-powershell/.
