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
| Artefato | Path |
|---|---|
| Interface + enums | coezzion-nuget-common/.../Data/IDbConnectionFactory.cs |
| Implementação | coezzion-nuget-common/.../Data/DbConnectionFactory.cs |
| DI no jobs | Jobs.API/Configuration/DatabaseConfig.cs → AddTransient<IDbConnectionFactory, DbConnectionFactory>() |
Task<IDbConnection> GetConnectionAsync(
EConnectionType connectionType, // ReadOnly | Write
EDatabaseType databaseType = CoreDb, // CoreDb | BackofficeDb | LinkDb | ActivationDb | IntegrationDb
[CallerMemberName] string callerMemberName = "");
Mapeamento CoreDb
| Tipo | Connection string key | Database (dev) |
|---|---|---|
| Write | PostgresCoreDb | CoreDB |
| Read | PostgresCoreDbRead | CoreDB |
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ório | Connection | Uso |
|---|---|---|
UserRepository.DeleteUserAsync | Write + CoreDb | UPDATE "Users" |
RewardsAddUpPointsMaterializedViewRepository | Write + CoreDb | REFRESH MATERIALIZED VIEW |
DropperContactsDailySnapshotRepository | Write + ActivationDb | INSERT … 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}.StoresGetOldAppVersionUsersAsync— public +{schema}.SellersVacationsSellerRepository.SellerUserIdsCrossIdIncludingInactivesAsync—{schema}.SellersJOINPUBLIC.Users
3. EF no jobs para writes
Jobs escrevem via EF em Activation / Link, não no fluxo de delete de user.
| Context | Papel no delete de user |
|---|---|
CoreDbContextRead | leituras User |
CoreOrgDbContextRead | leituras 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}
| Objeto | Schema | Acessível via Write/CoreDb? |
|---|---|---|
| Users | public | sim |
| UserOrganization | public | sim |
| Sellers | tenant (arezzo, …) | sim, com {schema}."Sellers" qualificado |
PostgresCoreDb não define SearchPath de tenant — sempre qualificar tabelas tenant.
5. Trade-offs
| Abordagem | Prós | Contras |
|---|---|---|
| Dapper multi-statement + tx | Alinha com delete atual; 1 connection; atomicidade; cross-schema já proven | Schema interpolado (validar); tx é padrão novo no jobs Dapper |
| Dapper 3 calls sem tx | Simples | Estado parcial (Users ok, Sellers não) |
| EF CoreDb + CoreOrg | Entidades de domínio | 2 contexts; atomicidade difícil; overkill |
6. Recomendação adotada (grill D3)
- Manter Dapper +
IDbConnectionFactoryWrite/CoreDb - Um batch SQL (Users + Sellers + UserOrganization)
BeginTransaction/ Commit / Rollback- Assinatura
DeleteUserAsync(int userId, string schema) - Validar schema antes de interpolar:
^[a-zA-Z_][a-zA-Z0-9_]*$ - 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
| Risco | Mitigação |
|---|---|
| SQL injection via schema | Regex/allowlist antes de interpolar |
| Estado parcial | Transaction única |
| Reexecução | WHERE DateDeleted IS NULL; DELETE UO com 0 rows |
| User sem Seller | 0 rows = sucesso |
| Parallel delete | connection por iteração |