Megosztás a következőn keresztül:


Válaszok létrehozása minimális API-alkalmazásokban

Jegyzet

Ez nem a cikk legújabb verziója. Az aktuális kiadásról a cikk .NET 10-es verziójában olvashat.

Figyelmeztetés

A ASP.NET Core ezen verziója már nem támogatott. További információ: .NET és .NET Core támogatási szabályzat. Az aktuális kiadásért tekintse meg ennek a cikknek .NET 9-es verzióját.

Ez a cikk azt ismerteti, hogyan hozhat létre válaszokat minimális API-végpontokhoz a ASP.NET Core-ban. A minimális API-k számos módot kínálnak az adatok és a HTTP-állapotkódok visszaadására.

A minimális végpontok a következő típusú visszatérési értékeket támogatják:

  1. string – Ez magában foglalja Task<string> és ValueTask<string>.
  2. T (Bármely más típus) – Ebbe beletartozik Task<T> és ValueTask<T>.
  3. IResult alapú – Ez magában foglalja Task<IResult> és ValueTask<IResult>.

Fontos

A ASP.NET Core 10-től kezdve az ismert API-végpontok már nem lesznek átirányítva a bejelentkezési lapokra hitelesítés használatakor cookie . Ehelyett 401/403 állapotkódokat adnak vissza. További részletekért lásd az API-végpontok hitelesítési viselkedését a ASP.NET Core-ban.

string visszaadott értékek

Magatartás Tartalomtípus
A keretrendszer közvetlenül a válaszba írja a sztringet. text/plain

Vegye figyelembe az alábbi útvonalkezelőt, amely egy Hello world szöveget ad vissza.

app.MapGet("/hello", () => "Hello World");

A 200 állapotkód text/plain Content-Type fejléccel és az alábbi tartalommal lesz visszaadva.

Hello World

T (bármely más típus) visszaadja az értékeket

Magatartás Tartalomtípus
A keretrendszer JSON-szerializálja a választ. application/json

Vegye figyelembe a következő útvonalkezelőt, amely egy névtelen típust ad vissza, amely egy Message karakterlánc-tulajdonságot tartalmaz.

app.MapGet("/hello", () => new { Message = "Hello World" });

A 200 állapotkód application/json Content-Type fejléccel és az alábbi tartalommal lesz visszaadva.

{"message":"Hello World"}

IResult visszaadott értékek

Magatartás Tartalomtípus
A keretrendszer meghívja IResult.ExecuteAsync. A IResult megvalósítása határozza meg.

A IResult felület egy HTTP-végpont eredményét képviselő szerződést határoz meg. A statikus Eredmények osztály és a statikus TypedResults különböző IResult típusú válaszok létrehozására szolgálnak.

TypedResults és Results

A Results és TypedResults statikus osztályok hasonló eredménysegítőket biztosítanak. A TypedResults osztály a típusú megfelelője a Results osztálynak. A Results segítők visszatérési típusa azonban IResult, míg az egyes TypedResults segítők visszatérési típusa az IResult implementálási típusok egyike. A különbség azt jelenti, hogy Results segítők esetében átalakításra van szükség, ha a konkrét típusra van szükség, például egységteszteléshez. A megvalósítási típusok a Microsoft.AspNetCore.Http.HttpResults névtérben vannak definiálva.

A TypedResults helyett a Results visszaadása a következő előnyökkel jár:

  • TypedResults segítők erősen gépelt objektumokat adnak vissza, amelyek javítják a kód olvashatóságát, az egységtesztelést és csökkentik a futásidejű hibák esélyét.
  • A implementáció típusa automatikusan biztosítja az OpenAPI választípus-metaadatait a végpont leírásához.

Fontolja meg a következő végpontot, amelyhez egy 200 OK állapotkód jön létre a várt JSON-válaszsal.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

A végpont helyes dokumentálásához a bővítménymetódus Produces van meghívva. Azonban nem szükséges meghívni Produces, ha TypedResults van használva Resultshelyett, ahogy az alábbi kódban látható. TypedResults automatikusan biztosítja a végpont metaadatait.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

A választípusok leírásáról további információért lásd az OpenAPI-támogatás a minimális API-kbanrészt.

A tesztelési eredménytípusokra vonatkozó példákért tekintse meg a tesztelési dokumentációt.

Mivel az Results összes metódusa IResult típust ad vissza az aláírásban, a fordító automatikusan ezt a típust állapítja meg a kérés delegált visszatérési típusaként, amikor egy végpont különböző eredményeket ad vissza. TypedResults megköveteli a Results<T1, TN> használatát az ilyen típusú meghatalmazottaktól.

A következő módszer azért lefordul, mert Results.Ok és Results.NotFound is úgy vannak deklarálva, hogy IResult-t adnak vissza, annak ellenére, hogy a visszaadott objektumok tényleges konkrét típusai eltérőek:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

A következő metódus nem fordítható le, mert a TypedResults.Ok és a TypedResults.NotFound különböző típusok visszaadásaként van deklarálva, és a fordító nem próbálja meg a legjobb egyező típust kikövetkeztetni:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

A TypedResults használatához a visszatérési típust teljes mértékben deklarálni kell; ha a metódus aszinkron, a deklaráció során a visszatérési típust be kell csomagolni egy Task<>-be. A TypedResults használata bőbeszédűbb, de ez az a kompromisszum, amelyért cserébe a típusadatok statikusan elérhetők, így az OpenAPI-ban önmagukat leírhatják.

app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
   await db.Todos.FindAsync(id)
    is Todo todo
       ? TypedResults.Ok(todo)
       : TypedResults.NotFound());

Eredmények<TResult1, TResultN>

Használja a Results<TResult1, TResultN> a végpontkezelő visszatérési típusát IResult helyett, amikor:

  • A rendszer több IResult implementációtípust ad vissza a végpontkezelőtől.
  • A statikus TypedResult osztály a IResult objektumok létrehozásához használható.

Ez az alternatíva jobb, mint a IResult visszaadása, mivel az általános egyesítési típusok automatikusan megőrzik a végpont metaadatait. Mivel az Results<TResult1, TResultN> egyesítő típusok implicit öntött operátorokat implementálnak, a fordító automatikusan konvertálhatja az általános argumentumokban megadott típusokat az egyesítő típus egy példányává.

Ez azzal a többletelőnnyel jár, hogy fordítási időt biztosít annak ellenőrzéséhez, hogy az útvonalkezelő valójában csak az általa deklarált eredményeket adja vissza. Ha olyan típust próbál visszaadni, amely nincs Results<> általános argumentumai között deklarálva, az fordítási hibát eredményez.

Vegye figyelembe a következő végpontot, amelynek 400 BadRequest állapotkódját adja vissza, ha a orderId nagyobb, mint 999. Ellenkező esetben létrehoz egy 200 OK-t a várt tartalommal.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

A végpont helyes dokumentálásához a bővítménymetódus Produces van meghívva. Mivel azonban a TypedResults segéd automatikusan tartalmazza a végpont metaadatait, a Results<T1, Tn> egyesítési típust is visszaadhatja, ahogyan az az alábbi kódban is látható.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Beépített eredmények

A Results és TypedResults statikus osztályokban gyakori eredmény-segítők léteznek. TypedResults visszaadása előnyösebb, mint Resultsvisszaadása. További információ: TypedResults vs Results.

Az alábbi szakaszok a gyakori eredménysegítők használatát mutatják be.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync a JSON visszaadásának alternatív módja:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Egyéni állapotkód

app.MapGet("/405", () => Results.StatusCode(405));

Belső kiszolgálóhiba

app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));

Az előző példa egy 500-ás állapotkódot ad vissza.

Probléma és validálási probléma

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

Érvényesítési hibaválaszok testreszabása az IProblemDetailsService használatával

A minimális API-érvényesítési logika hibaválaszainak testreszabása implementációval IProblemDetailsService . Regisztrálja ezt a szolgáltatást az alkalmazás szolgáltatásgyűjteményében a konzisztens és felhasználóspecifikus hibaválaszok engedélyezéséhez. A minimális API-ellenőrzés támogatása a .NET 10 ASP.NET Core-ban jelent meg.

Egyéni érvényesítési hibaválaszok implementálása:

  • Az alapértelmezett implementáció implementálása IProblemDetailsService vagy használata
  • A szolgáltatás regisztrálása a DI-tárolóban
  • Az érvényesítési rendszer automatikusan a regisztrált szolgáltatást használja az érvényesítési hibaválaszok formázására

Az alábbi példa bemutatja, hogyan regisztrálhatja és konfigurálhatja az IProblemDetailsService érvényesítési hibaválaszok testreszabását:

using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        if (context.ProblemDetails.Status == 400)
        {
            context.ProblemDetails.Title = "Validation error occurred";
            context.ProblemDetails.Extensions["support"] = "Contact support@example.com";
            context.ProblemDetails.Extensions["traceId"] = Guid.NewGuid().ToString();
        }
    };
});

Érvényesítési hiba esetén a rendszer a IProblemDetailsService hibaválasz létrehozására szolgál, beleértve a CustomizeProblemDetails visszahívásban hozzáadott testreszabásokat is.

Egy teljes alkalmazásért tekintse meg a Minimális API-mintaalkalmazást , amely bemutatja, hogyan szabhatja testre az érvényesítési hibaválaszokat a IProblemDetailsService ASP.NET Core Minimal API-k használatával.

Szöveg

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();

Results.Stream túlterhelések pufferelés nélkül teszik lehetővé a mögöttes HTTP-válaszfolyam elérését. Az alábbi példa ImageSharp használatával adja vissza a megadott kép kisebb méretét:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

Az alábbi példa folyamatosan továbbít egy képet az Azure Blob tárhelyről.

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

Az alábbi példa egy Azure Blobból streamel egy videót:

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Server-Sent események (SSE)

A TypedResults.ServerSentEvents API támogatja a ServerSentEvents eredmény visszaadását.

Server-Sent Események egy kiszolgáló leküldéses technológiája, amellyel a kiszolgáló egyetlen HTTP-kapcsolaton keresztül küldhet eseményüzeneteket egy ügyfélnek. A .NET-ben az eseményüzenetek objektumokként SseItem<T> jelennek meg, amelyek tartalmazhatnak eseménytípust, azonosítót és hasznos adatokat T.

A TypedResults osztály egy ServerSentEvents nevű statikus metódussal rendelkezik, amely az eredmény visszaadására ServerSentEvents használható. A metódus első paramétere az IAsyncEnumerable<SseItem<T>> ügyfélnek küldendő eseményüzenetek streamje.

Az alábbi példa bemutatja, hogyan használhatja az TypedResults.ServerSentEvents API-t a pulzusesemények JSON-objektumként való visszaadására az ügyfél számára:

app.MapGet("sse-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<SseItem<int>> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return new SseItem<int>(heartRate, eventType: "heartRate")
            {
                ReconnectionInterval = TimeSpan.FromMinutes(1)
            };
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken));
});

További információkért tekintse meg a Minimális API mintaalkalmazást az TypedResults.ServerSentEvents API használatával, amely szövegként ad vissza pulzuseseményeket, ServerSentEvents, és JSON-objektumokat az ügyfélnek.

Átirányít

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Fájl

app.MapGet("/download", () => Results.File("myfile.text"));

HttpResult-felületek

A Microsoft.AspNetCore.Http névtér alábbi felületei lehetővé teszik a IResult típus észlelését futásidőben, ami a szűrő implementációk gyakori mintája:

Íme egy példa egy szűrőre, amely az alábbi felületek egyikét használja:

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

További információért lásd: Minimális API alkalmazások szűrői és IResult implementálási típusok.

Fejlécek módosítása

A válaszfejlécek módosításához használja a HttpResponse objektumot:

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

Válaszok testreszabása

Az alkalmazások egyéni IResult típus implementálásával szabályozhatják a válaszokat. Az alábbi kód egy PÉLDA EGY HTML-eredménytípusra:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Javasoljuk, hogy adjon hozzá egy bővítménymetódust a Microsoft.AspNetCore.Http.IResultExtensions az egyéni eredmények felderíthetőbbé tétele érdekében.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Az egyéni IResult-típus a IEndpointMetadataProvider felület implementálásával saját annotációt is biztosíthat. A következő kód például egy megjegyzést ad hozzá az előző HtmlResult típushoz, amely leírja a végpont által létrehozott választ.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

A ProducesHtmlMetadata a IProducesResponseTypeMetadata implementációja, amely meghatározza a létrehozott választartalom típusát text/html és az állapotkódot 200 OK.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Egy másik módszer a Microsoft.AspNetCore.Mvc.ProducesAttribute használata a létrehozott válasz leírására. Az alábbi kód a PopulateMetadatahasználatára módosítja a ProducesAttribute metódust.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

JSON-szerializálási beállítások konfigurálása

Alapértelmezés szerint a minimális API-alkalmazások Web defaults lehetőségeket használnak a JSON szerializálása és deszerializálása során.

JSON-szerializálási beállítások konfigurálása globálisan

A beállítások globálisan konfigurálhatók egy alkalmazáshoz a ConfigureHttpJsonOptionsmeghívásával. Az alábbi példa a nyilvános mezőket és a JSON-kimenet formátumát tartalmazza.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Mivel a mezők szerepelnek, az előző kód beolvassa a NameField értéket, és belefoglalja a kimeneti JSON-fájlba.

JSON-szerializálási beállítások konfigurálása végponthoz

A végpont szerializálási beállításainak konfigurálásához hívja meg Results.Json, és adjon át neki egy JsonSerializerOptions objektumot, ahogyan az az alábbi példában látható:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

Alternatívaként használja a WriteAsJsonAsync túlterhelését, amely elfogadja a JsonSerializerOptions objektumot. Az alábbi példa ezt a túlterhelést használja a kimeneti JSON formázásához:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

További erőforrások

A minimális végpontok a következő típusú visszatérési értékeket támogatják:

  1. string – Ez magában foglalja Task<string> és ValueTask<string>.
  2. T (Bármely más típus) – Ebbe beletartozik Task<T> és ValueTask<T>.
  3. IResult alapú – Ez magában foglalja Task<IResult> és ValueTask<IResult>.

string visszaadott értékek

Magatartás Tartalomtípus
A keretrendszer közvetlenül a válaszba írja a sztringet. text/plain

Vegye figyelembe az alábbi útvonalkezelőt, amely egy Hello world szöveget ad vissza.

app.MapGet("/hello", () => "Hello World");

A 200 állapotkód text/plain Content-Type fejléccel és az alábbi tartalommal lesz visszaadva.

Hello World

T (bármely más típus) visszaadja az értékeket

Magatartás Tartalomtípus
A keretrendszer JSON-szerializálja a választ. application/json

Vegye figyelembe a következő útvonalkezelőt, amely egy névtelen típust ad vissza, amely egy Message karakterlánc-tulajdonságot tartalmaz.

app.MapGet("/hello", () => new { Message = "Hello World" });

A 200 állapotkód application/json Content-Type fejléccel és az alábbi tartalommal lesz visszaadva.

{"message":"Hello World"}

IResult visszaadott értékek

Magatartás Tartalomtípus
A keretrendszer meghívja IResult.ExecuteAsync. A IResult megvalósítása határozza meg.

A IResult felület egy HTTP-végpont eredményét képviselő szerződést határoz meg. A statikus Eredmények osztály és a statikus TypedResults különböző IResult típusú válaszok létrehozására szolgálnak.

TypedResults és Results

A Results és TypedResults statikus osztályok hasonló eredménysegítőket biztosítanak. A TypedResults osztály a típusú megfelelője a Results osztálynak. A Results segítők visszatérési típusa azonban IResult, míg az egyes TypedResults segítők visszatérési típusa az IResult implementálási típusok egyike. A különbség azt jelenti, hogy Results segítők esetében átalakításra van szükség, ha a konkrét típusra van szükség, például egységteszteléshez. A megvalósítási típusok a Microsoft.AspNetCore.Http.HttpResults névtérben vannak definiálva.

A TypedResults helyett a Results visszaadása a következő előnyökkel jár:

  • TypedResults segítők erősen gépelt objektumokat adnak vissza, amelyek javítják a kód olvashatóságát, az egységtesztelést és csökkentik a futásidejű hibák esélyét.
  • A implementáció típusa automatikusan biztosítja az OpenAPI választípus-metaadatait a végpont leírásához.

Fontolja meg a következő végpontot, amelyhez egy 200 OK állapotkód jön létre a várt JSON-válaszsal.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

A végpont helyes dokumentálásához a bővítménymetódus Produces van meghívva. Azonban nem szükséges meghívni Produces, ha TypedResults van használva Resultshelyett, ahogy az alábbi kódban látható. TypedResults automatikusan biztosítja a végpont metaadatait.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

A választípusok leírásáról további információért lásd az OpenAPI-támogatás a minimális API-kbanrészt.

Ahogy korábban említettük, TypedResultshasználatakor nincs szükség átalakításra. Fontolja meg a következő minimális API-t, amely egy TypedResults osztályt ad vissza

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

A következő vizsgálat a teljes betontípust ellenőrzi:

[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}

Mivel az Results összes metódusa IResult típust ad vissza az aláírásban, a fordító automatikusan ezt a típust állapítja meg a kérés delegált visszatérési típusaként, amikor egy végpont különböző eredményeket ad vissza. TypedResults megköveteli a Results<T1, TN> használatát az ilyen típusú meghatalmazottaktól.

A következő módszer azért lefordul, mert Results.Ok és Results.NotFound is úgy vannak deklarálva, hogy IResult-t adnak vissza, annak ellenére, hogy a visszaadott objektumok tényleges konkrét típusai eltérőek:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

A következő metódus nem fordítható le, mert a TypedResults.Ok és a TypedResults.NotFound különböző típusok visszaadásaként van deklarálva, és a fordító nem próbálja meg a legjobb egyező típust kikövetkeztetni:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

A TypedResultshasználatához a visszatérési típust teljes mértékben deklarálni kell, amely aszinkron esetben a Task<> burkolót igényli. A TypedResults használata bőbeszédűbb, de ez az a kompromisszum, amelyért cserébe a típusadatok statikusan elérhetők, így az OpenAPI-ban önmagukat leírhatják.

app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
   await db.Todos.FindAsync(id)
    is Todo todo
       ? TypedResults.Ok(todo)
       : TypedResults.NotFound());

Eredmények<TResult1, TResultN>

Használja a Results<TResult1, TResultN> a végpontkezelő visszatérési típusát IResult helyett, amikor:

  • A rendszer több IResult implementációtípust ad vissza a végpontkezelőtől.
  • A statikus TypedResult osztály a IResult objektumok létrehozásához használható.

Ez az alternatíva jobb, mint a IResult visszaadása, mivel az általános egyesítési típusok automatikusan megőrzik a végpont metaadatait. Mivel az Results<TResult1, TResultN> egyesítő típusok implicit öntött operátorokat implementálnak, a fordító automatikusan konvertálhatja az általános argumentumokban megadott típusokat az egyesítő típus egy példányává.

Ez azzal a többletelőnnyel jár, hogy fordítási időt biztosít annak ellenőrzéséhez, hogy az útvonalkezelő valójában csak az általa deklarált eredményeket adja vissza. Ha olyan típust próbál visszaadni, amely nincs Results<> általános argumentumai között deklarálva, az fordítási hibát eredményez.

Vegye figyelembe a következő végpontot, amelynek 400 BadRequest állapotkódját adja vissza, ha a orderId nagyobb, mint 999. Ellenkező esetben létrehoz egy 200 OK-t a várt tartalommal.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

A végpont helyes dokumentálásához a bővítménymetódus Produces van meghívva. Mivel azonban a TypedResults segéd automatikusan tartalmazza a végpont metaadatait, a Results<T1, Tn> egyesítési típust is visszaadhatja, ahogyan az az alábbi kódban is látható.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Beépített eredmények

A Results és TypedResults statikus osztályokban gyakori eredmény-segítők léteznek. TypedResults visszaadása előnyösebb, mint Resultsvisszaadása. További információ: TypedResults vs Results.

Az alábbi szakaszok a gyakori eredménysegítők használatát mutatják be.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync a JSON visszaadásának alternatív módja:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Egyéni állapotkód

app.MapGet("/405", () => Results.StatusCode(405));

Belső kiszolgálóhiba

app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));

Az előző példa egy 500-ás állapotkódot ad vissza.

Probléma és validálási probléma

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

Szöveg

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();

Results.Stream túlterhelések pufferelés nélkül teszik lehetővé a mögöttes HTTP-válaszfolyam elérését. Az alábbi példa ImageSharp használatával adja vissza a megadott kép kisebb méretét:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

Az alábbi példa folyamatosan továbbít egy képet az Azure Blob tárhelyről.

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

Az alábbi példa egy Azure Blobból streamel egy videót:

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Átirányít

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Fájl

app.MapGet("/download", () => Results.File("myfile.text"));

HttpResult-felületek

A Microsoft.AspNetCore.Http névtér alábbi felületei lehetővé teszik a IResult típus észlelését futásidőben, ami a szűrő implementációk gyakori mintája:

Íme egy példa egy szűrőre, amely az alábbi felületek egyikét használja:

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

További információért lásd: Minimális API alkalmazások szűrői és IResult implementálási típusok.

Fejlécek módosítása

A válaszfejlécek módosításához használja a HttpResponse objektumot:

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

Válaszok testreszabása

Az alkalmazások egyéni IResult típus implementálásával szabályozhatják a válaszokat. Az alábbi kód egy PÉLDA EGY HTML-eredménytípusra:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Javasoljuk, hogy adjon hozzá egy bővítménymetódust a Microsoft.AspNetCore.Http.IResultExtensions az egyéni eredmények felderíthetőbbé tétele érdekében.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Az egyéni IResult-típus a IEndpointMetadataProvider felület implementálásával saját annotációt is biztosíthat. A következő kód például egy megjegyzést ad hozzá az előző HtmlResult típushoz, amely leírja a végpont által létrehozott választ.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

A ProducesHtmlMetadata a IProducesResponseTypeMetadata implementációja, amely meghatározza a létrehozott választartalom típusát text/html és az állapotkódot 200 OK.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Egy másik módszer a Microsoft.AspNetCore.Mvc.ProducesAttribute használata a létrehozott válasz leírására. Az alábbi kód a PopulateMetadatahasználatára módosítja a ProducesAttribute metódust.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

JSON-szerializálási beállítások konfigurálása

Alapértelmezés szerint a minimális API-alkalmazások Web defaults lehetőségeket használnak a JSON szerializálása és deszerializálása során.

JSON-szerializálási beállítások konfigurálása globálisan

A beállítások globálisan konfigurálhatók egy alkalmazáshoz a ConfigureHttpJsonOptionsmeghívásával. Az alábbi példa a nyilvános mezőket és a JSON-kimenet formátumát tartalmazza.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Mivel a mezők szerepelnek, az előző kód beolvassa a NameField értéket, és belefoglalja a kimeneti JSON-fájlba.

JSON-szerializálási beállítások konfigurálása végponthoz

A végpont szerializálási beállításainak konfigurálásához hívja meg Results.Json, és adjon át neki egy JsonSerializerOptions objektumot, ahogyan az az alábbi példában látható:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

Alternatívaként használja a WriteAsJsonAsync túlterhelését, amely elfogadja a JsonSerializerOptions objektumot. Az alábbi példa ezt a túlterhelést használja a kimeneti JSON formázásához:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

További erőforrások

A minimális végpontok a következő típusú visszatérési értékeket támogatják:

  1. string – Ez magában foglalja Task<string> és ValueTask<string>.
  2. T (Bármely más típus) – Ebbe beletartozik Task<T> és ValueTask<T>.
  3. IResult alapú – Ez magában foglalja Task<IResult> és ValueTask<IResult>.

string visszaadott értékek

Magatartás Tartalomtípus
A keretrendszer közvetlenül a válaszba írja a sztringet. text/plain

Vegye figyelembe az alábbi útvonalkezelőt, amely egy Hello world szöveget ad vissza.

app.MapGet("/hello", () => "Hello World");

A 200 állapotkód text/plain Content-Type fejléccel és az alábbi tartalommal lesz visszaadva.

Hello World

T (bármely más típus) visszaadja az értékeket

Magatartás Tartalomtípus
A keretrendszer JSON-szerializálja a választ. application/json

Vegye figyelembe a következő útvonalkezelőt, amely egy névtelen típust ad vissza, amely egy Message karakterlánc-tulajdonságot tartalmaz.

app.MapGet("/hello", () => new { Message = "Hello World" });

A 200 állapotkód application/json Content-Type fejléccel és az alábbi tartalommal lesz visszaadva.

{"message":"Hello World"}

IResult visszaadott értékek

Magatartás Tartalomtípus
A keretrendszer meghívja IResult.ExecuteAsync. A IResult megvalósítása határozza meg.

A IResult felület egy HTTP-végpont eredményét képviselő szerződést határoz meg. A statikus Eredmények osztály és a statikus TypedResults különböző IResult típusú válaszok létrehozására szolgálnak.

TypedResults és Results

A Results és TypedResults statikus osztályok hasonló eredménysegítőket biztosítanak. A TypedResults osztály a típusú megfelelője a Results osztálynak. A Results segítők visszatérési típusa azonban IResult, míg az egyes TypedResults segítők visszatérési típusa az IResult implementálási típusok egyike. A különbség azt jelenti, hogy Results segítők esetében átalakításra van szükség, ha a konkrét típusra van szükség, például egységteszteléshez. A megvalósítási típusok a Microsoft.AspNetCore.Http.HttpResults névtérben vannak definiálva.

A TypedResults helyett a Results visszaadása a következő előnyökkel jár:

  • TypedResults segítők erősen gépelt objektumokat adnak vissza, amelyek javítják a kód olvashatóságát, az egységtesztelést és csökkentik a futásidejű hibák esélyét.
  • A implementáció típusa automatikusan biztosítja az OpenAPI választípus-metaadatait a végpont leírásához.

Fontolja meg a következő végpontot, amelyhez egy 200 OK állapotkód jön létre a várt JSON-válaszsal.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

A végpont helyes dokumentálásához a bővítménymetódus Produces van meghívva. Azonban nem szükséges meghívni Produces, ha TypedResults van használva Resultshelyett, ahogy az alábbi kódban látható. TypedResults automatikusan biztosítja a végpont metaadatait.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

A választípusok leírásáról további információért lásd az OpenAPI-támogatás a minimális API-kbanrészt.

Ahogy korábban említettük, TypedResultshasználatakor nincs szükség átalakításra. Fontolja meg a következő minimális API-t, amely egy TypedResults osztályt ad vissza

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

A következő vizsgálat a teljes betontípust ellenőrzi:

[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}

Mivel az Results összes metódusa IResult típust ad vissza az aláírásban, a fordító automatikusan ezt a típust állapítja meg a kérés delegált visszatérési típusaként, amikor egy végpont különböző eredményeket ad vissza. TypedResults megköveteli a Results<T1, TN> használatát az ilyen típusú meghatalmazottaktól.

A következő módszer azért lefordul, mert Results.Ok és Results.NotFound is úgy vannak deklarálva, hogy IResult-t adnak vissza, annak ellenére, hogy a visszaadott objektumok tényleges konkrét típusai eltérőek:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

A következő metódus nem fordítható le, mert a TypedResults.Ok és a TypedResults.NotFound különböző típusok visszaadásaként van deklarálva, és a fordító nem próbálja meg a legjobb egyező típust kikövetkeztetni:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

A TypedResultshasználatához a visszatérési típust teljes mértékben deklarálni kell, amely aszinkron esetben a Task<> burkolót igényli. A TypedResults használata bőbeszédűbb, de ez az a kompromisszum, amelyért cserébe a típusadatok statikusan elérhetők, így az OpenAPI-ban önmagukat leírhatják.

app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
   await db.Todos.FindAsync(id)
    is Todo todo
       ? TypedResults.Ok(todo)
       : TypedResults.NotFound());

Eredmények<TResult1, TResultN>

Használja a Results<TResult1, TResultN> a végpontkezelő visszatérési típusát IResult helyett, amikor:

  • A rendszer több IResult implementációtípust ad vissza a végpontkezelőtől.
  • A statikus TypedResult osztály a IResult objektumok létrehozásához használható.

Ez az alternatíva jobb, mint a IResult visszaadása, mivel az általános egyesítési típusok automatikusan megőrzik a végpont metaadatait. Mivel az Results<TResult1, TResultN> egyesítő típusok implicit öntött operátorokat implementálnak, a fordító automatikusan konvertálhatja az általános argumentumokban megadott típusokat az egyesítő típus egy példányává.

Ez azzal a többletelőnnyel jár, hogy fordítási időt biztosít annak ellenőrzéséhez, hogy az útvonalkezelő valójában csak az általa deklarált eredményeket adja vissza. Ha olyan típust próbál visszaadni, amely nincs Results<> általános argumentumai között deklarálva, az fordítási hibát eredményez.

Vegye figyelembe a következő végpontot, amelynek 400 BadRequest állapotkódját adja vissza, ha a orderId nagyobb, mint 999. Ellenkező esetben létrehoz egy 200 OK-t a várt tartalommal.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

A végpont helyes dokumentálásához a bővítménymetódus Produces van meghívva. Mivel azonban a TypedResults segéd automatikusan tartalmazza a végpont metaadatait, a Results<T1, Tn> egyesítési típust is visszaadhatja, ahogyan az az alábbi kódban is látható.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId) 
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Beépített eredmények

A Results és TypedResults statikus osztályokban gyakori eredmény-segítők léteznek. TypedResults visszaadása előnyösebb, mint Resultsvisszaadása. További információ: TypedResults vs Results.

Az alábbi szakaszok a gyakori eredménysegítők használatát mutatják be.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync a JSON visszaadásának alternatív módja:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Egyéni állapotkód

app.MapGet("/405", () => Results.StatusCode(405));

Szöveg

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();

Results.Stream túlterhelések pufferelés nélkül teszik lehetővé a mögöttes HTTP-válaszfolyam elérését. Az alábbi példa ImageSharp használatával adja vissza a megadott kép kisebb méretét:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

Az alábbi példa folyamatosan továbbít egy képet az Azure Blob tárhelyről.

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

Az alábbi példa egy Azure Blobból streamel egy videót:

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Átirányít

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Fájl

app.MapGet("/download", () => Results.File("myfile.text"));

HttpResult-felületek

A Microsoft.AspNetCore.Http névtér alábbi felületei lehetővé teszik a IResult típus észlelését futásidőben, ami a szűrő implementációk gyakori mintája:

Íme egy példa egy szűrőre, amely az alábbi felületek egyikét használja:

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

További információért lásd: Minimális API alkalmazások szűrői és IResult implementálási típusok.

Válaszok testreszabása

Az alkalmazások egyéni IResult típus implementálásával szabályozhatják a válaszokat. Az alábbi kód egy PÉLDA EGY HTML-eredménytípusra:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Javasoljuk, hogy adjon hozzá egy bővítménymetódust a Microsoft.AspNetCore.Http.IResultExtensions az egyéni eredmények felderíthetőbbé tétele érdekében.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Az egyéni IResult-típus a IEndpointMetadataProvider felület implementálásával saját annotációt is biztosíthat. A következő kód például egy megjegyzést ad hozzá az előző HtmlResult típushoz, amely leírja a végpont által létrehozott választ.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

A ProducesHtmlMetadata a IProducesResponseTypeMetadata implementációja, amely meghatározza a létrehozott választartalom típusát text/html és az állapotkódot 200 OK.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Egy másik módszer a Microsoft.AspNetCore.Mvc.ProducesAttribute használata a létrehozott válasz leírására. Az alábbi kód a PopulateMetadatahasználatára módosítja a ProducesAttribute metódust.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

JSON-szerializálási beállítások konfigurálása

Alapértelmezés szerint a minimális API-alkalmazások Web defaults lehetőségeket használnak a JSON szerializálása és deszerializálása során.

JSON-szerializálási beállítások konfigurálása globálisan

A beállítások globálisan konfigurálhatók egy alkalmazáshoz a ConfigureHttpJsonOptionsmeghívásával. Az alábbi példa a nyilvános mezőket és a JSON-kimenet formátumát tartalmazza.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Mivel a mezők szerepelnek, az előző kód beolvassa a NameField értéket, és belefoglalja a kimeneti JSON-fájlba.

JSON-szerializálási beállítások konfigurálása végponthoz

A végpont szerializálási beállításainak konfigurálásához hívja meg Results.Json, és adjon át neki egy JsonSerializerOptions objektumot, ahogyan az az alábbi példában látható:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

Alternatívaként használja a WriteAsJsonAsync túlterhelését, amely elfogadja a JsonSerializerOptions objektumot. Az alábbi példa ezt a túlterhelést használja a kimeneti JSON formázásához:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

További erőforrások