znlgis 博客

GIS开发与技术分享

第十八章:模板引擎与视图

一、模板引擎概述

1.1 什么是模板引擎

模板引擎是一种将数据与模板结合生成最终输出的技术。在 Web 开发中,模板引擎广泛应用于以下场景:

  • HTML 页面渲染:服务器端生成动态网页
  • 邮件内容生成:基于模板生成格式化邮件
  • 文档生成:PDF、Word、Excel 等文档的自动生成
  • 代码生成:根据模板自动生成代码文件
  • 短信/通知内容:动态生成通知消息

1.2 常见模板引擎对比

模板引擎 语言/平台 特点 适用场景
Razor .NET 原生支持,功能强大 MVC 视图、Blazor
Scriban .NET 轻量、安全、高性能 邮件模板、代码生成
Liquid .NET 安全沙箱、用户可编辑 CMS、用户自定义模板
T4 .NET 设计时代码生成 代码生成器
RazorLight .NET 脱离 MVC 使用 Razor 独立模板渲染

1.3 Furion 中的模板引擎支持

Furion 框架提供了开箱即用的模板引擎支持,主要包括:

// Furion 模板引擎核心功能
// 1. 视图引擎 - 基于 Razor 的增强视图
// 2. 字符串模板渲染 - 动态编译和渲染字符串模板
// 3. 模板文件管理 - 统一的模板文件管理方案

二、Razor 视图引擎基础

2.1 Razor 语法基础

Razor 是 ASP.NET Core 的默认视图引擎,使用 .cshtml 文件扩展名。

// Program.cs 中启用 Razor 视图
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(); // MVC + 视图支持
// 或
builder.Services.AddRazorPages(); // Razor Pages 支持

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

2.2 Razor 语法详解

@* Views/Home/Index.cshtml *@
@model HomeViewModel

@{
    ViewData["Title"] = "首页";
    Layout = "_Layout";
}

<!-- 变量输出 -->
<h1>欢迎, @Model.UserName!</h1>

<!-- 条件判断 -->
@if (Model.IsAdmin)
{
    <div class="alert alert-info">
        您是管理员,拥有所有权限。
    </div>
}
else
{
    <div class="alert alert-warning">
        普通用户,部分功能受限。
    </div>
}

<!-- 循环 -->
<table class="table">
    <thead>
        <tr>
            <th>序号</th>
            <th>名称</th>
            <th>创建时间</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Items)
        {
            <tr>
                <td>@item.Id</td>
                <td>@item.Name</td>
                <td>@item.CreatedTime.ToString("yyyy-MM-dd HH:mm")</td>
            </tr>
        }
    </tbody>
</table>

<!-- 表单 -->
<form asp-action="Create" asp-controller="Home" method="post">
    <div class="form-group">
        <label asp-for="UserName">用户名</label>
        <input asp-for="UserName" class="form-control" />
        <span asp-validation-for="UserName" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">提交</button>
</form>

<!-- 原始 HTML 输出 -->
@Html.Raw(Model.HtmlContent)

<!-- 分部视图 -->
@await Html.PartialAsync("_UserCard", Model.User)

<!-- Section 定义 -->
@section Scripts {
    <script src="~/js/home.js"></script>
}

2.3 布局页与分部视图

@* Views/Shared/_Layout.cshtml - 布局页 *@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - 我的应用</title>
    <link rel="stylesheet" href="~/css/site.css" />
    @RenderSection("Styles", required: false)
</head>
<body>
    <header>
        @await Html.PartialAsync("_Navigation")
    </header>

    <main class="container">
        @RenderBody()
    </main>

    <footer>
        <p>&copy; @DateTime.Now.Year 我的应用</p>
    </footer>

    <script src="~/js/site.js"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>
@* Views/Shared/_UserCard.cshtml - 分部视图 *@
@model UserDto

<div class="card">
    <div class="card-body">
        <h5 class="card-title">@Model.Name</h5>
        <p class="card-text">
            <span>邮箱: @Model.Email</span><br />
            <span>部门: @Model.Department</span>
        </p>
    </div>
</div>

三、Furion 视图引擎增强

3.1 动态编译模板

Furion 支持在运行时动态编译和渲染 Razor 模板字符串:

using Furion.ViewEngine;

/// <summary>
/// 模板渲染服务
/// </summary>
public class TemplateService
{
    private readonly IViewEngine _viewEngine;

    public TemplateService(IViewEngine viewEngine)
    {
        _viewEngine = viewEngine;
    }

    /// <summary>
    /// 渲染字符串模板
    /// </summary>
    public async Task<string> RenderStringTemplateAsync()
    {
        // 简单字符串模板
        var template = "你好, @Model.Name! 今天是 @DateTime.Now.ToString(\"yyyy年MM月dd日\")";

        var result = await _viewEngine.RunCompileAsync(template, new
        {
            Name = "张三"
        });

        return result;
        // 输出: 你好, 张三! 今天是 2024年01月15日
    }

    /// <summary>
    /// 渲染复杂模板
    /// </summary>
    public async Task<string> RenderComplexTemplateAsync()
    {
        var template = @"
<h1>@Model.Title</h1>
<table>
    <tr><th>序号</th><th>名称</th><th>价格</th></tr>
    @foreach (var item in Model.Items)
    {
        <tr>
            <td>@item.Id</td>
            <td>@item.Name</td>
            <td>@item.Price.ToString(""C"")</td>
        </tr>
    }
</table>
<p>总计: @Model.Items.Sum(i => i.Price).ToString(""C"")</p>";

        var result = await _viewEngine.RunCompileAsync(template, new
        {
            Title = "商品列表",
            Items = new[]
            {
                new { Id = 1, Name = "商品A", Price = 99.9m },
                new { Id = 2, Name = "商品B", Price = 199.9m },
                new { Id = 3, Name = "商品C", Price = 299.9m }
            }
        });

        return result;
    }
}

3.2 模板文件渲染

/// <summary>
/// 从文件加载模板并渲染
/// </summary>
public class FileTemplateService
{
    private readonly IViewEngine _viewEngine;

    public FileTemplateService(IViewEngine viewEngine)
    {
        _viewEngine = viewEngine;
    }

    /// <summary>
    /// 渲染模板文件
    /// </summary>
    public async Task<string> RenderFromFileAsync(string templateName, object model)
    {
        // 从 Templates 目录加载模板
        var templatePath = Path.Combine(
            AppContext.BaseDirectory, "Templates", $"{templateName}.cshtml");

        if (!File.Exists(templatePath))
        {
            throw new FileNotFoundException($"模板文件 {templateName} 不存在");
        }

        var templateContent = await File.ReadAllTextAsync(templatePath);
        return await _viewEngine.RunCompileAsync(templateContent, model);
    }

    /// <summary>
    /// 带缓存的模板渲染
    /// </summary>
    public async Task<string> RenderWithCacheAsync(
        string templateName, object model)
    {
        var templatePath = Path.Combine(
            AppContext.BaseDirectory, "Templates", $"{templateName}.cshtml");
        var templateContent = await File.ReadAllTextAsync(templatePath);

        // 使用模板缓存键,避免重复编译
        return await _viewEngine.RunCompileFromCachedAsync(
            templateContent, model, cacheFileName: templateName);
    }
}

四、邮件模板生成

4.1 HTML 邮件模板

@* Templates/Email/Welcome.cshtml *@
@model WelcomeEmailModel

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        body { font-family: 'Microsoft YaHei', Arial, sans-serif; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .header { background-color: #4CAF50; color: white; padding: 20px;
                   text-align: center; border-radius: 5px 5px 0 0; }
        .content { padding: 20px; background-color: #f9f9f9; }
        .footer { text-align: center; padding: 10px;
                   color: #888; font-size: 12px; }
        .button { background-color: #4CAF50; color: white; padding: 10px 20px;
                   text-decoration: none; border-radius: 5px; display: inline-block; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>欢迎加入 @Model.AppName</h1>
        </div>
        <div class="content">
            <p>亲爱的 @Model.UserName:</p>
            <p>感谢您注册 @Model.AppName,您的账号已创建成功。</p>
            <p>账号信息:</p>
            <ul>
                <li>用户名:@Model.UserName</li>
                <li>邮箱:@Model.Email</li>
                <li>注册时间:@Model.RegisterTime.ToString("yyyy-MM-dd HH:mm:ss")</li>
            </ul>
            <p>请点击以下链接激活您的账号:</p>
            <p style="text-align: center;">
                <a href="@Model.ActivationUrl" class="button">激活账号</a>
            </p>
            <p>此链接将在 @Model.ExpireHours 小时后失效。</p>
        </div>
        <div class="footer">
            <p>此邮件由系统自动发送,请勿回复。</p>
            <p>&copy; @DateTime.Now.Year @Model.AppName</p>
        </div>
    </div>
</body>
</html>

4.2 邮件发送服务

/// <summary>
/// 邮件模板服务
/// </summary>
public class EmailTemplateService
{
    private readonly IViewEngine _viewEngine;
    private readonly IEmailSender _emailSender;
    private readonly ILogger<EmailTemplateService> _logger;

    public EmailTemplateService(
        IViewEngine viewEngine,
        IEmailSender emailSender,
        ILogger<EmailTemplateService> logger)
    {
        _viewEngine = viewEngine;
        _emailSender = emailSender;
        _logger = logger;
    }

    /// <summary>
    /// 发送欢迎邮件
    /// </summary>
    public async Task SendWelcomeEmailAsync(User user, string activationToken)
    {
        var model = new WelcomeEmailModel
        {
            UserName = user.UserName,
            Email = user.Email,
            AppName = "我的应用",
            RegisterTime = DateTime.Now,
            ActivationUrl = $"https://example.com/activate?token={activationToken}",
            ExpireHours = 24
        };

        var templatePath = Path.Combine(
            AppContext.BaseDirectory, "Templates", "Email", "Welcome.cshtml");
        var template = await File.ReadAllTextAsync(templatePath);
        var htmlBody = await _viewEngine.RunCompileAsync(template, model);

        await _emailSender.SendEmailAsync(
            user.Email,
            $"欢迎加入{model.AppName}",
            htmlBody,
            isHtml: true);

        _logger.LogInformation("欢迎邮件已发送至 {Email}", user.Email);
    }

    /// <summary>
    /// 发送密码重置邮件
    /// </summary>
    public async Task SendPasswordResetEmailAsync(User user, string resetToken)
    {
        var template = @"
<!DOCTYPE html>
<html>
<body style='font-family: Microsoft YaHei, Arial;'>
    <div style='max-width: 600px; margin: 0 auto;'>
        <h2>密码重置</h2>
        <p>亲爱的 @Model.UserName:</p>
        <p>您正在重置密码,验证码为:</p>
        <h1 style='color: #e74c3c; text-align: center;
            letter-spacing: 5px;'>@Model.Code</h1>
        <p>验证码有效期为 @Model.ExpireMinutes 分钟,请勿泄露给他人。</p>
        <p style='color: #888; font-size: 12px;'>
            如果不是您本人的操作,请忽略此邮件。</p>
    </div>
</body>
</html>";

        var htmlBody = await _viewEngine.RunCompileAsync(template, new
        {
            UserName = user.UserName,
            Code = resetToken,
            ExpireMinutes = 15
        });

        await _emailSender.SendEmailAsync(
            user.Email,
            "密码重置验证码",
            htmlBody,
            isHtml: true);
    }
}

/// <summary>
/// 欢迎邮件模型
/// </summary>
public class WelcomeEmailModel
{
    public string UserName { get; set; }
    public string Email { get; set; }
    public string AppName { get; set; }
    public DateTime RegisterTime { get; set; }
    public string ActivationUrl { get; set; }
    public int ExpireHours { get; set; }
}

五、短信模板

5.1 短信模板服务

/// <summary>
/// 短信模板服务
/// </summary>
public class SmsTemplateService
{
    private readonly IViewEngine _viewEngine;

    /// <summary>
    /// 短信模板定义
    /// </summary>
    private static readonly Dictionary<string, string> Templates = new()
    {
        ["verification"] = "【@Model.AppName】您的验证码为 @Model.Code," +
            "@Model.ExpireMinutes 分钟内有效,请勿泄露。",
        ["order_shipped"] = "【@Model.AppName】您的订单 @Model.OrderNo 已发货," +
            "快递公司:@Model.ExpressCompany,单号:@Model.TrackingNo。",
        ["payment_success"] = "【@Model.AppName】您已成功支付 @Model.Amount 元," +
            "订单号:@Model.OrderNo,感谢您的购买。",
        ["appointment"] = "【@Model.AppName】您有一个预约提醒:@Model.DateTime," +
            "地点:@Model.Location,请准时参加。"
    };

    public SmsTemplateService(IViewEngine viewEngine)
    {
        _viewEngine = viewEngine;
    }

    /// <summary>
    /// 生成短信内容
    /// </summary>
    public async Task<string> GenerateSmsContentAsync(
        string templateName, object model)
    {
        if (!Templates.TryGetValue(templateName, out var template))
        {
            throw new ArgumentException($"短信模板 {templateName} 不存在");
        }

        return await _viewEngine.RunCompileAsync(template, model);
    }

    /// <summary>
    /// 发送验证码短信
    /// </summary>
    public async Task<string> GenerateVerificationSmsAsync(
        string appName, string code, int expireMinutes = 5)
    {
        return await GenerateSmsContentAsync("verification", new
        {
            AppName = appName,
            Code = code,
            ExpireMinutes = expireMinutes
        });
    }
}

六、PDF 文档生成

6.1 基于模板的 PDF 生成

/// <summary>
/// PDF 文档生成服务(基于 DinkToPdf 或 iTextSharp)
/// </summary>
public class PdfGeneratorService
{
    private readonly IViewEngine _viewEngine;

    public PdfGeneratorService(IViewEngine viewEngine)
    {
        _viewEngine = viewEngine;
    }

    /// <summary>
    /// 生成发票 PDF
    /// </summary>
    public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice)
    {
        // 1. 使用模板引擎生成 HTML
        var template = await File.ReadAllTextAsync(
            Path.Combine(AppContext.BaseDirectory,
                "Templates", "Pdf", "Invoice.cshtml"));

        var html = await _viewEngine.RunCompileAsync(template, invoice);

        // 2. 使用 HTML 转 PDF 工具生成 PDF
        var converter = new SynchronizedConverter(new PdfTools());
        var doc = new HtmlToPdfDocument
        {
            GlobalSettings = new GlobalSettings
            {
                ColorMode = ColorMode.Color,
                Orientation = Orientation.Portrait,
                PaperSize = PaperKind.A4,
                Margins = new MarginSettings { Top = 10, Bottom = 10 }
            },
            Objects =
            {
                new ObjectSettings
                {
                    HtmlContent = html,
                    WebSettings =
                    {
                        DefaultEncoding = "utf-8"
                    }
                }
            }
        };

        return converter.Convert(doc);
    }
}

/// <summary>
/// 发票模型
/// </summary>
public class InvoiceModel
{
    public string InvoiceNo { get; set; }
    public DateTime InvoiceDate { get; set; }
    public string CompanyName { get; set; }
    public string CustomerName { get; set; }
    public string CustomerAddress { get; set; }
    public List<InvoiceItem> Items { get; set; }
    public decimal TotalAmount => Items?.Sum(i => i.Total) ?? 0;
    public decimal TaxRate { get; set; } = 0.13m;
    public decimal TaxAmount => TotalAmount * TaxRate;
    public decimal GrandTotal => TotalAmount + TaxAmount;
}

public class InvoiceItem
{
    public string Description { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Total => Quantity * UnitPrice;
}

6.2 发票 HTML 模板

@* Templates/Pdf/Invoice.cshtml *@
@model InvoiceModel

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        body { font-family: 'SimSun', serif; font-size: 12px; }
        .invoice-header { text-align: center; border-bottom: 2px solid #333;
                          padding-bottom: 10px; }
        table { width: 100%; border-collapse: collapse; margin: 15px 0; }
        th, td { border: 1px solid #333; padding: 8px; text-align: left; }
        th { background-color: #f0f0f0; }
        .text-right { text-align: right; }
        .total-row { font-weight: bold; background-color: #ffd; }
    </style>
</head>
<body>
    <div class="invoice-header">
        <h1>发 票</h1>
        <p>发票号码:@Model.InvoiceNo</p>
    </div>

    <p>开票日期:@Model.InvoiceDate.ToString("yyyy年MM月dd日")</p>
    <p>销售方:@Model.CompanyName</p>
    <p>购买方:@Model.CustomerName</p>
    <p>地址:@Model.CustomerAddress</p>

    <table>
        <thead>
            <tr>
                <th>序号</th>
                <th>项目描述</th>
                <th>数量</th>
                <th>单价(元)</th>
                <th>金额(元)</th>
            </tr>
        </thead>
        <tbody>
            @for (int i = 0; i < Model.Items.Count; i++)
            {
                var item = Model.Items[i];
                <tr>
                    <td>@(i + 1)</td>
                    <td>@item.Description</td>
                    <td>@item.Quantity</td>
                    <td class="text-right">@item.UnitPrice.ToString("N2")</td>
                    <td class="text-right">@item.Total.ToString("N2")</td>
                </tr>
            }
            <tr class="total-row">
                <td colspan="4" class="text-right">小计</td>
                <td class="text-right">@Model.TotalAmount.ToString("N2")</td>
            </tr>
            <tr>
                <td colspan="4" class="text-right">
                    税额(@((Model.TaxRate * 100).ToString("N0"))%)
                </td>
                <td class="text-right">@Model.TaxAmount.ToString("N2")</td>
            </tr>
            <tr class="total-row">
                <td colspan="4" class="text-right">合计</td>
                <td class="text-right">@Model.GrandTotal.ToString("N2")</td>
            </tr>
        </tbody>
    </table>
</body>
</html>

七、报表模板

7.1 数据报表生成

/// <summary>
/// 报表生成服务
/// </summary>
public class ReportService
{
    private readonly IViewEngine _viewEngine;

    public ReportService(IViewEngine viewEngine)
    {
        _viewEngine = viewEngine;
    }

    /// <summary>
    /// 生成销售月报
    /// </summary>
    public async Task<string> GenerateMonthlySalesReportAsync(
        int year, int month, List<SalesData> salesData)
    {
        var template = @"
<html>
<head>
    <style>
        body { font-family: 'Microsoft YaHei'; }
        table { width: 100%; border-collapse: collapse; }
        th, td { border: 1px solid #ddd; padding: 8px; }
        th { background-color: #4CAF50; color: white; }
        tr:nth-child(even) { background-color: #f2f2f2; }
        .summary { margin: 20px 0; padding: 15px;
                    background-color: #e8f5e9; border-radius: 5px; }
    </style>
</head>
<body>
    <h1>@Model.Year 年 @Model.Month 月 销售报表</h1>
    <div class='summary'>
        <p>总销售额:<strong>@Model.TotalAmount.ToString(""C"")</strong></p>
        <p>订单数量:<strong>@Model.OrderCount</strong></p>
        <p>平均客单价:<strong>@Model.AverageOrderAmount.ToString(""C"")</strong></p>
    </div>

    <h2>销售明细</h2>
    <table>
        <tr>
            <th>日期</th>
            <th>商品</th>
            <th>数量</th>
            <th>金额</th>
        </tr>
        @foreach (var item in Model.Items)
        {
            <tr>
                <td>@item.Date.ToString(""MM-dd"")</td>
                <td>@item.ProductName</td>
                <td>@item.Quantity</td>
                <td style='text-align:right'>@item.Amount.ToString(""N2"")</td>
            </tr>
        }
    </table>
    <p style='color: #888;'>报表生成时间:@DateTime.Now.ToString(""yyyy-MM-dd HH:mm:ss"")</p>
</body>
</html>";

        var model = new
        {
            Year = year,
            Month = month,
            Items = salesData,
            TotalAmount = salesData.Sum(s => s.Amount),
            OrderCount = salesData.Count,
            AverageOrderAmount = salesData.Count > 0
                ? salesData.Average(s => s.Amount) : 0m
        };

        return await _viewEngine.RunCompileAsync(template, model);
    }
}

public class SalesData
{
    public DateTime Date { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal Amount { get; set; }
}

八、代码生成器

8.1 基于 Scriban 的代码生成

// 安装 Scriban 包
// dotnet add package Scriban

using Scriban;

/// <summary>
/// 代码生成器服务
/// </summary>
public class CodeGeneratorService
{
    /// <summary>
    /// 生成实体类代码
    /// </summary>
    public string GenerateEntityCode(EntityDefinition definition)
    {
        var template = Template.Parse(@"
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace .Entities
{
    /// <summary>
    /// 
    /// </summary>
    [Table("""")]
    public class 
    {
        
        /// <summary>
        /// 
        /// </summary>
        
        [Key]
        
        
        [MaxLength()]
        
        
        [Required]
        
        public   { get; set; }

        
    }
}");

        return template.Render(new
        {
            Namespace = definition.Namespace,
            Description = definition.Description,
            TableName = definition.TableName,
            ClassName = definition.ClassName,
            Properties = definition.Properties
        });
    }

    /// <summary>
    /// 生成服务层代码
    /// </summary>
    public string GenerateServiceCode(EntityDefinition definition)
    {
        var template = Template.Parse(@"
using Furion.DynamicApiController;
using Microsoft.AspNetCore.Mvc;

namespace .Services
{
    /// <summary>
    /// 服务
    /// </summary>
    [ApiDescriptionSettings(Name = """")]
    public class AppService : IDynamicApiController
    {
        private readonly IRepository<> _repository;

        public AppService(IRepository<> repository)
        {
            _repository = repository;
        }

        /// <summary>
        /// 获取列表
        /// </summary>
        public async Task<List<>> GetListAsync()
        {
            return await _repository.AsQueryable().ToListAsync();
        }

        /// <summary>
        /// 获取详情
        /// </summary>
        public async Task<> GetAsync(int id)
        {
            return await _repository.FindOrDefaultAsync(id);
        }

        /// <summary>
        /// 新增
        /// </summary>
        public async Task<> PostAsync( entity)
        {
            var result = await _repository.InsertAsync(entity);
            return result.Entity;
        }

        /// <summary>
        /// 更新
        /// </summary>
        public async Task PutAsync( entity)
        {
            await _repository.UpdateAsync(entity);
        }

        /// <summary>
        /// 删除
        /// </summary>
        public async Task DeleteAsync(int id)
        {
            await _repository.DeleteByIdAsync(id);
        }
    }
}");

        return template.Render(new
        {
            Namespace = definition.Namespace,
            Description = definition.Description,
            ClassName = definition.ClassName
        });
    }
}

/// <summary>
/// 实体定义模型
/// </summary>
public class EntityDefinition
{
    public string Namespace { get; set; }
    public string ClassName { get; set; }
    public string TableName { get; set; }
    public string Description { get; set; }
    public List<PropertyDefinition> Properties { get; set; }
}

public class PropertyDefinition
{
    public string Name { get; set; }
    public string Type { get; set; }
    public string Description { get; set; }
    public bool IsKey { get; set; }
    public bool IsRequired { get; set; }
    public int MaxLength { get; set; }
}

8.2 使用代码生成器

// 使用代码生成器的示例
var generator = new CodeGeneratorService();

var definition = new EntityDefinition
{
    Namespace = "MyApp",
    ClassName = "Product",
    TableName = "Products",
    Description = "商品",
    Properties = new List<PropertyDefinition>
    {
        new() { Name = "Id", Type = "int", Description = "主键",
            IsKey = true },
        new() { Name = "Name", Type = "string", Description = "商品名称",
            IsRequired = true, MaxLength = 200 },
        new() { Name = "Price", Type = "decimal", Description = "价格",
            IsRequired = true },
        new() { Name = "Description", Type = "string", Description = "描述",
            MaxLength = 2000 },
        new() { Name = "Stock", Type = "int", Description = "库存数量" },
        new() { Name = "CreatedTime", Type = "DateTime", Description = "创建时间" }
    }
};

var entityCode = generator.GenerateEntityCode(definition);
var serviceCode = generator.GenerateServiceCode(definition);

// 将生成的代码写入文件
await File.WriteAllTextAsync($"Entities/{definition.ClassName}.cs", entityCode);
await File.WriteAllTextAsync($"Services/{definition.ClassName}AppService.cs", serviceCode);

九、视图组件(ViewComponent)

9.1 创建视图组件

/// <summary>
/// 通知列表视图组件
/// </summary>
public class NotificationListViewComponent : ViewComponent
{
    private readonly INotificationService _notificationService;

    public NotificationListViewComponent(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public async Task<IViewComponentResult> InvokeAsync(int count = 5)
    {
        var userId = HttpContext.User.FindFirst("UserId")?.Value;
        var notifications = await _notificationService
            .GetLatestNotificationsAsync(userId, count);

        return View(notifications);
    }
}
@* Views/Shared/Components/NotificationList/Default.cshtml *@
@model List<NotificationDto>

<div class="notification-panel">
    <h4>最新通知 <span class="badge">@Model.Count</span></h4>
    @if (Model.Any())
    {
        <ul class="list-group">
            @foreach (var notification in Model)
            {
                <li class="list-group-item @(notification.IsRead ? "" : "unread")">
                    <strong>@notification.Title</strong>
                    <small class="text-muted">@notification.CreatedTime.ToString("MM-dd HH:mm")</small>
                    <p>@notification.Content</p>
                </li>
            }
        </ul>
    }
    else
    {
        <p class="text-muted">暂无通知</p>
    }
</div>

@* 在页面中使用视图组件 *@
@await Component.InvokeAsync("NotificationList", new { count = 10 })

十、前后端分离架构下的模板使用

10.1 模板在前后端分离中的应用场景

在前后端分离架构下,模板引擎主要用于后端生成非视图内容:

应用场景 说明 输出格式
邮件生成 注册确认、密码重置、营销邮件 HTML
报表导出 生成可下载的报表文件 HTML/PDF
通知消息 站内通知、推送消息模板 纯文本/HTML
代码生成 脚手架工具、低代码平台 源代码
文档生成 合同、协议、证书 PDF/Word

10.2 API 返回渲染后的 HTML

/// <summary>
/// 模板渲染 API
/// </summary>
[ApiDescriptionSettings(Name = "Template")]
public class TemplateAppService : IDynamicApiController
{
    private readonly IViewEngine _viewEngine;

    public TemplateAppService(IViewEngine viewEngine)
    {
        _viewEngine = viewEngine;
    }

    /// <summary>
    /// 预览邮件模板
    /// </summary>
    [NonUnify]
    public async Task<IActionResult> PreviewEmailTemplate(string templateName)
    {
        var sampleData = GetSampleData(templateName);
        var templatePath = Path.Combine(
            AppContext.BaseDirectory, "Templates", "Email",
            $"{templateName}.cshtml");
        var template = await File.ReadAllTextAsync(templatePath);
        var html = await _viewEngine.RunCompileAsync(template, sampleData);

        return new ContentResult
        {
            Content = html,
            ContentType = "text/html",
            StatusCode = 200
        };
    }

    private object GetSampleData(string templateName)
    {
        return templateName switch
        {
            "Welcome" => new WelcomeEmailModel
            {
                UserName = "测试用户",
                Email = "test@example.com",
                AppName = "示例应用",
                RegisterTime = DateTime.Now,
                ActivationUrl = "https://example.com/activate?token=sample",
                ExpireHours = 24
            },
            _ => new { }
        };
    }
}

十一、Blazor 集成概述

11.1 Blazor 基本配置

// Program.cs - Blazor Server 配置
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

// 注册 Furion 服务
builder.Services.AddControllers()
    .AddDynamicApiControllers();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.MapControllers();  // 同时支持 API 和 Blazor

app.Run();

11.2 Blazor 组件示例

@* Pages/UserList.razor *@
@page "/users"
@inject HttpClient Http

<h3>用户管理</h3>

@if (users == null)
{
    <p>加载中...</p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>用户名</th>
                <th>邮箱</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var user in users)
            {
                <tr>
                    <td>@user.Id</td>
                    <td>@user.UserName</td>
                    <td>@user.Email</td>
                    <td>
                        <button class="btn btn-sm btn-primary"
                            @onclick="() => EditUser(user.Id)">编辑</button>
                        <button class="btn btn-sm btn-danger"
                            @onclick="() => DeleteUser(user.Id)">删除</button>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private List<UserDto> users;

    protected override async Task OnInitializedAsync()
    {
        var result = await Http.GetFromJsonAsync<RESTfulResult<List<UserDto>>>(
            "/api/user/list");
        users = result?.Data;
    }

    private void EditUser(int id)
    {
        NavigationManager.NavigateTo($"/users/edit/{id}");
    }

    private async Task DeleteUser(int id)
    {
        await Http.DeleteAsync($"/api/user/{id}");
        users = users.Where(u => u.Id != id).ToList();
    }
}

十二、静态文件服务

12.1 静态文件配置

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 默认静态文件(wwwroot 目录)
app.UseStaticFiles();

// 自定义静态文件目录
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "uploads")),
    RequestPath = "/uploads",
    // 设置缓存策略
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers.Append(
            "Cache-Control", "public, max-age=604800");
    }
});

// 启用目录浏览(仅开发环境)
if (app.Environment.IsDevelopment())
{
    app.UseDirectoryBrowser(new DirectoryBrowserOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(builder.Environment.ContentRootPath, "uploads")),
        RequestPath = "/uploads"
    });
}

12.2 文件 MIME 类型配置

// 自定义 MIME 类型映射
var mimeProvider = new FileExtensionContentTypeProvider();
mimeProvider.Mappings[".dwg"] = "application/acad";
mimeProvider.Mappings[".shp"] = "application/x-shapefile";
mimeProvider.Mappings[".md"] = "text/markdown";

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = mimeProvider,
    ServeUnknownFileTypes = false  // 不提供未知类型的文件
});

十三、模板引擎最佳实践

13.1 最佳实践总结

实践项 建议 说明
模板分类管理 按用途分目录存放 Email/、Pdf/、Sms/、Code/
模板缓存 使用缓存编译结果 避免重复编译开销
模板安全 注意 XSS 防护 使用 @ 而非 @Html.Raw
模板预编译 生产环境预编译 提高首次渲染性能
模板测试 编写模板渲染单元测试 确保模板正确性
前后端分离 后端模板仅用于非视图场景 邮件、报表、文档

13.2 模板渲染单元测试

/// <summary>
/// 模板渲染单元测试
/// </summary>
public class TemplateServiceTests
{
    private readonly IViewEngine _viewEngine;

    public TemplateServiceTests()
    {
        var services = new ServiceCollection();
        services.AddViewEngine();
        var provider = services.BuildServiceProvider();
        _viewEngine = provider.GetRequiredService<IViewEngine>();
    }

    [Fact]
    public async Task RenderWelcomeEmail_ShouldContainUserName()
    {
        // Arrange
        var template = "你好, @Model.Name!";
        var model = new { Name = "张三" };

        // Act
        var result = await _viewEngine.RunCompileAsync(template, model);

        // Assert
        Assert.Contains("张三", result);
    }

    [Fact]
    public async Task RenderInvoice_ShouldCalculateTotal()
    {
        // Arrange
        var template = "总计: @Model.Items.Sum(i => i.Price).ToString(\"N2\")";
        var model = new
        {
            Items = new[]
            {
                new { Price = 100.0m },
                new { Price = 200.0m }
            }
        };

        // Act
        var result = await _viewEngine.RunCompileAsync(template, model);

        // Assert
        Assert.Contains("300.00", result);
    }
}

通过本章的学习,你已经掌握了 Furion 框架中模板引擎与视图的完整使用方法。无论是传统的 MVC 视图渲染,还是前后端分离架构下的邮件模板、报表生成、代码生成,模板引擎都是不可或缺的工具。在下一章中,我们将学习部署与 DevOps 相关内容。