Este é um aplicativo de amostra de Blazor que permite crudação de pessoas. Isso mostra a seguinte pilha:
Veja o aplicativo em execução: Blazor-Wasm-Crud Dot Fly Dot Dev 
dotnet ef (> = V7.0.13) dotnet tool install -- global dotnet - ef -- version 7.0 . 13Crie e migre o banco de dados SQLite e inicie o servidor.
.local.ps1 run Se for a primeira vez que executa o aplicativo, isso criará .srcPeople.BlazorWasmServerpeople.db .
Veja o aplicativo em execução em https: // localhost: 7102

Execute os testes
.local.ps1 test.local.ps1 migrate Se você fizer alterações nos modelos de banco de dados, precisará criar uma nova migração. Isso criará um novo arquivo de migração .srcPeople.InfrastructureMigrations .
.local.ps1 migration MyNewMigration Este aplicativo foi criado usando o modelo de hospedagem WASM com a ideia de que o cliente deve fazer chamadas HTTP para uma API REST. Não tenho certeza se esse modelo de uso de uma versão WASM do .NET HttpClient é melhor ou pior do que um cliente baseado em JavaScript que usa fetch , mas certamente permite compartilhar modelos de solicitação/resposta entre o cliente e o servidor.
Um soluço que encontrei é que não é trivial passar argumentos para os manipuladores de eventos. Por exemplo, ao clicar no botão Excluir para excluir uma pessoa, quero passar o ID da pessoa para o manipulador de eventos. Acabei usando uma expressão lambda:
< button @onclick =" @((e) => DeletePerson(person.Id)) " > Delete </ button >Isso funciona, mas vem com o aviso:
Criar um grande número de delegados de eventos em um loop pode causar um desempenho ruim de renderização. Para obter mais informações, consulte as melhores práticas do ASP.NET Core Blazor Performance.
Eu mantive a interface do usuário global de lidar com erros, mas gostaria de melhorar essa experiência. Isso é especialmente perceptível quando verificamos códigos de status de resposta da API com
response . EnsureSuccessStatusCode ( ) ; ... mas não tem uma maneira de exibir uma mensagem útil para o usuário sem introduzir um bloco try/catch .
O principal a ser observado aqui é que o controlador está fazendo 2 coisas:
IPeopleServiceO padrão básico é:
[ 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 ) ) ) ;
} A resposta para 400 e 200, sendo a mesma forma ( SomeResponse ), simplifica e adiciona flexibilidade ao cliente que chama a API, pois sempre pode desserializar a resposta no mesmo tipo e, em seguida, pode (1) verificar o código .error === true para determinar se a resposta é um erro ou não, ou (2) verifique o código HTTP. A forma desse objeto retornou parece:
{
"error" : false ,
"errors" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} Isso permite a extensão da resposta no futuro, se necessário. Por exemplo, se quiséssemos adicionar uma matriz de warnings , poderíamos fazê -lo sem ser uma mudança de ruptura para o cliente.
{
"error" : false ,
"errors" : [],
"warnings" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} O IPeopleService e a Person também respondem com OneOf tipos. Por exemplo:
interface IPeopleService {
Result < PersonNotFound , Person > FindPerson ( Guid id ) ;
}e
class Person {
private Person ( ) { }
public static Result < PersonInvalid , Person > Create ( PersonRequest request ) {
if ( /* request is invalid */ ) {
return new PersonInvalid ( ) ;
}
return new Person ( ) {
// ...
} ;
}
}Isso torna o domínio e a aplicação explícitos sobre o que pode dar errado. Isso força os chamadores (aplicativo e controlador) a lidar com todos os cenários de retorno. Também impede que as exceções de validação de arremesso, que eu vejo com muita frequência:
class Person {
public Person ( string name ) {
if ( string . IsNullOrWhiteSpace ( name ) ) {
// bad!
throw new PersonInvalidException ( ) ;
}
Name = name ;
}
} Exceções devem ser excepcionais. Se esperamos que uma Person possa ser inválida, devemos informar o chamador como tal. Isso exige que você não use a new palavra -chave, mas use um método estático de fábrica, mas é uma troca que eu acho que vale a pena.
Oneof tem alguns tipos básicos de retorno, mas eu prefiro criar meus próprios tipos de retorno com registros C#, pois geralmente são apenas um revestimento e são mais expressivos do que o Success / NotFound / etc. da One of One Of.
public record PersonNotFound ( Guid Id ) {
public string Message => $ "Person with ID { Id } not found" ;
}
public record PersonBirthCannotBeInFuture ( ) ;
public record PersonInvalid ( FieldErrors Errors ) ; Sem oneof.monads, o PeopleService precisaria verificar .IsT1 e usar .AsT0 e .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 ( ) ;
} Com o Oneof.Monads, obtemos um Result<Error, Success> tipo que envolve .IsT0 com um .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 ( ) ;
} Há muita discussão sobre o uso de um DbContext diretamente como um repositório, mas isso torna os testes de unidade mais difíceis, pois exige que você crie DbSet simulado/falso. Para este aplicativo, preferia usar meu próprio repositório, o que me permite criar facilmente um repositório falso para testes de unidade.
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 ) ;
}
}O SQLITE permite que rapidamente suba e funcionando sem precisar instalar um servidor de banco de dados. Além disso, a versão na memória é ótima para testes de integração, pois nos permite ainda usar o provedor de banco de dados "real", mas sem precisar se preocupar em limpar o banco de dados após cada teste. Frequentemente, os testes de integração alternam para um banco de dados de núcleo EF de memória, que exige que você não use nenhum recurso específico do banco de dados e altere o mecanismo de armazenamento de dados de backing; portanto, quando você executa testes de integração dessa maneira, você não está realmente testando o mesmo armazenamento de dados. Se estiver usando um banco de dados como o MySQL ou o PostGres, eu criaria um aplicativo Docker Compose para hospedar um banco de dados de teste.
Eu conectei o log para os testes de integração com ITestOutputHelper com meziantou.extensions.logging.xunit. Isso permite ver os logs do servidor ao executar os testes de integração WebApplicationFactory .
A prática recomendada aqui é escrever seu xunit [Fact] nos arquivos .razor . Isso parece estranho no começo, mas permite o suporte rico do editor dos componentes do Blazor.
A execução dos comandos dotnet para migrações e executando o aplicativo pode se cansar para copiar/colar; portanto, para a maioria dos projetos, crio um script de estilo PowerShell de estilo "makefile" para executar tarefas comuns de desenvolvimento local. Eu escrevi sobre isso em detalhes aqui: https://kevinareed.com/2021/04/14/creating-a-command baseado em cli-in-in-powershell/.
