Minimale APIs in .NET: Erstellen leichter HTTP-APIs
Wenn Sie schon seit einiger Zeit APIs mit ASP.NET Core erstellen, sind Sie wahrscheinlich mit dem Controller-basierten Ansatz bestens vertraut: Erstellen Sie eine Controller-Klasse, dekorieren Sie sie mit Attributen, injizieren Sie Ihre Dienste über den Konstruktor und vernetzen Sie Ihre Routen. Es funktioniert, und es funktioniert gut – aber manchmal fühlt es sich so an, als würden Sie eine Menge Zeremonien schreiben, die darauf hinauslaufen, „diese Anfrage anzunehmen, etwas zu tun und eine Antwort zu geben“.
Genau dieses Problem wollen Minimal APIs lösen. Mit den in .NET 6 eingeführten Minimal-APIs können Sie HTTP-Endpunkte mit sehr wenig Boilerplate definieren. Keine Controller, keine Attribute, kein Jonglieren mit Startklassen – nur direkte Route-zu-Handler-Zuordnungen in einem klaren, funktionalen Stil.
In diesem Beitrag möchte ich Sie durch alles führen, was Sie über Minimal-APIs wissen müssen: von Ihrem ersten Endpunkt bis hin zur Organisation großer Anwendungen, der Handhabung von Authentifizierung, Validierung, OpenAPI-Dokumenten und Leistungsüberlegungen. Kommen wir zur Sache.
Warum minimale APIs?
Bevor wir loslegen, sprechen wir darüber, warum Sie Minimal-APIs dem traditionellen Controller-basierten Ansatz vorziehen würden.
Controller sind großartig, wenn Sie ein strukturiertes, eigenwilliges Framework benötigen. Sie bieten Ihnen sofort Modellbindung, Filter, Inhaltsverhandlung und eine klare Trennung der Anliegen. Bei großen Unternehmensanwendungen mit Dutzenden von Entwicklern kann die von Controllern erzwungene Konsistenz ein echter Vorteil sein.
Minimale APIs hingegen glänzen, wenn Sie wollen:
- Weniger Boilerplate – keine Controller-Klassen, keine
[ApiController]-Attribute, keine separate Startkonfiguration. - Schnellerer Start – weniger reflexionsbasierte Vorgänge beim Booten.
- Einfacheres mentales Modell – eine Route wird direkt einem Handler zugeordnet. Das ist es.
- Microservice-freundlich – wenn Ihre API 5–10 Endpunkte hat, kann sich ein vollständiges Controller-Setup wie ein Overkill anfühlen.
Die gute Nachricht ist, dass dies keine Entweder-Oder-Entscheidung ist. Sie können Controller und Minimal-APIs im selben Projekt mischen. Wenn Sie sich jedoch erst einmal mit dem Minimalansatz vertraut gemacht haben, werden Sie vielleicht öfter zu ihm greifen, als Sie erwarten würden.
Erste Schritte
Lassen Sie uns eine Minimal-API von Grund auf erstellen. Wenn Sie das .NET SDK installiert haben, ist es so einfach:
dotnet new web -n MyMinimalApi
cd MyMinimalApi
Dadurch erhalten Sie ein Program.cs, das etwa so aussieht:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Das ist es. Das ist eine funktionierende API. Kein Startup.cs, keine Controller-Klasse, keine Routing-Konfiguration. Sie führen dotnet run aus, drücken http://localhost:5000 und erhalten „Hallo Welt!“ zurück. Die Einfachheit hier ist der springende Punkt.
Machen wir es mit einer klassischen Todo-API etwas nützlicher:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var todos = new List<Todo>();
app.MapGet("/todos", () => todos);
app.MapGet("/todos/{id:int}", (int id) =>
todos.FirstOrDefault(t => t.Id == id) is { } todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todos", (Todo todo) =>
{
todos.Add(todo);
return Results.Created($"/todos/{todo.Id}", todo);
});
app.MapDelete("/todos/{id:int}", (int id) =>
{
var todo = todos.FirstOrDefault(t => t.Id == id);
if (todo is null) return Results.NotFound();
todos.Remove(todo);
return Results.NoContent();
});
app.Run();
record Todo(int Id, string Title, bool IsComplete);
Wir haben vollständiges CRUD in weniger als 30 Zeilen. Das ist die Kraft des Minimalansatzes.
Routenhandler
Routenhandler sind die Funktionen, die ausgeführt werden, wenn eine Anfrage mit einer Route übereinstimmt. Sie haben mehrere Möglichkeiten, sie zu definieren.
Lambda-Ausdrücke
Der gebräuchlichste Ansatz und was Sie in den meisten Beispielen sehen werden:
app.MapGet("/hello", () => "Hello!");
app.MapGet("/hello/{name}", (string name) => $"Hello, {name}!");
Methodengruppen
Für komplexere Logik können Sie auf eine benannte Methode verweisen:
app.MapGet("/products", GetProducts);
app.MapPost("/products", CreateProduct);
static IResult GetProducts(AppDbContext db)
=> Results.Ok(db.Products.ToList());
static IResult CreateProduct(Product product, AppDbContext db)
{
db.Products.Add(product);
db.SaveChanges();
return Results.Created($"/products/{product.Id}", product);
}
```Dies ist mein bevorzugter Ansatz für alles, was über einen Einzeiler hinausgeht. Dadurch bleibt Ihr Routenzuordnungsabschnitt sauber und lesbar – Sie können auf einen Blick sehen, welche Endpunkte vorhanden sind, ohne sich durch Implementierungsdetails wühlen zu müssen.
### Lokale Funktionen
Sie können auch lokale Funktionen verwenden, was nützlich ist, wenn Sie Handler in der Nähe ihrer Routendefinitionen halten möchten:
```csharp
app.MapGet("/health", CheckHealth);
IResult CheckHealth()
{
// Some health check logic
return Results.Ok(new { Status = "Healthy", Timestamp = DateTime.UtcNow });
}
Parameterbindung
Eines der Dinge, die ich an Minimal APIs wirklich schätze, ist die intuitive Parameterbindung. Das Framework ermittelt basierend auf Kontext und Typ, wo Werte abgerufen werden sollen.
Routenparameter
app.MapGet("/users/{id:int}", (int id) => $"User {id}");
app.MapGet("/files/{*path}", (string path) => $"File: {path}"); // catch-all
Parameter der Abfragezeichenfolge
app.MapGet("/search", (string? query, int page = 1, int pageSize = 20) =>
Results.Ok(new { query, page, pageSize }));
Nullable-Typen werden zu optionalen Parametern. Standardwerte funktionieren genau so, wie Sie es erwarten.
Anforderungstext
app.MapPost("/orders", (Order order) =>
{
// 'order' is automatically deserialized from the JSON body
return Results.Created($"/orders/{order.Id}", order);
});
Header und Servicebindung
app.MapGet("/protected", (
[FromHeader(Name = "X-Api-Key")] string apiKey,
[FromServices] ILogger<Program> logger) =>
{
logger.LogInformation("Request with API key: {Key}", apiKey[..4] + "****");
return Results.Ok("Authorized");
});
HttpContext und HttpRequest
Wenn Sie Zugriff auf eine niedrigere Ebene benötigen:
app.MapGet("/info", (HttpContext context) =>
{
var userAgent = context.Request.Headers.UserAgent.ToString();
var ip = context.Connection.RemoteIpAddress?.ToString();
return Results.Ok(new { userAgent, ip });
});
Benutzerdefinierte Bindung mit BindAsync
Für komplexe Typen können Sie eine statische BindAsync-Methode implementieren:
public record PaginationParams(int Page, int PageSize)
{
public static ValueTask<PaginationParams?> BindAsync(HttpContext context)
{
int.TryParse(context.Request.Query["page"], out var page);
int.TryParse(context.Request.Query["pageSize"], out var pageSize);
var result = new PaginationParams(
Page: page > 0 ? page : 1,
PageSize: pageSize > 0 ? Math.Min(pageSize, 100) : 20
);
return ValueTask.FromResult<PaginationParams?>(result);
}
}
app.MapGet("/items", (PaginationParams pagination, AppDbContext db) =>
{
var items = db.Items
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToList();
return Results.Ok(items);
});
Das ist unglaublich kraftvoll. Sie definieren die Bindungslogik einmal und verwenden sie auf allen Ihren Endpunkten wieder.
Validierung
Ein Bereich, in dem Minimal-APIs im Vergleich zu Controllern nicht so sofort einsatzbereit sind, ist die Modellvalidierung. Es gibt keine automatische [Required]- oder [StringLength]-Durchsetzung. Aber es gibt klare Muster für den Umgang damit.
Manuelle Validierung
Der einfachste Ansatz – einfach im Handler validieren:
app.MapPost("/users", (CreateUserRequest request) =>
{
if (string.IsNullOrWhiteSpace(request.Email))
return Results.BadRequest(new { Error = "Email is required" });
if (request.Name?.Length > 100)
return Results.BadRequest(new { Error = "Name must be 100 characters or less" });
// Create user...
return Results.Created($"/users/{request.Email}", request);
});
Verwendung einer Validierungsbibliothek
Für alles, was nicht trivial ist, würde ich empfehlen, nach FluentValidation oder einer ähnlichen Bibliothek zu greifen:
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
public CreateUserValidator()
{
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Age).InclusiveBetween(0, 150);
}
}
Sie können dies über Endpunktfilter mit Ihren Endpunkten verbinden, was uns zum nächsten Abschnitt bringt.
Endpunktfilter
Endpunktfilter sind eine der besten Funktionen von Minimal APIs. Betrachten Sie sie als Middleware, die jedoch auf bestimmte Endpunkte und nicht auf die gesamte Pipeline beschränkt ist. Sie wurden in .NET 7 eingeführt und eignen sich hervorragend für übergreifende Themen.
Basisfilter
app.MapPost("/todos", (Todo todo) =>
{
// Handle the request
return Results.Created($"/todos/{todo.Id}", todo);
})
.AddEndpointFilter(async (context, next) =>
{
var todo = context.GetArgument<Todo>(0);
if (string.IsNullOrEmpty(todo.Title))
{
return Results.BadRequest(new { Error = "Title is required" });
}
return await next(context);
});
Validierungsfilter mit FluentValidation
Hier wird es wirklich leistungsstark – ein wiederverwendbarer Validierungsfilter:
public class ValidationFilter<T> : IEndpointFilter where T : class
{
private readonly IValidator<T> _validator;
public ValidationFilter(IValidator<T> validator)
{
_validator = validator;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var model = context.Arguments
.OfType<T>()
.FirstOrDefault();
if (model is null)
return Results.BadRequest(new { Error = "Request body is required" });
var result = await _validator.ValidateAsync(model);
if (!result.IsValid)
{
var errors = result.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
return Results.ValidationProblem(errors);
}
return await next(context);
}
}
// Usage
app.MapPost("/users", (CreateUserRequest request) =>
{
// Only reached if validation passes
return Results.Created($"/users/{request.Email}", request);
})
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>();
Protokollierungsfilter
app.MapGet("/products", (AppDbContext db) => db.Products.ToList())
.AddEndpointFilter(async (context, next) =>
{
var logger = context.HttpContext
.RequestServices.GetRequiredService<ILogger<Program>>();
var sw = Stopwatch.StartNew();
var result = await next(context);
sw.Stop();
logger.LogInformation("Endpoint executed in {Elapsed}ms", sw.ElapsedMilliseconds);
return result;
});
Sie können mehrere Filter verketten und sie werden in der Reihenfolge ausgeführt, in der sie hinzugefügt werden – genau wie Middleware.
OpenAPI / Swagger-Integration
Eine gute API-Dokumentation ist nicht mehr optional. Glücklicherweise bieten Minimal APIs über das Paket Microsoft.AspNetCore.OpenApi erstklassige Unterstützung für OpenAPI.
Grundeinrichtung
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.MapGet("/todos", () => new List<Todo>())
.WithName("GetTodos")
.WithDescription("Retrieves all todo items")
.WithTags("Todos");
app.Run();
Umfangreiche Endpunkt-Metadaten
Sie können detaillierte Informationen zu Ihren Endpunkten bereitstellen:
app.MapPost("/todos", (Todo todo) =>
{
return Results.Created($"/todos/{todo.Id}", todo);
})
.WithName("CreateTodo")
.WithDescription("Creates a new todo item")
.WithTags("Todos")
.Accepts<Todo>("application/json")
.Produces<Todo>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.ProducesValidationProblem();
Verwenden von WithOpenApi zur Anpassung
Für eine differenzierte Kontrolle über das generierte OpenAPI-Dokument:
app.MapGet("/todos/{id:int}", (int id) => Results.Ok(new Todo(id, "Sample", false)))
.WithOpenApi(operation =>
{
operation.Summary = "Get a specific todo";
operation.Description = "Retrieves a single todo item by its unique identifier.";
operation.Parameters[0].Description = "The unique identifier of the todo item";
return operation;
});
Dadurch erhalten Sie das gleiche Maß an Dokumentationskontrolle wie mit Swashbuckle-XML-Kommentaren zu Controllern, jedoch auf explizitere, Code-First-Art.
Authentifizierung und Autorisierung
Das Sichern von Minimal-API-Endpunkten folgt den gleichen Mustern wie der Rest von ASP.NET Core – Sie wenden sie nur anders an.
Grundeinrichtung```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = “https://your-identity-provider.com”; options.Audience = “your-api”; });
builder.Services.AddAuthorizationBuilder() .AddPolicy(“AdminOnly”, policy => policy.RequireRole(“Admin”)) .AddPolicy(“PremiumUser”, policy => policy.RequireClaim(“subscription”, “premium”));
var app = builder.Build();
app.UseAuthentication(); app.UseAuthorization();
### Anwenden der Autorisierung auf Endpunkte
```csharp
// Require any authenticated user
app.MapGet("/profile", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Name = user.FindFirstValue(ClaimTypes.Name),
Email = user.FindFirstValue(ClaimTypes.Email)
});
}).RequireAuthorization();
// Require a specific policy
app.MapDelete("/admin/users/{id}", (int id) =>
{
// Delete user logic
return Results.NoContent();
}).RequireAuthorization("AdminOnly");
// Allow anonymous access
app.MapGet("/public/health", () => Results.Ok(new { Status = "Healthy" }))
.AllowAnonymous();
Beachten Sie, wie Sie ClaimsPrincipal direkt in Ihre Handler-Parameter einfügen können – den Rest erledigt das Framework. Dies ist eines dieser kleinen Dinge, die Minimal APIs wirklich elegant erscheinen lassen.
Große APIs organisieren
Der Elefant im Raum mit Minimal APIs ist die Organisation. Wenn Ihr Program.cs 50 Endpunkte hat, wird es ein Chaos. Hier sind die Muster, die ich verwende, um die Dinge überschaubar zu halten.
Routengruppen
Mit Routengruppen (eingeführt in .NET 7) können Sie die Konfiguration über verwandte Endpunkte hinweg teilen:
var todos = app.MapGroup("/todos")
.WithTags("Todos")
.RequireAuthorization();
todos.MapGet("/", (AppDbContext db) => db.Todos.ToListAsync());
todos.MapGet("/{id:int}", (int id, AppDbContext db) => db.Todos.FindAsync(id));
todos.MapPost("/", (Todo todo, AppDbContext db) =>
{
db.Todos.Add(todo);
db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
});
Alle Endpunkte in der Gruppe haben das Präfix /todos, das Tag Todos und die Autorisierungsanforderung gemeinsam. Sauber.
Erweiterungsmethoden
Dies ist das Muster, das wirklich skaliert. Verschieben Sie jede Gruppe von Endpunkten in eine eigene statische Klasse:
// Endpoints/TodoEndpoints.cs
public static class TodoEndpoints
{
public static RouteGroupBuilder MapTodoEndpoints(this WebApplication app)
{
var group = app.MapGroup("/todos").WithTags("Todos");
group.MapGet("/", GetAll);
group.MapGet("/{id:int}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id:int}", Update);
group.MapDelete("/{id:int}", Delete);
return group;
}
private static async Task<IResult> GetAll(AppDbContext db)
=> Results.Ok(await db.Todos.ToListAsync());
private static async Task<IResult> GetById(int id, AppDbContext db)
=> await db.Todos.FindAsync(id) is { } todo
? Results.Ok(todo)
: Results.NotFound();
private static async Task<IResult> Create(Todo todo, AppDbContext db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
}
private static async Task<IResult> Update(int id, Todo updated, AppDbContext db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Title = updated.Title;
todo.IsComplete = updated.IsComplete;
await db.SaveChangesAsync();
return Results.Ok(todo);
}
private static async Task<IResult> Delete(int id, AppDbContext db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
}
// Program.cs — stays clean
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapTodoEndpoints();
app.MapUserEndpoints();
app.MapOrderEndpoints();
app.Run();
Ihr Program.cs wird zu einem Inhaltsverzeichnis für Ihre API. Jede Endpunktgruppe befindet sich in einer eigenen Datei. Dies ist der Ansatz, den ich für Produktionsanwendungen empfehle.
Die Carter-Bibliothek
Wenn Sie noch mehr Struktur wünschen, bietet die Carter-Bibliothek einen modulbasierten Ansatz:
public class TodoModule : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/todos", async (AppDbContext db) =>
await db.Todos.ToListAsync());
app.MapPost("/todos", async (Todo todo, AppDbContext db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
});
}
}
Carter erkennt und registriert automatisch alle Module. Es ist ein schöner Mittelweg zwischen dem reinen Minimal-API-Ansatz und vollständigen Controllern.
TypedResults und Antworttypen
Ab .NET 7 können Sie für typsichere Antworten TypedResults anstelle von Results verwenden. Dies mag wie eine kleine Änderung erscheinen, hat aber echte Vorteile für die OpenAPI-Dokumentation und Testbarkeit.
app.MapGet("/todos/{id:int}", async Task<Results<Ok<Todo>, NotFound>> (int id, AppDbContext db) =>
{
var todo = await db.Todos.FindAsync(id);
return todo is not null
? TypedResults.Ok(todo)
: TypedResults.NotFound();
});
Der Rückgabetyp Results<Ok<Todo>, NotFound> teilt dem Framework (und Ihren OpenAPI-Dokumenten) explizit mit, welche Antworttypen dieser Endpunkt erzeugen kann. Kein Raten mehr, keine manuellen Produces<>() Aufrufe für einfache Fälle.
Für mehrere mögliche Ergebnisse:
app.MapPost("/todos",
async Task<Results<Created<Todo>, ValidationProblem, Conflict>>
(Todo todo, AppDbContext db) =>
{
if (string.IsNullOrEmpty(todo.Title))
return TypedResults.ValidationProblem(
new Dictionary<string, string[]>
{
{ "Title", new[] { "Title is required" } }
});
if (await db.Todos.AnyAsync(t => t.Title == todo.Title))
return TypedResults.Conflict();
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todos/{todo.Id}", todo);
});
Ich habe begonnen, TypedResults in allen neuen Projekten zu verwenden. Der Compiler erkennt Abweichungen zwischen Ihren deklarierten Rückgabetypen und dem, was Sie tatsächlich zurückgeben, wodurch eine ganze Reihe von Laufzeitüberraschungen vermieden werden.
Leistungsüberlegungen
Eines der Verkaufsargumente von Minimal APIs ist die Leistung, und es lohnt sich zu verstehen, warum sie schneller sind.
Reduzierter Startaufwand. Controller verlassen sich stark auf Reflektion, um Endpunkte zu erkennen, Modelle zu binden und Filter anzuwenden. Minimale APIs verwenden Quellgeneratoren (ab .NET 7), um Bindungscode zur Kompilierungszeit zu generieren. Dies bedeutet weniger Arbeit beim Start und weniger Speicherzuweisung pro Anfrage.
Keine MVC-Pipeline. Controller-basierte APIs durchlaufen die gesamte MVC-Pipeline: Aktionsauswahl, Modellbindung, Aktionsfilter, Ergebnisausführung. Minimale APIs überspringen all das und gehen direkt vom Routing zu Ihrem Handler.
RequestDelegate-Kompilierung. Das Framework kompiliert Ihre Lambda-Ausdrücke in optimierte RequestDelegate-Instanzen. Der resultierende Code kommt dem sehr nahe, was Sie von Hand schreiben würden, wenn Sie direkt mit HttpContext arbeiten würden.
Hier einige praktische Tipps zur Maximierung der Leistung:
// Use AsNoTracking for read-only queries
app.MapGet("/products", async (AppDbContext db) =>
await db.Products.AsNoTracking().ToListAsync());
// Return results directly — avoid unnecessary allocations
app.MapGet("/count", async (AppDbContext db) =>
await db.Products.CountAsync());
// Use cancellation tokens for long-running operations
app.MapGet("/report", async (AppDbContext db, CancellationToken ct) =>
await db.Orders
.AsNoTracking()
.GroupBy(o => o.Status)
.Select(g => new { Status = g.Key, Count = g.Count() })
.ToListAsync(ct));
```Erwähnenswert ist auch, dass der Leistungsunterschied zwischen Controllern und Minimal-APIs mit jeder .NET-Version immer kleiner wird. Bei den meisten Anwendungen wird der Unterschied nicht Ihr Engpass sein, sondern Ihre Datenbankabfragen und externen Serviceanrufe. Wählen Sie basierend auf Entwicklererfahrung und Projektanforderungen, nicht auf Benchmarks.
## Fazit
Minimale APIs haben seit ihrer Einführung in .NET 6 einen langen Weg zurückgelegt. Was als „Hallo Welt“-Demofunktion begann, hat sich zu einer legitimen Wahl für Produktions-APIs entwickelt. Mit Endpunktfiltern, Routengruppen, typisierten Ergebnissen und solider OpenAPI-Unterstützung verfügen Sie über alles, was Sie zum Aufbau gut strukturierter, wartbarer Dienste benötigen.
Meine Empfehlung? Wenn Sie ein neues API-Projekt starten – insbesondere einen Microservice oder eine fokussierte interne API – probieren Sie Minimal APIs ernsthaft aus. Nutzen Sie das Erweiterungsmethodenmuster für die Organisation, stützen Sie sich bei übergreifenden Anliegen auf Endpunktfilter und nutzen Sie `TypedResults` für die Typensicherheit.
Bei bestehenden Controller-basierten Projekten gibt es keine Eile bei der Migration. Beide Ansätze funktionieren gut und Sie können sie sogar nebeneinander verwenden. Aber wenn Sie das nächste Mal einen kleinen Dienst oder eine schnelle interne API hinzufügen müssen, überspringen Sie die Controller und gehen Sie auf ein Minimum zurück. Möglicherweise kehren Sie nicht zurück.
Viel Spaß beim Codieren!