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

  • 14
  • 0

最近在測試 .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 時代去做一些騷操作..

 

---

The bug existed in all possible states.
Until I ran the code.