这是一个允许人crud的样本大发应用程序。这显示了以下堆栈:
请参阅运行应用程序:Blazor-Wasm-Crud Dot fly dot dev 
dotnet ef (> = = V7.0.13) dotnet tool install -- global dotnet - ef -- version 7.0 . 13创建和迁移SQLITE数据库,然后启动服务器。
.local.ps1 run如果这是第一次运行该应用程序,这将创建.srcPeople.BlazorWasmServerpeople.db 。
在https:// localhost:7102查看运行应用程序

运行测试
.local.ps1 test.local.ps1 migrate如果您更改数据库模型,则需要创建一个新的迁移。这将创建一个新的迁移文件.srcPeople.InfrastructureMigrations 。
.local.ps1 migration MyNewMigration该应用程序是使用WASM托管模型构建的,其想法是客户端应向REST API进行HTTP调用。我不确定这种使用.NET的HttpClient的WASM版本的模型比使用fetch的JavaScript客户端更好或更糟,但是它肯定允许在客户端和服务器之间共享请求/响应模型。
我遇到的一个打ic是,将争论传递给事件处理人员并不是很小的。例如,在单击“删除”按钮删除一个人时,我想将人的ID传递给活动处理程序。我最终使用了lambda表达式:
< button @onclick =" @((e) => DeletePerson(person.Id)) " > Delete </ button >这起作用,但带有警告:
在循环中创建大量的事件代表可能会导致渲染性能差。有关更多信息,请参见ASP.NET Core Blazor绩效最佳实践。
我保留了全球错误处理UI,但希望改善这种体验。当我们检查API响应状态代码时,这尤其明显
response . EnsureSuccessStatusCode ( ) ; ...但是当前没有办法向用户显示有用的消息而不引入try/catch块。
这里要注意的主要内容是控制器正在做2件事:
IPeopleService基本模式是:
[ 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 ) ) ) ;
} 400和200的响应具有相同的形状( SomeResponse )简化并为调用API的客户端增加了灵活性,因为它始终可以将响应验证为相同类型,然后可以(1)检查.error === true以确定响应是否是错误的,或(2)检查HTTP状态代码。该对象返回的形状看起来像:
{
"error" : false ,
"errors" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
}如有必要,这允许将来扩展响应。例如,如果我们想添加一个warnings阵列,我们可以这样做,而不会对客户进行破坏。
{
"error" : false ,
"errors" : [],
"warnings" : [],
"person" : {
"id" : " 00000000-0000-0000-0000-000000000000 " ,
"firstName" : " John " ,
"lastName" : " Doe "
}
}IPeopleService以及Person以OneOf类型的响应。例如:
interface IPeopleService {
Result < PersonNotFound , Person > FindPerson ( Guid id ) ;
}和
class Person {
private Person ( ) { }
public static Result < PersonInvalid , Person > Create ( PersonRequest request ) {
if ( /* request is invalid */ ) {
return new PersonInvalid ( ) ;
}
return new Person ( ) {
// ...
} ;
}
}这使得域和应用程序明确说明了什么问题。这迫使呼叫者(应用程序和控制器)处理所有返回方案。它还可以防止投掷验证例外,我经常看到这一点:
class Person {
public Person ( string name ) {
if ( string . IsNullOrWhiteSpace ( name ) ) {
// bad!
throw new PersonInvalidException ( ) ;
}
Name = name ;
}
}例外应该是例外。如果我们期望一个Person无效,那么我们应该这样通知呼叫者。这确实需要您不使用new关键字,而是使用静态工厂方法,但这是我认为值得的权衡。
Oneof具有一些基本的返回类型,但是我更喜欢使用C#记录创建自己的返回类型,因为它们通常只有一个衬里,并且比Oneof的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 ) ; 如果没有一个monads, PeopleService将需要检查.IsT1并使用.AsT0和.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 ( ) ;
}使用One.monads,我们将获得Result<Error, Success>将.IsT0包装的type tith .ist0更具表现力.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 ( ) ;
}直接将DbContext作为存储库进行了很多讨论,但这使单元测试更加困难,因为它要求您创建模拟/假DbSet 。对于此应用程序,我更喜欢使用自己的存储库,这使我可以轻松地为单元测试创建一个假存储库。
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让我们可以快速启动并运行,而无需安装数据库服务器。此外,内存版本非常适合集成测试,因为它允许我们仍然使用“真实”数据库提供商,但不必担心每次测试后都会清理数据库。通常,集成测试会切换到内存EF核心数据库,这要求您不使用任何数据库特定功能并更改备份数据存储机制,因此,当您以这种方式运行集成测试时,您并没有真正测试同一数据存储。如果使用MySQL或Postgres之类的数据库,我将创建一个Docker组成的应用程序来托管测试数据库。
我已经将LIGNG与Meziantou.extensions.logging.xunit的ITestOutputHelper进行了登录。这允许在运行WebApplicationFactory集成测试时从服务器中查看日志。
建议的做法是在.razor文件中编写您的xunit [Fact] s。一开始这感觉很奇怪,但可以提供大型构件的丰富编辑器支持。
运行用于迁移和运行应用程序的dotnet命令可能会变得累人/粘贴,因此,对于大多数项目,我创建了一个“ makefile”样式的powerShell脚本来运行常见的本地开发任务。我在此处详细介绍了这一点:https://kevinareed.com/2021/04/14/creating-a-command-cli-cli-cli-in-cli-in-powershell/。
