ErrorOrAspNetCoreExtensions 2.0.1
dotnet add package ErrorOrAspNetCoreExtensions --version 2.0.1
NuGet\Install-Package ErrorOrAspNetCoreExtensions -Version 2.0.1
<PackageReference Include="ErrorOrAspNetCoreExtensions" Version="2.0.1" />
paket add ErrorOrAspNetCoreExtensions --version 2.0.1
#r "nuget: ErrorOrAspNetCoreExtensions, 2.0.1"
// Install ErrorOrAspNetCoreExtensions as a Cake Addin #addin nuget:?package=ErrorOrAspNetCoreExtensions&version=2.0.1 // Install ErrorOrAspNetCoreExtensions as a Cake Tool #tool nuget:?package=ErrorOrAspNetCoreExtensions&version=2.0.1
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
Registering problem details services (optional, but recommended)
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 | Versions 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. |
-
net8.0
- ErrorOr (>= 2.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
This release introduces breaking changes, i.e. problemDetails.Title property is now created from error.Code and problemDetails.Detail is created from error.Description.