.NET 10

.NET 10 如何動態載入 cshtml?使用 RazorEngineCore 實現免重新編譯

作者

本文介紹如何在 .NET 10 中利用 RazorEngineCore 實作動態模板渲染,將 .cshtml 檔案排除在編譯流程之外並在執行期讀取,達成類似傳統 ASP.NET Web Forms (ASPX) 僅需修改檔案即可更新介面的開發體驗。

 .NET 10 如何動態載入 cshtml?使用 RazorEngineCore 實現免重新編譯

最近在測試 .NET 10 製作網站時,遇到一個很現實的問題
以前使用 ASP.NET Web Forms,只要建立一個 product.aspx?id=1, 我就可以去更改 .aspx 裡面的內容或是樣式
設計師修改版面時,只要替換 .aspx,完全不用重新編譯整個網站
但是到了 ASP.NET Core Razor Pages,.cshtml 預設會在發布時編譯進 DLL

這邊有點麻煩,尤其是現在 agent  coding ,有些我已經處理好了,但是因為設計師可能要改些東西
就需要重新 Build , Publish , Deploy 

這對大量客製化網站來說有點麻煩,今天測試標很簡單,使用 .NET 10 ,保留 SSR (Server-Side Rendering )
資料在執行其取得,cshtml 可以在執行期的時候修改一些葉面上的資料,不用發佈 DLL

1. 先安裝 RazorEngineCore

dotnet add package RazorEngineCore

2. 建立商品模型

namespace TestWebApp.Models;

public sealed class Product
{
    public int Id { get; init; }

    public string Name { get; init; } = string.Empty;

    public decimal Price { get; init; }

    public string Description { get; init; } = string.Empty;

    public string ImageUrl { get; init; } = string.Empty;

    public bool InStock { get; init; }
}

測試資料先放在靜態 DataSet 中

using TestWebApp.Models;

namespace TestWebApp.Data;

public static class DataSet
{
    public static IReadOnlyDictionary<int, Product> Products { get; } =
        new Dictionary<int, Product>
        {
            [1] = new Product
            {
                Id = 1,
                Name = "65W USB-C 快充充電器",
                Price = 990,
                Description = "支援手機、平板及筆電快速充電。",
                ImageUrl = "https://placehold.co/800x600?text=Product+1",
                InStock = true
            },
            [2] = new Product
            {
                Id = 2,
                Name = "藍牙無線喇叭",
                Price = 1680,
                Description = "小型輕量設計,支援藍牙連線。",
                ImageUrl = "https://placehold.co/800x600?text=Product+2",
                InStock = false
            },
            [3] = new Product
            {
                Id = 3,
                Name = "多功能無線充電座",
                Price = 1280,
                Description = "可同時替手機、耳機及智慧手錶充電。",
                ImageUrl = "https://placehold.co/800x600?text=Product+3",
                InStock = true
            }
        };

    public static Product? GetProduct(int id)
    {
        return Products.TryGetValue(id, out var product)
            ? product
            : null;
    }
}

3. 建立動態商品頁面

頁面放在 Pages/Product.cshtml

code:

@inherits RazorEngineCore.RazorEngineTemplateBase<TestWebApp.Models.Product>

@{
    var title = Model.Name + "|測試商城";
}

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="utf-8" />
    <meta name="viewport"
          content="width=device-width, initial-scale=1" />

    <title>@title</title>

    <meta name="description"
          content="@Model.Description" />

    <style>
        body {
            margin: 0;
            font-family: Arial, sans-serif;
            background: #f4f4f4;
        }

        .product {
            width: min(900px, calc(100% - 40px));
            margin: 60px auto;
            padding: 30px;
            background: white;
            border-radius: 16px;
        }

        .product img {
            width: 100%;
            max-width: 420px;
        }

        .price {
            margin: 20px 0;
            font-size: 28px;
            font-weight: bold;
        }
    </style>
</head>

<body>
    <main class="product">
        <h1>@Model.Name</h1>

        <img src="@Model.ImageUrl"
             alt="@Model.Name" />

        <p>@Model.Description</p>

        <div class="price">
            NT$ @Model.Price.ToString("N0")
        </div>

        @if (Model.InStock)
        {
            <button type="button">
                加入購物車
            </button>
        }
        else
        {
            <strong>目前缺貨</strong>
        }

        <hr />

        <small>
            商品編號:@Model.Id
        </small>
    </main>
</body>
</html>

這個.cshtml 不是真正傳統意義上的 Razor Page , 它只是放在 Pages 目錄中的動態模板,所以不能加入
@page@model

4. 動態載入頁面,主要要修改 Program.cs 裡面要加入這一段, 放在 app.Run(); 前,建立商品路由

app.MapGet(
    "/product",
    async (
        int? id,
        IWebHostEnvironment environment,
        CancellationToken cancellationToken) =>
    {
        if (id is null)
        {
            return Results.BadRequest("缺少商品 id。");
        }

        var product = DataSet.GetProduct(id.Value);

        if (product is null)
        {
            return Results.NotFound("找不到商品。");
        }

        var templatePath = Path.Combine(
            environment.ContentRootPath,
            "Pages",
            "Product.cshtml");

        if (!File.Exists(templatePath))
        {
            return Results.Problem(
                title: "找不到商品模板",
                detail: templatePath);
        }

        var templateContent =
            await File.ReadAllTextAsync(
                templatePath,
                cancellationToken);

        try
        {
            IRazorEngine razorEngine = new RazorEngine();

            var compiledTemplate =
                razorEngine.Compile<RazorEngineTemplateBase<Product>>(
                    templateContent);

            var html = compiledTemplate.Run(
                template =>
                {
                    template.Model = product;
                });

            return Results.Content(
                html,
                "text/html; charset=utf-8");
        }
        catch (Exception exception)
        {
            return Results.Problem(
                title: "Razor 模板編譯或執行失敗",
                detail: exception.ToString(),
                statusCode: StatusCodes.Status500InternalServerError);
        }
    });

之後啟動網站開啟 /product?id=1 就會輸出完整 HTML

5. 做到現在你會發現,你邊議會不過,因為那cshtml 不是一個正常的 Razor Page ,所以得在 .csproj 排除 Razor SDK 編譯
並且 Release 的狀況也要設定,這邊留一段 sample code

<ItemGroup>
    <Content Remove="Pages\Product.cshtml" />
    <RazorGenerate Remove="Pages\Product.cshtml" />

    <None Include="Pages\Product.cshtml">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <CopyToPublishDirectory>Always</CopyToPublishDirectory>
    </None>
</ItemGroup>

之後發佈你會看到

publish/
├─ Pages/
│  └─ Product.cshtml
├─ TestWebApp.dll
└─ web.config

之後你去修改, Product.cshtml 就不用重新編譯

RazorEngineCore 不是完整的 Razor Pages,也沒有 PageModel、Tag Helper 或 _Layout.cshtml 這些功能
但是如果你是需要製作一個核心資料功能,但是你希望前端是透過其他人(包含設計師、AI Agent)
會去改變風格跟布局,但是核心資料部會去更動到,我覺得這是一個比較方便的做法
可以做到像是以前 ASPX 時代去做一些騷操作..