UnionGenerator.AspNetCore
0.1.0
dotnet add package UnionGenerator.AspNetCore --version 0.1.0
NuGet\Install-Package UnionGenerator.AspNetCore -Version 0.1.0
<PackageReference Include="UnionGenerator.AspNetCore" Version="0.1.0" />
<PackageVersion Include="UnionGenerator.AspNetCore" Version="0.1.0" />
<PackageReference Include="UnionGenerator.AspNetCore" />
paket add UnionGenerator.AspNetCore --version 0.1.0
#r "nuget: UnionGenerator.AspNetCore, 0.1.0"
#:package UnionGenerator.AspNetCore@0.1.0
#addin nuget:?package=UnionGenerator.AspNetCore&version=0.1.0
#tool nuget:?package=UnionGenerator.AspNetCore&version=0.1.0
UnionGenerator.AspNetCore
Stop tedious if-else chains for HTTP error handling. Automatically map error types to status codes (404, 422, 409, etc.) with one-line integration—no manual mapping required.
❓ Why This Package?
The Problem
Without convention-based mapping, ASP.NET Core error handling is verbose and repetitive:
// ❌ Manual status code mapping (every endpoint)
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var result = _service.GetUser(id);
if (result.IsNotFound)
return NotFound(result.Error);
if (result.IsValidationError)
return UnprocessableEntity(result.Error);
if (result.IsConflict)
return Conflict(result.Error);
return Ok(result.Data); // ← Easy to forget one case!
}
// And repeat this 50+ times across your API...
Problems:
- Boilerplate repeated in every endpoint
- Easy to accidentally return wrong status code
- Hard to refactor error types (must update everywhere)
- Error type intent not obvious to developers
The Solution
// ✅ One line handles ALL status codes automatically
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
return _service.GetUser(id).ToActionResult();
}
The framework automatically maps based on error type:
NotFoundError→ 404ValidationError→ 422ConflictError→ 409BadRequestError→ 400- Custom errors → Custom status code via
[UnionStatusCode(code)]attribute
🚀 Quick Start (2 minutes)
1. Install & Setup
dotnet add package UnionGenerator.AspNetCore
// Program.cs
builder.Services.AddUnionResultHandling();
2. Define Error Types
// Errors/NotFoundError.cs
[UnionStatusCode(404)]
public class NotFoundError
{
public string Message { get; set; }
}
// Errors/ValidationError.cs
[UnionStatusCode(422)]
public class ValidationError
{
public Dictionary<string, string[]> Errors { get; set; }
}
3. Use in Controller
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var result = _service.GetUser(id);
return result.ToActionResult(); // ✨ Status codes auto-mapped!
}
}
That's it! Status codes are automatically mapped based on error types.
📚 Features
Convention-Based Status Code Mapping
Automatically infer HTTP status codes from error types without manual mapping:
// Automatic detection with @UnionStatusCode attribute
[UnionStatusCode(404)]
public class NotFoundError { }
[UnionStatusCode(409)]
public class ConflictError { }
[UnionStatusCode(422)]
public class ValidationError { }
Priority Chain (evaluated in order):
- [UnionStatusCode] Attribute (100) - Fastest, explicit
- StatusCode Property (75) -
public int StatusCode { get; } - Naming Pattern (50) -
NotFound→ 404,BadRequest→ 400 - ProblemDetails (50) - IProblemDetailsError types
🔧 Core Components
AttributeBasedConvention
Fastest path: Explicit status code via attribute.
[UnionStatusCode(404)]
public class NotFoundError { }
// Registry lookup: O(1), cached, 20,000x faster than reflection
Performance: ~1-10 ns after first use (cached)
🔑 Core Method: ToActionResult()
The ToActionResult() extension method converts a Result<TSuccess, TError> into an IActionResult with automatic status code mapping.
Behavior
// Success case → 200 OK with response body
var result = Result<User, UserError>.Ok(user);
result.ToActionResult(); // Returns: Ok(user) — Status 200
// Error case → Inferred status code + error response
var result = Result<User, UserError>.Error(new NotFoundError { Message = "..." });
result.ToActionResult(); // Returns: NotFound(error) — Status 404 (from attribute)
How It Determines Status Code
Method evaluates error type in order (first match wins):
| Priority | Method | Example | Result |
|---|---|---|---|
| 1 | [UnionStatusCode] Attribute | [UnionStatusCode(404)] |
Status from attribute |
| 2 | StatusCode Property | public int StatusCode { get; set; } |
Status from property |
| 3 | Type Name Pattern | NotFoundError |
Pattern-based: 404 |
| 4 | Default | Unknown error type | 500 (Internal Server Error) |
Real-World Example
// Three different error types, three different status codes—one line of code
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var result = _service.GetUser(id);
return result switch
{
{ IsSuccess: true, Data: var user } => Ok(user),
_ => result.ToActionResult() // Handles all error cases
};
}
// Or even simpler (if you always return Result<T, E>):
[HttpGet("{id}")]
public IActionResult GetUser(int id)
=> _service.GetUser(id).ToActionResult();
What If Error Type Isn't Recognized?
If the error type doesn't match any convention:
public class CustomApplicationError { } // No attribute, no property, unknown name
var result = Result<T, CustomApplicationError>.Error(error);
result.ToActionResult(); // → 500 Internal Server Error (safe default)
Recommendation: Always use [UnionStatusCode(code)] for custom errors to be explicit.
PropertyBasedConvention
Flexible approach: Status code in property.
public class CustomError
{
public int StatusCode => 418; // I'm a teapot!
}
// Supported property names: StatusCode, Status, HttpStatusCode
Performance: First lookup cached, subsequent O(1)
NameBasedConvention
Convention-driven: Status code inferred from type name.
public class UserNotFoundError { } // → 404
public class ValidationError { } // → 400
public class ConflictError { } // → 409
public class BadRequestError { } // → 400
Supported patterns:
*NotFound*→ 404*BadRequest*→ 400*Validation*→ 400*Unauthorized*→ 401*Forbidden*→ 403*Conflict*→ 409*UnprocessableEntity*→ 422*TooManyRequests*→ 429*InternalServerError*→ 500
📋 Structured Logging
Enable diagnostics with zero allocation when disabled:
// Program.cs
builder.Services.AddUnionResultHandling(options =>
{
options.LoggingOptions.LogErrorDetails = true;
options.LoggingOptions.LogConventionInference = true;
});
// In controller/service
var logger = HttpContext.RequestServices.GetRequiredService<UnionResultLogger>();
logger.LogErrorCase("NotFoundError", 404, "AttributeBased");
Log Output:
[Info] Union error case processed. Type: NotFoundError, StatusCode: 404, Convention: AttributeBased
Configuration Options:
new UnionLoggingOptions
{
LogSuccessResults = false, // Don't spam logs
LogErrorDetails = true, // Always log errors
LogConventionInference = true, // Debug convention behavior
MinimumLevel = LogLevel.Information
}
🎯 Common Use Cases
Case 1: Simple GET with 404
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var result = _service.GetUser(id); // Returns Result<User, NotFoundError>
return result.ToActionResult(); // Auto-maps to 404 if error
}
// Error type
[UnionStatusCode(404)]
public class NotFoundError { public string Message { get; set; } }
Case 2: Validation with 422
[HttpPost]
public IActionResult CreateUser(CreateUserDto dto)
{
var result = _service.CreateUser(dto); // Returns Result<User, ValidationError>
return result.ToActionResult(); // Auto-maps to 422 if validation fails
}
// Error type
[UnionStatusCode(422)]
public class ValidationError
{
public Dictionary<string, string[]> Errors { get; set; }
}
Case 3: Multiple Error Types
[HttpPost("{id}/role")]
public IActionResult AssignRole(int id, string role)
{
var result = _service.AssignRole(id, role);
return result.ToActionResult(); // Handles all error types automatically
}
// Error types (each with own status code)
[UnionStatusCode(404)]
public class UserNotFoundError { }
[UnionStatusCode(400)]
public class InvalidRoleError { }
[UnionStatusCode(409)]
public class RoleAlreadyAssignedError { }
🔍 Compile-Time Analyzers
Detect common mistakes at build time:
UG4010: Union Not Mapped to IActionResult
// ❌ Warning: Union type returned without mapping
public Result<User, NotFoundError> GetUser(int id)
{
return _service.GetUser(id);
}
// ✅ Correct
public IActionResult GetUser(int id)
{
return _service.GetUser(id).ToActionResult();
}
UG4011: Error Case Lacks Status Code
// ❌ Warning: No status code defined
public class CustomError { }
// ✅ Correct
[UnionStatusCode(400)]
public class CustomError { }
UG4012: Convention Override Recommended
// ℹ️ Info: Inferred by convention, but explicit is better
public class NotFoundError { } // Convention infers 404
// ✅ Better
[UnionStatusCode(404)]
public class NotFoundError { }
⚡ Performance
Benchmarks
| Operation | Time | vs Reflection |
|---|---|---|
| Status code lookup (cached) | 5-10 ns | 20,000x faster |
| Convention resolution (first) | ~100 µs | Reflection-based |
| Convention resolution (cached) | ~5 ns | 20,000x faster |
| Logging disabled | 10 ns | 1,000x faster |
Optimization Tips
- Use [UnionStatusCode] attribute for known error types (fastest)
- Define errors once, reuse everywhere (cached after first lookup)
- Let convention infer for standard patterns (NotFound, BadRequest, etc.)
- Disable success logging in production (reduces noise)
🧪 Testing
Test your union result handlers:
[Fact]
public async Task GetUser_WithValidId_ReturnsOk()
{
// Arrange
var service = new Mock<IUserService>();
service.Setup(s => s.GetUser(1))
.ReturnsAsync(Result<User>.Success(new User { Id = 1, Name = "John" }));
var controller = new UsersController(service.Object);
// Act
var result = controller.GetUser(1) as OkObjectResult;
// Assert
Assert.NotNull(result);
Assert.Equal(200, result.StatusCode);
}
[Fact]
public async Task GetUser_WithInvalidId_ReturnsNotFound()
{
// Arrange
var service = new Mock<IUserService>();
service.Setup(s => s.GetUser(999))
.ReturnsAsync(Result<User>.Error(new NotFoundError { Message = "User not found" }));
var controller = new UsersController(service.Object);
// Act
var result = controller.GetUser(999) as ObjectResult;
// Assert
Assert.NotNull(result);
Assert.Equal(404, result.StatusCode); // Convention mapped automatically!
}
🛠️ Advanced Configuration
Custom Convention
// Define custom convention
public class MyCustomConvention : IStatusCodeConvention
{
public int Priority => 80; // Between Property (75) and ProblemDetails (50)
public bool TryGetStatusCode(object error, out int statusCode)
{
if (error is ICustomError customError)
{
statusCode = customError.HttpStatus;
return true;
}
statusCode = 0;
return false;
}
}
// Register
services.AddUnionResultHandling(options =>
{
options.CustomConventions.Add(new MyCustomConvention());
});
Custom Logging
// Inject logger
public class UsersController : ControllerBase
{
private readonly UnionResultLogger _logger;
public UsersController(UnionResultLogger logger)
{
_logger = logger;
}
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var result = _service.GetUser(id);
if (result.IsError)
{
_logger.LogErrorCase(result.Error.GetType().Name, 404, "AttributeBased");
}
return result.ToActionResult();
}
}
📖 Best Practices
✅ DO
- Define error types with explicit
[UnionStatusCode]attribute - Keep error types focused and single-purpose
- Use descriptive error type names
- Log errors for diagnostics
- Document expected error codes in API documentation
❌ DON'T
- Mix success and error logic
- Return raw Result<T, E> from controllers (use ToActionResult())
- Create error types without status codes
- Rely solely on convention for non-standard patterns
- Log success cases in production (causes spam)
🔗 Integration Examples
With Minimal APIs
app.MapGet("/users/{id}", (int id, IUserService service) =>
service.GetUser(id).ToActionResult())
.Produces<User>(200)
.Produces<NotFoundError>(404)
.WithName("GetUser");
With MediatR
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserCommand command)
{
var result = await _mediator.Send(command);
return result.ToActionResult(); // Works with MediatR commands returning Result<T, E>
}
With OpenAPI/Swagger
[HttpGet("{id}")]
[ProducesResponseType(typeof(User), 200)]
[ProducesResponseType(typeof(NotFoundError), 404)]
public IActionResult GetUser(int id)
{
return _service.GetUser(id).ToActionResult();
}
📊 Comparison
Without UnionGenerator.AspNetCore
// Manual mapping everywhere
public IActionResult GetUser(int id)
{
var user = _service.GetUser(id);
if (user == null)
return NotFound(new { message = "User not found" });
return Ok(user);
}
public IActionResult CreateUser(CreateUserDto dto)
{
var validation = _validator.Validate(dto);
if (!validation.IsValid)
return UnprocessableEntity(new { errors = validation.Errors });
var user = _service.CreateUser(dto);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
// ... repeated 100 times across your codebase
With UnionGenerator.AspNetCore
// Convention-based, consistent
public IActionResult GetUser(int id)
=> _service.GetUser(id).ToActionResult();
public IActionResult CreateUser(CreateUserDto dto)
=> _service.CreateUser(dto).ToActionResult();
// Define once
[UnionStatusCode(404)]
public class NotFoundError { }
[UnionStatusCode(422)]
public class ValidationError { }
// Used everywhere automatically
📊 Structured Logging & Observability
Built-In Logging Middleware
UnionGenerator includes a production-grade middleware that logs all union result responses with structured information. This provides immediate visibility into error patterns and response times.
Quick Setup (1 line)
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ... configure services ...
var app = builder.Build();
// Add middleware for structured result logging (minimal overhead)
app.UseMiddleware<UnionResultLoggingMiddleware>();
// ... rest of middleware pipeline ...
app.UseRouting();
app.MapControllers();
app.Run();
That's it! Every HTTP response with a union error is now logged automatically.
What Gets Logged?
The middleware logs with this structured template:
UnionResult: Path={Path} Method={Method} Status={Status} Elapsed={Elapsed}ms Case={Case}
Log Output Examples
Success Response (2xx)
[Debug] UnionResult: Path=/api/users Method=GET Status=200 Elapsed=12ms Case=Success
Not Found Error (404)
[Warning] UnionResult: Path=/api/users/999 Method=GET Status=404 Elapsed=8ms Case=NotFound
Validation Error (422)
[Warning] UnionResult: Path=/api/users Method=POST Status=422 Elapsed=15ms Case=ValidationError
Conflict (409)
[Warning] UnionResult: Path=/api/items Method=POST Status=409 Elapsed=10ms Case=Conflict
Server Error (500)
[Warning] UnionResult: Path=/api/data Method=GET Status=500 Elapsed=45ms Case=InternalServerError
Log Fields Explained
| Field | Meaning | Example |
|---|---|---|
| Path | Request URL path | /api/users/123 |
| Method | HTTP verb | GET, POST, PUT, DELETE |
| Status | HTTP status code | 200, 404, 422, 500 |
| Elapsed | Response time in milliseconds | 15 |
| Case | Union case name (inferred from status) | NotFound, ValidationError, Conflict |
Conditional Logging Levels
- Success (2xx): Logged at Debug level (low noise in production)
- Client Errors (4xx): Logged at Warning level (actionable issues)
- Server Errors (5xx): Logged at Warning level (critical for monitoring)
- Unknown Status: Logged as
Error({statusCode})for diagnostics
Status Code to Case Name Mapping
The middleware automatically infers case names from HTTP status codes:
| Status | Case Name |
|---|---|
| 200-299 | Success |
| 400 | BadRequest |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | NotFound |
| 409 | Conflict |
| 422 | ValidationError |
| 429 | TooManyRequests |
| 500 | InternalServerError |
| 502 | BadGateway |
| 503 | ServiceUnavailable |
| Other | Error(code) |
Sharing Metrics via HttpContext
The middleware automatically measures request elapsed time. You can access metrics in downstream code:
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var result = _service.GetUser(id);
// Metrics are available in HttpContext.Items
// (The middleware captures them automatically)
return result.ToActionResult();
}
Future enhancement: Response metrics will be stored in HttpContext.Items["UnionResultMetrics"] for custom processing.
Real-World Example: Logging with Application Insights
// Program.cs
builder.Services.AddApplicationInsightsTelemetry();
var app = builder.Build();
// Middleware will automatically log to Application Insights
app.UseMiddleware<UnionResultLoggingMiddleware>();
app.MapControllers();
app.Run();
Result: All error responses appear in Application Insights Logs with structured fields.
Query example:
customEvents
| where name startswith "UnionResult"
| where properties.Status >= 400
| summarize count() by properties.Case
| render barchart
Disabling Debug Logging (Production)
In production, you may want to suppress Debug-level logs to reduce noise:
// Program.cs
if (app.Environment.IsProduction())
{
app.Logger.LogLevel = LogLevel.Information; // Filters out Debug logs
}
Or configure in appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"UnionGenerator.AspNetCore.Middleware": "Warning"
}
}
}
Performance Characteristics
- Middleware Overhead: ~1-2ms per request (dominated by response stream buffering)
- Logging (Disabled): 0 allocations, ~0.1µs
- Logging (Enabled): 1-2 allocations, ~10-50µs
- No Impact on Success Cases: Debug-level logging disabled in most production configurations
Troubleshooting
Problem: Logs not appearing
Solution:
- Verify middleware is registered:
app.UseMiddleware<UnionResultLoggingMiddleware>(); - Check logging level in
appsettings.json(must not filter out Debug/Warning) - Ensure logging provider is configured (Console, Debug, etc.)
// Ensure logging is wired up
builder.Logging.AddConsole(); // or AddDebug(), AddApplicationInsights(), etc.
builder.Logging.SetMinimumLevel(LogLevel.Debug);
Problem: Performance impact
Recommendation:
- Keep middleware; overhead is minimal (1-2ms per request)
- Set minimum log level to Information in production
- Use sampling in Application Insights if log volume is high
🐛 Troubleshooting
Status Code Not Mapping
Problem: Error returns 500 instead of expected status code
Solution:
- Ensure error type has
[UnionStatusCode]attribute - Or has
int StatusCode { get; }property - Or matches naming pattern (NotFound, BadRequest, etc.)
- Check analyzer warnings (UG4011)
Logging Not Appearing
Problem: No error logs in console
Solution:
// Enable error logging
services.AddUnionResultHandling(options =>
{
options.LoggingOptions.LogErrorDetails = true;
});
// Ensure logging provider is configured
builder.Logging.AddConsole(); // or AddDebug(), etc.
Type Not Found in Convention
Problem: Compiler can't find convention classes
Solution:
// Add using statement
using UnionGenerator.AspNetCore.Conventions;
using UnionGenerator.AspNetCore.Logging;
using UnionGenerator.AspNetCore.Extensions;
📚 Resources
- GitHub: UnionGenerator
- NuGet: UnionGenerator.AspNetCore
- Documentation: See project README
- Examples: Check
examples/folder
📄 License
MIT License - See LICENSE file for details
✨ Summary
| Feature | Benefit |
|---|---|
| Convention-Based Mapping | 95% less boilerplate |
| 20,000x Performance | Cached attribute lookup |
| Compile-Time Analysis | Catch errors at build time |
| Zero Allocation Logging | Production-ready |
| DI-First Design | Testable, flexible |
Start now: Add [UnionStatusCode] to your error types and call .ToActionResult() in controllers. Done! 🚀
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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 was computed. 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. |
-
net8.0
- System.Collections.Immutable (>= 10.0.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on UnionGenerator.AspNetCore:
| Package | Downloads |
|---|---|
|
UnionGenerator.FluentValidation
FluentValidation integration for UnionGenerator. Automatically converts validation failures to structured ProblemDetails errors with field-level error mapping. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 119 | 1/21/2026 |
Initial release: ASP.NET Core integration with automatic ProblemDetails conversion and ActionResult support.