Skip to content

Replace IResultMapper with ResultMapper base class - #94

Merged
bfarmer67 merged 5 commits into
developfrom
improve-result-mapper
Mar 19, 2026
Merged

Replace IResultMapper with ResultMapper base class#94
bfarmer67 merged 5 commits into
developfrom
improve-result-mapper

Conversation

@bfarmer67

@bfarmer67 bfarmer67 commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Description

  • Replace IResultMapper interface with ResultMapper base class with overridable virtual methods for each decision point:

    • MapException
    • GetStatusCode
    • MapValidationFailures
    • MapCancellation
    • MapSuccess
  • Add ResultMapper.Create() factory for one-off lambda customizations without subclassing

  • Add chainable IResult decorators: .WithHeader() and .WithNoContent()

  • Change default validation status code from 400 to 422 (RFC 9110)

  • Change default exception handling from throw to ProblemDetails 500

  • Null success results now return 404 NotFound instead of Ok(null)

  • Remove where TResult : class constraint from contentSelector overload

  • Rename selector parameter to contentSelector

Motivation

User code review identified three gaps in the result mapping API:

  • No way to decorate successful responses (e.g., ETag headers) without bypassing error handling
  • Hardcoded status codes in TryHandleInvalidCommand with no extension point
  • IResultMapper could not compose with selector overloads - no overload accepted both a mapper and a contentSelector

The user built 80 lines of custom extension methods as workarounds. With this PR, all three patterns reduce to:

// Single GET with ETag
return commandResult.ToResult( x => x?.ToPresentation(), conflictMapper )
    .WithHeader( "ETag", commandResult.Result?.VersionTag );

// Collection GET
return commandResult.ToResult( x => x.ToPresentationList() );

// Write with conflict handling
return commandResult.ToResult( conflictMapper ).WithNoContent();

Breaking changes

  • IResultMapper interface removed (released 2 days ago)
  • Default validation status code: 400 -> 422
  • Default exception handling: throws CommandException -> returns ProblemDetails 500 (override MapException returning null to restore throw behavior)
  • Null success result: Ok(null) -> NotFound

Tests

  • 68 AspNetCore tests covering all ResultMapper virtual methods, decorator chaining, cancellation, priority status codes, lambda factory, and all ToResult overloads
  • 228 total tests pass across net8.0, net9.0, net10.0

bfarmer67 and others added 5 commits March 19, 2026 10:51
- Replace IResultMapper interface with ResultMapper base class with
  virtual methods: MapException, GetStatusCode, MapValidationFailures,
  MapCancellation, MapSuccess<T>
- Add ResultMapper.Create() factory for one-off lambda customizations
- Change default validation status code from 400 to 422
- Change default exception handling from throw to ProblemDetails 500
- Add chainable IResult decorators: WithHeader, WithNoContent
- Add DecoratedResult internal class for response customization
- Rename selector parameter to contentSelector
- Null success results now return 404 NotFound instead of Ok(null)
- Update docs and tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DecoratedResult now flattens decorator chains so WithHeader can be
  called any number of times without nesting
- WithNoContent preserves all previously chained headers, not just the
  last one
- GetPriorityStatusCode is now public virtual and respects custom status
  codes from overridden GetStatusCode (uses Min for unknown codes)
- Non-generic ToResult now delegates to virtual MapSuccess() for
  consistency with generic overloads
- Added 68 AspNetCore tests covering all code paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…erload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bfarmer67
bfarmer67 merged commit 68b3806 into develop Mar 19, 2026
11 checks passed
bfarmer67 added a commit that referenced this pull request Mar 19, 2026
* chore: format code with dotnet format

* chore(nbgv): set version via CI: 2.7.0

* feat: add IResultMapper, IPipelineMiddlewareProvider, and PipelineFactory.Create

Issue 1 - Smarter ToResult:
- Add IResultMapper interface for DI-injectable cross-cutting result mapping
- Add resultMapper lambda overloads to ToResult for inline error/success handling
- Both generic CommandResult<T> and non-generic CommandResult variants supported

Issue 2 - Injectable pipeline middleware:
- Add IPipelineMiddlewareProvider interface with Hooks and Wraps properties
- Add UseHooks/UseWraps builder extensions for explicit middleware application
- Add PipelineFactory.Create convenience method that applies hooks after Start
  and wraps before Build automatically
- Add WithMiddlewareProvider to PipelineContextFactoryFixture for test support
- Add DefaultMiddlewareProvider static property for test-wide defaults

* docs: add middleware provider and result mapper examples to README, index, and validation docs

* chore: format code with dotnet format

* feat: add PipelineFactory.Create overload and documentation

- Add a convenience `Create` method for building pipelines using a configuration function.
- Add XML documentation to `Start` and `Create` methods to clarify their usage and monadic nature.
- Add null validation for the middleware provider in the existing `Create` overload.

* fix: add null and empty guards for Hooks and Wraps in MiddlewareProviderBuilder

* Inject pipeline middleware (#90)

* feat: add IResultMapper, IPipelineMiddlewareProvider, and PipelineFactory.Create

Issue 1 - Smarter ToResult:
- Add IResultMapper interface for DI-injectable cross-cutting result mapping
- Add resultMapper lambda overloads to ToResult for inline error/success handling
- Both generic CommandResult<T> and non-generic CommandResult variants supported

Issue 2 - Injectable pipeline middleware:
- Add IPipelineMiddlewareProvider interface with Hooks and Wraps properties
- Add UseHooks/UseWraps builder extensions for explicit middleware application
- Add PipelineFactory.Create convenience method that applies hooks after Start
  and wraps before Build automatically
- Add WithMiddlewareProvider to PipelineContextFactoryFixture for test support
- Add DefaultMiddlewareProvider static property for test-wide defaults

* docs: add middleware provider and result mapper examples to README, index, and validation docs

* feat: add PipelineFactory.Create overload and documentation

- Add a convenience `Create` method for building pipelines using a configuration function.
- Add XML documentation to `Start` and `Create` methods to clarify their usage and monadic nature.
- Add null validation for the middleware provider in the existing `Create` overload.

* fix: add null and empty guards for Hooks and Wraps in MiddlewareProviderBuilder

* chore(nbgv): set version via CI: 2.8.0

* test: update Create test to expect ArgumentNullException for null provider

Update the PipelineFactory.Create test to reflect recent changes that added null validation for the middleware provider.

* chore: format code with dotnet format

* chore(nbgv): set version via CI: 2.8.1

* fix: allow null provider in PipelineFactory.Create for consistency with UseHooks/UseWraps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(nbgv): set version via CI: 2.8.2

* Replace IResultMapper with ResultMapper base class (#94)

* feat: replace IResultMapper with ResultMapper base class

- Replace IResultMapper interface with ResultMapper base class with
  virtual methods: MapException, GetStatusCode, MapValidationFailures,
  MapCancellation, MapSuccess<T>
- Add ResultMapper.Create() factory for one-off lambda customizations
- Change default validation status code from 400 to 422
- Change default exception handling from throw to ProblemDetails 500
- Add chainable IResult decorators: WithHeader, WithNoContent
- Add DecoratedResult internal class for response customization
- Rename selector parameter to contentSelector
- Null success results now return 404 NotFound instead of Ok(null)
- Update docs and tests

* chore: format code with dotnet format

* fix: production-grade quality improvements to ResultMapper

- DecoratedResult now flattens decorator chains so WithHeader can be
  called any number of times without nesting
- WithNoContent preserves all previously chained headers, not just the
  last one
- GetPriorityStatusCode is now public virtual and respects custom status
  codes from overridden GetStatusCode (uses Min for unknown codes)
- Non-generic ToResult now delegates to virtual MapSuccess() for
  consistency with generic overloads
- Added 68 AspNetCore tests covering all code paths

* chore: format code with dotnet format

* refactor: remove unnecessary class constraint from contentSelector overload

* chore(nbgv): set version via CI: 2.9.0
bfarmer67 added a commit that referenced this pull request Mar 19, 2026
* chore: format code with dotnet format

* chore(nbgv): set version via CI: 2.7.0

* feat: add IResultMapper, IPipelineMiddlewareProvider, and PipelineFactory.Create

Issue 1 - Smarter ToResult:
- Add IResultMapper interface for DI-injectable cross-cutting result mapping
- Add resultMapper lambda overloads to ToResult for inline error/success handling
- Both generic CommandResult<T> and non-generic CommandResult variants supported

Issue 2 - Injectable pipeline middleware:
- Add IPipelineMiddlewareProvider interface with Hooks and Wraps properties
- Add UseHooks/UseWraps builder extensions for explicit middleware application
- Add PipelineFactory.Create convenience method that applies hooks after Start
  and wraps before Build automatically
- Add WithMiddlewareProvider to PipelineContextFactoryFixture for test support
- Add DefaultMiddlewareProvider static property for test-wide defaults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add middleware provider and result mapper examples to README, index, and validation docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: format code with dotnet format

* feat: add PipelineFactory.Create overload and documentation

- Add a convenience `Create` method for building pipelines using a configuration function.
- Add XML documentation to `Start` and `Create` methods to clarify their usage and monadic nature.
- Add null validation for the middleware provider in the existing `Create` overload.

* fix: add null and empty guards for Hooks and Wraps in MiddlewareProviderBuilder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Inject pipeline middleware (#90)

* feat: add IResultMapper, IPipelineMiddlewareProvider, and PipelineFactory.Create

Issue 1 - Smarter ToResult:
- Add IResultMapper interface for DI-injectable cross-cutting result mapping
- Add resultMapper lambda overloads to ToResult for inline error/success handling
- Both generic CommandResult<T> and non-generic CommandResult variants supported

Issue 2 - Injectable pipeline middleware:
- Add IPipelineMiddlewareProvider interface with Hooks and Wraps properties
- Add UseHooks/UseWraps builder extensions for explicit middleware application
- Add PipelineFactory.Create convenience method that applies hooks after Start
  and wraps before Build automatically
- Add WithMiddlewareProvider to PipelineContextFactoryFixture for test support
- Add DefaultMiddlewareProvider static property for test-wide defaults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add middleware provider and result mapper examples to README, index, and validation docs

* feat: add PipelineFactory.Create overload and documentation

- Add a convenience `Create` method for building pipelines using a configuration function.
- Add XML documentation to `Start` and `Create` methods to clarify their usage and monadic nature.
- Add null validation for the middleware provider in the existing `Create` overload.

* fix: add null and empty guards for Hooks and Wraps in MiddlewareProviderBuilder

* chore(nbgv): set version via CI: 2.8.0

* test: update Create test to expect ArgumentNullException for null provider

Update the PipelineFactory.Create test to reflect recent changes that added null validation for the middleware provider.

* chore: format code with dotnet format

* chore(nbgv): set version via CI: 2.8.1

* fix: allow null provider in PipelineFactory.Create for consistency with UseHooks/UseWraps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(nbgv): set version via CI: 2.8.2

* Replace IResultMapper with ResultMapper base class (#94)

* feat: replace IResultMapper with ResultMapper base class

- Replace IResultMapper interface with ResultMapper base class with
  virtual methods: MapException, GetStatusCode, MapValidationFailures,
  MapCancellation, MapSuccess<T>
- Add ResultMapper.Create() factory for one-off lambda customizations
- Change default validation status code from 400 to 422
- Change default exception handling from throw to ProblemDetails 500
- Add chainable IResult decorators: WithHeader, WithNoContent
- Add DecoratedResult internal class for response customization
- Rename selector parameter to contentSelector
- Null success results now return 404 NotFound instead of Ok(null)
- Update docs and tests

* chore: format code with dotnet format

* fix: production-grade quality improvements to ResultMapper

- DecoratedResult now flattens decorator chains so WithHeader can be
  called any number of times without nesting
- WithNoContent preserves all previously chained headers, not just the
  last one
- GetPriorityStatusCode is now public virtual and respects custom status
  codes from overridden GetStatusCode (uses Min for unknown codes)
- Non-generic ToResult now delegates to virtual MapSuccess() for
  consistency with generic overloads
- Added 68 AspNetCore tests covering all code paths

* chore: format code with dotnet format

* refactor: remove unnecessary class constraint from contentSelector overload

* chore(nbgv): set version via CI: 2.9.0

* chore(nbgv): set version via CI: 2.9.1

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@bfarmer67
bfarmer67 deleted the improve-result-mapper branch March 19, 2026 20:47
bfarmer67 added a commit that referenced this pull request Apr 12, 2026
* chore: format code with dotnet format

* chore(nbgv): set version via CI: 2.7.0

* feat: add IResultMapper, IPipelineMiddlewareProvider, and PipelineFactory.Create

Issue 1 - Smarter ToResult:
- Add IResultMapper interface for DI-injectable cross-cutting result mapping
- Add resultMapper lambda overloads to ToResult for inline error/success handling
- Both generic CommandResult<T> and non-generic CommandResult variants supported

Issue 2 - Injectable pipeline middleware:
- Add IPipelineMiddlewareProvider interface with Hooks and Wraps properties
- Add UseHooks/UseWraps builder extensions for explicit middleware application
- Add PipelineFactory.Create convenience method that applies hooks after Start
  and wraps before Build automatically
- Add WithMiddlewareProvider to PipelineContextFactoryFixture for test support
- Add DefaultMiddlewareProvider static property for test-wide defaults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add middleware provider and result mapper examples to README, index, and validation docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: format code with dotnet format

* feat: add PipelineFactory.Create overload and documentation

- Add a convenience `Create` method for building pipelines using a configuration function.
- Add XML documentation to `Start` and `Create` methods to clarify their usage and monadic nature.
- Add null validation for the middleware provider in the existing `Create` overload.

* fix: add null and empty guards for Hooks and Wraps in MiddlewareProviderBuilder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Inject pipeline middleware (#90)

* feat: add IResultMapper, IPipelineMiddlewareProvider, and PipelineFactory.Create

Issue 1 - Smarter ToResult:
- Add IResultMapper interface for DI-injectable cross-cutting result mapping
- Add resultMapper lambda overloads to ToResult for inline error/success handling
- Both generic CommandResult<T> and non-generic CommandResult variants supported

Issue 2 - Injectable pipeline middleware:
- Add IPipelineMiddlewareProvider interface with Hooks and Wraps properties
- Add UseHooks/UseWraps builder extensions for explicit middleware application
- Add PipelineFactory.Create convenience method that applies hooks after Start
  and wraps before Build automatically
- Add WithMiddlewareProvider to PipelineContextFactoryFixture for test support
- Add DefaultMiddlewareProvider static property for test-wide defaults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add middleware provider and result mapper examples to README, index, and validation docs

* feat: add PipelineFactory.Create overload and documentation

- Add a convenience `Create` method for building pipelines using a configuration function.
- Add XML documentation to `Start` and `Create` methods to clarify their usage and monadic nature.
- Add null validation for the middleware provider in the existing `Create` overload.

* fix: add null and empty guards for Hooks and Wraps in MiddlewareProviderBuilder

* chore(nbgv): set version via CI: 2.8.0

* test: update Create test to expect ArgumentNullException for null provider

Update the PipelineFactory.Create test to reflect recent changes that added null validation for the middleware provider.

* chore: format code with dotnet format

* chore(nbgv): set version via CI: 2.8.1

* fix: allow null provider in PipelineFactory.Create for consistency with UseHooks/UseWraps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(nbgv): set version via CI: 2.8.2

* Replace IResultMapper with ResultMapper base class (#94)

* feat: replace IResultMapper with ResultMapper base class

- Replace IResultMapper interface with ResultMapper base class with
  virtual methods: MapException, GetStatusCode, MapValidationFailures,
  MapCancellation, MapSuccess<T>
- Add ResultMapper.Create() factory for one-off lambda customizations
- Change default validation status code from 400 to 422
- Change default exception handling from throw to ProblemDetails 500
- Add chainable IResult decorators: WithHeader, WithNoContent
- Add DecoratedResult internal class for response customization
- Rename selector parameter to contentSelector
- Null success results now return 404 NotFound instead of Ok(null)
- Update docs and tests

* chore: format code with dotnet format

* fix: production-grade quality improvements to ResultMapper

- DecoratedResult now flattens decorator chains so WithHeader can be
  called any number of times without nesting
- WithNoContent preserves all previously chained headers, not just the
  last one
- GetPriorityStatusCode is now public virtual and respects custom status
  codes from overridden GetStatusCode (uses Min for unknown codes)
- Non-generic ToResult now delegates to virtual MapSuccess() for
  consistency with generic overloads
- Added 68 AspNetCore tests covering all code paths

* chore: format code with dotnet format

* refactor: remove unnecessary class constraint from contentSelector overload

* chore(nbgv): set version via CI: 2.9.0

* chore(nbgv): set version via CI: 2.9.1

* Fix WithNoContent to preserve error results instead of silently replacing them (#97)

WithNoContent unconditionally replaced any IResult with 204 NoContent,
including validation errors (422), unauthorized (401), not found (404),
and server errors (500). Now only confirmed 2xx results are replaced;
errors and unknown status codes pass through unchanged.

* chore(nbgv): set version via CI: 2.10-alpha

* Fix nullable reference type warnings in tests

Use the null-forgiving operator when passing null to methods in CommandResultExtensionsTests and ResultMapperTests to satisfy compiler nullability checks.

* chore(nbgv): set version via CI: 2.11.0

* Comments

* chore(nbgv): set version via CI: 2.11.1

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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