ErrorOrAspNetCoreExtensions 2.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package ErrorOrAspNetCoreExtensions --version 2.0.0                
NuGet\Install-Package ErrorOrAspNetCoreExtensions -Version 2.0.0                
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="ErrorOrAspNetCoreExtensions" Version="2.0.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ErrorOrAspNetCoreExtensions --version 2.0.0                
#r "nuget: ErrorOrAspNetCoreExtensions, 2.0.0"                
#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.
// Install ErrorOrAspNetCoreExtensions as a Cake Addin
#addin nuget:?package=ErrorOrAspNetCoreExtensions&version=2.0.0

// Install ErrorOrAspNetCoreExtensions as a Cake Tool
#tool nuget:?package=ErrorOrAspNetCoreExtensions&version=2.0.0                

ErrorOrAspNetCoreExtensions 🔥

A collection of extension methods designed to reduce the amount of boilerplate code 🥱 needed when returning appropriate HTTP responses.

Significantly improves the developer experience of using discriminated unions in ASP.NET Core applications 😎

[!WARNING] Version 2.0.0 introduces breaking changes, i.e. problemDetails.Title property is now created from error.Code and problemDetails.Detail is created from error.Description.

Table of Contents

Installation

Via dotnet cli:

dotnet add package ErrorOrAspNetCoreExtensions

Or via package manager console:

Install-Package ErrorOrAspNetCoreExtensions

Example configuration:

builder.Services
       .AddProblemDetails(options =>
           options.CustomizeProblemDetails = ctx =>
           {
               ctx.ProblemDetails.Extensions.Add(
                   "instance",
                   $"{ctx.HttpContext.Request.Method} {ctx.HttpContext.Request.Path}"
               );
           }
       );

Usage:

When using the methods this package provides, errors are resolved like this by default:

  • ErrorType.Validation ⇒ 400 BadRequest
  • ErrorType.Unauthorized ⇒ 401 Unauthorized
  • ErrorType.Forbidden ⇒ 403 Forbidden
  • ErrorType.NotFound ⇒ 404 NotFound
  • ErrorType.Conflict ⇒ 409 Conflict
  • Any other type ⇒ 500 InternalServerError

All errors are returned in ProblemDetails format, like that:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "The requested todo item was not found.",
  "status": 404,
  "instance": "GET /api/todos/420",
  "trace-id": "0HN4IA8I0CGOG:00000001"
}

However, if you have some specific use case where you want to use ErrorOr's feature of custom errors, then you can register the appropriate HTTP status code in the error's metadata, using the key ErrorOrAspNetCoreExtensions.StatusCodeKey

I'm aware that you may not want to pollute your domain/application space with HTTP related stuff, but as these are only extension methods that are meant to reduce repeating logic, I can't do much more other than provide this little "hack".

If you have some scenario for which the default implementation doesn't suit your needs, then probably you want to handle it manually, or throw an exception either way.

ToOk extension methods

Method that just returns 200 OK:

app.MapGet(
    "/api/todos/{id:int}",
    async (
        int id,
        ISender mediator,
        IMapper mapper,
        CancellationToken cancellationToken
    ) =>
    {
        var query = new GetTodoQuery(id);
        var result = await mediator.Send(query, cancellationToken);

        return result.ToOkWithoutBody();
    }
);

Method that returns the service/query result directly without mapping:

app.MapGet(
    "/api/todos/{id:int}",
    async (
        int id,
        ISender mediator,
        IMapper mapper,
        CancellationToken cancellationToken
    ) =>
    {
        var query = new GetTodoQuery(id);
        var result = await mediator.Send(query, cancellationToken);

        return result.ToOk();
    }
);

Method that returns the response mapped to the API contract:

app.MapGet(
    "/api/todos/{id:int}",
    async (
        int id,
        ISender mediator,
        IMapper mapper,
        CancellationToken cancellationToken
    ) =>
    {
        var query = new GetTodoQuery(id);
        var result = await mediator.Send(query, cancellationToken);

        return result.ToOk(_mapper.Map<GetTodoResponse>);
    }
);

ToCreated extension methods

Method that just returns 201 Created:

app.MapPost(
    "/api/todos",
    async (
        CreateTodoRequest request,
        ISender mediator,
        IMapper mapper,
        CancellationToken cancellationToken
    ) =>
    {
        var command = mapper.Map<CreateTodoCommand>(request);
        var result = await mediator.Send(command, cancellationToken);

        return result.ToCreatedWithoutBody();
    }
);

Method that returns the service/command response directly:

app.MapPost(
    "/api/todos",
    async (
        CreateTodoRequest request,
        ISender mediator,
        IMapper mapper,
        CancellationToken cancellationToken
    ) =>
    {
        var command = mapper.Map<CreateTodoCommand>(request);
        var result = await mediator.Send(command, cancellationToken);

        // or you can construct the URI like that: value => new Uri($"/api/todos/{value.Id}")
        return result.ToCreated(value => $"/api/todos/{value.Id}");
    }
);

Method that returns mapped result to the API contract model:

app.MapPost(
    "/api/todos",
    async (
        CreateTodoRequest request,
        ISender mediator,
        IMapper mapper,
        CancellationToken cancellationToken
    ) =>
    {
        var command = mapper.Map<CreateTodoCommand>(request);
        var result = await mediator.Send(command, cancellationToken);

        // or you can construct the URI like that: value => new Uri($"/api/todos/{value.Id}")
        return result.ToCreated(value => $"/api/todos/{value.Id}", _mapper.Map<CreateTodoResponse>);
    }
);

ToNoContent extension method

app.MapDelete(
    "/api/todos/{id:int}",
    async (
        int id,
        ISender mediator,
        IMapper mapper,
        CancellationToken cancellationToken
    ) =>
    {
        var command = new DeleteTodoCommand(id);
        var result = await mediator.Send(command, cancellationToken);

        return result.ToNoContent();
    }
);

ToFileStream extension method

[!IMPORTANT] You don't need to worry about disposing the IFileStreamResult.FileContent Stream, because ASP.NET Core handles that for you under the hood when sending the HTTP response. For the curious, that behavior is defined in the FileStreamHttpResult.ExecuteAsync() method.

app.MapDelete(
    "/api/files/{id:int}",
    async (
        int id,
        ISender mediator,
        IMapper mapper,
        CancellationToken cancellationToken
    ) =>
    {
        // service/query should return a response that implements the IFileStreamResult (see below)
        var query = new GetFileQuery(id);
        var result = await mediator.Send(query, cancellationToken);

        // you can also pass some arguments:
        // return result.ToFileStream(enableRangeProcessing: true, entityTag: ...);
        return result.ToFileStream();
    }
);

If you want to use the ToFileStream() method, the result of the operation should implement the IFileStreamResult:

interface IFileStreamResult
{
    Stream FileContent { get; }
    string? ContentType { get; }
    string? DownloadFileName { get; }
    DateTimeOffset? LastModified { get; }
}

Issues

If you encounter any bugs or have any suggestions for improvements, please open an issue.

License

This project is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
2.0.1 320 8/3/2024
2.0.0 75 8/3/2024
1.0.0 178 6/22/2024

This release introduces breaking changes, i.e. problemDetails.Title property is now created from error.Code and problemDetails.Detail is created from error.Description.