Understanding Partial Views in ASP.NET MVC Application


Abstract:
In ASP.NET WebForms, UserControls were used to break the application into smaller pieces. Each piece represented an important part of the application. In ASP.NET MVC application you can achieve the same effect using RenderPartial and RenderAction methods. In this article we are going to demonstrate how to use RenderPartial to construct and use partial views.
Understanding RenderPartial:
RenderPartial serves the purpose of rendering a UserControl in an ASP.NET MVC application. The views rendered are called PartialViews. In order to use a partial view in your application add a MVC UserControl to your application. The screenshot below shows which project template to select when adding a user control to your MVC application.

You can add the MVC View UserControl in your current view folder or in the shared folder. First the view folder will be searched for the specified user control followed by the shared folder.
We have added a simple "Categories.ascx" to our views folder. Now, let’s see how we can load the partial views on the page. Inside the view page you can load the partial view using RenderPartial HTML helper method.

Code Snippet
  1. <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"><h2>List</h2>
  2. <% Html.RenderPartial("Categories"); %> </asp:Content>

 

Now, if you run the page you will see that the partial view is loaded inside the view page as shown in the screenshot below:

Passing Data to Partial View:
In the previous example our partial view was independent which means it was not dependent on any data populated by the view page. In real world applications this might not be the case. We may need to pass additional data to our partial view. The good news is that the partial view has access to the same ViewData dictionary which is used by the view page.
Consider a situation in which we need to pass a list of categories to the partial view. The view page List.aspx has the controller CategoryController. The controller List action is responsible for populating the categories in the ViewData as shown below:

 

Code Snippet
  1. public ActionResult List()
  2. {
  3. var categories = new List<Category>(){new Category() {Name = "Beverages"},new Category() {Name = "Condiments"},new Category() {Name = "Meat"}};ViewData["Categories"] = categories;return View();
  4. }

 

Now, the Categories.ascx partial view can easily access the ViewData["Categories"] as shown below:


 

Code Snippet
  1. <h2>Categories</h2><% foreach (var category in ((IEnumerable<Category>)ViewData["Categories"])){ %><li><%= category.Name %></li><% } %>

This shows that the ViewData dictionary is shared between the view page and the view user control (partial view). We can even make it better by strong typing the partial view and sending the model as a second parameter to the RenderPartial method. The code below shows how to make the ViewUserControl as a strongly typed view which can handle IEnumerable<Category> collection.

Code Snippet
  1. <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Category>>" %><%@ Import Namespace="MyMVCApplication.Models"%><%@ Import Namespace="Microsoft.Web.Mvc"%><h2>Categories</h2><% foreach (var category in Model){ %><li> <%= category.Name %> </li>
  2. <% } %>

 

And here is the RenderPartial method passing the Model as the second parameter.


 

Code Snippet
  1. <% Html.RenderPartial("Categories",ViewData["Categories"]); %>

Or even better you can assign the ViewData.Model and use that to pass the Model to the RenderPartial method.

Code Snippet
  1. <% Html.RenderPartial("Categories",Model); %>

The result is shown below:

Since, the ViewData dictionary is shared between the ViewUserControl and the ViewPage you can easily make changes to the object in the ViewUserControl and it will be reflected in the ViewPage using the controller.
Caching Partial Views:
In WebForms you could easily cache the contents of the UserControl. Let’s check out how we can cache partial views in an ASP.NET MVC application.
Using OutputCache Directive:
The most direct way of caching the partial views is by using the OutputCache directive. Let’s add the directive on the ViewUserControl and see the result.


 

Code– Sokhanh03 Blog
  1. <%@ OutputCache Duration="60" VaryByParam="None" %><h2>Categories</h2>Categories.ascx: <%= DateTime.Now.ToLongTimeString() %>

We are caching the ViewUserControl to "60" seconds which means it is cached to 1 minute. The DateTime is displayed on the view to indicate the request time. The DateTime value will indicate if the partial view is cached or not.  
The view page List.aspx is also adjusted. It now includes an ActionLink which will call the "List" action. It also consists of a DateTime indicator just like ViewUserControl.

Code Snippet
  1. List.aspx: <%= DateTime.Now.ToLongTimeString() %><% Html.RenderPartial("Categories",Model); %><%= Html.ActionLink<CategoryController>( x=> x.List(),"Show List") %>

When you run the page and click the "Show List" button you will find out that the view user control "Categories" is never cached. Both the view page and the view user control will show the same time. There is a problem inHtml.RenderPartial which prevents it from caching the user control. Html.RenderPartial method completely ignores the OutputCache directive in the user control. 
Using OutputCache Action Filters:
Our next technique is to use the OutputCache action filters to cache the contents of the action. This means that we need to return the user control from the controller action. This is performed by using RenderAction HTML helper method. The render action method will invoke the action on the controller and inject the returned HTML to the calling page.

Code Snippet
  1. List.aspx: <%= DateTime.Now.ToLongTimeString() %><% Html.RenderAction("PartialList"); %><%= Html.ActionLink<CategoryController>( x=> x.List(),"Show List") %>

The RenderAction in the above code will invoke the "PartialList" action which is defined below:


 

Code Snippet
  1. [OutputCache(Duration = 60, VaryByParam = "None")]public ActionResult PartialList(){var categories = new List<Category>(){new Category() {Name = "Beverages"},new Category() {Name = "Condiments"},new Category() {Name = "Meat"}};ViewData.Model = categories;return View("Categories");}

The result is shown in the screenshot below:

The screenshot above shows that the complete view page along with the partial view is cached. Once, again we have failed to cached the component. Let’s see our last approach.
WebForms Engine to the Rescue:
Phil Haack blogged about this method a while ago. He leveraged the power of the WebForms engine to cache the partial view. You can read his post here.
The idea is simple! We are going to use our view user control as a ASP.NET WebForms user control using arunat="server" tag. First, we need to register our user control in the directive which is accomplished using the following code:


 

Code Snippet
  1. <%@ Register Src ="~/Views/Category/Categories.ascx" TagName="Partial" TagPrefix="mvc" %>

Now, we can use the UserControl in our page using the code below:

 

Code Snippet
  1. <mvc:Partial runat="server" />

Now, if you run the application and click the "Show List" button you will notice that the user control is cached at a differen t time than the page.

Hopefully, in the future release of MVC the defect in RenderPartial wil be fixed!  
Conclusion:
In this article we learned about Partial Views. We also learned how to perform caching using partial views. In the next article we are going to take a look at the RenderAction HTML helper method and how it can be used in an ASP.NET MVC application.
[Download Sample]

Viết ASP.NET bằng MVP và NHibernate phần cuối – Áp dụng MVP


Bài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết NHibernateBest 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. Thêm project Presenter với các lớp Presenter và các interface View

– Trong phần 4, chúng ta đã chuẩn bị xong mọi thứ để bắt đầu implement các thành phần web cho project. Chúng ta đã có các lớp Entity để sử dụng, các lớp Dao để truy xuất database và các lớp Factory để tạo ra những Dao object cần thiết. Như vậy chúng ta đã có thành phần Model trong Model – View – Presenter. Trong phần này, chúng ta sẽ hoàn thành các phần còn lại là View và Presenter.

– Khi áp dụng MVP, chúng ta hãy nghĩ rằng các trang web cũng như những winform. Chúng có textbox để ta viết vào, có button để click lên. Khi muốn thể hiện một list các dữ liệu gì đó chúng ta có thể sử dụng GridView cho Web hay ListViewControl cho Windows. Hay nói cách khác, giao diện web hay win là các cách để chúng ta thể hiện dữ liệu lưu trữ trong database. Khi tương tác với các control dù trên web hay trên win, sẽ phát sinh các event như Button_ClickedSelectedIndex_Changed và ta sẽ implement những gì mình muốn trong các sự kiện này. Một giá trị string trong cơ sở dữ liệu có thể được thể hiện bằng một label trên web, hay một textbox,… Một danh sách các record trong một table nào đó có thể được hiển thị lên Web dựa vào một Repeater hay một GridView, tùy cách ta muốn thể hiện. Vì vậy bước đầu tiên để làm các trang ASPX hay những UserControl, ta phải khai báo các interface View tương ứng trước.

– Interface View trước hết nó là … những Interface gồm những Public Properties và một số những Method :bbpraroi:. Các Interface này giống như một khuôn mẫu mà khi bạn muốn làm một giao diện web hay win để hiển thị dữ liệu của bạn bạn phải tuân theo. Ví dụ: Chúng ta muốn làm một trang ASPX để hiển thị danh sách tất cả customer ra màn hình, thông tin ta cần ở đây là một List<Customer>. Dù là trên win hay trên web, nếu ta có một List<Customer> có dữ liệu thì ta có thể hiển thị list đó lên màn hình bằng cách sử dụng bất kì control thích hợp nào. Vậy interfaceIListCustomersView ở đây sẽ được khai báo như sau:

public interface IListCustomersView
{

IList<Customer> Customers { set; }

}

Code 1: Nội dung interface IListCustomersView cho 1 view là 1 trang ASPX đơn giản 

– Các bạn có chú ý tại sao property Customers chỉ được khai báo là set mà không có get không? Tại vì mục tiêu của chúng ta ở đây là thể hiện dữ liệu ra màn hình, nên dữ liệu đọc từ database sẽ được set vào view chứ không cần get cái gì từ view để save xuống database, nên khai báo get ở đây là không cần thiết.:bbpraroi:

– Vậy làm ra cái interface này để làm gì?
– Xin thưa rằng trang aspx để hiển thị list các customer sẽ implement interface này.
– Vậy implement interface này để làm gì?
– Để lớp ListCustomersPresenter sẽ tương tác với trang aspx thông qua một interface thông thường thay vì thông qua lớp ListCustomer (một lớp kết thừa từ System.UI.Web.Page).
– Tại sao lớp ListCustomersPresenter lại phải thông qua một interface mà không làm việc trực tiếp với lớp C# CodeBehind của trang aspx?
– Tại vì lớp ListCustomersPresenters và interface View được đặt trong một project dạng class library độc lập với project web, sau này muốn làm một giao diện cho windows thì lấy dll này ra sử dụng, chỉ cần viết một lớp winform implement interface trên là đủ.
– Tại sao phải làm vòng vòng, tạo ra interface rồi lớp presenter làm gì, sao không viết bình thường vô code behind của trang cho rồi?
– Tại vì mình đang sử dụng pattern MVP
:bbpnodo:

————————————————————–

– Vậy bây giờ mình sẽ implement lớp ListCustomersPresenter. Theo như các bằng hữu trên giang hồ, coder ở Trung Nguyên Việt Nam cũng như cao thủ ở phương Tây … độc :bbpcuoi3:đều thích theo kiểu tà đạo như thế này. Các trang aspx, ascx thì gọi là các View và các lớp behind của nó implement interface View tương ứng là điều ai cũng biết là điều gì đấy rồi nên không nói nữa. Trong sự kiện Page_Load của các View này, sẽ khởi tạo một instance của Presenter tương ứng, và một trong những paramenter của các lớp Presenter sẽ là chính trang đang khởi tạo nó. Phức tạp quá :bbptuc:, vậy coi hàm khởi tạo như sau là hiểu liền:

public class ListCustomersPresenter
{

public ListCustomersPresenter(IListCustomersView view, ICustomerDao customerDao)
{

Check.Require(view != null, “view may not be null”);
Check.Require(customerDao != null, “customerDao may not be null”);this.view = view;
this.customerDao = customerDao;

}

public void InitView()
{

view.Customers = customerDao.GetAll();

}

private IListCustomersView view;
private ICustomerDao customerDao;

}

Code 2: Code của 1 presenter đơn giản 

– Lớp ListCustomersPresenter sẽ giữ một field thuộc kiểu của View mà nó sẽ chịu trách nhiệm “present”, ngoài ra nó sẽ có thêm các object khác để phục vụ cho việc present đó, trong trường hợp này là CustomerDao. Một số Presenter khác có thể cần nhiều Dao hơn, cho nên hàm khởi tạo của chúng có thể cũng cần nhiều parameter hơn để init giá trị cho các object Dao này. Thực ra việc init giá trị cho các Dao này trong constructor của lớp Presenter là optional, nếu ta có thể làm khởi tạo chúng trong lúc khởi tạo lớp Presenter thì cũng không cần sử dụng constructor của Presenter để init giá trị làm gì. Nhưng việc truyền instance của View cho lớp Presenterbằng constructor theo tui là một convention, và là một ràng buộc rằng một Presenter được tạo ra vì nó có vai trò “present” cho một View nào đó. Do đó một View tương ứng nên và phải tồn tại; và phải được truyền ngay cho Presenter của nó. Trong những cách hợp lý thì Constructor là cách hợp lý nhất vì lập trình viên sẽ không bao giờ bị quên.:bbpraroi:

– Ta hãy coi nội dung của file Code Behind để xem người ta đã làm điều đó như thế nào (How did they do that?:bbpnghi:

public partial class ListCustomers : BasePage, IListCustomersView
{

protected override void PageLoad()
{

if (!IsPostBack)
{

InitView();

}

}

private void InitView()
{

ListCustomersPresenter presenter = new ListCustomersPresenter(this, DaoFactory.GetDao<ICustomerDao>(“CustomerDao”));
presenter.InitView();

}

public IList<Customer> Customers
{

set
{

grdEmployees.DataSource = value;
grdEmployees.DataBind();

}

}

}

Code 3: Code behind của một view là 1 page ASPX 

– Như chúng ta thấy, khi set giá trị cho list Customer, đồng thời datagrid cũng được bind luôn. Mọi xử lý phức tạp đã được implement trong lớp Presenter tương ứng. Ở trên là sample cho lớp View và Presenter tương ứng đối với trường hợp View là 1 trang aspx. Vậy nếu View là mộtUserControl thì sao? Thông thường, như đã nói ở trên, các presenter thường được người ta khởi tạo trong khi Page_Load. Tuy sự kiện Page_Load cũng có trong UserControl nhưng để cho nó thống nhất, người ta thường khởi tạo tất cả các Presenter trong trang aspx rồi set cho UserControl thay vì làm trực tiếp bên trong UserControl. Do đó những UserControl sẽ có một private field kiểu presenter và sẽ có một method để set giá trị cho private field này. Ví dụ như interface của IEditCustomerView như sau:

public interface IEditCustomerView
{

void AttachPresenter(EditCustomerPresenter presenter);
Customer Customer { set; }
void UpdateValuesOn(Customer customer);

}

Code 4: Nội dung interface IEditCustomerView cho 1 view là UserControl

– Trong một trang Edit Customer, theo cách nghĩ thông thường chúng ta sẽ load thông tin của một User nào đó đưa lên text box, người xem có thể sửa trực tiếp vào text box sau đó click vào 1 button là Save để lưu dữ liệu, hoặc click vào một button là Cancel để quay lại. Trong ASP.NET khi một button được click sẽ phát sinh ra event Button_Clicked, và những xử lý để save dữ liệu sẽ được thực hiện trong code của event này. Vậy làm cách nào chúng ta đưa những xử lý đó vào trong lớp Presenter? Thực sự thì có hai cách để làm điều này, cách thứ nhất cũng là cách đơn giản dễ hiểu nhất, bình dân nhất là ta sẽ implement một public method gọi là Update cho lớp Presenter, và trong sự kiện Button_Clicked, ta sẽ gọi method đó từ instance presenter; cách thứ hai là ta sẽ khai báo một delegate hay mộtEventHander trong interface IEditCustomerView, và trong sự kiệnButton_Clicked, ta sẽ fire event này. Tất nhiên nếu làm theo cách thứ hai thì bên trong code của lớp Presenter phải implement event được khai báo trong View. Để cho đơn giản, dễ hiểu, trong ví dụ này chúng ta sẽ sử dụng cách thứ nhất tuy tui thích cách thứ hai hơn, vì nó có vẽ p…rồ hơn :bbpcuoi3:

public partial class Views_EditCustomerView : BaseUserControl, IEditCustomerView
{

public void AttachPresenter(EditCustomerPresenter presenter)
{

this.presenter = presenter;

}

public Customer Customer
{

set
{

Check.Require(value != null, “Customer may not be null”);
// Must implement this function to show customer info
ShowCustomerDetails(value);

}

}

protected void btnUpdate_OnClick(object sender, EventArgs e)
{

presenter.Update(hidCustomerID.Value);
/*…………………
…………………..
…………………*/

}

private EditCustomerPresenter presenter;

}

Code 5: Code behind của một view là UserControl

II. Test các lớp Presenter

-Như đã biết một trong những lý do người ta áp dụng MVP vì khả năng dễ test của nó. Các lớp Presenter, interface View được đặt trong một project class library nên việc test chúng vô cùng đơn giản. Chúng ta hãy xem một ví dụ test lớp Presenter bằng cách sử dụng Rhino Mock:

[TestFixture]
public class ListCustomersPresenterTests
{

[Test]
public void TestInitView()
{

ListCustomersViewStub view = new ListCustomersViewStub();
ListCustomersPresenter presenter = new ListCustomersPresenter(view,
new MockCustomerDaoFactory().CreateMockCustomerDao());
presenter.InitView();Assert.IsNotNull(view.Customers);
Assert.AreEqual(3, view.Customers.Count);

}

private class ListCustomersViewStub : IListCustomersView
{

public IList<Customer> Customers
{

set { customers = value; }
// Not required by IListCustomersView, but useful for unit test verfication
get { return customers; }

}

private IList<Customer> customers;

}

}


public class MockCustomerDaoFactory
{

public ICustomerDao CreateMockCustomerDao()
{

MockRepository mocks = new MockRepository();ICustomerDao mockedCutomerDao = mocks.CreateMock<ICustomerDao>();
Expect.Call(mockedCutomerDao.GetAll())
.Return(new TestCustomersFactory().CreateCustomers());
Expect.Call(mockedCutomerDao.GetById(null, false)).IgnoreArguments()
.Return(new TestCustomersFactory().CreateCustomer());

mocks.Replay(mockedCutomerDao);

return mockedCutomerDao;

}

}

Code 6: Test một lớp Presenter đơn giản bằng Rhino Mock

– Trong sample này thì những xử lý của lớp Presenter khá đơn giản nên có thể ta chưa thấy rõ được sự cần thiết khi test chúng. Theo tui trên nguyên tắc mọi code trong project dạng class library nên được test, ít nhất các hàm test sẽ cover những dòng code mà ta viết và nâng % coverage của chương trình lên. Nghe đồn có một tay coder bên Mỹ nổi tiếng viết chương trình không bao giờ debug, build 1 lần là chạy, không biết có thiệt không nhưng tui thà chịu khó viết unit test còn hơn ngồi debug khi chương trình đã go live.:bbpbuon:

III. Kết luận

– MVP tưởng khó nhưng không khó, tưởng phức tạp nhưng không đến nỗi vậy, khi xài là ghiền. Điều tui thích nhất ở MVP là khả năng tùy biến, tự mình thiết kế lớp lang chứ không ràng buộc như một số framework chẳng hạn như ASP.NET MVC, tuy nhiên mỗi cái nó có cái hay riêng. Ngoài MVP, còn có khá nhiều Enterprise Architecture khác rất đáng tìm hiểu, trong lúc đọc bài này trên Code Project thì tui cũng biết thêm kiến trúc như S#arp Architecture và Web Client Software Factory (cái này do một đại ca trong team giới thiệu). Chắc chắn tui sẽ tìm hiểu tiếp mấy cái này đặng viết blog chơi :bbpcuoi3:. Loạt bài dịch và chế về NHibernate và MVP sẽ dừng ở phần 5 này. Có một số tuyệt chiêu khác trong bài gốc của tác giả mà tui không nhắc tới như smoke test, cái đấy cũng hay và ai thích có thể tìm hiểu thêm cách dùng. Trong giai đoạn này thì Microsoft đưa ra rất nhiều công nghệ và nhiều khi làm cho lập trình viên chúng ta cảm thấy đuối, .NET framework 3.5 học chưa hết giờ mấy ổng sắp đưa ra .NET framework 4.0, C# 3.x chưa xài nhiều giờ đã có C# 4.0. Dù sao công nghệ vẫn là công nghệ, chúng sẽ thay đổi rất thường xuyên, nhưng mấy cái kiến trúc này thì hầu như không phụ thuộc công nghệ nên tui nghĩ ưu tiên nghiên cứu nó cho lành.:bbpraroi:

– Cuối cùng, mong là loạt bài này có ích cho những ai chưa biết – đang muốn – định tìm hiểu về NHibernate và MVP, xen kẽ trong các phần tui cố ý viết nhiều về các kĩ thuật liên quan như WindsorUnit Test, … nên làm cho bài nào cũng dài ngoằng, nhiều người đọc thấy đuối, nhưng tui sẽ vẫn viết dài như vậy, thậm chí dài hơn, có điều sẽ bớt phần dịch và thêm phần bịa, chẳng hạn bài này toàn tui chế, ko dịch từ nguồn nào cả :bbptthan:.

– Anyway, thanks for reading.:bbpchao:

Code phần 5: http://nthoaiblog.googlepages.com/EnterpriseSample-part5.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ó một số điểm khác biệt…

Phần IPhần II

Phần III

Phần IV

Viết ASP.NET bằng MVP và NHibernate phần 4 – Castle Windsor


Bà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 :bbpcuoi3:. 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::bbpraroi:
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 libraryMicroKernel 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ơnMicroKernel 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 :bbpraroi:

Castle Windsor

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>

<components>

<component id=”orderDao”

type=”MyProjectName.Data.NHibernateOrderDao, MyProjectName.Data”
service=”MyProjectName.Core.DataInterfaces.IOrderDao, MyProjectName.Core”>
<parameters>

<nameOfParameter1>value1</nameOfParameter1>
<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>

<configSections>

<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));

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();

}

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. :bbpraroi:
– 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ộtHttpModule 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ệcLogging hay check Security, trong trường hợp này ta đã implement lớpNHibernateSessionModule(Xem bài 3) để quản lý việc đóng và commit transaction. Lớp này được đặt trong project ProjectBase.Datavà 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);

}

}

}

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>

<compilation debug=”true” />
<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áchuynguyen_fisherman (đồng nghiệp, PM của project NHAU:bbpcheer:) đã đề 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:

Castle Windsor

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:

public interface IDaoFactory
{

T GetDao(string id) where T : IDao;

}

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());

}

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 :bbpxtay:

IDaoFactory daoFactory = new NHibernateDaoFactory();
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àyhttp://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspxsẽ 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õ :bbpraroi:
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/InversionOfControlAndDependencyInjectionWithCastleWindso

Viết ASP.NET bằng MVP và NHibernate phần 3 – Unit Testing


Bà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 Unit Testing

– Xin mở đầu bằng một tình huống thế này: Anh Nguyễn Văn Chuối được assign một task là viết một hàm kiểm tra tính hợp lệ cho dữ liệu nhập vào một text box. Dữ liệu này là một chuỗi các chữ số và dấu chấm của một số kiểu double có giá trị lớn hơn hoặc bằng 0. Giá trị chuỗi chỉ được chứa tối đa 1 chữ số sau dấu chấm. Chuỗi số này có thể là giá trị của số nguyên tức là không có dấu chấm nào cả, và cuối cùng giá trị của số nhập vào phải nằm trong khoảng 0 đến 100. Với nhiều điều kiện ràng buộc như vậy anh Chuối quyết định áp dụng Test Driven Developmentkết hợp với Unit Test để thực hiện và anh Chuối đã viết một hàm test thể hiện mọi yêu cầu như sau:

ASP.NET, NHibernate và MVP

Code 1: Ví dụ về một hàm test
– Tất nhiên sau khi viết hàm Test, anh Chuối sẽ bắt tay vào implement class NumberChecker để cái test này pass. Anh Chuối cho rằng nên kết hợp Regular Expression và các hàm Parse của kiểu double là nhanh nhất :bbpraroi:, do đó anh Chuối đã làm như sau:

ASP.NET, NHibernate và MVP

Code 2: Implement lớp checknumber để Unit Test pass
– Lúc đó, trong team của anh Chuối có chị Bưởi là một QC khét tiếng khó chịu, chị Bưởi này có tật là test và soi mói chương trình rất kĩ và đã phát hiện ra rất nhiều bug hiểm hóc mà một developer chân chính như anh Chuối không ngờ tới. Sau khi test, chị Bưởi phát hiện ra rằng nếu người ta nhập vào 000 thì chương trinh vẫn cho nhập, ngược lại khi nhập +100 thì chương trình báo lỗi không hợp lệ. Phát hiện được bug này chị Bưởi rất vui và assign liền 1 bug cho anh Chuối với status là critical. :bbptuc:
– Rõ ràng là anh Chuối đã tính toán thiếu một số trường hợp. Hàm test sẽ được thêm vào một số dòng code để check các trường hợp chị Bưởi liệt kê, đồng thời regular expression của anh Chuối phải được sửa lại cho đúng.

ASP.NET, NHibernate và MVP, Unit Testing

– Trong ngành phần mềm, thuật ngữ Unit Testing là một phương pháp dùng để kiểm tra tính đúng đắn của một đơn vị source code. Một Unit (đơn vị) source code là phần nhỏ nhất có thể test được của chương trình. Trong lập trình thủ tục, một unit có thể là cả chương trình, một function hay một procedure. Còn trong lập trình hướng đối tượng, đơn vị nhỏ nhất có lẽ là một method của một class nào đó.
– Điều kiện lý tưởng nhất là mỗt test case phải độc lập với những test case khác. Người ta có thể dùng nhiều kĩ thuật như stubs, mock hoặc fake objects, … để phục vụ việc test các module trong chương trình. Viết Unit test là trách nhiệm, nghĩa vụ và quyền lợi của các lập trình viên, lập trình viên chúng ta nên sử dụng Unit Test để bảo đảm những gì mình viết chạy đúng như yêu cầu phần mềm, và nhất là đúng với cách mình hiểu.:bbpcuoi5:

I.1 Lợi ích của Unit Test

– Nhiều :bbpraroi:. Mục đích của Unit Test là cô lập từng phần của chương trình và đảm bảo những phần đó chạy đúng như yêu cầu. Unit test giúp bảo đảm tính chính xác của chương trình, nó giúp thiết lập những ràng buộc và những phần code của chúng ta phải thực hiện chính xác những ràng buộc đó. Kết quả là Unit Test đem lại rất nhiều lợi ích, nhưng rõ ràng nhất là nó giúp phát hiện lỗi và những vấn đề liên quan ngay từ những phase đầu tiên của quá trình phát triển phần mềm.

I.2 Unit Test giúp cho việc sửa đổi dễ dàng hơn

– Trên lý thuyết, Unit Test cho phép lập trình viên refactor code và bảo đảm những gì anh ta viết vẫn chạy đúng sau khi code bị thay đổi. Để làm được điều này, người ta buộc phải viết các test case cho tất cả các function và methods và do đó bất cứ một thay đổi nào làm chương trình chạy sai sẽ bị phát hiện kịp thời và buộc người gây lỗi phải fix ngay. Còn trong thực tế để những test case của bạn cover hết toàn bộ những trường hợp trong chương trình lại là một vấn đề khác.
– Nếu như team của bạn có sử dụng những hệ thống build tự động như CruiseControl.NET có sử dụng Nunit test thì mỗi lần commit code gây lỗi sẽ dễ dàng phát hiện thủ phạm như hình dưới đây:

ASP.NET, NHibernate và MVP

Hình 1: Giao diện Report của CruiseControl.NET

I.3 Unit test giúp tính hợp code dễ hơn

– Bạn làm việc trong một team, mỗi người làm một phần của chương trình và mỗi phần bạn viết đều đã apply unit test kĩ càng. Đến khi kết hợp những thành phần của team với nhau, quá trình đó nói chung sẽ rất xuông sẽ và ít lỗi hơn nhiều so với việc mạnh ai nấy code rồi cuối cùng merge lại với nhau.

I.4 Document và Design

– Mỗi một test case bạn viết có thể được xem như API document cho chính method được test. Một team member vào sau bạn có thể dự vào test case đó để hiểu hàm này công dụng là gì, input thế nào và output ra sao.
– Trong quá trình phát triển phần mềm, document của chương trình bao gồm các design, requirement có thể bị bỏ quên và trở nên “out of date” nhưng những Unit Test Cases sẽ luôn chính xác những gì chương trình thực hiện và vì vậy ở một khía cạnh nào đó, Unit Test có thể được xem như một dạng document của chương trình.:bbpraroi:

I.5 Những hạn chế của Unit Test

– Người ta khó có thể viết Unit Test để bắt tất cả các lỗi của 1 chương trình. Thêm vào đó, những test case ta viết chỉ kiểm lỗi những unit nhỏ nhất của chương trình do đó không thể nào lường trước những vấn đề có thể xảy ra khi kết hợp các module với nhau. Unit testing sẽ thể hiện được hiệu quả rõ nhất khi kết hợp nó với những kĩ thuật test khác và tất nhiên sẽ cần tới sức người. Unit Testing không thể nào thay thế được QC – Tester và cũng như nhiều kiểu test khác, nó chỉ có thể kiểm tra được những lỗi đã biết chứ không thể sử dụng nó để tìm ra các lỗi tiềm ẩn của chương trình.
– Software testing là một tổ hợp của nhiều trường hợp. Ví dụ như để kiểm tra một hàm trả về kiểu boolean, tức là có hai trường hợp trả về chúng ta thường phải viết ít nhất hai dòng code để test lần gọi hàm đó. Anh Nguyễn Văn Chuối rất thường viết những hàm dài cả trăm dòng code với nhiều if / else, làm sao bảo đảm rằng anh Chuối có thể viết một test case có thể cover hết những trường hợp có thể xảy ra. Trong trường hợp đó anh Chuối có thể refactor code để chia nhỏ thân hàm thành nhiều hàm nhỏ hơn rồi từ đó test các hàm nhỏ đó. Nếu team của bạn có sử dụng một Continuous Enviroment với NCoverExplorer thì sẽ dễ dàng phát hiện test case của bạn cover bao nhiêu % chương trình:

ASP.NET, NHibernate và MVP

Hình 2: Giao diện Report của NCover trong dashboard của CruiseControl.NET
– Có nhiều trường hợp khác chúng ta không thể nào sử dụng Unit Test, chẳng hạn như không thế test private class, private method, … nên nói chung Unit Test là một công cụ hỗ trợ chứ không thể thay thế các kĩ thuật test đang được nhiều người sử dụng.:bbpbuon:

II. Một số tool và framework hỗ trợ Unit Testing

II.1 NUnit:

Là một unit-testing framework cho ngôn ngữ lập trình .NET được port từ Junit. NUnit có hai dạng là console và GUI. Thực sự thì NUnit thường được sử dụng kết hợp với CruiseControl.NET và dùng để test tự động trên build server, và lập trình viên bình thường cũng không cần download về máy làm gì.:bbpbuon:

II.2 TestDriven.NET:

Là một trong những tool không thể thiếu đối với dân .NET. Khi install vào máy, nó sẽ tích hợp một menu vào Visual Studio.NET và cho phép chúng test, debug các class/method rất tiện lợi, ngoài ra ta có thể sử dụng assembly nunit.framework trong thư mục cài đặt của TestDriven.NET để sử dụng cho project test.

ASP.NET, NHibernate và MVP

Hình 3: Menu run test khi cài TestDriven.NET

II.3 NCover, NCoverExplorer:

Như giới thiệu ở trên, các tool này giúp chúng ta kiểm soát mức độ cover của các test case đối với source code, và cũng giống như NUnit, chúng thường được kết hợp với CruiseControl.NET để report sau khi source code được build tự động.

II.4 NMock, NMock2, Rhino Mock và TypeMock

– Các tool trên giúp chúng ta giả lập một object để test một component của chương trình khi mà component này có reference đến một component khác. Chúng ta sẽ sử dụng các kĩ thuật mock này để test project EnterpriseSample.Core bằng cách giả lập các object kiểu IxxxDao mà không cần đến EnterpriseSample.Data. Cách sử dụng các tool trên tương đối giống nhau và sẽ được ví dụ bằng Rhino Mock trong phần dưới đây, thông dụng nhất có lẽ là Rhino Mock và Type Mock

III. Tạo project test sử dụng NUnit và Rhino Mock

– Người ta thường tạo một project dạng class library dành cho các test class. Project này theo đúng tên gọi của nó chỉ có ý nghĩa để test và không có vai trò gì trong sản phầm phần mềm cuối cùng. Thực ra NUnit có thể test bất kí test class nào bên trong một assembly bất kì nên project test có thể là Console application, window application, v.v nhưng thông thường người ta sẽ chọn project loại class library. Có một lưu ý là test class của bạn phải được khai báo public, test method cũng thế. Khi sử dụng NUnit.Framework, các bạn sẽ phải làm quen với những Attribute như [TestFixture], [Test], [Setup], [TearDown], … xin được giải thích ngắn gọn những Attribute thường được sử dụng nhất như sau:
[TestFixture]: Dùng để đánh đấu 1 class là test class, những class khác không có Attribute này sẽ mặc định bị ignore khi NUnit test assembly của bạn.
[Test]: Dùng để đánh dấu 1 method là test method, ý nghĩa của nó tương tự như TestFixture nhưng scope ở cấp method.
[Setup]: Dùng để đánh dấu 1 method sẽ được gọi trước khi 1 test case được gọi. Nếu trong 1 test class có 10 method test, thì mỗi lần một method test được chạy thì NUnit sẽ chạy method được đánh dấu với Setup trước tiên.
[TearDown]: Ngược với Setup, chạy sau mỗi test method.
[TestFixtureSetup]: Tương tự như Setup nhưng ở cấp của class, khi 1 test class được test thì method nào được đánh dấu với attribute này sẽ được chạy trước tiên.
[TestFixtureTearDown]: Ngược với TestFixtureSetup.
– Vậy để apply NUnit Test thì công việc vô cùng đơn giản: tạo một project class library, thêm reference đến dll nunit.framework, thêm 1 class mới, khai báo nó thành public, thêm using nunit.framework, thêm attribute[TestFixture] vào đầu của class, viết một method test và khai báo với attribute [Test]. Cơ bản như vậy là đủ để test, bạn có thể kết hợp nhiều attribute khác cũng như nguyên tắc Inheritance của lập trình hướng đối tượng để có một project test uyển chuyển. Người ta thường sử dụng[Setup] để mở một transaction scope, sau đó dùng [TearDown] để roll back transaction khi test các Dao, như vậy sẽ không có dữ liệu bị thêm xóa vào database và bảo đảm dữ liệu test sẽ như nhau trước khi test các method.:bbpskien:

III.1 Tạo dữ liệu test với NUnit

– Trên nguyên tắc, trước khi test bất kì một method test nào thì dữ liệu test phải như nhau. Ví dụ như bạn muốn test xem một Customer có thể thêm và xóa Order hay không thì trước khi test hàm AddOrder và DeleteOrder thông tin về Customer cũng như số lượng Order mà Customer đó đang giữ phải như nhau. Vì vậy người ta thường tạo những lớp Factory chỉ dành riêng để tạo ra dữ liệu Test nhất quán.
– Dữ liệu test của chúng ta trong trường hợp này là các object Customer, Order và HistoricalOrderSummary. Thế nên ta sẽ tạo ra các lớp Factory để tạo các List những object này, các lớp Factory này được đặt trong folder TestFactories bên trong project Test. Ví dụ nội dung lớp TestCustomerFactory như sau:

ASP.NET, NHibernate và MVP

Code 3: Lớp Factory để tạo các object làm dữ liệu test

III.2 Tạo các Mock Factory và Stub objects

– Nếu các bạn còn nhớ thì trong project EnterpriseSample.Core, ta đã khai báo các Interface DAO, các lớp Domain như Customer, Order sẽ reference đến những Interface này. Còn implementation thực sự của các interface Dao để truy xuất database được đặt ở project EnterpriseSample.Data. Như vậy khi test project EnterpriseSample.Core, người ta thường sử dụng các kĩ thuật Mock hoặc tạo một class implement các Interface này để test. Các Mock hay Stub này sẽ là cascadeur cho các lớp Dao khi ta test EnterpriseSample.Core. Đoạn code dưới đây sử dụng RhinoMock để tạo ra một mock object kiểu ICustomerDao, đóng thể cho CustomerDao:

ASP.NET, NHibernate và MVP

Code 4: Sử dụng Rhino Mock để tạo một Mocked Dao object
– Anh Nguyễn Văn Chuối thuyết minh đoạn code trên như thế này: tui dùng MockRepository tạo ra một mock object thuộc kiểu ICustomerDao, đặt tên nó là mockedCustomerDao rồi nói với nó là: “lỡ ai có biểu mày lại hỏi mày có biết GetAll hay không thì mày trả lời là biết và đưa cho người ta danh sách Customer của thằng TestCustomersFactory. Còn ai hỏi mày biết GetById không thì cũng trả lời như vậy nghe chưa!”. Cuối cùng tui dùng MockRepository để ghi nhớ thằng mock Object vừa được dặn dò kĩ lưỡng, bất cứ ai hỏi đển thằng mocked object này tui sẽ biểu nó ra nói chuyện.:bbpnodo:
– Thực ra trong bài viết này tác giả Billy McCafferty có thể sử dụng kĩ thuật Mock là đủ, nhưng theo tui nghĩ bác Billy McCafferty muốn cho chúng ta thấy có những cách khác mà không cần dùng Mock, vì thế nên có sự xuất hiện của lớp OrderDaoStub:

ASP.NET, NHibernate và MVP

Code 5: Ví dụ một lớp Dao Stub dùng để test
– Khi implement 1 interface, buộc lòng chúng ta phải implement tất cả những gì được khai báo trong interface đó nên các bạn thấy rằng lớp Stub này phải khai báo rất nhiều hàm trong khi chúng ta chỉ muốn fake hàm GetByExample. Vì vậy dân đen như tụi mình cư dùng các kĩ thuật Mock cho lành.:bbpcuoi3:

III.3 Test Các Domain classes

– Trên nguyên tắc, tất cả các dòng code của bạn viết phải được test qua có nghĩa là từng constructor, từng putblic setter, getter đều nên được test. Nhưng đối với những người có máu lười như tôi thì có thể bỏ qua một số thứ. Các lớp để test các domain class được đặt trong folder Domain bên trong project Test. Nếu bạn có 10 lớp Domain trong chương trình hãy viết 10 lớp test tương ứng ví dụ như sau:

ASP.NET, NHibernate và MVP

Code 6: Viết Unit Test cho các Domain Classes

III. 4 Test Nhibernate Dao

– Trong phần 3 này chúng ta hãy cứ tiếp tục chấp nhận điều sau: Khi một Dao cần truy xuất database, nó sẽ cần một Nhibernate Session để làm chuyện đó. Nó sẽ lấy Session này ở đâu? Nó sẽ lấy Session nhờ vào lớp NhibernateSessionManager và kết hợp với một giá trị string chứa đường dẫn của một file config chứa các setting cần thiết như Connection String đến database thực. Và đường dẫn này được hard code như là một static property của lớp TestGlobals.cs. Để tiếp tục, yêu cầu các bạn đang sử dụng db server SQL Express 2005 và đã có database Northwind. Nếu chưa có các bạn có thể download ở đây rồi attach Northwnd.MDF vào db server.
– Các lớp Nhibernate Dao là những lớp trực tiếp truy xuất database và chúng ta chuẩn bị test nó. Để test các lớp Dao này chúng ta cần một database thực sư và chúng ta đã chuẩn bị như đã nói ở trên. Xin nhắc lại một lần nữa là trên nguyên tắc, các hàm test nên không ảnh hưởng đến kết quả test của những hàm test khác, điều này có nghĩa là dữ liệu trước và sau khi thực hiện một hàm test là nhất quán. Để đạt được mục đích này, chúng ta tạo một lớp NhibernateTestCase, các lớp test case khác sẽ inherit từ lớp này. Trước khi tìm hiểu tại sao làm vậy, hãy xem implementation của nó:

ASP.NET, NHibernate và MVP

Code 7: Lớp Test base
– Vậy bất kì lớp test nào inherit từ lớp này sẽ kế thừa được TestFixtureSetup và TestFixtureTearDown của nó. Có nghĩa là trước khi một lớp test được thực thi, NHIbernate Session Manager sẽ mở một transaction và rollback ngay sau khi test xong, nhờ thế dữ liệu test sẽ không bao giờ bị thay đổi. Còn bây giờ là nội dung một lớp Dao Test:

ASP.NET, NHibernate và MVP

Code 8: Lớp Test NHibernate Dao
Trong phần 2, chúng ta đã có một lớp Generic Dao giúp tiết kiệm code cho rất nhiều Dao Object khác nhau. Điều này dẫn đến việc là lớp Dao nào nên được test và lớp nào không? Để trả lời câu hỏi này, tác giả đã đưa ra các kinh nghiệm của mình khi viết Test Class::bbpxtay:
+ Phải thực hiện test mọi method của Generic Dao. Nếu như bạn có 10 lớp Daos inherit generic Dao này thì chỉ một lớp bất kì trong số các lớp Daos này được test là đủ.
+ Phải test tất cả các method phụ của mỗi Dao nếu bạn có implement thêm.
+ Nếu có một lớp Dao nào không inherit từ Generic Dao như lớp HistoricalOrderSummaryDao thì lớp đó phải được test.
+ Phải chắc chắn dữ liệu test nhất quán trước và sau khi một Dao unit test được gọi và các unit test phải độc lập với nhau.

IV. Tóm tắt & Kết luận

– Trong phần 3 này ta đã làm quen với Unit Testing, các tool và framework phụ trợ, ta cũng đã tìm hiểu qua công dụng và ý nghĩa của từng lớp, từng folder bên trong một project Test. Cách tổ chức lớp cũng như cách tác giả viết Unit Test rất tốt để tham khảo. Bản thân tôi cũng có viết Unit Test nhưng sau khi xem bài viết của Billy McCafferty thì đã quyết định từ nay về sau nếu có viết test sẽ theo cách làm của bác Billy.:bbpcuoi3:
– Viết Unit Test tuy không bắt buộc nhưng nó đóng vai trò quan trọng trong qúa trình làm phần mềm. Đối với một số khách hàng lớn họ có thể yêu cầu chúng ta viết Unit Test và phải thoả mãn cover 80% code chẳng hạn. Unit Test không hẳn chỉ để test chương trình, ta có thể sử dụng nó như là một công cụ hỗ trợ debug nhanh khi implement một chức năng nào đó khá phức tạp. Kết hợp với một số kĩ thuật Mock, ta có thể test ngay một module của chương trình khi chưa có hoặc chưa hoàn thành xong các module khác…
– Chắc hẳn chúng ta vẫn còn nhiều thắc mắc đối với cách hoạt động của lớp NhibernateSessionManager. Lớp này thực sự có công dụng gì và được tổ chức thế nào? Hãy chờ hồi sau sẽ rõ :bbpcuoi3:
Code phần 3: http://nthoaiblog.googlepages.com/EnterpriseSample-part3.zip
Các đoạn code minh họa trong bài viết được rút gọn cho dễ hiểu, code được implement cuối cùng trong project sẽ có nhiều điểm khác biệt…
(Còn tiếp)

Viết ASP.NET bằng MVP và NHibernate phần 2 – Implement Data Layer


Bà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. Nhắc lại kĩ thuật Separated Interface

Separated Interface là một kĩ thuật trong lập trình nhằm đạt được mục tiêu phân chia rạch ròi sự phụ thuộc giữa các tầng (tiers) trong chương trình. Kĩ thuật này được giới thiệu lần đầu bởi sư phụ Martin Fowler, nó cũng là một trong những nguyên tắc lập trình nổi tiếng được nhắc đến trong cuốn sách Agile Software Development của Robert Martin. Trong thực tế, kĩ thuật này thường được áp dụng khi người ta implement Domain Layer và Data Access Layer. Có một ví dụ thế này, trong domain layer ta tạo một lớpCustomer. Lớp Customer này sẽ có nhiều method, một trong số đó là method GetAllCustomer(). Vậy lớp Customer cần phải sử dụng một data access object để xử lý yêu cầu đó. Kết quả là đối tượng Customer ít nhất đã có một sự phụ thuộc nhất định đến CustomerDao (nằm trong Data Access Layer). Mặt khác đối với lớp CustomerDao, nó sẽ trực tiếp truy xuất vào database và trả về kết quả là một mảng/list các đối tượng kiểu Customer, vì thế nó phải phụ thuộc ngược lại Domain Layer vì như chúng ta đã thống nhất với nhau mình sẽ khai báo các class Entity trong Domain Layer (Project EnterpriseSample.Core). Nếu chưa/không biết kĩ thuật này, có thể chúng ta có thể dùng cheat code bằng cách tách tất cả những lớp định nghĩa Entity ra một project khác ví dụ như EnterpriseSampe.Entities. Còn nếu đã biết Dependency Inversion Priciple rồi thì ta sẽ làm một cách sạch sẽ và trong sáng hơn nhiều :bbpraroi:.
– Có một giải pháp còn cheat ghê hơn nữa:bbpcuoi3: là đưa tất cả các lớp DAO, nói chung là mọi thứ trong Data Access Layer vào chung một assembly với Domain Layer, tức là vào chung project EnterpriseSample.Core. Và để các lớp này khỏi chung chạ với nhau, ta phải tạo ra một sự độc lập ảo bằng cách cho chúng vào những folder khác nhau trong project chẳng hạn như folder Domain và folder Data. Cách làm này sẽ gây ra những vấn đề sau đây:
+ Domain layer và Data layer sẽ phụ thuộc hai chiều vào nhau.
+ Ở chung một nhà lại phụ thuộc nhiều thế này thì lâu ngày … sẽ có con chung :bbpxtay:. Code của 2 layer này sẽ trùng lặp với nhau không chừng.
+ Nếu trong code của Entity object sử dụng trực tiếp một instance của class DAO thì sẽ rất khó Unit Test nó mà không cần database thực. Vì khó test nên có thể người ta sẽ lười test, mà lười rồi thì khỏi viết Unit Test :bbpraroi:.
– Thực ra cách mà tác giả đề nghị là domain và data layers nên được đặt trong các assembly độc lập. Chẳng hạn như XXXX.Core và XXXX.Data. Domain layer (assembly XXXX.Core) sẽ chứa những lớp domain và DAO Interface. Còn Data Layer (assembly XXXX.Data) chỉ chứa những lớp DAO, những lớp DAO này là những lớp sẽ implement các Interface nói trên trong domain layer. Các bạn hãy xem lại hình vẽ cấu trúc này lần nữa:

ASP.NET, NHibernate và MVP

Hình 1: Quan hệ giữa project Core và project Data
– Cách làm trong sáng này có một số lợi ích sau:
+Domain Layer sẽ hoàn toàn độc lập với các assembly liên quan đến database như NHibernate hay System.Data.SqlClient
+Domain Layer không cần quan tâm xem data layer hoạt động thế nào, truy xuất loại database gì… Vì thế việc chuyển đổi các loại database khác nhau cũng như thay đổi chi tiết implementation của tầng data layer rất dễ dàng và không hề ảnh hưởng đến code của domain layer.
+ Vì hai layer này quan hệ một chiều với nhau như vậy nên những fan của Unit testing có thể sử dụng các kĩ thuật “Mocked” data-access layer trong domain layer để test các lớp Entity mà không cần có database thực.

II. Khai báo các Interface cần thiết

– Bây giờ chúng ta sẽ từng bước khai báo các interface này. Trước hết là interface IDao, đây là một interface chung nhất khai báo các function cơ bản nhất mà bất cứ một data access object nào cũng có, nội dung interface này như sau::bbpraroi:

ASP.NET, NHibernate và MVP

Code 1: Khai báo các function chung trong interface IDao
– Đây là 1 generic interface với hai kiểu generic là T và IdT. Nếu ta nhìn phần khai báo các function thì cũng đoán ra nếu một lớp DAO nào implement interface này hoặc một interface nào inherit từ interface này sẽ phải khai báo kiểu của Entity và kiểu Id của Entity đó. Ví dụ như hai interface ICustomerDao và ISupplierDao như sau:

ASP.NET, NHibernate và MVP

Code 2: Các interface kế thừa từ Generic interface IDao
– Vậy đến khi implement thực sự các Data Access Object, ta sẽ implement các interface như ICustomerDao, ISupplierDao. Ở phần 1, chúng ta đã tạo 1 lớp là HistoricalOrderSummaryDao để giữ giá trị khi gọi Stored Procedure. Vì đây là một “value class“ đặc biệt, không phải là một Entity chính thức nên nó không cần Id; do đó interface DAO tương ứng cũng không cần phải kế thừa từ interface IDao:

ASP.NET, NHibernate và MVP

Code 3: Interface IHistoricalOrderSummaryDao không cần kế thừa từ IDao
– Cuối cùng chúng ta sẽ tạo một interface IDaoFactory, nếu các bạn đã đọc về Factory Pattern thì trong trường hợp này chúng ta đang sử dụngAbstract Factory. Sẽ có 1 lớp Factory implement interface này và tùy chúng ta sử dụng ORM nào thì các lớp DAO tương ứng sẽ được tạo ra. Cho nên trong phần sau chúng ta sẽ viết 1 lớp NHibernateDaoFactory với mục đích tạo ra các NHibernate Dao Object::bbpcuoi5:

Code 4: Nội dung của interface IDaoFactory

III. Làm việc với NHibernate Session

– Khi viết chương trình có sử dụng database, người ta thường tìm cách giới hạn số lần mở connection đến database server sao cho càng ít kết nối càng tốt. Nếu các bạn quan tâm đến vẫn đề này hẵn các bạn sẽ biết đến khái niệm Transaction. Trong NHibernate cũng có một khái niệm như vậy và chúng ta hãy tạm chấp nhận một NHibernate Session là một connection đến database server bằng NHibernate. Khi viết một ứng dụng web, việc giảm thiểu các connection đến database là điều rất đáng để quan tâm. Giả sử chúng ta viết 1 trang aspx trên trang đó có sử dụng 10 usercontrol, mỗi usercontrol lại cần kết nối với database để load dữ liệu riêng của nó. Như vậy một lần mở trang web ta đã mở 10+ connection đến database server. Chúng ta có thể giải quyết vấn đề nhức đầu này bằng cách đưa tất cả các request đó vào cùng một transtaction. Cụ thể thế nào thì chúng ta sẽ bàn vào bài sau, còn trong phạm vi bài này chúng ta hãy chấp nhận rằng mỗi một Dao object khi cần kết nối đến database để đọc/ghi dữ liệu nó sẽ cần đến một NHibernate.ISession. Và tác giả đã implement một lớp NHibernateSessionManager cho chúng ta sử dụng, đối số cần truyền cho method GetSesssionFrom() là một giá trị string chứa đường dẫn đến file config của NHibernate. Cách làm này của tác giả có rất nhiều mục đích như: hỗ trợ truy xuất nhiều database một lúc và sử dụng một kĩ thuật gọi là OpenSessionInView để quản lý connection đến database server,.. Tác giả đã implement các lớp Configuration để hỗ trợ đọc các file config NHibernate vì các config này được đặt vào một file xml riêng và không còn nằm chung trong file web/app.config. Các lớp này được viết một lần nhưng ta có thể sử dụng chúng cho nhiều project khác nên chúng được đặt trong một assembly riêng tên là ProjectBase.Data. Chúng được đặt trong thư mục NhibernateSessionMgmt; nếu các bạn quan tâm có thể tìm hiểu khái niệm Configuration của .NET Framework 2.0
Sau đây là một ví dụ có sử dụng NHibernate session:

Code 9: Sử dụng NHibernate Session

IV. Implement DaoFactory và các lớp Dao tương ứng

– Vậy là cơ bản chúng ta đã hoàn thành xong Domain Layer. Xin nhắc lại project EnterpriseSample.Core chỉ chứa các lớp Entity và các interface Dao, không chứa bất kì một lớp Dao nào. Ngay sau đây chúng ta sẽ tạo các lớp Dao này trong project EnterpriseSample.Data:

IV.1 Generic DAO

– Các lớp Dao là những lớp trực tiếp sử dụng NHibernate để thực hiện các lệnh thêm xóa, sửa, update vào database. Ví dụ ta có CustomerDao sẽ implement các function như UpdateCustomer, GetAllCustomers. Nhìn chung thì mỗi một Entity trong chương trình đều có các method tương tự nhau như: GetById, Save, Delete, Update. Thử tưởng tượng bạn có 20 tables, ứng với 20 class Entity trong chương trình và mỗi một Entity như vậy phải implement ít nhất 4 function GetById, Save, Delete, Update thì đúng là một cực hình. Người ta sẽ giải quyết vấn đề trùng lặp code này bằng cách sử dụng Generic của .NET framework 2.0, tất cả những hàm tương tự nhau trong các lớp Entity sẽ được implement trong một class Generic:

Code 5: Lớp Generic Dao AbstractNHibernateDao
– Các lớp Dao trong chương trình ngoài implement interface IxxxDao sẽ inherit từ lớp abstract này:

Code 6: Cách sử dụng lớp Generic AbstractNHibernateDao

IV.2 DAO Factory

– Dao Factory là gì và tại sao ta lại cần một lớp Factory? Nếu bạn có quan tâm đến Design Pattern chắc là bạn đã nghe đến các pattern như Abstract Factory, Factory Method. Một Dao Factory có khả năng tạo ra các Dao Object theo một tiêu chuẩn nào đó. Trong project này ta sử dụng NHibernate để thao tác với cơ sở dữ liệu, vậy thì Dao Factory của ta sẽ tạo ra các NHibernate Dao Object để tương tác với NHibernate. Ta sẽ tạo ra một lớp là NHibernateDaoFactory với tiêu chí đó, code như sau:

Code 7: Lớp NHibernateDaoFactory

IV.3 Nhận kết quả từ Stored Procedured như thế nào?

– Trong bài trước chúng ta có implement lớp HistoricalOrderSumary để giữ giá trị trả về khi gọi một stored procedure. Ta cũng đã làm một file mapping xml tương ứng cho nó. Nhưng để thực hiện các bước cụ thể nhằm map các kết quả từ database, ta phải làm quen một khái niệm trong NHibernate là IQuery và Transform. Chúng ta sẽ tạo một instance kiểuIQuery bằng static method GetNamedQuery với parameter là tên query được khai báo trong file mapping XML, từ đó hướng dẫn NHibernate map kết quả trả về bằng constructor của lớp HistoricalOrderSumary:

Code 8: Cách map kết quả từ stored procedure

V. Kết luận

– Qua phần hai này, chúng ta đã làm quen với khái niệm Session củaNHibernate một cách rất khái quát :bbpcuoi5:, trong bài kế tiếp chúng ta sẽ tìm hiều kĩ hơn tại sao tác giả lại tổ chức lớp lang như vậy. Các lớp phục vụ Session management này được viết một lần và sử dụng lại ở nhiều project khác nhau nên tác giả đề nghị ta đưa các lớp này qua một assembly khác để tái sử dụng. Interface IDao và lớp generic DaoAbstractNHibernateDao cũng được đặt trong assembly này vì chúng rấtgeneric và có thể tái sử dụng ở những project khác mà không cần sửa code. Trong assembly EnterpriseSample.Data, chúng ta implement các lớp Dao cũng như NHibernateDaoFactory để tạo ra các NHibernate Dao. Nhờ sử dụng Generic nên rất nhiều code được sử dụng lại nên các bạn thấy rằng rất ít lớp trong EnterpriseSamle.Data và code của những lớp này cũng khá là gọn. Các lớp Dao cũng như lớp Generic Dao sử dụng một instanceNHibernate.ISession để thao tác với database, ta chỉ cần truyền đường dẫn file config và Session Manager sẽ trả về một NHibernate session. Mục đích của cách làm này là giúp giảm thiểu số lần mở connection đến database server để tăng performance. Chúng ta sẽ còn bàn nhiều về các kĩ thuật liên quan trong bài thứ 4.:bbpraroi:
Code phần 2: http://nthoaiblog.googlepages.com/EnterpriseSample-part2.zip
Các đoạn code minh họa trong bài viết được rút gọn cho dễ hiểu, code được implement cuối cùng trong project sẽ có nhiều điểm khác biệt với hình…
(Còn tiếp)

Phần I

Viết ASP.NET bằng MVP và NHibernate phần 1 – Domain Classes


Bài viết này được dịch, tóm tắt và bổ sung dựa theo 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 100% từ code mẫu của tác giả Billy McCafferty.

I/ Introduction – Why use an ORM?

Hiện nay vẫn có nhiều người không chấp nhận các công nghệ ORM, nói chung đó thường là những người thích dùng đồ chơi của Microsoft. Giang hồ thường có một luật bất thành văn rằng “nếu nó chưa được Microsoft nghĩ ra thì nên đợi xem Microsost đưa ra cách làm trước”. Cho nên sau một thời gian được chờ đợi hơi lâu, Microsoft cũng nghĩ ra LinQ to Entities trong C# 3.0. Vậy là thời điểm hiện nay, lập trình viên được lựa chọn nhiều công nghệ ORM để sử dụng. Trong số những người không thích ORM, có những người cho rằng ORMs sẽ làm giảm performance của chương trình và chúng chỉ giúp rút ngắn các phase đầu trong quá trình phát triển chương trình (làm Data Acess Layer). Một số ý kiến còn cho rằng sử dụng ORM còn có thể gây khó khăn trong việc maintain project. Và chỉ đến khi các vấn đề trên được chú ý thì người ta mới nhìn nhận những sự thật sau:
+ Dưới sự hỗ trợ của ORMs điển hình như NHibernate sẽ làm tăng performance của bạn với cương vị là một lập trình viên. Càng tốn nhiều thời gian để làm data access layer thì thời gian còn lại của bạn để làm những phần khác cũng như tối ưu hóa chương trình (nếu cần thiết) sẽ giảm đi. Khi ta dùng một số profiling tool để phát hiện nguyên nhân chạy chậm thì sẽ phát hiện một số ít nơi trong code là thủ phạm, khi đó chúng ta buộc lòng phải thực hiện các sửa đổi cần thiết để giải quyết. Trong những trường hợp như vậy thì có hay không có sử dụng ORM đều như nhau. Mặc dù rất hiếm gặp nhưng nếu thủ phạm làm cho chương trình chạy chậm là ORM framework và không có cách nào để sửa code thì ta vẫn còn một chiêu cuối là thay luôn một ORM framework khác, thậm chí implement lại data access layer theo cách của mình nếu như bạn muốn, tất nhiên chỉ làm được như vậy khi data access layer của chúng ta được tổ chức khéo léo bằng cách sử dụng nhiều abstraction.
+ Điểm thứ hai đối với các ORM, đặc biệt là NHibernate framework, đã hỗ trợ tất cả các yêu cầu đối với một framework truy xuất cơ sở dữ liệu. Nó giúp tiết kiệm công sức của lập trình viên, giúp cho chương trình chạy ổn định và dễ maintain. Tất nhiên caching cũng được hỗ trợ trong NHibernate. Ngoài ra, các feature khác như lazy-loading, inheritance, generics, hỗ trợ stored procedure cũng có trong NHibernate.
+ Điểm cuối cùng, những người luôn cho rằng ORMs như NHibernate sẽ khó maintain về sau chính là những người đang làm việc với những hệ thống phần mềm đã không được thiết kế tốt để có khả năng maintain bất cứ một data access layer nào. Vì vậy, dù Nhibertate là một lựa chọn tốt cho những chương trình sử dụng database thì không có nghĩa là bạn không cần thiết kế chương trình thật tốt.
– Không cần phải nói nhiều nữa, cũng như các ORM tools khác, NHibernate sẽ giảm bớt hàng ngàn dòng code cũng như các stored procedures cho chúng ta, vì thế nó cho phép chúng ta dành thời gian và công sức cho phần chính của chương trình: domain model và bussiness logic.

II/ Defining the Domain Layer

– Khi làm việc với project .NET có sử dụng database, có lẽ việc đầu tiên mà nhiều người sẽ làm là viết các lớp Domain. Lớp Domain hay còn gọi là Entity là lớp C# tương ứng với một table trong database. Các property trong những lớp Entities cũng tương ứng với các column trong table đó, và tất nhiên kiểu dữ liệu của chúng cũng tương ứng 1:1 với nhau. NHibernate sẽ sử dụng những lớp Entities được định nghĩa trong project kết hợp với các file XML mà chúng ta sẽ tạo ra và dựa vào đó để truy xuất database. Các file XML này thường được đặt tên ứng với tên lớp của chúng ta, ví dụ ta có lớp Customer.cs thì người ta thường tạo ra một file tương ứng là Customer.hbm.xml. HBM là chữ viết tắt của Hibernate mapping. Nội dung xml của nó sẽ dùng để mapping các field của lớp với các column trong database và tất nhiên chúng ta phải viết theo đúng quy cách của NHibernate. Quy tắc đặt tên file như tôi nói ở đây chỉ nhằm mục đích dễ quản lý, thực ra chúng ta vẫn có thể viết tất cả các đoạn XML mapping trong cùng 1 file và đặt tên file xml này tuỳ ý vì NHibernate sẽ được chỉ định nơi để tìm các nội dung XML này trong file app/web config. Các file XML này sẽ được build cùng với projects như là các embeded resources nên chúng ta phải chú ý đến chi tiết này khi thêm các file HBM vào project.
– Như đã nói thì các lớp Entity sẽ tương ứng với các table trong database. Về lý thuyết chúng ta có thể đặt tên các property tuỳ ý không cần phải theo tên của column trong database, NHibernate sẽ phân biệt được và biết cách map giữa property nào với column nào trong database. Nhưng để dễ hiểu và dễ bảo trì, người ta thường tự giác đặt tên chúng như nhau. Sau đây là ví dụ về một lớp Entity và nội dung XML mapping tương ứng với nó:

ASP.NET, NHibernate và MVP

ASP.NET, NHibernate và MVP

Code 1: 1 Domain class và xml file mapping tương ứng
– Như vậy Domain layer của chúng ta sẽ có các lớp Entity, các file HBM tương ứng. Chúng ta sẽ đặt tên cho project chứa các file này là XXXX.Core. Ngoài ra, project này còn chứa các interface của các data access object. Và vì chúng ta là những người có tổ chức nên các interface này sẽ được đặt trong một thư mục riêng: DataInterrfaces và vì vậy namespace của các Interface trong thư mục này sẽ tương ứng với tên thư mục của nó: EnterpriceSample.Core.DataInterfaces. Chúng ta sẽ bàn chi tiết các bước tạo những file interface này ở phần hai; còn tại sao phải đặt chúng ở Domain Layer sẽ được giải thích ngay sau đây.

II.1 Separated Interface, Implemented

– Trong project EnterpriceSample.Core không chứa bất cứ phần code nào implement các data access object, chúng ta sẽ chỉ định nghĩa các interface của chúng. Các lớp DAO implement các interface này sẽ được đặt ở một layer khác, đó sẽ là project EnterpriceSample.Data. Cách làm này có vẽ khá lạ lùng, vì chúng ta vẫn quen với suy nghĩ layer bên trên sẽ phụ thuộc vào layer bên dưới, tầng web sẽ reference đến tầng Service, Service sẽ phụ thuộc vào Data Access Layer. Thế nhưng ở đây ta sẽ làm khác đi một chút, tầng Service hay ở đây gọi là Core sẽ không phụ thuộc vào Data, ngược lại nó sẽ khai báo một số Interface nó cần sử dụng và các lớp DAO trong tầng Data sẽ phải phụ thuộc vào layer Core này và implement các Interfaces được khai báo trong đó. Kĩ thuật này được gọi là “Separated Interface”. Nếu như chúng ta gọi EnterpriceSample.Core là một “uppler-level layer” và EnterpriceSample.Data là “lower-level layer” thì: “each of the upper-level layers declares an abstract interface for the services that it needs. The lower-level layers are then realized from these abstract interfaces. … Thus, the upper layers do not depend on the lower layers. Instead, the lower layers depend on abstract service interfaces declared in the upper layers” (Robert Martin).

ASP.NET, NHibernate và MVP

Hình 1: Quan hệ giữa project Core và project Data
– Chúng ta sẽ bàn về các bước tạo các Interface này ở bài sau.

II.2 Generic IDs and Object Comparisons

– Trong project EnterpriceSample.Core, phần lớn các lớp Domain/Entity inherits từ lớp DomainObject. Lớp này khai báo một số function để phục vụ trong việc so sánh hai đối tượng cùng kiểu. Domain Object là một lớp Generic, nó nhận vào một kiểu dữ liệu dùng để khai báo ID của một domain object. Generic property kiểu này cho phép ta định nghĩa chung các lớp có property ID kiểu string như lớp Customer và cả các lớp có property ID kiểu long; ví dụ như Order. Các bạn cũng chú ý rằng ID này chỉ khai báo public getter nhưng không cho public setter. Giả sử ta lấy một Customer từ database và vô tình thay đổi giá trị ID của nó rồi save lại, dữ liệu này sẽ ghi đè vào record của Customer khác trong database. Vì vậy các lớp Entity chỉ nên được khai báo ID với setter là protected, có nghĩa là chỉ NHibernate sẽ đọc database vả chỉ nó được set các thông tin đó vào Domain object của chúng ta. Nhưng sẽ có một số trường hợp các domain object cần được set giá trị cho ID của chúng. Với những trường hợp như vậy, ta có thể sử dụng một cách work-around là viết một function để Set ID như ý muốn. Mỗi lần sử dụng hàm này buộc chúng ta phải nghĩ đến những trường hợp save đè dữ liệu không lường trước. Trong bài viết, tác giả có giới thiệu thêm một interface là IHasAssignedId để giúp chúng ta làm việc này. Đoạn code sau đây sẽ mô tả cách sử dụng interface này:

ASP.NET, NHibernate và MVP

Code 2: Cách sử dụng interface IHasAssignedId
– Trong code các bạn thấy lớp Customer có khai báo implement interface IHasAsssigneed và bên trong thân code của nó sẽ phải implement method SetAssignedIDTo. Ngoài ra thân hàm có sử dụng các public static method của lớp Check. Đây là một lớp Util chúng ta sử dụng để kiểm tra các ràng buộc và sẽ được đề cập trong bài viết khác.

II.3 Mapping the Domain to the Database

– Trong NHibernate có hai cách để map các domain objects vào database.: HBMs sử dụng các file XML và sử dụng các Attribute. Thuận lợi chính khi sử dụng XML mapping là chúng hoàn toàn độc lập với những lớp C# mà chúng mô tả. Điều này giúp cho lớp Domain của chúng ta giữ được tính chất như một POCOs (plain old C# objects). Nhưng bù lại các file mapping xml lại sinh ra một bất lợi đó là chúng ta phải tốn khá nhiều công sức để giữ cho những nội dung mapping phải luôn chính xác giữa định nghĩa lớp và cấu trúc database:

ASP.NET, NHibernate và MVP

Code 3: Quan hệ giữa các Entity trong code C#

ASP.NET, NHibernate và MVP

Code 4: Quan hệ giữa các Entity được khai báo đúng quy cách trong XML mapping file
– Các bạn thấy rằng hai lớp Product và Suplier có quan hệ với nhau, và trong xml mapping data tương ứng cũng có quan hệ one-to-many để thể hiện ràng buộc này. Nếu như chúng ta có thay đổi database, thì việc tiếp theo cần làm ngay là sửa file mapping này theo thay đổi đó.
– Ngược lại sử dụng Attribute để map lại buộc chúng ta can thiệp trực tiếp vô code, nhưng được cái cách đó giúp cho việc mapping đơn giản hơn. Sử dụng Attribute để map làm cho lớp Domains của chúng ta trở nên giống như khi sử dụng Active Record. Ngoài việc can thiệp trực tiếp vào code của các lớp Domain, cách làm này còn buộc chúng ta phải reference tớiNHibernate.Mapping.Attributes và khiến cho Domain Layer của chúng ta mất đi tính độc lập với các assembly đáng lẽ chỉ xuất hiện trong tầng Data. Dường như có một convention chung là domain layer nên độc lập với những gì của data layer. Khi làm đúng như vậy, bạn sẽ không phải bận tâm khi cần có sự thay đổi ở data layer. Do đó, bất cứ khi nào tạo một project mới, ta nên quyết định xem nên sử dụng ORM nào; sử dụng kết hợp nhiều thứ một lúc có thể gây ra confusion khi ta không biết object nào cần nên map và cái nào không cần thiết để map. Dù sao đi nữa thì đó là kinh nghiệm của tác giả rất đáng để tham khảo; còn đoạn code dưới đây sẽ cho thấy cách sử dụng NHibernate mapping bằng Attributes:

ASP.NET, NHibernate và MVP

Code 5: Sử dụng Attribute để map
(Nếu muốn sử dụng Attribute để mapping, các bạn phải download thêm 1 assembly là NHibernate.Mapping.Attributes trên sourceforce mới sử dụng được.)

II.4 NHibernate Support for Stored Procedures

– Nếu bạn muốn map kết quả trả về của một Stored Procedure về thì làm thế nào? Cũng như các ORM tool khác, NHibernate cũng hỗ trợ chuyện đó dễ dàng. Giả sử như bạn cần trả về số lượng các product được order bởi một customer biết trước. Chúng ta sẽ viết một stored procedure để thực hiện chuyện đó. Ngoài ra chúng ta sẽ viết một value object HistoricalOrderSummary để lưu trữ kết quả trả về của Stored Procedure. Chúng ta lưu ý là lớp này không inherite từ DomainObject và vì vậy không cần một HBM đúng nghĩa tương ứng với nó. Tuy nhiên chúng ta sẽ tạo ra một file HistoricalOrderSummary.hbm.xml và khai báo trong đó tên của Stored Procedure sẽ sử dụng và kết quả đó map như thế nào. (Chi tiết làm sao các dữ liệu được map với domain objects sẽ được đề cập trong bài viết sau khi chúng ta bàn về các bước implement project EnterpriceSample.Data)

ASP.NET, NHibernate và MVP

Code 6: Một value class để giữ kết quả trả về từ stored procedure

ASP.NET, NHibernate và MVP

Code 7: Map kết quả trả về từ stored procedure trong XML
– Đưa ra khả năng tương tác với stored procedure lại nảy sinh một vấn đề giữa quyết định cái gì nên được xử lý tính toán bằng stored procedure và cái gì nên được thực hiện trong domain layer. Ngoài stored procedure, C# code có thể lấy tất cả các order được customer đó đặt mua, duyệt qua các order đó và tính tổng các product. Nhưng rõ ràng rằng để SQL server thực hiện chuyện đó sẽ hiệu quả hơn nhiều nếu như customer này đã đặt hàng ngàn Order dẫn đến rất nhiều thông tin được lưu trong database. Mặt khác, giả sử khi ta thử dùng một chương trình analysis nào đó để để đo thử và thấy rằng tính toán bằng C# sẽ chạy lẹ hơn khi gọi Stored Procedure, thì lúc đó nên để domain layer làm công việc xử lý nếu muốn. Nói chung chúng ta nên dung hòa giữa hai lựa chọn này để tối ưu hóa performance.

III. Tóm tắt

Trong phần một này chúng ta đã tạm thời chấp nhận việc phải tạo các lớp Domain để sử dụng với NHibernate, chúng ta cũng biết rằng ngoài việc tạo các lớp này còn phải viết thêm các file XML để mapping chúng với database. Tất cả các file này nên được đặt trong cùng một project, có thể được đặt tên project là TênProject.Core. Thêm nữa, các file XML phải là các embeded resource và phải được viết theo convention của NHibernate. Trongphần tiếp theo chúng ta sẽ tìm hiểu kĩ hơn bước khai báo các Interface được sử dụng trong layer Data.
Code phần 1: http://nthoaiblog.googlepages.com/EnterpriseSample-part1.zip
Các đoạn code minh họa trong bài viết được rút gọn cho dễ hiểu, code được implement cuối cùng trong project sẽ có một số điểm khác biệt chẳng hạn như thêm constructor, thêm một số method, …
(Còn tiếp)

Làm thế nào để tạo một Webservice và cách sử dụng một webservice đã có ở trên máy local và ở trên mạng


Trước hết, ta cần khởi tạo một ứng dụng webservice. Đây là một ứng dụng để kiểm tra tỉ giá ngoại tệ và update các loại tỉ giá đó. CSDL là sql server 2000. gồm có 2 bảng có đính kèm luôn trong ứng dụng.

Ta tạo một webservice có tên là GetExchangeRateService có nội dung như sau:

 

 

[WebMethod]
public string GetExchangeRate(string currency1, string currency2)
{

SqlConnection con = new SqlConnection("Server=(local);Database=AptechBankDB;UID=sa;PWD=sa");
con.Open();

DataSet ds = new DataSet();

SqlCommand cmd = new SqlCommand("pr_GetExchangeRate", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@Currency1",SqlDbType.VarChar));
cmd.Parameters.Add(new SqlParameter("@Currency2", SqlDbType.VarChar));

cmd.Parameters["@Currency1"].Value = currency1;
cmd.Parameters["@Currency2"].Value = currency2;

SqlDataAdapter da = new SqlDataAdapter(cmd);
da.Fill(ds);

if (ds.Tables[0].Rows.Count > 0)
{
return ds.Tables[0].Rows[0]["Rate"].ToString();
}
else
{
return "0";
}

}

Phương thức này có 2 tham số: tham số thứ 1 là ngoại tệ quy đổi, tham số thứ 2 là tiền quy đổi. Giá trị trả về là tỉ giá của 2 loại tiền tệ đó.

Ta tạo websivice thứ 2 có tên là: UpdateExchangeRateService

 

[WebMethod]
public bool UpdateExchangeRate(string currency1, string currency2, float rate, string email, string pass)
{
SqlConnection con = new SqlConnection("Server=(local);Database=AptechBankDB;UID=sa;PWD=sa");
con.Open();

DataSet ds = new DataSet();

SqlCommand cmd = new SqlCommand("pr_Check", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@Email", SqlDbType.VarChar));
cmd.Parameters.Add(new SqlParameter("@Password", SqlDbType.VarChar));

cmd.Parameters["@Email"].Value = email;
cmd.Parameters["@Password"].Value = pass;

SqlDataAdapter da = new SqlDataAdapter(cmd);
da.Fill(ds);

if (ds.Tables[0].Rows.Count > 0)
{

SqlCommand cmdUpd = new SqlCommand("pr_UpdateExchangeRate", con);
cmdUpd.CommandType = CommandType.StoredProcedure;
cmdUpd.Parameters.Add(new SqlParameter("@Currency1", SqlDbType.VarChar));
cmdUpd.Parameters.Add(new SqlParameter("@Currency2", SqlDbType.VarChar));
cmdUpd.Parameters.Add(new SqlParameter("@Rate", SqlDbType.Float));

cmdUpd.Parameters["@Currency1"].Value = currency1;
cmdUpd.Parameters["@Currency2"].Value = currency2;
cmdUpd.Parameters["@Rate"].Value = rate;

int i = cmdUpd.ExecuteNonQuery();

if (i > 0)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}

Phương thức này có 5 tham số. 2 tham số đầu tiên là các tỉ giá ngoại tệ để update, rate ở đây là tỉ lệ giữa các ngoại tệ đó.

Còn 2 tham số thứ 4 và 5 dùng để kiểm tra sự hợp pháp của quyền admin.

Tiếp theo, ta xây dựng một giao diện website để sử dụng các webservice mà mình vừa tạo. Chú ý ta phải add Web Reference vào website này. Đối với VS 2005, có 2 cách để chèn vào, đó là sử dụng luôn webservice mình tạo trong cùng một solution, thứ 2 là webservice ở trên local, tức là mình phải đặt service đó trong IIS hoặc chọn nò là websharing.

 

webReference (400 x 285)

Với việc kiểm tra tỉ giá ngoại tệ, ta chỉ cần dùng dòng lệnh sau:

localhost.GetExchangeRateService getEx = new localhost.GetExchangeRateService();

lblResults.Text = getEx.GetExchangeRate(txtCurrency1.Text, txtCurrency2.Text);

Tương tự như trên, ta làm phần update tỉ giá:

localhost.UpdateExchangeRateService up = new localhost.UpdateExchangeRateService();

bool isTrue = up.UpdateExchangeRate(txtCurrency1.Text, txtCurrency2.Text, float.Parse(txtRate.Text), txtEmail.Text, txtPass.Text);

if (isTrue)
{
lblResults.Text = "Update Successful!";
}
else
{
lblResults.Text = "Update fail!";
}

rên đây là một ứng dụng nhỏ về xây dựng một webservice. bạn có thể download mã nguồn tại đây