Azure Functions で1ページを cshtml で出力したい
cshtml を1つだけホストしたいけど、そのために ASP.NET と PaaS は重いなと思い、Azure Functions でレンダリングするようにしました。
下記のような条件を満たす Azure Functions を作成します。
- Azure Functions で、1ページだけ HTML を出力したい
- 1ページだけだけど、クエリ文字列やフォーム送信のデータは、取得して C# で処理したい
- Razor(cshtml)で書きたい
今回作成したページの図です:
用途としては、アクセスが少ない適当なサンプルページを公開したいものとします。
別件ですが、cshtml ではなく静的な html をホストしたい場合は、こちら Serving an HTML Page from Azure Functions を参考にします。
サンプルコード
概要
Azure Functions で、HTTP Trigger の Function を作成します。プロジェクト配下には cshtml ファイルを配置し、それを読み取り、Razor コンパイルを行って、text/html
形式で HTTP レスポンスを返します。
Razor でコンパイルする方法ですが、RazorEngine では駄目そうだったので(参照:Error in Azure Function · Issue #496 · Antaris/RazorEngine)、Westwind.RazorHosting パッケージを使用します。
手順
- Visual Studio を使用して Azure で初めての関数を作成する | Microsoft Docs を参考し、C# プリコンパイル形式の Azure Function のプロジェクトを作成します。
- NuGet で、NuGet Gallery | Westwind.RazorHosting をインストールします。(現在は version 3.1.0)
- NuGet「Microsoft.NET.Sdk.Functions」を最新版にアップデートします。
- プロジェクト配下に、「templates」フォルダを作成します。
- 「templates」フォルダの下に、「Page.cshtml」ファイルを作成します。 「Page.cshtml」のプロパティ設定にて、「出力ディレクトリにコピー」の値を「新しい場合はコピーにする」に設定します。
- HTTP Trigger の関数「Function1.cs」を、アクセス権は Anonymous にして作成します(参考:関数を作成する)。
作成後は、このようなソリューション構成になります。
- 「Page.cshtml」と「Function1.cs」の中身を、次のように記述します。
Page.cshtml:
<!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Sample</title> </head> <body> <h1>Sample</h1> <div>NameInQuery:@Model.NameInQuery</div> <div>NameInBody:@Model.NameInBody</div> <form method="get"> name: <input type="text" value="" name="name" /> <input type="submit" value="GET" /> </form> <form method="post"> name: <input type="text" value="" name="name" /> <input type="submit" value="POST" /> </form> </body> </html>
Function1.cs:
using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Host; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Westwind.RazorHosting; namespace Miso.SampleRazorFunctions { public static class Function1 { private static string compiledId = ""; private static RazorEngine<RazorTemplateBase> host = new RazorEngine<RazorTemplateBase>(); [FunctionName(nameof(Function1))] public static async Task<HttpResponseMessage> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log, ExecutionContext context) { //.dllを追加する場合 //host.AddAssembly(Path.Combine(context.FunctionAppDirectory, $@"bin\System.ValueTuple.dll")); if (string.IsNullOrEmpty(compiledId)) { compiledId = await Helper.GetCompileId(host, context, "Page.cshtml"); } log.Info($"compileId:{compiledId}"); if (!string.IsNullOrEmpty(host.ErrorMessage)) { log.Error(host.ErrorMessage); return req.CreateResponse(HttpStatusCode.InternalServerError, host.ErrorMessage); } // parse query parameter string nameInQueryParameter = req.GetQueryNameValuePairs() .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0) .Value; // Get request body var formData = await req.Content.ReadAsFormDataAsync(); var nameInBody = formData == null ? "" : formData["name"]; var model = new Data() { NameInQuery = nameInQueryParameter, NameInBody = nameInBody, }; string result = host.RenderTemplateFromAssembly(compiledId, model); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(result, Encoding.UTF8, "text/html") }; } } public class Data { public string NameInQuery { get; set; } public string NameInBody { get; set; } } public class Helper { public static async Task<string> GetCompileId(RazorEngine<RazorTemplateBase> host, ExecutionContext context, string csHtmlFileName) { string template; string templatePath = Path.Combine(context.FunctionAppDirectory, $@"templates\{csHtmlFileName}"); using (var reader = File.OpenText(templatePath)) { template = await reader.ReadToEndAsync(); } return host.CompileTemplate(template); } } }
以上で、デバッグ実行し、URL にアクセスすると(例:http://localhost:7071/api/Function1)、レンダリングされた HTML を表示できます。
ノウハウなど
この方法では、通常の Visual Studio における ASP.NET 開発とは違い、.cshtml にエラーがある場合(Razor コンパイルでエラーが発生する場合)は、実行してみないと内容が分かりません。ですので、エラーの原因を調査するのに少々手間がかかります。もし、エラーが発生している場合は、host.CompileTemplate(template)
を実行した時点で、host
オブジェクト内にエラーメッセージが格納されます。上記のサンプルコードでは、エラーが発生した場合はその内容を HTTP レスポンスで返すようにしています。
.cshtml において、NuGet で取得してきた .dll などを参照している場合は、以下のようにアセンブリの追加を宣言します(上記のコードにも記述しています)。
host.AddAssembly(Path.Combine(context.FunctionAppDirectory, $@"bin\System.ValueTuple.dll"));`
同じように、namespace を追加する場合は、このように。
host.AddNamespace("Microsoft.Azure.WebJobs.Host")
必要な .dll が存在するかどうかを確認するときは、プロジェクトを Debug ビルドした後 \ソリューション名\bin\Debug\net462\bin
内を見てみます。
コンパイルの後(host.CompileTemplate(template);
実行後)に、host
変数内から、アセンブリ情報や namespace 情報を確認できます。その他 Razor コンパイルについては、パッケージの README.md を参照します。