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.