вторник, 7 апреля 2015 г.

Не пора ли написать код, который будет писать код (CodeDOM)

Думаю мало кто удивится, узнав, что существует множество способов авто-генерации кода. Это code-behind файлы, которые любезно создаёт Visual Studio, это T4 шаблоны, которые позволяют упростить генерацию однотипных файлов. Так же есть ряд способов на этапе исполнения программы динамически определить логику работы (System.Reflection.Emit namespace или Expression Trees). Но сегодня я хотел бы рассказать о другом подходе – CodeDOM.

В данной статье я покажу 2 примера:

  1. Как динамически создать программу “Hello World”. В результате у нас будет *.cs файл и скомпилированная программа, которую мы запустим.
  2. На базе уже имеющегося кода из примера 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 файлов на основе таблицы в базе данных.

Имея вот такую вот таблицу в базе
image

мы можем получить вот такой файл

//------------------------------------------------------------------------------
// <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'е.

Комментариев нет:

Отправить комментарий