必须用expression捕获字段路径并配合自定义属性固化来源,通过datalineage显式传递上下文,序列化时用自定义converter透传溯源信息,跨服务需在传输头中嵌入关键路径字符串。

用 Expression + 自定义属性标记字段来源
直接硬编码字符串追踪字段名会崩,改个变量名血缘就断了。必须让编译器帮你看住字段定义点。Expression 是唯一能在运行时保留原始变量路径的方式,配合自定义属性(如 [DataOrigin])打标,才能把「这个值最初来自哪个类的哪个属性」固化下来。
常见错误是只记录最终值,不记录表达式树里的 MemberExpression 路径。结果日志里只看到 "value=123",完全看不出它曾从 Order.Customer.Name 经过两次映射而来。
- 在数据入口处(比如 DTO 构造或 API 参数绑定后),用
Expression<func>></func>捕获字段引用:var expr = (Expression<Func<string>>)(() => order.Customer.Name);
- 解析
expr.Body,逐级提取MemberExpression,拼出"Order.Customer.Name"这样的路径字符串 - 把这个路径和当前上下文(文件名、行号、转换函数名)一起存入一个轻量级元数据容器,比如
DataLineage类 - 每次调用
MapTo<t>()</t>或Select()时,把上游的DataLineage实例传下去,而不是重新生成
JSON 序列化时保留字段溯源信息
很多 C# 项目用 System.Text.Json 做中间格式传输,但默认序列化会抹掉所有元数据。不干预的话,血缘信息在第一次序列化后就彻底丢失。
关键不是“加个字段存溯源”,而是让序列化器知道:这个字段的值本身要带来源描述,且不能污染业务逻辑。
- 不要给业务模型加
LineageInfo属性——违反单一职责,还影响 Swagger 和数据库映射 - 改用
JsonSerializerOptions.Converters注册自定义转换器,针对特定类型(如string或包装类TrackedValue<t></t>)注入溯源字段 - 示例中,
TrackedValue<string></string>包含Value和SourcePath,但序列化时只输出{"value":"xxx","source":"User.FirstName"},不暴露内部结构 - 注意
JsonIgnore不能用于动态控制,必须用转换器,否则反序列化时无法还原路径上下文
StackTrace 不可靠,改用显式上下文传递
有人想靠 new StackTrace() 解析调用栈来推断字段流转路径,这在 Release 模式、IL 合并、异步方法下基本失效。JIT 内联、async 状态机都会让栈帧错位或消失。
真正能落地的只有「每个转换函数都明确接收并返回血缘对象」。这不是加负担,是把隐式依赖显性化。
- 避免写
Transform(Order o) => new OrderDto { Name = o.Name }这种无上下文函数 - 改为
Transform(Order o, DataLineage lineage) => new OrderDto { Name = Track(o.Name, lineage.WithStep("MapToDto")) } -
Track()是一个轻量包装函数,返回TrackedValue<t></t>,内部保存路径+时间戳+调用方标识 - 所有文件读取、配置解析、数据库查询入口,都必须初始化第一个
DataLineage实例,比如DataLineage.FromFile("orders.json", line: 42)
跨进程/微服务时血缘链断裂怎么办
HTTP 请求、消息队列、gRPC 调用天然隔离内存上下文。你本地构造的 DataLineage 对象到下一个服务就变成 null。必须把关键路径信息塞进传输载体本身。
不是靠全局 ID 或 traceId,而是把字段级溯源作为 payload 的一部分透传,哪怕只传最简路径字符串。
- REST API:用请求头
X-Data-Source: User.Email传递源头字段路径(长度有限,只传最关键的那一个) - Kafka 消息:在
Headers中添加"lineage-source"键,值为"UserProfile.Email → NotificationRequest.Recipient" - gRPC:在
Metadata中携带,服务端用context.RequestHeaders.Get("lineage-source")读取并合并进本地血缘链 - 禁止依赖分布式追踪系统(如 OpenTelemetry)自动关联字段——它只管 span,不管字段怎么变的
最难的不是技术实现,是说服团队接受「每个数据转换函数必须多传一个参数」。没人愿意改旧代码,但只要入口和出口守住了,中间链路就能逐步补全。别指望一劳永逸,血缘是活的,得跟着代码一起长。










