RazorRendererを作成してASP.NETの.cshtmlファイルのHTML文字列を取得する方法

Higtyのシステムの作り方

背景

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());