Standardisiertes API-Response-Format in ASP.NET Core (Result<T>)
Standardisieren Sie Erfolgs- und Fehlerantworten in ASP.NET Core mit einem Result<T>-Wrapper (message, code, data, errors) für eine vorhersagbare Client-Verarbeitung.
Viele APIs liefern je nach Endpoint unterschiedliche Response-Strukturen zurück:
manchmal rohe Daten, manchmal { error: "..." }, manchmal sogar Plain-Text.
Das macht die Client-Seite (Frontend/Mobile) unnötig kompliziert, weil für jedes Endpoint ein eigenes Parsing/Handling nötig wird.
Eine einfache Lösung ist, Responses mit einem Result<T>-Wrapper zu standardisieren.
Der Client erhält eine vorhersehbare Struktur (message, code, data, errors),
während die API weiterhin sinnvolle HTTP-Statuscodes verwendet (200/201/400/404/409/500).
Ein wichtiges Detail: errors: [] bei Erfolg oder data: null bei Fehlern bläht Payloads unnötig auf.
In diesem Beispiel wird JsonIgnore mit WhenWritingNull verwendet, damit null-Properties nicht serialisiert werden.
Ziel
- Eine konsistente Response-Struktur für Erfolg und Fehler zurückgeben.
- Payloads kompakt halten, indem
null-Properties nicht ausgegeben werden. - Client-Handling vorhersagbar machen (insbesondere für UI und Validation).
- HTTP-Statuscodes sinnvoll nutzen und trotzdem Response-Bodies standardisieren.
Ziel-Response-Format
Bei Erfolg wird nur data ohne unnötige Felder zurückgegeben:
{
"data": {
"id": 42,
"code": "RSV-2025-00042",
"guestName": "Alex",
"date": "2026-02-21"
},
"message": "Reservation loaded.",
"code": "RESERVATION_LOADED"
}
Bei Fehlern wird nur errors (ohne data) zurückgegeben:
{
"message": "Validation failed.",
"code": "VALIDATION_ERROR",
"errors": [
{ "field": "id", "message": "Id must be greater than zero." }
]
}
Result-Modelle erstellen (kompaktes JSON mit JsonIgnore)
Definieren Sie ein generisches Result<T> sowie ein kleines Error-DTO.
null-Properties werden nicht in die JSON-Ausgabe geschrieben.
using System.Text.Json.Serialization;
public sealed record ErrorItem(string Field, string Message);
public sealed class Result<T>
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public T? Data { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<ErrorItem>? Errors { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Message { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Code { get; init; }
public static Result<T> Ok(T data, string? message = null, string? code = null)
=> new()
{
Data = data,
Message = message,
Code = code
};
public static Result<T> Fail(string message, string code, List<ErrorItem>? errors = null)
=> new()
{
Message = message,
Code = code,
Errors = errors
};
}
In dieser Version gibt es bewusst kein success-Feld.
Erfolg/Fehler wird bereits durch den HTTP-Statuscode klar ausgedrückt, und der Payload bleibt kleiner.
Reservation-Modell + Service
Um das Beispiel näher an die Praxis zu bringen, wird ein kleines Modell und ein Service verwendet:
public sealed record ReservationDto(int Id, string Code, string GuestName, DateOnly Date);
public sealed class ReservationService
{
// In einer echten Anwendung würde hier ein Datenbankzugriff stattfinden.
public Task<ReservationDto?> GetByIdAsync(int id, CancellationToken ct)
{
if (id == 42)
{
return Task.FromResult<ReservationDto?>(
new ReservationDto(42, "RSV-2025-00042", "Alex", new DateOnly(2026, 2, 21))
);
}
return Task.FromResult<ReservationDto?>(null);
}
}
Verwendung im Controller (mit Service)
Der Controller standardisiert den Response-Body mit Result<T> und nutzt gleichzeitig sinnvolle HTTP-Statuscodes.
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/reservations")]
public sealed class ReservationsController : ControllerBase
{
private readonly ReservationService _service;
public ReservationsController(ReservationService service)
{
_service = service;
}
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id, CancellationToken ct)
{
if (id <= 0)
{
return BadRequest(Result<object>.Fail(
message: "Validation failed.",
code: "VALIDATION_ERROR",
errors: new List<ErrorItem>
{
new("id", "Id must be greater than zero.")
}
));
}
var reservation = await _service.GetByIdAsync(id, ct);
if (reservation is null)
{
return NotFound(Result<object>.Fail(
message: "Reservation not found.",
code: "RESERVATION_NOT_FOUND"
));
}
return Ok(Result<ReservationDto>.Ok(
data: reservation,
message: "Reservation loaded.",
code: "RESERVATION_LOADED"
));
}
}
Optional: Helper für Validation-Fehler
Für Validation-Szenarien halten kleine Helper den Controller-Code sauber:
public static class ErrorList
{
public static List<ErrorItem> One(string field, string message)
=> new() { new ErrorItem(field, message) };
public static List<ErrorItem> Many(params (string field, string message)[] items)
=> items.Select(x => new ErrorItem(x.field, x.message)).ToList();
}
Häufige Verbesserungen
-
Verwenden Sie stabile, dokumentierte Codes (z. B.
RESERVATION_NOT_FOUND) und halten Sie diese serviceübergreifend konsistent. -
Keine Exception-Details in
Messageausgeben. Vollständige Details serverseitig loggen. -
Wenn RFC 7807 strikt eingehalten werden soll: für Fehler
ProblemDetails, für ErfolgResult<T>verwenden. -
Für Log-Korrelation eine
traceIdergänzen (oder als Header zurückgeben).
TL;DR
Result<T>standardisiert Responses mitmessage,code,data,errors.- Mit
[JsonIgnore(Condition = WhenWritingNull)]werdennull-Properties ausgelassen und der Payload bleibt klein. - HTTP-Statuscodes sinnvoll nutzen und gleichzeitig vorhersehbare JSON-Bodies zurückgeben.
- Interne Details serverseitig loggen und sichere Nachrichten an den Client zurückgeben.