Sem SaveChanges
O pipeline de unit of work abre a transação, salva uma vez depois que o handler retorna e faz commit. Três escritas em três tabelas viram uma mudança atômica.
Organize comandos, consultas e eventos com handlers focados nas regras de negócio. O Nexo integra validação, persistência, transações e eventos de domínio em um fluxo consistente.
O handler inteiro
Nenhum repositório para escrever, nenhum SaveChanges para lembrar, nenhuma transação para abrir, nenhum evento para publicar à mão.
public sealed class CreateProductHandler(
ICreate<Product> create,
IReadByExternalId<Category> categories)
: ICommandHandler<CreateProduct, Guid>
{
public async Task<Guid> HandleAsync(CreateProduct command, CancellationToken ct)
{
var category = await categories.ReadRequiredAsync(command.CategoryId, ct);
var product = await create.CreateAsync(
new Product(category.Id, command.Sku, command.Price),
ct);
return product.ExternalId;
// Sem SaveChanges. Sem transação. Sem publicar evento.
// O pipeline abre a transação, salva uma vez, despacha os eventos
// de domínio e faz commit. Se algo falhar, nada é gravado.
}
}Instalação
dotnet add package Nexo.EntityFrameworkCore
dotnet add package Nexo.FluentValidationbuilder.Services.AddDbContext<CatalogDbContext>((sp, options) => options
.UseSqlServer(connectionString)
.UseNexo(sp));
builder.Services.AddNexo<CatalogDbContext>(nexo => nexo
.AddHandlersFromAssemblyOf<Program>()
.AddValidatorsFromAssemblyOf<Program>()
.UseCurrentUser<HttpCurrentUserAccessor>());AddNexo<TContext> monta os pipelines na ordem certa: logging, enriquecimento, validação e unit of work — este por último, encostado no handler, para que a transação cubra só o handler e seus eventos.
O que você ganha
O pipeline de unit of work abre a transação, salva uma vez depois que o handler retorna e faz commit. Três escritas em três tabelas viram uma mudança atômica.
ICreate<T>, IUpdate<T>, IDelete<T>, IQueryable<T> e as leituras por chave, injetadas direto. O handler declara no construtor exatamente o que ele pode fazer.
A entidade registra com Raise; o pipeline coleta depois do save — quando as chaves já existem — e despacha na mesma transação.
Implemente IHaveAudit ou ISoftDeletable e o interceptor cuida do resto. Uma entidade que não implementa nada segue um POCO puro.
FluentValidation roda antes do handler, e a falha chega como ValidationFailedException. Sua camada web nunca referencia FluentValidation.
PageRequest faz clamp do que vem da query string, e ToPageAsync devolve a página mais o total em duas idas ao banco.
Leitura
O IQueryable<T> injetado é composável e não rastreia. Projete antes de materializar e o banco devolve só as colunas que a API envia.
public Task<Page<ProductSummary>> HandleAsync(SearchProducts query, CancellationToken ct) =>
products
.Where(p => query.Term == null || p.Sku.Contains(query.Term))
.OrderBy(p => p.Sku)
.Select(p => new ProductSummary(
p.ExternalId,
p.Sku,
p.Price,
p.Category!.Name,
priceHistory.Count(h => h.ProductId == p.Id)))
.ToPageAsync(query.Request, ct);Escrita
O pipeline coleta os eventos depois do save — quando as chaves geradas pelo banco já existem — e despacha dentro da mesma transação. Se o handler do evento falhar, o comando inteiro é desfeito.
public sealed class Product : AggregateRoot, IHaveAudit, ISoftDeletable
{
public Product(string sku, decimal price)
{
Sku = sku;
Price = price;
Raise(new ProductCreated(ExternalId, sku, price));
}
public void Reprice(decimal price)
{
if (price == Price) return;
var previous = Price;
Price = price;
Raise(new ProductRepriced(ExternalId, previous, price));
}
}