Jak tworzyć odpowiedzi w minimalnych aplikacjach interfejsu API
Uwaga
Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.
Ważne
Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.
Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.
Minimalne punkty końcowe obsługują następujące typy zwracanych wartości:
string
— Dotyczy to równieżTask<string>
iValueTask<string>
.T
(Dowolny inny typ) — dotyczy to równieżTask<T>
elementów iValueTask<T>
.IResult
based — obejmuje toTask<IResult>
iValueTask<IResult>
.
string
zwracane wartości
Zachowanie | Typ zawartości |
---|---|
Struktura zapisuje ciąg bezpośrednio w odpowiedzi. | text/plain |
Rozważmy następującą procedurę Hello world
obsługi tras, która zwraca tekst.
app.MapGet("/hello", () => "Hello World");
Kod 200
stanu jest zwracany z nagłówkiem text/plain
Content-Type i następującą zawartością.
Hello World
T
(Dowolny inny typ) zwracane wartości
Zachowanie | Typ zawartości |
---|---|
Struktura JSON serializuje odpowiedź. | application/json |
Rozważmy następującą procedurę obsługi tras, która zwraca typ anonimowy zawierający Message
właściwość ciągu.
app.MapGet("/hello", () => new { Message = "Hello World" });
Kod 200
stanu jest zwracany z nagłówkiem application/json
Content-Type i następującą zawartością.
{"message":"Hello World"}
IResult
zwracane wartości
Zachowanie | Typ zawartości |
---|---|
Struktura wywołuje metodę IResult.ExecuteAsync. | Decyzja o wdrożeniu IResult . |
Interfejs IResult
definiuje kontrakt reprezentujący wynik punktu końcowego HTTP. Statyczna klasa Results i statyczny typdResults służą do tworzenia różnych obiektów reprezentujących różne IResult
typy odpowiedzi.
TypedResults a wyniki
Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults
jest typem równoważnej Results
klasie. Jednak Results
zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults
pomocnika jest jednym z IResult
typów implementacji. Różnica oznacza, że w przypadku Results
pomocników wymagana jest konwersja, gdy wymagany jest konkretny typ, na przykład do testowania jednostkowego. Typy implementacji są definiowane Microsoft.AspNetCore.Http.HttpResults w przestrzeni nazw.
Zwracanie TypedResults
zamiast Results
ma następujące zalety:
TypedResults
Pomocnicy zwracają silnie typizowane obiekty, co może zwiększyć czytelność kodu, testowanie jednostkowe i zmniejszyć prawdopodobieństwo błędów środowiska uruchomieniowego.- Typ implementacji automatycznie udostępnia metadane typu odpowiedzi dla interfejsu OpenAPI w celu opisania punktu końcowego.
Rozważ następujący punkt końcowy, dla którego 200 OK
jest generowany kod stanu z oczekiwaną odpowiedzią JSON.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Aby poprawnie udokumentować ten punkt końcowy, wywoływana jest metoda Produces
rozszerzeń. Nie jest jednak konieczne wywołanie Produces
metody , jeśli TypedResults
jest używana zamiast Results
, jak pokazano w poniższym kodzie. TypedResults
automatycznie udostępnia metadane punktu końcowego.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Aby uzyskać więcej informacji na temat opisywania typu odpowiedzi, zobacz Obsługa interfejsu OpenAPI w minimalnych interfejsach API.
Jak wspomniano wcześniej, w przypadku korzystania z programu TypedResults
konwersja nie jest wymagana. Rozważmy następujący minimalny interfejs API, który zwraca klasę TypedResults
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Następujące testy sprawdzają pełny typ konkretnego:
[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);
});
}
Ponieważ wszystkie metody zwracane Results
IResult
w podpisie, kompilator automatycznie wywnioskuje, że jako delegat żądania zwraca typ zwracany podczas zwracania różnych wyników z jednego punktu końcowego. TypedResults
wymaga użycia z Results<T1, TN>
takich delegatów.
Następująca metoda kompiluje się, ponieważ obie Results.Ok
metody i Results.NotFound
są deklarowane jako zwracane IResult
, mimo że rzeczywiste konkretne typy zwracanych obiektów są różne:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Poniższa metoda nie jest kompilowana, ponieważ i TypedResults.NotFound
są deklarowane jako zwracane różne typy, a TypedResults.Ok
kompilator nie podejmie próby wywnioskowania najlepszego zgodnego typu:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Aby użyć TypedResults
metody , typ zwracany musi być w pełni zadeklarowany, co w przypadku, gdy asynchroniczny wymaga Task<>
otoki. Użycie TypedResults
jest bardziej pełne, ale to kompromis w przypadku statycznego udostępniania informacji o typie i w związku z tym możliwość samodzielnego opisywania interfejsu 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());
Wyniki<TResult1, TResultN>
Użyj Results<TResult1, TResultN>
jako typu zwracanego programu obsługi punktów końcowych IResult
zamiast w następujących przypadkach:
- Z programu obsługi punktu końcowego jest zwracanych wiele
IResult
typów implementacji. - Klasa statyczna
TypedResult
służy do tworzeniaIResult
obiektów.
Ta alternatywa jest lepsza niż zwracanie IResult
, ponieważ typy związków ogólnych automatycznie zachowują metadane punktu końcowego. Results<TResult1, TResultN>
Ponieważ typy unii implementują niejawne operatory rzutowania, kompilator może automatycznie konwertować typy określone w argumentach ogólnych na wystąpienie typu unii.
Ma to dodatkową zaletę zapewnienia sprawdzania czasu kompilacji, że program obsługi tras rzeczywiście zwraca tylko wyniki, które deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych, aby spowodować Results<>
błąd kompilacji.
Rozważ następujący punkt końcowy, dla którego 400 BadRequest
jest zwracany kod stanu, gdy orderId
wartość jest większa niż 999
. W przeciwnym razie tworzy element 200 OK
z oczekiwaną zawartością.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Aby poprawnie udokumentować ten punkt końcowy, wywoływana jest metoda Produces
rozszerzenia. Jednak ponieważ TypedResults
pomocnik automatycznie zawiera metadane punktu końcowego, można zamiast tego zwrócić Results<T1, Tn>
typ unii, jak pokazano w poniższym kodzie.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Wbudowane wyniki
Typowe pomocniki wyników istnieją w Results klasach statycznych i .TypedResults Zwracanie jest preferowane TypedResults
do zwracania Results
wartości . Aby uzyskać więcej informacji, zobacz TypedResults vs Results (TypdResults a wyniki).
W poniższych sekcjach przedstawiono użycie typowych pomocników wyników.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync jest alternatywnym sposobem zwracania kodu JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Niestandardowy kod stanu
app.MapGet("/405", () => Results.StatusCode(405));
Wewnętrzny błąd serwera
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
Powyższy przykład zwraca kod stanu 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
przeciążenia umożliwiają dostęp do bazowego strumienia odpowiedzi HTTP bez buforowania. W poniższym przykładzie użyto elementu ImageSharp w celu zwrócenia mniejszego rozmiaru określonego obrazu:
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);
}
Poniższy przykład przesyła strumieniowo obraz z usługi 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");
});
Poniższy przykład przesyła strumieniowo wideo z obiektu blob platformy 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);
});
Przekierowanie
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Plik
app.MapGet("/download", () => Results.File("myfile.text"));
Interfejsy HttpResult
Następujące interfejsy w Microsoft.AspNetCore.Http przestrzeni nazw umożliwiają wykrywanie IResult
typu w czasie wykonywania, który jest typowym wzorcem implementacji filtrów:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Oto przykład filtru, który używa jednego z następujących interfejsów:
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
};
});
Aby uzyskać więcej informacji, zobacz Filtry w minimalnych aplikacjach interfejsu API i typach implementacji IResult.
Dostosowywanie odpowiedzi
Aplikacje mogą kontrolować odpowiedzi, implementując typ niestandardowy IResult . Poniższy kod jest przykładem typu wyniku 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);
}
}
Zalecamy dodanie metody rozszerzenia w celu Microsoft.AspNetCore.Http.IResultExtensions zwiększenia możliwości odnajdywania tych niestandardowych wyników.
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();
Ponadto typ niestandardowy IResult
może zapewnić własną adnotację przez zaimplementowanie interfejsu IEndpointMetadataProvider . Na przykład poniższy kod dodaje adnotację do poprzedniego HtmlResult
typu, który opisuje odpowiedź wygenerowaną przez punkt końcowy.
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());
}
}
Jest ProducesHtmlMetadata
to implementacja IProducesResponseTypeMetadata , która definiuje wygenerowany typ text/html
zawartości odpowiedzi i kod 200 OK
stanu .
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Alternatywnym podejściem jest użycie metody do Microsoft.AspNetCore.Mvc.ProducesAttribute opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata
, aby użyć ProducesAttribute
metody .
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Konfigurowanie opcji serializacji JSON
Domyślnie minimalne aplikacje interfejsu API używają Web defaults
opcji podczas serializacji i deserializacji JSON.
Globalne konfigurowanie opcji serializacji JSON
Opcje można skonfigurować globalnie dla aplikacji, wywołując ConfigureHttpJsonOptionsmetodę . Poniższy przykład zawiera pola publiczne i formaty danych wyjściowych 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
// }
Ponieważ pola są uwzględniane, powyższy kod odczytuje NameField
i zawiera je w wyjściowym formacie JSON.
Konfigurowanie opcji serializacji JSON dla punktu końcowego
Aby skonfigurować opcje serializacji dla punktu końcowego, wywołaj Results.Json i przekaż do niego JsonSerializerOptions obiekt, jak pokazano w poniższym przykładzie:
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
// }
Alternatywnie należy użyć przeciążenia WriteAsJsonAsync , które akceptuje JsonSerializerOptions obiekt. W poniższym przykładzie użyto tego przeciążenia do sformatowania wyjściowego kodu 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
// }
Dodatkowe zasoby
Minimalne punkty końcowe obsługują następujące typy zwracanych wartości:
string
— Dotyczy to równieżTask<string>
iValueTask<string>
.T
(Dowolny inny typ) — dotyczy to równieżTask<T>
elementów iValueTask<T>
.IResult
based — obejmuje toTask<IResult>
iValueTask<IResult>
.
string
zwracane wartości
Zachowanie | Typ zawartości |
---|---|
Struktura zapisuje ciąg bezpośrednio w odpowiedzi. | text/plain |
Rozważmy następującą procedurę Hello world
obsługi tras, która zwraca tekst.
app.MapGet("/hello", () => "Hello World");
Kod 200
stanu jest zwracany z nagłówkiem text/plain
Content-Type i następującą zawartością.
Hello World
T
(Dowolny inny typ) zwracane wartości
Zachowanie | Typ zawartości |
---|---|
Struktura JSON serializuje odpowiedź. | application/json |
Rozważmy następującą procedurę obsługi tras, która zwraca typ anonimowy zawierający Message
właściwość ciągu.
app.MapGet("/hello", () => new { Message = "Hello World" });
Kod 200
stanu jest zwracany z nagłówkiem application/json
Content-Type i następującą zawartością.
{"message":"Hello World"}
IResult
zwracane wartości
Zachowanie | Typ zawartości |
---|---|
Struktura wywołuje metodę IResult.ExecuteAsync. | Decyzja o wdrożeniu IResult . |
Interfejs IResult
definiuje kontrakt reprezentujący wynik punktu końcowego HTTP. Statyczna klasa Results i statyczny typdResults służą do tworzenia różnych obiektów reprezentujących różne IResult
typy odpowiedzi.
TypedResults a wyniki
Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults
jest typem równoważnej Results
klasie. Jednak Results
zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults
pomocnika jest jednym z IResult
typów implementacji. Różnica oznacza, że w przypadku Results
pomocników wymagana jest konwersja, gdy wymagany jest konkretny typ, na przykład do testowania jednostkowego. Typy implementacji są definiowane Microsoft.AspNetCore.Http.HttpResults w przestrzeni nazw.
Zwracanie TypedResults
zamiast Results
ma następujące zalety:
TypedResults
Pomocnicy zwracają silnie typizowane obiekty, co może zwiększyć czytelność kodu, testowanie jednostkowe i zmniejszyć prawdopodobieństwo błędów środowiska uruchomieniowego.- Typ implementacji automatycznie udostępnia metadane typu odpowiedzi dla interfejsu OpenAPI w celu opisania punktu końcowego.
Rozważ następujący punkt końcowy, dla którego 200 OK
jest generowany kod stanu z oczekiwaną odpowiedzią JSON.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Aby poprawnie udokumentować ten punkt końcowy, wywoływana jest metoda Produces
rozszerzeń. Nie jest jednak konieczne wywołanie Produces
metody , jeśli TypedResults
jest używana zamiast Results
, jak pokazano w poniższym kodzie. TypedResults
automatycznie udostępnia metadane punktu końcowego.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Aby uzyskać więcej informacji na temat opisywania typu odpowiedzi, zobacz Obsługa interfejsu OpenAPI w minimalnych interfejsach API.
Jak wspomniano wcześniej, w przypadku korzystania z programu TypedResults
konwersja nie jest wymagana. Rozważmy następujący minimalny interfejs API, który zwraca klasę TypedResults
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Następujące testy sprawdzają pełny typ konkretnego:
[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);
});
}
Ponieważ wszystkie metody zwracane Results
IResult
w podpisie, kompilator automatycznie wywnioskuje, że jako delegat żądania zwraca typ zwracany podczas zwracania różnych wyników z jednego punktu końcowego. TypedResults
wymaga użycia z Results<T1, TN>
takich delegatów.
Następująca metoda kompiluje się, ponieważ obie Results.Ok
metody i Results.NotFound
są deklarowane jako zwracane IResult
, mimo że rzeczywiste konkretne typy zwracanych obiektów są różne:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Poniższa metoda nie jest kompilowana, ponieważ i TypedResults.NotFound
są deklarowane jako zwracane różne typy, a TypedResults.Ok
kompilator nie podejmie próby wywnioskowania najlepszego zgodnego typu:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Aby użyć TypedResults
metody , typ zwracany musi być w pełni zadeklarowany, co w przypadku, gdy asynchroniczny wymaga Task<>
otoki. Użycie TypedResults
jest bardziej pełne, ale to kompromis w przypadku statycznego udostępniania informacji o typie i w związku z tym możliwość samodzielnego opisywania interfejsu 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());
Wyniki<TResult1, TResultN>
Użyj Results<TResult1, TResultN>
jako typu zwracanego programu obsługi punktów końcowych IResult
zamiast w następujących przypadkach:
- Z programu obsługi punktu końcowego jest zwracanych wiele
IResult
typów implementacji. - Klasa statyczna
TypedResult
służy do tworzeniaIResult
obiektów.
Ta alternatywa jest lepsza niż zwracanie IResult
, ponieważ typy związków ogólnych automatycznie zachowują metadane punktu końcowego. Results<TResult1, TResultN>
Ponieważ typy unii implementują niejawne operatory rzutowania, kompilator może automatycznie konwertować typy określone w argumentach ogólnych na wystąpienie typu unii.
Ma to dodatkową zaletę zapewnienia sprawdzania czasu kompilacji, że program obsługi tras rzeczywiście zwraca tylko wyniki, które deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych, aby spowodować Results<>
błąd kompilacji.
Rozważ następujący punkt końcowy, dla którego 400 BadRequest
jest zwracany kod stanu, gdy orderId
wartość jest większa niż 999
. W przeciwnym razie tworzy element 200 OK
z oczekiwaną zawartością.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Aby poprawnie udokumentować ten punkt końcowy, wywoływana jest metoda Produces
rozszerzenia. Jednak ponieważ TypedResults
pomocnik automatycznie zawiera metadane punktu końcowego, można zamiast tego zwrócić Results<T1, Tn>
typ unii, jak pokazano w poniższym kodzie.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Wbudowane wyniki
Typowe pomocniki wyników istnieją w Results klasach statycznych i .TypedResults Zwracanie jest preferowane TypedResults
do zwracania Results
wartości . Aby uzyskać więcej informacji, zobacz TypedResults vs Results (TypdResults a wyniki).
W poniższych sekcjach przedstawiono użycie typowych pomocników wyników.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync jest alternatywnym sposobem zwracania kodu JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Niestandardowy kod stanu
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
przeciążenia umożliwiają dostęp do bazowego strumienia odpowiedzi HTTP bez buforowania. W poniższym przykładzie użyto elementu ImageSharp w celu zwrócenia mniejszego rozmiaru określonego obrazu:
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);
}
Poniższy przykład przesyła strumieniowo obraz z usługi 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");
});
Poniższy przykład przesyła strumieniowo wideo z obiektu blob platformy 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);
});
Przekierowanie
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Plik
app.MapGet("/download", () => Results.File("myfile.text"));
Interfejsy HttpResult
Następujące interfejsy w Microsoft.AspNetCore.Http przestrzeni nazw umożliwiają wykrywanie IResult
typu w czasie wykonywania, który jest typowym wzorcem implementacji filtrów:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Oto przykład filtru, który używa jednego z następujących interfejsów:
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
};
});
Aby uzyskać więcej informacji, zobacz Filtry w minimalnych aplikacjach interfejsu API i typach implementacji IResult.
Dostosowywanie odpowiedzi
Aplikacje mogą kontrolować odpowiedzi, implementując typ niestandardowy IResult . Poniższy kod jest przykładem typu wyniku 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);
}
}
Zalecamy dodanie metody rozszerzenia w celu Microsoft.AspNetCore.Http.IResultExtensions zwiększenia możliwości odnajdywania tych niestandardowych wyników.
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();
Ponadto typ niestandardowy IResult
może zapewnić własną adnotację przez zaimplementowanie interfejsu IEndpointMetadataProvider . Na przykład poniższy kod dodaje adnotację do poprzedniego HtmlResult
typu, który opisuje odpowiedź wygenerowaną przez punkt końcowy.
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());
}
}
Jest ProducesHtmlMetadata
to implementacja IProducesResponseTypeMetadata , która definiuje wygenerowany typ text/html
zawartości odpowiedzi i kod 200 OK
stanu .
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Alternatywnym podejściem jest użycie metody do Microsoft.AspNetCore.Mvc.ProducesAttribute opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata
, aby użyć ProducesAttribute
metody .
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Konfigurowanie opcji serializacji JSON
Domyślnie minimalne aplikacje interfejsu API używają Web defaults
opcji podczas serializacji i deserializacji JSON.
Globalne konfigurowanie opcji serializacji JSON
Opcje można skonfigurować globalnie dla aplikacji, wywołując ConfigureHttpJsonOptionsmetodę . Poniższy przykład zawiera pola publiczne i formaty danych wyjściowych 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
// }
Ponieważ pola są uwzględniane, powyższy kod odczytuje NameField
i zawiera je w wyjściowym formacie JSON.
Konfigurowanie opcji serializacji JSON dla punktu końcowego
Aby skonfigurować opcje serializacji dla punktu końcowego, wywołaj Results.Json i przekaż do niego JsonSerializerOptions obiekt, jak pokazano w poniższym przykładzie:
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
// }
Alternatywnie należy użyć przeciążenia WriteAsJsonAsync , które akceptuje JsonSerializerOptions obiekt. W poniższym przykładzie użyto tego przeciążenia do sformatowania wyjściowego kodu 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
// }