空談録

http://artfulplace.net/blogs/ からひっこしつつ

Microsoft.AspNetCore.Razor.Language + Roslyn + .NET Core でファイル生成をしたい (その2)

読みやすいブログが分からないのでひとまず分けてみるやつ。

artfulplace.hatenablog.com

↑の続きです。C#のコードが作られたのでRoslynで実行するだけの簡単なお仕事。

ではやっていきましょう。

Razorのコードを整形する

まあさすがにそのまま投げてもこけるのが目に見えてるっていうところです。
生成されたコードで呼ばれている"Write"と"WriteLiteral"の呼ぶ先を用意しないとそこで落ちてしまいます。

usingの設定とかをざっくり行ってくれるメソッドを作りましょう。
大体こんな感じでしょうか?

internal class RazorCodeFormatter
{
    internal static string Format(string generatedCode)
    {
        var code = "using System;" + NewLine() + "using System.IO;" + NewLine() + NewLine();
        foreach (var line in generatedCode.Split(new string[] { NewLine() }, StringSplitOptions.RemoveEmptyEntries))
        {
            var trimLine = line.Trim();

            if (trimLine.StartsWith("#"))
            {
                continue;
            }

            if (trimLine.StartsWith("public async override"))
            {
                code += "        public static Action<string> WriteLiteral { get; set; }" + NewLine();
                code += "        public static Action<object> Write { get; set; }" + NewLine();

                code += NewLine();

                code += "        public static void TemplateMain()" + NewLine();
            }
            else
            {
                code += line + NewLine();
            }
        }

        return code;
    }

    private static string NewLine()
    {
        return Environment.NewLine;
    }
}

#pragmaとか#lineを消しつつ、コードを再構築します。
基本は普通につないでます。

違うところとしてはExecuteAsyncの宣言部分です。
この部分についてはこちらで呼ぶ分にはawaitableである必要がないこと、ほぼ確実に一回しか呼ばれないことから別の宣言を作るには適しているという特徴があります。

まあとりあえずstatic voidにしておきます。
名前は何でもいいです。TemplateMainにしておきました。

またWrite, WriteLiteralの値を受け取りつつ実行できるようにするために宣言を加えています。
static ActionでWriteLiteral, Writeを表現している理由は .NET Core固有の問題なんですが、ビルドするときに参照しているライブラリと、実行するときに参照しているライブラリが違うため、Roslynで生成するライブラリから呼び出しているアプリケーションの参照を行えないためです。
(アプリケーション側ではSystem.Runtimeなどを参照していますが、実行時に参照しているのは System.Private.CoreLib.ni.dllです。)

整形メソッドを呼ぶと前回のコードがこんな感じになります。

using System;
using System.IO;

namespace Razor
{
    public class Template
    {
        public static Action<string> WriteLiteral { get; set; }
        public static Action<object> Write { get; set; }

        public static void TemplateMain()
        {

    var test = "hogehogefugafuga";
            WriteLiteral("\r\n\r\na ");
Write(DateTime.Now.ToString());
            WriteLiteral(" Eeeeeeee\r\n\r\n");
Write(false);
            WriteLiteral("\r\n\r\nname = ");
  Write(test);
            WriteLiteral("\r\n");
        }
    }
}

インデントが崩れるのは仕様です…。

ここまで出来たらRoslynで実行していきましょう。

Roslynで実行しよう!

正直Roslynわからないというのでググったら一瞬で答えが出たのでコピペします。

.NET Core - Roslyn と .NET Core によるクロスプラットフォーム コードの生成
↑の図7にある "Emit API とリフレクションによるコードのコンパイルと実行" のコードを丸パク参考にさせていただいてコードを書くと次の通り。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

// ~~~~~~

internal class CodeBuilder
{
    internal static void Build(string code, string filePath)
    {
        var tree = SyntaxFactory.ParseSyntaxTree(code);
        string fileName = "mylib.dll";

        var systemRefLocation = typeof(object).GetTypeInfo().Assembly.Location;
        var systemReference = MetadataReference.CreateFromFile(systemRefLocation);
        
        var compilation = CSharpCompilation.Create(fileName)
          .WithOptions(
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
          .AddReferences(systemReference)
          .AddSyntaxTrees(tree);
        string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
        EmitResult compilationResult = compilation.Emit(path);
        if (compilationResult.Success)
        {
            Assembly asm =
              AssemblyLoadContext.Default.LoadFromAssemblyPath(path);

            RazorViewReceiver.InitializeReceiver(filePath);

            var type = asm.GetType("Razor.Template");
            type.GetProperty("WriteLiteral").SetValue(null, new Action<string>(x => RazorViewReceiver.WriteLiteral(x)));
            type.GetProperty("Write").SetValue(null, new Action<object>(x => RazorViewReceiver.Write(x)));

            type.GetMethod("TemplateMain").Invoke(null, null);

            RazorViewReceiver.FinishReceive();
        }
        else
        {
            foreach (Diagnostic codeIssue in compilationResult.Diagnostics)
            {
                string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()}, Location: { codeIssue.Location.GetLineSpan()}, Severity: {codeIssue.Severity}";
                Console.WriteLine(issue);
            }
        }
    }
}

自分で書いたコード探す方が手間というくらいコピペ…。
Reflectionしか書いてないので解説についてはさっきのページを読むとよいかと

System.Runtimeらへんに参照を追加してコンパイルして作成したDLLを読み込んでReflectionで実行という流れです。
WriteとWriteLiteralにはラムダ式で参照していきます。実行時参照は共通化されるので、SetValueは普通に実行できます。

最後にRazorViewReceiverについて書いていきましょう。

Razorの出力をファイルに書き出す

普通にWriteとWriteLiteral受け取ってStreamWriterで書くだけです。簡単ですね。

public class RazorViewReceiver
{
    private static FileStream file { get; set; }
    private static StreamWriter writer { get; set; }

    public static void InitializeReceiver(string fileName)
    {
        file = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite);
        writer = new StreamWriter(file);
    }

    public static void FinishReceive()
    {
        writer.Flush();
        writer.Dispose();
        file.Dispose();
    }

    public static void WriteLiteral(string content)
    {
        writer.Write(content);
    }

    public static void Write(object content)
    {
        writer.Write(content.ToString());
    }
}

改行文字は勝手にやってくれるので、WriteLineは使わないでおきます。
あとはStreamWriterを開くのと閉じるのを明示的に行うためにInitializeとFinishを用意してTemplateMainを挟むとOKです。

ここまでやったらFormatとBuildをGenerateCodeに対して実行します。
実行した結果が下の画像です。

f:id:fantasticswallow:20170811161918p:plain

すると…なんということでしょう! みすぼらしいcshtmlからこれまたよくわからないファイルが生まれているではありませんか!

まあこれだけだと使いにくい気もします。C#のコードをもっと呼べるようにしたいとかできるはずなのですが今回は面倒なのでしません。


というわけでRazorを使ってファイル生成しました!という話でした。
指定したフォルダのcshtmlを全部一括でやるとかまではやりたいんですけどシンプルにやるならこんくらいかなぁと。

最近会社がMacで家がWindowsで文字を入力するのがカオスになっています。
キーボードの配列くらい統一してくれませんかね…………。

東方の新作が出ている音がするけど辞書ファイルを作るモチベーションが足りていない……。

この辺で。