Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Uwaga
Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu .NET 9.
Ostrzeżenie
Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu .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 .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>
iValueTask<T>
. -
IResult
oparty — 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ę obsługi ścieżki, która zwraca tekst Hello world
.
app.MapGet("/hello", () => "Hello World");
Kod stanu 200
jest zwracany wraz z nagłówkiem Content-Type text/plain
oraz 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 stanu 200
jest zwracany wraz z nagłówkiem Content-Type application/json
oraz 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 kontra wyniki
Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults
jest typowanym odpowiednikiem klasy Results
. Jednak Results
zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults
pomocnika jest jednym z IResult
typów implementacji. Różnica polega na tym, że dla Results
pomocników konieczna jest konwersja, gdy potrzebny jest konkretny typ, na przykład podczas testowania jednostkowego. Typy implementacji są definiowane w przestrzeni nazw Microsoft.AspNetCore.Http.HttpResults.
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 generowany jest kod stanu 200 OK
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 rozszerzeń Produces
. 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 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ący test sprawdza pełny typ konkretnego obiektu.
[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 w Results
zwracają IResult
w swojej sygnaturze, kompilator automatycznie wywnioskuje, że delegat żądania ma taki typ zwracany, gdy zwracane są różne wyniki z jednego punktu końcowego.
TypedResults
wymaga użycia Results<T1, TN>
przez takich delegatów.
Następująca metoda kompiluje się, ponieważ zarówno Results.Ok
, jak i Results.NotFound
są zadeklarowane jako zwracające 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ż TypedResults.Ok
i TypedResults.NotFound
są zadeklarowane jako zwracające różne typy, a 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
, typ zwracany musi być w pełni zadeklarowany. Gdy metoda jest asynchroniczna, deklaracja wymaga opakowania typu zwracanego w Task<>
. Użycie TypedResults
jest bardziej rozbudowane, ale to jest kompromis związany z tym, że informacje o typie są statycznie dostępne i mogą samodzielnie opisywać interfejs 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 zamiast IResult
w następujących przypadkach:
- Obsługiwacz punktu końcowego zwraca 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ę umożliwienia sprawdzenia w czasie kompilacji, czy handler trasy rzeczywiście zwraca wyłącznie wyniki, które faktycznie deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych Results<>
, skutkuje błędem kompilacji.
Rozważ następujący punkt końcowy, dla którego kod stanu 400 BadRequest
jest zwracany, gdy orderId
jest większe 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 udokumentować ten punkt końcowy poprawnie, wywoływana jest metoda rozszerzenia Produces
. 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 rezultaty
Typowe pomocniki wyników istnieją w klasach statycznych Results i TypedResults. Zwracanie TypedResults
jest preferowane od zwracania Results
. Aby uzyskać więcej informacji, zobacz TypedResults vs Results.
W poniższych sekcjach przedstawiono użycie typowych pomocników wyników.
JSON (JavaScript Object Notation)
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.
Problem i walidacja problemu
app.MapGet("/problem", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
return TypedResults.Problem("This is an error with extensions",
extensions: extensions);
});
Tekst
app.MapGet("/text", () => Results.Text("This is some text"));
Strumień
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);
});
zdarzenia Server-Sent (SSE)
Interfejs API TypedResults.ServerSentEvents obsługuje zwracanie wyniku ServerSentEvents .
Server-Sent Events to technologia serwerowa typu push, która umożliwia serwerowi wysyłanie strumienia wiadomości o zdarzeniach do klienta za pośrednictwem jednego połączenia HTTP. Na platformie .NET komunikaty o zdarzeniach są reprezentowane jako SseItem<T>
obiekty, które mogą zawierać typ zdarzenia, identyfikator i ładunek danych typu T
.
Klasa TypedResults ma metodę statyczną o nazwie ServerSentEvents , która może służyć do zwracania ServerSentEvents
wyniku. Pierwszym parametrem tej metody jest to IAsyncEnumerable<SseItem<T>>
, który reprezentuje strumień komunikatów zdarzeń, które mają być wysyłane do klienta.
W poniższym przykładzie pokazano, jak używać interfejsu TypedResults.ServerSentEvents
API do zwracania strumienia zdarzeń tętna jako obiektów JSON do klienta:
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));
});
Aby uzyskać więcej informacji, zobacz przykładową aplikację Minimal API zwracającą strumień zdarzeń tętna jako ciąg, TypedResults.ServerSentEvents
oraz obiekty JSON do klienta.
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.
Modyfikowanie nagłówków
Użyj obiektu HttpResponse
, aby zmodyfikować nagłówki odpowiedzi:
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";
});
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 do Microsoft.AspNetCore.Http.IResultExtensions, aby zwiększyć 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
implementacją IProducesResponseTypeMetadata, która definiuje typ zawartości odpowiedzi text/html
i kod stanu 200 OK
.
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 Microsoft.AspNetCore.Mvc.ProducesAttribute do opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata
, aby używać ProducesAttribute
.
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 ConfigureHttpJsonOptions. 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 akceptującego obiekt JsonSerializerOptions. 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>
iValueTask<T>
. -
IResult
oparty — 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ę obsługi ścieżki, która zwraca tekst Hello world
.
app.MapGet("/hello", () => "Hello World");
Kod stanu 200
jest zwracany wraz z nagłówkiem Content-Type text/plain
oraz 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 stanu 200
jest zwracany wraz z nagłówkiem Content-Type application/json
oraz 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 kontra wyniki
Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults
jest typowanym odpowiednikiem klasy Results
. Jednak Results
zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults
pomocnika jest jednym z IResult
typów implementacji. Różnica polega na tym, że dla Results
pomocników konieczna jest konwersja, gdy potrzebny jest konkretny typ, na przykład podczas testowania jednostkowego. Typy implementacji są definiowane w przestrzeni nazw Microsoft.AspNetCore.Http.HttpResults.
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 generowany jest kod stanu 200 OK
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 rozszerzeń Produces
. 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 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ący test sprawdza pełny typ konkretnego obiektu.
[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 w Results
zwracają IResult
w swojej sygnaturze, kompilator automatycznie wywnioskuje, że delegat żądania ma taki typ zwracany, gdy zwracane są różne wyniki z jednego punktu końcowego.
TypedResults
wymaga użycia Results<T1, TN>
przez takich delegatów.
Następująca metoda kompiluje się, ponieważ zarówno Results.Ok
, jak i Results.NotFound
są zadeklarowane jako zwracające 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ż TypedResults.Ok
i TypedResults.NotFound
są zadeklarowane jako zwracające różne typy, a 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
, typ zwracany musi być w pełni zadeklarowany, co, jeśli jest asynchroniczny, wymaga Task<>
otoki. Użycie TypedResults
jest bardziej rozbudowane, ale to jest kompromis związany z tym, że informacje o typie są statycznie dostępne i mogą samodzielnie opisywać interfejs 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 zamiast IResult
w następujących przypadkach:
- Obsługiwacz punktu końcowego zwraca 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ę umożliwienia sprawdzenia w czasie kompilacji, czy handler trasy rzeczywiście zwraca wyłącznie wyniki, które faktycznie deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych Results<>
, skutkuje błędem kompilacji.
Rozważ następujący punkt końcowy, dla którego kod stanu 400 BadRequest
jest zwracany, gdy orderId
jest większe 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 udokumentować ten punkt końcowy poprawnie, wywoływana jest metoda rozszerzenia Produces
. 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 rezultaty
Typowe pomocniki wyników istnieją w klasach statycznych Results i TypedResults. Zwracanie TypedResults
jest preferowane od zwracania Results
. Aby uzyskać więcej informacji, zobacz TypedResults vs Results.
W poniższych sekcjach przedstawiono użycie typowych pomocników wyników.
JSON (JavaScript Object Notation)
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.
Problem i walidacja problemu
app.MapGet("/problem", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
return TypedResults.Problem("This is an error with extensions",
extensions: extensions);
});
Tekst
app.MapGet("/text", () => Results.Text("This is some text"));
Strumień
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.
Modyfikowanie nagłówków
Użyj obiektu HttpResponse
, aby zmodyfikować nagłówki odpowiedzi:
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";
});
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 do Microsoft.AspNetCore.Http.IResultExtensions, aby zwiększyć 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
implementacją IProducesResponseTypeMetadata, która definiuje typ zawartości odpowiedzi text/html
i kod stanu 200 OK
.
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 Microsoft.AspNetCore.Mvc.ProducesAttribute do opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata
, aby używać ProducesAttribute
.
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 ConfigureHttpJsonOptions. 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 akceptującego obiekt JsonSerializerOptions. 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>
iValueTask<T>
. -
IResult
oparty — 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ę obsługi ścieżki, która zwraca tekst Hello world
.
app.MapGet("/hello", () => "Hello World");
Kod stanu 200
jest zwracany wraz z nagłówkiem Content-Type text/plain
oraz 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 stanu 200
jest zwracany wraz z nagłówkiem Content-Type application/json
oraz 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 kontra wyniki
Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults
jest typowanym odpowiednikiem klasy Results
. Jednak Results
zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults
pomocnika jest jednym z IResult
typów implementacji. Różnica polega na tym, że dla Results
pomocników konieczna jest konwersja, gdy potrzebny jest konkretny typ, na przykład podczas testowania jednostkowego. Typy implementacji są definiowane w przestrzeni nazw Microsoft.AspNetCore.Http.HttpResults.
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 generowany jest kod stanu 200 OK
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 rozszerzeń Produces
. 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 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ący test sprawdza pełny typ konkretnego obiektu.
[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 w Results
zwracają IResult
w swojej sygnaturze, kompilator automatycznie wywnioskuje, że delegat żądania ma taki typ zwracany, gdy zwracane są różne wyniki z jednego punktu końcowego.
TypedResults
wymaga użycia Results<T1, TN>
przez takich delegatów.
Następująca metoda kompiluje się, ponieważ zarówno Results.Ok
, jak i Results.NotFound
są zadeklarowane jako zwracające 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ż TypedResults.Ok
i TypedResults.NotFound
są zadeklarowane jako zwracające różne typy, a 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
, typ zwracany musi być w pełni zadeklarowany, co, jeśli jest asynchroniczny, wymaga Task<>
otoki. Użycie TypedResults
jest bardziej rozbudowane, ale to jest kompromis związany z tym, że informacje o typie są statycznie dostępne i mogą samodzielnie opisywać interfejs 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 zamiast IResult
w następujących przypadkach:
- Obsługiwacz punktu końcowego zwraca 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ę umożliwienia sprawdzenia w czasie kompilacji, czy handler trasy rzeczywiście zwraca wyłącznie wyniki, które faktycznie deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych Results<>
, skutkuje błędem kompilacji.
Rozważ następujący punkt końcowy, dla którego kod stanu 400 BadRequest
jest zwracany, gdy orderId
jest większe 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 udokumentować ten punkt końcowy poprawnie, wywoływana jest metoda rozszerzenia Produces
. 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 rezultaty
Typowe pomocniki wyników istnieją w klasach statycznych Results i TypedResults. Zwracanie TypedResults
jest preferowane od zwracania Results
. Aby uzyskać więcej informacji, zobacz TypedResults vs Results.
W poniższych sekcjach przedstawiono użycie typowych pomocników wyników.
JSON (JavaScript Object Notation)
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));
Tekst
app.MapGet("/text", () => Results.Text("This is some text"));
Strumień
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 do Microsoft.AspNetCore.Http.IResultExtensions, aby zwiększyć 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
implementacją IProducesResponseTypeMetadata, która definiuje typ zawartości odpowiedzi text/html
i kod stanu 200 OK
.
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 Microsoft.AspNetCore.Mvc.ProducesAttribute do opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata
, aby używać ProducesAttribute
.
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 ConfigureHttpJsonOptions. 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 akceptującego obiekt JsonSerializerOptions. 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
// }