Trying to investigate ProblemDetails
and met some issues.
My code:
using Microsoft.AspNetCore.Builder;
using WebApplication3;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseStatusCodePages();
app.UseExceptionHandler();
app.MapGet("/hello", () =>
{
throw new Exception("Oulah. Hello API has problems!");
});
app.MapGet("/bad", () => Results.BadRequest(new
{
message = "Very bad, you have a problem."
}));
app.Run();
As you can see there are 2 API methods - hello
and bad
.
My issue: I do get the expected response body with hello
:
But with bad
, it is a different story:
ProblemDetail
is not activated.
I can't see what is different in my code than in other examples I can see.
What I need:
- To understand what is happening
- What I can do
Thank you
Trying to investigate ProblemDetails
and met some issues.
My code:
using Microsoft.AspNetCore.Builder;
using WebApplication3;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseStatusCodePages();
app.UseExceptionHandler();
app.MapGet("/hello", () =>
{
throw new Exception("Oulah. Hello API has problems!");
});
app.MapGet("/bad", () => Results.BadRequest(new
{
message = "Very bad, you have a problem."
}));
app.Run();
As you can see there are 2 API methods - hello
and bad
.
My issue: I do get the expected response body with hello
:
But with bad
, it is a different story:
ProblemDetail
is not activated.
I can't see what is different in my code than in other examples I can see.
What I need:
- To understand what is happening
- What I can do
Thank you
Share Improve this question edited Jan 19 at 11:24 marc_s 754k184 gold badges1.4k silver badges1.5k bronze badges asked Jan 19 at 11:11 Frédéric De Lène MirouzeFrédéric De Lène Mirouze 6191 gold badge7 silver badges36 bronze badges3 Answers
Reset to default 1It's not auto-"activated", because your current setup converts ONLY Exception
s to Problems.
If you had added:
app.UseStatusCodePages()
and removed the message
app.MapGet("/bad", () => Results.BadRequest());
you'd get the extra "activation" of requests 4xx-5xx with no body being converted to ProblemDetails
.
If you want to return ProblemDetails
with a message you need to do it yourself:
app.MapGet("/bad", () =>
TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest,
title: "Bad"));
When you use Results.BadRequest
with an anonymous object, you are simply telling the controller to return a 400
status code with the json-ified object passed as an argument.
If you want the controller to return a representation of the ProblemDetails
class, you can use Results.Problem
(i.e. Results.Problem("This is a problem", statusCode: 400)
).
When an exception is thrown inside a controller, it is automatically caught and a ProblemDetails
is returned with a standard error message. Otherwise, you have to handle it manually.
Returning a bad request from a minimal endpoint does not automatically return ProblemDetails
, as compared to controller-based endpoints. This is because minimal APIs don't automatically use the IProblemDetailsService
for exception handling unless explicitly configured. This means that, by default, you won't get ProblemDetails
returned on exceptions or when using methods like Results.BadRequest
.
It's not a good practice to throw an exception from your endpoint. Instead, you can return a Problem
.
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest);
The above line of code will return a default ProblemDetails
response. You can further configure ProblemDetails
in your IoC Container (pipeline). For example:
builder.Services.AddProblemDetails (options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Instance =
$"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
context.ProblemDetails.Extensions.TryAdd (
"requestId",
context.HttpContext.TraceIdentifier);
var activity = context.HttpContext.Features.Get<IHttpActivityFeature>()!.Activity;
context.ProblemDetails.Extensions.TryAdd ("traceId", activity.Id);
};
});
If you still intend to throw an exception or wish to configure exceptions to automatically return a ProblemDetails
. Please refer below code:
// Controllers are required to access ProblemDetailsFactory.
builder.Services.AddControllers();
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Instance =
$"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
context.ProblemDetails.Extensions.TryAdd(
"requestId",
context.HttpContext.TraceIdentifier);
var activity = context.HttpContext.Features.Get<IHttpActivityFeature>()!.Activity;
context.ProblemDetails.Extensions.TryAdd("traceId", activity.Id);
};
});
builder.Services.AddExceptionHandler<ProblemExceptionHandler>();
My minimal endpoint:
app.MapGet ("/exception", (HttpContext httpContext) =>
{
// Remember to AddControllers() to IoC Container.
var problemDetailsFactory = httpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
// Fetch pre-configured Problem Details from IoC Container.
var problem = problemDetailsFactory.CreateProblemDetails (
httpContext: httpContext,
statusCode: StatusCodes.Status400BadRequest,
title: "An exception occurred.");
throw new MyCustomException("An exception occured", "Hello, Exception!", problem);
});
My custom exception and global exception handler class:
public class ProblemException : Exception
{
public string Error { get; set; }
public new string Message { get; set; }
public ProblemDetails Problem { get; set; }
public ProblemException(string error, string message, ProblemDetails problem) : base(message)
{
Error = error;
Message = message;
Problem = problem;
}
}
public class ProblemExceptionHandler : IExceptionHandler
{
private readonly IProblemDetailsService _problemDetailsService;
public ProblemExceptionHandler(IProblemDetailsService problemDetailsService)
{
_problemDetailsService = problemDetailsService;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if (exception is not ProblemException problemException) return true;
var problemDetails = problemException.Problem;
Dictionary<string, object> extensions = new();
extensions.Add ("Error", problemException.Error);
extensions.TryAdd ("Message", problemException.Message);
problemDetails.Extensions.TryAdd ("Details", extensions);
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return await _problemDetailsService.TryWriteAsync(
new ProblemDetailsContext()
{
HttpContext = httpContext,
ProblemDetails = problemDetails
});
}
}
I hope you find this helpful.