Ok, going off of Giacomo De Liberali answer, I did some digging and found a decent solution.
I looked up the source code for the default implementation of ProblemDetailsFactory (v6.0.1) and wrote a service that works similarly. This way I can avoid throwing exceptions when I'm returning an fully expected error response.
I used the IHttpAccessor
service instead of passing the HttpContext as parameter. This service needs to be registered beforehand like this in Problem.cs
:
builder.Services.AddHttpContextAccessor();
I populate the default fields for the ProblemDetails
instance with the help of the IOptions<ApiBehaviorOptions>
service that's registered by default.
Finally, I pass an optional object as parameter in the Create(..)
method and add it's properties with the problemDetails.Extensions.Add(...)
method.
Here's the full implementation I'm using currently:
public class ProblemService
{
private readonly IHttpContextAccessor _contextAccessor;
private readonly ApiBehaviorOptions _options;
public ProblemService(IOptions<ApiBehaviorOptions> options, IHttpContextAccessor contextAccessor)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor));
}
public ProblemDetails Create(int? statusCode = null, object? extensions = null)
{
var context = _contextAccessor.HttpContext ?? throw new NullReferenceException();
statusCode ??= 500;
var problemDetails = new ProblemDetails
{
Status = statusCode,
Instance = context.Request.Path
};
if (extensions != null)
{
foreach (var extension in extensions.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
{
problemDetails.Extensions.Add(extension.Name, extension.GetValue(extensions, null));
}
}
ApplyProblemDetailsDefaults(context, problemDetails, statusCode.Value);
return problemDetails;
}
private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode)
{
problemDetails.Status ??= statusCode;
if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
{
problemDetails.Title ??= clientErrorData.Title;
problemDetails.Type ??= clientErrorData.Link;
}
var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
if (traceId != null)
{
problemDetails.Extensions["traceId"] = traceId;
}
}
}
And here's how to use it after injecting it into the controller:
return Unauthorized(_problem.Create(StatusCodes.Status401Unauthorized, new
{
I18nKey = LoginFailureTranslationKey.AccessFailed
));