Nexo

Documentação

Entity Framework Core

Como o provider funciona, o que ele registra e as opções da sessão.

O que é registrado

C#
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 o DbContext. 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.Linq ao EF Core.
  • As leituras, nas duas formas: IReadById<,> e IReadById<> (chave long), IReadByExternalId<,> e IReadByExternalId<> (Guid), os ranges e os IExists*.
  • 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.

C#
services.AddNexoEntityFrameworkCore<CatalogDbContext>();
services.AddNexoEntityFrameworkCore<BillingDbContext>();

// Ou, com os pipelines montados para você:
services.AddNexo<CatalogDbContext>(nexo => nexo.UseDbContext<BillingDbContext>());

Instalando o interceptor

C#
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

C#
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çãoPadrãoEfeito
SaveBehaviorUnitOfWorkQuando as mudanças vão ao banco. Immediate salva a cada operação — conveniente fora de um mediator, mas abre mão da atomicidade.
TrackReadstrueAs leituras por chave voltam rastreadas, então mutar a entidade já é o update. Queries injetadas nunca rastreiam, independente disso.
SoftDeletetrueIDelete marca entidades ISoftDeletable em vez de removê-las.
ApplyAudittrueLiga as estampas do interceptor.
AssignMissingExternalIdstrueAtribui um Guid sequencial no insert quando o campo está vazio. Rede de segurança: as bases já atribuem na construção.
UseTransactionsnullnull 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 entidadeO que a convenção faz
IHaveExternalIdExternalId obrigatório, índice único.
IHaveTimestampsCreatedOn obrigatório.
IHaveAuditCreatedBy e UpdatedBy com HasMaxLength (128 por padrão).
IHaveRowVersionRowVersion como token de concorrência.
ISoftDeletableDeletedBy 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.

C#
// 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.

C#
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.

Shell
# 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.Sqlite

O 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.

ProviderTransaçãoChave long geradaIHaveRowVersion
SQL ServerSimSimSim, rowversion nativo
PostgreSQLSimSimVia xmin, com mapeamento explícito
MySQLSimSimNão nativo
SQLiteSimSimColuna mapeada, não mantida pelo banco
MongoDBSó em replica setNão — use GuidNão
In-memoryNãoSimNã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.)

C#
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.

C#
// Depois do AddNexo: os checks são um por contexto que o Nexo conhece.
builder.Services.AddHealthChecks().AddNexoDbContextChecks(tags: ["ready"]);

app.MapHealthChecks("/health");
JSON
{
  "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.