0%

利用特性将数据映射到字段

以下是利用 C# 特性(Attributes) 将外部数据(如 JSON、数据库记录)动态映射到对象字段的完整实现方法,结合反射和自定义特性实现自动化映射


实现步骤

  1. 定义自定义特性:创建特性类标记字段与数据源的映射关系。
  2. 标记目标字段:在类中使用自定义特性标注需要映射的字段。
  3. 编写映射逻辑:通过反射解析特性并动态赋值。

完整代码

1. 定义映射特性

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;

// 自定义特性:标记字段对应的数据源键名
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class MapToAttribute : Attribute
{
public string Key { get; }

public MapToAttribute(string key)
{
Key = key;
}
}

2. 标记目标类字段

1
2
3
4
5
6
7
8
9
10
11
public class User
{
[MapTo("user_name")] // 映射数据源中的 "user_name" 键
public string Name;

[MapTo("age")]
private int Age;

[MapTo("registration_date")]
public DateTime RegisterDate { get; set; }
}

3. 数据映射工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
using System.Collections.Generic;
using System.Reflection;

public static class DataMapper
{
// 将字典数据映射到对象
public static T MapToObject<T>(Dictionary<string, object> data) where T : new()
{
T obj = new T();
Type type = typeof(T);

foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
var attribute = field.GetCustomAttribute<MapToAttribute>();
if (attribute != null && data.TryGetValue(attribute.Key, out object value))
{
// 类型转换处理
object convertedValue = Convert.ChangeType(value, field.FieldType);
field.SetValue(obj, convertedValue);
}
}

// 处理属性(可选)
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var attribute = prop.GetCustomAttribute<MapToAttribute>();
if (attribute != null && data.TryGetValue(attribute.Key, out object value))
{
prop.SetValue(obj, Convert.ChangeType(value, prop.PropertyType));
}
}

return obj;
}
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 模拟从数据库/JSON获取的数据
var data = new Dictionary<string, object>
{
{ "user_name", "Alice" },
{ "age", 30 },
{ "registration_date", "2023-10-01" }
};

// 自动映射数据到对象
User user = DataMapper.MapToObject<User>(data);

Console.WriteLine(user.Name); // 输出: Alice
Console.WriteLine(user.RegisterDate); // 输出: 2023/10/01

高级优化

1. 支持嵌套对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Address
{
[MapTo("city")]
public string City { get; set; }
}

public class User
{
[MapTo("address_info")] // 假设数据源中 address_info 是嵌套字典
public Address Address;
}

// 修改映射逻辑,递归处理嵌套类型
if (field.FieldType.IsClass && field.FieldType != typeof(string))
{
object nestedObj = MapToObject(field.FieldType, data[attribute.Key] as Dictionary<string, object>);
field.SetValue(obj, nestedObj);
}

2. 类型转换增强

1
2
3
4
5
6
7
8
9
10
11
// 添加自定义类型转换器
public static object SafeConvert(object value, Type targetType)
{
if (targetType == typeof(DateTime))
{
return DateTime.Parse(value.ToString());
}
return Convert.ChangeType(value, targetType);
}

// 替换代码中的 Convert.ChangeType 为 SafeConvert

3. 缓存反射信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用 MemoryCache 缓存 FieldInfo 和特性
private static readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

public static T MapToObjectOptimized<T>(Dictionary<string, object> data) where T : new()
{
Type type = typeof(T);
var fields = _cache.GetOrCreate(type.FullName, entry =>
{
return type.GetFields()
.Select(f => new
{
Field = f,
Attribute = f.GetCustomAttribute<MapToAttribute>()
})
.Where(x => x.Attribute != null)
.ToList();
});

// ... 使用缓存的 fields 进行赋值
}

关键注意事项

  1. 性能优化

    • 反射操作较慢,建议缓存 FieldInfo 和特性信息
    • 对于高频调用场景,可使用表达式树或 Source Generators 预编译映射逻辑
  2. 错误处理

    • 处理 Convert.ChangeType 的类型转换异常
    • 检查数据源键是否存在
  3. 特性扩展

    • 可增加更多属性控制映射行为(如是否必填、默认值)
    1
    2
    3
    4
    5
    6
    public class MapToAttribute : Attribute
    {
    public string Key { get; }
    public bool Required { get; set; }
    public object DefaultValue { get; set; }
    }
  4. 兼容性

    • 同时支持字段 (Field) 和属性 (Property)
    • 处理私有字段需指定 BindingFlags.NonPublic

替代方案对比

方法 优点 缺点
反射+特性 灵活,无需预编译代码 性能较低
表达式树 性能接近原生代码 实现复杂度高
Source Generators 零运行时开销,高性能 需要学习 Roslyn API
第三方库 功能完善(如 AutoMapper) 引入外部依赖

通过自定义特性结合反射,可以实现灵活的数据到对象的映射逻辑,适用于配置文件解析、ORM 框架、API 响应反序列化等场景。对于性能敏感项目,建议使用缓存或编译时生成方案。