Dependency injection trong ASP.NET Core

2022-08-07 10:40:56 | khoảng 28 phút đọc

Xin chào các bạn, mình là Huy, một developer. Trong bài học này chúng ta sẽ cùng tìm hiểu về một thuật ngữ phổ biến trong lập trình hướng đối tượng đó chính là Dependency injection và xem cách mà nó được áp dụng trong một dự án ASP.NET Core như thế nào. Bài viết này đòi hỏi các bạn có kiến thức nền tảng về lập trình hướng đối tượng vì trong bài có một số thuật ngữ đặc thù, mình sẽ cố gắng diễn giải để các bạn dễ tiếp cận.

Dependency injection (DI) là một kỹ thuật dùng để đạt được Inversion of Control (IoC)1 giữa các lớp và các phụ thuộc của chúng.

Trước tiên để bắt đầu bài học này, chúng ta sẽ khởi tạo một project ASP.NET Core web app với tên là dependencyinjection. Bạn có thể tạo bằng Visual Studio hoặc .NET CLI như sau:

dotnet new webapp -o dependencyinjection

Tổng quan về Dependency injection

Một dependency là một đối tượng mà đối tượng khác phụ thuộc vào.

Giả sử dự án của chúng ta có một service dùng để gửi thông báo trên terminal. Chúng ta tạo folder Services và file Services/ConsoleNotification.cs ngay tại thư mục gốc của dự án.

Xem xét lớp ConsoleNotification, nó chứa phương thức SendMessage là thứ mà các class khác trong ứng dụng phụ thuộc vào.

public class ConsoleNotification
{
public void SendMessage(string message)
{
Console.WriteLine($"ConsoleNotification.WriteMessage called. Message: {message}");
}
}

Tiếp theo chúng ta mở file Pages/Index.cshtml.cs và chỉnh sửa code như sau:

public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
 
+ private readonly ConsoleNotification _notification = new();
 
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
 
public void OnGet()
{
+ _notification.SendMessage("IndexModel.OnGet");
}
}

Chúng ta chạy dự án và truy cập vào địa chỉ https://localhost:{port}, sau đó quay lại terminal để xem kết quả thu được.

ConsoleNotification.WriteMessage called. Message: IndexModel.OnGet

Source code: Dependency

Class trên tạo và trực tiếp phụ thuộc vào class ConsoleNotification. Việc làm này là có vấn đề và nên tránh bởi một số lý do sau:

  • Để thay đổi ConsoleNotification với một triển khai khác, IndexModel phải thay đổi.
  • Nếu ConsoleNotification có phụ thuộc các dependency khác, chúng phải được cấu hình ở class IndexModel. Trong một dự án lớn với nhiều class phụ thuộc vào ConsoleNotification, việc cấu hình này rải rác khắp nơi trong ứng dụng.
  • Triển khai này khó để thực hiện unit test.

Dependency injection giải quyết những vấn đề này thông qua:

  • Sử dụng một interface hoặc một base class để abstract các dependency implementation.
  • Đăng ký các dependency trong một service provider. ASP.NET Core cung cấp một service provider tích hợp sẵn, đó là IServiceProvider. Các service thường được đăng ký tại file Program.cs trong ứng dụng.
  • Injection các service đến constructor của những class muốn sử dụng nó. Framework sẽ đảm nhận trách nhiệm khởi tạo các dependency và loại bỏ nó khi không cần thiết nữa.

Vậy chúng ta giải quyết việc này như thế nào? Các bạn tiếp tục theo dõi nhé.

Từ ví dụ trên, chúng ta sẽ định nghĩa một interface INotification chứa method SendMessage. File này sẽ được nằm trong thư mục Interfaces.

public interface INotification
{
public void SendMessager(string message);
}

Interface này sẽ được implement bởi ConsoleNotification.

-public class ConsoleNotification
+public class ConsoleNotification : INotification
{
public void SendMessage(string message)
{
Console.WriteLine($"ConsoleNotification.WriteMessage called. Message: {message}");
}
}

Tiếp đến chúng ta đăng ký service INofication với kiểu cụ thể là ConsoleNotification. Việc này thường được thực hiện trong file Program.cs.

var builder = WebApplication.CreateBuilder(args);
 
// Add services to the container.
builder.Services.AddRazorPages();
+builder.Services.AddScoped<INotification, ConsoleNotification>();
 
var app = builder.Build();

Bây giờ ta quay lại file Pages/Index.cshtml.cs và sử dụng kỹ thuật dependency injection.

-private readonly ConsoleNotification _notification = new();
+private readonly INotification _notification;
 
-public IndexModel(ILogger<IndexModel> logger)
+public IndexModel(ILogger<IndexModel> logger, INotification notification)
{
_logger = logger;
+ _notification = notification;
}

Chạy lại ứng dụng và ta vẫn nhận được kết quả như ban đầu, nhưng là với kỹ thuật dependency injection.

ConsoleNotification.WriteMessage called. Message: IndexModel.OnGet

Với DI pattern, các controller hoặc Razor Page:

  • Không cần sử dụng loại cụ thể ConsoleNotification, chỉ cần dùng INotification, điều này thật dễ dàng để thay đổi lớp triển khai mà không cần phải thay đổi code trong controller hoặc Razor Page.
  • Không cần khởi tạo class ConsoleNotification, nó đã được tạo trong DI container.

Lớp triển khai của interface INotification có thể nâng cấp bằng cách sử dụng trình logging được tích hợp sẵn.

Giả sử bây giờ hệ thống muốn gửi thông báo qua kênh log, vì vậy chúng ta tạo thêm một kênh thông báo nữa là LoggingNotification, class này sẽ có một dependency là ILogger. Các bạn tạo file Services/LoggingNotification.cs với đoạn code sau:

public class LoggingNotification : INotification
{
private readonly ILogger<LoggingNotification> _logger;
 
public LoggingNotification(ILogger<LoggingNotification> logger)
{
_logger = logger;
}
 
public void SendMessage(string message)
{
_logger.LogInformation($"LoggingNotification.SendMessage called. Message: {message}");
}
}

Nếu nhiều bạn chưa quen thì ngay bây giờ có thể đã "sốt sắn" vào chỉnh sửa lại code ở file Pages/Index.cshtml.cs. Nhưng với DI pattern, chúng ta chỉ cần vào file Program.cs và thay đổi thành class LoggingNotification mà interface INotification sẽ triển khai.

-builder.Services.AddScoped<INotification, Console.logNotification>();
+builder.Services.AddScoped<INotification, LoggingNotification>();

Kết quả nhận được ở màn hình terminal đã thay đổi thành:

LoggingNotification.SendMessage called. Message: IndexModel.OnGet

Source code: Dependency injection

Trong một dự án thực tế, thì phương pháp DI này thực hiện theo kiểu nối chuỗi không có gì lạ. Mỗi dependency được yêu cầu sẽ gọi các dependency của riêng nó. Container có nhiệm vụ resolve các dependency và trả về các service đã được resolve đầy đủ. Tập hợp các dependency cần được resolve thường được gọi bằng các tên thuật ngữ như dependency tree, dependency graph, hoặc object graph.

Trong thuật ngữ dependency injection, một service:

  • Nó thường là một đối tượng cung cấp một dịch vụ cho những đối tượng khác, chẳng hạn như INotification, cung cấp dịch vụ gửi thông báo cho các đối tượng nào phụ thuộc nó.
  • Nó không liên quan đến một web service, mặc dù dịch vụ đó có thể sử dụng web service.

Đăng ký nhóm service với extension method

ASP.NET Core framework sử dụng một quy ước để đăng ký một nhóm service liên quan với nhau. Quy ước này sử dụng một extension method Add{GROUP_NAME} để đăng ký tất cả các service được yêu cầu bởi tính năng framework. Chẳng hạn, extendsion method AddControllers đăng ký các service bắt buộc cho MVC Controller.

Đoạn code bên dưới tạo mởi Razor Page template, sử dụng trình tài khoản cá nhân và hiển thị cách thêm các service vào container bằng extension method AddDbContextAddDefaultIdentity:

var builder = WebApplication.CreateBuilder(args);
 
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
 
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
 
builder.Services.AddRazorPages();

Giờ chúng ta hãy thử tạo một extension method để đăng ký dịch vụ gửi thông báo từ ví dụ trước. Tại đường dẫn thư mục gốc của dự án, chúng ta tạo file MyServicesCollectionExtensions.cs với nội dung như sau:

1using dependencyinjection.Interfaces;
2using dependencyinjection.Services;
3 
4namespace Microsoft.Extensions.DependencyInjection
5{
6 public static class MyServicesCollectionExtensions
7 {
8 public static IServiceCollection AddServices(this IServiceCollection services)
9 {
10 services.AddScoped<INotification, LoggingNotification>();
11 
12 return services;
13 }
14 }
15}

ASP.NET khuyến khích viết các extension method cho việc đăng ký nhóm service tuân theo quy ước của namespace Microsoft.Extensions.DependencyInjection. Việc này giúp:

  • Dễ dàng đóng gói các đăng ký dịch vụ.
  • Cung cấp quy ước để IDE dễ dàng gợi ý cho chúng ta khi code.

Tiếp đến ta chỉ cần chỉnh lại việc đăng ký service bằng cách sử dụng extension method vừa định nghịa trong file Program.cs:

-builder.Services.AddScoped<INotification, LoggingNotification>();
+builder.Services.AddServices();

Sau đó chúng ta chạy lại ứng dụng và kiểm tra kết quả.

LoggingNotification.SendMessage called. Message: IndexModel.OnGet

Source code: Đăng ký nhóm service với extension method

Thời gian sống của service

Một service có thể được đăng ký với một trong các thời gian sống dưới đây:

  • Transient
  • Scoped
  • Singleton

Transient

Thời gian sống Transient được tạo mỗi lần chúng được yêu cầu từ phía service container. Loại này phù hợp cho các service nhẹ, không có trạng thái. Trong các ứng dụng xử lý request, các Transient service được hủy bỏ sau khi request kết thúc.

Để đăng ký một Trasient service, ta sử dụng method AddTransient.

Lấy lại ví dụ trước, các bạn vào file MyServicesCollectionExtensions.cs và chỉnh sửa lại như sau:

-services.AddScoped<INotification, LoggingNotification>();
+services.AddTransient<INotification, LoggingNotification>();

Sau đó chúng ta cần thêm đoạn code này vào file Program.cs để tracking quá trình yêu cầu service từ phía container.

builder.Services.AddRazorPages();
builder.Services.AddServices();
 
+// Service lifetimes testing
+var provider = builder.Services.BuildServiceProvider();
+for (int n = 0; n < 2; n++) {
+ using (var scope = provider.CreateScope()) {
+ Console.WriteLine($"Scope: {n}");
+ 
+ for (int i = 0; i < 5; i++)
+ {
+ var service = scope.ServiceProvider.GetRequiredService<INotification>();
+ Console.WriteLine($"\tRequest {i}: {service.GetHashCode()}");
+ }
+ }
+}
 
var app = builder.Build();

Đoạn code trên sẽ yêu cầu service INotification từ service container thông qua scope chỉ định. Mỗi scope sẽ làm mới service container, giúp việc kiểm thử của chúng ta trực quan và chính xác hơn.

Chạy lại ứng dụng và xem kết quả:

Scope: 0
Request 0: 567760
Request 1: 5109846
Request 2: 45988614
Request 3: 11244347
Request 4: 34090260
Scope: 1
Request 0: 9847715
Request 1: 21520579
Request 2: 59467483
Request 3: 65445301
Request 4: 52136803

Phân tích kết quả thu được, ta thấy trong mỗi request, hay thậm chí mỗi scope, đối tượng khởi tạo của service INotification đều có mã định danh khác nhau. Như vậy chứng minh được rằng các Transisent service đều được khởi tạo mới khi có yêu cầu từ service container.

Scoped

Trong ứng dụng web, thời gian sống Scoped được xác định là những service được tạo một lần trên một request (kết nối). Đăng ký một Scoped service bằng cách sử dụng method AddScoped. Trong các ứng dụng xử lý request, các Scoped service được hủy bỏ sau khi request kết thúc.

Khi làm việc với Entity Framework Core, extension method AddDbContext đăng ký các kiểu DbContext mặc định với thời gian sống là Scoped.

Quay trở lại với ví dụ trên, tại file MyServicesCollectionExtensions.cs ta chỉnh sửa từ Transient sang Scoped như sau:

-services.AddTransient<INotification, LoggingNotification>();
+services.AddScoped<INotification, LoggingNotification>();

Sau đó chúng ta sẽ chạy ứng dụng, kiểm tra kết quả từ đoạn code testing ở file Program.cs.

Scope: 0
Request 0: 567760
Request 1: 567760
Request 2: 567760
Request 3: 567760
Request 4: 567760
Scope: 1
Request 0: 45988614
Request 1: 45988614
Request 2: 45988614
Request 3: 45988614
Request 4: 45988614

Như các bạn quan sát được ở kết quả trên, những lần yêu cầu Scoped service trong một một service đều trả về cùng một đối tượng khởi tạo, ta có thể biết được điều đó vì chúng có cùng mã định danh. Nhưng qua đến scope 1, các mã định dạnh này tuy vẫn giống nhau qua những lần yêu cầu như nó đã khác đi so với scope 0. Từ đó ta có thể kết luận được rằng với các Scoped service, đối tượng khởi tạo được tạo mới trong scope mới, hay nói cách khác là được tạo một lần trên mỗi request.

Singleton

Các Singleton service được tạo ra:

  • Lần đầu chúng được yêu cầu.
  • Bởi nhà phát triển, khi họ cung cấp một đối tượng triển khai trực tiếp vào container. Cách này thường hiếm khi gặp.

Mỗi yêu cầu đến service container để lấy các Singleton service đều được sử dụng cùng một khởi tạo. Nếu ứng dụng yêu cầu Singleton, hãy để service container quản lý thời gian sống của các service này. không triển khai Singleton pattern và cung cấp code để hủy bỏ các Singleton. Các service được resolve từ service container không nên được hủy bỏ bởi code. Nếu một loại hoặc factory được đăng ký như một Singleton, service container sẽ tự động hủy bỏ nó.

Đăng ký Singleton service với method AddSingleton. Singleton service phải có luồng an toàn và thường được sử dụng như các service không trạng thái.

Trong ứng dụng xử lý request, Singleton service được hủy bỏ khi ServiceProvider bị hủy bỏ do ứng dụng bị tắt đi, bởi bộ nhớ không được giải phóng cho đến khi ứng dụng tắt.

Để rõ hơn trong cách hoạt động của các Singleton service, chúng ta quay lại với file MyServicesCollectionExtensions.cs và chỉnh phương thức đăng ký thành AddSingleton.

-services.AddScoped<INotification, LoggingNotification>();
+services.AddSingleton<INotification, LoggingNotification>();

Sau đó chúng ta restart lại ứng dụng và theo dõi kết quả bên dưới:

Scope: 0
Request 0: 567760
Request 1: 567760
Request 2: 567760
Request 3: 567760
Request 4: 567760
Scope: 1
Request 0: 567760
Request 1: 567760
Request 2: 567760
Request 3: 567760
Request 4: 567760

Như các bạn thấy, tất cả những lần yêu cầu ở cả hai scope đều trả về cùng một mã định danh, tức là chúng đều nhận cùng một đối tượng khởi tạo. Đó là cách hoạt động của Singleton service.

Chú ý

KHÔNG resolve một Scoped service từ một Singleton và cẩn thận xem xét để không làm điều này một cách gián tiếp, ví dụ như thông qua một Transient service. Nó có thể là nguyên nhân dẫn đến service không có trạng thái chính xác khi xử lý các request tiếp theo. Tốt hơn là nên:

  • Resolve một Singleton service từ một Scoped hay Transient service.
  • Resolve một Scoped service từ Scoped service khác hoặc từ một Transient service.

Mặc định, trong môi trường phát triển, resolve một service từ service có thời gian sống dài hơn sẽ ném về một exception.

Source code: Thời gian sống của service

Các phương thức đăng ký service

Framework cung cấp các extension method hữu ích trong từng ngữ cảnh cho việc đăng ký service:

Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()

  • Tự động hủy có đối tượng: Có
  • Đa lớp triển khai: Có
  • Truyền đối số: Không

Ví dụ:

services.AddSingleton<IMyDep, MyDep>();

Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})

  • Tự động hủy có đối tượng: Có
  • Đa lớp triển khai: Có
  • Truyền đối số: Có

Ví dụ:

services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep(99));

Add{LIFETIME}<{IMPLEMENTATION}>()

  • Tự động hủy có đối tượng: Có
  • Đa lớp triển khai: Không
  • Truyền đối số: Không

Ví dụ:

services.AddSingleton<MyDep>();

AddSingleton<{SERVICE}>(new {IMPLEMENTATION})

  • Tự động hủy có đối tượng: Không
  • Đa lớp triển khai: Có
  • Truyền đối số: Có

Ví dụ:

services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep(99));

AddSingleton(new {IMPLEMENTATION})

  • Tự động hủy có đối tượng: Không
  • Đa lớp triển khai: Không
  • Truyền đối số: Có

Ví dụ:

services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep(99));

Thông thường sử dụng các phương thức hỗ trợ đa triển khai để thuận tiện việc mocking khi test.

Việc đăng ký một service chỉ với một kiểu triển khai tương đương với việc đăng ký service đó với cùng kiểu triển khai và kiểu service. Đây là lý do vì sao không thể đăng ký nhiều triển khai của một service bằng các extension method không sử dụng kiểu service rõ ràng. Các method này có thể đăng ký nhiều trường hợp của một service, nhưng tất cả chúng đều có cùng một kiểu triển khai.

Trở lại ví dụ trước, chúng ta mở file MyServicesCollectionExtensions.cs và thay đổi method AddServices như sau:

public static IServiceCollection AddServices(this IServiceCollection services)
{
services.AddSingleton<INotification, ConsoleNotification>();
services.AddSingleton<INotification, LoggingNotification>();
 
return services;
}

Ở đoạn code trên, AddSingleton được gọi hai lần với INotification là loại service. Lần gọi AddSingleton thứ hai ghi đè lần gọi trước khi resolve INotification và thêm vào cuộc gọi trước đó để nhiều service được resolve thông qua IEnumerable<INotification>. Các service sẽ xuất hiện theo thứ tự chúng được đăng ký khi resolve thông qua IEnumerable<{SERVICE}>.

Để kiểm tra cách mà nó hoạt động, chúng ta mở file Pages/IndexModel.cs và thay đổi hàm contructor của lớp IndexModel như sau:

public IndexModel(INotification notification, IEnumerable<INotification> notifications)
{
Console.WriteLine($"notification is LoggingNotification: {notification is LoggingNotification}");
 
var dependencyArray = notifications.ToArray();
 
Console.WriteLine($"dependencyArray[0] is ConsoleNotification: {dependencyArray[0] is ConsoleNotification}");
Console.WriteLine($"dependencyArray[1] is LoggingNotification: {dependencyArray[1] is LoggingNotification}");
 
_notification = notification;
}

Sau đó chúng ta chạy lại ứng dụng và xem kết quả trả về từ terminal:

notification is LoggingNotification: True
dependencyArray[1] is LoggingNotification: True
dependencyArray[0] is ConsoleNotification: True

Kết quả trả về đúng như lý thuyết ở trên:

  • Với tham số đầu tiên là notification, nó sẽ resolve LoggingNotification vì nó được đăng ký sau cùng.
  • Tham số notifications sẽ resolve các service thông qua IEnumerable<INotification>. Chúng ta ép kiểu về thành kiểu mảng, sau đó xét từng phần tử và thấy rằng thứ tự của các service đúng như ở bước đăng ký.

Source code: Các phương thức đăng ký service

Constructor injection

Các service có thể resolve bằng cách sử dụng:

  • IServiceProvider
  • ActivatorUtilities:
    • Tạo các đối tượng chưa được đăng ký trong container.
    • Được sử dụng với một số tính năng của framework.

Các constructor có thể chấp nhận các đối số chưa được cung cấp bởi dependency injection, nhưng các đối số phải gán giá trị mặc định.

Khi các service được resolve bằng IServiceProvider hoặc ActivatorUtilities, constructor injection yêu cầu constructor phải là public.

Khi các service được resolve bằng ActivatorUtilities, constructor injection yêu cầu chỉ có một constructor phù hợp tồn tại. Constructor overload được hỗ trợ nhưng yêu cầu toàn bộ các đối số phải được thực hiện bằng dependency injection.

Scope validation

CreateDefaultBuilder đặt ServiceProviderOptions.ValidateScopestrue nếu môi trường của ứng dụng là Development.

Khi ValidateScopes được đặt là true, service provider mặc định sẽ kiểm tra để xác minh rằng:

  • Các Scoped service không trực tiếp hoặc gián tiếp resolve từ root service provider.
  • Các Scoped service không trực tiếp hoặc gián tiếp inject từ các Singleton.

Root service provider được tạo khi BuildServiceProvider được gọi. Thời gian sống của root service provider tương ứng với thời gian sống của ứng dụng/máy chủ khi service provider bắt đầu với ứng dụng và bị hủy bỏ khi ứng dụng tắt.

Các Scoped service bị hủy bỏ bởi container tạo ra chúng. Nếu một Scoped service được tạo trong root container thì thời gian tồn tại của nó có hiệu lực như thời gian sống Singleton vì nó chỉ bị hủy bỏ bởi root container khi mà ứng dụng/máy chủ tắt.

Để luôn luôn có thể xác thực scope, bao gồm cả môi trường Production, ta có thể cấu hình ServiceProviderOptions bằng cách sử dụng method UseDefaultServiceProvider trên host builder.

var builder = WebApplication.CreateBuilder(args);
 
builder.Host.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = true;
});

Request Services

Các service và dependency của chúng trong một ASP.NET Core request được thể hiện thông qua HttpContext.RequestServices.

Framework tạo một scope trên một request, và RequestServices thể hiện các service provider trong phạm vi. Tất cả các service trong phạm vi đều có giá trị miễn là request còn hoạt động.

Ghi chú

Ưu tiên yêu cầu các dependency như một tham số constructor hơn là resolve service từ RequestServices. Điều này giúp cho việc kiểm thử dễ dàng hơn.

Thiết kế service cho dependency injection

Khi thiết kế service cho dependency injection:

  • Tránh các lớp và thành phần trạng thái, tĩnh. Tránh tạo các trạng thái toàn cục bằng cách thiết kế ứng dụng để sử dụng các Singleton service.
  • Tránh tạo trực tiếp các lớp dependency trong service.
  • Viết các service nhỏ, có cấu trúc tốt và dễ dàng kiểm thử.

Nếu một lớp có quá nhiều dependency, điều đó có thể là dấu hiệu cho thấy lớp đó có quá nhiều trách nhiệm, và vi phạm Nguyên tắc trách nhiệm duy nhất2. Cố gắng cấu trúc lại lớp bằng cách di chuyển một vài trách nhiệm của nó đến lớp mới. Hãy nhớ rằng các lớp Razor Pages page model và MVC controller nên tập trung vào các mối quan tâm về giao diện người dùng.

Hủy bỏ service

Container gọi Dispose của các loại IDispose mà nó tạo ra. Các service được resolve từ container nên không bao giờ bị hủy bỏ bởi nhà phát triển. Nếu một loại hoặc một factory được đăng ký như một Singleton thì container sẽ tự động hủy nó.

Quay trở lại với source code của chúng ta, trước tiên hãy implement IDispose lần lượt vào các service mà chúng ta có.

Ở file Services/ConsoleNotification.cs:

-public class ConsoleNotification : INotification
+public class ConsoleNotification : INotification, IDisposable
{
+ private bool _disposed;
 
public void SendMessage(string message)
{
Console.WriteLine($"ConsoleNotification.SendMessage called. Message: {message}");
}
 
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+ 
+ Console.WriteLine("ConsoleNotification.Dispose");
+ _disposed = true;
+ }
}

Ở file Services/LoggingNotification.cs:

-public class LoggingNotification : INotification
+public class LoggingNotification : INotification, IDisposable
{
private readonly ILogger<LoggingNotification> _logger;
 
+ private bool _disposed;
 
public LoggingNotification(ILogger<LoggingNotification> logger)
{
_logger = logger;
}
 
public void SendMessage(string message)
{
_logger.LogInformation($"LoggingNotification.SendMessage called. Message: {message}");
}
 
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+ 
+ Console.WriteLine("LoggingNotification.Dispose");
+ _disposed = true;
+ }
}

Mình sẽ giải thích một chút về code ở hai file service trên. Đơn giản là sau khi implement IDispose thì chúng ta cần phải khai báo method Dispose mà lớp triển khai đó cung cấp. Đoạn code trong method này đơn giản chỉ là việc in ra dòng tin nhắn trên terminal trước khi hủy bỏ service.

Tiếp theo tại file MyServicesCollectionExtensions.cs, chúng ta đăng ký lần lượt Scoped cho ConsoleNotification và Singleton cho LoggingNotification.

public static IServiceCollection AddServices(this IServiceCollection services)
{
services.AddScoped<ConsoleNotification>();
services.AddSingleton<LoggingNotification>();
 
return services;
}

Cuối cùng trở lại file Pages/IndexModel.cs và thay đổi class IndexModel bằng đoạn code bên dưới:

public class IndexModel : PageModel
{
private readonly ConsoleNotification _consoleNotification;
private readonly LoggingNotification _loggingNotification;
 
public IndexModel(ConsoleNotification consoleNotification, LoggingNotification loggingNotification)
{
_consoleNotification = consoleNotification;
_loggingNotification = loggingNotification;
}
 
public void OnGet()
{
_consoleNotification.SendMessage("IndexModel.OnGet");
_loggingNotification.SendMessage("IndexModel.OnGet");
}
}

Vậy là đã hoàn tất, việc của chúng ta bây giờ chỉ là chạy lại ứng dụng và theo dõi kết quả trên terminal:

ConsoleNotification.SendMessage called. Message: IndexModel.OnGet
LoggingNotification.SendMessage called. Message: IndexModel.OnGet
ConsoleNotification.Dispose

Như các bạn thấy, cả hai service ConsoleNotificationLoggingNotification đều đã được gọi, nhưng vì chỉ có mỗi ConsoleNotification là đăng ký với thời gian sống là Scoped nên sẽ bị hủy bỏ sau khi kết thúc request.

Các bạn thử nhất tổ hợp Ctrl + C để tắt ứng dụng, ngay lúc này một dòng thông báo sẽ hiện lên để báo rằng LoggingNotification đã được hủy bỏ.

LoggingNotification.Dispose

Source code: Hủy bỏ service

Các service không được tạo bởi service container

Xem xét đoạn code bên dưới:

services.AddSingleton(new ConsoleNotification());
services.AddSingleton(new LoggingNotification());

Trong đoạn code trên:

  • Các khởi tạo của service không được tạo bằng serivce container.
  • Framework không thể hủy bỏ các service này tự động.
  • Nhà phát triển phải có trách nhiệm hủy bỏ chúng.

Tổng kết

Bài này có lẽ chưa thể nói là "tất tần tật" về Dependency Injection trong ASP.NET Core framework, nhưng với lượng kiến thức này cũng có thể cho chúng ta cái nhìn tổng quan và áp dụng kỹ thuật này vào dự án. Đây là một nội dung có vẻ "khó nuốt" đối với những bạn mới tiếp cận về lập trình, nhưng nó rất phổ biến trong các framework hiện nay. Nếu bạn cảm thấy chưa thực sự rõ về nội dung bài viết này thì cũng đừng nên quá lo lắng, vì đây có thể xem là kiến thức nâng cao. Chúng ta có thể quay lại bài viết này sau khi đã làm quen, tiếp cận nhiều với ASP.NET Core framework. Hi vọng bài viết này giúp ích được cho các bạn. Cảm ơn các bạn đã quan tâm theo dõi, hẹn gặp lại!


  1. Inversion of Control (IoC) là một nguyên lý thiết kế trong công nghệ phần mềm trong đó các thành phần nó dựa vào để làm việc bị đảo ngược quyền điều khiển khi so sánh với lập trình hướng thủ thục truyền thống. (theo xuanthulab

  2. Trong lập trình hướng đối tượng, nguyên tắc trách nhiệm duy nhất (Single Responsibility Principle - SRP) phát biểu rằng mỗi lớp chỉ nên có một trách nhiệm duy nhất, và rằng trách nhiệm đó nên được đóng gói hoàn toàn bởi lớp đó. Tất cả các dịch vụ của lớp đó cần được định hướng chặt chẽ theo trách nhiệm đó. (theo Wikipedia) 

search
Bài viết
Series
Thẻ

Không tìm thấy kết quả nào.