Alyio.AspNetCore.ApiMessages 3.0.1

dotnet add package Alyio.AspNetCore.ApiMessages --version 3.0.1
                    
NuGet\Install-Package Alyio.AspNetCore.ApiMessages -Version 3.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Alyio.AspNetCore.ApiMessages" Version="3.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Alyio.AspNetCore.ApiMessages" Version="3.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Alyio.AspNetCore.ApiMessages" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Alyio.AspNetCore.ApiMessages --version 3.0.1
                    
#r "nuget: Alyio.AspNetCore.ApiMessages, 3.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Alyio.AspNetCore.ApiMessages@3.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Alyio.AspNetCore.ApiMessages&version=3.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Alyio.AspNetCore.ApiMessages&version=3.0.1
                    
Install as a Cake Tool

Alyio.AspNetCore.ApiMessages

Build Status NuGet Version

A lightweight ASP.NET Core library that seamlessly integrates with ASP.NET Core's error handling mechanisms to standardize API error handling and messages into Problem Details for HTTP APIs responses.

Table of Contents

Why Alyio.AspNetCore.ApiMessages?

Developing robust and user-friendly APIs often involves handling responses consistently, whether they indicate an error or a successful operation. In ASP.NET Core, achieving this standardization across various scenarios (exceptions, validation errors, successful resource creation) can be repetitive and lead to inconsistent API contracts.

By utilizing Alyio.AspNetCore.ApiMessages, the development of reliable, maintainable, and developer-friendly APIs is facilitated with reduced effort.

  • Standardized Error Responses: Automatically transforms exceptions into RFC 9457 (formerly RFC 7807) Problem Details responses, ensuring consistent API error formats.

  • Standardized Success Responses: Provides mechanisms for consistent HTTP 201 Created responses, including HATEOAS links.

  • Seamless ASP.NET Core Integration: Leverages ASP.NET Core's built-in ProblemDetails and IProblemDetailsService, along with exception handling (IExceptionHandler, ExceptionHandlerMiddleware) and filtering (IEndpointFilter, ExceptionFilterAttribute) mechanisms.

  • Custom Exception Definition: Enables defining custom, domain-specific API messages by implementing the IApiMessage interface for clear error representation.

  • Middleware-Based Handling: Provides comprehensive middleware-based exception handling and Problem Details generation for both legacy (.NET 6.0 using ApiMessageHandlerMiddleware) and modern (.NET 8.0+ leveraging ExceptionHandlerMiddleware) ASP.NET Core applications.

  • Attribute-Based Handling: Apply ApiMessageAttribute directly to controllers/action methods or register it globally as an action filter for targeted exception handling.

  • Endpoint Filter Integration: Apply API message handling directly to individual minimal API endpoints or to groups of endpoints (for .NET 8.0+) using the AddApiMessage endpoint filter extension.

  • Dual-Targeting: Supports the legacy .NET 6.0, and .NET 8.0+ (including .NET 9.0 and .NET 10.0) with optimized configurations for each.

Installation

Install the NuGet package:

dotnet add package Alyio.AspNetCore.ApiMessages

Quick Start

This guide demonstrates configuring global exception handling for a .NET 8.0+ application to automatically convert exceptions into standardized ProblemDetails responses.

1. Configure Services and Middleware in Program.cs

First, register the required services by calling builder.Services.AddApiMessages(). Then, enable the global exception handler by adding app.UseExceptionHandler() to the request processing pipeline.

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services for controllers and API message handling.
builder.Services.AddControllers();
builder.Services.AddApiMessages();

var app = builder.Build();

// Enable the global exception handler. This must be placed early
// in the pipeline to catch exceptions from subsequent middleware.
app.UseExceptionHandler();

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

2. Throw an API Message Exception

With the global handler configured, any thrown exception that implements IApiMessage will be automatically caught and formatted. No special attributes are required on the controller.

// Controllers/ProductsController.cs
using Alyio.AspNetCore.ApiMessages;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        if (id <= 0)
        {
            // This exception will be handled by the global exception handler.
            throw new NotFoundException($"Product with ID '{id}' was not found.");
        }
        return Ok(new { ProductId = id, Name = "Sample Product" });
    }
}

3. Observe the Standardized Error Response

A GET request to /api/products/0 will trigger the NotFoundException, and the middleware will produce the following 404 Not Found response.

Resulting HTTP Response:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "detail": "Product with ID '0' was not found.",
  "instance": "/api/products/0",
  "traceId": "00-..."
}

This configuration ensures that all IApiMessage exceptions are handled consistently across the application. For further details on defining custom exceptions, using attribute-based filters, or configuring for legacy runtimes, refer to the Usage section.

Usage

  • Prioritize .NET 8.0+: If applicable, target .NET 8.0+ to leverage the latest ASP.NET Core features like IExceptionHandler and IEndpointFilter, which offer more streamlined and performant error handling.

  • Global Exception Handling: Always configure a global exception handler to ensure all unhandled exceptions across the application result in consistent Problem Details responses.

    • For .NET 8.0+, use app.UseExceptionHandler() in conjunction with builder.Services.AddApiMessages().

    • For .NET 6.0, use app.UseExceptionHandler(ExceptionHandler.WriteUnhandledMessageAsync) and app.UseApiMessage().

  • Define Custom API Messages: For domain-specific errors that require a standardized Problem Details response, create custom exceptions that implement the IApiMessage interface to define specific type, title, and detail for business logic errors.

  • Targeted Exception Handling:

    • Use [ApiMessage] attribute on controllers or individual action methods for handling IApiMessage exceptions thrown within the methods.

    • For Minimal APIs or endpoint routing in .NET 8.0+, apply .AddApiMessage() to specific endpoints.

  • Security: Information exposed in Problem Details responses should be reviewed. Avoid leaking sensitive exception details (e.g., stack traces) in production environments. The library's default behavior for generic exceptions (when handled by ExceptionHandler.WriteUnhandledMessageAsync in .NET 6.0) provides a basic message, but review what information is appropriate for the production environment.

Configuration

The library provides different configuration patterns based on the target .NET framework.

For .NET 8.0+

The primary approach for configuring API message handling involves combining services.AddApiMessages() and app.UseExceptionHandler() to enable comprehensive API message handling.

In Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddApiMessages(); // Adds ProblemDetails, ApiMessageFilter, and ApiMessagesExceptionHandler

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseExceptionHandler(); // Uses ApiMessagesExceptionHandler for global exception handling

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();
For the Legacy .NET 6.0

The primary approach for configuring API message handling in legacy .NET 6.0 applications involves utilizing middleware and the traditional exception handler.

In Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseExceptionHandler(ExceptionHandler.WriteUnhandledMessageAsync); // Handles unhandled exceptions
app.UseApiMessage(); // Catches IApiMessage exceptions

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Defining and Throwing API Messages

The library provides several built-in exceptions for common HTTP error scenarios, including BadRequestException, ForbiddenException, InternalServerErrorException, NotFoundException, and UnauthorizedException. For custom API messages, implement the IApiMessage interface.

Here's how to define and throw a custom API message:

// 1. Define a custom API message
public class ProductNotFoundException : Exception, IApiMessage
{
    public ProblemDetails ProblemDetails { get; }

    public ProductNotFoundException(int productId)
    {
        ProblemDetails = new ProblemDetails
        {
            Title = "Product Not Found",
            Status = StatusCodes.Status404NotFound,
            Detail = $"Product with ID '{productId}' was not found.",
            Type = "https://example.com/problems/product-not-found"
        };
    }
}

// 2. Throw it in a controller action
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        // Simulate product not found
        if (id == 0)
        {
            throw new ProductNotFoundException(id);
        }
        return Ok($"Product {id}");
    }
}

// 3. Example HTTP Response (for GET /api/products/0):

// HTTP/1.1 404 Not Found
// 
// Content-Type: application/problem+json; charset=utf-8
// 
// {
//   "type": "https://example.com/problems/product-not-found",
//   "title": "Product Not Found",
//   "status": 404,
//   "detail": "Product with ID '0' was not found.",
//   "instance": "/api/products/0",
//   "traceId": "00-..."
// }

Using Action Filters (for Controller-Based APIs)

While middleware-based handling is the common approach for global exception management, [ApiMessage] attribute offers a more granular way to handle IApiMessage exceptions. It can be applied to controllers or individual action methods to automatically handle exceptions thrown within the methods, or registered globally as an action filter.

[ApiController]
[Route("api/[controller]")]
[ApiMessage] // Apply to the whole controller
public class MyController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        if (id <= 0)
        {
            throw new BadRequestException("Invalid ID provided.");
        }
        return Ok();
    }
}
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "Invalid ID provided.",
  "instance": "/api/mycontroller/0",
  "traceId": "00-..."
}
[HttpPost]
[ApiMessage] // Apply to a specific action
public IActionResult Create([FromBody] MyModel model)
{
    if (!ModelState.IsValid)
    {
        throw new BadRequestException(ModelState);
    }
    // ...
    return CreatedAtAction(nameof(Get), new { id = 1 }, null);
}
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "ValidationFailed",
  "status": 400,
  "detail": "One or more validation errors occurred.",
  "instance": "/api/mycontroller",
  "traceId": "00-...",
  "errors": {
    "PropertyName": [
      "Error message for PropertyName."
    ]
  }
}
// Registered globally as an action filter
builder.Services.AddControllers(options => options.Filters.Add(typeof(ApiMessageAttribute)));

Using Endpoint Filters (for .NET 8.0+ Minimal APIs)

Apply API message handling directly to individual minimal API endpoints or to groups of endpoints (for .NET 8.0+) using the AddApiMessage endpoint filter extension.

app.MapGet("/items/{id}", (int id) =>
{
    if (id <= 0)
    {
        throw new BadRequestException("Invalid ID provided.");
    }
    return Results.Ok($"Item {id}");
}).AddApiMessage(); // Apply the endpoint filter

Built-in API Message Examples

The library provides several built-in exceptions that implement IApiMessage for common HTTP error scenarios. Here are examples of their usage and the Problem Details responses they generate.

ForbiddenException (HTTP 403)
[ApiController]
[Route("api/examples")]
public class ForbiddenExampleController : ControllerBase
{
    [HttpGet("forbidden")]
    public IActionResult GetForbidden()
    {
        // Simulate a forbidden access scenario
        throw new ForbiddenException("Access to this resource is forbidden.");
    }
}
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.4",
  "title": "Forbidden",
  "status": 403,
  "detail": "Access to this resource is forbidden.",
  "instance": "/api/examples/forbidden",
  "traceId": "00-..."
}
InternalServerErrorException (HTTP 500)
[ApiController]
[Route("api/examples")]
public class InternalServerErrorExampleController : ControllerBase
{
    [HttpGet("internal-server-error")]
    public IActionResult GetInternalServerError()
    {
        // Simulate an unexpected server error
        throw new InternalServerErrorException("An unexpected error occurred on the server.");
    }
}
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "An unexpected error occurred on the server.",
  "instance": "/api/examples/internal-server-error",
  "traceId": "00-..."
}
NotFoundException (HTTP 404)
[ApiController]
[Route("api/examples")]
public class NotFoundExampleController : ControllerBase
{
    [HttpGet("not-found")]
    public IActionResult GetNotFound()
    {
        // Simulate a resource not found scenario
        throw new NotFoundException("The requested resource was not found.");
    }
}
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "detail": "The requested resource was not found.",
  "instance": "/api/examples/not-found",
  "traceId": "00-..."
}
UnauthorizedException (HTTP 401)
[ApiController]
[Route("api/examples")]
public class UnauthorizedExampleController : ControllerBase
{
    [HttpGet("unauthorized")]
    public IActionResult GetUnauthorized()
    {
        // Simulate an unauthorized access scenario
        throw new UnauthorizedException("Authentication is required to access this resource.");
    }
}
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.2",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authentication is required to access this resource.",
  "instance": "/api/examples/unauthorized",
  "traceId": "00-..."
}

Handling 201 Created Responses with CreatedMessage

In RESTful API design, an HTTP 201 Created status code indicates that a request has been fulfilled and has resulted in one or more new resources being created. The Location header in the response typically points to the URI of the primary resource created.

The library provides the CreatedMessage class and associated extension methods to standardize and simplify the generation of 201 Created responses. This ensures consistency, proper HATEOAS (Hypermedia as the Engine of Application State) links, and ease of consumption by API clients.

The CreatedMessage Class

The CreatedMessage class is a data structure designed for use as the body of a 201 Created response. It includes:

  • Id: A string identifier for the newly created resource.
  • Links: A collection of HATEOAS Link objects, typically including a self link pointing to the URI of the created resource.
Using Extension Methods for Convenience

While manual construction of a CreatedMessage is possible, the library offers convenient extension methods on ControllerBase to streamline the process. These methods automatically handle setting the HTTP 201 status code, populating the Location header, and constructing the CreatedMessage object with appropriate links.

  • CreatedMessageAtAction(string actionName, object routeValues, string id): Generates the Location header and self link based on an action method.
  • CreatedMessageAtRoute(string routeName, object routeValues, string id): Generates the Location header and self link based on a named route.

A comprehensive example demonstrating the use of CreatedMessageAtAction is provided below:

[ApiController]
[Route("api/examples")]
public class ResourceController : ControllerBase
{
    [HttpPost("resources")]
    public IActionResult CreateResource()
    {
        // Simulate resource creation logic
        var newResourceId = Guid.NewGuid().ToString();

        // The CreatedMessageAtAction extension method is used to build the
        // response body, including a 'self' link, conforming to the CreatedMessage structure.
        var createdMessage = this.CreatedMessageAtAction(
            actionName: nameof(GetResourceById),
            routeValues: new { id = newResourceId },
            id: newResourceId);

        // The standard CreatedAtAction method creates the HTTP 201 response,
        // sets the 'Location' header, and includes the createdMessage object as the response body.
        return CreatedAtAction(nameof(GetResourceById), new { id = newResourceId }, createdMessage);
    }

    [HttpGet("resources/{id}", Name = "GetResourceById")]
    public IActionResult GetResourceById(string id)
    {
        // Logic to retrieve the resource by ID
        // For demonstration purposes, a simple message is returned.
        return Ok($"Successfully retrieved resource with ID: {id}");
    }
}
Example HTTP 201 Response

When the CreateResource endpoint is called, the response will appear similar to the following:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /api/examples/resources/a1b2c3d4-e5f6-7890-1234-567890abcdef

{
  "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
  "links": [
    {
      "rel": "self",
      "href": "/api/examples/resources/a1b2c3d4-e5f6-7890-1234-567890abcdef",
      "method": "GET"
    }
  ]
}

Note: The Location header is automatically set by the CreatedMessageAtAction extension method. Its value matches the href in the self link within the response body, providing a consistent and discoverable API experience.

Migration Guide from 2.x to 3.x

Version 3.0 introduces significant breaking changes as part of its modernization. The library aligns with .NET 8+ standards, embraces ProblemDetails for RFC 9457 compliance, and simplifies its public API.

Summary of Breaking Changes

  • Public API: The BadRequestMessage class and other similar classes have been replaced by new exception classes, such as BadRequestException.
  • IApiMessage Interface: The interface is simplified to expose a single ProblemDetails property.
  • Configuration: The setup method is changed. The legacy UseApiMessage middleware is available for .NET 6 but is not the recommended approach for modern applications.
  • JSON Response Format: The error response format is changed from a custom schema to the standard ProblemDetails JSON object. This may affect clients expecting the old format.
  • Validation Error Structure: The structure of validation errors within the ProblemDetails object is updated to align with ASP.NET Core conventions.

API Developer Migration (Server-Side)

  1. Configuration (Program.cs):

    • For .NET 8+ (Recommended): app.UseApiMessageHandler() should be replaced with builder.Services.AddApiMessages() and app.UseExceptionHandler().
    • For .NET 6: The old app.UseApiMessageHandler() should be replaced with the new app.UseApiMessage().
  2. Exception Classes:

    • All usages of old message classes (e.g., new BadRequestMessage()) are to be replaced with their new exception equivalents (new BadRequestException()).
    • Custom exceptions implementing IApiMessage must be refactored to create and expose a ProblemDetails object.
  3. CreatedMessage Usage: The CreatedMessage class and its helper extensions now use System.Text.Json and nullable reference types. All usage should be reviewed for compatibility.

API Consumer Migration (Client-Side)

  1. Error Response Handling: Clients must be updated to parse the standard ProblemDetails JSON object instead of the old custom error format.

  2. Validation Error Logic: The structure of validation errors has changed. Client-side code that displays validation messages must be updated to handle the new dictionary structure.

    • Old errors field: ["FieldName: Error message"]
    • New errors field: {"FieldName": ["Error message"]}

Problem Details for HTTP APIs

This library adheres to the Problem Details for HTTP APIs specification (RFC 9457, which obsoletes RFC 7807). This standard provides a machine-readable format for HTTP API responses to convey details of a problem. It defines a JSON (or XML) structure with fields like type, title, status, detail, and instance, allowing clients to understand and handle API errors consistently.

Contributing

Contributions are welcome! Please feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Contact

For questions or feedback, please open an issue on the GitHub repository.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net6.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.1 32 9/27/2025
3.0.1-preview.10 33 9/27/2025
2.1.0 2,264 11/10/2018
2.0.2 3,174 1/11/2018
2.0.0 2,228 8/22/2017
1.1.3 2,202 8/11/2017
1.1.2 2,217 8/4/2017
1.1.1 2,053 7/18/2017
1.1.0 1,950 6/16/2017
1.0.6 2,089 5/5/2017
1.0.5 2,045 5/4/2017
1.0.4 2,365 4/11/2017
1.0.2 2,789 12/26/2016
1.0.1 2,637 12/19/2016