ASP.NET Core Dependency Injection Thực hành tốt nhất, Mẹo & Thủ thuật

Trong bài viết này, tôi sẽ chia sẻ kinh nghiệm và đề xuất của tôi về việc sử dụng Dependency Injection trong các ứng dụng ASP.NET Core. Động lực đằng sau những nguyên tắc này là;

  • Hiệu quả thiết kế dịch vụ và phụ thuộc của họ.
  • Ngăn chặn các vấn đề đa luồng.
  • Ngăn chặn rò rỉ bộ nhớ.
  • Ngăn chặn các lỗi tiềm ẩn.

Bài viết này giả định rằng bạn đã quen thuộc với Dependency Injection và ASP.NET Core ở mức cơ bản. Nếu không, vui lòng đọc tài liệu ASP.NET Core Dependency Injection.

Khái niệm cơ bản

Xây dựng tiêm

Con constructor tiêm được sử dụng để khai báo và có được sự phụ thuộc của một dịch vụ vào việc xây dựng dịch vụ. Thí dụ:

dịch vụ lớp học công cộng
{
    riêng tư IP sinhtRep repository _productRep repository;
    Dịch vụ sản phẩm công cộng (IP sinh sảnRep repository sản phẩm lưu trữ)
    {
        _productRep repository = sản phẩmRep repository;
    }
    công khai void Xóa (int id)
    {
        _productRep repository.Delete (id);
    }
}

ProductService đang tiêm IP sinhtRep repository như một phần phụ thuộc trong hàm tạo của nó sau đó sử dụng nó bên trong phương thức Delete.

Thực hành tốt:

  • Xác định các phụ thuộc cần thiết một cách rõ ràng trong hàm tạo dịch vụ. Do đó, dịch vụ không thể được xây dựng mà không có sự phụ thuộc của nó.
  • Chỉ định phụ thuộc được chèn vào trường / thuộc tính chỉ đọc (để tránh vô tình gán giá trị khác cho nó trong một phương thức).

Tiêm tài sản

Hộp chứa phụ thuộc tiêu chuẩn ASP.NET Core tựa không hỗ trợ tiêm thuộc tính. Nhưng bạn có thể sử dụng một container khác hỗ trợ tiêm tài sản. Thí dụ:

sử dụng Microsoft.Extensions.Logging;
sử dụng Microsoft.Extensions.Logging.Abstrilities;
không gian tên MyApp
{
    dịch vụ lớp học công cộng
    {
        công khai ILogger  Logger {get; bộ; }
        riêng tư IP sinhtRep repository _productRep repository;
        Dịch vụ sản phẩm công cộng (IP sinh sảnRep repository sản phẩm lưu trữ)
        {
            _productRep repository = sản phẩmRep repository;
            Logger = NullLogger  .Instance;
        }
        công khai void Xóa (int id)
        {
            _productRep repository.Delete (id);
            Logger.LogIn information (
                $ "Đã xóa sản phẩm có id = {id}");
        }
    }
}

ProductService đang khai báo một thuộc tính Logger với setter công khai. Container tiêm phụ thuộc có thể đặt Logger nếu nó có sẵn (đã đăng ký với container DI trước đó).

Thực hành tốt:

  • Sử dụng tiêm tài sản chỉ cho các phụ thuộc tùy chọn. Điều đó có nghĩa là dịch vụ của bạn có thể hoạt động đúng mà không cần các phụ thuộc này được cung cấp.
  • Sử dụng mẫu đối tượng Null (như trong ví dụ này) nếu có thể. Nếu không, luôn luôn kiểm tra null trong khi sử dụng phụ thuộc.

Định vị dịch vụ

Mẫu định vị dịch vụ là một cách khác để có được sự phụ thuộc. Thí dụ:

dịch vụ lớp học công cộng
{
    riêng tư IP sinhtRep repository _productRep repository;
    ILogger chỉ đọc riêng tư  _logger;
    Dịch vụ sản phẩm công cộng (IServiceProvider serviceProvider)
    {
        _productRep repository = serviceProvider
          .GetRequiredService  ();
        _logger = dịch vụ trình duyệt
          .GetService > () ??
            NullLogger  .Instance;
    }
    công khai void Xóa (int id)
    {
        _productRep repository.Delete (id);
        _logger.LogIn information ($ "Đã xóa sản phẩm có id = {id}");
    }
}

ProductService đang tiêm IServiceProvider và giải quyết các phụ thuộc bằng cách sử dụng nó. GetRequiredService ném ngoại lệ nếu phụ thuộc được yêu cầu không được đăng ký trước đó. Mặt khác, GetService chỉ trả về null trong trường hợp đó.

Khi bạn giải quyết các dịch vụ bên trong hàm tạo, chúng sẽ được giải phóng khi dịch vụ được giải phóng. Vì vậy, bạn không quan tâm đến việc phát hành / xử lý các dịch vụ được giải quyết bên trong hàm tạo (giống như hàm tạo và hàm thuộc tính).

Thực hành tốt:

  • Không sử dụng mẫu định vị dịch vụ bất cứ khi nào có thể (nếu loại dịch vụ được biết đến trong thời gian phát triển). Bởi vì nó làm cho sự phụ thuộc ngầm. Điều đó có nghĩa là nó không thể dễ dàng nhìn thấy các phụ thuộc trong khi tạo một thể hiện của dịch vụ. Điều này đặc biệt quan trọng đối với các bài kiểm tra đơn vị nơi bạn có thể muốn chế giễu một số phụ thuộc của dịch vụ.
  • Giải quyết các phụ thuộc trong hàm tạo dịch vụ nếu có thể. Giải quyết trong một phương thức dịch vụ làm cho ứng dụng của bạn phức tạp hơn và dễ bị lỗi hơn. Tôi sẽ đề cập đến các vấn đề & giải pháp trong các phần tiếp theo.

Thời gian phục vụ

Có ba vòng đời dịch vụ trong ASP.NET Core Dependency Injection:

  1. Dịch vụ tạm thời được tạo ra mỗi khi chúng được tiêm hoặc yêu cầu.
  2. Dịch vụ phạm vi được tạo ra trên mỗi phạm vi. Trong một ứng dụng web, mọi yêu cầu web tạo ra một phạm vi dịch vụ riêng biệt mới. Điều đó có nghĩa là các dịch vụ phạm vi thường được tạo ra cho mỗi yêu cầu web.
  3. Dịch vụ Singleton được tạo trên mỗi container DI. Điều đó thường có nghĩa là chúng chỉ được tạo một lần cho mỗi ứng dụng và sau đó được sử dụng cho toàn bộ thời gian sử dụng của ứng dụng.

DI container theo dõi tất cả các dịch vụ được giải quyết. Các dịch vụ được phát hành và xử lý khi thời gian kết thúc của chúng:

  • Nếu dịch vụ có phụ thuộc, chúng cũng tự động được phát hành và xử lý.
  • Nếu dịch vụ triển khai giao diện IDis Dùng một lần, phương thức Vứt bỏ sẽ tự động được gọi khi phát hành dịch vụ.

Thực hành tốt:

  • Đăng ký dịch vụ của bạn như thoáng qua bất cứ nơi nào có thể. Bởi vì nó đơn giản để thiết kế các dịch vụ thoáng qua. Bạn thường không quan tâm đến vấn đề đa luồng và rò rỉ bộ nhớ và bạn biết dịch vụ này có tuổi thọ ngắn.
  • Sử dụng cẩn thận trọn đời dịch vụ trong phạm vi vì nó có thể khó khăn nếu bạn tạo phạm vi dịch vụ trẻ em hoặc sử dụng các dịch vụ này từ một ứng dụng không phải là web.
  • Sử dụng singleton trọn đời cẩn thận kể từ đó bạn cần xử lý các vấn đề rò rỉ bộ nhớ đa luồng và tiềm năng.
  • Không phụ thuộc vào dịch vụ tạm thời hoặc phạm vi từ dịch vụ đơn lẻ. Bởi vì dịch vụ tạm thời trở thành một cá thể đơn lẻ khi một dịch vụ đơn lẻ tiêm vào nó và điều đó có thể gây ra vấn đề nếu dịch vụ tạm thời không được thiết kế để hỗ trợ cho một kịch bản như vậy. ASP.NET Core container mặc định DI container đã ném ngoại lệ trong các trường hợp như vậy.

Giải quyết các dịch vụ trong cơ thể Phương thức

Trong một số trường hợp, bạn có thể cần giải quyết dịch vụ khác theo phương thức dịch vụ của mình. Trong những trường hợp như vậy, đảm bảo rằng bạn phát hành dịch vụ sau khi sử dụng. Cách tốt nhất để đảm bảo rằng tạo ra một phạm vi dịch vụ. Thí dụ:

lớp công khai PriceCalculator
{
    IServiceProvider chỉ đọc riêng tư _serviceProvider;
    công khai PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    công khai tính toán (Sản phẩm sản phẩm, số lượng int,
      Nhập taxStrargetyServiceType)
    {
        sử dụng (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrargety = (ITaxStrargety) scope.ServiceProvider
              .GetRequiredService (taxStrargetyServiceType);
            var price = sản phẩm. Giá * tính;
            giá hoàn trả + taxStrargety.CalculateTax (price);
        }
    }
}

PriceCalculator tiêm IServiceProvider vào hàm tạo của nó và gán nó cho một trường. PriceCalculator sau đó sử dụng nó bên trong phương thức Tính toán để tạo phạm vi dịch vụ con. Nó sử dụng scope.ServiceProvider để giải quyết các dịch vụ, thay vì đối tượng _serviceProvider được chèn. Do đó, tất cả các dịch vụ được giải quyết từ phạm vi sẽ tự động được phát hành / xử lý ở cuối câu lệnh sử dụng.

Thực hành tốt:

  • Nếu bạn đang giải quyết một dịch vụ trong thân phương thức, luôn tạo phạm vi dịch vụ con để đảm bảo rằng các dịch vụ được giải quyết được phát hành đúng.
  • Nếu một phương thức lấy IServiceProvider làm đối số, thì bạn có thể trực tiếp giải quyết các dịch vụ từ nó mà không cần quan tâm đến việc phát hành / xử lý. Tạo / quản lý phạm vi dịch vụ là trách nhiệm của mã gọi phương thức của bạn. Theo nguyên tắc này làm cho mã của bạn sạch hơn.
  • Đừng giữ một tham chiếu đến một dịch vụ được giải quyết! Mặt khác, nó có thể gây rò rỉ bộ nhớ và bạn sẽ truy cập vào một dịch vụ bị loại bỏ khi bạn sử dụng tham chiếu đối tượng sau này (trừ khi dịch vụ được giải quyết là đơn lẻ).

Dịch vụ đơn

Các dịch vụ đơn lẻ thường được thiết kế để giữ trạng thái ứng dụng. Bộ đệm là một ví dụ tốt về trạng thái ứng dụng. Thí dụ:

lớp dịch vụ công cộng
{
    riêng tư chỉ đọc đồng thời  _cache;
    Dịch vụ tệp công cộng ()
    {
        _cache = new ConcurrencyDixi  ();
    }
    byte công khai [] GetFileContent (chuỗi filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            trả về File.Read ALLBytes (filePath);
        });
    }
}

FileService đơn giản lưu trữ nội dung tệp để giảm đọc đĩa. Dịch vụ này nên được đăng ký là singleton. Nếu không, bộ nhớ đệm sẽ không hoạt động như mong đợi.

Thực hành tốt:

  • Nếu dịch vụ giữ trạng thái, nó sẽ truy cập vào trạng thái đó theo cách an toàn luồng. Bởi vì tất cả các yêu cầu đồng thời sử dụng cùng một thể hiện của dịch vụ. Tôi đã sử dụng đồng thời từ điển thay vì từ điển để đảm bảo an toàn cho chuỗi.
  • Không sử dụng dịch vụ phạm vi hoặc tạm thời từ các dịch vụ đơn lẻ. Bởi vì, các dịch vụ tạm thời có thể không được thiết kế để an toàn cho chuỗi. Nếu bạn phải sử dụng chúng, thì hãy quan tâm đến đa luồng trong khi sử dụng các dịch vụ này (ví dụ sử dụng khóa).
  • Rò rỉ bộ nhớ thường được gây ra bởi các dịch vụ đơn lẻ. Chúng không được phát hành / xử lý cho đến khi kết thúc ứng dụng. Vì vậy, nếu họ khởi tạo các lớp (hoặc tiêm) nhưng không giải phóng / loại bỏ chúng, chúng cũng sẽ lưu lại trong bộ nhớ cho đến khi kết thúc ứng dụng. Đảm bảo rằng bạn phát hành / loại bỏ chúng đúng lúc. Xem phần Dịch vụ giải quyết trong phần Phương thức bên trên.
  • Nếu bạn lưu trữ dữ liệu (nội dung tệp trong ví dụ này), bạn nên tạo một cơ chế để cập nhật / làm mất hiệu lực dữ liệu được lưu trong bộ đệm khi nguồn dữ liệu gốc thay đổi (khi tệp được lưu trong bộ đệm thay đổi trên ví dụ này).

Dịch vụ phạm vi

Phạm vi trọn đời đầu tiên có vẻ là một ứng cử viên tốt để lưu trữ trên mỗi dữ liệu yêu cầu web. Bởi vì ASP.NET Core tạo ra một phạm vi dịch vụ cho mỗi yêu cầu web. Vì vậy, nếu bạn đăng ký một dịch vụ như phạm vi, nó có thể được chia sẻ trong khi yêu cầu web. Thí dụ:

lớp công khai RequestItemsService
{
    Từ điển chỉ đọc riêng tư  _items;
    công khai RequestItemsService ()
    {
        _items = Từ điển mới  ();
    }
    public void Set (tên chuỗi, giá trị đối tượng)
    {
        _items [tên] = giá trị;
    }
    đối tượng công khai Nhận (tên chuỗi)
    {
        trả về _items [tên];
    }
}

Nếu bạn đăng ký RequestItemsService dưới dạng phạm vi và đưa nó vào hai dịch vụ khác nhau, thì bạn có thể nhận được một mục được thêm từ dịch vụ khác vì chúng sẽ chia sẻ cùng một ví dụ RequestItemsService. Đó là những gì chúng tôi mong đợi từ các dịch vụ phạm vi.

Nhưng .. thực tế có thể không phải lúc nào cũng như vậy. Nếu bạn tạo phạm vi dịch vụ con và giải quyết RequestItemsService từ phạm vi con, thì bạn sẽ nhận được một phiên bản mới của RequestItemsService và nó sẽ không hoạt động như bạn mong đợi. Vì vậy, dịch vụ phạm vi không phải lúc nào cũng có nghĩa là thể hiện trên mỗi yêu cầu web.

Bạn có thể nghĩ rằng bạn không phạm sai lầm rõ ràng như vậy (giải quyết một phạm vi trong phạm vi con). Nhưng, đây không phải là một lỗi (một cách sử dụng rất thường xuyên) và trường hợp có thể không đơn giản như vậy. Nếu có một biểu đồ phụ thuộc lớn giữa các dịch vụ của bạn, bạn không thể biết liệu có ai đã tạo phạm vi con hay không và giải quyết một dịch vụ tiêm dịch vụ khác mà cuối cùng tiêm dịch vụ có phạm vi.

Thực hành tốt:

  • Một dịch vụ phạm vi có thể được coi là một tối ưu hóa trong đó nó được tiêm bởi quá nhiều dịch vụ trong một yêu cầu web. Do đó, tất cả các dịch vụ này sẽ sử dụng một phiên bản duy nhất của dịch vụ trong cùng một yêu cầu web.
  • Các dịch vụ phạm vi don lồng cần được thiết kế theo chủ đề an toàn. Bởi vì, chúng thường được sử dụng bởi một yêu cầu web / luồng. Nhưng trong trường hợp đó, bạn không nên chia sẻ phạm vi dịch vụ giữa các luồng khác nhau!
  • Hãy cẩn thận nếu bạn thiết kế một dịch vụ có phạm vi để chia sẻ dữ liệu giữa các dịch vụ khác trong một yêu cầu web (đã giải thích ở trên). Bạn có thể lưu trữ trên mỗi dữ liệu yêu cầu web bên trong HttpContext (tiêm IHttpContextAccessor để truy cập nó), đây là cách an toàn hơn để làm điều đó. Tuổi thọ của HTTPContext không có phạm vi. Trên thực tế, nó không được đăng ký với DI (đó là lý do tại sao bạn không nên tiêm nó, nhưng thay vào đó lại tiêm IHttpContextAccessor). Việc triển khai HTTPContextAccessor sử dụng AsyncLocal để chia sẻ cùng một HTTPContext trong khi yêu cầu web.

Phần kết luận

Lúc đầu, việc sử dụng phụ thuộc có vẻ đơn giản để sử dụng, nhưng có những vấn đề rò rỉ đa luồng và bộ nhớ tiềm ẩn nếu bạn không tuân theo một số nguyên tắc nghiêm ngặt. Tôi đã chia sẻ một số nguyên tắc tốt dựa trên kinh nghiệm của bản thân trong quá trình phát triển khung công tác nồi hơi ASP.NET.