CodeDOM (Code Document Object Model) là một API của .Net giúp bạn có thể viết ra những chương trình tự động sinh ra mã lệnh một cách nhanh chóng và dễ dàng. Thậm chí chỉ cần vài dòng code bạn có thể biên dịch mã lệnh C#, JScript, VB.Net thành một tập tin assembly. Vì thế, CodeDom là một bộ thư viện hữu ích nếu như bạn muốn tạo một compiler đơn giản cho các ngôn ngữ trên
Các khái niệm cơ bản
Các lớp cần để sử dụng nằm trong namespace System.CodeDom.
Trước tiên bạn cần hiểu mô hình và các khái niệm cơ bản được sử dụng trong CodeDom.
Trong đó:
- CompileUnit: Đơn vị chứa toàn bộ CodeDom với một collection các Namespace.
- Namespace: Tên của namespace
- NamespaceImport: Các namespace được sử dụng (bằng chỉ thị using trong C#)
- Type: class, structure, interface, enumeration
- Property, Method, Field: các thành viên của lớp
- Statement/ Expression: câu lệnh/ biểu thức trong method hoặc property.
Tạo class Rectangle bằng CodeDom
Để minh họa cho việc sử dụng CodeDom, tôi sẽ tạo một class Rectangle để tính diện tích hình chữ nhật bao gồm phương thức Main và chạy trong Console (bạn không nên nhầm lẫn class này với class Rectangle trong System.Drawing)
Mã nguồn bằng C# được sinh ra từ CodeDom như sau:
Rectangle.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
namespace CodeDomExample { using System; public class Rectangle { private Int32 _length; private Int32 _width; public Rectangle(Int32 length, Int32 width) { this.Length = length; this.Width = width; } public virtual Int32 Length { get { return this._length; } set { this._length = value; } } public virtual Int32 Width { get { return this._width; } set { this._width = value; } } public virtual Int32 CalculateArea() { int area = Length * Width; return area; } public static void Main() { Rectangle rectClass = new Rectangle(5, 4); int area = rectClass.CalculateArea(); Console.WriteLine(String.Format("Area of rectangle is: {0}",area)); } } } |
Các bước thực hiện
Để sử dụng CodeDom tạo ra lớp Rectangle (hoặc một class hay assembly bất kì). Bạn thực hiện tuần tự các bước từ cao xuống thấp như trong mô hình CodeDom ở trên.
Phần tạo đối tượng CompileUnit có thể bỏ qua nếu như bạn chỉ cần một namespace. Bạn có thể sinh code và biên dịch trực tiếp từ namespace này.
Trong ví dụ này tôi sẽ viết một phương thức CreateRectangleCode() để tạo ra CodeDom cho đoạn mã Rectangle trên. Phương thức này trả về một đối tượng CompileUnit và sẽ được tạo ra cuối cùng để trả về cho phương thức.
Chú ý: Các bước sau sử dụng khá nhiều lớp khác nhau trong System.CodeDom. Mỗi lớp có công dụng riêng để định nghĩa biến, phương thức, khai báo, gán giá trị,… Các lớp này đều được thừa kế từ hai lớp abstract chính là CodeExpression và CodeStatement. Bạn có thể lúng túng và cảm thấy khó hiểu, tuy nhiên chỉ cần dựa vào tên của lớp bạn có thể biết được công dụng của chúng và thông qua IntelliSense và MSDN là đủ để viết được các đoạn mã cần thiết.
1. Tạo Namespace và import các namespace cần thiết:
1 2 3 |
CodeNamespace cNamespace = new CodeNamespace("CodeDomExample"); cNamespace.Imports.Add(new CodeNamespaceImport("System")); |
2. Tạo class và thêm vào namespace
1 2 3 |
CodeTypeDeclaration cClass=new CodeTypeDeclaration("Rectangle"); cNamespace.Types.Add(cClass); |
3. Tạo các field:
Ta cần hai private field _length, _width kiểu Int32 chứa chiều dài, rộng của hình chữ nhật và thêm vào lớp. Constructor của CodeMemberField có tham số đầu tiên là kiểu và tham số thứ hai là tên field. Tùy theo overload mà bạn có thể đặt tham số thứ nhất là một chuỗi, một đối tượng kiểu Type hay CodeTypeReference:
1 2 3 4 5 6 |
CodeMemberField field1 = new CodeMemberField("Int32", "_length"); field1.Attributes = MemberAttributes.Private; CodeMemberField field2 = new CodeMemberField("Int32", "_width"); field2.Attributes = MemberAttributes.Private; cClass.Members.Add(field1); cClass.Members.Add(field2); |
Để kết hợp các attribute với nhau, bạn dùng toán tử | (OR), ví dụ:
field1.Attributes = MemberAttributes.Private | MemberAttributes.Static;
4. Tạo các property:
Sau khi có hai field _length và _width, bạn cần tạo hai property tương ứng là Length và Width bằng lớp CodeMemberProperty. Các property bao gồm các thuộc tính:
– bool HasGet: getter
– bool HasSet: setter
– CodeStatementCollection GetStatement: các câu lệnh trong getter
– CodeStatementCollection SetStatement: các câu lệnh trong setter
1 2 3 4 5 6 |
CodeMemberProperty property1 = new CodeMemberProperty(); property1.Name = "Length"; property1.Attributes = MemberAttributes.Public; property1.Type = new CodeTypeReference("Int32"); property1.HasGet = true; property1.HasSet = true; |
Để viết lệnh cho getter cho property này, bạn cần dùng lớp thêm một đối tượng kiểu CodeMethodReturnStatement vào GetStatement để trả về field _length. Cú pháp như sau:
GetStatements ( return ( field ( this._length ) ) )
Và mã C#:
1 2 3 |
property1.GetStatements.Add(new CodeMethodReturnStatement( new CodeFieldReferenceExpression( new CodeThisReferenceExpression(),"_length"))); |
Với SetStatement ta sẽ dùng CodeAssignStatement để gán dữ liệu từ tham số cho field:
1 2 3 4 |
property1.SetStatements.Add(new CodeAssignStatement( new CodeFieldReferenceExpression( new CodeThisReferenceExpression(),"_length"), new CodeArgumentReferenceExpression("value"))); |
Với propery Width còn lại bạn cũng làm tương tự, chỉ khác phần tên.
5. Tạo Constructor:
Constructor mà ta cần tạo sẽ có hai tham số length và width để khởi tạo giá trị cho hai field tương ứng của class.
1 2 3 4 |
CodeConstructor cConstructor = new CodeConstructor(); cConstructor.Attributes = MemberAttributes.Public; cConstructor.Parameters.Add(new CodeParameterDeclarationExpression("Int32", "length")); cConstructor.Parameters.Add(new CodeParameterDeclarationExpression("Int32", "width")); |
Sau đó gán hai tham số này cho các property Length, Width rồi thêm vào class:
1 2 3 4 5 6 7 8 9 |
cConstructor.Statements.Add(new CodeAssignStatement(new CodePropertyReferenceExpression( new CodeThisReferenceExpression(),"Length"), new CodeArgumentReferenceExpression("length"))); cConstructor.Statements.Add(new CodeAssignStatement(new CodePropertyReferenceExpression( new CodeThisReferenceExpression(), "Width"), new CodeArgumentReferenceExpression("width"))); cClass.Members.Add(cConstructor); |
6. Tạo phương thức CalculateArea():
1 2 3 4 |
CodeMemberMethod cMethod = new CodeMemberMethod(); cMethod.Name = "CalculateArea"; cMethod.Attributes = MemberAttributes.Public; cMethod.ReturnType = new CodeTypeReference("Int32"); |
Phương thức này ta cần khai báo biến area và gán giá trị Length * Width bằng CodeVariableDeclarationStatement. Ta dùng overload sau của lớp này:
public CodeVariableDeclarationStatement(
Type type,
string name,
CodeExpression initExpression
)
Cuối cùng trả về biến area này cho phương thức: |
1 2 |
cMethod.Statements.Add(new CodeMethodReturnStatement( new CodeVariableReferenceExpression("area"))); |
7. Tạo phương thức Main()
Mặc dù cũng là phương thức nhưng CodeDom có một lớp riêng để định nghĩa một phương thức có Entry Point. Cách sử dụng tương tự như bạn tạo phương thức thông thường, tất nhiên là bạn không cần gán tên cho phương thức này:
CodeEntryPointMethod mainMethod = new CodeEntryPointMethod();
Thêm câu lệnh khai báo và khởi tạo một đối tượng Rectangle trong phương thức Main() với tham số là 5 và 4:
1 2 3 4 |
mainMethod.Statements.Add(new CodeVariableDeclarationStatement("Rectangle", "rectClass", new CodeObjectCreateExpression("Rectangle", new CodePrimitiveExpression(5), new CodePrimitiveExpression(4)))); |
Tiếp tục khai báo một biến area và gọi phương thức CalculateArea của Rectangle để tính diện tích:
1 2 3 |
mainMethod.Statements.Add(new CodeVariableDeclarationStatement(typeof(int),"area", new CodeSnippetExpression("rectClass.CalculateArea()"))); |
Dùng phương thức Console.WriteLine() để in biến area và thêm Main() và class:
1 2 3 4 5 |
mainMethod.Statements.Add(new CodeMethodInvokeExpression( new CodeTypeReferenceExpression("Console"), "WriteLine", new CodeVariableReferenceExpression("area"))); cClass.Members.Add(mainMethod); |
8. Đóng gói tất cả vào CompileUnit:
1 2 3 4 |
CodeCompileUnit comUnit = new CodeCompileUnit(); comUnit.Namespaces.Add(cNamespace); return comUnit; |
Đơn giản hóa công việc bằng CodeSnippetExpression
Việc tạo các statement mà bạn đã làm trên các bước trên khá dài dòng và dễ gây nhầm lẫn. Bạn có thể đơn giản hóa điều này bằng cách dùng lớp CodeSnippetExpression để tạo các statement thông qua các chuỗi lệnh truyền vào.
Ví dụ để tạo ra dòng lệnh sau:
Console.WriteLine(area)
Thay vì dùng CodeMethodInvokeExpression kết hợp với CodeTypeReferenceExpression và CodeVariableReferenceExpression như dưới đây:
1 |
mainMethod.Statements.Add(new CodeMethodInvokeExpression( new CodeTypeReferenceExpression("Console"), "WriteLine", new CodeVariableReferenceExpression("area"))); |
Bạn chỉ cần viết:
1 |
mainMethod.Statements.Add(new CodeSnippetExpression("System.Console.WriteLine(area)")); |
Hoặc để return biến area cho phương thức. Thay vì:
1 |
cMethod.Statements.Add(new CodeMethodReturnStatement( new CodeVariableReferenceExpression("area"))); |
Ta sử dụng:
1 |
cMethod.Statements.Add(new CodeSnippetExpression("return area")); |
Sinh mã và biên dịch với CodeDomProvider
CodeDomProvider là một abstract class với các lớp thừa kế là CSharpCodeProvider, VBCodeProvider và JScriptCodeProvider. Lớp này cung cấp các phương thức GenerateCodeFromXX() và CompileAssemblyFromXX() để sinh ra mã và biên dịch thành assembly từ các đối tượng của CodeDom hoặc mã nguồn.
Để thuận tiện, ta tạo một phương thức để tạo ra CodeDomProvider dựa theo ngôn ngữ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private static CodeDomProvider GetCodeDomProvider(string language) { CodeDomProvider codeProvider; language = language.ToUpper(); if (language == "CSHARP") codeProvider = new CSharpCodeProvider(); else if (language == "VBNET") codeProvider = new VBCodeProvider(); else throw new ArgumentException("'{0}' language is not supported", language); return codeProvider; } |
Phương thức để Generate mã nguồn từ CodeDomProvider:
1 2 3 4 5 6 7 8 9 |
private static void GenerateCode(CodeCompileUnit comUnit, TextWriter textWriter, string language) { CodeDomProvider codeProvider = GetCodeDomProvider(language); CodeGeneratorOptions genOptions = new CodeGeneratorOptions(); genOptions.IndentString = " "; codeProvider.GenerateCodeFromCompileUnit(comUnit, textWriter, genOptions); } |
Đối tượng CodeGeneratorOptions để xác định các tùy chọn định dạng của mã nguồn được sinh ra. Ví dụ ở trên thuộc tính IndentString được gán là một chuỗi khoảng trắng để quy định chuỗi kí tự thụt đầu dòng ở mỗi dòng lệnh.
Phương thức trên nhận một TextWriter để xác định luồng xuất mã nguồn. Muốn xuất ra một file văn bản bạn tạo một StreamWriter và tạo một StringWriter nếu muốn lưu mã nguồn vào biến chuỗi.
Phương thức để Compile từ CodeDomProvider:
1 2 3 4 5 6 7 8 9 10 11 |
private static void GenerateExecutableFile(CodeCompileUnit comUnit, string language) { CodeDomProvider codeProvider = GetCodeDomProvider(language); CompilerParameters comParam = new CompilerParameters(); comParam.GenerateExecutable = true; comParam.OutputAssembly = "Rectangle.exe"; comParam.GenerateInMemory = false; codeProvider.CompileAssemblyFromDom(comParam, comUnit); } |
Để compile, ta cần một đối tượng CompilerParameters chứa các tùy chọn biên dịch. Phương thức CompileAssemblyFromDom() biên dịch và trả về một đối tượng CompilerResults chứa kết quả biên dịch. Bạn có thể dựa vào property Errors của đối tượng này để lấy các lỗi phát sinh nếu có. Ví dụ tôi in ra các lỗi phát sinh bằng cách dùng vòng lặp như sau:
1 2 3 4 5 6 7 8 9 10 |
CompilerResults comResults = codeProvider.CompileAssemblyFromDom(comParam, comUnit); if (comResults.Errors.Count > 0) { Console.ForegroundColor = ConsoleColor.Red; foreach (CompilerError ce in comResults.Errors) { Console.WriteLine("Line: {0}: Error Number:{1}\n{2}", ce.Line, ce.ErrorNumber, ce.ErrorText); } Console.ResetColor(); } |
Cuối cùng ta tạo một phương thức Main() để kiểm tra mọi thứ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public static void Main() { CodeDomTest test = new CodeDomTest(); CodeCompileUnit comUnit = test.CreateRectangleCode(); //string language="csharp"; string language = "vbnet"; StringBuilder strBuilder = new StringBuilder(); StringWriter writer = new StringWriter(strBuilder); //StreamWriter writer= new StreamWriter("C:\\a.cs"); //writer.AutoFlush = true; GenerateCode(comUnit, writer, language); Console.WriteLine(strBuilder.ToString()); GenerateExecutableFile(comUnit, language); Console.Read(); } |
Output (VB.Net):