Думаю мало кто удивится, узнав, что существует множество способов авто-генерации кода. Это code-behind файлы, которые любезно создаёт Visual Studio, это T4 шаблоны, которые позволяют упростить генерацию однотипных файлов. Так же есть ряд способов на этапе исполнения программы динамически определить логику работы (System.Reflection.Emit namespace или Expression Trees). Но сегодня я хотел бы рассказать о другом подходе – CodeDOM.
В данной статье я покажу 2 примера:
- Как динамически создать программу “Hello World”. В результате у нас будет *.cs файл и скомпилированная программа, которую мы запустим.
- На базе уже имеющегося кода из примера 1 мы увидим как просто сделать генерацию DTO классов на основе схемы имеющейся базы данных.
Hello World!
Для запуска нашей первой, динамически созданной программы, нам необходимо выполнить следующие шаги:
- Создать “код-граф”, который будет определять ход выполнения программы
- Сгенерировать *.cs файл на основе графа и сохранить на диск
- Скомпилировать созданный ранее файл и получить *.exe файл
- Запустить *.exe файл
Вот как это выглядит в коде:
private static void RunHelloWorld()
{
const string FileName = "HelloWorld";
const string SrcFile = FileName + ".cs";
const string ExeFile = FileName + ".exe";
// Create code graph
CodeCompileUnit codeGraph = HelloWorldBuilder.Build();
// Generate file based on code graph
FileGenerator.CreateFile(codeGraph, SrcFile);
// Compile generated source file into an executable output file
CodeCompiler.Compile(SrcFile, ExeFile);
// Execute compiled app
Process.Start(ExeFile);
}
Теперь давайте подробней разберём каждый шаг.
Создание графа
Первым шагом будет создание CodeCompileUnit, который будет представлять контейнер, содержащий наш код. Затем мы создаём CodeNamespace, который задаёт наше пространство имен и добавляем его внутрь CodeCompileUnit’а. Остальной код уже будет содержаться в объекте CodeNamespace. В него мы добавим объявление класса MyClass, внутри которого будет метод Main(). Ниже можно увидеть как это выглядит в коде:
static class HelloWorldBuilder
{
public static CodeCompileUnit Build()
{
// Create code container
var compileUnit = new CodeCompileUnit();
// Declare a new namespace
var samples = new CodeNamespace("MyNameSpace");
compileUnit.Namespaces.Add(samples);
// Import System namespace
samples.Imports.Add(new CodeNamespaceImport("System"));
// Declare class
var myClass = new CodeTypeDeclaration("MyClass");
samples.Types.Add(myClass);
// Create Main method.
var mainMethod = BuildMainMethod();
myClass.Members.Add(mainMethod);
return compileUnit;
}
private static CodeEntryPointMethod BuildMainMethod()
{
var method = new CodeEntryPointMethod();
// Create a type reference for the System.Console class.
var csSystemConsoleType = new CodeTypeReferenceExpression("System.Console");
// Add Console.WriteLine statement
method.Statements.Add(
new CodeMethodInvokeExpression(
csSystemConsoleType,
"WriteLine",
new CodePrimitiveExpression("Hello World!")));
// Add another Console.WriteLine statement
method.Statements.Add(
new CodeMethodInvokeExpression(
csSystemConsoleType,
"WriteLine",
new CodePrimitiveExpression("Press the Enter key to exit.")));
// Add the ReadLine statement.
method.Statements.Add(
new CodeMethodInvokeExpression(
csSystemConsoleType,
"ReadLine"));
return method;
}
}
Генерация *.cs файла
Что бы сгенерировать файл нам потребуется CodeDomProvider. Именно он на основе имеющегося CodeComplieUnit’а и при помощи TextWriter'а сгенерирует и сохранит файл на диск.
static class FileGenerator
{
public static void CreateFile(CodeCompileUnit compileunit, string fileName)
{
CreateFolderIfNeeded(fileName);
using (var writer = new StreamWriter(fileName, false))
{
var provider = CodeDomProvider.CreateProvider("CSharp");
var options = new CodeGeneratorOptions { BracingStyle = "C", IndentString = " " };
provider.GenerateCodeFromCompileUnit(compileunit, writer, options);
}
}
private static void CreateFolderIfNeeded(string fileName)
{
var dirPath = Path.GetDirectoryName(fileName);
if (!string.IsNullOrWhiteSpace(dirPath) && !Directory.Exists(dirPath))
Directory.CreateDirectory(dirPath);
}
}
Стоить отметить, что приятным бонусом будет генерация кода на разных .Net совместимых языках. “Из коробки” мы можем выбирать из C#, VisualBasic и JScript. Но при огромном желание можно реализовать свой провайдер и генерить хоть на Brainfuck'e.
Компиляция
Для компиляции нам так же нужен CodeDomProvider, который может создать DLL или EXE файл, на основе файла исходного кода.
static class CodeCompiler
{
public static void Compile(string sourceFile, string exeFile)
{
// add System.dll reference
string[] referenceAssemblies = { "System.dll" };
var cp = new CompilerParameters(referenceAssemblies, exeFile, false);
// specify the result type (exe, not dll)
cp.GenerateExecutable = true;
// compile
var provider = CodeDomProvider.CreateProvider("CSharp");
provider.CompileAssemblyFromFile(cp, sourceFile);
}
}
Вот и всё. Могу вас поздравить с тем, что теперь вы знаете, как создать, скомпилировать и запустить простейшую программу при помощи CodeDOM!
Генерация Dto файлов
Теперь, зная, как сгенерировать файл я хочу показать пример генерации Dto файлов на основе таблицы в базе данных.
Имея вот такую вот таблицу в базе
мы можем получить вот такой файл
//------------------------------------------------------------------------------
// <auto-generated>
// Этот код создан программой.
// Исполняемая версия:4.0.30319.34209
//
// Изменения в этом файле могут привести к неправильной работе и будут потеряны в случае
// повторной генерации кода.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MyNamespace
{
public class Empoyees
{
private int _id;
private string _firstname;
private string _lastname;
private System.Nullable<int> _age;
public int Id
{
get { return this._id; }
set { this._id = value; }
}
public string FirstName
{
get { return this._firstname; }
set { this._firstname = value; }
}
public string LastName
{
get { return this._lastname; }
set { this._lastname = value; }
}
public System.Nullable<int> Age
{
get { return this._age; }
set { this._age = value; }
}
}
}
Код, который это делает, выглядит следующим образом:
IEnumerable<TableInfo> tables = DbHelper.GetTableInfos().Result;
foreach (var table in tables)
{
CodeCompileUnit codeGraph = DtoBuilder.Build(table, "MyNamespace");
var srcPath = "./Dtos/" + table.Name + ".cs";
FileGenerator.CreateFile(codeGraph, srcPath);
}
DbHelper – вспомогательный класс, который подключается к базе данных и возвращает схему таблиц в удобном для обработке виде. Код DtoBuilder ниже:
static class DtoBuilder
{
public static CodeCompileUnit Build(TableInfo tableInfo, string namespase)
{
// Create code container
var compileUnit = new CodeCompileUnit();
// Declare a new namespace
var codeNamespace = new CodeNamespace(namespase);
compileUnit.Namespaces.Add(codeNamespace);
// Declare class
var dtoClass = new CodeTypeDeclaration(tableInfo.Name);
codeNamespace.Types.Add(dtoClass);
// Add backup fields for properties
foreach (var column in tableInfo.Columns)
{
var field = CreatePrivateField(column);
dtoClass.Members.Add(field);
}
// Add properties
foreach (var column in tableInfo.Columns)
{
var prop = CreateProperty(column);
dtoClass.Members.Add(prop);
}
return compileUnit;
}
private static CodeMemberField CreatePrivateField(ColumnInfo column)
{
return new CodeMemberField
{
Type = GetType(column),
Name = FormatPrivateFieldName(column),
};
}
private static CodeMemberProperty CreateProperty(ColumnInfo column)
{
var prop = new CodeMemberProperty
{
Name = column.Name,
Type = GetType(column),
Attributes = MemberAttributes.Public | MemberAttributes.Final
};
var backupFieldName = FormatPrivateFieldName(column);
var _this = new CodeThisReferenceExpression();
prop.GetStatements.Add(
new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(_this, backupFieldName)));
var propValue = new CodePropertySetValueReferenceExpression();
prop.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(_this, backupFieldName), propValue));
return prop;
}
private static string FormatPrivateFieldName(ColumnInfo info)
{
return "_" + info.Name.ToLower();
}
private static CodeTypeReference GetType(ColumnInfo column)
{
switch (column.DataType)
{
case "int":
return column.IsNullable
? new CodeTypeReference(typeof(int?))
: new CodeTypeReference(typeof(int));
case "nvarchar":
return new CodeTypeReference(typeof(string));
default:
throw new NotSupportedException("DbType '" + column.DataType + "' is not supported yet");
}
}
}
Полную версию проекта можно скачать на GitHub'е.
Комментариев нет:
Отправить комментарий