normalian blog

Let's talk about Microsoft Azure, ASP.NET and Java!

RazorEngine の使い方 〜それなりな応用編〜

.NET 系でテンプレートエンジンが必要になった際、twitter で「Java だと Velocity とかの古参&王道ライブラリがあるけど、.NETだとなんじゃらほい?」と呟いたところ、「ASP.NET MVC 系なら RazorEngine では?」との言葉を頂いた。確かに ASP.NET MVC3 から HTML のテンプレートエンジンは Razor を利用しており、安定度はかなり高そうだと思いためしてみた。

まずはの参考情報

まずは公式+αである以下の情報を眺めた。

[ソリューション エクスプローラー]上のプロジェクト項目を右クリックし、[[NuGet パッケージ管理]..]を実行する。これにより選択し、以下の画面の[NuGet パッケージの管理]ダイアログがウィザードを起動するので、右上の検索欄項目にから「Razor」を入力して、「RazorEngine」のライブラリを検索してRazorEngineライブラリをインストールする。
また、別途 NuGet パッケージ管理のコンソールから以下を入力することでもインストール可能だ。

Install-Package RazorEngine

RazorEngine の TIPS 系 〜ViewBag を利用する場合〜

ASP.NET MVC でも利用可能な ViewBag を利用することができる。以下の様にインスタンスを作成し、Razor.Parse() メソッドの引数として渡すことで利用できる。

//出力:ビューバグ用
var viewBag = new RazorEngine.Templating.DynamicViewBag();
viewBag.AddValue("Comment", "ビューバグ用");
Console.WriteLine(RazorEngine.Razor.Parse(@"
@ViewBag.Comment", new List<string>() {
     "ASP.NET MVC",
     "ASP.NET Web API",
     "SignalR"
 }, viewBag, null));

RazorEngine の TIPS 系 〜LINQ を利用する場合〜

LINQを利用する場合、ASP.NET MVC 側でも行っているように、一行目で「@model」を利用して型付を行う必要がある。RazorEngine 内部で dynamic として受けてしまうため、拡張メソッドである LINQ が上手く動作しないために @model の記載が必要となる。

//出力:Model の方を宣言すればそのままでもLINQはOK
Console.WriteLine(RazorEngine.Razor.Parse(@"@model IEnumerable<string>
@if(Model.Any()){
@: Model の方を宣言すればそのままでもLINQはOK
}else{
@: 存在してない
}", STR_LIST));

当然 @model を記載しなくとも、拡張メソッドとしてでなければLINQのメソッドは利用可能だ。

//出力:Enumerable.Any() は OK
Console.WriteLine(RazorEngine.Razor.Parse(@"@if(Enumerable.Any(Model)){
@: Enumerable.Any() は OK
}else{
@: 存在してない
}", STR_LIST));

また、キャストを行ってもLINQの利用は可能だ。

Console.WriteLine(RazorEngine.Razor.Parse(@"@{
var list = (IEnumerable<string>)Model;
}
@if(list.Any()){
@: (IEnumerable<string>) すればOK
}else{
@: 存在してない
}", STR_LIST));

RazorEngine の TIPS 系 〜名前空間の追加〜

名前空間を追加する場合、以下の様に RazorEngine.Templating.TemplateService インスタンスを生成し、AddNamespace() メソッドを利用して名前空間を追加する。

namespace ConsoleApp1{
  public class Person
  {
  	public string Name { get; set; }
  	public uint Age { get; set; }
  	public IList<string> FavoriteTechs { get; set; }
  }
  (中略)

static void RazorEngineShow()
{
        using (var templateService = new RazorEngine.Templating.TemplateService())
        {
            // 自分のアセンブリの名前空間を追加
            templateService.AddNamespace(@"ConsoleApp1");
            // 自分のアセンブリじゃない名前空間をそのまま追加しようとすると落ちる
            //templateService.AddNamespace(@"Newtonsoft.Json");

            var viewBag = new RazorEngine.Templating.DynamicViewBag();
            viewBag.AddValue("Str", "ビューバグ用");

            Console.WriteLine(templateService.Parse<IEnumerable<string>>(@"@{
var person = new Person(){ Age=22 };
}
@person.Age
@ViewBag.Str", STR_LIST, viewBag, "mycache")); //キャッシュ名がnullだと落ちる
        }
}

ただし、サードパーティ製のDLLを利用している場合等は、これだけでは不十分だ。こちらについては次節を参照して欲しい。

RazorEngine の TIPS 系 〜アセンブリ名前空間の追加〜

アセンブリを追加する場合、以下の様に RazorEngine.Compilation.DirectCompilerServiceBase を継承したクラスと RazorEngine.Compilation.ICompilerServiceFactory を継承したクラスを作成し、RazorEngine.Configuration.TemplateServiceConfiguration のインスタンスを作成して拡張ポイントに値を設定する必要がある。
以下は Json.NET の DLL を読み込むサンプルとなる。

namespace ConsoleApp1{
    //アセンブリ追加用クラス①
    public class JsonNetCSharpDirectCompilerService : RazorEngine.Compilation.DirectCompilerServiceBase
    {
        public JsonNetCSharpDirectCompilerService(bool strictMode = true, Func<ParserBase> markupParserFactory = null)
            : base(
                new CSharpRazorCodeLanguage(strictMode),
                new CSharpCodeProvider(),
                markupParserFactory) { }
        public override IEnumerable<string> IncludeAssemblies()
        {
            // Json.NET のアセンブリの場所を追加
            return new[] { typeof(Binder).Assembly.Location, typeof(Newtonsoft.Json.JsonConverter).Assembly.Location };
        }
    }
    //アセンブリ追加用クラス②
    public class CustomCompilerServiceFactory : RazorEngine.Compilation.ICompilerServiceFactory
    {
        public ICompilerService CreateCompilerService(Language language)
        {
            switch (language)
            {
                case Language.CSharp:
                    return new JsonNetCSharpDirectCompilerService();

                case Language.VisualBasic:
                    return new VBDirectCompilerService();

                default:
                    throw new ArgumentException("Unsupported language: " + language);
            }
        }
    }

  public class Person
  {
  	public string Name { get; set; }
  	public uint Age { get; set; }
  	public IList<string> FavoriteTechs { get; set; }
  }
}
  (中略)

static void RazorEngineShow()
{
        var config = new RazorEngine.Configuration.TemplateServiceConfiguration();
        config.CompilerServiceFactory = new CustomCompilerServiceFactory();
        using (var templateService = new RazorEngine.Templating.TemplateService(config))
        {
            templateService.AddNamespace(@"ConsoleApp1");
            templateService.AddNamespace(@"Newtonsoft.Json");

            //設定した TemplateService を Razor に設定
            RazorEngine.Razor.SetTemplateService(templateService);
            Console.WriteLine(RazorEngine.Razor.Parse<IEnumerable<string>>(@"@{
var str = JsonConvert.SerializeObject(new Person()
    {
        Age = 22
    });
}
@str", STR_LIST, new DynamicViewBag(), "mycache")); //キャッシュ名がnullだと落ちる
        }
}

独自アセンブリを RazorEngine のテンプレート内部で利用するのはちょっと骨が折れるようだ…。