C#:使用泛型与模板类型为任意类添加标识符属性
CRUD API中DTO重复问题的解决方案
问题背景
开发包含大量CRUD功能的API时,新增与编辑操作的DTO仅差一个标识记录的Id字段,随着代码库扩大,手动维护重复DTO的成本越来越高。尝试过继承基类但仍需额外声明类,希望通过类似JavaScript展开运算符的效果,用泛型给任意类添加Id属性且保持属性同层级(而非嵌套)。
新增地址DTO示例:
public class AddressDto { public string AddressLine1 { get; set; } public string AddressLine2 { get; set; } public string City { get; set; } public string County { get; set; } public string Postcode { get; set; } //... }
编辑地址DTO示例:
public class ExistingAddressDto { public Guid Id { get; set; } public string AddressLine1 { get; set; } public string AddressLine2 { get; set; } public string City { get; set; } public string County { get; set; } public string Postcode { get; set; } //... }
当前泛型尝试(不符合需求):
// 声明 public class Existing<T> where T : class { public Guid Id { get; set; } public T Value { get; set; } // 不希望嵌套这个属性 } // 使用 var existingAddress = new Existing<AddressDto>();
可行解决方案
1. T4模板自动生成DTO
用T4模板可以基于现有DTO自动生成带Id的版本,完全避免手动重复编写代码。
创建一个.tt文件,示例模板代码如下:
<#@ template language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Reflection" #> <#@ output extension=".cs" #> <# // 扫描程序集中所有后缀为Dto且不是Existing开头的类 var dtoTypes = typeof(AddressDto).Assembly.GetTypes() .Where(t => t.Name.EndsWith("Dto") && !t.Name.StartsWith("Existing")); foreach(var type in dtoTypes) { #> namespace YourNamespace // 替换为你的命名空间 { public class Existing<#= type.Name #> { public Guid Id { get; set; } <# // 遍历原DTO的所有公共属性 foreach(var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { #> public <#= prop.PropertyType.FullName #> <#= prop.Name #> { get; set; } <# } #> } } <# } #>
添加该模板后,每次修改原DTO时,右键模板选择"运行自定义工具"即可自动更新带Id的DTO类,属性与原DTO完全同层级。
2. 自定义C#源生成器(C# 9+)
源生成器是编译时执行的工具,能实时生成代码,比T4更灵活,体验更好:
- 创建一个源生成器类库项目,实现
ISourceGenerator接口 - 定义一个特性(如
[GenerateExistingDto]),标记需要生成带Id版本的DTO - 在生成器逻辑中扫描标记特性的类,自动生成包含原属性+
Id的ExistingXXXDto类
这种方式无需手动触发生成,编译时自动同步,适合大型项目使用。
3. 继承简化方案(折中)
如果不想引入代码生成工具,可以采用继承方式,仅需给编辑DTO添加Id属性,无需重复编写其他属性:
// 新增DTO(基类) public class AddressDto { public string AddressLine1 { get; set; } public string AddressLine2 { get; set; } public string City { get; set; } public string County { get; set; } public string Postcode { get; set; } } // 编辑DTO(仅声明继承和Id) public class ExistingAddressDto : AddressDto { public Guid Id { get; set; } }
虽然仍需额外声明类,但代码量极少,维护成本低,适合小型项目或不想引入额外工具的场景。
为什么泛型嵌套不可行?
C#没有JavaScript的对象展开语法,泛型类无法将泛型参数T的属性直接提升到当前类层级,因此Existing<T>只能通过嵌套的Value属性访问原DTO内容,无法满足属性同层级的需求。
内容的提问来源于stack exchange,提问作者marcuthh
相关产品推荐
相关产品推荐

