Background Service trong ASP.NET Core

Background Service trong ASP.NET Core

ASP.NET Core giúp giảm bớt sự phức tạp của việc lưu trữ một ứng dụng web trong khi mang lại cho các nhà phát triển sức mạnh mà trước đây chúng ta chưa từng có. Chúng ta có thể host các ứng dụng ASP.NET Core trong các ứng dụng console, mang đến cho chúng ta một số kịch bản mới mẻ và thú vị.

Bài viết này sẽ chỉ cho chúng ta cách triển khai một background service bên trong ứng dụng ASP.NET Core và cách giao tiếp với background service của chúng ta từ một HTTP request.

Background Service là gì?

Hiện nay, .NET Core cung cấp interface IHostBuilder, cho phép ngay cả những ứng dụng console đơn giản nhất cũng có các tính năng mạnh mẽ như dependency injection, logging và nhiều thứ khác nữa như một phần của tích hợp thống nhất.

Những người làm việc với ASP.NET Core có thể sẽ quen thuộc với đoạn mã sau được tìm thấy trong tệp Program của ứng dụng web của họ.

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    private static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Background Service là một dịch vụ chạy không đồng bộ trong máy chủ ứng dụng console với ý tưởng nó không can thiệp vào tiến trình chính.

Các nhà phát triển ASP.NET có thể không nhận ra rằng ASP.NET Host là một background service (dịch vụ nền), nó chính xác là GenericWebHostService. Như chúng ta có thể đoán, trách nhiệm của dịch vụ này là bắt đầu lắng nghe các HTTP request đến và xử lý chúng thông qua đường dẫn của ASP.NET Core. Đây là một background service có sẵn mà chúng ta có thể đã sử dụng, nhưng còn việc tạo background service của riêng chúng ta thì sao?

Tạo background service bắt đầu bằng cách triển khai interface IHostedService, nhưng chúng ta có thể có một bước khởi đầu nâng cao hơn bằng cách kế thừa lớp BackgroundServiceđược tìm thấy trong namespace Microsoft.Extensions.Hosting. Hãy xem một triển khai trống được thiết kế để chạy một hành động lặp lại.

public class Worker: BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // keep looping until we get a cancellation request
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // todo: Add repeating code here
                // add a delay to not run in a tight loop
                await Task.Delay(1000, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // catch the cancellation exception
                // to stop execution
                return;
            }
        }
    }
}

Các thành phần thiết yếu của một background service là phương thức ExecuteAsync mà chúng ta phải override (ghi đè). Vì quá trình thực thi không đồng bộ (async), chúng ta có quyền truy cập CancellationToken, sẽ gửi tín hiệu khi quá trình ngừng hoạt động.

Điều quan trọng nữa là chúng ta sử dụng CancellationToken với tất cả các hoạt động không đồng bộ để có thể dừng hành động bất cứ lúc nào. Trong ví dụ của trên, chúng ta đang lặp lại mã của mình miễn là ứng dụng đang hoạt động.

Trong những trường hợp sử dụng cụ thể nào, chúng ta có thể sử dụng BackgroundService khi nào?

  • Gửi email bên ngoài luồng web.
  • Xác định các thay đổi đối với tài nguyên trạng thái.
  • Các tác vụ kéo dài, chẳng hạn như xử lý hàng đợi dữ liệu.

Nhìn chung, một instance của BackgroundService cho phép chúng ta có thể thực hiện các tác vụ song song mà không cần sự tương tác trực tiếp của người dùng.

Để đăng ký instance của một BackgroundService, chúng ta cần sửa đổi phương thức CreateHostBuilder có trong class Program.

private static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Chúng ta sẽ nhận thấy rằng chúng ta đã thêm một cuộc gọi mới tới phương thức ConfigureServices ở cuối phương thức CreateDefaultBuilder. Chúng ta có thể thêm kiểu Worker mới của mình trong lệnh gọi trình xây dựng này bằng cách gọi phương thức AddHostedService.

Tất cả các background service được đăng kýSingeltonxử lý thông qua interfaceIHostedService.

Chúng ta sẽ cần nhớ điều đó cho phần tiếp theo.

Làm việc với Background Service trong ASP.NET Core

Trong khi service BackgroundServicechạy không đồng bộ trong nền, điều đó không có nghĩa là chúng ta không thể truy cập nó từ một HTTP request. Vì tất cả các background service của chúng ta, bao gồm cả các background service của ASP.NET Core, đều chạy trong cùng một host, chúng cũng chia sẻ cùng một IServiceCollection.

Bay giờ chúng ta hãy viết một service Worker mới để lưu trữ biểu tượng cảm xúc của người dùng khi họ truy cập thông qua HTTP request và in chúng ra đầu ra của console.

public class Worker : BackgroundService
{
    private static List<string> Emojis { get; } = new() { "😀" };

    public string Html => string.Join(
        string.Empty, 
        Emojis.Select(e => HtmlEncoder.Default.Encode(e))
    );

    private string Output =>
        string.Join(string.Empty, Emojis);
    
    public void AddEmoji(string emoji) => Emojis.Add(emoji);
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                Console.WriteLine($"{DateTime.Now:u}: Hello, {Output}");
                await Task.Delay(1000, stoppingToken);
            }
            catch (OperationCanceledException )
            {
                return;
            }
        }
    }
}

Hãy nhớ rằng tất cả các background service đều là singleton, vì vậy chúng phải quản lý trạng thái và các phần phụ thuộc của chúng. Chúng ta có thể truyền các giá trị bằng cách chèn vào phương thức khởi tạo, nhưng chúng ta cần nhận ra rằng những phụ thuộc đó sẽ tồn tại trong vòng đời của ứng dụng. Trong ví dụ này, service Workersẽ quản lý một property Emojis kiểu List<string> để lưu trữ tất cả các biểu tượng cảm xúc.

Vui lòng không sử dụng ví dụ này trong môi trường production, vì danh sách không bị ràng buộc kích thước sẽ gây rò rỉ bộ nhớ (memory leak).

Trước khi chúng ta đi sâu vào endpoint của ASP.NET Core, chúng ta hãy thảo luận về cách chúng ta sẽ truy cập background service từ IServiceCollectionthông qua các interface IServiceProvider. Chúng ta biết những gì?

  • Có thể có nhiều instance của IHostedService.
  • Chúng được đăng ký bằng interface IHostedService.
  • Chúng đều là singleton.

Sau khi biết được những điều này, chúng ta hãy viết một phương thức mở rộng để truy xuất kiểu cụ thể của chúng ta Workerdễ chịu hơn.

public static class ServiceProviderExtensions
{
    public static TWorkerType GetHostedService<TWorkerType>
        (this IServiceProvider serviceProvider) =>
        serviceProvider
            .GetServices<IHostedService>()
            .OfType<TWorkerType>()
            .FirstOrDefault();
}

Chúng ta cần truy xuất tất cả các instance của interface IHostedServicevà sau đó lọc chúng bằng sử dụng phương thức mở rộng OfType của LINQ. Hãy tạo một vài endpoint ASP.NET Core.

endpoints.MapGet("/", async context =>
{
    var worker = context.RequestServices.GetHostedService<Worker>();
    context.Response.ContentType = "text/html";
    await context.Response.WriteAsync($"<h1>Hello {worker?.Html}!</h1>");
});

Ở đoạn code trên, tại endpoint / có phương thức GET chúng ta lấy instance của service Workervà xuất ra phản hồi cho client. Làm sao để nhận dữ liệu đầu vào từ người dùng? Ở đây chúng ta sẽ triển khai một endpoint có phương thức POST.

endpoints.MapPost("/", async context =>
{
    var emoji = context.Request.Form["emoji"].FirstOrDefault();
    if (emoji is not null)
    {
        var worker = context.RequestServices.GetHostedService<Worker>(); 
        if (worker is not null)
        {
            worker.AddEmoji(emoji);
            context.Response.StatusCode = (int) HttpStatusCode.NoContent;
            return;
        }
    }
    
    context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
});

Vì service Worker của chúng ta là singleton, chúng ta có thể thêm các phần tử vào danh sách nội bộ bằng cách sử dụng phương thức AddEmoji.

Khi xây dựng một background service như thế này, sẽ tốt hơn nếu sử dụng một cơ chế lưu trữ lâu dài như SQL Server, PostgreSQL hoặc thậm chí là SQLite. Hãy xem kết quả từ trình duyệt của chúng ta.

Làm việc với Background Service trong ASP.NET Core

Điều gì về đầu ra console của chúng ta?

Làm việc với Background Service trong ASP.NET Core

Hãy thực hiện một POST request đến endpoint / của chúng ta.

Làm việc với Background Service trong ASP.NET Core

Như chúng ta có thể thấy, cả đầu ra HTML và đầu ra console của chúng ta đều đã thay đổi. Đầu tiên, hãy xem đầu ra HTML.

Làm việc với Background Service trong ASP.NET Core

Bây giờ đầu ra console được cập nhật.

Làm việc với Background Service trong ASP.NET Core

Phần kết luận

Interface IHostedService là một phần không thể thiếu trong những kinh nghiệm lập trình .NET, dù chúng ta có nhận ra hay không. Chúng ta có thể triển khai BackgroundServicevà đăng ký nó cùng với dịch vụ máy chủ ASP.NET Core.

Các background service của chúng ta chia sẻ tài nguyên giống như các background service có sẵn của ASP.NET Core, chúng ta có thể truy cập các background service của mình bằng cách sử dụng một phương thức mở rộng đơn giản để truy xuất instance của chúng chính xác. Kết hợp những kiến ​​thức này lại với nhau, chúng ta có thể làm được một số điều khá hay.

Tôi hy vọng bạn thích bài viết trên blog này và học được điều gì đó mới. Tôi sẽ đánh giá cao bạn chia sẻ bài viết này hoặc để lại bình luận bên dưới. Như mọi khi, cảm ơn vì đã đọc.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *