2011年1月3日月曜日

Razor Hosting

Hosting the Razor Engine for Templating in Non-Web Applications - Rick Strahl's Web Log

いや~、参っちゃった。年末にRazor Templating Engine出てたじゃないですか。で、Instapaper見てみたらこっちも”後で読む”扱いになってたので、そろそろ読むかと読んでみたわけですよ。

そしたらこっちはハナからAppDomainを別にしてコンパイルするのを前提に作られてるじゃない。もちろんプロセスのAppDomainと同じところでコンパイルも出来ますよ。その辺はぬかりないデスヨ。同じ目的でもこんなにも設計が違うんだね~、っていう感じです。ちなみにRickさんのRazor HostingではVB.NETコンパイラは入ってないです。

はい、Hello World!

var engine = new RazorEngine<RazorTemplateBase>();
dynamic context = new ExpandoObject();
context.Name = "World!";
var result = engine.RenderTemplate("Hello @Context.Name", new string[] { }, context);

匿名クラスじゃダメだったからdynamicなExpandoObjectで。まぁ、その辺は適当でね。

続いて、HtmlHelperを使うようにして見ましょう!RTE(Razor Templating Engine)にはちゃんとサンプルがあったからそのまま簡単にできたけど、今回は無いのでしょうがないから自分で書く。と、言ってもRTEのをままもってきて、RazorEngineのジェネリックに基底クラスとして指定です。この辺ちょっとあれっすね。まぁ、いいや。

internal class HtmlHelperFactory
{
    #region Methods
    /// <summary>
    /// Creates a <see cref="HtmlHelper{T}"/> for the specified model.
    /// </summary>
    /// <typeparam name="T">The model type.</typeparam>
    /// <param name="model">The model to create a helper for.</param>
    /// <param name="writer">The writer used to output html.</param>
    /// <returns>An instance of <see cref="HtmlHelper{T}"/>.</returns>
    public HtmlHelper<T> CreateHtmlHelper<T>(T model, TextWriter writer)
    {
        var container = new InternalViewDataContainer<T>(model);
        var context = new ViewContext(
            new ControllerContext(),
            new InternalView(),
            container.ViewData,
            new TempDataDictionary(),
            writer);

        return new HtmlHelper<T>(context, container);
    }
    #endregion

    #region Types
    /// <summary>
    /// Defines an internal view.
    /// </summary>
    private class InternalView : IView
    {
        #region Methods
        /// <summary>
        /// Renders the contents of the view to the specified writer.
        /// </summary>
        /// <param name="context">The current View context.</param>
        /// <param name="writer">The writer used to generate the output.</param>
        public void Render(ViewContext context, TextWriter writer) { }
        #endregion
    }

    /// <summary>
    /// Defines an internal view data container.
    /// </summary>
    private class InternalViewDataContainer<T> : IViewDataContainer
    {
        #region Methods
        /// <summary>
        /// Initialises a new instance of <see cref="InternalViewDataContainer{T}"/>.
        /// </summary>
        public InternalViewDataContainer(T model)
        {
            ViewData = new ViewDataDictionary<T>(model);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the view data dictionary.
        /// </summary>
        public ViewDataDictionary ViewData { get; set; }
        #endregion
    }
    #endregion
}

/// <summary>
/// Provides a base implementation of a template base that supports <see cref="HtmlHelper{T}"/>s.
/// </summary>
/// <typeparam name="T">The model type.</typeparam>
[RequireNamespaces("System.Web.Mvc.Html")]
public class HtmlRazorTemplateBase<T> : RazorTemplateBase
{
    private readonly HtmlHelperFactory factory = new HtmlHelperFactory();

    /// <summary>
    /// Initialises a new instance of <see cref="HtmlTemplateBase{T}"/>.
    /// </summary>
    public HtmlRazorTemplateBase()
    {
        CreateHelper(Context);
    }
    /// <summary>
    /// Gets the <see cref="HtmlHelper{T}"/> for this template.
    /// </summary>
    public HtmlHelper<T> Html { get; private set; }

    private void CreateHelper(T model)
    {
        Html = factory.CreateHtmlHelper(model, Response.Writer);
    }
}

そのままですみませんね!

後は、テンプレートを書いてテイッ!と実行。

static string Template =
@"
@using System.Web.Mvc
@using System.Web.Mvc.Html
Hello @Context.Name
@AppDomain.CurrentDomain.FriendlyName
Email: @Html.TextBoxFor(m => m.Email)";

public void ExecuteParse()
{
    ExecuteParse(-1);
}

public void ExecuteParse(int counter)
{
    var model = new PageModel
                    {
                        Name = "World", 
                        Email = "someone@somewhere.com"
                    };
    var asms = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic ).Select(a=>a.Location);
    asms = asms.Concat(typeof(System.Web.Mvc.HtmlHelper)
                        .Assembly
                        .GetReferencedAssemblies().Select(an => Assembly.Load(an.FullName).Location)).Distinct();
    var refs = asms.Where(a => !a.EndsWith("System.dll")
                    && !a.EndsWith("System.Core.dll")
                    && !a.EndsWith("Microsoft.CSharp.dll")).ToArray();

    for (var i = 0; i < (counter != -1 ? 1 : Repeat); i++)
    {
        var sw = new Stopwatch();
        sw.Start();
        var engine = new RazorEngine<HtmlRazorTemplateBase<PageModel>>();
        var result = engine.RenderTemplate(Template, refs, model);

        sw.Stop();

        Console.WriteLine(result);
        Console.WriteLine(
            "{0} - {1} {2} ms {3:0,###} bytes",
            AppDomain.CurrentDomain.FriendlyName,
            counter != -1 ? counter : i,
            sw.ElapsedMilliseconds,
            Process.GetCurrentProcess().PrivateMemorySize64);
    }
}

なんですが、ここでかなりはまりました。

Template Execution Error: Security Transparent メソッド 'System.Web.Mvc.TypeDescriptorHelper.Get(System.Type)' がセキュリティ上重要なメソッド 'System.ComponentModel.DataAnnotations.AssociatedMetadataTypeTypeDescriptionProvider..ctor(System.Type)' にアクセスしようとして失敗しました。

アセンブリ 'System.ComponentModel.DataAnnotations, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' は条件付きの APTCA アセンブリであり、現在の AppDomain では有効化されていません。このアセンブリを有効化して部分信頼コードまたは Security Transparent コードで使用できるようにするには、AppDomain の作成時に、アセンブリ名 'System.ComponentModel.DataAnnotations, PublicKey=0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9' を PartialTrustVisibleAssemblies リストに追加してください。

なんすかこのエラー。コエー。入っちゃいけない領域に入っちゃった系のエラーなんじゃ...。

ASP.NET アプリケーションでコード アクセス セキュリティを使用します。

なんかDataAnnotationsも含まれてるね~。透過的モデルに移行したアセンブリ。

だいたいが

[assembly: System.Security.AllowPartiallyTrustedCallers()]

これ書けって書いてるんだけど、これ何さ。ってなもんで、いろいろよくわかんない。そもそもRTEでは動くんだから何か違いが有るんだろうと、参照アセンブリとかいろいろ比較してみたわけですよ。

そしたらですね、ちゃんと違いがありました。

Razor Templating Engine

↑ここに行って、RazorEngine.Templates.csprojを見てみてください。

rh1

衝撃!分かります?普通さ~、Razorっつったら、MVC3を想定するじゃないっすか。だから、HtmlHelper使うために参照するアセンブリもMVC3のものにしちゃうじゃないっすか。そこだった...。ショック。MVC2で動かしてやがった。なんか意味があるんだろうね。いつかちゃんと調べるとしてですね、とりあえずRazor Hostingの場合もMVC2のアセンブリを参照してHtmlHelperを呼び出すとちゃんとできた。

rh2

はぁ...。ぜんぜんわかんなかった。いや、今でも理由はよく分からない。ATPCAってなにさ。部分信頼ってなんだね。AllowPartiallyTrustedCallersつけたら↓こう。

rh3

型 'ConsoleApplication1.HtmlRazorTemplateBase`1<T>' で継承セキュリティ ルールの違反が発生しました。派生型は、基本型のセキュリティ アクセシビリティと一致するか、それより低いアクセスが設定されている必要があります。

CASが廃止。.NET 4のセキュリティはどうなるのか? - @IT

ふんふん、そっか!なるほど!

...

よし、がんばろう。

で、コンパイル済みのものを呼び出すときは

var engine = new RazorEngine<HtmlRazorTemplateBase<PageModel>>();
var compiledId = engine.ParseAndCompileTemplate(refs, new StringReader(Template));

var result = engine.RenderTemplateFromAssembly(compiledId, model);

こうね。はいはい。

続いて、違うAppDomainで実行するときはこう。

var host = new RazorStringHostContainer<HtmlRazorTemplateBase<PageModel>>
{
    UseAppDomain = true,
    ReferencedAssemblies = refs.ToList()
};

host.Start();
var result = host.RenderTemplate(Template, model);
host.Stop();

ちゃんとHost.Start()とStop()しようね。これがCreateAppDomainとUnload。

ちなみにこのコードはこのままじゃRazor Hostingでは動きません。何でかというと、RazirStringHostContainerはジェネリックじゃないから。基底クラスの指定方法がHost派生しかなかったから、それじゃ面倒なので、RazorStringHostContainer自体を書き換えてます。

といっても、定義だけ。

    public class RazorStringHostContainer<T> : RazorBaseHostContainer<T>
        where T : RazorTemplateBase, new()
    {        

rh4

ちょいちょいAppDomainを生成するのはやっぱり遅すぎ。もうちょっと賢くやんないとね。

と、言うわけでRazorのテンプレートエンジン2作品を取り上げてみたけど、HtmlHelperを使ったりするようにWebで使う場合はAPTCAのところをちゃんとクリアしてからじゃないとダメですね。

ここまで書いてて気がついたんだけど、MVC3ってどうやってるのか見るといいのかな。BuildManagerだと違ったりするのかな。うぬ~。なぞは深まるばかり。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。