Skip to content

Make Response and InvokeAsync non-nullable; unify request/response constraints#19

Merged
marklauter merged 1 commit into
mainfrom
lauterm/non-nullable-response
Jun 30, 2026
Merged

Make Response and InvokeAsync non-nullable; unify request/response constraints#19
marklauter merged 1 commit into
mainfrom
lauterm/non-nullable-response

Conversation

@marklauter

Copy link
Copy Markdown
Owner

Summary

Fixes a long-standing design flaw: RequestContext.Response was TResponse? and InvokeAsync returned Task<TResponse?>, forcing every caller to null-check a response the pipeline is expected to produce.

  • Constrain TResponse : notnull across the core, the static factories, the RequestMiddleware delegate, and PlumberApplicationFactory.
  • RequestContext.Response is now a non-nullable TResponse (initialized to default!); InvokeAsync (handler + factory) returns Task<TResponse>.
  • Unify the request constraint: the Diagnostics and Serilog extensions used TRequest : class while the core used TRequest : notnull. That blocked value-type requests (e.g. int) from compiling against the diagnostics/logging middleware even though the core pipeline accepts them. Nothing in those bodies relied on reference semantics, so classnotnull everywhere.

Request and response are now symmetric — any non-null value or reference type, with Unit as the "nothing" type on either side.

Request Response
Constraint TRequest : notnull TResponse : notnull
Allowed any value/reference type any value/reference type
"nothing" Unit Unit
Disallowed null null

Note on runtime semantics

The notnull constraint stops callers supplying a nullable type argument, and the property/return read as non-null. There is no runtime guard forcing middleware to assign Response — a reference-type pipeline that returns without setting it yields default! typed as non-null. This is deliberate: Unit event-style pipelines rely on default(Unit) being the valid sole value, and it matches the ASP.NET HttpContext.Response convention.

Breaking change

Public signatures of RequestContext.Response, InvokeAsync, and the generic constraints on the extension methods change. Major version bump.

Verification

  • dotnet build — clean, 0 warnings
  • dotnet format --severity info --verify-no-changes — clean
  • dotnet test — 179 passed, 0 failed, 100% line/branch/method coverage

🤖 Generated with Claude Code

…constraints

Response on RequestContext was TResponse? and InvokeAsync returned
Task<TResponse?>, forcing every caller to null-check a response the
pipeline is expected to produce. Constrain TResponse : notnull and make
Response a non-nullable TResponse (initialized to default!), so InvokeAsync
returns Task<TResponse>.

Also unify the request constraint: the Diagnostics and Serilog extensions
used TRequest : class while the core used TRequest : notnull, which blocked
value-type requests (e.g. int) from compiling against the diagnostics/
logging middleware even though the core pipeline accepts them. Nothing in
those bodies relied on reference semantics, so relax class -> notnull
everywhere. Request and response are now symmetric: any non-null value or
reference type, with Unit as the "nothing" type on either side.

BREAKING CHANGE: public signatures of RequestContext.Response, InvokeAsync,
and the generic constraints on the extension methods change. Major version
bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@marklauter marklauter merged commit f766196 into main Jun 30, 2026
8 checks passed
@marklauter marklauter deleted the lauterm/non-nullable-response branch June 30, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant