Documentação
Entity Framework Core
Como o provider funciona, o que ele registra e as opções da sessão.
O que é registrado
services.AddNexoEntityFrameworkCore<CatalogDbContext>();Essa chamada registra, todos com escopo Scoped:
DbSessionProvider— entrega a sessão de cada contexto e acha a certa para uma entidade. É ele que faz as operações somarem num unit of work por banco.DbSession— a ponte com oDbContext. Sem genérico, resolve o primeiro contexto registrado.IUnitOfWork,IDataStore,IDomainEventCollector,AuditInterceptor.- Os genéricos abertos:
IQueryable<>,ICreate<>,ICreateRange<>,IUpdate<>,IUpdateRange<>,IDelete<>,IDeleteRange<>,IRestore<>,IBulkUpdate<>,IBulkDelete<>. - O executor que liga os operadores de
Nexo.Linqao EF Core. - As leituras, nas duas formas:
IReadById<,>eIReadById<>(chavelong),IReadByExternalId<,>eIReadByExternalId<>(Guid), os ranges e osIExists*. NexoDbContextRegistry— a lista dos contextos registrados, na ordem de registro.
Chame uma vez por banco. As operações são genéricos abertos registrados uma só vez; o que uma segunda chamada acrescenta é o contexto. A partir daí cada operação encontra o banco procurando qual contexto mapeia a entidade — ICreate<Invoice> escreve no banco de faturamento e ICreate<Product> no de catálogo, sem mudar nenhum dos dois handlers.
services.AddNexoEntityFrameworkCore<CatalogDbContext>();
services.AddNexoEntityFrameworkCore<BillingDbContext>();
// Ou, com os pipelines montados para você:
services.AddNexo<CatalogDbContext>(nexo => nexo.UseDbContext<BillingDbContext>());Instalando o interceptor
builder.Services.AddDbContext<CatalogDbContext>((sp, options) => options
.UseSqlServer(connectionString)
.UseNexo(sp));Esquecer o UseNexo não passa em silêncio. Sem o interceptor, auditoria, soft delete por Remove e atribuição de identificador simplesmente não aconteceriam — e ninguém perceberia até uma auditoria perguntar quem criou a linha. Por isso a primeira sessão de um contexto sem ele falha, com uma mensagem que diz o que adicionar. Um contexto que de fato não quer auditoria diz isso com ApplyAudit = false e fica dispensado.
Em todos os contextos, inclusive os de documento. É um interceptor só, e ele lê as opções do contexto que está salvando — então um banco relacional estampa as colunas de auditoria enquanto outro, configurado com ApplyAudit = false, não estampa.
Opções da sessão
services.AddNexoEntityFrameworkCore<CatalogDbContext>(session =>
{
session.SaveBehavior = SaveBehavior.UnitOfWork;
session.TrackReads = true;
session.SoftDelete = true;
session.ApplyAudit = true;
session.AssignMissingExternalIds = true;
session.UseTransactions = null;
});As opções são por contexto: este callback ajusta um banco. Para o padrão de todos, use services.Configure<DbSessionOptions>(…) — cada contexto parte dele e o callback dele sobrescreve o que quiser.
| Opção | Padrão | Efeito |
|---|---|---|
SaveBehavior | UnitOfWork | Quando as mudanças vão ao banco. Immediate salva a cada operação — conveniente fora de um mediator, mas abre mão da atomicidade. |
TrackReads | true | As leituras por chave voltam rastreadas, então mutar a entidade já é o update. Queries injetadas nunca rastreiam, independente disso. |
SoftDelete | true | IDelete marca entidades ISoftDeletable em vez de removê-las. |
ApplyAudit | true | Liga as estampas do interceptor. |
AssignMissingExternalIds | true | Atribui um Guid sequencial no insert quando o campo está vazio. Rede de segurança: as bases já atribuem na construção. |
UseTransactions | null | null detecta pelo provider (IsRelational). Defina explicitamente para forçar a decisão. |
ApplyNexoConventions
Percorre todas as entidades do modelo e aplica o que cada trait pede. Chame por último, depois da sua configuração — o que você definiu explicitamente sempre ganha.
| Trait na entidade | O que a convenção faz |
|---|---|
IHaveExternalId | ExternalId obrigatório, índice único. |
IHaveTimestamps | CreatedOn obrigatório. |
IHaveAudit | CreatedBy e UpdatedBy com HasMaxLength (128 por padrão). |
IHaveRowVersion | RowVersion como token de concorrência. |
ISoftDeletable | DeletedBy com tamanho, índice em IsDeleted e o query filter e => !e.IsDeleted. |
Tipos owned são ignorados: eles compartilham a configuração do pai.
Como o IQueryable injetado funciona
O container não entrega DbSet<T> num registro genérico aberto, então há um envelope fino. Ele repassa Provider e Expression intocados — todo operador LINQ compõe exatamente como no próprio set — e implementa IAsyncEnumerable<T>, o que faz ToListAsync funcionar direto na instância injetada, sem operador nenhum antes.
// Funciona porque o envelope é um IAsyncEnumerable.
var all = await products.ToListAsync(ct);
// Funciona porque o Provider é o do EF: o Where devolve a query real do EF.
var some = await products.Where(p => p.Price > 100).ToListAsync(ct);Concorrência
DbUpdateConcurrencyException é traduzida em ConcurrencyConflictException, com o nome da entidade na mensagem e a exceção original em InnerException. Quem trata nunca precisa referenciar EF Core.
ConcurrencyConflictException conflict => new ProblemDetails
{
Status = StatusCodes.Status409Conflict,
Title = "Concurrent modification",
Detail = conflict.Message,
},Providers
O Nexo fala com um DbContext e delega a tradução ao provider do EF Core, então o provider é uma decisão sua e ele não participa dela. A única regra é a do próprio EF Core: o provider tem que ser da linha 10, a do EF Core que o Nexo usa.
# Instale só o que você vai usar. Para vários bancos, um por banco.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package MySql.EntityFrameworkCore # MySQL com EF Core 10
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package MongoDB.EntityFrameworkCore # sim, o Mongo é um provider EF Core
dotnet add package Microsoft.EntityFrameworkCore.SqliteO sample roda com três ao mesmo tempo — SQL Server, MySQL e MongoDB. Como se registra mais de um, e o que isso muda em transação e em query, está em Vários bancos.
| Provider | Transação | Chave long gerada | IHaveRowVersion |
|---|---|---|---|
| SQL Server | Sim | Sim | Sim, rowversion nativo |
| PostgreSQL | Sim | Sim | Via xmin, com mapeamento explícito |
| MySQL | Sim | Sim | Não nativo |
| SQLite | Sim | Sim | Coluna mapeada, não mantida pelo banco |
| MongoDB | Só em replica set | Não — use Guid | Não |
| In-memory | Não | Sim | Não |
Saindo da abstração
Para o que as operações não cobrem — SQL cru, um bulk de biblioteca de terceiro — injete DbSession e use o DbContext. Você continua no mesmo unit of work. (Para um UPDATE/DELETE por predicado, prefira IBulkUpdate<T>/IBulkDelete<T>, que já estampam auditoria e soft delete.)
public sealed class ArchiveOldOrdersHandler(DbSession session)
: ICommandHandler<ArchiveOldOrders, int>
{
public Task<int> HandleAsync(ArchiveOldOrders command, CancellationToken ct) =>
session.Context.Orders
.Where(o => o.PlacedOn < command.Before)
.ExecuteUpdateAsync(o => o.SetProperty(x => x.Archived, true), ct);
}Health checks
Um health check por contexto registrado, com o nome do contexto — porque com três bancos "unhealthy" sozinho não responde nada: qual deles caiu decide se a API inteira está fora ou só uma parte.
// Depois do AddNexo: os checks são um por contexto que o Nexo conhece.
builder.Services.AddHealthChecks().AddNexoDbContextChecks(tags: ["ready"]);
app.MapHealthChecks("/health");{
"status": "healthy",
"stores": {
"CatalogDbContext": "up",
"PurchasingDbContext": "up",
"ActivityDbContext": "up"
}
}Esse formato é o do sample, que passa um ResponseWriter próprio ao MapHealthChecks. Um banco fora faz o endpoint responder 503, que é o que um orquestrador precisa para parar de mandar tráfego.