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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DcsvIo.D2.Handler" Version="0.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DcsvIo.D2.Handler" Version="0.1.2" />
                    
Directory.Packages.props
<PackageReference Include="DcsvIo.D2.Handler" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DcsvIo.D2.Handler --version 0.1.2
                    
#r "nuget: DcsvIo.D2.Handler, 0.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DcsvIo.D2.Handler@0.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DcsvIo.D2.Handler&version=0.1.2
                    
Install as a Cake Addin
#tool nuget:?package=DcsvIo.D2.Handler&version=0.1.2
                    
Install as a Cake Tool

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

  1. Resolve options: per-call → DefaultOptions → platform defaults
  2. Scope pre-check: if ScopeRequirement is non-null with a non-empty Scopes set, checks Context.Request.Scopes using the declared HandlerScopeMatchAny (at least one overlap) or All (every scope present); skip when ScopeRequirement is null or Scopes is empty; mismatch → increments HandlerTelemetry.SR_Invoked + SR_Failed, skips activity span and duration recording, returns D2Result.Forbidden
  3. 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
  4. Counter: HandlerTelemetry.SR_Invoked.Add(1, handlerNameTag)
  5. ExecuteAsync inside try/catch
  6. Stopwatch + threshold logging: warn at SlowThreshold, error at CriticalThreshold
  7. Result handling:
    • Success → Succeeded counter + debug log
    • Failure → Failed counter + debug log
    • OperationCanceledException with our ct canceledD2Result.Canceled + info log (intentional caller cancellation)
    • OperationCanceledException without our ct canceledD2Result.ServiceUnavailable + warn log (downstream timeout — HttpClient timeout, SQL command timeout, internal handler watchdog)
    • Other exception → D2Result.UnhandledException + error log + activity status set
  8. 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 IRequestContext from the validated bearer + ambient request data
  • RabbitMQ consumer: the consuming service's consumer pipeline builds IRequestContext from 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.AbstractionsIHandler, HandlerOptions, IHandlerContext
  • DcsvIo.D2.Context.Abstractions — request context
  • DcsvIo.D2.Result — result type
  • Microsoft.Extensions.DependencyInjection.Abstractions / Microsoft.Extensions.Logging.Abstractions

  • DcsvIo.D2.Handler.AbstractionsIHandler + HandlerOptions + IHandlerContext
  • DcsvIo.D2.Handler.Repo — EF/PG exception remapping subclass
  • DcsvIo.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
0.1.2 137 7/17/2026
0.1.1 144 7/17/2026