Dependency Injection

ASP.NET Boilerplate Dependency Injection

About Dependency Injection in ASP.NET Boilerplate Framework

Quang Trong VU
Old Dev

--

Danh mục trong bài viết:

  1. Dependency Injection là gì?
    - Các vấn đề của cách làm truyền thống.
    - Giải pháp.
  2. Dependency Injection Infrastructure của ASP.NET Boilerplate
    - Register dependency.
    - Resolve dependency.
    - ASP.NET Core Integration

I. Dependency Injection là gì?

Nếu bạn biết rồi thì có thể chuyển tiếp sang phần sau. Ở đây mình paste lại đoạn định nghĩa trên Wikipedia:

“Dependency injection is a software design pattern in which one or more dependencies (or services) are injected, or passed by reference, into a dependent object (or client) and are made part of the client’s state. The pattern separates the creation of a client’s dependencies from its own behavior, which allows program designs to be loosely coupled and to follow the dependency inversion and single responsibility principles. It directly contrasts the service locator pattern, which allows clients to know about the system they use to find dependencies.” — Wikipedia.

Rất khó để quản lý các dependency và phát triển một ứng dụng dạng module và well-structured mà không sử dụng kỹ thuật Dependency Injection.

Vấn đề của cách làm truyền thống.

Trong một ứng dụng, các class phụ thuộc lẫn nhau. Giả sử bạn có một Application Service sử dụng một Repository để insert Entities vào database. Trong tình huống này, Application Service class sẽ phụ thuộc vào Repository class. Như ví dụ sau:

public class PersonAppService
{
private IPersonRepository _personRepository;

public PersonAppService()
{
_personRepository = new PersonRepository();
}

public void CreatePerson(string name, int age)
{
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
}
}

PersonalAppService (Application Service) dùng PersonRepository để insert một Person vào database. Mặc dù nhìn có vẻ không gây hại gì, nhưng dưới đây là những vấn đề của đoạn code này.

  • PersonAppService dùng tham chiếu đếnIPersonRepository cho CreatePerson method. Method này phụ thuộc vào IPersonRepository chứ không phải PersonRepository concrete class. Tuy nhiên trong constructor lại phụ thuộc vào PersonRepository concrete class chứ không phải interface. Điều này vi phạm nguyên lý Dependency Inversion (SOLID). Nôm na, Component nên phụ thuộc vào interface thay vì concrete.
  • Nếu PersonAppService create PersonRepository, thì nó sẽ phụ thuộc vào một implementation cụ thể của IPersonRepository interface. Như vậy nó sẽ không thể dùng những implementation khác. Điều này làm cho việc tách biệt Interface và Implementation trở nên vô nghĩa. Ngoài ra việc tightly-coupled này sẽ làm cho code khó re-use, không thể thực hiện mock test.
  • Giả sử sau này chúng ta có thể muốn thay đổi PersonRepository. Chẳng hạn chuyển nó lên mức Singleton dùng chung cho toàn bộ ứng dụng. Hoặc muốn tạo ra nhiều implementation của IPersonRepository và load các implementation này lên theo điều kiện. Như vậy chúng ta sẽ cần phải thay đổi toàn bộ các class phụ thuộc vào IPersonRepository

Để giải quyết vấn đề này có thể sử dụng Factory Method pattern. Như ví dụ dưới đây:

public class PersonAppService
{
private IPersonRepository _personRepository;

public PersonAppService()
{
_personRepository = PersonRepositoryFactory.Create();
}

public void CreatePerson(string name, int age)
{
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
}
}

PersonRepositoryFactory là một static class nó create và return một instance kiểu IPersonRepository. Cái này được biết đến dưới cái tên Service Locator pattern. Vấn đề create một implementation được giải quyết với việc PersonAppService không trực tiếp tạo một implementation của IPersonRepository và nó độc lập với PersonRepository implementation. Tuy nhiên nó vẫn còn một vài vấn đề.

  • Với cài đặt này, PersonAppService phụ thuộc vào PersonRepositoryFactory. Điều này có thể dễ chấp nhận hơn, nhưng thực chất nó vẫn là một hard-dependency.
  • Rất tẻ nhạt và duplicate code để viết factory method/class cho mỗi repository hoặc mỗi dependency.
  • Còn nữa, test, nó không dễ để test, vì khó để tạo một mock implementation của IPersonRepository cho PersonAppService.

Giải Pháp

Dưới đây là một số giải pháp giúp tránh phụ thuộc vào các concrete class.

(1) Constructor Injection Pattern

Có thể viết lại ví dụ bên trên như sau:

public class PersonAppService
{
private IPersonRepository _personRepository;

public PersonAppService(IPersonRepository personRepository)
{
_personRepository = personRepository;
}

public void CreatePerson(string name, int age)
{
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
}
}

Kiểu này được biết đến là Constructor Injection. PersonAppService không biết về implementation. Và đây là cách create và pass implementation vào.

var repository = new PersonRepository();
var personService = new PersonAppService(repository);
personService.CreatePerson("John Doe", 32);

Constructor Injection là một cách rất tốt để giúp một class độc lập với việc tạo ra các object phụ thuộc, nhưng dưới đây là các vấn đề của đoạn code trên:

  • Việc tạo một PersonAppService trở nên khó hơn. Nó có 4 phụ thuộc. Chúng ta phải tạo 4 đối tượng phụ thuộc này và truyền chúng vào constructor của PersonAppService.
  • Các Dependent class đến lượt nó lại có thể có những phụ thuộc khác (ở đây ví dụ như PersonRepository lại có những phụ thuộc khác). Chúng ta phải tạo tất cả những dependency này của PersonAppService, tất cả những dependency của những dependency đó, và cứ như vậy(!). Chúng ta thậm chí có thể không tạo nổi một object đơn bởi dependency graph quá phức tạp.

Nhưng may mắn là chúng ta có những Dependency Injection framework, cái sẽ tự động quản lý các dependency này.

(2) Property Injection Pattern

Constructor Injection pattern là một cách rất tốt để cung cấp các dependency cho một class. Nhưng theo cách này, bạn không thể tạo một instance của một class nếu không cung cấp đầy đủ các dependency.

Trong một vài tình huống một class có thể phụ thuộc vào một class khác, nhưng nó vẫn có thể chạy mà không có dependency. Điều này thường đúng với những vấn đề cross-cutting concern kiểu như là logging. Một class có thể chạy mà không có logging, nhưng có thể ghi log nếu nó được cung cấp một logger. Trong trường hợp này, bạn có thể định nghĩa các dependency như là những public property thay vì để chúng ở constructor. Chúng ta có thể xem ví dụ sau:

public class PersonAppService
{
public ILogger Logger { get; set; }

private IPersonRepository _personRepository;

public PersonAppService(IPersonRepository personRepository)
{
_personRepository = personRepository;
Logger = NullLogger.Instance;
}

public void CreatePerson(string name, int age)
{
Logger.Debug("Inserting a new person to database with name = " + name);
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
Logger.Debug("Successfully inserted!");
}
}

NullLogger.Instance là một singleton object cài đặt ILogger, nhưng nó không làm gì. Nó không ghi log. Nó cài đặt ILogger với những method rỗng. PersonAppService có thể ghi log nếu bạn set Logger property sau khi tạo PersonAppService object.

var personService = new PersonAppService(new PersonRepository());
personService.Logger = new Log4NetLogger();
personService.CreatePerson("John Doe", 32);

Giả sử Log4NetLogger implement ILogger và nó ghi log sử dụng Log4Net library bởi vậy PersonAppService có thể thực sự ghi log. Nếu bạn không set Logger, nó sẽ không ghi log. Chúng ta có thể nói rằng ILogger là một optional dependency của PersonAppService.

Hầu hết các Dependency Injection Framework hỗ trợ Property Injection pattern.

(3) Dependency Injection Framework

Có nhiều Dependency Injection Framework hỗ trợ tự động resolve các dependency. Chúng tạo các object với tất cả các dependency, và dependency của dependency, một cách đệ quy. Đơn giản bạn chỉ việc viết các class của bạn với Constructor & Property Injection pattern, và DI framework sẽ xử lý các vấn đề còn lại. Trong một ứng dụng tốt, các class của bạn thậm chí độc lập với DI framework(!). Chỉ có một vài dòng code hoặc class sẽ explicitly interact với DI framework trong toàn bộ ứng dụng.

ASP.NET Boilerplate dùng Castle Windsor framework cho Dependency Injection. Nó là một trong những DI framework mature nhất đang được sử dụng. Cũng có nhiều framework khác như là Unity, Ninject, StructureMap, Autofac…

Trong một DI framework, đầu tiên bạn sẽ register interface/class vào DI framework, và sau đó resolve (create) một object. Trong Castle Windsor, nó kiểu như sau:

var container = new WindsorContainer();

container.Register(
Component.For<IPersonRepository>().ImplementedBy<PersonRepository>().LifestyleTransient(),
Component.For<IPersonAppService>().ImplementedBy<PersonAppService>().LifestyleTransient()
);

var personService = container.Resolve<IPersonAppService>();
personService.CreatePerson("John Doe", 32);

Đầu tiên, chúng ta tạo WindsorContainer và register PersonRepository và PersonAppService với interface của chúng. Sau đó chúng ta dùng container này để tạo một instance của IPersonAppService. Nó tạo một concrete class PersonAppService với những phụ thuộc đi kèm và return it. Trong ví dụ đơn giản này, nó không thể hiện rõ được những ưu điểm của việc sử dụng DI framework. Tuy nhiên, trong thực tế bạn sẽ có nhiều class và nhiều dependency trong ứng dụng của mình. Việc đăng ký các dependency là tách biệt với việc tạo và sử dụng các object, và nó chỉ được tạo trong quá trình startup của ứng dụng.

II. Dependency Injection Infrastructure của ASP.NET Boilerplate

ASP.NET Boilerplate làm cho việc sử dụng DI framework hầu như vô hình. Nó cũng giúp bạn viết ứng dụng theo các best practice và convention.

(1) Register các Dependency

Có những cách khác nhau để bạn register class vào DI system của ASP.NET Boilerplate. Hầu hết, conventional registration là dùng đủ.

Conventional Registration

ASP.NET Boilerplate tự động đăng ký tất cả Repository, Domain Service, Application Service, MVC Controller và Web API Controller theo convention. Ví dụ, bạn có thể có một IPersonAppService interface và một PersonAppService class implement interface đó.

public interface IPersonAppService : IApplicationService
{
//...
}

public class PersonAppService : IPersonAppService
{
//...
}

ASP.NET Boilerplate tự động register bởi nó implement IApplicationService interface (nó chỉ là một empty interface). Nó được register với lifecycle là transient, có nghĩa nó sẽ được tạo mỗi lần sử dụng (call). Khi bạn inject (sử dụng constructor injection) một IPersonAppService interface vào một class, thì một PersonAppService object sẽ được tạo và truyền đến constructor, một cách tự động.

Naming Convention là rất quan trọng ở chỗ này. Ví dụ, bạn có thể thay đổi tên của PersonAppService thành MyPersonAppService hoặc tên khác với hậu tố ‘PersonAppService’ phải được giữ nguyên. Nó sẽ register với IPersonAppService bởi chúng có cùng hậu tố. Nếu bạn đặt khác hậu tố ví dụ PeopleService, thì hệ thống sẽ không tự register, mà bạn phải register thủ công sử dụng self-registration (not the interface?).

ASP.NET Boilerplate có thể register các assemblies bằng quy ước (convention). Nó khá dễ.

IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());

Assembly.GetExecutingAssembly() lấy ra một reference đến assembly chứa đoạn code đang chạy. Bạn có thể truyền những assembly khác vào method RegisterAssemblyByConvention. Việc này thường được thực hiện khi module của bạn được initialize.

Bạn có thể viết conventional registration của riêng mình bằng việc implement IConventionalRegister interface và sau đó gọi method IocManager.AddConventionalRegister trong class của bạn. Bạn nên add code trên trong PreInitialize method của module.

Hepler Interfaces

Bạn có thể muốn đăng ký một specific class mà không tuân theo conventional registration rule. ASP.NET Boilerplate cung cấp ITransientDependency, IPerWebRequestDependency và ISingletonDependency interface.

public interface IPersonManager
{
//...
}

public class MyPersonManager : IPersonManager, ISingletonDependency
{
//...
}

Theo cách này, bạn có thể dễ dàng register MyPersonManager. Khi bạn cần inject IPersonManager, thì MyPersonManager class sẽ được sử dụng.

Lưu ý: IPerWebRequestDependency chỉ có thể dùng trên web layer.

Custom/Direction Registration

Nếu conventional registration không đủ cho yêu cầu của bạn, bạn có thể sử dụng IocManager hoặc Castle Windsor để đăng ký class và các dependency.

  • Using IocManager

Bạn có thể dùng IocManager để đăng ký các dependency (thường dùng trong PreInitialize method của module definition)

IocManager.Register<IMyService, MyService>(DependencyLifeStyle.Transient);
  • Using Castle Windsor API

Bạn có thể dùng IIocManager.IocContainer property để truy suất đến Castle Windsor Container và register các dependency. Ví dụ

IocManager.IocContainer.Register(Classes.FromThisAssembly().BasedOn<IMySpecialInterface>().LifestylePerThread().WithServiceSelf());

Bạn có thể tìm thêm thông tin tại Castle’s document

(2) Resolving Dependency

Việc Register sẽ thông báo cho IoC (Inversion of Control) Container (a.k.a DI framework) về các class, những dependency của nó, và lifetime. Ở nơi nào đó trong ứng dụng, bạn sẽ cần tạo object bằng việc sử dụng IoC container. ASP.NET cung cấp một vài lựa chọn để resolve các dependency.

  • Constructor & Property Injection

Như là một best practice, bạn nên dùng constructor và property injection để đưa vào các dependency. Bạn nên dùng cách này bất cứ khi nào có thể.

public class PersonAppService
{
public ILogger Logger { get; set; }

private IPersonRepository _personRepository;

public PersonAppService(IPersonRepository personRepository)
{
_personRepository = personRepository;
Logger = NullLogger.Instance;
}

public void CreatePerson(string name, int age)
{
Logger.Debug("Inserting a new person to database with name = " + name);
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
Logger.Debug("Successfully inserted!");
}
}

IPersonRepository được inject từ constructor và ILogger được inject với một public property. Theo cách này, code của bạn sẽ không nhận biết gì về dependency injection framework. Đây là cách dùng DI system đúng đắn nhất.

  • IIocResolver, IIocManager và IScopedIocResolver

Bạn có thể phải trực tiếp resolve các dependency chứ không thể dùng constructor & property injection. Tất nhiên resolve bằng cách này là điều cần tránh và hạn chế tối đa. ASP.NET Boilerplate cung cấp một vài service có thể được inject và sử dụng dễ dàng. Ví dụ

public class MySampleClass : ITransientDependency
{
private readonly IIocResolver _iocResolver;

public MySampleClass(IIocResolver iocResolver)
{
_iocResolver = iocResolver;
}

public void DoIt()
{
//Resolving, using and releasing manually
var personService1 = _iocResolver.Resolve<PersonAppService>();
personService1.CreatePerson(new CreatePersonInput { Name = "John", Surname = "Doe" });
_iocResolver.Release(personService1);

//Resolving and using in a safe way
using (var personService2 = _iocResolver.ResolveAsDisposable<PersonAppService>())
{
personService2.Object.CreatePerson(new CreatePersonInput { Name = "John", Surname = "Doe" });
}
}
}

IIocResolve được dùng để resolve và release object. Có một vài overload của Resolve method. Release method được dùng để release một component (object). Rất quan trọng phải gọi Release method nếu bạn manually resolve một object. Nếu không ứng dụng của bạn có thể bị memory leak. Để đảm bảo việc release object, dùng ResolveAsDisposable bất cứ khi nào có thể (như trong ví dụ trên). Release được tự động gọi khi kết thúc using block.

IIocResolver (và IIocManager) cũng có CreateScope extension method (định nghĩa trong Abp.Dependency namespace) để release tất cả các resolved dependency một cách an toàn. Ví dụ:

using (var scope = _iocResolver.CreateScope())
{
var simpleObj1 = scope.Resolve<SimpleService1>();
var simpleObj2 = scope.Resolve<SimpleService2>();
//...
}

Kết thúc using block, tất cả các resolved dependency sẽ được tự động remove. Một scope cũng có thể inject sử dụng IScopedIocResolver. Bạn có thể inject interface này và resolve dependency. Khi class của bạn được release, tất cả các resolved dependency sẽ được sẽ được release. Dùng cái này một cách cẩn thận! Nếu lớp của bạn có một long life (giả sử một singleton), và bạn đang resolve quá nhiều object, thì tất cả chúng sẽ được giữ lại trong memory đến khi class của bạn được release.

Nếu bạn muốn chạm trực tiếp vào IoC Container (Castle Windsor) để resolve các dependency, bạn có thể dùng constructor inject một IIocManager và dùng thuộc tính IIocManager.IocContainer. Nếu bạn trong một static context hoặc không thể inject IIocManager, với giải pháp cuối cùng, bạn có thể dùng singleton object IocManager.Instance ở mọi nơi. Tuy nhiên, trong trường hợp này code của bạn sẽ không dễ dàng để test.

Extra: IShouldInitialize interface:

Một vài class cần được initialize trước khi nó được sử dụng lần đầu. IShouldInitialize có một Initialize() method. Nếu bạn implement nó, thì Initialize() method được tự động gọi ngay sau khi tạo object (trước khi nó được dùng). Bạn cần inject/resolve object để làm việc với tính năng này. (?)

(3) ASP.NET Core Integration

ASP.NET Core đã có sẵn một built-in dependency injection system với Microsoft.Extensions.DependencyInjection package. ASP.NET Boilerplate dùng Castle.Windsor.MsDependencyInjection package để tích hợp DI system của nó với ASP.NET Core, bởi vậy bạn không cần phải suy nghĩ về nó.

Final Notes

ASP.NET Boilerplate đơn giản và tự động hoá Dependency Injection chỉ cần bạn tuân thủ theo các rule và sử dụng các cấu trúc bên trên. Hầu hết các cấu trúc này sẽ đáp ứng đủ các nhu cầu của bạn. Nếu bạn cần nhiều hơn, bạn có thể trực tiếp sức mạnh của Castle Windsor để thực hiện nhiều task, kiểu như custom registration, injection hook, interceptor và những thứ khác.

--

--

Quang Trong VU
Old Dev
Editor for

Software Developer — Life’s a journey — Studying and Sharing