Nexo
net10.0 · EF Core 10

CQRS e acesso a dados integrados para .NET

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.

ICreate<T>IUpdate<T>IDelete<T>IQueryable<T>IReadByExternalId<T>Page<T>IPipeline

O handler inteiro

É isso. Não tem mais nada escondido.

Nenhum repositório para escrever, nenhum SaveChanges para lembrar, nenhuma transação para abrir, nenhum evento para publicar à mão.

CreateProductHandler.cs
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

Dois pacotes, duas chamadas

Shell
dotnet add package Nexo.EntityFrameworkCore
dotnet add package Nexo.FluentValidation
Program.cs
builder.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

Decisões tomadas por você, e explicadas

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.

Sem repositório

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.

Eventos de domínio de verdade

A entidade registra com Raise; o pipeline coleta depois do save — quando as chaves já existem — e despacha na mesma transação.

Auditoria e soft delete opcionais

Implemente IHaveAudit ou ISoftDeletable e o interceptor cuida do resto. Uma entidade que não implementa nada segue um POCO puro.

Validação sem vazamento

FluentValidation roda antes do handler, e a falha chega como ValidationFailedException. Sua camada web nunca referencia FluentValidation.

Paginação que não vira table scan

PageRequest faz clamp do que vem da query string, e ToPageAsync devolve a página mais o total em duas idas ao banco.

Leitura

LINQ, join, subquery e projeção

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.

ProductQueryHandlers.cs
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

A entidade anuncia o que aconteceu

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.

Product.cs
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));
    }
}