Dependency Injection và Inversion of Control – Phần 3: DI Container. Áp dụng DI vào ASP.NET MVC

Series bài viết Dependency Injection và Inversion of Control gồm 3 phần:

  1. Định nghĩa
  2. Áp dụng DI vào code
  3. Viết DI Container. Áp dụng DI vào ASP.NET MVC

Sau 2 phần đầu, chắc các bạn đã có cái nhìn tổng quan về DI và cách áp dụng chúng vào code. Đa phần chúng ta không tự viết sử dụng các DI Container nổi tiếng như: Unity, NInject, StructureMap.

Để hiểu nguyên lý hoạt động của chúng, mình sẽ cùng các bạn cách viết một DI Container đơn giản (chúng cũng không quá “ghê gớm” hay phức tạp như bạn nghĩ đâu). Sau đó mình sẽ hướng dẫn cách sử dụng cái DI Container có sẵn, cũng như áp dụng IoC và project MVC.

1. Tự viết 1 DI Container đơn giản

Các bạn có thể dùng git để clone project về máy và bắt đầu làm theo mình: https://github.com/ToiDiCodeDaoSampleCode/SimpleIoC. Các class và interface vẫn như trong phần 2, có điều mình đã bổ sung thêm 1 số class mock – module giả. Trong thực tế, ta sử dụng các class mock này để viết Unit Test.

solution

DI Container thường có 1 function dùng để setup module và interface, một function khác để lấy module dựa theo interface. Ở đây mình gọi 2 function đó là SetModule GetModule.

    public class DIContainer
    {
        public static void SetModule<TInterface, TModule>()
        {
            SetModule(typeof(TInterface), typeof(TModule));
        }

        public static T GetModule<T>()
        {
            return (T)GetModule(typeof(T));
        }
    }

Code của hàm Main cũng rất đơn giản. Ta chỉ cài đặt các interface và module tương ứng thông qua function SetModule. Với class Cart, ta chỉ cần gọi hàm GetModule. DIContainer sẽ tự inject IDatabase, ILogger vào theo code ta đã viết.

     //Với mỗi Interface, ta define một Module tương ứng
     DIContainer.SetModule<IDatabase, Database>();
     DIContainer.SetModule<ILogger, Logger>();
     DIContainer.SetModule<IEmailSender, EmailSender>();

     DIContainer.SetModule<Cart, Cart>();

     //DI Container sẽ tự inject Database, Logger vào Cart
     var myCart = DIContainer.GetModule<Cart>(); 

Class DI Container sẽ có các đặc tính sau:

  • Lưu trữ các Interface, Module tương ứng vào một Dictionary có Key là Interface, Value là Module. Để lấy một Module từ Container, ta cần đưa vào Interface của Module đó.
  • Khi cài đặt một module, container sẽ tìm Constructor đầu tiên của module đó.
  • Nếu contructor không có tham số (Module không có dependency), container sẽ gọi constructor này để khởi tạo module.
  • Nếu constructor này có tham số (Có dependency), container sẽ khởi tạo các tham số này, gán chúng vào constructor của module. Đây là quá trình injection.

Việc implement cũng không phức tạp lắm, bạn đọc code và comment sẽ hiểu thôi.

    public class DIContainer
    {
        //Dictionary để chứa các interface và module tương ứng
        private static readonly Dictionary<Type, object> 
                   ResgisteredModules = new Dictionary<Type, object>();

        //Hai hàm cơ bản, ở đây mình chuyển <T> thành 
        //dạng Type trong C# để dễ viết code
        public static void SetModule<TInterface, TModule>()
        {
            SetModule(typeof(TInterface), typeof(TModule));
        }

        public static T GetModule<T>()
        {
            return (T)GetModule(typeof(T));
        }


        private static void SetModule(Type interfaceType, Type moduleType)
        {
            //Kiểm tra module đã implement interface chưa
            if (!interfaceType.IsAssignableFrom(moduleType))
            {
                throw new Exception("Wrong Module type");
            }

            //Tìm constructor đầu tiên
            var firstConstructor = moduleType.GetConstructors()[0];
            object module = null;
            //Nếu như không có tham số
            if (!firstConstructor.GetParameters().Any())
            {
                //Khởi tạo module
                module = firstConstructor.Invoke(null); // new Database(), new Logger()
            }
            else
            {
                //Lấy các tham số của constructor
                var constructorParameters = firstConstructor.GetParameters(); //IDatebase, ILogger

                var moduleDependecies = new List<object>();
                foreach (var parameter in constructorParameters)
                {
                    var dependency = GetModule(parameter.ParameterType); //Lấy module tương ứng từ DIContainer
                    moduleDependecies.Add(dependency);
                }

                //Inject các dependency vào constructor của module
                module = firstConstructor.Invoke(moduleDependecies.ToArray());
            }
            //Lưu trữ interface và module tương ứng
            ResgisteredModules.Add(interfaceType, module);
        }

        private static object GetModule(Type interfaceType)
        {
            if (ResgisteredModules.ContainsKey(interfaceType))
            {
                return ResgisteredModules[interfaceType];
            }
            throw new Exception("Module not register");
        }
    }

Kết quả:

rs

2. Sử dụng DI Container từ các framework có sẵn

Tất nhiên, nếu người khác đã viết sẵn, kiểm thử và fix lỗi, chúng ta có thể tái sử dụng mà không cần phải viết lại từ đầu cho mệt. Mình sẽ hướng dẫn các bạn sử dụng DI Container của NinjectUnity.

Dùng Nuget (hoặc Package Manager Console) để cài đặt NinjectUnity. Nhấp chuột phải vào project SimpleIoC, chọn Manage Nuget packages.

1 2

Vì bạn đã chia code ra thành các module và interface rồi, ta chỉ cần thay code của DIContainer thành code Unity và Ninject là được.

Ninject

     var kernel = new StandardKernel();
     kernel.Bind<IDatabase>().To<Database>();
     kernel.Bind<ILogger>().To<Logger>();
     kernel.Bind<IEmailSender>().To<EmailSender>();
     kernel.Bind<Cart>().To<Cart>();

     //DI Container sẽ tự inject Database, Logger vào Cart
     var myCart = kernel.Get<Cart>();

Unity

     var container = new UnityContainer();
     container.RegisterType<IDatabase, Database>();
     container.RegisterType<ILogger, Logger>();
     container.RegisterType<IEmailSender, EmailSender>();
     container.RegisterType<Cart, Cart>();

     //DI Container sẽ tự inject Database, Logger vào Cart
     var myCart = container.Resolve<Cart>();

3. Áp dụng IoC vào project MVC

Đa phần các framework như Spring.NET, ASP.NET MVC, Spring, Struts đều có sẵn DI Container, hoặc cho phép tích hợp DI Container bên ngoài. Vì đây là blog C# nên mình sẽ hướng dẫn cách tích hợp IoC vào project MVC. Mọi công đoạn chỉ mất từ 2-5 phút. (WebForm không áp dụng IoC được vì Page Cycle khá phức tạp, không cho phép ta can thiệp vào quá trình khởi tạo page).

Bước 1. Tạo 1 project MVC trong cùng Solution. Add References tới project SimpleIoC để tái sử dụng class cho tiện.

4

Bước 2. Thêm parameter vào constructor của controller, module Cart sẽ được inject vào. Nếu chạy thử bạn sẽ bị lỗi “No paramterless constructor …”

5 6

Bước 3. Sử dụng Nuget, tìm và cài Unity Bootstraper for ASP.NET MVC (Các DI Container khác như Ninject, StructureMap đều có Bootstraper cho MVC cả, đừng lo). Ta sẽ thấy có file mới tên là UnityConfig được tạo ra.

Ta vào file này và thực hiện việc setup các interface và module. Tùy theo phiên bản MVC mà nội dung các file tạo ra có khác nhau chút đỉnh. Tuy nhiên cú pháp cài đặt vẫn như cũ.

8

Bước 4. Chạy thử và … xong. Chúc mừng bạn đã hoàn thành bài viết 3 phần về IoC đầy khó khăn và gian khổ.

10

Nhờ IoC và mock, ta có thể Unit Test từng module riêng lẻ. Các bạn có thể xem thêm ở đây: https://duyphuong13.wordpress.com/2013/12/15/mot-so-ky-thuat-trong-unit-test/, vì có bạn đã viết khá chi tiết rồi nên mình không viết lại nữa. Nếu có góp ý hay gạch đá gì các bạn cứ ném thoải mái trong phần comment nhé.

16 thoughts on “Dependency Injection và Inversion of Control – Phần 3: DI Container. Áp dụng DI vào ASP.NET MVC”

  1. Nếu không dùng framework có sẵn thì làm sao để inject Cart vào HomeController(Cart cart) được vậy bạn? Mình chạy nó báo “No parameterless constructor defined for this object”

    Like

  2. Cảm ơn anh nhiều về bài viết!
    Em có một thắc mắc là nếu mình có 2 module cấp cao lần lượt cần sử dụng 2 module cấp thấp AFormatter, BFormatter. 2 module cấp thấp đều implement interface IFormatter. Trong trường hợp này mình có áp dụng được DI không anh?

    Like

  3. mình loay hoay code lại bằng java mà cái hàm trong DIContainer khó hiểu quá.
    Không biết chỉnh sửa lại như thế nào cho hợp lí ở trong class đó vậy chủ thớt

    Like

  4. public static void SetModule()
    public static T GetModule()

    viết trong java thì nó là gì vậy nhỉ?
    Trong java ko có kiểu Type nên mình hơi rối cách triển khai?

    Like

  5. private static readonly Dictionary ResgisteredModules = new Dictionary();
    Bài viết dễ hiểu nhưng e thắc mắc: Tại sao phải dùng readonly vậy a code dạo?
    Cảm ơn!

    Like

  6. Mình nghĩ, trong hàm SetModule ko declare biến object module, mà chỉ cần Invoke constructor có param hay ko param của Module là đủ. Và sau khi check xem Module đã implement Interface ? Nếu chưa throw Exception, nếu đã implement thì phải add vào Dictionary RegisteredModule trước khi làm những process khác, vì vậy khi gọi hàm GetModule mới trả về được Module tương ứng để inject vào constructor trước khi Invoke constructor đó.

    Like

Leave a comment