Common Middleware

This page catalogs Benzene's general-purpose, transport-agnostic .Use*() middleware — the building blocks you'll reach for on almost any pipeline, regardless of whether it's running on AWS Lambda, Azure Functions, ASP.NET Core, or a self-hosted worker. Transport-specific middleware (UseApiGateway, UseSqs, UseSns, UseKafka, UseEventHub, ...) is covered in the platform getting-started guides instead.

Every entry below has been verified against the current source in src/. Where a deeper reference page already exists (health checks, validation, monitoring), this page gives the essentials and links out for the full story.

Table of contents


UseTimer

Package: Benzene.Diagnostics (Benzene.Diagnostics.Timers.Extensions)

Opens a named Activity span around the rest of the pipeline. Every middleware already gets its own Activity automatically via AddDiagnostics()UseTimer is for naming a specific stage explicitly so it stands out in an exported trace. Internally it resolves the registered IProcessTimerFactory (the AddDiagnostics()-registered default, ActivityProcessTimerFactory, opens a real Activity); if none is registered it's a no-op wrapper around next().

public static IMiddlewarePipelineBuilder<TContext> UseTimer<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, string timerName)

// overload: report elapsed milliseconds yourself instead of via IProcessTimerFactory
public static IMiddlewarePipelineBuilder<TContext> UseTimer<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, Action<TContext, long> onTimer)
app.UseTimer("benzene-message-application");

// or handle the elapsed time yourself:
app.UseTimer((context, elapsedMs) => myMetrics.Record(elapsedMs));

See also: Monitoring — Named timers.


UseBenzeneEnrichment

Package: Benzene.Diagnostics (Benzene.Diagnostics.EnrichmentExtensions)

One portable, explicit-opt-in call that attaches invocationId, traceId, spanId, topic, transport, and handler to the logging scope (via ILogger.BeginScope) for the duration of the request, and tags the current Activity with benzene.invocationId — all in a single method that works the same way on AWS Lambda, Azure Functions, and ASP.NET Core. Each key is resolved independently and simply omitted if its backing service isn't registered for that pipeline (for example, invocationId requires UseBenzeneInvocation() to have been called on this pipeline or an outer one; it's omitted inside a per-message SQS/SNS/Kafka sub-pipeline, since IBenzeneInvocation doesn't flow into those nested DI scopes today).

public static IMiddlewarePipelineBuilder<TContext> UseBenzeneEnrichment<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app)
app.UseBenzeneEnrichment();

When to use it: on every platform. It replaced the AWS-only WithRequestId()/WithApplication() log-context extensions, which have been removed.


UseBenzeneMetrics

Package: Benzene.Diagnostics (Benzene.Diagnostics.MetricsExtensions)

Records benzene.messages.processed (a count) and benzene.message.duration (in milliseconds) for the wrapped pipeline stage, tagged by topic, transport, and result (success/failure, read from IHasMessageResult when the context implements it). Unlike the automatic per-middleware Activity spans from AddDiagnostics(), this is once-per-message granularity and must be added explicitly around the stage you want measured.

public static IMiddlewarePipelineBuilder<TContext> UseBenzeneMetrics<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app)
app.UseBenzeneMetrics();

Wire Benzene.OpenTelemetry's AddBenzeneInstrumentation() against an OTel MeterProviderBuilder to actually export these to a real backend — see Monitoring — OpenTelemetry.


UseW3CTraceContext

Package: Benzene.Diagnostics (Benzene.Diagnostics.W3CTraceContextExtensions)

Reads the traceparent/tracestate headers (matched case-insensitively) and starts the pipeline's root Activity with the parsed remote context as its parent, so distributed traces continue across services instead of each hop starting a new, disconnected trace. Falls back to a normal, parentless root span when the headers are missing or fail to parse — it's always safe to add.

public static IMiddlewarePipelineBuilder<TContext> UseW3CTraceContext<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app)
app.UseW3CTraceContext();

Add this as the FIRST middleware in the pipeline — everything added after it inherits this Activity as the ambient Activity.Current parent, so every automatically-wrapped middleware span from AddDiagnostics() correctly nests under the remote trace.

Only wired for HTTP-based transports today (ASP.NET Core, Azure Functions' ASP.NET-style trigger, API Gateway) — SQS/SNS/Kafka/Event Hub inbound extraction is not yet implemented. To propagate a trace to a downstream Benzene service, see .UseW3CTraceContext() on an outbound route, described in Monitoring — Distributed Tracing.


UseBenzeneInvocation

Package: Benzene.Core.Middleware (Benzene.Core.Middleware.BenzeneInvocationExtensions)

Builds and exposes an IBenzeneInvocation for the duration of the request so it can be injected wherever needed, and so UseBenzeneEnrichment() can populate invocationId. You don't normally call the core overload directly — hosting platforms expose their own zero-argument overload that supplies the factory (e.g. Benzene.Aws.Lambda.Core's or Benzene.AspNet.Core's UseBenzeneInvocation()).

public static IMiddlewarePipelineBuilder<TContext> UseBenzeneInvocation<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app,
    Func<IServiceResolver, TContext, IBenzeneInvocation> factory)
// on AWS Lambda / ASP.NET Core, just call the platform's zero-arg overload:
app.UseBenzeneInvocation();

Requesting IBenzeneInvocation from DI before this middleware has run for the current invocation throws a BenzeneException — it's only populated for the duration of the request this middleware wraps.


UseHealthCheck

Package: Benzene.HealthChecks (Benzene.HealthChecks.Extensions)

Lets health checks be triggered by sending a message on a given topic. By default, "healthcheck" always matches too (alongside whatever topic you register), so a single service's own health check is always reachable at the default topic while you're free to also expose it under something like "<service-name>:healthcheck" for cross-service calls.

public static IMiddlewarePipelineBuilder<TContext> UseHealthCheck<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, string topic, params IHealthCheck[] healthChecks)

public static IMiddlewarePipelineBuilder<TContext> UseHealthCheck<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, string topic, Action<IHealthCheckBuilder> action)

public static IMiddlewarePipelineBuilder<TContext> UseHealthCheck<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, string topic, IHealthCheckBuilder builder)
app.UseHealthCheck("healthcheck", x => x
    .AddHealthCheck<MyDatabaseHealthCheck>()
    .AddHealthCheck(resolver => resolver.GetService<MyQueueHealthCheck>()));

// or pass already-constructed instances directly:
app.UseHealthCheck("healthcheck", new MyDatabaseHealthCheck(), new MyQueueHealthCheck());

IHealthCheckBuilder only supports AddHealthCheck<THealthCheck>() (resolved from DI) and AddHealthCheck(Func<IServiceResolver, IHealthCheck>) directly — plus the AddHealthChecks(params IHealthCheck[]) and AddHealthCheckFactory(IHealthCheckFactory) extension helpers built on top of those.

On HTTP-based transports (ASP.NET Core, API Gateway, Benzene.SelfHost.Http), an additional overload lets you match on HTTP method and path instead of (or as well as) a topic — see Benzene.SelfHost.Http.Extensions.UseHealthCheck(method, path, ...).

See also: Health Checks for a full worked example.


UseSpec

Package: Benzene.Schema.OpenApi (Benzene.Schema.OpenApi.Extensions)

Registers a handler that serves OpenAPI/AsyncAPI (and Benzene's own code-gen) schemas for the service under the given topic ("spec" by default). This is essential if you want to use the Benzene command-line code-gen tools against this service.

public static IMiddlewarePipelineBuilder<TContext> UseSpec<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app)                       // uses topic "spec"

public static IMiddlewarePipelineBuilder<TContext> UseSpec<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, string topic)
app.UseSpec("spec");

See also: Spec for the request/response format.


UseMessageHandlers

Package: Benzene.Core.MessageHandlers (Benzene.Core.MessageHandlers.MiddlewarePipelineExtensions)

The middleware that routes the raw message to a message handler, by pulling out the topic and deserializing the payload. You can add additional middleware to the message router itself — most commonly validation — via the router => callback.

public static IMiddlewarePipelineBuilder<TContext> UseMessageHandlers<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app)
    // scans AppDomain.CurrentDomain.GetAssemblies()

public static IMiddlewarePipelineBuilder<TContext> UseMessageHandlers<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, params Assembly[] assemblies)

public static IMiddlewarePipelineBuilder<TContext> UseMessageHandlers<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, params Type[] types)

public static IMiddlewarePipelineBuilder<TContext> UseMessageHandlers<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, Action<MessageRouterBuilder> router)

public static IMiddlewarePipelineBuilder<TContext> UseMessageHandlers<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, Assembly assembly, Action<MessageRouterBuilder> router)

public static IMiddlewarePipelineBuilder<TContext> UseMessageHandlers<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, Assembly[] assemblies, Action<MessageRouterBuilder> router)

public static IMiddlewarePipelineBuilder<TContext> UseMessageHandlers<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, Type[] types, Action<MessageRouterBuilder> router)
app.UseMessageHandlers(router => router
    .UseFluentValidation());

See also: Message Handlers.


UsePresetTopic

Package: Benzene.Core.MessageHandlers (Benzene.Core.MessageHandlers.MiddlewarePipelineExtensions)

Routes every message on this one pipeline to a fixed topic, regardless of what (if anything) the transport message itself carries. For a queue or subscription whose producer isn't a Benzene client and never sets the usual topic message attribute/property (a raw SQS send, a Service Bus topic fed by another system, etc.) — call it before UseMessageHandlers(), on that specific pipeline only. A queue whose producer does send a proper topic just omits it and is unaffected.

public static IMiddlewarePipelineBuilder<TContext> UsePresetTopic<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, string topicId, string version = "")
// A queue whose producer never sets a topic attribute at all:
app.UseSqs(sqs => sqs
    .UsePresetTopic("orders.created")
    .UseMessageHandlers());

// A second queue in the same app whose producer DOES send a proper topic just omits it:
app.UseSqs(otherQueueConfig, sqs => sqs
    .UseMessageHandlers());

Once set, the preset topic always wins for that pipeline's messages — even if a message happens to carry a stray topic attribute — so there's no separate "fallback vs. override" mode to configure.

Implementation note for anyone extending Benzene with a new transport: the preset topic is carried in a small scoped (per-message) DI service, not a property on the transport context — a context type stays a pure description of the transport message, per this repo's context-purity convention. UsePresetTopic<TContext> has no constraints on TContext at all, so any transport gets this for free by registering PresetTopicMessageTopicGetter<TContext> (decorating its own real topic getter) as the default IMessageTopicGetter<TContext>, alongside a services.TryAddScoped<PresetTopicHolder>() call — no context changes required. Benzene.Aws.Lambda.Sqs, Benzene.Aws.Sqs, and Benzene.Azure.Function.ServiceBus are the reference implementations.


UseFluentValidation

Package: Benzene.FluentValidation (Benzene.FluentValidation.DependencyExtensions)

Nests inside UseMessageHandlers(router => ...). Attempts to find a FluentValidation IValidator<T> for the request type; if a validator is found and validation fails, it short-circuits with a validation-failure result before the request ever reaches the message handler. If no validator is registered for the type, the request passes straight through.

public static IMessageRouterBuilder UseFluentValidation(
    this IMessageRouterBuilder builder, params Assembly[] assemblies)

public static IMessageRouterBuilder UseFluentValidation(
    this IMessageRouterBuilder builder, Type[] types)
app.UseMessageHandlers(router => router
    .UseFluentValidation());

See also: Fluent Validation.


UseJsonSchema

Package: Benzene.JsonSchema (Benzene.JsonSchema.Extensions)

Generates a JSON Schema (draft 2020-12, via JsonSchema.Net.Generation) from the request type of the handler registered for the current topic, and validates the incoming payload against it before the handler runs. Uses camelCase property names to match the default serializer. If the topic has no registered handler, validation is skipped. Generated schemas are cached per request type.

public static IMiddlewarePipelineBuilder<TContext> UseJsonSchema<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app)
    where TContext : class
app.UseJsonSchema();

Register a custom IJsonSchemaProvider<TContext> if you need schema generation behavior other than the default.


UseLogResult / UseLogContext

Package: Benzene.Core.Middleware (Benzene.Core.Middleware.LoggerExtensions)

Both attach properties to the logging scope (via ILogger.BeginScope) for the duration of the request, configured through the same ILogContextBuilder<TContext> fluent builder:

public static IMiddlewarePipelineBuilder<TContext> UseLogResult<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, Action<ILogContextBuilder<TContext>> action)

public static IMiddlewarePipelineBuilder<TContext> UseLogContext<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, Action<ILogContextBuilder<TContext>> action)
app.UseLogResult(x => x
    .WithCorrelationId()
    .WithTopic()
    .WithTransport());

Log-context builder extensions (.With*())

These configure what UseLogResult/UseLogContext attach to the scope:

Extension Package Adds
WithCorrelationId() Benzene.Diagnostics (Correlation.Extensions) the current ICorrelationId value, keyed correlationId
WithTopic() Benzene.Core.MessageHandlers the current message's topic, keyed topic ("<missing>" if unresolvable)
WithTransport() Benzene.Core.MessageHandlers the current ICurrentTransport.Name, keyed transport
WithHeaders(params string[] headers) Benzene.Core.MessageHandlers the named request headers, each keyed by its own header name

The AWS-only WithRequestId() and WithApplication() log-context extensions (Benzene.Aws.Lambda.Core.LogContextBuilderExtensions) have been removed — use UseBenzeneEnrichment(), which attaches the equivalent invocationId key on every platform, not just AWS Lambda. (The still-current WithApplication() in Benzene.Core.MessageHandlers is IApplicationInfo-based and transport-agnostic, and is a different method — it remains.)

app.UseBenzeneEnrichment();

See also: Monitoring — Logging.


UseExceptionHandler

Package: Benzene.Core.Middleware (Benzene.Core.Middleware.Extensions)

Adds centralized exception handling around the rest of the pipeline: any exception thrown by downstream middleware or the handler is caught and passed to onException, letting you log it, transform it into an error response on the context, or otherwise handle it in a context-aware way.

public static IMiddlewarePipelineBuilder<TContext> UseExceptionHandler<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, Action<TContext, Exception> onException)
app.UseExceptionHandler((context, exception) =>
{
    // e.g. log it and/or map it onto the context's result
});

UseRetry

Package: Benzene.Resilience (Benzene.Resilience.Extensions)

Wraps the rest of the pipeline in a retry loop with exponential backoff. Retries on any exception except OperationCanceledException by default, and does not retry a successful result unless you supply shouldRetryContext. This is a small, hand-rolled retry loop — it does not depend on Polly.

public static IMiddlewarePipelineBuilder<TContext> UseRetry<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app,
    int numberOfRetries = 3,
    TimeSpan? initialDelay = null,      // defaults to 200ms
    double backoffFactor = 2.0,
    Func<Exception, bool>? shouldRetry = null,          // defaults to "retry unless OperationCanceledException"
    Func<TContext, bool>? shouldRetryContext = null,     // defaults to "never retry a completed result"
    Func<TimeSpan, Task>? delay = null)                  // defaults to Task.Delay
app.UseRetry(numberOfRetries: 5, initialDelay: TimeSpan.FromMilliseconds(100));

UseCors

Package: Benzene.Http (Benzene.Http.Cors.Extensions)

Adds CORS handling for HTTP-based contexts: validates the Origin header against your configured allowed domains/headers and automatically answers preflight OPTIONS requests. Add it early in the pipeline so CORS headers get applied to every response.

public static IMiddlewarePipelineBuilder<TContext> UseCors<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, CorsSettings corsSettings)
    where TContext : IHttpContext
app.UseCors(new CorsSettings
{
    AllowedDomains = new[] { "https://example.com" },
    AllowedHeaders = new[] { "Content-Type", "Authorization" },
    ExposedHeaders = new[] { "X-Total-Count" },  // adds Access-Control-Expose-Headers
    AllowCredentials = true,   // adds Access-Control-Allow-Credentials: true
    MaxAgeSeconds = 600,       // adds Access-Control-Max-Age on preflight responses
});

Applies to any TContext : IHttpContext — ASP.NET Core, API Gateway, Benzene.SelfHost.Http, etc. Behavior tracks the CORS specification the same way Microsoft.AspNetCore.Cors does:


UseOAuth2Bearer

Package: Benzene.Auth.OAuth2 (Benzene.Auth.OAuth2.Extensions)

OAuth2 bearer token (JWT) validation for services that have no security-terminating gateway (API Gateway, a load balancer with auth, etc.) in front of them. Reads Authorization: Bearer <token>, validates it against a JWKS endpoint (via OIDC discovery or a bare JWKS URI), and either short-circuits with Unauthorized or sets the authenticated caller for later pipeline steps. See the Authentication Patterns cookbook for a full worked example.

public static IMiddlewarePipelineBuilder<TContext> UseOAuth2Bearer<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, OAuth2BearerOptions options)
    where TContext : IHttpContext
app.UseOAuth2Bearer(new OAuth2BearerOptions
{
    Authority = "https://your-tenant.auth0.com/.well-known/openid-configuration",
    ValidIssuers = new[] { "https://your-tenant.auth0.com/" },
    ValidAudiences = new[] { "your-api-identifier" },
    ValidAlgorithms = new[] { "RS256" },
});

Applies to any TContext : IHttpContext. Authority/JwksUri, ValidIssuers, ValidAudiences, and ValidAlgorithms are all required — an empty allowlist would silently under-validate every token, so UseOAuth2Bearer throws at wire-up time rather than accepting tokens from any issuer/audience/algorithm. ValidAlgorithms in particular has no permissive default: a validator that trusted whatever alg a token claimed would be open to algorithm-confusion attacks (RFC 8725 §3.1). A failed validation always returns a generic Unauthorized detail — the real reason (bad signature, expired, wrong issuer/audience) is never echoed back to the caller, only logged server-side. On success, the validated claims are available to RequireScope (below) and to your own handlers via Benzene.Auth.Core.AuthenticationHolder.


UseBasicAuth

Package: Benzene.Auth.Basic (Benzene.Auth.Basic.Extensions)

RFC 7617 HTTP Basic authentication — the simplest option when you just need a username/password gate (a single service account, an internal admin surface) rather than full OAuth2. Validates the decoded credentials against your own IBasicAuthCredentialValidator; ships no default implementation, so there's no hardcoded-credential footgun to accidentally deploy.

public static IMiddlewarePipelineBuilder<TContext> UseBasicAuth<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app,
    IBasicAuthCredentialValidator validator, string realm = "Benzene")
    where TContext : IHttpContext
public class ServiceAccountValidator : IBasicAuthCredentialValidator
{
    public Task<ClaimsPrincipal?> ValidateAsync(string username, string password)
    {
        var expected = Environment.GetEnvironmentVariable("SERVICE_ACCOUNT_PASSWORD");
        if (username != "service-account" || password != expected)
        {
            return Task.FromResult<ClaimsPrincipal?>(null);
        }
        var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username) });
        return Task.FromResult<ClaimsPrincipal?>(new ClaimsPrincipal(identity));
    }
}

app.UseBasicAuth(new ServiceAccountValidator());

Applies to any TContext : IHttpContext. A missing/malformed header or a validator returning null short-circuits with Unauthorized and a WWW-Authenticate: Basic realm="..." challenge header (per RFC 7617, so browsers/HTTP clients actually prompt for credentials). Credentials are split on the first : only — a password containing : is preserved intact, not truncated.


RequireScope

Package: Benzene.Auth.OAuth2 (Benzene.Auth.OAuth2.Extensions)

Basic scope-based authorization: requires the caller authenticated by UseOAuth2Bearer, earlier in the pipeline, to hold at least one of the given scopes. Reads both the scope claim (RFC 8693, space-delimited) and the scp claim (Azure AD's convention — a space-delimited string or a JSON array, depending on issuer). This is intentionally the ceiling of Benzene's built-in authorization — roles, resource-based policies, and RBAC are application concerns layered on top of the ClaimsPrincipal this middleware exposes, not something Benzene provides itself.

public static IMiddlewarePipelineBuilder<TContext> RequireScope<TContext>(
    this IMiddlewarePipelineBuilder<TContext> app, params string[] anyOfScopes)
    where TContext : IHttpContext
app.UseOAuth2Bearer(oauth2Options)
   .RequireScope("orders:write");   // any one of the given scopes is sufficient

Applies to any TContext : IHttpContext, chained after UseOAuth2Bearer. No authenticated caller at all (no auth middleware ran, or it ran and failed) short-circuits with Unauthorized — a caller present but missing every requested scope short-circuits with Forbidden instead. These are deliberately distinct statuses (Unauthorized = not authenticated, Forbidden = authenticated but not permitted); collapsing them would leave API consumers unable to tell the two apart from a 403 alone.


UseXml

Package: Benzene.Xml (Benzene.Xml.DependencyInjectionExtensions)

Registers an XML IMediaFormat<TContext> (XmlMediaFormat<TContext>) alongside the default JSON one, so the pipeline can serialize/deserialize XML payloads in addition to JSON — which format applies to a given request/response is resolved per-message by IMediaFormatNegotiator<TContext> (content-type for reads, accept for writes).

public static IMiddlewarePipelineBuilder<TContext> UseXml<TContext>(
    this IMiddlewarePipelineBuilder<TContext> source)
    where TContext : class
app.UseXml();

UseMessagePack

Package: Benzene.MessagePack (Benzene.MessagePack.DependencyInjectionExtensions)

Registers a MessagePack IMediaFormat<TContext> (MessagePackMediaFormat<TContext>) alongside the default JSON one (and XML, if UseXml() is also called) — same per-message negotiation as UseXml(), via content-type/accept: application/msgpack. MessagePack is a genuinely binary format, but every Benzene transport's body is a string; MessagePackSerializer Base64-armors the msgpack bytes so it works unchanged through the existing string-based pipeline (see Benzene.MessagePack's CLAUDE.md for the full rationale) — a client sending/receiving MessagePack must Base64-decode/encode the body accordingly.

public static IMiddlewarePipelineBuilder<TContext> UseMessagePack<TContext>(
    this IMiddlewarePipelineBuilder<TContext> source)
    where TContext : class
app.UseMessagePack();

A note on removed packages

Benzene.Datadog, Benzene.Zipkin, and Benzene.Aws.XRay have been removed. Their vendor-specific timer backends are superseded by Benzene.Diagnostics's ActivityProcessTimerFactory, which backs UseTimer(name) with a real System.Diagnostics.Activity and works with any OpenTelemetry-compatible backend via Benzene.OpenTelemetry's AddBenzeneInstrumentation() — see Monitoring — OpenTelemetry.