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を見てみてください。
衝撃!分かります?普通さ~、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だと違ったりするのかな。うぬ~。なぞは深まるばかり。