RazorRendererを作成してASP.NETの.cshtmlファイルのHTML文字列を取得する方法
背景
ASP.NETのMinimal APIでは最小のコードでレスポンスを生成できます。これはユーザーの追加、タスクの変更などのデータ操作をするAPIを定義する際にとても短くコードを書けます。
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services.AddHttpContextAccessor();
services.AddScoped<RazorRenderer>();
var app = builder.Build();
app.UseRouting();
app.UseAuthorization();
app.MapGet("/todoitems", async (TodoDb db) => await db.Todos.ToListAsync());
Minimal APIは元々APIを最小のコードで記述するためにデザインされています。APIでデータ更新などをする場合はとても使い勝手が良いのですが、HTMLのビューを返す時にはHTMLの文字列を自分で構築する必要があります。ASP.NET MVC Controllerを使えばHTMLを返すことはできますが、Minimal APIでHTMLを返すコードを書きたい場合には少し手間がかかります。
.cshtmlのHTMLを簡単に取得できるクラスなどがあると良さそうです。
RazorRenderer
ということで.cshtmlファイルからHTMLの文字列を作成するためのクラスを作ってみました。
レポジトリ
https://github.com/higty/higlabo
ソースコード
https://github.com/higty/higlabo/blob/master/Net8/HigLabo.Web/Core/RazorRenderer.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
namespace HigLabo.Web;
public class RazorRenderer(IHttpContextAccessor contextAccessor, IRazorViewEngine viewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider)
{
private IHttpContextAccessor _contextAccessor = contextAccessor;
private IRazorViewEngine _viewEngine = viewEngine;
private ITempDataProvider _tempDataProvider = tempDataProvider;
private IServiceProvider _serviceProvider = serviceProvider;
public async ValueTask ToHtmlAsync(string viewName)
{
var d = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
return await ToHtmlAsync(viewName, d);
}
public async ValueTask ToHtmlAsync(string viewName, TModel model)
{
var d = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
d.Model = model;
return await ToHtmlAsync(viewName, d as ViewDataDictionary);
}
public async ValueTask ToHtmlAsync(string viewName, ViewDataDictionary viewData)
{
var context = _contextAccessor.HttpContext;
if (context == null) { throw new InvalidOperationException(); }
var actionContext = new ActionContext(context, context.GetRouteData(), new ActionDescriptor());
var tempData = new TempDataDictionary(context, _tempDataProvider);
using (var output = new StringWriter())
{
var partialView = FindView(actionContext, viewName);
var viewContext = new ViewContext(actionContext, partialView, viewData, tempData, output, new HtmlHelperOptions());
await partialView.RenderAsync(viewContext);
return output.ToString();
}
}
public async ValueTask WriteHtmlAsync(HttpContext context, string viewName)
{
await WriteHtmlAsync(context.Response, viewName);
}
public async ValueTask WriteHtmlAsync(HttpResponse response, string viewName)
{
var d = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var html = await ToHtmlAsync(viewName, d);
await response.WriteAsync(html);
}
public async ValueTask WriteHtmlAsync(HttpContext context, string viewName, TModel model)
{
await WriteHtmlAsync(context.Response, viewName, model);
}
public async ValueTask WriteHtmlAsync(HttpResponse response, string viewName, TModel model)
{
var html = await ToHtmlAsync(viewName, model);
await response.WriteAsync(html);
}
private IView FindView(ActionContext actionContext, string viewName)
{
var getPartialResult = _viewEngine.GetView(null, viewName, false);
if (getPartialResult.Success)
{
return getPartialResult.View;
}
var findPartialResult = _viewEngine.FindView(actionContext, viewName, false);
if (findPartialResult.Success)
{
return findPartialResult.View;
}
var searchedLocations = getPartialResult.SearchedLocations.Concat(findPartialResult.SearchedLocations);
var errorMessage = string.Join(Environment.NewLine, new[] { $"Unable to find partial '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));
throw new InvalidOperationException(errorMessage);
}
}
このクラスを使って.cshtmlファイルからHTMLを取得するには以下のように使います。
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services.AddHttpContextAccessor();
services.AddScoped<RazorRenderer>();
var app = builder.Build();
app.UseRouting();
app.UseAuthorization();
app.MapGet("/portal", (RazorRenderer renderer) => await renderer.WriteHtmlAsync("/Pages/Portal.cshtml"));
DIでRazorRendererをインジェクションしてWriteHtmlAsyncメソッドを呼び出すだけでHTMLが取得できます。
var html = await renderer.WriteHtmlAsync("/Pages/Portal.cshtml");
var html1 = await renderer.WriteHtmlAsync("/Pages/Portal.cshtml", new PortalModel());