ASP.NET Core: Cách tạo phản hồi (response) trong ứng dụng API tối thiểu


Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

Điểm cuối tối thiểu hỗ trợ các kiểu giá trị trả về sau:

  1. string - Bao gồm Task<string> và ValueTask<string>.
  2. T (Bất kỳ kiểu nào) - Bao gồm Task<T> và ValueTask<T>.
  3. IResult dựa trên - Bao gồm Task<IResult> và ValueTask<IResult>.

Giá trị trả về kiểu string 

Hành vi Loại nội dung
Framework viết chuỗi trực tiếp vào phản hồi. text/plain

Xem xét trình xử lý route sau, trả về văn bản là Hello world.

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

Mã trạng thái 200 được trả về với header Content-Type text/plain và nội dung sau.

Hello World

Trả về giá trị T (Bất kỳ kiểu nào)

Hành vi Loại nội dung
Framework JSON tuần tự hóa phản hồi. application/json

Hãy xem xét trình xử lý định tuyến sau, trình xử lý này trả về một kiểu ẩn danh có chứa property chuỗi Message.

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

Mã trạng thái 200 được trả về với header Content-Type application/json và nội dung sau.

{"message":"Hello World"}

Giá trị trả về kiểu IResult

Hành vi Loại nội dung
Framework gọi IResult.ExecuteAsync. Quyết định bởi việc thực thi IResult.

Interface IResult xác định một hợp đồng đại diện cho kết quả của một điểm cuối HTTP. Lớp Result tĩnh và TypedResults tĩnh  được sử dụng để tạo các đối tượng IResult khác nhau đại diện cho các loại phản hồi khác nhau.

TypedResults so với Results

Các lớp tĩnh Results và TypedResults cung cấp các trình trợ giúp kết quả tương tự. Lớp TypedResults là  tương đương với typed của lớp Results. Tuy nhiên, kiểu trả về của trình trợ giúp Results là IResult, trong khi mỗi kiểu trả về của trình trợ giúp TypedResults là một trong các kiểu thực thi IResult. Sự khác biệt có nghĩa là đối với trình trợ giúp Results, việc chuyển đổi là cần thiết khi cần kiểu cụ thể, ví dụ như để thử nghiệm đơn vị. Các kiểu thực thi được xác định trong namespace Microsoft.AspNetCore.Http.HttpResults.

Trả về TypedResults thay vì Results có những lợi thế sau:

Hãy xem xét điểm cuối sau đây, mã  200 OK trạng thái có phản hồi JSON dự kiến ​​được tạo.

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

Để ghi lại chính xác điểm cuối này, phương thức tiện ích mở rộng  Produces được gọi. Tuy nhiên, không cần thiết phải gọi  Produces if  TypedResults được sử dụng thay vì  Results, như được hiển thị trong đoạn mã sau. TypedResults tự động cung cấp siêu dữ liệu cho điểm cuối.

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

Để biết thêm thông tin về cách mô tả loại phản hồi, hãy xem  hỗ trợ OpenAPI trong các API tối thiểu .

Như đã đề cập trước đây, khi sử dụng  TypedResults, không cần chuyển đổi. Hãy xem xét API tối thiểu sau đây trả về một  TypedResults lớp

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

Phép thử sau đây kiểm tra loại bê tông đầy đủ:

[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);
    });
}

Bởi vì tất cả các phương thức được  Results trả về  IResult trong chữ ký của chúng, trình biên dịch sẽ tự động suy ra đó là kiểu trả về của đại biểu yêu cầu khi trả về các kết quả khác nhau từ một điểm cuối duy nhất. TypedResults yêu cầu sử dụng  Results<T1, TN> từ đại biểu như vậy.

Phương thức sau đây biên dịch vì cả  Results.Ok  và  Results.NotFound  đều được khai báo là return  IResult, mặc dù kiểu cụ thể thực tế của các đối tượng được trả về là khác nhau:

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

Phương thức sau đây không biên dịch, bởi vì  TypedResults.Ok và  TypedResults.NotFound được khai báo là trả về các kiểu khác nhau và trình biên dịch sẽ không cố suy ra kiểu phù hợp nhất:

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

Để sử dụng  TypedResults, kiểu trả về phải được khai báo đầy đủ, khi không đồng bộ yêu cầu  Task<> trình bao bọc. Việc sử dụng  TypedResults dài dòng hơn, nhưng đó là sự đánh đổi để có sẵn thông tin loại tĩnh và do đó có khả năng tự mô tả thành OpenAPI:

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

Kết quả<TResult1, TResultN>

Sử dụng  Kết quả<TResult1, TResultN>  làm loại trả về trình xử lý điểm cuối thay vì  IResult khi:

  • Nhiều  IResult loại triển khai được trả về từ trình xử lý điểm cuối.
  • Lớp tĩnh  TypedResult được sử dụng để tạo  IResult các đối tượng.

Phương án thay thế này tốt hơn là quay lại  IResult vì các loại kết hợp chung tự động giữ lại siêu dữ liệu điểm cuối. Và vì  Results<TResult1, TResultN> các kiểu kết hợp triển khai các toán tử ép kiểu ẩn, nên trình biên dịch có thể tự động chuyển đổi các kiểu được chỉ định trong các đối số chung thành một thể hiện của kiểu kết hợp.

Điều này có thêm lợi ích là cung cấp kiểm tra thời gian biên dịch rằng trình xử lý tuyến đường thực sự chỉ trả về kết quả mà nó tuyên bố. Cố gắng trả về một loại không được khai báo là một trong các đối số chung dẫn  Results<> đến lỗi biên dịch.

Hãy xem xét điểm cuối sau đây, mã  400 BadRequest trạng thái được trả về khi  orderId lớn hơn  999. Mặt khác, nó tạo ra một  200 OK nội dung mong đợi.

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

Để ghi lại chính xác điểm cuối này, phương thức mở rộng  Produces được gọi. Tuy nhiên, vì  TypedResults trình trợ giúp tự động bao gồm siêu dữ liệu cho điểm cuối, nên  Results<T1, Tn> thay vào đó, bạn có thể trả về loại kết hợp, như được minh họa trong đoạn mã sau.

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

Kết quả tích hợp

Trình trợ giúp kết quả phổ biến tồn tại trong  các lớp tĩnh Kết quả  và  TypedResults  . Quay lại  TypedResults được ưu tiên hơn quay lại  Results. Để biết thêm thông tin, hãy xem  TypedResults so với Kết quả .

Các phần sau đây minh họa cách sử dụng các trình trợ giúp kết quả phổ biến.

JSON

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

WriteAsJsonAsync  là một cách khác để trả về JSON:

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

Mã trạng thái tùy chỉnh

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

Chữ

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

Suối

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

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

app.Run();

Quá tải results.Stream  cho phép truy cập vào luồng phản hồi HTTP bên dưới mà không cần lưu vào bộ đệm. Ví dụ sau sử dụng  ImageSharp  để trả về kích thước đã giảm của hình ảnh đã chỉ định:

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);
}

Ví dụ sau truyền hình ảnh từ  bộ lưu trữ Azure Blob :

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");
});

Ví dụ sau truyền phát video từ Azure Blob:

// 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);
});

chuyển hướng

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

Tài liệu

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

Giao diện HttpResult

Các giao diện sau trong  không gian tên Microsoft.AspNetCore.Http  cung cấp một cách để phát hiện  IResult loại trong thời gian chạy, đây là mẫu phổ biến trong triển khai bộ lọc:

Dưới đây là ví dụ về bộ lọc sử dụng một trong các giao diện sau:

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
    };
});

Để biết thêm thông tin, hãy xem  Bộ lọc trong các ứng dụng API tối thiểu  và  các loại triển khai IResult .

tùy chỉnh câu trả lời

Các ứng dụng có thể kiểm soát phản hồi bằng cách triển khai  loại IResult tùy chỉnh  . Đoạn mã sau là một ví dụ về loại kết quả 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);
    }
}

Chúng tôi khuyên bạn nên thêm phương pháp tiện ích mở rộng vào  Microsoft.AspNetCore.Http.IResultExtensions  để làm cho các kết quả tùy chỉnh này dễ khám phá hơn.

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();

Ngoài ra, một  IResult loại tùy chỉnh có thể cung cấp chú thích của riêng nó bằng cách triển khai  giao diện IEndpointMetadataProvider  . Ví dụ: đoạn mã sau thêm chú thích vào  HtmlResult loại trước mô tả phản hồi do điểm cuối tạo ra.

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());
    }
}

Việc  ProducesHtmlMetadata triển khai  IProducesResponseTypeMetadata  xác định loại nội dung phản hồi được tạo  text/html và mã trạng thái  200 OK.

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

    public int StatusCode => 200;

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

Một cách tiếp cận khác là sử dụng  Microsoft.AspNetCore.Mvc.ProducesAttribute  để mô tả phản hồi được tạo ra. Đoạn mã sau thay đổi  PopulateMetadata phương thức sử dụng  ProducesAttribute.

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

Định cấu hình các tùy chọn tuần tự hóa JSON

Theo mặc định, các ứng dụng API tối thiểu sử dụng  các tùy chọn mặc định của Web  trong quá trình tuần tự hóa và giải tuần tự hóa JSON.

Định cấu hình các tùy chọn tuần tự hóa JSON trên toàn cầu

Các tùy chọn có thể được định cấu hình trên toàn cầu cho một ứng dụng bằng cách gọi  ConfigureHttpJsonOptions . Ví dụ sau bao gồm các trường công khai và định dạng đầu ra 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
// }

Vì các trường được bao gồm nên mã trước đó sẽ đọc  NameField và đưa nó vào JSON đầu ra.

Định cấu hình tùy chọn tuần tự hóa JSON cho điểm cuối

Để định cấu hình các tùy chọn tuần tự hóa cho một điểm cuối, hãy gọi  Results.Json  và truyền cho nó một  đối tượng JsonSerializerOptions  , như minh họa trong ví dụ sau:

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
// }

Thay vào đó, hãy sử dụng quá tải  WriteAsJsonAsync  chấp nhận  đối tượng JsonSerializerOptions  . Ví dụ sau sử dụng quá tải này để định dạng JSON đầu ra:

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
// }

Tài nguyên bổ sung

Nguồn: learn.microsoft.com
» Tiếp: Tạo API web với ASP.NET Core dựa trên controller
« Trước: Hướng dẫn tạo API tối thiểu với ASP.NET Core
Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!