18 Feb 2025 - Matthias Voigt
In vielen ASP.NET Core-Projekten werden API-Antworten mit IActionResult gestaltet, um Fehler wie NotFound(), BadRequest() oder Forbidden() zurückzugeben. Alternativ setzen einige Entwickler auf Exceptions, die dann von einer Middleware abgefangen werden. Beide Ansätze haben jedoch Nachteile.
In diesem Beitrag zeige ich einen besseren Weg: Eine strukturierte Rückgabe mit einem generischen Result<T>-Wrapper, der die Vorteile beider Ansätze kombiniert.
IActionResult und ExceptionsIActionResult führt zu aufgeblähtem CodeEin typischer Controller mit IActionResult sieht oft so aus:
[HttpGet("{contactId:guid}")]
public async Task<IActionResult> GetContact(Guid contactId)
{
var userId = _context.Principal.GetUserId();
var contact = await _contactService.GetContact(userId, contactId);
if (contact == null)
return NotFound(new { message = "Contact not found" });
if (contact.IsDeleted)
return StatusCode(StatusCodes.Status410Gone, new { message = "Contact has been deleted" });
if (!contact.IsAccessibleBy(userId))
return Forbid();
return Ok(contact);
}
IActionResult verwenden und sich explizit um Fehler kümmern.Einige setzen auf Exceptions, um Fehler zentral über eine Middleware zu handhaben:
[HttpGet("{contactId:guid}")]
public async Task<ContactDto> GetContact(Guid contactId)
{
var userId = _context.Principal.GetUserId();
var contact = await _contactService.GetContact(userId, contactId);
if (contact == null)
throw new KeyNotFoundException("Contact not found");
if (contact.IsDeleted)
throw new InvalidOperationException("Contact has been deleted");
if (!contact.IsAccessibleBy(userId))
throw new UnauthorizedAccessException("Access denied");
return contact;
}
NotFound.NotFound oder Gone sind keine Fehler, sondern valide API-Zustände.Daher ist die Lösung, reguläre API-Zustände nicht als Exception zu behandeln, sondern strukturiert zurückzugeben.
Result<T>-WrapperAnstatt IActionResult oder Exceptions verwenden wir eine strukturiere Rückgabe, die neben dem eigentlichen Ergebnis auch einen HTTP-Status und eine Fehlermeldung enthalten kann.
Result<T>-Klasse definierenpublic class Result<T>
{
public T? Value { get; }
public int StatusCode { get; }
public string? ErrorMessage { get; }
private Result(T? value, int statusCode, string? errorMessage = null)
{
Value = value;
StatusCode = statusCode;
ErrorMessage = errorMessage;
}
public static Result<T> Success(T value) => new(value, StatusCodes.Status200OK);
public static Result<T> NotFound(string message = "Resource not found") => new(default, StatusCodes.Status404NotFound, message);
public static Result<T> Gone(string message = "Resource no longer available") => new(default, StatusCodes.Status410Gone, message);
public static Result<T> Forbidden(string message = "Access denied") => new(default, StatusCodes.Status403Forbidden, message);
public static Result<T> Unauthorized(string message = "Authentication required") => new(default, StatusCodes.Status401Unauthorized, message);
}
Jetzt haben wir eine standardisierte Möglichkeit, API-Ergebnisse mit Statuscodes zurückzugeben.
Result<T> zurück[HttpGet("{contactId:guid}")]
public async Task<Result<ContactDto>> GetContact(Guid contactId)
{
var userId = _context.Principal.GetUserId();
var contact = await _contactService.GetContact(userId, contactId);
if (contact == null)
return Result<ContactDto>.NotFound();
if (contact.IsDeleted)
return Result<ContactDto>.Gone();
if (!contact.IsAccessibleBy(userId))
return Result<ContactDto>.Forbidden();
return Result<ContactDto>.Success(contact);
}
Jetzt sind unsere Controller schlank und geben nur das Ergebnis zurück. Kein IActionResult, keine unnötigen Exceptions.
Result<T> in HTTP-Antworten umpublic class ResultMiddleware
{
private readonly RequestDelegate _next;
public ResultMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await _next(context);
if (context.Items.TryGetValue("Result", out var resultObj) && resultObj is Result<object> result)
{
context.Response.StatusCode = result.StatusCode;
if (result.Value != null)
{
await context.Response.WriteAsJsonAsync(result.Value);
}
else if (!string.IsNullOrEmpty(result.ErrorMessage))
{
await context.Response.WriteAsJsonAsync(new { message = result.ErrorMessage });
}
}
}
}
Dann in Program.cs registrieren:
app.UseMiddleware<ResultMiddleware>();
Middleware setzt automatisch den richtigen HTTP-Status. Die API bleibt REST-konform und einfach zu testen.
Der Result<T>-Ansatz bietet eine strukturierte, saubere und performante API-Fehlerbehandlung:
IActionResult mehr nötigNotFound()-RückgabenDieser Ansatz macht APIs konsistenter, testbarer und einfacher zu warten.