Thứ Ba, 15 tháng 1, 2013

Áp dụng Dependency Injection với StructureMap vào ASP.NET MVC


Bài viết này nhằm giới thiệu về mẫu Dependency Injection và cách áp dụng chúng vào ASP.NET MVC bằng thư viện StructureMap.

1. Giới thiệu về Dependency Injection (DI)

Dependency Injection (gọi tắt là DI) là một mẫu thiết kế phần mềm cho phép chọn lựa thành phần phần mềm cần sử dụng tại thời điểm chạy ứng dụng chứ không phải tại thời điểm biên dịch. Và nhớ thế, nó có thể được dùng như là một cách để nạp các plugin một cách tự động, hoặc tự động thay đổi đối tượng cần sử dụng ở trong các môi trường khác nhau (ví dụ như môi trường test và môi trường thực tế).
DI là mẫu thiết kế bao gồm ít nhất 3 thành phần sau:
·         Một đối tượng/lớp có các lời gọi đến các thành phần chưa biết trước
·         Lời khai báo các thành phần mà lớp đó phụ thuộc, đó là các interface định nghĩa các phương thức mà lớp nói trên có thể sử dụng.
·         Một injector (đôi khi được gọi là provider hoặc container) giúp tạo ra các đối tượng của các lớp được cài đặt áp dụng theo các interface được yêu cầu nói trên.
Dưới đây là một hình vẽ mô tả mẫu thiết kế DI

Hình vẽ này minh hoạt một lớp A cần sử dụng một số phương thức được mô tả trong một interface là ISerrvice A, trong thời gian phát triển, ClassA hoàn toàn không có nhận thức về các kế thừa của IServiceA, nó chỉ gọi các phương thức của một đối tượng IServiceA. Khi chương trình khởi chạy, thì đối tượng Builder sẽ khởi tạo một đối tượng ClassA và gắn vào nó những đối tượng mà nó phụ thuộc, trong trường hợp đối tượng nó bị thuộc là IServiceA, Builder sẽ khởi tạo một đối tượng ServiceA và gắn vào cho đối tượng ClassA.
Một số câu hỏi bạn sẽ đặt ra, đó là, tại sao lại là đối tượng ServiceA mà không phải các đối tượng khác, tại vì sao Builder (hay Container) biết mà inject (gắn) vào…v.v Các thắc mắc này sẽ được làm rõ qua ví dụ và qua các project mà bạn sẽ tự làm trong tương lai.
Một số trường hợp bạn sẽ cần áp dụng DI
·         Nếu bạn muốn dự án có thể mở rộng một cách dễ dàng
·         Nếu bạn muốn dự án có thể hỗ trợ nhiều cấu hình triển khai khác nhau
·         Bạn  muốn áp dụng TDD (Test Driven Development) vào dự án và muốn ứng dụng được kiểm thử tự động trong đa số trường hợp
·         Nếu bạn muốn cô lập hóa một số hệ thống con khỏi ứng dụng và sau đó có thể có các giải pháp thay thế, tuy nhiên lại không làm xáo trộn toàn bộ hệ thống.
·         Bạn muốn áp dụng cơ chế plug-in cho các thành phần của ứng dụng

Bản thân người viết, đã sử dụng DI trong vòng hơn bốn năm qua, từ Castle Windsor đến StructureMap, và đến nay là Autofac hoặc MEF. Sử dụng DI có cái lợi là rất tốt cho việc áp dụng UnitTest và TDD, tuy nhiên, điểm bất lợi là khiến cho bạn sẽ mất nhiều thời gian hơn một chút khi bạn chưa quen, và có những vấn đề bạn sẽ gặp khó khăn khi áp dụng DI. Ví dụ đơn giản nhất là câu hỏi tôi cần cung cấp bao nhiêu đối tượng DbContext (khi dùng Entity Framework) cho ứng dụng, một đối tượng trên 1 request, một đối tượng cho toàn thời gian sống của chương trình ? Hay làm sao để áp dụng DI cho việc xây dựng các WCF/Web Services, hoặc làm sao để áp dụng vào việc sử dụng các WCF Services. Để trả lời được các câu hỏi kiểu như trên, bạn nên luyện tập sử dụng dần dần và áp dụng vào các dự án từ nhỏ đến lớn, đừng vội hăm hở áp dụng ngay vào các dự án chính của mình nếu bạn chưa có kinh nghiệm.
Một tiết lộ nho nhỏ, ba website cấu thành nên Jou Lập trình đều áp dụng Dependency Injection ngay từ đầu và chúng chạy khá tốt. Chúng sử dụng Autofac. Còn tại sao tôi giới thiệu StructureMap với các bạn, vì nó dễ sử dụng và sẽ là một khởi đầu suôn sẻ với DI nếu bạn là người mới bắt đầu.

2. StructureMap – Open source dependency injector for .NET

Có nhiều công cụ đóng vai trò của một Builder (Container/Injector), nổi tiếng nhất vẫn là Microsoft Unity, Castle Windsor, Structure Map, NInject và Autofac. Tuy nhiên, Microsoft Unity và Castle Windsor khá phức tạp trong việc cài đặt, các phương tiện như Structure Map và Ninject dễ sử dụng hơn.
Trước đây DI thường chỉ được áp dụng cho các dự án lớn dành cho doanh nghiệp, nhưng với StructureMap và sự đơn giản mà nó mang lại, bạn có thể áp dụng nó một cách dễ dàng. Tuy nhiên, hiện tại StructureMap không được cập nhật nhiều từ năm 2009, tuy nhiên vì nó đơn giản nhưng rất mạnh mẽ và dễ sử dụng mà người ta vẫn dùng StructureMap như một trong những lựa chọn hàng đầu khi muốn áp dụng Dependency Injection vào ứng dụng của mình. Đáng tiếc StructureMap không hỗ trợ cho các ứng dụng Silverlight/Windows Phone 7. Nếu bạn muốn áp dụng DI với các ứng dụng SL/WP7, bạn nên lựa chọn Ninject hoặc Autofac. Với tôi thì Autofac là sự thay thế tuyệt với đối với StrutureMap, nhưng Ninject có vẻ dễ sử dụng hơn.
StructureMap giúp việc áp dụng DI dễ dàng hơn nhiều so với trước đây. Khi bạn bắt đầu viết ứng dụng với tư duy là luôn áp dụng nguyên lý DI, thì mã của bạn sẽ dễ hiểu hơn, dễ dàng thay đổi hơn, ít lỗi hơn và đặc biệt là linh hoạt hơn. Thay vì đau đầu mỗi khi muốn thay đổi một thành phần trong ứng dụng vì sợ nó sẽ làm hỏng các thành phần khác, khi bạn sử dụng DI, bạn sẽ biến ứng dụng của bạn thành các thành phần độc lập, không lặp lại; các thành phần có khả năng gắn kết cao, và vì thế bạn có thể dễ dàng kết hợp chúng lại để tạo nên một ứng dụng tuyệt vời.
Để sử dụng StructureMap, bạn cần viếng thăm website của công cụ này (http://docs.structuremap.net ), hãy đọc và làm theo các ví dụ, khi bạn đã quen với StructureMap, bạn có thể sử dụng bất kỳ công cụ DI/IoC nào khác một cách dễ dàng.. ^^

3. Demo

Tôi sẽ sử dụng lại ví dụ của một bài viết về Linq-to-XML để biểu diễn cho bài viết này, tuy nhiên nó sẽ được thay đổi tên đôi chút và được thay đổi để phù hợp với một ứng dụng áp dụng DI. Bạn có thể ghé thăm bài viết Giới thiệu về Linq-to-XML và đọc sơ qua bài viết nếu bạn chưa biết về Linq-to-XML và muốn hiểu rõ hơn về logic của ứng dụng Demo về quản lý các ghi chú (Note).
Giả sử tôi đã sử dụng ứng dụng được tạo ở bài viết Giới thiệu về Linq-to-XML để quản lý các ghi chú, tuy nhiên, sau đó một thời gian, vì muốn sử dụng CSDL quan hệ để có thể cải tiến về sau, tôi cần phải thay đổi NoteRepository để có thể truy xuất dữ liệu trong SQL Server hoặc SQLCE. Nhưng tôi không muốn sửa code của XmlNoteRepository cũ vì nó vẫn tốt trong các trường hợp đơn giản, tôi muốn làm thêm một Repository mới, và muốn có sự tồn tại của hai phiên bản repository ở đâu đó trong ứng dụng, đặc biệt khi cần áp dụng Repository nào, tôi không cần phải đi thay đổi các lớp đang sử dụng chúng.

3.1. Định nghĩa giao diện INoteRepository

Việc đầu tiên, tôi sẽ định nghĩa một giao diện chung, một contract, để khi thiết lập các Repository cho Note đều phải tuân thủ theo, đó là INoteRepository với cài đặt như sau:


using System.Collections.Generic;
using DependencyInjectionEx.Models;
 
namespace DependencyInjectionEx.Repositories
{
    public interface INoteRepository
    {
        void Insert(Note note);
        IList GetAll();
        bool Delete(int id);
        bool Update(Note note);
        Note Get(int id);
    }
}
 

3.2. Điều chỉnh lại XmlNoteRepository

Điều chỉnh lại XmlNoteRepository để nó kế thừa từ INoteRepository:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using DependencyInjectionEx.Models;
 
namespace DependencyInjectionEx.Repositories
{
    public class XmlNoteRepository : INoteRepository
    {
        private readonly string _filePath;
 
        public XmlNoteRepository()
        {
            _filePath = HttpContext.Current.Server.MapPath("~/App_Data/Data.xml");
        }
 
        public void Insert(Note note)
        {
            var doc = XDocument.Load(_filePath);
            try
            {
                var lastId = (from c in doc.Descendants("Note")
                              select (int)c.Element("Id")
                            ).OrderBy(x => x).Max();
 
                note.Id = lastId + 1;
 
            }
            catch (Exception)
            {
                note.Id = 1;
            }
 
            var noteNode =
                      new XElement("Note",
                      new XElement("Id", note.Id),
                      new XElement("Content", note.Content),
                      new XElement("Title", note.Title),
                      new XElement("CreateDate", note.CreateDate.ToShortDateString())
                  );
            doc.Element("Notes").Add(noteNode);
            doc.Save(_filePath);
            //doc.Descendants("Customer").Single(t => t.Descendants("CustomerID").Single().Value == "1")
 
            //    .Add(purchase);
        }
 
        public IList GetAll()
        {
            var doc = XDocument.Load(_filePath);
            try
            {
                var list = (from c in doc.Descendants("Note")
                            select new Note
                                       {
                                           Id = (int)c.Element("Id"),
                                           Title = (string)c.Element("Title"),
                                           Content = (string)c.Element("Content"),
                                           CreateDate = (DateTime)c.Element("CreateDate")
                                       }
                            ).OrderByDescending(x => x.Id).ToList();
                return list;
            }
            catch (Exception)
            {
                return new List();
            }
        }
 
        public bool Delete(int id)
        {
            try
            {
                var doc = XDocument.Load(_filePath);
                doc.Elements("Notes").Elements("Note").Where(x => x.Element("Id").Value == id.ToString()).Remove();
                doc.Save(_filePath);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
 
        }
 
        public bool Update(Note note)
        {
            try
            {
                var doc = XDocument.Load(_filePath);
                var noteNode = doc.Elements("Notes").Elements("Note").Where(x => x.Element("Id").Value == note.Id.ToString()).Take(1);
                noteNode.Elements("Title").SingleOrDefault().Value = note.Title;
                noteNode.Elements("Content").SingleOrDefault().Value = note.Content;
                doc.Save(_filePath);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }
 
        public Note Get(int id)
        {
            var doc = XDocument.Load(_filePath);
            try
            {
                var note = (from c in doc.Descendants("Note")
                            where c.Element("Id").Value == id.ToString()
                            select new Note
                            {
                                Id = (int)c.Element("Id"),
                                Title = (string)c.Element("Title"),
                                Content = (string)c.Element("Content"),
                                CreateDate = (DateTime)c.Element("CreateDate")
                            }
                            ).SingleOrDefault();
                return note;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}
 

3.3. Điều chỉnh NoteController

Chúng ta không muốn NoteController biết nó đang sử dụng instance (thực thể) nào của INoteRepository, hay nói một cách khác, việc sử dụng lớp kế thừa nào của INoteRepository được quyết định tại thời điểm chạy ứng dụng (runtime) và độc lập với các lớp sử dụng chúng (như NoteController). Mã của NoteController sẽ được điều chỉnh để chỉ nhận biến cho constructor là một đối tượng kiểu INoteRepository:


using System;
using System.Web.Mvc;
using DependencyInjectionEx.Models;
using DependencyInjectionEx.Repositories;
 
namespace DependencyInjectionEx.Controllers
{
    public class NoteController : Controller
    {
        private readonly INoteRepository _noteRepository;
        //
        // GET: /Note/
        public NoteController(INoteRepository noteRepository)
        {
            _noteRepository = noteRepository;
        }
 
        public ActionResult Index()
        {
            var items = _noteRepository.GetAll();
            return View(items);
        }
 
        [HttpGet]
        public ActionResult Create()
        {
            return View();
        }
        [HttpPost]
        public ActionResult Create(Note note)
        {
            note.CreateDate = DateTime.Now;
            if (ModelState.IsValid)
            {
                _noteRepository.Insert(note);
                return RedirectToAction("Index");
            }
 
            return View(note);
        }
 
        public ActionResult Delete(int id)
        {
            var result = _noteRepository.Delete(id);
            return RedirectToAction("Index");
        }
 
        [HttpGet]
        public ActionResult Edit(int id)
        {
            var note = _noteRepository.Get(id);
            return View(note);
        }
 
        [HttpPost]
        public ActionResult Edit(Note note)
        {
            if (ModelState.IsValid)
            {
                _noteRepository.Update(note);
                return RedirectToAction("Index");
            }
            return View(note);
        }
    }
}
 
 Bạn hãy lưu ý những dòng in đậm, và có thể bạn sẽ thắc mắc, nếu mình chạy ứng dụng bây giờ thì chuyện gì sẽ xảy ra nhỉ? Câu chuyện xảy ra đó là ứng dụng MVC sẽ không biết xử lý thế nào với một NoteController không có cấu tử không có tham số (parameter less constructor), và nó càng không biết phải đối xử thế nào với tham số INoteRepository.
May thay, ASP.NET MVC có hỗ trợ Dependency Injection đó là DependencyResolver, bạn chỉ cần viết lớp kế thừa giao diện IDependencyResolver với các phương thức cần cài đặt là GetService (lấy dịch vụ tương ứng với kiểu dữ liệu là tham số đưa vào) và GetServices (lấy hết tất cả các dịch vụ là thực thể tương ứng với kiểu dữ liệu là tham số đưa vào). Nhưng chúng ta biết cài đặt lớp kế thừa từ IDenpendencyResolver như thế nào đây? Thắc mắc này sẽ được giải đáp sau, chúng ta sẽ cài đặt thêm một phiên bản của INoteRepository để dễ dàng thay đổi khi cần thiết.

3.4. Cài đặt thêm NoteRepository để truy xuất dữ liệu thông qua Entity Framework Code First

Để sử dụng EF Code First, chúng ta cần tạo ra lớp kế thừa từ lớp DbContext với tên gọi là NoteDbContext:


using System.Data.Entity;
using DependencyInjectionEx.Models;
 
namespace DependencyInjectionEx.Repositories
{
    public class NoteDbContext: DbContext
    {
        public DbSet  Notes { get; set; }
    }
}
 
 Tiếp theo chúng ta cài đặt cho NoteRepository kế thừa từ INoteRepository và sử dụng NoteDbContext:


using System.Collections.Generic;
using System.Linq;
using DependencyInjectionEx.Models;
 
namespace DependencyInjectionEx.Repositories
{
    public class NoteRepository: INoteRepository
    {
        private readonly NoteDbContext _db = new NoteDbContext();
 
        public void Insert(Note note)
        {
            _db.Notes.Add(note);
            _db.SaveChanges();
        }
 
        public IList GetAll()
        {
            return _db.Notes.ToList();
        }
 
        public bool Delete(int id)
        {
            var note = _db.Notes.SingleOrDefault(x => x.Id == id);
 
            if (note != null)
            {
                _db.Notes.Remove(note);
                _db.SaveChanges();
                return true;
            }
 
            return false;
        }
 
        public bool Update(Note note)
        {
            var updateNote = Get(note.Id);
 
            if (updateNote == null) return false;
 
            updateNote.Title = note.Title;
            updateNote.Content = note.Content;
            updateNote.CreateDate = note.CreateDate;
 
            _db.SaveChanges();
            return true;
        }
 
        public Note Get(int id)
        {
            var note = _db.Notes.SingleOrDefault(x => x.Id == id);
            return note;
        }
    }
}
 
 Để NoteRepository có thể kết nối được với Database, chúng ta cần định nghĩa một chuỗi kết nối và đặt nó trong tập tin Web.config với tên gọi là NoteDbContext. Với ví dụ này tôi sử dụng một CSDL SqlCE 4.0 và lưu nó ở thư mục App_Data, bạn không cần phải tạo nó từ trước, NoteDbContext khi chạy lần đầu tiên sẽ tạo ra nó. Điều chỉnh hoặc thêm vào node như sau:



  
    "NoteDbContext"
connectionString="Data Source=|DataDirectory|MyData.sdf;Persist Security Info=False;" providerName="System.Data.SqlServerCE.4.0" />

3.5 Sử dụng StructureMap để áp dụng DI vào ứng dụng

Lúc này ứng dụng của bạn vẫn chưa thể chạy được, khi truy xuất vào NoteController sẽ gây ra lỗi, và vì vậy chúng ta cần sự trợ giúp của các thư viện hỗ trợ DI và hỗ trợ DI cho ASP.NET MVC 3 như StructureMap, NInject, Unity.. Ở đây tôi chọn StructureMap.
Hãy sử dụng Nuget và cài đặt hai gói dưới đây:

Lúc đó thư viện StructureMap sẽ được tham chiếu trong dự án của bạn, và sẽ xuất hiện hai lớp IoC và SmDependencyResolver hỗ trợ DI tại thư mục DependencyResolution.
Lớp IoC đóng vai trò quyết định trong việc lựa chọn dịch vụ/lớp cho contract/interface. Giả sử chúng ta chọn XmlNoteRepository cho INoteRepository, chúng ta cần xác định chúng trong lớp IoC như sau:


using DependencyInjectionEx.Repositories;
using StructureMap;
 
namespace DependencyInjectionEx.DependencyResolution {
    public static class IoC {
        public static IContainer Initialize() {
            ObjectFactory.Initialize(x =>
                                         {
                                             x.Scan(scan =>
                                                        {
                                                            scan.TheCallingAssembly();
                                                            scan.WithDefaultConventions();
                                                        });
                                             x.For().HttpContextScoped().Use();
                                         });
            return ObjectFactory.Container;
        }
    }
}
 
 Thứ hai sẽ là lớp SmDependencyResolver:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using DependencyInjectionEx.Repositories;
using StructureMap;
 
namespace DependencyInjectionEx.DependencyResolution
{
    public class SmDependencyResolver : IDependencyResolver {
 
        private readonly IContainer _container;
 
        public SmDependencyResolver(IContainer container) {
            _container = container;
        }
 
        public object GetService(Type serviceType) {
            if (serviceType == null) return null;
            try {
                  return serviceType.IsAbstract || serviceType.IsInterface
                           ? _container.TryGetInstance(serviceType)
                           : _container.GetInstance(serviceType);
            }
            catch {
 
                return null;
            }
        }
 
        public IEnumerable<object> GetServices(Type serviceType) {
            return _container.GetAllInstances(serviceType).Cast<object>();
        }
    }
}
 
 Khi bạn muốn NoteRepository là sự lựa chọn mặc định, bạn hãy điều chỉnh dòng code trong IoC như dưới đây


x.For().HttpContextScoped().Use(); 
 Nhờ vào thư viện WebActivator.dll, hai lớp trên sẽ được sử dụng một cách tự động trong ứng dụng của bạn, và bạn hãy thử tải ví dụ về và hạy chạy thử, bạn sẽ thấy ngạc nhiên, kỳ lạ, và sẽ dần yêu thích DI.
Lúc này, bạn sẽ đặt ra một câu hỏi, tôi viết cả mấy chục dòng code chỉ để làm một việc đơn giản là khởi tạo đối tượng _noteRepository ngay trong constructor của NoteController. Có lợi ích gì ở đây?
Bạn hãy thử tưởng tượng, chúng ta có hàng chục lớp sử dụng INoteRepository và bạn phải đi thay thế hàng chục nơi mỗi khi bạn cần thay đổi loại CSDL, và mệt mỏi hơn nữa nếu có hàng chục các dịch vụ khác nhau có nhiều biến thể như vậy. Thay đổi một nơi duy nhất, hoặc thay đổi trong tập tin cấu hình mà không cần biên dịch lại sẽ là sự lựa chọn tốt hơn nhiều.

4. Kết luận

Trong bài viết này tôi đã chia sẻ cùng bạn về Dependency Injection một cách sơ lược và cách áp dụng thư viện DI là StructureMap.dll vào ứng dụng ASP.NET MVC 3. Chúc các bạn ứng dụng DI thành công!
GetServices(Type serviceType) { return _container.GetAllInstances(serviceType).Cast(); } } } Khi bạn muốn NoteRepository là sự lựa chọn mặc định, bạn hãy điều chỉnh dòng code trong IoC như dưới đây x.For().HttpContextScoped().Use(); Nhờ vào thư viện WebActivator.dll, hai lớp trên sẽ được sử dụng một cách tự động trong ứng dụng của bạn, và bạn hãy thử tải ví dụ về và hạy chạy thử, bạn sẽ thấy ngạc nhiên, kỳ lạ, và sẽ dần yêu thích DI. Lúc này, bạn sẽ đặt ra một câu hỏi, tôi viết cả mấy chục dòng code chỉ để làm một việc đơn giản là khởi tạo đối tượng _noteRepository ngay trong constructor của NoteController. Có lợi ích gì ở đây? Bạn hãy thử tưởng tượng, chúng ta có hàng chục lớp sử dụng INoteRepository và bạn phải đi thay thế hàng chục nơi mỗi khi bạn cần thay đổi loại CSDL, và mệt mỏi hơn nữa nếu có hàng chục các dịch vụ khác nhau có nhiều biến thể như vậy. Thay đổi một nơi duy nhất, hoặc thay đổi trong tập tin cấu hình mà không cần biên dịch lại sẽ là sự lựa chọn tốt hơn nhiều. 4. Kết luận Trong bài viết này tôi đã chia sẻ cùng bạn về Dependency Injection một cách sơ lược và cách áp dụng thư viện DI là StructureMap.dll vào ứng dụng ASP.NET MVC 3. Chúc các bạn ứng dụng DI thành công!

Không có nhận xét nào: