![]() |
VOOZH | about |
dotnet add package ErrorOrAspNetCoreExtensions --version 2.0.1
NuGet\Install-Package ErrorOrAspNetCoreExtensions -Version 2.0.1
<PackageReference Include="ErrorOrAspNetCoreExtensions" Version="2.0.1" />
<PackageVersion Include="ErrorOrAspNetCoreExtensions" Version="2.0.1" />Directory.Packages.props
<PackageReference Include="ErrorOrAspNetCoreExtensions" />Project file
paket add ErrorOrAspNetCoreExtensions --version 2.0.1
#r "nuget: ErrorOrAspNetCoreExtensions, 2.0.1"
#:package ErrorOrAspNetCoreExtensions@2.0.1
#addin nuget:?package=ErrorOrAspNetCoreExtensions&version=2.0.1Install as a Cake Addin
#tool nuget:?package=ErrorOrAspNetCoreExtensions&version=2.0.1Install as a Cake Tool
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 😎
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.
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}"
);
}
);
When using the methods this package provides, errors are resolved like this by default:
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.
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>);
}
);
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>);
}
);
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();
}
);
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; }
}
If you encounter any bugs or have any suggestions for improvements, please open an issue.
This project is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
This package is not used by any NuGet packages.
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.