Xpandables.AspNetCore.Results
10.0.2
dotnet add package Xpandables.AspNetCore.Results --version 10.0.2
NuGet\Install-Package Xpandables.AspNetCore.Results -Version 10.0.2
<PackageReference Include="Xpandables.AspNetCore.Results" Version="10.0.2" />
<PackageVersion Include="Xpandables.AspNetCore.Results" Version="10.0.2" />
<PackageReference Include="Xpandables.AspNetCore.Results" />
paket add Xpandables.AspNetCore.Results --version 10.0.2
#r "nuget: Xpandables.AspNetCore.Results, 10.0.2"
#:package Xpandables.AspNetCore.Results@10.0.2
#addin nuget:?package=Xpandables.AspNetCore.Results&version=10.0.2
#tool nuget:?package=Xpandables.AspNetCore.Results&version=10.0.2
🌐 AspNetCore.Results
ASP.NET Core Integration for Result Pattern — Convert Results to HTTP responses, ProblemDetails, and ModelState with endpoint filters, MVC filters, middleware, and validation support.
📋 Overview
AspNetCore.Results provides comprehensive ASP.NET Core integration for the System.Results Result pattern. The library bridges the gap between your domain's Result type and ASP.NET Core's HTTP infrastructure, enabling automatic result-to-response conversion, validation filters, exception handling middleware, and consistent ProblemDetails responses.
Built for .NET 10 with C# 14 extension members, this package ensures your APIs return consistent, well-structured responses whether using minimal APIs or MVC controllers.
✨ Key Features
- 🔄 Result Extensions — Convert
ResulttoIActionResult,ProblemDetails, andModelStateDictionary - 🛡️ Endpoint Filters —
WithXResultSupport(),WithXResultFilter(),WithXResultValidation()for minimal APIs - 🏗️ MVC Filters —
ControllerResultFilterandControllerResultValidationFilterAttributefor controllers - 📝 Header Writing —
IResultHeaderWriterfor custom response header handling - ⚡ Exception Middleware —
ResultMiddlewarefor global exception-to-ProblemDetails conversion - ✅ Validation Integration — Automatic validation via
IResultEndpointValidatorandIValidatorProvider - 🔐 Authentication Support — Automatic
WWW-Authenticateheader for 401 responses
📦 Installation
dotnet add package AspNetCore.Results
Or via NuGet Package Manager:
Install-Package AspNetCore.Results
🚀 Quick Start
Service Registration
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Register Result middleware for exception handling
builder.Services.AddXResultMiddleware();
// Register endpoint validator with default implementation
builder.Services.AddXResultEndpointValidator();
// Register MVC options for controller Result support
builder.Services.AddXControllerResultMvcOptions();
// Add controllers
builder.Services.AddControllers();
var app = builder.Build();
// Use Result middleware for global exception handling
app.UseXResultMiddleware();
app.MapControllers();
app.Run();
🛡️ Minimal API Endpoint Filters
Apply Filters to Endpoints
using Microsoft.AspNetCore.Http;
using System.Results;
var app = builder.Build();
// Apply both validation and result filters
app.MapPost("/api/orders", async (CreateOrderRequest request, IOrderService orderService) =>
{
Result<Order> result = await orderService.CreateAsync(request);
return result; // Filter converts Result to HTTP response
})
.WithXResultSupport();
// Apply only result filter (converts Result to response)
app.MapGet("/api/orders/{id}", async (Guid id, IOrderService orderService) =>
{
Result<Order> result = await orderService.GetByIdAsync(id);
return result;
})
.WithXResultFilter();
// Apply only validation filter
app.MapPost("/api/products", async (CreateProductRequest request, IProductService productService) =>
{
Result<Product> result = await productService.CreateAsync(request);
return result;
})
.WithXResultValidation();
app.Run();
How Filters Work
The ResultEndpointFilter automatically:
- Intercepts
Resultresponses from endpoint handlers - Writes response headers via
IResultHeaderWriter(status code, Location, custom headers) - Converts failures to ProblemDetails using the configured
IProblemDetailsService - Extracts success values and returns them as the response body
- Handles exceptions and converts them to structured error responses
// Example: Handler returns Result<T>
app.MapGet("/api/users/{id}", async (Guid id, IUserService userService) =>
{
// Service returns Result<User>
return await userService.GetByIdAsync(id);
})
.WithXResultFilter();
// On success: Returns User with 200 OK
// On failure: Returns ProblemDetails with appropriate status code
🔄 Result Extensions
Convert Result to IActionResult (MVC)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System.Results;
[ApiController]
[Route("api/[controller]")]
public class UsersController(IUserService userService) : ControllerBase
{
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetUser(Guid id, CancellationToken ct)
{
Result<User> result = await userService.GetByIdAsync(id, ct);
// Converts Result to ObjectResult with correct status code
return result.ToActionResult();
}
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest request, CancellationToken ct)
{
Result<User> result = await userService.CreateAsync(request, ct);
return result.ToActionResult();
}
}
Convert Result to ProblemDetails
using Microsoft.AspNetCore.Http;
using System.Results;
// Manual conversion to ProblemDetails
app.MapGet("/api/custom/{id}", async (Guid id, HttpContext context, IDataService dataService) =>
{
Result<Data> result = await dataService.GetAsync(id);
if (result.IsFailure)
{
ProblemDetails problem = result.ToProblemDetails(context);
return Results.Problem(problem);
}
return Results.Ok(result.Value);
});
Convert Result to ModelStateDictionary
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System.Results;
[HttpPost]
public async Task<IActionResult> CreateWithView(CreateItemRequest request, CancellationToken ct)
{
Result<Item> result = await _itemService.CreateAsync(request, ct);
if (result.IsFailure)
{
// Convert Result errors to ModelState for view rendering
ModelStateDictionary modelState = result.ToModelStateDictionary();
foreach (var error in modelState)
{
foreach (var stateError in error.Value.Errors)
{
ModelState.AddModelError(error.Key, stateError.ErrorMessage);
}
}
return View(request);
}
return RedirectToAction("Details", new { id = result.Value!.Id });
}
Convert ModelState to Result
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System.Results;
using System.Net;
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest request, CancellationToken ct)
{
if (!ModelState.IsValid)
{
// Convert ModelState errors to Result
Result validationResult = ModelState.ToResult(HttpStatusCode.BadRequest);
return validationResult.ToActionResult();
}
Result<User> result = await _userService.CreateAsync(request, ct);
return result.ToActionResult();
}
Convert BadHttpRequestException to Result
using Microsoft.AspNetCore.Http;
using System.Results;
// The library automatically handles this in ResultMiddleware
// But you can also use it manually:
try
{
// Some operation that might throw
}
catch (BadHttpRequestException ex)
{
Result result = ex.ToResult();
// result.StatusCode = exception's status code
// result.Errors contains the parameter name and error message
}
🏗️ MVC Controller Support
Automatic Result Filter Registration
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Register MVC options with Result filters
builder.Services.AddXControllerResultMvcOptions();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
This registers:
ControllerResultValidationFilterAttribute— Validates ModelState before action executionControllerResultFilter— ProcessesResultreturn types and writes headers
Controller with Result Support
using Microsoft.AspNetCore.Mvc;
using System.Results;
[ApiController]
[Route("api/[controller]")]
public class ProductsController(IProductService productService) : ControllerBase
{
[HttpGet]
public async Task<Result<IEnumerable<Product>>> GetProducts(CancellationToken ct)
{
// Return Result directly - filter handles conversion
return await productService.GetAllAsync(ct);
}
[HttpGet("{id:guid}")]
public async Task<Result<Product>> GetProduct(Guid id, CancellationToken ct)
{
return await productService.GetByIdAsync(id, ct);
}
[HttpPost]
public async Task<Result<Product>> CreateProduct(CreateProductRequest request, CancellationToken ct)
{
// ControllerResultValidationFilterAttribute validates ModelState
// ControllerResultFilter handles Result response
return await productService.CreateAsync(request, ct);
}
}
Manual Filter Application
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Results;
[ApiController]
[Route("api/[controller]")]
[ControllerResultValidationFilterAttribute] // Validates ModelState
public class OrdersController(IOrderService orderService) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request, CancellationToken ct)
{
// ModelState already validated by filter
Result<Order> result = await orderService.CreateAsync(request, ct);
return result.ToActionResult();
}
}
⚡ Exception Handling Middleware
The ResultMiddleware provides global exception handling:
var builder = WebApplication.CreateBuilder(args);
// Register middleware
builder.Services.AddXResultMiddleware();
var app = builder.Build();
// Use middleware - catches unhandled exceptions
app.UseXResultMiddleware();
app.MapGet("/api/risky", () =>
{
throw new InvalidOperationException("Something went wrong!");
// Middleware converts to ProblemDetails response
});
app.Run();
Exception Handling Behavior
The middleware handles:
BadHttpRequestException→ Converts usingToResult()extensionResultException→ Extracts the embeddedResult- Other exceptions → Converts using
exception.ToResult()
All exceptions are converted to ProblemDetails responses when the response hasn't started.
📝 Custom Header Writing
Default Header Writer
The ResultHeaderWriter automatically:
- Sets
Content-Typeheader - Sets response status code from
Result.StatusCode - Adds
Locationheader ifResult.Locationis set - Appends custom headers from
Result.Headers - Adds
WWW-Authenticateheader for 401 responses
Custom Header Writer Implementation
using Microsoft.AspNetCore.Http;
using System.Results;
public sealed class CustomResultHeaderWriter : IResultHeaderWriter
{
public async Task WriteAsync(HttpContext context, Result result)
{
context.Response.ContentType = "application/json; charset=utf-8";
context.Response.StatusCode = (int)result.StatusCode;
// Add custom correlation header
if (result.Extensions.TryGetValue("CorrelationId", out var correlationId))
{
context.Response.Headers["X-Correlation-Id"] = correlationId?.ToString();
}
// Add timing header
context.Response.Headers["X-Response-Time"] = DateTime.UtcNow.ToString("O");
// Handle Location header for created resources
if (result.Location is not null)
{
context.Response.Headers.Location = result.Location.ToString();
}
await Task.CompletedTask;
}
}
// Register custom writer
builder.Services.AddXResultHeaderWriter<CustomResultHeaderWriter>();
✅ Validation Integration
Automatic Validation with IRequiresValidation
The ResultEndpointValidator automatically validates endpoint arguments that implement IRequiresValidation:
using System.ComponentModel.DataAnnotations;
using System.Results;
// Request implements IRequiresValidation
public record CreateOrderRequest(string CustomerId, List<OrderItem> Items) : IRequiresValidation;
// Validator for the request
public sealed class CreateOrderRequestValidator : IValidator<CreateOrderRequest>
{
public async ValueTask<IReadOnlyCollection<ValidationResult>> ValidateAsync(
CreateOrderRequest instance,
CancellationToken cancellationToken = default)
{
List<ValidationResult> results = [];
if (string.IsNullOrWhiteSpace(instance.CustomerId))
{
results.Add(new ValidationResult("CustomerId is required", ["CustomerId"]));
}
if (instance.Items is null || instance.Items.Count == 0)
{
results.Add(new ValidationResult("At least one item is required", ["Items"]));
}
return results;
}
public IReadOnlyCollection<ValidationResult> Validate(CreateOrderRequest instance)
=> ValidateAsync(instance).GetAwaiter().GetResult();
}
// Register validator
builder.Services.AddXResultEndpointValidator();
builder.Services.AddScoped<IValidator<CreateOrderRequest>, CreateOrderRequestValidator>();
// Endpoint with automatic validation
app.MapPost("/api/orders", async (CreateOrderRequest request, IOrderService orderService) =>
{
// Validator runs before handler if request implements IRequiresValidation
return await orderService.CreateAsync(request);
})
.WithXResultValidation()
.WithXResultFilter();
🔧 Extension Methods Summary
IServiceCollection Extensions
| Method | Description |
|---|---|
AddXResultMiddleware() |
Registers ResultMiddleware for exception handling |
AddXResultEndpointValidator() |
Registers default endpoint validator with dependencies |
AddXResultEndpointValidator<T>() |
Registers custom endpoint validator |
AddXResultHeaderWriter() |
Registers default header writer |
AddXResultHeaderWriter<T>() |
Registers custom header writer |
AddXControllerResultMvcOptions() |
Registers MVC options with Result filters |
IApplicationBuilder Extensions
| Method | Description |
|---|---|
UseXResultMiddleware() |
Adds exception handling middleware to pipeline |
IEndpointConventionBuilder Extensions
| Method | Description |
|---|---|
WithXResultSupport() |
Adds both validation and result filters |
WithXResultFilter() |
Adds result-to-response conversion filter |
WithXResultValidation() |
Adds automatic validation filter |
Result Extensions
| Method | Description |
|---|---|
ToActionResult() |
Converts Result to IActionResult |
ToProblemDetails(context) |
Converts Result to ProblemDetails |
ToModelStateDictionary() |
Converts Result errors to ModelStateDictionary |
ModelStateDictionary Extensions
| Method | Description |
|---|---|
ToResult(statusCode) |
Converts ModelState errors to Result |
BadHttpRequestException Extensions
| Method | Description |
|---|---|
ToResult() |
Converts exception to Result with error details |
✅ Best Practices
✅ Do
- Use
WithXResultSupport()for endpoints that need both validation and result conversion - Register
AddXResultMiddleware()for global exception handling - Return
Result<T>from services and let filters handle HTTP conversion - Implement
IRequiresValidationon request types for automatic validation - Use
AddXControllerResultMvcOptions()for MVC controller support
❌ Don't
- Mix manual and automatic Result handling — choose one approach per endpoint
- Catch exceptions in handlers when using
ResultMiddleware— let middleware handle them - Return raw exceptions — wrap them in
Result.Failure()or let middleware convert them - Forget to register
IResultHeaderWriter— required for endpoint filters
📚 Related Packages
| Package | Description |
|---|---|
| System.Results | Core Result and Result<T> types |
| System.Validation | Specifications and validator abstractions |
| AspNetCore.Net | Minimal API infrastructure and endpoint routing |
| AspNetCore.AsyncPaged | Async paged enumerable HTTP streaming |
📄 License
Apache License 2.0 - Copyright © Kamersoft 2025
Contributions welcome at Xpandables.Net on GitHub.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Xpandables.AspNetCore.Net (>= 10.0.2)
- Xpandables.Results (>= 10.0.2)
- Xpandables.Validation (>= 10.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.