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では動くんだから何か違いが有るんだろうと、参照アセンブリとかいろいろ比較してみたわけですよ。
そしたらですね、ちゃんと違いがありました。
↑ここに行って、RazorEngine.Templates.csprojを見てみてください。
衝撃!分かります?普通さ~、Razorっつったら、MVC3を想定するじゃないっすか。だから、HtmlHelper使うために参照するアセンブリもMVC3のものにしちゃうじゃないっすか。そこだった...。ショック。MVC2で動かしてやがった。なんか意味があるんだろうね。いつかちゃんと調べるとしてですね、とりあえずRazor Hostingの場合もMVC2のアセンブリを参照してHtmlHelperを呼び出すとちゃんとできた。
はぁ...。ぜんぜんわかんなかった。いや、今でも理由はよく分からない。ATPCAってなにさ。部分信頼ってなんだね。AllowPartiallyTrustedCallersつけたら↓こう。
型 '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() {
ちょいちょいAppDomainを生成するのはやっぱり遅すぎ。もうちょっと賢くやんないとね。
と、言うわけでRazorのテンプレートエンジン2作品を取り上げてみたけど、HtmlHelperを使ったりするようにWebで使う場合はAPTCAのところをちゃんとクリアしてからじゃないとダメですね。
ここまで書いてて気がついたんだけど、MVC3ってどうやってるのか見るといいのかな。BuildManagerだと違ったりするのかな。うぬ~。なぞは深まるばかり。