DcsvIo.D2.Handler
0.1.2
dotnet add package DcsvIo.D2.Handler --version 0.1.2
NuGet\Install-Package DcsvIo.D2.Handler -Version 0.1.2
<PackageReference Include="DcsvIo.D2.Handler" Version="0.1.2" />
<PackageVersion Include="DcsvIo.D2.Handler" Version="0.1.2" />
<PackageReference Include="DcsvIo.D2.Handler" />
paket add DcsvIo.D2.Handler --version 0.1.2
#r "nuget: DcsvIo.D2.Handler, 0.1.2"
#:package DcsvIo.D2.Handler@0.1.2
#addin nuget:?package=DcsvIo.D2.Handler&version=0.1.2
#tool nuget:?package=DcsvIo.D2.Handler&version=0.1.2
DcsvIo.D2.Handler
BaseHandler<TSelf, TInput, TOutput> — the abstract base every handler in every service inherits (CQRS handlers, repo handlers, messaging consumers, scheduled jobs, anything handler-shaped). Provides per-handler scope pre-check, OTel activity + 4 metrics + log scope + stopwatch + universal try/catch around the subclass's ExecuteAsync. Sibling DcsvIo.D2.Handler.Repo adds EF / PG exception → D2Result mapping on top.
JWT signature / expiry / audience / fingerprint-binding validation is a transport-level concern handled by auth middleware (HTTP / gRPC / AMQP) BEFORE the handler runs. By the time a handler executes, the bearer token has already been validated for the host service.
Install
dotnet add package DcsvIo.D2.Handler
⚠ PII safety — REQUIRED setup before deploying any handler
BaseHandler logs handler input via Serilog destructuring ({@Input}) at Debug level, gated by HandlerOptions.LogInput (default true). Without proper redaction wiring, ANY field on the input that contains PII (email, phone, name, address, raw IP, etc.) will appear verbatim in logs whenever Debug-level logging is enabled.
Every consuming service MUST register the [RedactData]-aware Serilog destructuring policy at startup. Hosts compose this themselves with DcsvIo.D2.Logging / Serilog wiring — BaseHandler does not require a host mega-aggregator. If you bootstrap logging manually, register the policy yourself.
If you can't guarantee the redaction policy is in place (e.g. a one-off tool, an early bootstrap path), set LogInput = false in DefaultOptions for every handler that touches PII. Prefer registering the redaction policy once at host startup over relying on per-handler LogInput = false.
Public API
| Type | Role |
|---|---|
BaseHandler<TSelf, TInput, TOutput> |
Abstract base. Virtual HandleAsync (entry point) + non-virtual RunCorePipelineAsync (observability + try/catch) + abstract ExecuteAsync (subclass logic) |
HandlerContext<T> |
Typed-logger context. Open-generic registration via AddD2Handler |
HandlerTelemetry |
Static OTel primitives — ActivitySource + Meter + 4 instruments (d2.handler.invoked / succeeded / failed counters + d2.handler.duration histogram) |
AddD2Handler |
DI extension — registers open-generic HandlerContext<T> |
BaseHandler shape
public abstract class BaseHandler<TSelf, TInput, TOutput> : IHandler<TInput, TOutput>
where TSelf : BaseHandler<TSelf, TInput, TOutput>
{
protected BaseHandler(HandlerContext<TSelf> context);
protected IHandlerContext Context { get; }
protected virtual HandlerOptions DefaultOptions { get; }
public virtual ValueTask<D2Result<TOutput?>> HandleAsync(
TInput input, CancellationToken ct = default, HandlerOptions? options = null);
protected ValueTask<(D2Result<TOutput?> Result, Exception? CapturedException)> RunCorePipelineAsync(
TInput input, CancellationToken ct, HandlerOptions? options);
protected abstract ValueTask<D2Result<TOutput?>> ExecuteAsync(TInput input, CancellationToken ct);
}
RunCorePipelineAsync returns the captured exception alongside the result so EF-flavored subclasses (BaseRepoHandler) can remap typed exceptions to D2Result failure codes from their own overridden HandleAsync.
HandleAsync flow
- Resolve options: per-call →
DefaultOptions→ platform defaults - Scope pre-check: if
ScopeRequirementis non-null with a non-emptyScopesset, checksContext.Request.Scopesusing the declaredHandlerScopeMatch—Any(at least one overlap) orAll(every scope present); skip whenScopeRequirementis null orScopesis empty; mismatch → incrementsHandlerTelemetry.SR_Invoked+SR_Failed, skips activity span and duration recording, returnsD2Result.Forbidden - Activity start:
HandlerTelemetry.SR_ActivitySource.StartActivity(handlerName). Tags emitted (when present):- Always:
d2.handler.name - When user identity present:
d2.user_id,d2.org_id,d2.org_type,d2.org_role - When impersonating:
d2.impersonating,d2.impersonation_kind,d2.impersonator_id,d2.impersonator_org_id,d2.impersonator_org_type,d2.impersonator_org_role
- Always:
- Counter:
HandlerTelemetry.SR_Invoked.Add(1, handlerNameTag) ExecuteAsyncinside try/catch- Stopwatch + threshold logging: warn at
SlowThreshold, error atCriticalThreshold - Result handling:
- Success →
Succeededcounter + debug log - Failure →
Failedcounter + debug log OperationCanceledExceptionwith ourctcanceled →D2Result.Canceled+ info log (intentional caller cancellation)OperationCanceledExceptionwithout ourctcanceled →D2Result.ServiceUnavailable+ warn log (downstream timeout — HttpClient timeout, SQL command timeout, internal handler watchdog)- Other exception →
D2Result.UnhandledException+ error log + activity status set
- Success →
- Duration:
HandlerTelemetry.SR_Duration.Record(elapsedMs, handlerNameTag)always
TraceId is auto-injected on every emitted D2Result via Context.Request.TraceId.
Why distinguish caller-canceled from downstream-timeout
A downstream timeout (e.g. HttpClient.Timeout firing) surfaces as OperationCanceledException whose token is the timeout's internal token — NOT our ct. Treating it as UnhandledException (500) implies a bug in our code; treating it as ServiceUnavailable (503) correctly signals "a dependency we needed isn't responding." That's the right HTTP semantic and the right operational signal.
DI Registration
services.AddD2Handler(); // registers open-generic HandlerContext<T> as Transient
// Per-handler registration (typically in the app layer AddXxxApp extension):
services.AddTransient<IGetUserById, GetUserById>();
AddD2Handler does NOT register IRequestContext — that's transport-specific. Each consuming transport stack is responsible for constructing per-request IRequestContext and putting it into the DI scope before any handler resolves:
- HTTP / gRPC.AspNetCore: the consuming service's startup wires HTTP middleware that builds
IRequestContextfrom the validated bearer + ambient request data - RabbitMQ consumer: the consuming service's consumer pipeline builds
IRequestContextfrom the AMQP frame headers + decrypted body
Tests provide a MutableRequestContext test fixture builder.
Telemetry instruments
| Instrument | Type | Unit | Description |
|---|---|---|---|
d2.handler.invoked |
Counter (long) | {calls} |
Handler invocations attempted |
d2.handler.succeeded |
Counter (long) | {calls} |
Handler invocations that returned Success == true |
d2.handler.failed |
Counter (long) | {calls} |
Handler invocations that returned Success == false OR threw |
d2.handler.duration |
Histogram (double) | ms |
Handler invocation wall-clock duration |
All instruments tag with d2.handler.name = typeof(TSelf).Name. Hosts register the OTel SDK via DcsvIo.D2.Telemetry (AddD2Telemetry) so MeterProvider / TracerProvider capture the DcsvIo.D2.Handler source.
Subclassing pattern
public sealed class GetUserById(HandlerContext<GetUserById> context, IUserRepo repo)
: BaseHandler<GetUserById, GetUserByIdInput, UserDto>, IGetUserById
{
protected override HandlerOptions DefaultOptions => new()
{
ScopeRequirement = new ScopeRequirement(HandlerScopeMatch.Any, new HashSet<string>(StringComparer.Ordinal) { Scopes.Self.Read }),
SlowThreshold = TimeSpan.FromMilliseconds(50),
};
protected override async ValueTask<D2Result<UserDto?>> ExecuteAsync(
GetUserByIdInput input, CancellationToken ct)
{
var user = await repo.FindAsync(input.Id, ct);
if (user is null)
{
return D2Result<UserDto?>.NotFound();
}
return D2Result<UserDto?>.Ok(user.ToDto());
}
}
Handler primary-constructor parameters do NOT take the r_ prefix (carve-out from the standard naming convention).
Dependencies
DcsvIo.D2.Handler.Abstractions—IHandler,HandlerOptions,IHandlerContextDcsvIo.D2.Context.Abstractions— request contextDcsvIo.D2.Result— result typeMicrosoft.Extensions.DependencyInjection.Abstractions/Microsoft.Extensions.Logging.Abstractions
Related packages
DcsvIo.D2.Handler.Abstractions—IHandler+HandlerOptions+IHandlerContextDcsvIo.D2.Handler.Repo— EF/PG exception remapping subclassDcsvIo.D2.Telemetry— OTel SDK wiring that captures handler instruments
Recommended layout: per-op handler folders (Application/Handlers/{Commands,Queries}/<Op>/) with primary-constructor handlers (ctor params do not take the r_ field prefix).
| 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.Context.Abstractions (>= 0.1.1)
- DcsvIo.D2.Handler.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.Abstractions (>= 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 (4)
Showing the top 4 NuGet packages that depend on DcsvIo.D2.Handler:
| Package | Downloads |
|---|---|
|
DcsvIo.D2.Messaging.Abstractions
Transport-agnostic messaging abstractions for D2 — the [MqPub] / [MqSub] vocabulary, the message-bus contract, and DLQ failure-metadata wire shapes. |
|
|
DcsvIo.D2.Handler.Repo
EF-flavored BaseRepoHandler for D2 — converts database exceptions captured during execution into typed D2Result failures. |
|
|
DcsvIo.D2.Messaging.RabbitMq
Default RabbitMQ implementation of the D2 messaging abstractions — publishing, subscribing, encryption frames, and dead-letter handling. |
|
|
DcsvIo.D2.Telemetry
OpenTelemetry SDK setup for D2 — traces, metrics, logs, OTLP exporters, an IP-restricted Prometheus endpoint, and aggregation of every shared library's ActivitySource and Meter. |
GitHub repositories
This package is not used by any popular GitHub repositories.