using System.Text.Json.Serialization;
using System.Xml.Serialization;
using Dpz.Core.Entity.Base.MapperConfig;
using Dpz.Core.Public.Entity.Seo;
using Mapster;
namespace Dpz.Core.Public.ViewModel;
/// <summary>
/// Represents the VmPageMetadata type.
/// </summary>
public class VmPageMetadata : IHaveCustomMapping
{
/// <summary>
/// Gets or sets Id.
/// </summary>
public string? Id { get; set; }
/// <summary>
/// 标准化关联路径(小写)
/// </summary>
public string? RelationPath { get; set; }
/// <summary>
/// 是否公共元数据。
/// </summary>
public bool IsPublicMetadata { get; set; }
/// <summary>
/// 公共元数据是否应用到没有显式配置的页面。
/// </summary>
public bool ApplyToUnconfiguredPages { get; set; }
/// <summary>
/// 页面元数据继承方式。
/// </summary>
public PageMetadataInheritanceMode InheritanceMode { get; set; } =
PageMetadataInheritanceMode.Inherit;
/// <summary>
/// 结构化路由关联。
/// </summary>
public PageMetadataRoute? Route { get; set; }
/// <summary>
/// 标准化路由键。
/// </summary>
public string? RouteKey { get; set; }
/// <summary>
/// 非必要不要设置
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 关键字
/// </summary>
public List<string> Keywords { get; set; } = [];
/// <summary>
/// 描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 其它元数据
/// </summary>
[XmlIgnore]
public Dictionary<string, string> Metas { get; set; } = [];
/// <summary>
/// Gets or sets SerializableMetas.
/// </summary>
[XmlArray(ElementName = "Metas")]
[XmlArrayItem(ElementName = "Meta")]
[JsonIgnore]
public KeyValueElement[] SerializableMetas =>
Metas.Select(x => new KeyValueElement(x.Key, x.Value)).ToArray();
/// <summary>
/// 关联,唯一 {controller,action,id...}。仅用于兼容旧数据。
/// </summary>
public List<string> Relations { get; set; } = [];
/// <summary>
/// Executes CreateMappings.
/// </summary>
public static void CreateMappings(TypeAdapterConfig cfg)
{
cfg.NewConfig<PageMetadata, VmPageMetadata>().MapWith(x => ToViewModel(x));
cfg.NewConfig<VmPageMetadata, PageMetadata>().MapWith(x => ToEntity(x));
}
private static VmPageMetadata ToViewModel(PageMetadata source)
{
return new VmPageMetadata
{
Id = source.Id.ToString(),
RelationPath = source.RelationPath,
IsPublicMetadata = source.IsPublicMetadata,
ApplyToUnconfiguredPages = source.ApplyToUnconfiguredPages,
InheritanceMode = source.InheritanceMode,
Route = source.Route,
RouteKey = source.RouteKey,
Title = source.Title,
Keywords = source.Keywords,
Description = source.Description,
Metas = source.Metas,
Relations = source.Relations,
};
}
private static PageMetadata ToEntity(VmPageMetadata source)
{
return new PageMetadata
{
IsPublicMetadata = source.IsPublicMetadata,
ApplyToUnconfiguredPages = source.ApplyToUnconfiguredPages,
InheritanceMode = source.InheritanceMode,
Route = source.Route,
RouteKey = source.RouteKey,
Title = source.Title,
Keywords = source.Keywords,
Description = source.Description,
Metas = source.Metas,
Relations = source.Relations,
};
}
}
文件概览
VmPageMetadata.cs 是 SEO「页面元数据」体系的对外视图模型(ViewModel / DTO),位于 Dpz.Core.Public.ViewModel 命名空间。它把持久化实体 PageMetadata.cs 的数据以适合 API 传输、视图渲染和 XML 输出的形式暴露出来,同时承担实体与 DTO 之间的双向映射定义。
该类实现了 IHaveCustomMapping(定义见 IHaveCustomMapping.cs),该接口只有一个 static abstract void CreateMappings(TypeAdapterConfig cfg),属于 Mapster 的“约定式映射注册”入口。
成员职责
1. 数据字段(与实体一一对应)
以下字段与实体 PageMetadata 同名同义,用于在管理端/渲染端传递 SEO 配置:
Id:这里声明为string?,而实体继承的BaseEntity使用的是对象 Id 类型(ToViewModel里以source.Id.ToString()转换,ToEntity时不回填)。RelationPath:标准化关联路径(小写)。注意实体里的RelationPath是[BsonIgnore]的只读计算属性(由RouteKey或Relations推导),而 ViewModel 中是可读写的普通属性,映射时由ToViewModel显式复制。IsPublicMetadata、ApplyToUnconfiguredPages、InheritanceMode(默认Inherit,枚举措见 PageMetadataInheritanceMode.cs):控制是否作为“公共元数据”、是否应用到未显式配置的页面,以及继承策略。Route(PageMetadataRoute.cs)与RouteKey:结构化路由关联及其标准化键(由PageMetadataRoute.BuildRouteKey()生成)。Title、Keywords、Description、Metas:面向 SEO 的标题、关键字、描述与自定义 meta 键值集合。Relations:{controller,action,id...}形式的旧数据关联列表,注释明确说明仅用于兼容旧数据。
2. 序列化适配
Metas上标注[XmlIgnore],因为Dictionary<string,string>不能直接被 XML 序列化。SerializableMetas是一个只读投影属性:用 LINQ 把Metas转成KeyValueElement[](KeyValueElement.cs),并标注[XmlArray(ElementName = "Metas")]/[XmlArrayItem(ElementName = "Meta")]以适配 XML 输出,同时[JsonIgnore]避免在 JSON 中重复输出。说明:在可检索到的源码中,仅在该文件内部出现了
SerializableMetas的定义,未发现其他调用/引用点,因此其消费方(例如 XML 序列化端点)我无法从现有检索结果确认。
3. 映射定义(静态方法)
CreateMappings:向TypeAdapterConfig注册PageMetadata → VmPageMetadata和VmPageMetadata → PageMetadata两个方向的映射,都使用MapWith委托而非自动成员匹配。ToViewModel:显式逐字段投影,做出两处“不直接拷贝”的处理——Id转字符串、RelationPath取自实体的计算属性。ToEntity:反向投影,不写入Id、RelationPath、RouteKey;RouteKey交由服务层规范化流程生成(见下文协作)。
与相关文件的协作
- 服务层 PageMetadataService.cs 以
VmPageMetadata作为查询/保存的出入参类型:GetPageMetadataAsync、GetPublicMetadataAsync、GetByIdAsync、GetPageAsync、GetPageMetadataByRouteAsync、ResolvePageMetadataAsync等方法通过mapper.Map<VmPageMetadata>(entity)产出该 DTO;SavePageMetadataAsync/NormalizePageMetadata/Merge则在 DTO 上做校验、规范化(计算RouteKey、清空公共元数据的 Route/Relations 等)与多来源合并。 - 管理接口 SeoController.cs 直接用
VmPageMetadata组装保存请求、声明返回值类型,并转发给IPageMetadataService。 - MVC 渲染链路:控制器(如 ArticleController.cs、CodeController.cs、AlbumController.cs、SteamController.cs)构造
VmPageMetadata作为“显式元数据”放入ViewData["PageMetadata"],由 GlobalFilter.cs 调用ResolvePageMetadataAs...解析合并,最终渲染到 _PageMetadataPartial.cshtml。 - 预览 DTO SeoPreviewDto.cs 以
VmPageMetadata?作为可选的ExplicitMetadata字段类型。
映射与调用关系示意:
flowchart LR
E["PageMetadata 实体<br/>(Entity/Seo)"]
VM["VmPageMetadata<br/>(本文件)"]
SVC["PageMetadataService"]
WEB["SeoController / WebApi"]
MVC["MVC 控制器 + GlobalFilter<br/>_PageMetadataPartial.cshtml"]
E -- "MapWith: ToViewModel" --> VM
VM -- "MapWith: ToEntity" --> E
SVC -- "mapper.Map 返回/接收" --> VM
WEB -- "请求/响应模型" --> VM
MVC -- "ViewData 显式元数据" --> VM
SVC -- "ResolvePageMetadataAsync 合并" --> VM
小结
VmPageMetadata 是 SEO 页面元数据在应用层与外部(Web API、MVC 视图、XML 输出)之间的统一数据契约:字段布局镜像 PageMetadata 实体,Id/RelationPath 等做了传输友好的差异处理;通过 IHaveCustomMapping 静态注册双向显式映射,避免依赖自动约定;并通过 SerializableMetas 桥接不适合 XML 序列化的字典结构。其真正的业务语义(继承/合并、路由匹配、公共元数据规则、缓存失效)都在 PageMetadataService.cs 中实现,本文件更多是“形状与映射”的定义者。
需要提醒的点:
ToEntity不还原Id与RouteKey,这意味着从 DTO 反向生成实体后,路由键依赖服务层NormalizePageMetadata补齐;SerializableMetas目前未在可检索范围内发现调用方,若后续要用于 XML 输出需确认对应序列化入口。