DcsvIo.D2.Handler.Repo
0.1.2
dotnet add package DcsvIo.D2.Handler.Repo --version 0.1.2
NuGet\Install-Package DcsvIo.D2.Handler.Repo -Version 0.1.2
<PackageReference Include="DcsvIo.D2.Handler.Repo" Version="0.1.2" />
<PackageVersion Include="DcsvIo.D2.Handler.Repo" Version="0.1.2" />
<PackageReference Include="DcsvIo.D2.Handler.Repo" />
paket add DcsvIo.D2.Handler.Repo --version 0.1.2
#r "nuget: DcsvIo.D2.Handler.Repo, 0.1.2"
#:package DcsvIo.D2.Handler.Repo@0.1.2
#addin nuget:?package=DcsvIo.D2.Handler.Repo&version=0.1.2
#tool nuget:?package=DcsvIo.D2.Handler.Repo&version=0.1.2
DcsvIo.D2.Handler.Repo
EF-flavored BaseRepoHandler<TSelf, TInput, TOutput> — sits on top of BaseHandler from DcsvIo.D2.Handler. Overrides HandleAsync to convert any database exception captured during ExecuteAsync into a typed D2Result failure (concurrency conflict, unique violation, deadlock, connection failure, etc.) so callers can branch on what actually went wrong instead of getting a generic 500.
Provider-agnostic by design: catches the BCL-typed DbUpdateConcurrencyException directly and routes everything else through an injected IDbExceptionClassifier. Provider-specific knowledge lives in sibling packages (e.g. DcsvIo.D2.Handler.Repo.Postgres).
Install
dotnet add package DcsvIo.D2.Handler.Repo
Public API
| Type | Role |
|---|---|
BaseRepoHandler<TSelf, TInput, TOutput> |
Abstract subclass of BaseHandler. Constructor takes an injected IDbExceptionClassifier. Override HandleAsync calls RunCorePipelineAsync then dispatches the captured exception through the classifier to a typed D2Result factory. |
MapDbException (virtual) |
Per-handler refinement hook — attach domain-specific TKMessage + InputError; return null for default factory. |
Zero provider deps in this package (no Npgsql). Refs DcsvIo.D2.Handler, DcsvIo.D2.Handler.Abstractions, DcsvIo.D2.Handler.Repo.Abstractions, DcsvIo.D2.Result, and Microsoft.EntityFrameworkCore.
Mapping
| Captured exception | Classified as | Default D2Result |
|---|---|---|
DbUpdateConcurrencyException |
ConcurrencyConflict (BCL — handled directly) |
D2Result.ConcurrencyConflict() |
IDbExceptionClassifier.Classify(ex) returns UniqueViolation |
UniqueViolation |
D2Result.UniqueViolation() |
Returns ForeignKeyViolation |
ForeignKeyViolation |
D2Result.ForeignKeyViolation() |
Returns NotNullViolation |
NotNullViolation |
D2Result.NotNullViolation() |
Returns CheckViolation |
CheckViolation |
D2Result.CheckViolation() |
Returns Timeout |
Timeout |
D2Result.DbTimeout() |
Returns Deadlock |
Deadlock |
D2Result.DbDeadlock() |
Returns ConnectionFailure |
ConnectionFailure |
D2Result.DbConnectionFailure() |
Classifier returns null |
unknown | Falls through — BaseHandler's UnhandledException preserved |
OperationCanceledException is intentionally NOT remapped here — BaseHandler.RunCorePipelineAsync already handles it (returns D2Result.Canceled for caller-initiated cancellation, D2Result.ServiceUnavailable for downstream timeouts not tied to the request token).
Per-handler refinement
The default factory dispatch produces a generic message ("This value is already in use") with no field-level information — useful for diagnostics + programmatic discrimination, but weak UX for form-driven flows.
Handlers that know their constraint identity should override MapDbException to attach a domain-specific TKMessage + InputError:
public sealed class CreateUser(
HandlerContext<CreateUser> context,
IDbExceptionClassifier classifier,
IAppDbContext db)
: BaseRepoHandler<CreateUser, CreateUserInput, UserDto>(context, classifier), ICreateUser
{
protected override async ValueTask<D2Result<UserDto?>> ExecuteAsync(
CreateUserInput input, CancellationToken ct)
{
var user = User.Create(input);
db.Users.Add(user);
await db.SaveChangesAsync(ct);
return D2Result<UserDto?>.Created(user.ToDto());
}
protected override D2Result<UserDto?>? MapDbException(Exception ex, DbFailureKind kind)
{
// The DB-side unique index `users_email_key` covers the email column.
if (kind == DbFailureKind.UniqueViolation && IsEmailIndex(ex))
{
return D2Result<UserDto?>.UniqueViolation(
messages: [TK.Auth.Errors.EMAIL_ALREADY_TAKEN],
inputErrors: [new InputError("email", "EMAIL_ALREADY_TAKEN")]);
}
return null; // fall back to the generic factory
}
}
Returning null from the override means "use the default" — handlers only customize the cases they care about.
Caller-side discrimination
Callers branch on the typed booleans from DcsvIo.D2.Handler.Repo.Abstractions:
var result = await createUser.HandleAsync(input);
if (result.IsUniqueViolation) return Conflict(result); // 409, surface to user
if (result.IsConcurrencyConflict) return await ReloadAndMergeAsync(input); // optimistic-concurrency retry
if (result.IsTransientDbFailure) return await retry.RetryAsync(...); // deadlock / timeout / connection
if (result.IsForeignKeyViolation) return BadRequest(result); // referenced item missing
The roll-up IsTransientDbFailure covers deadlock + timeout + connection-failure (caller may safely retry). Concurrency conflicts are intentionally excluded from the roll-up — they need reload-then-merge, not a blind retry.
IsTransientDbFailure is a separate axis from the built-in IsTransientRetryable (IsServiceUnavailable || IsRateLimited) on D2Result from the result lib. A generic retry policy that wants to catch BOTH the HTTP-flavored AND DB-flavored transient sets should check the union: result.IsTransientRetryable || result.IsTransientDbFailure.
DI registration
BaseRepoHandler requires an IDbExceptionClassifier from DI. The composition root registers a provider-specific implementation:
services.AddD2Handler();
services.AddD2Postgres(); // registers PostgresDbExceptionClassifier as IDbExceptionClassifier
services.AddDbContext<AppDbContext>(o => o.UseNpgsql(...));
services.AddTransient<ICreateUser, CreateUser>();
Without a registered classifier, resolving any BaseRepoHandler subclass fails fast at the container.
Dependencies
DcsvIo.D2.Handler— base +HandlerContext<T>DcsvIo.D2.Handler.Abstractions—IHandler,HandlerOptionsDcsvIo.D2.Handler.Repo.Abstractions—IDbExceptionClassifier,DbFailureKind,D2Resultextension factoriesDcsvIo.D2.Result— baseD2ResultMicrosoft.EntityFrameworkCore—DbUpdateConcurrencyException
No Npgsql, no provider-specific deps.
Related packages
DcsvIo.D2.Handler.Repo.Abstractions— vocabulary + extension factories + booleansDcsvIo.D2.Handler.Repo.Postgres— PostgreSQL classifier implDcsvIo.D2.Handler— base handler
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- DcsvIo.D2.Handler (>= 0.1.2)
- DcsvIo.D2.Handler.Abstractions (>= 0.1.1)
- DcsvIo.D2.Handler.Repo.Abstractions (>= 0.1.1)
- DcsvIo.D2.Result (>= 0.1.1)
- dotenv.net (>= 4.0.2)
- JetBrains.Annotations (>= 2025.2.4)
- Microsoft.EntityFrameworkCore (>= 10.0.7)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.7)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Caching.Memory (>= 10.0.7)
- Microsoft.Extensions.Configuration (>= 10.0.7)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Logging (>= 10.0.7)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Options (>= 10.0.7)
- Microsoft.IdentityModel.Tokens (>= 8.16.0)
- NodaTime (>= 3.2.2)
- Npgsql (>= 10.0.2)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.1)
- Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime (>= 10.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.