patterns & practices Application Architecture Guide 2.0
Posted On Friday, January 23, 2009 at at 1:13 PM by UnknownKey Links
- Application Architecture Guide 2.0 - http://www.codeplex.com/AppArchGuide
- Application Architecture Knowledge Base – http://www.codeplex.com/AppArch
Why ASP.NET Developers Should Care about Windows Azure
Posted On at at 10:49 AM by Unknownhttp://stephenwalther.com/blog/archive/2009/01/11/why-asp.net-developers-should-care-about-windows-azure.aspx
By Stephen Walther
Paging with LinQ to SQL
Posted On Friday, January 16, 2009 at at 5:48 PM by Unknown- 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.
- Để 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.
- 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.
@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:
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.
<Database Name="NORTHWND" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
<Function Name="dbo.GetCustomers" Method="GetCustomers">
<Parameter Name="NumberOfRecord" Parameter="numberOfRecord" DbType="Int" />
<ElementType Name="nthoai.blogspot.com.PaggingLTS.Entity.CustomerCount">
</ElementType>
<ElementType Name="nthoai.blogspot.com.PaggingLTS.Entity.Customer">
<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 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.
{
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
{
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: 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.
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.
Achieving POCOs in Linq to SQL
Posted On Friday, January 2, 2009 at at 3:38 PM by UnknownBài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết 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...
- 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, INotifyPropertyChanging và INotifyPropertyChanged. 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ì?
+ 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
+ INotifyPropertyChanging và INotifyPropertyChanged 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.
- Trong bài này, chúng ta sẽ tìm cách né EntitySet và EntityRef. 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 . Ví dụ được sử dụng trong bài này là các model Questions và Answers. Một Questions có thể có nhiều Answers tương ứng với nó:
Hình 1: Class Question và Answer ví dụ trong bài viết
- Và sau đây là code POCO của lớp Questions và Answer. 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 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
- 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
{
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:
<Database Name="POCOLINQ" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
<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">
<Column Name="QuestionId" Member="QuestionId" Storage="_QuestionId" DbType="Int NOT NULL IDENTITY" IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" />
<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
{
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.
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 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
- 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:
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
{
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 Questions và Answer như sau:
private Question _Question;
public Question Question
{
get { return _Question; }
set
{
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
- 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òn tiếp)
Code sample: http://nthoaiblog.googlepages.com/POCOLINQ2SQL.zip