Это образец Blazor Application, которое позволяет Crud людей. Это демонстрирует следующий стек:
См. Рабочее приложение: Blazor-Wasm-Crud Dot Dot Dot Dev 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 с идеей, что клиент должен делать HTTP -вызовы в API REST. Я не уверен, что эта модель использования версии HttpClient от WASM лучше или хуже, чем клиент, основанный на JavaScript, который использует fetch , но, безусловно, позволяет обмен запрос/ответами модели между клиентом и сервером.
Один из тех, с кем я столкнулся, заключается в том, что не тривиально передавать аргументы для обработчиков событий. Например, нажав кнопку «Удалить», чтобы удалить человека, я хочу передать идентификатор человека в обработчик событий. В итоге я использовал выражение лямбды:
< button @onclick =" @((e) => DeletePerson(person.Id)) " > Delete </ button >Это работает, но идет с предупреждением:
Создание большого количества делегатов событий в цикле может привести к плохой производительности рендеринга. Для получения дополнительной информации см. Лучшие практики ASP.NET Core Blazor Performance.
Я сохранил глобальный пользовательский интерфейс, но хотел бы улучшить этот опыт. Это особенно заметно, когда мы проверяем коды состояния ответа 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 ключевое слово, а вместо этого используете статический метод фабрики, но это компромисс, который, я думаю, стоит того.
У одного есть некоторые основные типы возврата, но я предпочитаю создавать свои собственные типы возврата с помощью C# Records, поскольку они обычно только один вкладыша и более выразительны, чем Success OneOf / NotFound / и т. Д.
public record PersonNotFound ( Guid Id ) {
public string Message => $ "Person with ID { Id } not found" ;
}
public record PersonBirthCannotBeInFuture ( ) ;
public record PersonInvalid ( FieldErrors Errors ) ; Без OneOf.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 ( ) ;
} С OneOf.Monads мы получаем Result<Error, Success> тип, который обертывает .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 EF в памяти, которая требует, чтобы вы не использовали какие -либо функции, специфичные для базы данных, и изменяют механизм хранения данных, поэтому, когда вы запускаете интеграционные тесты таким образом, вы на самом деле не тестируете один и тот же хранилище данных. Если я использую такую базу данных, как MySQL или Postgres, я бы создал приложение Docker Compose для размещения тестовой базы данных.
Я подключил журнал для интеграционных тестов с ITestOutputHelper с meziantou.extensions.logging.xunit. Это позволяет видеть журналы с сервера при запуске тестов интеграции WebApplicationFactory .
Рекомендуемая практика здесь состоит в том, чтобы написать свой XUNIT [Fact] в .razor Сначала это кажется странным, но позволяет богатым редактором поддержки компонентов Blazor.
Запуск команд dotnet для миграций и запуска приложения может стать утомительным для копирования/вставки, поэтому для большинства проектов я создаю сценарий «Makefile» PowerShell для выполнения общих локальных задач DEV. Я подробно рассказывал об этом здесь: https://kevinareed.com/2021/04/14/creating-a-command на основе cli-in-powershell/.
