Showing posts with label ORM. Show all posts
Showing posts with label ORM. Show all posts

Paging with LinQ to SQL

- Nếu ai đã từng làm chức năng phân trang cho một website thì đều biết rằng có nhiều phong cách để thực hiện, nhưng ý tưởng thì tương đối giống nhau. Bạn cần lấy đúng số lượng record cần hiển thị từ database để thể hiện dữ liệu lên màn hình. Sẽ có một control để hiển thị các số thứ tự trang, vị trí trang hiện tại, các quick button để đi tới trang kế tiếp, trang trước đó, v.v. Vì vậy ngoài việc lấy đúng số lượng record mong muốn, ta cần lấy được tổng số record trong database để có thể tính được tổng số trang cho control pagging.:bbpskien:

- Để thực hiện các trang report tương đối phức tạp, người ta thường sử dụng Stored Procedure. Khi kết hợp với function ROW_NUMBER() của SQL 2005, người ta có thể dễ dàng viết câu query để lấy đúng phần dữ liệu mong muốn. Trong LinQ2SQL, việc gọi 1 Stored Procedure khá dễ dàng. Kết quả sẽ được map vào một DTO (Data transfer object) cho chúng ta. Code của tất cả các thành phần cần thiết như các lớp data context, lớp của các lớp DTO cũng như file xml mapping có thể được sinh ra dễ dàng chỉ với một thao tác drag and drop. Trong bài viết này, tôi xin giới thiệu một tính năng khác khá hay của LinQ2SQL, chúng ta sẽ sử dụng tính năng này để vừa tính tổng số lượng record đồng thời lấy phần dữ liệu cho một trang mà chỉ cần một lần gọi Stored Procedure.:bbpcuoi5:

- Khi chúng ta viết một Stored Procedure như ở code số 1, rồi execute nó thì kết quả nhận được sẽ gồm hai phần, phần thứ nhất là số lượng record ứng với câu query đầu tiên trong Stored Procedure, và phần còn lại là các record thoả điều kiện.

CREATE PROCEDURE [dbo].[GetCustomers]

@FromIndex INT,
@NumberOfRecord INT

AS
BEGIN

SELECT COUNT(*) as TotalCustomer FROM [Customers]

SELECT * FROM
(SELECT ROW_NUMBER() OVER (ORDER BY U.CustomerID) AS RowNumber, *
FROM [Customers] U ) AS Customer
WHERE RowNumber > @FromIndex AND RowNumber <= @FromIndex + @NumberOfRecord

END

Code 1: Câu Stored Procedure để lấy dữ liệu phân trang


- Tui xin không đi sâu giải thích các query của Stored Procedure trong code 1, chỉ lưu ý là khi Stored Procedure này được gọi thì sẽ có 2 kết quả trả về như đã nói ở trên. LinQ2SQL cho phép ta nhận được cả hai kết quả này bằng cách sử dụng IMultipleResults. Tất cả các thứ ta cần để gọi một Stored Procedure bằng LinQ2SQL là một file xml mapping, các lớp C# mà ta hay gọi là DTO để map dữ liệu từ Stored Procedure, và một lớp DataContext. Tất cả những thứ này đều có thể được sinh ra tự động bằng cách drag and drop các database object vào màn hình designer của LinQ2SQL Classes. Xem hình 1::bbpxtay:
Pagging with LinQ 2 SQL

Hình 1: Màn hình designer của LinQ2SQL Classes

- Tuy các thứ ta cần có thể được sinh ra tự động từ việc drag and drop, nhưng thường thì ta phải sửa lại một số chỗ để được kết quả cuối cùng. Để generate được code gọi Stored Procedure bằng LinQ2SQL, ta chỉ việc kéo và thả Stored Procedure mong muốn vào màn hình designer. Sau đó, ta xem file dbml bằng một XML Viewer sẽ thấy được nội dung xml mapping. Xem code C# của file dbml này ta sẽ thấy được code mình muốn, ta sẽ phải sửa lại code này để có thể nhận cả 2 kết quả trả về của Stored Procedure mà mình thiết kế. Tất cả code C# cho lớp DataContext, cho các lớp DTO đều được sinh ra bên trong một file C# của LinQ2SQL Classes. Chúng ta sẽ copy những nội dung mong muốn và tạo thành các lớp riêng như ý mình. Các code dưới đây là nội dung của file mapping và các lớp DTO.

<?xml version="1.0" encoding="utf-8"?>

<Database Name="NORTHWND" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">

<Function Name="dbo.GetCustomers" Method="GetCustomers">
<Parameter Name="FromIndex" Parameter="fromIndex" DbType="Int" />
<Parameter Name="NumberOfRecord" Parameter="numberOfRecord" DbType="Int" />

<ElementType Name="nthoai.blogspot.com.PaggingLTS.Entity.CustomerCount">
<Column Name="TotalCustomer" Member="TotalCustomer" Storage="_TotalCustomer" DbType="Int" CanBeNull="true" />

</ElementType>

<ElementType Name="nthoai.blogspot.com.PaggingLTS.Entity.Customer">
<Column Name="CustomerID" Member="CustomerID" Storage="_CustomerID" DbType="NChar(5) NOT NULL" CanBeNull="false" />
<Column Name="CompanyName" Member="CompanyName" Storage="_CompanyName" DbType="NVarChar(40) NOT NULL" CanBeNull="false" />
<Column Name="ContactName" Member="ContactName" Storage="_ContactName" DbType="NVarChar(30)" CanBeNull="true" />
<Column Name="ContactTitle" Member="ContactTitle" Storage="_ContactTitle" DbType="NVarChar(30)" CanBeNull="true" />
<Column Name="Address" Member="Address" Storage="_Address" DbType="NVarChar(60)" CanBeNull="true" />
<Column Name="City" Member="City" Storage="_City" DbType="NVarChar(15)" CanBeNull="true" />
<Column Name="Region" Member="Region" Storage="_Region" DbType="NVarChar(15)" CanBeNull="true" />
<Column Name="PostalCode" Member="PostalCode" Storage="_PostalCode" DbType="NVarChar(10)" CanBeNull="true" />
<Column Name="Country" Member="Country" Storage="_Country" DbType="NVarChar(15)" CanBeNull="true" />
<Column Name="Phone" Member="Phone" Storage="_Phone" DbType="NVarChar(24)" CanBeNull="true" />
<Column Name="Fax" Member="Fax" Storage="_Fax" DbType="NVarChar(24)" CanBeNull="true" />

</ElementType>

</Function>

</Database>

Code 2: Nội dung file mapping đã được sửa đổi




public class Customer
{

public Customer()
{ }

public string _CustomerID;
public string CustomerID
{

get { return _CustomerID; }
set { _CustomerID = value; }

}

public string _CompanyName;
public string CompanyName
{

get { return _CompanyName; }
set { _CompanyName = value; }

}

public string _ContactName;
public string ContactName
{

get { return _ContactName; }
set { _ContactName = value; }

}

public string _ContactTitle;
public string ContactTitle
{

get { return _ContactTitle; }
set { _ContactTitle = value; }

}

public string _Address;
public string Address
{

get { return _Address; }
set { _Address = value; }

}

public string _City;
public string City
{

get { return _City; }
set { _City = value; }

}

public string _Region;
public string Region
{

get { return _Region; }
set { _Region = value; }

}

public string _PostalCode;
public string PostalCode
{

get { return _PostalCode; }
set { _PostalCode = value; }

}

public string _Country;
public string Country
{

get { return _Country; }
set { _Country = value; }

}

public string _Phone;
public string Phone
{

get { return _Phone; }
set { _Phone = value; }

}

public string _Fax;
public string Fax
{

get { return _Fax; }
set { _Fax = value; }

}

}


public class CustomerCount
{

public CustomerCount()
{
}

public System.Nullable<int> _TotalCustomer;
public System.Nullable<int> TotalCustomer
{

get
{

return this._TotalCustomer;

}
set
{

if (this._TotalCustomer != value)
{

this._TotalCustomer = value;

}

}

}

}

Code 3: Nội dung các file DTO C# để chứa dữ liệu được map từ database


- Tương tự bài trước, ta sẽ tạo ra một lớp DataContext riêng của mình với method gọi Stored Procedure đã được sửa đổi để có thể nhận được cả hai kết quả khi gọi Stored Procedure. Chỗ tôi sửa đổi ở đây chính là thay ISingleResult thành IMultipleResults. Có một điểm lạ là code sinh ra lúc nào cũng là ISingleResult cho dù Stored Procedure có trả về bao nhiêu tập dữ liệu đi nữa, nêu bắt buộc ta phải tự sửa lại ở bước này.:bbptuc:

public class NorthwindDataContext : System.Data.Linq.DataContext
{

public static string mappingFile = @"bin\Mapping.xml";
public static string connectionString = ConfigurationManager.ConnectionStrings["PaggingLTSDemo"].ToString();
public static string mappingPath = PathHelper.ResolePath(mappingFile);
static XmlMappingSource map = XmlMappingSource.FromXml(File.ReadAllText(mappingPath));

/// <summary>
/// Simple Constructor
/// </summary>
public NorthwindDataContext()

:base (connectionString, map)

{

}
/// <summary>
/// Create a LINQ to SQL data context
/// </summary>
/// <param name="connection"></param>
public NorthwindDataContext(string connection)

:base (connection, map)

{
}

[Function(Name = "dbo.GetBannerClickReportDetail")]
[ResultType( typeof(CustomerCount))]
[ResultType( typeof(Customer))]
public IMultipleResults GetCustomers([Parameter(Name = "FromIndex", DbType = "Int")] System.Nullable<int> fromIndex, [Parameter(Name = "NumberOfRecord", DbType = "Int")] System.Nullable<int> numberOfRecord)
{

IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), fromIndex, numberOfRecord);
return (IMultipleResults)result.ReturnValue;

}

}

Code 4: Lớp DataContext

- Chúng ta sẽ làm một lớp DAO để sử dụng, khi gọi hàm chúng ta sẽ nhận được một list các DTO và output tổng số lượng record để ta có thể tính toán số trang

public List<Customer> GetCustomers(int fromIndex, int numberOfRow, out int itemCount)
{

List<Customer> resultList = new List<Customer>();
itemCount = 0;

using (NorthwindDataContext db = new NorthwindDataContext())
{

IMultipleResults result = db.GetCustomers( fromIndex, numberOfRow);
itemCount = result.GetResult<CustomerCount>().First<CustomerCount>().TotalCustomer.Value;
resultList = result.GetResult<Customer>().ToList<Customer>();

}

return resultList;

}

Code 5: Method GetCustomers trong Data Access Object

- Bài viết này sử dụng database NorthWind. Các bạn có thể download file mdf cho NorthWind trên trang của Microsoft ở đây:download Nếu máy bạn đã cài SQL Express thì có thể chạy code sample mà không cần download vì tui đã attach sẵn Database này trong source code.

- Cuối cùng, code sample cho bài viết có thể được download ở đây.download:bbpcuoi3:

Kết luận


Bài viết này giới thiệu một cách phân trang cơ bản và đơn giản với LinQ. Thực hiện theo cách này chúng ta sẽ phải sử dụng Stored Procedure và chỉ cần gọi Stored Procedure này một lần. Trong bài viết tôi không trực tiếp sử dụng các code sinh ra từ VS.NET 2008 mà lại copy và paste chúng thành các file C# ứng với các layer độc lập. Tuy code được sinh tự động nhưng chắc chắn các bạn phải nhúng tay vào sửa lại để map theo ý muốn. Tuy phải động tay động chân nhưng khi đã quen thì sẽ không mất nhiều thời gian. Đặc biệt khi file xml mapping có lỗi thì tất nhiên sẽ không connect và map dữ liệu từ database được, nên thường thì ta nên có 1 unit test project để test các lớp mapping.:bbpraroi:

Posted in Labels: , , | Post a comment 0 comments

Achieving POCOs in Linq to SQL

Bài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết Achieving POCOs in Linq to SQL. Code sample trong bài tham khảo 99,99% từ bài viết gốc của tác giả Sidar Ok.


- LinQ2SQL là một công cụ khá hữu dụng trong các ORM tools hiện nay. Tuy nhiên các lớp Entity sinh ra thường khiến chúng ta không hài lòng vì chúng không phải những lớp C# thuần túy. Đối với một số người, các lớp Entity trong hệ thống cần phải là những POCO, có lẽ để không phải phụ thuộc vào LinQ vì chúng ta biết rằng những lớp này bên trong nó có sử dụng những lớp và interface khác của LinQ. Giảm phụ thuộc là một điều tốt. Tuy từ lúc làm lập trình viên tới giờ tui chưa bao giờ phải rơi vào trường hợp phải thay đổi công nghệ như tình huống chúng ta đang lo xa, nhưng ai mà biết trước được điều gì sẽ xảy ra cho project...:bbpktay:

- Một ngày đẹp trời nào đó, ta quyết định sử dụng LinQ2SQL để làm DataAccess nhưng lại muốn làm sao để dễ thay đổi khi có một kĩ thuật khác hay và có nhiều ưu điểm hơn LinQ2SQL ra đời. Cho nên lựa chọn tốt nhất là giữ cho các lớp Entity thật đơn giản và không phụ thuộc chút gì đến LinQ2SQL. Để thực hiện được điều đó, chúng ta nên biết rằng các lớp Entity được sinh tự động sẽ bị phụ thuộc vào lớp EntitySet, EntityRef, INotifyPropertyChangingINotifyPropertyChanged. Vậy không phải chỉ cần bỏ những nơi sử dụng các lớp ấy sao? Các lớp và interface này được tạo ra là có lý do của nó, nếu bỏ đi thì chắc chắn chúng ta sẽ phải hi sinh một số tính năng mà LinQ2SQL hỗ trợ. Những tính năng đó là gì?:bbpnen:

+ Khi bạn thêm một Entity mới vào một danh dách, EntitySet sẽ quản lý entity này cho bạn đồng thời nó cũng cập nhật mối quan hệ giữa entity này với parent của nó. Tương tự đối với EntityRef

+ INotifyPropertyChangingINotifyPropertyChanged giúp chúng ta đăng kí những sự kiện cần thiết khi một property của Entity thay đổi và nó cũng giúp hỗ trợ chức năng Lazy Load của LinQ2SQL. Vì vậy khi quyết định bỏ không sử dụng 2 interface trên, chúng ta sẽ mất đi tính năng Lazy Load của LinQ2SQL.

- Trong một số trường hợp Lazy Load rất hay nhưng đôi khi nó cũng gây khá nhiều phiền phức. Tôi chỉ sử dụng LinQ2SQL để làm các công việc cập nhật, update hoặc thêm một object đơn giản, những trường hợp phức tạp như xuất report, tôi vẫn sử dụng Stored Procedure và rất may là LinQ2SQL có hỗ trợ Stored Procedure.:bbpxtay:

- Trong bài này, chúng ta sẽ tìm cách né EntitySetEntityRef. Còn Lazy Loading sẽ nói trong bài khác. Khi sử dụng LinQ2SQL, có 2 cách để map các property của Entity với các column của table trong database. Chúng ta sẽ sử dụng XML mapping thay vì sử dụng attribute để giữ cho các lớp Entity trong sạch :bbpcuoi3:. Ví dụ được sử dụng trong bài này là các model QuestionsAnswers. Một Questions có thể có nhiều Answers tương ứng với nó:

Class Diagram

Hình 1: Class Question và Answer ví dụ trong bài viết

- Và sau đây là code POCO của lớp QuestionsAnswer. Chúng ta có thể sử dụng tool sqlmetal.exe để sinh ra các lớp này và file xml mapping. Tất nhiên chúng ta phải copy và paste đồng thời sửa đổi những phần cần thiết từ các file sinh ra để được những lớp POCO mong muốn.

public class Answer
{

public Answer()
{ }

private int _AnswerId;
public int AnswerId
{

get { return _AnswerId; }
set { _AnswerId = value; }

}

private int _QuestionId;
public int QuestionId
{

get { return _QuestionId; }
set { _QuestionId = value; }

}

private string _AnswerText;
public string AnswerText
{

get { return _AnswerText; }
set { _AnswerText = value; }

}

private bool _IsMarkedAsCorrect;
public bool IsMarkedAsCorrect
{

get { return _IsMarkedAsCorrect; }
set { _IsMarkedAsCorrect = value; }

}

private int _Vote;
public int Vote
{

get { return _Vote; }
set { _Vote = value; }

}

}

Code 1: Code của lớp POCO Answer
Các bạn đang xem bài viết về Achieving POCOs in Linq to SQL từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)


- Như các bạn thấy code của lớp Answer rất sạch sẽ, không có Attribute, không EntityRef, EntitySet gì cả. Tương tự đối với lớp Questions, vì một Questions có thể có nhiều quan hệ với các Answer nên ta sẽ sử dụng List như sau::bbpraroi:

public class Question
{

public Question()
{ }

private int _QuestionId;
public int QuestionId
{

get { return _QuestionId; }
set { _QuestionId = value; }

}

private string _QuestionText;
public string QuestionText
{

get { return _QuestionText; }
set { _QuestionText = value; }

}

private List<Answer> _Answers;
public List<Answer> Answers
{

get { return _Answers; }
set { _Answers = value; }

}

}

Code 2: Code của lớp POCO Question

- Chúng ta dung tool SQLMetal.exe để sinh ra file mapping cần thiết. File xml khi sinh ra sẽ map các column vào các property của lớp Entity như trong hình. Chúng ta sẽ phải sửa một số chỗ như Type Name cho đúng namespace của các lớp Entity:

<?xml version="1.0" encoding="utf-8"?>
<Database Name="POCOLINQ" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
<Table Name="dbo.Answer" Member="Answer">
<Type Name="POCODemo.Entity.Answer">
<Column Name="AnswerId" Member="AnswerId" Storage="_AnswerId" DbType="Int NOT NULL IDENTITY" IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" />
<Column Name="QuestionId" Member="QuestionId" Storage="_QuestionId" DbType="Int NOT NULL" />
<Column Name="AnswerText" Member="AnswerText" Storage="_AnswerText" DbType="Text NOT NULL" CanBeNull="false" UpdateCheck="Never" />
<Column Name="IsMarkedAsCorrect" Member="IsMarkedAsCorrect" Storage="_IsMarkedAsCorrect" DbType="Bit NOT NULL" />
<Column Name="Vote" Member="Vote" Storage="_Vote" DbType="Int NOT NULL" />
<Association Name="FK_Answer_Question" Member="Question" Storage="_Question" ThisKey="QuestionId" OtherKey="QuestionId" IsForeignKey="true" />

</Type>

</Table>
<Table Name="dbo.Question" Member="Question">
<Type Name="POCODemo.Entity.Question">
<Column Name="QuestionId" Member="QuestionId" Storage="_QuestionId" DbType="Int NOT NULL IDENTITY" IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" />
<Column Name="QuestionText" Member="QuestionText" Storage="_QuestionText" DbType="NVarChar(300) NOT NULL" CanBeNull="false" />
<Association Name="FK_Answer_Question" Member="Answers" Storage="_Answers" ThisKey="QuestionId" OtherKey="QuestionId" DeleteRule="NO ACTION" />

</Type>

</Table>

</Database>

Code 3: File xml mapping(đã được sửa đổi)

- SQLMetal.exe sẽ sinh ra một lớp DataContext cùng với nội dung các lớp Entity. Chúng ta đã copy code sang các lớp Entity, tất nhiên cũng phải tạo một lớp DataContext riêng để sử dụng.


C:\>sqlmetal /server:localhost\SQLEXPRESS /database:POCOLINQ /map:Mapping.xml /code:QuestionDataContext.cs
Microsoft (R) Database Mapping Generator 2008 version 1.00.21022
for Microsoft (R) .NET Framework version 3.5
Copyright (C) Microsoft Corporation. All rights reserved.

C:\>


Hình 2: Câu lệnh sinh code sqlmetal


public class QuestionDataContext : DataContext
{

static XmlMappingSource source = XmlMappingSource.FromXml(File.ReadAllText("Mapping.xml"));
static string connStr = "Data Source=localhost\\SQLEXPRESS;Initial Catalog=POCOLINQ;Integrated Security=True";

public QuestionDataContext()
: base(connStr, source)
{
}

public Table<Answer> Answers
{

get
{

return this.GetTable<Answer>();

}

}

public Table<Question> Questions
{

get
{

return this.GetTable<Question>();

}

}

}

Code 4: Lớp QuestionDataContext

- Bây giờ chúng ta sẽ thử viết một unit test xem code của chúng ta có truy xuất được database và đọc dữ liệu được không.
:bbpsdieu2:

[TestMethod]
public void GetQuestionTest()
{

QuestionRepository qRepository = new QuestionRepository();
int id = 2;
Question actual;
actual = qRepository.GetQuestion(id);
Assert.IsNotNull(actual);
Assert.IsTrue(actual.Answers.Count > 0);

}

Code 5: Unit Test get question by id

- Để code build thành công, chúng ta sẽ implement lớp QuestionRepository. Do đã lược bỏ các tính năng của LinQ2SQL, trong trường hợp này chúng ta phải làm điều đó bằng tay để load tất cả các Answer nếu có ứng với Questions trong database:

public class QuestionRepository
{

public Question GetQuestion(int id)
{

using (QuestionDataContext context = new QuestionDataContext())
{

DataLoadOptions options = new DataLoadOptions();
options.LoadWith<Question>(q => q.Answers);
context.LoadOptions = options;
return context.Questions.Single<Question>(q => q.QuestionId == id);

}

}

}

Code 6: Lớp QuestionRepository
Các bạn đang xem bài viết về Achieving POCOs in Linq to SQL từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)


- Khi run test, các bạn sẽ thấy test method sẽ pass, như vậy là đã đọc được từ database. Để chắc ăn chúng ta sẽ test các câu lệnh Insert:
:bbpraroi:

[TestMethod]
public void InsertQuestionTest()
{

QuestionRepository qRepository = new QuestionRepository();
Question question = new Question()
{

QuestionText = "Temp Question",
Answers = new List<Answer>()
{

new Answer()
{

AnswerText = "Temp Answer 1",
IsMarkedAsCorrect = true,
Vote = 10,

},

new Answer()
{

AnswerText = "Temp Answer 2",
IsMarkedAsCorrect = false,
Vote = 10,

},

new Answer()
{

AnswerText = "Temp Answer 3",
IsMarkedAsCorrect = true,
Vote = 10,

},

}

};

using (TransactionScope scope = new TransactionScope())
{

qRepository.InsertQuestion(question);
Assert.IsTrue(question.QuestionId > 0);
Assert.IsTrue(question.Answers[0].AnswerId > 0);
Assert.IsTrue(question.Answers[1].AnswerId > 0);
Assert.IsTrue(question.Answers[2].AnswerId > 0);

}

}

Code 7: Unit test câu lệnh Insert

- Để code build được, chúng ta viết hàm InsertQuestion cho QuestionRepository
:bbpraroi:

public void InsertQuestion(Question q)
{

using (QuestionDataContext context = new QuestionDataContext())
{

context.Questions.InsertOnSubmit(q);
context.SubmitChanges();

}

}

Code 8: Hàm Insert Question

- Khi run test này, chúng ta sẽ thấy lỗi sau đây:


TestCase 'POCODemo.Test.QuestionMappingTest.InsertQuestionTest'
failed: System.Data.SqlClient.SqlException: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Answer_Question".
The conflict occurred in database "POCOLINQ", table "dbo.Question", column 'QuestionId'.
The statement has been terminated.


Hình 3: Test insert failed

-Chúng ta biết rằng để save được các Entity Answer thì cần phải biết ID của Questions tương ứng. Nhưng làm thể nào để biết được ID của Questions. Vấn để ở chỗ back reference. Chúng ta cần phải tạo một reference đến parent Questions của mỗi Answer bởi vì chúng ta đã bỏ đi EntitySet nên giờ phải tự làm theo cách của chúng ta. Ta modify lại lớp QuestionsAnswer như sau:

// Class Answer
private Question _Question;
public Question Question
{

get { return _Question; }
set
{
_Question = value;
this._QuestionId = value.QuestionId;

}

}


// Class Question
private List<Answer> _Answers;
public List<Answer> Answers
{

get { return _Answers; }
set
{

_Answers = value;
foreach (var answer in _Answers)
{

answer.QuestionId = this.QuestionId;
answer.Question = this;

}

}

}

Code 9: Modify lớp Question và Answer
Các bạn đang xem bài viết về Achieving POCOs in Linq to SQL từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)


- Như vậy là test của chúng ta đã pass.

Kết luận: Mong muốn để đạt được tính decoupling giữa các domain entities và các kĩ thuật trong chương trình là một trong những vấn đề quan trọng mà ta hay gặp. Đối với LinQ2SQL, chúng ta phải quên đi các lớp EntityRef, EntitySet, InotifyPropertyChanged, INotifyPropertyChanging . Các công nghệ của Microsoft nói chung và LinQ nói riêng được thiết kế để rút ngắn thời gian lập trình. Quả thật nếu không quan tâm đến POCO thì có thể nói khi thiết kế xong Database thì với một câu lệnh sqlmetal ta đã có được DataAccessLayer. Theo tôi nên cân nhắc giữa các lựa chọn và tùy vào quy mô Project để có cách sử dụng thích hợp.
(Còn tiếp)
:bbpcheer: Happy New Year

Code sample: http://nthoaiblog.googlepages.com/POCOLINQ2SQL.zip


Posted in Labels: , , , | Post a comment 3 comments

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

}

}

}

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ác huynguyen_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à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õ :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)

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 Development kế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
Các bạn đang xem bài viết ASPNET bằng MVP và NHibernate phần 3 từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)


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
Các bạn đang xem bài viết ASPNET bằng MVP và NHibernate phần 3 từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)


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

rss
 

About Me

Place I've live
Near Bossley Park, Sydney, NSW, Australia
Place I've work
  • Freelancer (from 06/2010 to present)
  • Harvey Nash (from 05/2008 to 06/2010)
  • DataDesign Vietnam (10/2005 to 04/2008)
Place I've studied
  • University of Natural Science (Bachelor of Science HoChiMinh City Vietnam From 2001 to 2005)
  • Le Hong Phong High School (HoChiMinh City Vietnam From 1997 to 2000)