ASP.NET Core Minimal APIs w 2026: Architektura, Wydajność i Pytania Rekrutacyjne

Kompleksowy przewodnik po Minimal APIs w ASP.NET Core - architektura, grupy tras, filtry endpointów, Native AOT oraz kluczowe pytania na rozmowach kwalifikacyjnych dla programistów .NET.

ASP.NET Core Minimal APIs w 2026: Architektura, Wydajność i Pytania Rekrutacyjne

ASP.NET Core Minimal APIs eliminują rozbudowaną ceremonię tradycyjnych kontrolerów MVC, oferując uproszczone podejście do budowania endpointów HTTP ze znacznie mniejszą ilością kodu standardowego. Wprowadzone w .NET 6 i udoskonalane przez .NET 8, 9 oraz obecny .NET 10, Minimal APIs dojrzały do poziomu rozwiązania produkcyjnego dla mikroserwisów, funkcji serverless i lekkich usług webowych.

Minimal APIs vs Kontrolery

Minimal APIs wykorzystują handlery tras definiowane bezpośrednio w Program.cs, podczas gdy kontrolery MVC wymagają definicji klas, atrybutów i konwencjonalnego routingu. Dla prostych operacji CRUD lub mikroserwisów z mniej niż 20 endpointami, Minimal APIs zazwyczaj redukują kod o 40-60%.

Architektura Minimal API i Pipeline Żądań

Pipeline żądań ASP.NET Core przetwarza żądania HTTP przez komponenty middleware przed dotarciem do handlerów endpointów. Minimal APIs integrują się bezproblemowo z tym potokiem, oferując jednocześnie bardziej deklaratywną składnię definicji tras.

Program.cscsharp
var builder = WebApplication.CreateBuilder(args);

// Rejestracja serwisów dla dependency injection
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

var app = builder.Build();

// Konfiguracja pipeline'u middleware
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthorization();

// Definicje endpointów Minimal API
app.MapGet("/products", async (IProductRepository repo) =>
    Results.Ok(await repo.GetAllAsync()));

app.MapGet("/products/{id:int}", async (int id, IProductRepository repo) =>
    await repo.GetByIdAsync(id) is Product product
        ? Results.Ok(product)
        : Results.NotFound());

app.Run();

Ten wzorzec centralizuje definicje tras przy zachowaniu pełnego dostępu do kontenera dependency injection. Parametr IProductRepository demonstruje wstrzykiwanie bez konstruktora bezpośrednio do delegatów handlerów.

Grupy Tras i Organizacja Endpointów

W miarę rozwoju aplikacji organizacja endpointów staje się kluczowa. Grupy tras wprowadzone w .NET 7 zapewniają przestrzenie nazw i współdzieloną konfigurację bez rezygnacji z minimalnego podejścia.

ProductEndpoints.cscsharp
public static class ProductEndpoints
{
    public static void MapProductEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/products")
            .WithTags("Products")
            .RequireAuthorization();

        group.MapGet("/", GetAllProducts);
        group.MapGet("/{id:int}", GetProductById);
        group.MapPost("/", CreateProduct)
            .Accepts<CreateProductRequest>("application/json")
            .Produces<Product>(StatusCodes.Status201Created);
        group.MapPut("/{id:int}", UpdateProduct);
        group.MapDelete("/{id:int}", DeleteProduct)
            .RequireAuthorization("AdminOnly");
    }

    private static async Task<IResult> GetAllProducts(
        IProductRepository repo,
        CancellationToken ct)
    {
        var products = await repo.GetAllAsync(ct);
        return Results.Ok(products);
    }

    private static async Task<IResult> GetProductById(
        int id,
        IProductRepository repo,
        CancellationToken ct)
    {
        var product = await repo.GetByIdAsync(id, ct);
        return product is not null
            ? Results.Ok(product)
            : Results.NotFound();
    }

    private static async Task<IResult> CreateProduct(
        CreateProductRequest request,
        IProductRepository repo,
        IValidator<CreateProductRequest> validator,
        CancellationToken ct)
    {
        var validation = await validator.ValidateAsync(request, ct);
        if (!validation.IsValid)
            return Results.ValidationProblem(validation.ToDictionary());

        var product = await repo.CreateAsync(request.ToProduct(), ct);
        return Results.Created($"/api/products/{product.Id}", product);
    }
}

Wywołanie app.MapProductEndpoints() w Program.cs rejestruje wszystkie trasy produktów ze współdzielonymi wymaganiami autoryzacji i metadanymi OpenAPI. Ta struktura dobrze skaluje się dla aplikacji z setkami endpointów.

Wiązanie Parametrów i Walidacja

Minimal APIs obsługują wiele źródeł wiązania: parametry tras, query strings, nagłówki, ciała żądań i serwisy z DI. Zrozumienie priorytetów wiązania pozwala uniknąć typowych pułapek rekrutacyjnych.

Program.cs - Przykłady wiązania parametrówcsharp
app.MapGet("/search", (
    [FromQuery] string? query,           // Jawny query string
    [FromQuery] int page = 1,            // Wartość domyślna
    [FromQuery] int pageSize = 20,       // Wartość domyślna
    [FromHeader(Name = "X-Correlation-Id")] string? correlationId,
    ILogger<Program> logger) =>
{
    logger.LogInformation("Żądanie wyszukiwania: {Query}, Strona: {Page}, CorrelationId: {CorrelationId}",
        query, page, correlationId);

    return Results.Ok(new { query, page, pageSize, correlationId });
});

// Złożone wiązanie modelu z walidacją
app.MapPost("/orders", async (
    [FromBody] CreateOrderRequest request,
    [FromServices] IValidator<CreateOrderRequest> validator,
    [FromServices] IOrderService orderService,
    HttpContext context,
    CancellationToken ct) =>
{
    var validationResult = await validator.ValidateAsync(request, ct);

    if (!validationResult.IsValid)
    {
        return Results.ValidationProblem(
            validationResult.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(
                    g => g.Key,
                    g => g.Select(e => e.ErrorMessage).ToArray()));
    }

    var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
    var order = await orderService.CreateOrderAsync(request, userId!, ct);

    return Results.Created($"/orders/{order.Id}", order);
});

Atrybut [FromBody] jest opcjonalny dla typów złożonych, ale poprawia czytelność. FluentValidation integruje się naturalnie poprzez dependency injection, utrzymując logikę walidacji oddzielnie od handlerów endpointów.

Priorytet Źródeł Wiązania

Gdy nie określono atrybutu, Minimal APIs wnioskują źródła wiązania: najpierw parametry tras, następnie query strings dla typów prostych i ciało żądania dla typów złożonych. Jawne atrybuty jak [FromQuery] lub [FromBody] nadpisują to zachowanie.

Optymalizacja Wydajności z Native AOT

.NET 8 wprowadził wsparcie kompilacji Native AOT (Ahead-of-Time) dla Minimal APIs, produkując samodzielne pliki wykonywalne z czasem uruchomienia poniżej milisekundy. Ta możliwość czyni Minimal APIs idealnym wyborem dla wdrożeń serverless, gdzie opóźnienie cold start ma znaczenie.

Program.cs - Konfiguracja kompatybilna z AOTcsharp
var builder = WebApplication.CreateSlimBuilder(args);

// Serializacja JSON przyjazna dla AOT
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

var app = builder.Build();

app.MapGet("/health", () => Results.Ok(new HealthResponse("Healthy", DateTime.UtcNow)));

app.Run();

// Kontekst serializatora JSON generowany źródłowo
[JsonSerializable(typeof(HealthResponse))]
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
internal partial class AppJsonContext : JsonSerializerContext { }

public record HealthResponse(string Status, DateTime CheckedAt);

Metoda CreateSlimBuilder wyklucza zbędne funkcje frameworka, podczas gdy źródłowo generowany JsonSerializerContext eliminuje refleksję w czasie wykonania dla serializacji JSON. Opublikowane binaria AOT dla prostych API zazwyczaj mierzą 10-15 MB w porównaniu do 80+ MB dla standardowych samodzielnych wdrożeń.

Typowane Wyniki i Metadane Odpowiedzi

.NET 7 wprowadził TypedResults dla weryfikacji typów odpowiedzi w czasie kompilacji, poprawiając dokładność dokumentacji OpenAPI i wychwytując niezgodności typów podczas rozwoju.

csharp
// Silnie typowane wyniki z metadanymi OpenAPI
app.MapGet("/products/{id:int}", async Task<Results<Ok<Product>, NotFound, ProblemHttpResult>> (
    int id,
    IProductRepository repo,
    CancellationToken ct) =>
{
    try
    {
        var product = await repo.GetByIdAsync(id, ct);

        return product is not null
            ? TypedResults.Ok(product)
            : TypedResults.NotFound();
    }
    catch (Exception ex)
    {
        return TypedResults.Problem(
            detail: "Wystąpił błąd podczas pobierania produktu",
            statusCode: StatusCodes.Status500InternalServerError);
    }
})
.WithName("GetProductById")
.WithOpenApi(operation =>
{
    operation.Summary = "Pobiera produkt po ID";
    operation.Description = "Zwraca szczegóły produktu lub 404 jeśli nie znaleziono";
    return operation;
});

Typ unii Results<T1, T2, T3> deklaruje wszystkie możliwe typy odpowiedzi, które generatory Swagger/OpenAPI wykorzystują do tworzenia dokładnej dokumentacji. Ten wzorzec jest szczególnie cenny przy przygotowywaniu się do pytań rekrutacyjnych dotyczących projektowania API.

Gotowy na rozmowy o .NET?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Filtry Endpointów dla Zagadnień Przekrojowych

Filtry endpointów zapewniają funkcjonalność podobną do middleware, ograniczoną do konkretnych endpointów lub grup, obsługując zagadnienia takie jak logowanie, buforowanie i transformacja żądań.

ValidationFilter.cscsharp
public class ValidationFilter<T> : IEndpointFilter where T : class
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var validator = context.HttpContext
            .RequestServices
            .GetService<IValidator<T>>();

        if (validator is null)
            return await next(context);

        var argument = context.Arguments
            .OfType<T>()
            .FirstOrDefault();

        if (argument is null)
            return await next(context);

        var validationResult = await validator.ValidateAsync(argument);

        if (!validationResult.IsValid)
        {
            return Results.ValidationProblem(
                validationResult.Errors
                    .GroupBy(e => e.PropertyName)
                    .ToDictionary(
                        g => g.Key,
                        g => g.Select(e => e.ErrorMessage).ToArray()));
        }

        return await next(context);
    }
}

// Użycie w Program.cs
app.MapPost("/products", CreateProduct)
    .AddEndpointFilter<ValidationFilter<CreateProductRequest>>();

// Globalna rejestracja filtru przez grupę tras
var api = app.MapGroup("/api")
    .AddEndpointFilter(async (context, next) =>
    {
        var logger = context.HttpContext
            .RequestServices
            .GetRequiredService<ILogger<Program>>();

        var stopwatch = Stopwatch.StartNew();
        var result = await next(context);
        stopwatch.Stop();

        logger.LogInformation(
            "Endpoint {Method} {Path} ukończony w {ElapsedMs}ms",
            context.HttpContext.Request.Method,
            context.HttpContext.Request.Path,
            stopwatch.ElapsedMilliseconds);

        return result;
    });

Filtry wykonują się w kolejności rejestracji, gdzie najbardziej wewnętrzny filtr jest najbliżej handlera endpointu. Ta architektura umożliwia czyste oddzielenie logiki walidacji, logowania i autoryzacji.

Wzorce Uwierzytelniania i Autoryzacji

Minimal APIs obsługują te same mechanizmy uwierzytelniania i autoryzacji co kontrolery MVC, z bardziej deklaratywną składnią konfiguracji.

Program.cs - Konfiguracja uwierzytelniania JWTcsharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"))
    .AddPolicy("PremiumUser", policy =>
        policy.RequireClaim("subscription", "premium", "enterprise"));

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// Chronione endpointy
app.MapGet("/admin/users", async (IUserService userService) =>
    Results.Ok(await userService.GetAllUsersAsync()))
    .RequireAuthorization("AdminOnly");

app.MapGet("/profile", async (ClaimsPrincipal user, IUserService userService) =>
{
    var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
    var profile = await userService.GetProfileAsync(userId!);
    return Results.Ok(profile);
})
.RequireAuthorization();

// Anonimowy endpoint w chronionej grupie
var protectedGroup = app.MapGroup("/api/secure")
    .RequireAuthorization();

protectedGroup.MapGet("/public-info", () => Results.Ok("To jest publiczne"))
    .AllowAnonymous();

Metoda rozszerzenia RequireAuthorization przyjmuje nazwy polityk lub może być wywoływana bez argumentów, aby wymagać dowolnego uwierzytelnionego użytkownika. Zrozumienie tych wzorców jest niezbędne dla pytań rekrutacyjnych dotyczących uwierzytelniania i autoryzacji.

Pytania Rekrutacyjne: Typowe Wzorce

Rozmowy techniczne często eksplorują różnice między Minimal APIs a tradycyjnymi kontrolerami. Poniższa tabela podsumowuje kluczowe różnice:

| Aspekt | Minimal APIs | Kontrolery MVC | |--------|--------------|----------------| | Kod standardowy | Niski - bezpośrednie delegaty handlerów | Wyższy - klasa + metoda + atrybuty | | Routing | Inline z MapGet, MapPost | Oparty na atrybutach lub konwencjach | | Wiązanie modelu | Automatyczne z opcjonalnymi atrybutami | Konwencja + atrybuty | | Filtry | Filtry endpointów | Filtry akcji + middleware | | Wsparcie AOT | Pełne od .NET 8 | Ograniczone, oparte na refleksji | | Testowalność | Oparte na funkcjach, łatwe testy jednostkowe | Wymaga instancjonowania kontrolera | | Najlepsze dla | Mikroserwisy, proste API | Duże aplikacje, złożone przepływy |

Wskazówka Rekrutacyjna

Gdy zapytają "Kiedy wybrałbyś kontrolery zamiast Minimal APIs?", wspomnij o złożonych aplikacjach wymagających filtrów akcji, dostosowania wiązania modeli lub istniejących bazach kodu z ustalonymi wzorcami MVC. Minimal APIs doskonale sprawdzają się w nowych mikroserwisach i funkcjach serverless.

Testowanie Endpointów Minimal API

Testowanie integracyjne z WebApplicationFactory zapewnia realistyczną weryfikację endpointów bez wdrażania aplikacji.

ProductEndpointsTests.cscsharp
public class ProductEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly WebApplicationFactory<Program> _factory;

    public ProductEndpointsTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Zamiana prawdziwego repozytorium na mock
                services.RemoveAll<IProductRepository>();
                services.AddScoped<IProductRepository, MockProductRepository>();
            });
        });
        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task GetProducts_ReturnsOkWithProductList()
    {
        // Act
        var response = await _client.GetAsync("/api/products");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var products = await response.Content
            .ReadFromJsonAsync<List<Product>>();

        products.Should().NotBeNull();
        products.Should().HaveCountGreaterThan(0);
    }

    [Fact]
    public async Task GetProductById_WithInvalidId_ReturnsNotFound()
    {
        // Act
        var response = await _client.GetAsync("/api/products/99999");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    [Fact]
    public async Task CreateProduct_WithValidRequest_ReturnsCreated()
    {
        // Arrange
        var request = new CreateProductRequest("Testowy Produkt", 29.99m, "Opis testowy");

        // Act
        var response = await _client.PostAsJsonAsync("/api/products", request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        response.Headers.Location.Should().NotBeNull();
    }
}

W ramach głębszej eksploracji wzorców czystej architektury w aplikacjach .NET, warstwa serwisowa powinna być testowana niezależnie testami jednostkowymi, podczas gdy testy integracyjne weryfikują pełny pipeline żądań.

Podsumowanie

Minimal APIs reprezentują nowoczesne podejście do budowania usług HTTP w ASP.NET Core:

  • Grupy tras i organizacja endpointów skalują się od prostych mikroserwisów do złożonych aplikacji
  • Filtry endpointów zapewniają czyste oddzielenie zagadnień przekrojowych jak walidacja i logowanie
  • TypedResults umożliwiają weryfikację typów odpowiedzi w czasie kompilacji i dokładną dokumentację OpenAPI
  • Kompilacja Native AOT dostarcza czasy uruchomienia poniżej milisekundy dla wdrożeń serverless
  • Testowanie integracyjne z WebApplicationFactory zapewnia realistyczną weryfikację endpointów

Przy przygotowaniu do rozmów rekrutacyjnych warto skupić się na umiejętności wyjaśnienia, kiedy Minimal APIs są odpowiednie w porównaniu z tradycyjnymi kontrolerami, oraz wykazać zrozumienie pipeline'u żądań, dependency injection i wzorców uwierzytelniania.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Udostępnij

Powiązane artykuły