Jak vytvářet odpovědi v minimálních aplikacích API
Poznámka:
Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
Upozorňující
Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v tématu .NET a .NET Core Zásady podpory. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
Důležité
Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.
Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
Minimální koncové body podporují následující typy návratových hodnot:
string
- To zahrnujeTask<string>
aValueTask<string>
.T
(Jakýkoli jiný typ) - To zahrnujeTask<T>
aValueTask<T>
.IResult
na základě - To zahrnujeTask<IResult>
aValueTask<IResult>
.
string
návratové hodnoty
Chování | Typ obsahu |
---|---|
Architektura zapíše řetězec přímo do odpovědi. | text/plain |
Představte si následující obslužnou rutinu Hello world
trasy, která vrací text.
app.MapGet("/hello", () => "Hello World");
Stavový 200
kód se vrátí s hlavičkou text/plain
Content-Type a následujícím obsahem.
Hello World
T
(Jakýkoli jiný typ) vrácené hodnoty
Chování | Typ obsahu |
---|---|
Rozhraní JSON serializuje odpověď. | application/json |
Zvažte následující obslužnou rutinu trasy, která vrací anonymní typ obsahující Message
vlastnost řetězce.
app.MapGet("/hello", () => new { Message = "Hello World" });
Stavový 200
kód se vrátí s hlavičkou application/json
Content-Type a následujícím obsahem.
{"message":"Hello World"}
IResult
návratové hodnoty
Chování | Typ obsahu |
---|---|
Architektura volá IResult.ExecuteAsync. | Rozhodlo se implementací IResult . |
Rozhraní IResult
definuje kontrakt, který představuje výsledek koncového bodu HTTP. Static Results třída a static TypedResults slouží k vytvoření různých objektů, které představují různé IResult
typy odpovědí.
TypedResults vs. výsledky
Třídy Results a TypedResults statické třídy poskytují podobné sady pomocných rutin výsledků. Třída TypedResults
je typový ekvivalent Results
třídy. Návratový Results
typ pomocných rutin je IResultvšak , zatímco návratový typ jednotlivých TypedResults
pomocných rutin je jedním z IResult
typů implementace. Rozdíl znamená, že u Results
pomocných rutin je potřeba převod, když je třeba konkrétní typ, například pro testování jednotek. Typy implementace jsou definovány v Microsoft.AspNetCore.Http.HttpResults oboru názvů.
Vrácení TypedResults
místo Results
následujících výhod:
TypedResults
Pomocné rutiny vrací objekty silného typu, které můžou zlepšit čitelnost kódu, testování jednotek a snížit pravděpodobnost chyb za běhu.- Typ implementace automaticky poskytuje metadata typu odpovědi pro OpenAPI popis koncového bodu.
Zvažte následující koncový bod, pro který se vytvoří stavový kód s očekávanou 200 OK
odpovědí JSON.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Aby bylo možné zdokumentovat tento koncový bod správně, volá se metoda Produces
rozšíření. Není však nutné volat Produces
, pokud TypedResults
se používá místo Results
, jak je znázorněno v následujícím kódu. TypedResults
automaticky poskytuje metadata pro koncový bod.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Další informace o popisu typu odpovědi najdete v tématu Podpora OpenAPI v minimálních rozhraních API.
Jak už bylo zmíněno dříve, při použití TypedResults
není převod potřeba. Zvažte následující minimální rozhraní API, které vrací TypedResults
třídu.
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Následující zkoušky kontrolou úplného typu betonu:
[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);
});
}
Vzhledem k tomu, že všechny metody při Results
vrácení IResult
ve svém podpisu kompilátor automaticky odvodí, že jako typ vrácení delegáta požadavku při vrácení různých výsledků z jednoho koncového bodu. TypedResults
vyžaduje použití Results<T1, TN>
těchto delegátů.
Následující metoda se zkompiluje, protože obě Results.Ok
a Results.NotFound
jsou deklarovány jako vrácení IResult
, i když skutečné konkrétní typy vrácených objektů jsou odlišné:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Následující metoda se nekompiluje, protože TypedResults.Ok
a TypedResults.NotFound
jsou deklarovány jako vrácení různých typů a kompilátor se nepokusí odvodit nejlepší odpovídající typ:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Chcete-li použít TypedResults
, návratový typ musí být plně deklarován, což v případě asynchronního vyžaduje obálku Task<>
. Použití TypedResults
je více podrobné, ale to je kompromis pro to, aby informace o typu byly staticky dostupné a jsou tak schopny samostatně popsat OpenAPI:
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());
Výsledky<TResult1, TResultN>
Jako návratový typ obslužné rutiny koncového IResult
bodu použijte Results<TResult1, TResultN>
místo v těchto případech:
- Z obslužné rutiny koncového bodu se vrátí více
IResult
typů implementace. - Statická
TypedResult
třída se používá k vytvořeníIResult
objektů.
Tato alternativa je lepší než vrácení IResult
, protože obecné typy sjednocení automaticky uchovávají metadata koncového bodu. A vzhledem k tomu, že Results<TResult1, TResultN>
typy sjednocení implementují implicitní operátory přetypování, kompilátor může automaticky převést typy zadané v obecných argumentech na instanci typu sjednocení.
To má přidanou výhodu poskytování kontroly času kompilace, že obslužná rutina trasy skutečně vrací pouze výsledky, které deklaruje. Pokus o vrácení typu, který není deklarován jako jeden z obecných argumentů, aby výsledkem Results<>
byla chyba kompilace.
Vezměte v úvahu následující koncový bod, pro který se vrátí stavový 400 BadRequest
kód, když orderId
je větší než 999
. V opačném případě vytvoří s očekávaným obsahem 200 OK
.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Aby bylo možné tento koncový bod správně zdokumentovat, volá se metoda Produces
rozšíření. Vzhledem k tomu, že pomocná TypedResults
rutina automaticky obsahuje metadata koncového bodu, můžete místo toho vrátit Results<T1, Tn>
typ sjednocení, jak je znázorněno v následujícím kódu.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Předdefinované výsledky
Běžné pomocné rutiny výsledků existují ve statických Results TypedResults třídách. TypedResults
Vrácení je upřednostňované pro vrácení Results
. Další informace naleznete v tématu TypedResults vs Výsledky.
Následující části ukazují použití běžných pomocných rutin výsledků.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync je alternativní způsob, jak vrátit JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Vlastní stavový kód
app.MapGet("/405", () => Results.StatusCode(405));
Vnitřní chyba serveru
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
Předchozí příklad vrátí stavový kód 500.
Text
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
přetížení umožňují přístup k podkladovému streamu odpovědi HTTP bez ukládání do vyrovnávací paměti. Následující příklad používá ImageSharp k vrácení zmenšené velikosti zadané image:
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);
}
Následující příklad streamuje obrázek ze služby Azure Blob Storage:
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");
});
Následující příklad streamuje video z objektu blob Azure:
// 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);
});
Přesměrování
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Soubor
app.MapGet("/download", () => Results.File("myfile.text"));
Rozhraní HttpResult
Následující rozhraní v Microsoft.AspNetCore.Http oboru názvů poskytují způsob, jak zjistit IResult
typ za běhu, což je běžný vzor v implementacích filtrů:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Tady je příklad filtru, který používá jedno z těchto rozhraní:
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
};
});
Další informace najdete v tématu Filtry v minimálních aplikacích API a typech implementace IResult.
Přizpůsobení odpovědí
Aplikace můžou řídit odpovědi implementací vlastního IResult typu. Následující kód je příkladem typu výsledku HTML:
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);
}
}
Doporučujeme přidat metodu rozšíření, aby Microsoft.AspNetCore.Http.IResultExtensions byly tyto vlastní výsledky lépe zjistitelné.
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();
IResult
Vlastní typ může také poskytnout vlastní poznámku implementací IEndpointMetadataProvider rozhraní. Následující kód například přidá poznámku k předchozímu HtmlResult
typu, který popisuje odpověď vytvořenou koncovým bodem.
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());
}
}
Jedná se ProducesHtmlMetadata
o implementaci IProducesResponseTypeMetadata , která definuje vytvořený typ text/html
obsahu odpovědi a stavový kód 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Alternativním přístupem je popis Microsoft.AspNetCore.Mvc.ProducesAttribute vytvořené odpovědi. Následující kód změní metodu PopulateMetadata
, která se má použít ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Konfigurace možností serializace JSON
Ve výchozím nastavení používají Web defaults
minimální aplikace API možnosti při serializaci a deserializaci JSON.
Globální konfigurace možností serializace JSON
Možnosti lze pro aplikaci nakonfigurovat globálně vyvoláním ConfigureHttpJsonOptions. Následující příklad obsahuje veřejná pole a formátuje výstup JSON.
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
// }
Vzhledem k tomu, že jsou zahrnutá pole, předchozí kód přečte NameField
a zahrne ho do výstupního kódu JSON.
Konfigurace možností serializace JSON pro koncový bod
Pokud chcete nakonfigurovat možnosti serializace pro koncový bod, vyvoláte Results.Json ho a předejte mu JsonSerializerOptions objekt, jak je znázorněno v následujícím příkladu:
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
// }
Jako alternativu použijte přetížení WriteAsJsonAsync , které přijímá JsonSerializerOptions objekt. Následující příklad používá toto přetížení k formátování výstupního FORMÁTU JSON:
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
// }
Další materiály
Minimální koncové body podporují následující typy návratových hodnot:
string
- To zahrnujeTask<string>
aValueTask<string>
.T
(Jakýkoli jiný typ) - To zahrnujeTask<T>
aValueTask<T>
.IResult
na základě - To zahrnujeTask<IResult>
aValueTask<IResult>
.
string
návratové hodnoty
Chování | Typ obsahu |
---|---|
Architektura zapíše řetězec přímo do odpovědi. | text/plain |
Představte si následující obslužnou rutinu Hello world
trasy, která vrací text.
app.MapGet("/hello", () => "Hello World");
Stavový 200
kód se vrátí s hlavičkou text/plain
Content-Type a následujícím obsahem.
Hello World
T
(Jakýkoli jiný typ) vrácené hodnoty
Chování | Typ obsahu |
---|---|
Rozhraní JSON serializuje odpověď. | application/json |
Zvažte následující obslužnou rutinu trasy, která vrací anonymní typ obsahující Message
vlastnost řetězce.
app.MapGet("/hello", () => new { Message = "Hello World" });
Stavový 200
kód se vrátí s hlavičkou application/json
Content-Type a následujícím obsahem.
{"message":"Hello World"}
IResult
návratové hodnoty
Chování | Typ obsahu |
---|---|
Architektura volá IResult.ExecuteAsync. | Rozhodlo se implementací IResult . |
Rozhraní IResult
definuje kontrakt, který představuje výsledek koncového bodu HTTP. Static Results třída a static TypedResults slouží k vytvoření různých objektů, které představují různé IResult
typy odpovědí.
TypedResults vs. výsledky
Třídy Results a TypedResults statické třídy poskytují podobné sady pomocných rutin výsledků. Třída TypedResults
je typový ekvivalent Results
třídy. Návratový Results
typ pomocných rutin je IResultvšak , zatímco návratový typ jednotlivých TypedResults
pomocných rutin je jedním z IResult
typů implementace. Rozdíl znamená, že u Results
pomocných rutin je potřeba převod, když je třeba konkrétní typ, například pro testování jednotek. Typy implementace jsou definovány v Microsoft.AspNetCore.Http.HttpResults oboru názvů.
Vrácení TypedResults
místo Results
následujících výhod:
TypedResults
Pomocné rutiny vrací objekty silného typu, které můžou zlepšit čitelnost kódu, testování jednotek a snížit pravděpodobnost chyb za běhu.- Typ implementace automaticky poskytuje metadata typu odpovědi pro OpenAPI popis koncového bodu.
Zvažte následující koncový bod, pro který se vytvoří stavový kód s očekávanou 200 OK
odpovědí JSON.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Aby bylo možné zdokumentovat tento koncový bod správně, volá se metoda Produces
rozšíření. Není však nutné volat Produces
, pokud TypedResults
se používá místo Results
, jak je znázorněno v následujícím kódu. TypedResults
automaticky poskytuje metadata pro koncový bod.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Další informace o popisu typu odpovědi najdete v tématu Podpora OpenAPI v minimálních rozhraních API.
Jak už bylo zmíněno dříve, při použití TypedResults
není převod potřeba. Zvažte následující minimální rozhraní API, které vrací TypedResults
třídu.
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Následující zkoušky kontrolou úplného typu betonu:
[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);
});
}
Vzhledem k tomu, že všechny metody při Results
vrácení IResult
ve svém podpisu kompilátor automaticky odvodí, že jako typ vrácení delegáta požadavku při vrácení různých výsledků z jednoho koncového bodu. TypedResults
vyžaduje použití Results<T1, TN>
těchto delegátů.
Následující metoda se zkompiluje, protože obě Results.Ok
a Results.NotFound
jsou deklarovány jako vrácení IResult
, i když skutečné konkrétní typy vrácených objektů jsou odlišné:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Následující metoda se nekompiluje, protože TypedResults.Ok
a TypedResults.NotFound
jsou deklarovány jako vrácení různých typů a kompilátor se nepokusí odvodit nejlepší odpovídající typ:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Chcete-li použít TypedResults
, návratový typ musí být plně deklarován, což v případě asynchronního vyžaduje obálku Task<>
. Použití TypedResults
je více podrobné, ale to je kompromis pro to, aby informace o typu byly staticky dostupné a jsou tak schopny samostatně popsat OpenAPI:
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());
Výsledky<TResult1, TResultN>
Jako návratový typ obslužné rutiny koncového IResult
bodu použijte Results<TResult1, TResultN>
místo v těchto případech:
- Z obslužné rutiny koncového bodu se vrátí více
IResult
typů implementace. - Statická
TypedResult
třída se používá k vytvořeníIResult
objektů.
Tato alternativa je lepší než vrácení IResult
, protože obecné typy sjednocení automaticky uchovávají metadata koncového bodu. A vzhledem k tomu, že Results<TResult1, TResultN>
typy sjednocení implementují implicitní operátory přetypování, kompilátor může automaticky převést typy zadané v obecných argumentech na instanci typu sjednocení.
To má přidanou výhodu poskytování kontroly času kompilace, že obslužná rutina trasy skutečně vrací pouze výsledky, které deklaruje. Pokus o vrácení typu, který není deklarován jako jeden z obecných argumentů, aby výsledkem Results<>
byla chyba kompilace.
Vezměte v úvahu následující koncový bod, pro který se vrátí stavový 400 BadRequest
kód, když orderId
je větší než 999
. V opačném případě vytvoří s očekávaným obsahem 200 OK
.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Aby bylo možné tento koncový bod správně zdokumentovat, volá se metoda Produces
rozšíření. Vzhledem k tomu, že pomocná TypedResults
rutina automaticky obsahuje metadata koncového bodu, můžete místo toho vrátit Results<T1, Tn>
typ sjednocení, jak je znázorněno v následujícím kódu.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Předdefinované výsledky
Běžné pomocné rutiny výsledků existují ve statických Results TypedResults třídách. TypedResults
Vrácení je upřednostňované pro vrácení Results
. Další informace naleznete v tématu TypedResults vs Výsledky.
Následující části ukazují použití běžných pomocných rutin výsledků.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync je alternativní způsob, jak vrátit JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Vlastní stavový kód
app.MapGet("/405", () => Results.StatusCode(405));
Text
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
přetížení umožňují přístup k podkladovému streamu odpovědi HTTP bez ukládání do vyrovnávací paměti. Následující příklad používá ImageSharp k vrácení zmenšené velikosti zadané image:
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);
}
Následující příklad streamuje obrázek ze služby Azure Blob Storage:
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");
});
Následující příklad streamuje video z objektu blob Azure:
// 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);
});
Přesměrování
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Soubor
app.MapGet("/download", () => Results.File("myfile.text"));
Rozhraní HttpResult
Následující rozhraní v Microsoft.AspNetCore.Http oboru názvů poskytují způsob, jak zjistit IResult
typ za běhu, což je běžný vzor v implementacích filtrů:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Tady je příklad filtru, který používá jedno z těchto rozhraní:
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
};
});
Další informace najdete v tématu Filtry v minimálních aplikacích API a typech implementace IResult.
Přizpůsobení odpovědí
Aplikace můžou řídit odpovědi implementací vlastního IResult typu. Následující kód je příkladem typu výsledku HTML:
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);
}
}
Doporučujeme přidat metodu rozšíření, aby Microsoft.AspNetCore.Http.IResultExtensions byly tyto vlastní výsledky lépe zjistitelné.
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();
IResult
Vlastní typ může také poskytnout vlastní poznámku implementací IEndpointMetadataProvider rozhraní. Následující kód například přidá poznámku k předchozímu HtmlResult
typu, který popisuje odpověď vytvořenou koncovým bodem.
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());
}
}
Jedná se ProducesHtmlMetadata
o implementaci IProducesResponseTypeMetadata , která definuje vytvořený typ text/html
obsahu odpovědi a stavový kód 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Alternativním přístupem je popis Microsoft.AspNetCore.Mvc.ProducesAttribute vytvořené odpovědi. Následující kód změní metodu PopulateMetadata
, která se má použít ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Konfigurace možností serializace JSON
Ve výchozím nastavení používají Web defaults
minimální aplikace API možnosti při serializaci a deserializaci JSON.
Globální konfigurace možností serializace JSON
Možnosti lze pro aplikaci nakonfigurovat globálně vyvoláním ConfigureHttpJsonOptions. Následující příklad obsahuje veřejná pole a formátuje výstup JSON.
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
// }
Vzhledem k tomu, že jsou zahrnutá pole, předchozí kód přečte NameField
a zahrne ho do výstupního kódu JSON.
Konfigurace možností serializace JSON pro koncový bod
Pokud chcete nakonfigurovat možnosti serializace pro koncový bod, vyvoláte Results.Json ho a předejte mu JsonSerializerOptions objekt, jak je znázorněno v následujícím příkladu:
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
// }
Jako alternativu použijte přetížení WriteAsJsonAsync , které přijímá JsonSerializerOptions objekt. Následující příklad používá toto přetížení k formátování výstupního FORMÁTU JSON:
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
// }