Documentação
Primeiros passos
De pacote instalado a primeiro comando gravando no banco, em cinco passos.
Instalação
dotnet add package Nexo.EntityFrameworkCore
dotnet add package Nexo.FluentValidation # opcionalNexo.EntityFrameworkCore já traz Nexo e Nexo.Abstractions. Se você for implementar outro provider de persistência, referencie só Nexo.Abstractions no projeto de domínio. O provider do banco é escolha sua e vem à parte — Microsoft.EntityFrameworkCore.SqlServer, MySql.EntityFrameworkCore, Npgsql.EntityFrameworkCore.PostgreSQL, MongoDB.EntityFrameworkCore…
Os cinco passos
Modele a entidade
Herde de
Entitypara ganhar chavelonge umGuidpúblico, ou deAggregateRootse ela for registrar eventos de domínio. Os traits (IHaveAudit,ISoftDeletable) são opcionais e aditivos.Product.cs using Nexo.Data; public sealed class Product : AggregateRoot, IHaveAudit, ISoftDeletable { private Product() { Sku = string.Empty; } // materialização do EF public Product(string sku, decimal price) { Sku = sku; Price = price; Raise(new ProductCreated(ExternalId, sku, price)); } public string Sku { get; private set; } public decimal Price { get; private set; } public DateTimeOffset CreatedOn { get; set; } public DateTimeOffset? UpdatedOn { get; set; } public string? CreatedBy { get; set; } public string? UpdatedBy { get; set; } public bool IsDeleted { get; set; } public DateTimeOffset? DeletedOn { get; set; } public string? DeletedBy { get; set; } public void Reprice(decimal price) { if (price == Price) return; var previous = Price; Price = price; Raise(new ProductRepriced(ExternalId, previous, price)); } }Mapeie o contexto
ApplyNexoConventions()no fim doOnModelCreatingmapeia todos os traits de todas as entidades: unicidade do identificador externo, tamanho das colunas de auditoria, token de concorrência e o query filter que esconde as linhas apagadas. Chame por último, para que a sua configuração explícita sempre ganhe.CatalogDbContext.cs using Microsoft.EntityFrameworkCore; public sealed class CatalogDbContext(DbContextOptions<CatalogDbContext> options) : DbContext(options) { public DbSet<Product> Products => Set<Product>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>(product => { product.MapPrimaryKey(); product.Property(e => e.Sku).IsRequired().HasMaxLength(32); product.HasIndex(e => e.Sku).IsUnique(); }); modelBuilder.ApplyNexoConventions(); } }Registre
Duas chamadas.
UseNexo(sp)instala o interceptor de auditoria;AddNexo<TContext>registra o mediator, as operações de dados e os pipelines na ordem correta. Mais de um banco? UmAddDbContextpor banco e umUseDbContext<T>()dentro doAddNexo— veja Vários bancos.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>());Escreva o comando e o handler
O handler pede exatamente as capacidades que usa. Nada de
DbContext, nada deSaveChanges.CreateProduct.cs public sealed class CreateProduct : Command<Guid> { public string Sku { get; init; } = string.Empty; public decimal Price { get; init; } } public sealed class CreateProductValidator : AbstractValidator<CreateProduct> { public CreateProductValidator() { RuleFor(c => c.Sku).NotEmpty().MaximumLength(32); RuleFor(c => c.Price).GreaterThan(0); } } public sealed class CreateProductHandler(ICreate<Product> create) : ICommandHandler<CreateProduct, Guid> { public async Task<Guid> HandleAsync(CreateProduct command, CancellationToken ct) { var product = await create.CreateAsync(new Product(command.Sku, command.Price), ct); return product.ExternalId; } }Chame pelo mediator
No endpoint, no worker, no job. O
IMediatoré scoped, então cada requisição tem seu próprio unit of work.C# app.MapPost("/products", async ( CreateProduct command, IMediator mediator, CancellationToken ct) => { var id = await mediator.SendAsync(command, ct); return Results.Created($"/products/{id}", new { id }); });
O que acontece nessa chamada
SendAsync(CreateProduct)
│
├─ LoggingPipeline abre escopo de log com nome e id da mensagem
├─ MessageEnrichmentPipeline preenche CreatedBy a partir do usuário ambiente
├─ ValidationPipeline roda o CreateProductValidator — falhou? para aqui
└─ UnitOfWorkPipeline
├─ abre transação (com a execution strategy do provider)
├─ CreateProductHandler.HandleAsync
│ └─ ICreate<Product>.CreateAsync → só rastreia, não vai ao banco
├─ SaveChanges → INSERT, a chave é atribuída
├─ coleta ProductCreated da entidade
├─ BroadcastAsync(ProductCreated) → entra neste mesmo unit of work
├─ SaveChanges do que os handlers escreveram
└─ commitMontando os pipelines à mão
AddNexo<TContext> é um atalho opinativo. Quando você quer controlar a ordem ou inserir um pipeline próprio no meio, use as duas chamadas separadas:
builder.Services.AddNexoEntityFrameworkCore<CatalogDbContext>(session =>
{
session.TrackReads = true;
session.SoftDelete = true;
});
builder.Services.AddNexo(nexo => nexo
.AddHandlersFromAssemblyOf<Program>()
.AddLoggingPipeline()
.AddEnrichmentPipeline()
.AddPipeline<TenantPipeline>() // o seu, entre validação e transação
.AddValidationPipeline()
.AddUnitOfWorkPipeline()); // sempre por últimoCom vários bancos a forma é a mesma: AddNexoEntityFrameworkCore<T> uma vez por banco, e o AddNexo uma só vez no fim.
Mais de um banco
Os passos acima não mudam. O que muda é que existe um AddDbContext por banco, e os contextos extras entram por UseDbContext<T>():
builder.Services.AddDbContext<CatalogDbContext>((sp, o) => o
.UseSqlServer(configuration.GetConnectionString("Catalog")).UseNexo(sp));
builder.Services.AddDbContext<PurchasingDbContext>((sp, o) => o
.UseMySQL(configuration.GetConnectionString("Purchasing")!).UseNexo(sp));
builder.Services.AddDbContext<ActivityDbContext>((sp, o) => o
.UseMongoDB(configuration.GetConnectionString("Activity")!, "activity").UseNexo(sp));
builder.Services.AddNexo<CatalogDbContext>(nexo => nexo
.UseDbContext<PurchasingDbContext>()
.UseDbContext<ActivityDbContext>(session => session.UseTransactions = false)
.AddHandlersFromAssemblyOf<Program>()
.AddValidatorsFromAssemblyOf<Program>());E os handlers não mudam nada: ICreate<Product> acha o SQL Server porque é lá que Product está mapeado. Vários bancos cobre o resto — separar os modelos, transação entre bancos, e o que cada provider muda.