A library which contains following functions: - Siemens.AspNet ErrorHandling Classes - Siemens.AspNet ErrorHandling Middleware - Siemens.AspNet ErrorHandling ExceptionHandlers
$ dotnet add package Siemens.AspNet.ErrorHandlingThe Siemens.AspNet.ErrorHandling package provides middleware and services for handling errors in ASP.NET Core
applications. It includes features for translating error messages based on the "Accepted Languages" header, making it
easier to build multilingual applications with standardized error responses. The package is designed to work seamlessly
with its companion package,
Siemens.AspNet.ErrorHandling.Contracts Link to Documentation,
which defines the core error handling models and base classes like ProblemDetails and ValidationProblemDetails.
By using RFC 7807, we ensure that error responses are consistent, easily interpretable by clients, and capable of conveying rich, structured information about errors. This approach enhances interoperability and helps developers diagnose issues more effectively.
You can install the package using NuGet:
dotnet add package Siemens.AspNet.ErrorHandling
Install-Package Siemens.AspNet.ErrorHandling
In your ASP.NET Core application, you need to register the error handling services in the Startup.cs or Program.cs file.
AddErrorHandling adds all services for handling response error and logging (see the example below).
If you want to add only the error logging, please add and use AddErrorLogHandling and UseErrorLogHandling.
If you want to add only the handler itself to handle the error responses, please add AddErrorResponseHandling and
UseErrorResponseHandling.
When using the AddErrorHandling method, it will also add the following service options:
builder.Services.Configure<RouteHandlerOptions>(options =>
{
options.ThrowOnBadRequest = true;
});
🧠 Example Scenario:
With ThrowOnBadRequest = false
POST /api/submit
Body: { "age": "not_a_number" }
→ 400 Bad Request (no exception thrown)
With ThrowOnBadRequest = true
POST /api/submit
Body: { "age": "not_a_number" }
→ Throws exception (Model binding failure)
if you override the default behavior of the RouteHandlerOptions make sure to set the ThrowOnBadRequest=true!
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Register the error handler services for logging or response handling
// services.AddErrorResponseHandling();
// services.AddErrorLogHandling();
// Register the error handling services
services.AddErrorHandling();
// Other service registrations...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Add the error response handling
// app.UseErrorResponseHandling();
// Add the error Logging
// app.UseErrorLogHandling();
// Add the error handling middleware to the request pipeline
app.UseErrorHandlingMiddleware();
// Other middleware...
}
}
var builder = WebApplication.CreateBuilder(args);
// Register the error handler services for logging or response handling
// builder.Services.AddErrorResponseHandling();
// builder.Services.AddErrorLogHandling();
// Register the error handling services
builder.Services.AddErrorHandling();
// Other service registrations...
var app = builder.Build();
app.UseRouting();
// Important to use the full potential setup the error handler after app.UseRouting();
app.UseErrorHandling();
// Other middleware...
app.Run();
If you use the Siemens.AspNet.MinimalApi.Sdk (coming soon) package everything is already setup. Just use the
ServerlessMinimalWebApi and everything
is ready to go.
using Pulse.FieldingTool.Api;
using Siemens.AspNet.MinimalApi.Sdk;
var webApi = new ServerlessMinimalWebApi();
webApi.RegisterServices = (service,
config) =>
{
// Domain service registrations
service.AddApi(config);
};
webApi.MapEndpoints = endpoints =>
{
// Map api domain endpoints
endpoints.MapApi();
};
webApi.Run(args);
// This is important that you are able to use
// API test via WebApplicationFactory<Program>
// https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0
namespace Pulse.FieldingTool
{
public partial class Program;
}
(Optional) A new feature has been introduced that allows you to control which error response will be processed and which will ne hidden from the user
To conifgure this feature, add the following configuration to your appsettings.json or set it as an environment variable:
Example: Single codes
"ErrorHandlerSettings": {
"ShowExceptionDetailsOnErrorCodeRanges": "200;201;202"
}
Example: Ranges (0-499)
"ErrorHandlerSettings": {
"ShowExceptionDetailsOnErrorCodeRanges": "0-400"
}
Example: Single numbers and ranges
"ErrorHandlerSettings": {
"ShowExceptionDetailsOnErrorCodeRanges": "200;210-299;400-499"
}
Default settings: if you are not configure this settings explicitly everything in the range from 0-499 will be handled and shown. All above will be hidden.
Sample: Handled response
{
"type": "ValidationProblemDetails",
"title": "Your patch request contains invalid data",
"status": 422,
"detail": "Your PatchFormsConfigurationRequest for the object id: 6f9e1c3e-8fbc-4d2a-9a3d-3c2e8f0b7d2f was invalid. Please check the error details. You might pass invalid data, or try to patch properties which not exists or not allow to patch",
"errors": {
"formsId": [
"FormsId must be either GUID or long.",
"FormsId must not be one or more whitespace. Only GUID or a long is valid for the ProjectId"
]
}
Sample: Hidden response
{
"type": "ProblemDetails",
"title": "An unexpected error occured, please contact the service",
"status": 422,
"detail": "An unexpected error occured, please contact the service",
"errors": {}
}
Extended Validation problem details:
endpoints.WithMetadata(new AllowedBodyMetaInfo(typeof(CreateFormsConfigurationRequest))); // Your request body type
ValidationProblemDetailsExtended``instead of ValidationProblemDetails` endpoints.Produces<ValidationProblemDetailsExtended>(StatusCodes.Status422UnprocessableEntity)
Sample endpoint:
internal static class CreateFormsConfigurationEndpoint
{
internal static void MapCreateFormsConfiguration(this IEndpointRouteBuilder endpoints)
{
endpoints.MapPost("formsConfigurations", HandleAsync)
.Produces<CreateFormsConfigurationResponse>()
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
.Produces<ProblemDetails>(StatusCodes.Status401Unauthorized)
.Produces<ProblemDetails>(StatusCodes.Status403Forbidden)
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.Produces<ProblemDetails>(StatusCodes.Status409Conflict)
.Produces<ValidationProblemDetailsExtended>(StatusCodes.Status422UnprocessableEntity)
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError)
.Produces<ProblemDetails>(StatusCodes.Status503ServiceUnavailable)
.Produces<string>(StatusCodes.Status504GatewayTimeout) // AWS handled error -> returns HTML
.WithTags("FormsConfigurations")
.WithName("createFormsConfigurationV1")
.MapToApiVersion(1)
.WithDescriptionFromFile("Description.txt")
.WithSummaryFromFile("Summary.txt")
.WithMetadata(new AllowedBodyMetaInfo(typeof(CreateFormsConfigurationRequest)));
static async Task<CreateFormsConfigurationResponse> HandleAsync(CreateFormsConfigurationRequest createFormsConfigurationRequest,
CreateFormsConfigurationCommand createFormsConfigurationCommand,
CancellationToken cancellationToken = default)
{
var project = await createFormsConfigurationCommand.ExecuteAsync(createFormsConfigurationRequest, cancellationToken).ConfigureAwait(false);
return new CreateFormsConfigurationResponse(project);
}
}
}
New outcome:
{
"type": "ValidationProblemDetailsExtended",
"title": "Your patch request contains invalid data",
"status": 422,
"detail": "Your PatchFormsConfigurationRequest for the object id: 6f9e1c3e-8fbc-4d2a-9a3d-3c2e8f0b7d2f was invalid. Please check the error details. You might pass invalid data, or try to patch properties which not exists or not allow to patch",
"errors": {
"formsId": [
"FormsId must be either GUID or long.",
"FormsId must not be one or more whitespace. Only GUID or a long is valid for the ProjectId"
]
},
"errorDetails": {
"formsId": {
"currentValue": " ",
"errors": [
"FormsId must be either GUID or long.",
"FormsId must not be one or more whitespace. Only GUID or a long is valid for the ProjectId"
],
"samples": [
"a1b2c3d4-e5f6-7890-1234-567890abcdef",
"1"
],
"location": null
}
},
"Id": "6f9e1c3e-8fbc-4d2a-9a3d-3c2e8f0b7d2f",
"RequestType": "PatchFormsConfigurationRequest",
"RequestContent": {
"formsId": " ",
"projectId": "c36b49ba-bf79-4c86-9d41-8c356634316e",
"title": "$UniqueFormsConfigurationName$",
"formsType": "InvitationOnly",
"startDate": "2025-03-15T18:32:18Z",
"endDate": "2025-04-14T18:23:44Z",
"sessionEndsIn": "00:30:00",
"hasUpdate": false,
"languages": [
{
"englishName": "English (United States)",
"nativeName": "English (United States)",
"tag": "en-US",
"tagThreeLetter": "eng",
"parentTag": "en"
}
],
"properties": {
"exampleKey": "ExampleValue"
}
}
}
Invalid Json:
To have even better json error responses if the json payload is invalid we added a new feature to handle this.
Error json:
{
"Id": {%$$},
"DisplayName": "$UniqueEdgeName$",
"SourceCapabilityId": "$SourceCapabilityId$",
"TargetCapabilityId": "$TargetCapabilityId$",
"RelationType": "Simple",
"Position": {
"x": 0,
"y": 1
},
"Size": {
"Width": 2,
"Height": 3
}
}
Orginal Minimal API Response: (Without this error handler)
{
"content": {
"headers": [],
"value": {}
},
"statusCode": "BadRequest",
"headers": [
{
"key": "api-supported-versions",
"value": [
"1"
]
}
],
"trailingHeaders": [],
"isSuccessStatusCode": false
}
With the error handler
{
"type":"ValidationProblemDetailsExtended",
"title":"JsonReaderException was thrown.",
"status":422,
"detail":"Invalid property identifier character: %",
"errorDetails":{
"id":{
"currentValue":"{%$$}",
"errors":[
"Invalid property identifier character: %"
],
"samples":[
"example text"
],
"location":{
"row":"2",
"column":"9"
}
}
},
"errors":{
"id":[
"Invalid property identifier character: % Location line number: \u00272\u0027 line position: \u00279\u0027"
]
}
}
Hint: SampleValues
If you have properties which presents for example an string but can be a Guid or number you can optimize the errors
thrown by System.Text.Json or Newtonsoft.Json easily by adding an attribute over your properties in your request.
This is very useful if you have any crash by your serializers before you enter your endpoint code
public sealed record UpdateFormsConfigurationRequest
{
[SampleValue("a1b2c3d4-e5f6-7890-1234-567890abcdef", "1")]
public required string FormsId { get; init; } // SDC --> FormsId GUID // Pulse --> SurveyInstanceId long
[SampleValue("a1b2c3d4-e5f6-7890-1234-567890abcdef", "1")]
public required string ProjectId { get; init; } // SDC --> GUID // Pulse -->
}
Special Case: ValidationProblemDetailsException
When the thrown exception is a ValidationProblemDetails exception, this configuration not only hides the details but also filters the validation errors themselves. This ensures that no validation error messages are exposed in the response.
With this configuration, whenever an error with the specified code (e.g., 400, 422, 500) or within the specified range ( 500-590) is thrown, detailed error messages will be hidden from the response.
(Optional)
The Siemens.AspNet.ErrorHandling package offers robust support for translating error messages based on the client's "
Accepted Languages" header. This is achieved through a pattern-based approach to localization keys, allowing for
consistent and easily maintainable translations across your application.
The Siemens.AspNet.ErrorHandling package automatically selects the appropriate language for error messages based on
the client's "Accepted-Languages" HTTP header. Here’s how the language handling works:
For example, if the client requests de-DE (German for Germany) but only de.json is available, the package will use the translations from de.json. This fallback mechanism ensures that users receive the most appropriate localized content available.
To manage translations effectively, the Siemens.AspNet.ErrorHandling package uses JSON files for storing localized
error messages. Follow these guidelines to set up your translation files:
By organizing your translation files in this manner, you can maintain a clean and efficient structure that makes it easy to manage and update your localized content.
The translation keys for error messages follow a specific pattern, making it straightforward to manage translations for different exception types and scenarios. The pattern is as follows:
Variables:
Consider a scenario where you have a ValidationDetailsException in a class or method named MyAwesomeClass. The translation keys might look like this:
{
"MyAwesomeClass.ValidationDetailsException.Title.AUTH_TITLE_KEY": "This is my translated title",
"MyAwesomeClass.ValidationDetailsException.Details.AUTH_TITLE_KEY": "This is my translated details",
"MyAwesomeClass.ValidationDetailsException.Errors.AUTH_TITLE_KEY.Name.Name is required.": "This is my translated errors for name 1",
"MyAwesomeClass.ValidationDetailsException.Errors.AUTH_TITLE_KEY.Name.Name must be at least 3 characters long.": "This is my translated errors for name 2",
"MyAwesomeClass.ValidationDetailsException.Errors.AUTH_TITLE_KEY.Email.Email is not in a valid format.": "This is my translated errors for email"
}
This example shows how to throw an error in your c# code.
var errors = new Dictionary<string, string[]>
{
{ "Name", new[] { "Name is required.", "Name must be at least 3 characters long." } },
{ "Email", new[] { "Email is not in a valid format." } }
};
throw new ValidationDetailsException("AUTH_TITLE_KEY", "AUTH_DETAILS_KEY", errors);
If you have the need to handle specific exceptions in a better way as the current handler supports you can implement your own specific error handler quite easy:
internal static class AddExceptionILikeToHandleHandlerExtension
{
public static void AddExceptionILikeToHandleHandler(this IServiceCollection services)
{
services.AddSingletonIfNotExists<ISpecificErrorHandler, ExceptionILikeToHandleHandler>();
}
}
internal sealed class ExceptionILikeToHandleHandler() : SpecificErrorHandler<ExceptionILikeToHandle>
{
protected override Task<ProblemDetails> HandleExceptionAsync(HttpContext context,
ExceptionILikeToHandle exception)
{
// Add here your custom logic, code to extract infos specific on your exception you like to handle in a specific way
var reducedProblemDetails = new ProblemDetails
{
Title = exception.Message,
Status = problemDetails.Status ?? StatusCodes.Status500InternalServerError,
Type = nameof(ProblemDetails),
Detail = exception.Message,
};
return Task.FromResult(exception.ProblemDetails);
}
}
using Pulse.FieldingTool.Api;
using Siemens.AspNet.MinimalApi.Sdk;
var webApi = new ServerlessMinimalWebApi();
webApi.RegisterServices = (service,
config) =>
{
// Domain service registrations
service.AddApi(config);
// Custom error handling
service.AddExceptionILikeToHandleHandler()
};
webApi.MapEndpoints = endpoints =>
{
// Map api domain endpoints
endpoints.MapApi();
};
webApi.Run(args);
If you have the need to handle specific exceptions in a better way as the current handler supports you can implement your own specific error log handler quite easy:
internal static class AddValidationProblemDetailsExtendedExceptionLogHandlerExtensions
{
public static void AddValidationProblemDetailsExtendedExceptionLogHandler(this IServiceCollection services)
{
services.AddExceptionLocalizerService();
services.AddTranslationService();
services.AddExceptionHelper();
services.AddSingletonIfNotExists<ISpecificErrorLogHandler, ValidationProblemDetailsExtendedExceptionLogHandler>();
}
}
internal sealed class ValidationProblemDetailsExtendedExceptionLogHandler(ILogger<DefaultExceptionLogHandler> logger,
JsonSerializerOptions jsonSerializerOptions,
ExceptionHelper exceptionHelper) : SpecificErrorLogHandler<ValidationProblemDetailsExtendedException>(logger, jsonSerializerOptions, exceptionHelper)
{
protected override Task<ErrorLogInfo> GetErrorLogFromAsync(HttpContext httpContext,
ValidationProblemDetailsExtendedException exception)
{
// Here you can implement what ever you like to log or customize it.
var errorLogInfo = new ErrorLogInfo
{
StatusCode = exception.ValidationProblemDetailsExtended.Status ?? 500,
Title = exception.ValidationProblemDetailsExtended.Title ?? "No title provided",
Message = exception.ValidationProblemDetailsExtended.Detail ?? "No details provided",
StackTrace = exception.StackTrace?.Split(Environment.NewLine).ToImmutableList() ?? ImmutableList<string>.Empty,
RequestInfos = httpContext.Request.GetQueryRequestInfo(exception.ValidationProblemDetailsExtended.Extensions).ToImmutableDictionary(k => k.key, v => v.value),
ErrorDetails = exception.ValidationProblemDetailsExtended.ErrorDetails.ToImmutableDictionary(k => k.Key, v => (object?)v.Value),
Extensions = exception.ValidationProblemDetailsExtended.Extensions.AsReadOnly(),
ErrorType = exception.GetType().Name
};
return Task.FromResult(errorLogInfo);
}
}
using Pulse.FieldingTool.Api;
using Siemens.AspNet.MinimalApi.Sdk;
var webApi = new ServerlessMinimalWebApi();
webApi.RegisterServices = (service,
config) =>
{
// Domain service registrations
service.AddApi(config);
// Custom error handling
service.AddValidationProblemDetailsExtendedExceptionLogHandler()
};
webApi.MapEndpoints = endpoints =>
{
// Map api domain endpoints
endpoints.MapApi();
};
webApi.Run(args);
Contributions are welcome! Please submit pull requests or issues as needed.
Please refer to the repository's license file.