Esta es una aplicación de blazor de muestra que permite crud de personas. Esto muestra la siguiente pila:
Consulte la aplicación en ejecución: blazor-wasm-crud dot Fly Dot Dev 
dotnet ef (> = V7.0.13) dotnet tool install -- global dotnet - ef -- version 7.0 . 13Cree y migre la base de datos SQLite e inicie el servidor.
.local.ps1 run Si esta es la primera vez que ejecuta la aplicación, esto creará .srcPeople.BlazorWasmServerpeople.db
Ver la aplicación en ejecución en https: // localhost: 7102

Ejecutar las pruebas
.local.ps1 test.local.ps1 migrate Si realiza cambios en los modelos de base de datos, deberá crear una nueva migración. Esto creará un nuevo archivo de migración en .srcPeople.InfrastructureMigrations .
.local.ps1 migration MyNewMigration Esta aplicación se crea utilizando el modelo de alojamiento WASM con la idea de que el cliente debe hacer llamadas HTTP a una API REST. No estoy seguro de que este modelo de usar una versión WASM de HttpClient de .NET sea mejor o peor que un cliente basado en JavaScript que usa fetch , pero ciertamente permite compartir modelos de solicitud/respuesta entre el cliente y el servidor.
Un hipo con el que me encontré es que no es trivial aprobar argumentos a los manejadores de eventos. Por ejemplo, al hacer clic en el botón Eliminar para eliminar a una persona, quiero pasar la identificación de la persona al controlador de eventos. Terminé usando una expresión de lambda:
< button @onclick =" @((e) => DeletePerson(person.Id)) " > Delete </ button >Esto funciona, pero viene con la advertencia:
Crear una gran cantidad de delegados de eventos en un bucle puede causar un bajo rendimiento de representación. Para obtener más información, consulte ASP.NET Core Blazor Performance Mejores prácticas.
Mantuve el error global manejo de la interfaz de usuario, pero me gustaría mejorar esta experiencia. Esto es especialmente notable cuando verificamos los códigos de estado de respuesta de la API con
response . EnsureSuccessStatusCode ( ) ; ... Pero no tenga una manera actualmente para mostrar un mensaje útil para el usuario sin introducir un bloque try/catch .
Lo principal a tener en cuenta aquí es que el controlador está haciendo 2 cosas:
IPeopleServiceEl patrón básico es:
[ 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 respuesta para 400 y 200 es de la misma forma ( SomeResponse ) simplifica y agrega flexibilidad al cliente que llama a la API, ya que siempre puede deserializar la respuesta al mismo tipo, y luego puede (1) verificar .error === true para determinar si la respuesta es un error o no, o (2) verificar el código de estado HTTP. La forma de este objeto devuelta se parece:
{
"error" : false ,
"errors" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} Esto permite la extensión de la respuesta en el futuro si es necesario. Por ejemplo, si quisiéramos agregar una matriz warnings , podríamos hacerlo sin que fuera un cambio de ruptura para el cliente.
{
"error" : false ,
"errors" : [],
"warnings" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} El IPeopleService y también la Person responde con OneOf los tipos. Por ejemplo:
interface IPeopleService {
Result < PersonNotFound , Person > FindPerson ( Guid id ) ;
}y
class Person {
private Person ( ) { }
public static Result < PersonInvalid , Person > Create ( PersonRequest request ) {
if ( /* request is invalid */ ) {
return new PersonInvalid ( ) ;
}
return new Person ( ) {
// ...
} ;
}
}Esto hace que el dominio y la aplicación sean explícitos sobre lo que puede salir mal. Esto obliga a las personas que llaman (aplicación y controlador) a manejar todos los escenarios de devolución. También evita lanzar excepciones para la validación, que veo muy a menudo:
class Person {
public Person ( string name ) {
if ( string . IsNullOrWhiteSpace ( name ) ) {
// bad!
throw new PersonInvalidException ( ) ;
}
Name = name ;
}
} Las excepciones deben ser excepcionales. Si esperamos que una Person pueda ser inválida, entonces debemos informar a la persona que llama como tal. Esto requiere que no use la new palabra clave, sino que use un método de fábrica estática, pero es una compensación que creo que vale la pena.
OneF tiene algunos tipos de devolución básicos, pero prefiero crear mis propios tipos de devolución con los registros de C#, ya que generalmente son solo un revestimiento y son más expresivos que el Success / NotFound / Etc.
public record PersonNotFound ( Guid Id ) {
public string Message => $ "Person with ID { Id } not found" ;
}
public record PersonBirthCannotBeInFuture ( ) ;
public record PersonInvalid ( FieldErrors Errors ) ; Sin unof.monads, el PeopleService necesitaría verificar .IsT1 y usar .AsT0 y .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 ( ) ;
} Con OneOf.Monads, obtenemos un Result<Error, Success> tipo que envuelve .IsT0 con un .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 ( ) ;
} Hay mucha discusión sobre el uso de un DbContext directamente como un repositorio, pero esto hace que las pruebas unitarias sean más difíciles, ya que requiere que cree simulacro/ DbSet falso. Para esta aplicación, preferí usar mi propio repositorio que me permite crear fácilmente un repositorio falso para las pruebas unitarias.
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 nos permite ponernos rápidamente en funcionamiento sin tener que instalar un servidor de base de datos. Además, la versión en memoria es excelente para las pruebas de integración, ya que nos permite usar el proveedor de la base de datos "real", pero sin tener que preocuparse por limpiar la base de datos después de cada prueba. A menudo, las pruebas de integración cambiarán a una base de datos en memoria EF Core, que requiere que no use ninguna característica específica de la base de datos y cambie el mecanismo de almacenamiento de datos de respaldo, por lo que cuando ejecuta pruebas de integración de esta manera, realmente no está probando el mismo almacén de datos. Si usa una base de datos como MySQL o Postgres, crearía una aplicación Docker Compose para alojar una base de datos de prueba.
He conectado el registro para las pruebas de integración con ITestOutputHelper con Meziantou.extensions.logging.xunit. Esto permite ver los registros desde el servidor al ejecutar las pruebas de integración WebApplicationFactory .
La práctica recomendada aquí es escribir su XUnit [Fact] s en archivos .razor . Esto se siente extraño al principio, pero permite el soporte de editor rico de los componentes Blazor.
Ejecutar los comandos dotnet para migraciones y ejecutar la aplicación puede ser agotador para copiar/pegar, por lo que para la mayoría de los proyectos creo un script PowerShell de estilo "MakeFile" para ejecutar tareas de desarrollo locales comunes. He blogueado sobre esto en detalle aquí: https://kevinareed.com/2021/04/14/creating-a-command basado en cli-in-powershell/.
