Skip to main content

Pesquisa — Dapper + IDbConnectionFactory vs EF (US 198113)

Data: 2026-07-09
Objetivo: confirmar se DeleteUserAsync pode estender com um único batch SQL multi-schema atômico no mesmo banco.


1. IDbConnectionFactory

ArtefatoPath
Interface + enumscoezzion-nuget-common/.../Data/IDbConnectionFactory.cs
Implementaçãocoezzion-nuget-common/.../Data/DbConnectionFactory.cs
DI no jobsJobs.API/Configuration/DatabaseConfig.csAddTransient<IDbConnectionFactory, DbConnectionFactory>()
Task<IDbConnection> GetConnectionAsync(
EConnectionType connectionType, // ReadOnly | Write
EDatabaseType databaseType = CoreDb, // CoreDb | BackofficeDb | LinkDb | ActivationDb | IntegrationDb
[CallerMemberName] string callerMemberName = "");

Mapeamento CoreDb

TipoConnection string keyDatabase (dev)
WritePostgresCoreDbCoreDB
ReadPostgresCoreDbReadCoreDB

Org multi-tenant (PostgresCoreDbOrg) aponta para o mesmo CoreDB com SearchPath={0}.

Transactions

A factory não encapsula transaction. Padrão disponível: ADO.NET na connection retornada:

connection.Open();
using var tx = connection.BeginTransaction();
await connection.ExecuteAsync(sql, param, transaction: tx);
tx.Commit();

No jobs não há uso atual de BeginTransaction em repositórios Dapper (será o primeiro no delete estendido).


2. Uso de Dapper no jobs (writes)

RepositórioConnectionUso
UserRepository.DeleteUserAsyncWrite + CoreDbUPDATE "Users"
RewardsAddUpPointsMaterializedViewRepositoryWrite + CoreDbREFRESH MATERIALIZED VIEW
DropperContactsDailySnapshotRepositoryWrite + ActivationDbINSERT … ON CONFLICT

Leituras multi-schema já usam interpolação {schema}."Table" + parâmetros para valores (@userId, etc.). Schema nunca é parâmetro SQL (limitação Postgres em identifiers).

Exemplos cross-schema na mesma connection Core:

  • GetManagersByStoreAsync — public Users + UserOrganization + {schema}.Stores
  • GetOldAppVersionUsersAsync — public + {schema}.SellersVacations
  • SellerRepository.SellerUserIdsCrossIdIncludingInactivesAsync{schema}.Sellers JOIN PUBLIC.Users

3. EF no jobs para writes

Jobs escrevem via EF em Activation / Link, não no fluxo de delete de user.

ContextPapel no delete de user
CoreDbContextReadleituras User
CoreOrgDbContextReadleituras Seller
CoreDbContext (write)registrado, não usado por DeleteUserAsync
CoreOrgDbContext (write multi-tenant)registrado via SchemaNameScopedWrapper; sem write de Seller no jobs hoje

EF dual-context (CoreDb + CoreOrg) para Users+UO+Sellers exigiria unit-of-work compartilhado manualmente — mais complexo que Dapper + uma transaction no CoreDB.


4. Confirmação: mesmo banco

PostgresCoreDb = ...;Database=CoreDB;...
PostgresCoreDbOrg = ...;Database=CoreDB;...;SearchPath={0}
ObjetoSchemaAcessível via Write/CoreDb?
Userspublicsim
UserOrganizationpublicsim
Sellerstenant (arezzo, …)sim, com {schema}."Sellers" qualificado

PostgresCoreDb não define SearchPath de tenant — sempre qualificar tabelas tenant.


5. Trade-offs

AbordagemPrósContras
Dapper multi-statement + txAlinha com delete atual; 1 connection; atomicidade; cross-schema já provenSchema interpolado (validar); tx é padrão novo no jobs Dapper
Dapper 3 calls sem txSimplesEstado parcial (Users ok, Sellers não)
EF CoreDb + CoreOrgEntidades de domínio2 contexts; atomicidade difícil; overkill

6. Recomendação adotada (grill D3)

  1. Manter Dapper + IDbConnectionFactory Write/CoreDb
  2. Um batch SQL (Users + Sellers + UserOrganization)
  3. BeginTransaction / Commit / Rollback
  4. Assinatura DeleteUserAsync(int userId, string schema)
  5. Validar schema antes de interpolar: ^[a-zA-Z_][a-zA-Z0-9_]*$
  6. Não usar EF para esta US

Sketch

public async Task DeleteUserAsync(int userId, string schema)
{
// guard schema (regex/allowlist) — throw se inválido

var sql = $"""
UPDATE public."Users"
SET "DateDeleted" = NOW() AT TIME ZONE 'America/Sao_Paulo',
"Status" = 2
WHERE "Id" = @userId
AND "DateDeleted" IS NULL;

UPDATE {schema}."Sellers"
SET "DateDeleted" = NOW() AT TIME ZONE 'America/Sao_Paulo'
WHERE "UserId" = @userId
AND "DateDeleted" IS NULL;

DELETE FROM public."UserOrganization"
WHERE "UsersId" = @userId;
""";

using var connection =
await _dbConnectionFactory.GetConnectionAsync(EConnectionType.Write, EDatabaseType.CoreDb);

if (connection.State != ConnectionState.Open)
connection.Open();

using var tx = connection.BeginTransaction();
try
{
await connection.ExecuteAsync(sql, new { userId }, transaction: tx);
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
}

Nota: handler usa Parallel.ForEachAsync (DOP 3) — cada user deve abrir sua própria connection/tx (não compartilhar).


7. Riscos e mitigações

RiscoMitigação
SQL injection via schemaRegex/allowlist antes de interpolar
Estado parcialTransaction única
ReexecuçãoWHERE DateDeleted IS NULL; DELETE UO com 0 rows
User sem Seller0 rows = sucesso
Parallel deleteconnection por iteração