Ini adalah aplikasi blazor sampel yang memungkinkan orang banyak. Ini menampilkan tumpukan berikut:
Lihat aplikasi yang sedang berjalan: Blazor-Wasm-crud dot fly dot dev 
dotnet ef (> = V7.0.13) dotnet tool install -- global dotnet - ef -- version 7.0 . 13Buat dan migrasi database SQLite, dan mulai server.
.local.ps1 run Jika ini adalah pertama kalinya menjalankan aplikasi, ini akan membuat .srcPeople.BlazorWasmServerpeople.db .
Lihat aplikasi yang sedang berjalan di https: // localhost: 7102

Jalankan tes
.local.ps1 test.local.ps1 migrate Jika Anda membuat perubahan pada model database, Anda harus membuat migrasi baru. Ini akan membuat file migrasi baru di .srcPeople.InfrastructureMigrations .
.local.ps1 migration MyNewMigration Aplikasi ini dibangun menggunakan model hosting WASM dengan gagasan bahwa klien harus melakukan panggilan HTTP ke API REST. Saya tidak yakin bahwa model menggunakan versi WASM dari HttpClient .NET lebih baik atau lebih buruk daripada klien berbasis JavaScript yang menggunakan fetch , tetapi tentu saja memungkinkan model permintaan/respons berbagi antara klien dan server.
Salah satu cegukan yang saya temui adalah bahwa tidak sepele untuk memberikan argumen kepada penangan acara. Misalnya, saat mengklik tombol Hapus untuk menghapus seseorang, saya ingin meneruskan ID orang tersebut ke penangan acara. Saya akhirnya menggunakan ekspresi lambda:
< button @onclick =" @((e) => DeletePerson(person.Id)) " > Delete </ button >Ini berhasil, tetapi dilengkapi dengan peringatan:
Membuat sejumlah besar delegasi acara dalam satu lingkaran dapat menyebabkan kinerja rendering yang buruk. Untuk informasi lebih lanjut, lihat Asp.net Core Blazor Performance Praktik Terbaik.
Saya menjaga kesalahan penanganan kesalahan global, tetapi ingin meningkatkan pengalaman ini. Ini sangat terlihat ketika kami memeriksa kode status respons API dengan
response . EnsureSuccessStatusCode ( ) ; ... tetapi saat ini tidak memiliki cara untuk menampilkan pesan yang berguna kepada pengguna tanpa memperkenalkan blok try/catch .
Hal utama yang perlu diperhatikan di sini adalah bahwa pengontrol melakukan 2 hal:
IPeopleServicePola dasarnya adalah:
[ 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 ) ) ) ;
} Respons untuk 400 dan 200 berada dengan bentuk yang sama ( SomeResponse ) menyederhanakan dan menambahkan fleksibilitas pada klien yang memanggil API, karena selalu dapat deserialize respons ke jenis yang sama, dan kemudian dapat (1) memeriksa .error === true UNTUK MENYEDIAKAN apakah responsnya adalah kesalahan atau tidak, atau (2) memeriksa kode status HTTP. Bentuk objek ini kembali terlihat seperti:
{
"error" : false ,
"errors" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} Ini memungkinkan perluasan respons di masa depan jika perlu. Misalnya, jika kami ingin menambahkan array warnings , kami dapat melakukannya tanpa menjadi perubahan bagi klien.
{
"error" : false ,
"errors" : [],
"warnings" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
} IPeopleService dan juga Person merespons dengan OneOf jenis. Misalnya:
interface IPeopleService {
Result < PersonNotFound , Person > FindPerson ( Guid id ) ;
}Dan
class Person {
private Person ( ) { }
public static Result < PersonInvalid , Person > Create ( PersonRequest request ) {
if ( /* request is invalid */ ) {
return new PersonInvalid ( ) ;
}
return new Person ( ) {
// ...
} ;
}
}Ini membuat domain dan aplikasi eksplisit tentang apa yang bisa salah. Ini memaksa penelepon (aplikasi dan pengontrol) untuk menangani semua skenario pengembalian. Ini juga mencegah pengecualian untuk validasi, yang sangat sering saya lihat:
class Person {
public Person ( string name ) {
if ( string . IsNullOrWhiteSpace ( name ) ) {
// bad!
throw new PersonInvalidException ( ) ;
}
Name = name ;
}
} Pengecualian harus luar biasa. Jika kita berharap Person bisa tidak valid, maka kita harus memberi tahu penelepon seperti itu. Ini memang mengharuskan Anda untuk tidak menggunakan kata kunci new , tetapi menggunakan metode pabrik statis, tetapi itu adalah tradeoff yang menurut saya sepadan.
Oneof memiliki beberapa jenis pengembalian dasar, tetapi saya lebih suka membuat jenis pengembalian saya sendiri dengan catatan C#, karena mereka biasanya hanya satu liner dan lebih ekspresif daripada Success / NotFound / dll.
public record PersonNotFound ( Guid Id ) {
public string Message => $ "Person with ID { Id } not found" ;
}
public record PersonBirthCannotBeInFuture ( ) ;
public record PersonInvalid ( FieldErrors Errors ) ; Tanpa Oneof.monads, PeopleService perlu memeriksa .IsT1 dan menggunakan .AsT0 dan .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 ( ) ;
} Dengan OneOf.Monads, kami mendapatkan Result<Error, Success> ketik yang membungkus .IsT0 dengan .IsError() yang lebih ekspresif::
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 ( ) ;
} Ada banyak diskusi di sekitar menggunakan DbContext secara langsung sebagai repositori, tetapi ini membuat unit tes lebih sulit karena mengharuskan Anda membuat DbSet tiruan/palsu. Untuk aplikasi ini saya lebih suka menggunakan repositori saya sendiri yang memungkinkan saya untuk dengan mudah membuat repositori palsu untuk tes unit.
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 memungkinkan kami dengan cepat bangun dan berjalan tanpa harus menginstal server database. Juga, versi dalam memori sangat bagus untuk tes integrasi, karena memungkinkan kita untuk tetap menggunakan penyedia database "nyata", tetapi tanpa harus khawatir membersihkan database setelah setiap tes. Seringkali tes integrasi akan beralih ke database inti in memori EF, yang mengharuskan Anda untuk tidak menggunakan fitur spesifik database apa pun dan mengubah mekanisme penyimpanan data dukungan, jadi ketika Anda menjalankan tes integrasi dengan cara ini, Anda tidak benar -benar menguji penyimpanan data yang sama. Jika menggunakan database seperti MySQL atau Postgres, saya akan membuat aplikasi Docker Compose untuk meng -host database pengujian.
Saya telah menghubungkan logging untuk tes integrasi dengan ITestOutputHelper dengan meziantou.extensions.logging.xunit. Ini memungkinkan melihat log dari server saat menjalankan tes integrasi WebApplicationFactory .
Praktik yang disarankan di sini adalah untuk menulis [Fact] Anda di .razor Files. Ini terasa aneh pada awalnya, tetapi memungkinkan dukungan editor yang kaya dari komponen Blazor.
Menjalankan perintah dotnet untuk migrasi dan menjalankan aplikasi dapat menjadi melelahkan untuk disalin/ditempel, jadi untuk sebagian besar proyek saya membuat skrip PowerShell gaya "makefile" untuk menjalankan tugas dev lokal umum. Saya telah membuat blog tentang hal ini secara detail di sini: https://kevinareed.com/2021/04/14/creating-a-command berbasis cli-in-powershell/.
