Viết ASP.NET bằng MVP và NHibernate phần 4 - Castle Windsor
Posted On Friday, October 31, 2008 at at 9:20 PM by UnknownBài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết NHibernate Best Practices with ASP.NET, 1.2nd Ed trên code Project. Các code sample trong bài dựa vào database Northwind của Microsoft và tham khảo 99,99% từ code mẫu của tác giả Billy McCafferty.
I. Giới thiệu Inversion of Control và Castle Windsor
I.1 Giới thiệu
- Có rất nhiều website và blog nói về Inversion of Control và Dependency Injection. Có nguồn cho rằng đây là hai khái niệm khác nhau (MSDN) nhưng sự phụ Martin Fowler thì cho rằng Dependency Injection là tên gọi khác của Inversion of Control để khỏi confused. Tui thì thấy giải thích của bác Martin Fowler khá ổn, dù gì cũng là người lớn tuổi nên chắc không lừa con nít tụi mình . Trên trang MSDN cúng giải thích khái niệm này rất bình dân nên dân đen như tụi mình rất dễ hiểu:
Inversion of Control (IoC) có nghĩa rằng các object không phải tự nó tạo ra các objects mà nó phụ thuộc. Thay vào đó, nó sẽ có những objects nó cần nhờ vào một bên thứ ba.
- Còn Inversion of Control Container là một library dựa trên nguyên tắc của IoC để hỗ trợ chúng ta trong việc tạo và hủy các đối tượng trong chương trình. Khi nhắc đến Dependency Injection, ngoài Spring.NET có lẽ người ta sẽ nghĩ đến Castle Windsor. Castle Windsor là một Container mà phần chính của nó là Castle MicroKernel (cũng là 1 container). Theo như Castle Prolect thì Windsor Container là một library dựa trên library MicroKernel nhưng có hỗ trợ thêm khả năng config uyển chuyển và một số tính năng khác. Nói cách khác, Windsor Container tiện dụng hơn MicroKernel và đối với những ai đã sử dụng Spring.NET thì sẽ thấy rằng Windsor cách dùng đơn giản như đang giỡn
Hình : Lâu đài Windsor ở Anh Quốc
I.2 Cách dùng Windsor
- Trước tiên hãy nghĩ xem Castle Windsor giúp chúng ta giải quyết problem gì. Người ta thường phân chia chương trình thành các module, các layer hay các tầng, tầng này khai báo Interface và tầng kia sẽ có những lớp implement các interface này. Như vậy với cách làm thông thường chắc chắn bằng cách nào đó sẽ có sự phụ thuộc, sẽ có các dòng code đại loại như : IOrderDao orderDao = new NHibernateOrderDao(). Và như vậy rõ ràng nơi nào sử dụng dòng code này đã có sự phụ thuộc vào module/layer chứa lớp NHibernateOrderDao. Người ta muốn tránh sự phụ thuộc này bằng cách thay dòng code có chữ new bằng một cách khác và cách người ta sử dụng chính là Dependency Injection. Có 3 khái niệm Dependency Injection: Constructor injection, Setter Injection và Interface Injection nhưng cái chúng ta thường gặp nhất là Interface Injection. Trong phần này ta sẽ tìm hiểu cách sử dụng Windsor Conainer và Interface Injection.
- Để khỏi phải new 1 object NHibernateOrderDao như trên, ta sẽ sử dụng Castle Windsor. Ta tạo 1 file config tên là Windsor.config rồi add vào project, nội dung của file này như sau:
<?xml version="1.0"?>
<configuration>
</configuration>
<configuration>
<components>
</components>
<component id="orderDao"
</component>
type="MyProjectName.Data.NHibernateOrderDao, MyProjectName.Data"
service="MyProjectName.Core.DataInterfaces.IOrderDao, MyProjectName.Core">
<parameters>
</parameters>
service="MyProjectName.Core.DataInterfaces.IOrderDao, MyProjectName.Core">
<parameters>
<nameOfParameter1>value1</nameOfParameter1>
<nameOfParameter2>100</nameOfParameter2>
<nameOfParameter2>100</nameOfParameter2>
</parameters>
</component>
</components>
</configuration>
Code 1: Nội dung file config cho Castle Windsor
- Bước thứ hai, mình mở mở file config và thêm vào như sau:
<?xml version="1.0"?>
<configuration>
</configuration>
<configuration>
<configSections>
</configSections>
<castle>
</castle>
<section name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
</configSections>
<castle>
<include uri="Windsor.config" />
</castle>
</configuration>
Code 2: Thêm config cho Windsor vào file app/web.config
- Theo như config trên thì file app/web config và file Windsor.config phải ở cùng thư mục với nhau. Trong Code1, ta khai báo một component với id là orderDao (có thể đặt tên gì cũng được), trong đó type và service lần lượt là Interface và class implement tương ứng. Trong phần parameters, ta khai báo các value cần thiết cho constructor của lớp NHibernateOrderDao. Và cuối cùng trong chương trình, ta sẽ dùng code sau đây để lấy được instance của class NHibernateOrderDao như mong muốn:
IWindsorContainer windsorContainer = new WindsorContainer(new XmlInterpreter());
IOrderDao = (IOrderDao)windsorContainer.Resolve("orderDao");
// Or : IOrderDao = (IOrderDao)windsorContainer.Resolve(typeof(IOrderDao));
IOrderDao = (IOrderDao)windsorContainer.Resolve("orderDao");
// Or : IOrderDao = (IOrderDao)windsorContainer.Resolve(typeof(IOrderDao));
Code 3: Lấy instance của class NHibernateOrderDao như mong muốn
II. Tạo Web project và cấu hình Windsor, NHibernate
II.1 Load config cho Windsor trong global.asax
- Sau khi tạo một project Web, add file Windsor.config và sửa file web.config như đã nói ở trên xong thì điều tiếp theo ta sẽ nghĩ xem nên load cái config ấy như thế nào. Rõ ràng không nên cứ load config này mỗi lần muốn Resolve 1 object. Tương tự như khi sử dụng log4net cho web application, chúng ta phải load config log4net 1 lần duy nhất và code cho hành động này được đặt ở file global.asax. Nhưng khác với log4net, chúng ta cần một cách nào đó để giữ lại instance của WindsorContainer nên không thể thực hiện trong Global.asax được. Cách của bác Billy rất hay, đó là viết 1 lớp CustomHttpApplication, đặt tất cả các cấu hình cần thiết vào Application_Start, kế đến khai báo một public property để giữ instance của Windsor Container trong suốt thời gian sống của Application và vì thế instance của Windsor Container có thể được sử dụng bất cứ nơi nào trong tầng Web, cụ thể là các Page và các UserControl:
private static IWindsorContainer windsorContainer;
public static IWindsorContainer WindsorContainer
{
get { return windsorContainer; }
}
public void Application_Start(object sender, EventArgs e) {
// Initialize log4net
XmlConfigurator.Configure();
// Create the Windsor Container for IoC.
// Supplying "XmlInterpreter" as the parameter tells Windsor
// to look at web.config for any necessary configuration.
windsorContainer = new WindsorContainer(new XmlInterpreter());
}
public void Application_End(object sender, EventArgs e) {
windsorContainer.Dispose();
}
public static IWindsorContainer WindsorContainer
{
get { return windsorContainer; }
}
public void Application_Start(object sender, EventArgs e) {
// Initialize log4net
XmlConfigurator.Configure();
// Create the Windsor Container for IoC.
// Supplying "XmlInterpreter" as the parameter tells Windsor
// to look at web.config for any necessary configuration.
windsorContainer = new WindsorContainer(new XmlInterpreter());
}
public void Application_End(object sender, EventArgs e) {
windsorContainer.Dispose();
}
Code 4: Giữ instance của WindsorContainer trong HttpApplication
II.2 Open Session In View Principle
- Nếu google cụm từ trên thì các bạn sẽ thấy rất nhiều topic viết về chủ đề này trong đó tui thấy nên đọc nhất là trang của Hibernate.org. Vấn đề chúng ta mong muốn giải quyết ở đây là hạn chế số lần NHibernate mở connection đến database. Trong một view bất kì (page/usercontrol) có thể ta sẽ cần load nhiều dữ liệu, gọi nhiều query để load nhiều thông tin khác nhau để hiển thị trên form. Nếu với cách làm bình thường mỗi lần cần 1 Dao object nào đó select dữ liệu ta lại để NHibernate mở một connection thì performance của chương trình sẽ rất thấp, bởi vậy người ta nghĩ ra ý tưởng chỉ mở 1 connection ứng với mỗi View. Kĩ thuật của tác giả là khi có một request đến một view, một connection hay còn gọi là NHibernate session sẽ được tạo ra nếu chưa có, sau khi được sử dụng session NHibernate này sẽ được lưu trong 1 Hashtable, và nếu trong view đó tiếp tục còn Request khác thì session NHibernate này sẽ được lấy ra để tiếp tục sử dụng. Cuối cùng khi không còn Request nào nữa, ứng với sự kiện EndRequest của 1 view thì NHibernate session này sẽ được lấy ra lần cuối để close đi, sau đó empty Hashtable. Vậy nếu trên 1 trang aspx của ta có 3 usercontrol tương ứng 3 view sẽ có 3 lần mở connection đến database mà thôi thay vì mỗi usercontrol bản thân nó lại mở cả đống connection. Vậy làm sao để NHibernate Session chỉ mở một lần ứng với 1 View? Người ta có thể dùng HttpModule để thực hiện mục đích này.
- Như chúng ta biết HttpModule được khai báo trong file web.config và cho phép chúng ta implement những function bổ xung cho web application. Một HttpModule sẽ được gọi ngay vào đầu và sau khi có request đến 1 View bất kì. Thông thường người ta sử dụng HttpModule để phục vụ việc Logging hay check Security, trong trường hợp này ta đã implement lớp NHibernateSessionModule(Xem bài 3) để quản lý việc đóng và commit transaction. Lớp này được đặt trong project ProjectBase.Data và Pattern này có thể được sử dụng trong nhiều prolect khác nhau của bạn:
/// <summary>
/// Commits and closes the NHibernate session provided by the supplied <see cref="NHibernateSessionManager"/>.
/// Assumes a transaction was begun at the beginning of the request; but a transaction or session does
/// not *have* to be opened for this to operate successfully.
/// </summary>
private void CommitAndCloseSession(object sender, EventArgs e) {
OpenSessionInViewSection openSessionInViewSection = GetOpenSessionInViewSection();
try {
// Commit every session factory that's holding a transactional session
foreach (SessionFactoryElement sessionFactorySettings in openSessionInViewSection.SessionFactories) {
if (sessionFactorySettings.IsTransactional) {
NHibernateSessionManager.Instance.CommitTransactionOn(sessionFactorySettings.FactoryConfigPath);
}
}
}
finally {
// No matter what happens, make sure all the sessions get closed
foreach (SessionFactoryElement sessionFactorySettings in openSessionInViewSection.SessionFactories) {
NHibernateSessionManager.Instance.CloseSessionOn(sessionFactorySettings.FactoryConfigPath);
}
}
}
/// Commits and closes the NHibernate session provided by the supplied <see cref="NHibernateSessionManager"/>.
/// Assumes a transaction was begun at the beginning of the request; but a transaction or session does
/// not *have* to be opened for this to operate successfully.
/// </summary>
private void CommitAndCloseSession(object sender, EventArgs e) {
OpenSessionInViewSection openSessionInViewSection = GetOpenSessionInViewSection();
try {
// Commit every session factory that's holding a transactional session
foreach (SessionFactoryElement sessionFactorySettings in openSessionInViewSection.SessionFactories) {
if (sessionFactorySettings.IsTransactional) {
NHibernateSessionManager.Instance.CommitTransactionOn(sessionFactorySettings.FactoryConfigPath);
}
}
}
finally {
// No matter what happens, make sure all the sessions get closed
foreach (SessionFactoryElement sessionFactorySettings in openSessionInViewSection.SessionFactories) {
NHibernateSessionManager.Instance.CloseSessionOn(sessionFactorySettings.FactoryConfigPath);
}
}
}
Code 5: Implement 1 lớp HttpModule để apply Open In View Principle
- Và đây là một phần của file web.config để khai báo HttpModule này:
<system.web>
</system.web>
<compilation debug="true" />
<httpModules>
</httpModules>
<httpModules>
<add name="NHibernateSessionModule" type="ProjectBase.Data.NHibernateSessionModule, ProjectBase.Data" />
</httpModules>
</system.web>
Code 6: Khai báo HttpModule trong web.config
III. Cải tiến DaoFactory bằng Generic và Castle Windsor
- Hiện tại có một vấn đề với DaoFactory của chúng ta. Khi cần tạo mới một class XXXDao nào đó cho một entity mới, chúng ta phải mở code, sửa lại interface IDaoFactory và thêm vào một dòng IXXXDao GetXXXDao(). Tất nhiên chúng ta cũng phải sửa lại lớp NHibernateDaoFactory, implement thêm method GetXXXDao(). Như vậy khá là bất tiện. Bác huynguyen_fisherman (đồng nghiệp, PM của project NHAU) đã đề nghị tui cách làm khác như sau:
1/ Refactoring lại interface IDao, thay tên của nó thành IGenericDao:
Hình : Refactor lại interface IDao
2/ Thêm vào một interface rỗng IDao và để IGeneric Dao inherit từ interface IDao này
3/ Sửa lại interface IDaoFactory như sau:
Code 7: Sửa lại interface IDaoFactory
4/ Implement lại class NHibernateDaoFactory:
public class NHibernateDaoFactory : IDaoFactory
{
public NHibernateDaoFactory()
{
}
// id is "component id" that we "declare" in the CastleComponents.config
public T GetDao(string id) where T : IDao
{
Check.Require(string.IsNullOrEmpty(id) == false, "component id cannot be null or empty");
return (T)_windsorContainer.Resolve(typeof(T));
}
private IWindsorContainer _windsorContainer = new WindsorContainer(new XmlInterpreter());
}
{
public NHibernateDaoFactory()
{
}
// id is "component id" that we "declare" in the CastleComponents.config
public T GetDao
{
Check.Require(string.IsNullOrEmpty(id) == false, "component id cannot be null or empty");
return (T)_windsorContainer.Resolve(typeof(T));
}
private IWindsorContainer _windsorContainer = new WindsorContainer(new XmlInterpreter());
}
Code 8: Sửa lại class NHibernateDaoFactory
5/ Và đây là cách sử dụng để lấy 1 Dao object như ý muốn
IDaoFactory daoFactory = new NHibernateDaoFactory();
ICustomerDao customerDao = daoFactory.GetDao<ICustomerDao>("CustomerDao");
ICustomerDao customerDao = daoFactory.GetDao<ICustomerDao>("CustomerDao");
Code 9: Cách lấy một Dao bằng NHibernateDaoFactory
IV. Tóm tắt & Kết luận
- Castle Windsor là một IOC Container rất được ưa thích, nếu các bạn xem ở blog này http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspx sẽ thấy rằng Windsor xếp đầu bảng và ngoài nó ra còn rất nhiều library Dependcy Injection nữa. Trong số đó thì có Unity và NInjection là hai IoC Container tui muốn tìm hiểu khi có thời gian :D.
- Trong bài này ta đã tìm hiểu cách áp dụng Generic và IoC Container để làm cho code gọn gàng, dễ sửa đổi. Chúng ta chỉ mới tạo project web và chuẩn bị trước một số thứ như config NHibernate và Windsor cũng như làm quen với Open In View Principle. Trong phần tiếp theo chúng ta sẽ tìm hiểu cách áp dụng pattern MVP để làm các trang aspx. Cách làm mới có gì khác so với cách làm truyền thống? Hãy chờ hồi sau sẽ rõ
Code phần 4: http://nthoaiblog.googlepages.com/EnterpriseSample-part4.zip
Các đoạn code minh họa trong bài viết được tui rút gọn cho dễ hiểu, code được implement cuối cùng trong demo sẽ có nhiều điểm khác biệt...
(Còn tiếp)
Tham khảo:
http://martinfowler.com/articles/injection.html
http://msdn.microsoft.com/en-us/library/aa973811.aspx
http://www.castleproject.org/container/documentation/v1rc3/concepts/ioc.html
http://en.wikipedia.org/wiki/Inversion_of_control
http://sourceforge.net/forum/message.php?msg_id=2847509
http://www.hibernate.org/43.html
http://www.builderau.com.au/program/dotnet/print.htm?TYPE=story&AT=339284537-339028399t-320002019c
http://dotnetslackers.com/articles/designpatterns/InversionOfControlAndDependencyInjectionWithCastleWindsorContainerPart1.aspx
http://dotnetslackers.com/articles/designpatterns/InversionOfControlAndDependencyInjectionWithCastleWindsorContainerPart2.aspx
Hoi do la`m cai IoC dau tien voi tao dung ko :P.
A`, trong cai Castle Windsor co`n nhieu mo'n an choi lam, viet AOP du`ng Castle cu~ng ok luon a'
脚やせエステ茨城
脚やせエステ新宿
脚やせエステ三重
脚やせエステ松山
脱毛 高崎
脱毛 北千住
脱毛 豊田
脱毛 岡山
フェイシャルエステ札幌
フェイシャルエステ埼玉
フェイシャルエステ中野・杉並
フェイシャルエステ愛知
フェイシャルエステ岡山
フェイシャルエステ久留米
エステ体験 所沢
エステ体験 恵比寿
エステ体験 福井
エステ体験 天王寺
エステ体験 福岡
水戸 エステ
千葉市 エステ
上野 エステ
青葉台 エステ
富山 エステ
梅田 エステ
橿原 エステ
熊本 エステ
メンズエステ 千葉
メンズエステ 三重
メンズエステ 大分