DiponRoy 的 C# 简单模型/实体映射器第三次实现





5.00/5 (5投票s)
对代码进行了一些调整,包括能够为映射的属性名称添加别名
引言
这是 CPian DiponRoy 的技巧文章 C# 简单模型/实体映射器 的第三次修改版本,该文章发表于 2014 年 9 月 2 日。CPian ThiagoTane 于 2015 年 3 月 12 日发布了修订版。当然,如果您想要包括用于优化的生成的 IL 代码的完整功能,GitHub 上有 AutoMapper。
我对 Code Project 上的前两篇文章进行修订的目的是因为我想做一些有用的更改,并且使用 AutoMapper 这样的东西有点过头了。 虽然我欣赏 IL 生成,但我真的不想在 MapperConfiguration
对象中使用 CreateMap
注册我的映射。 我只是想在需要时映射两个对象,带有一些小功能,不需要复杂的配置。 我生活中的一个主题似乎是 KISS 原则,这就是为什么我经常自己动手的原因!
代码更改
我对代码做了三个改动
首先,映射方法 CreateMapped
从 this
对象确定源类型,所以不需要写
Student source = new Student() { Id = 1, Name = "Smith" };
StudentLog newMapped = source.CreateMapped<Student, StudentLog>();
可以写成
Student source = new Student() { Id = 1, Name = "Smith" };
StudentLog newMapped = source.CreateMapped<StudentLog>();
注意,删除了泛型参数 Student
。
其次,我添加了一个特性 MapperPropertyAttribute
,用于在目标属性名称不同时指定源属性。
例如,我有一个类 User
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string Salt { get; set; }
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public bool IsSysAdmin { get; set; }
public DateTime? LastLogin { get; set; }
public int? ExpiresIn { get; set; }
public long? ExpiresOn { get; set; }
public bool Deleted { get; set; }
}
但我希望登录响应返回具有不同名称的属性子集。MapperProperty
用于指定目标类中的属性名称转换
public class LoginResponse
{
[MapperProperty(Name = "AccessToken")]
public string access_token { get; set; }
[MapperProperty(Name = "RefreshToken")]
public string refresh_token { get; set; }
[MapperProperty(Name = "ExpiresIn")]
public int expires_in { get; set; }
[MapperProperty(Name = "ExpiresOn")]
public long expires_on { get; set; }
public string token_type { get; set; } = "Bearer";
}
一个示例用例代码片段是
var response = user.CreateMapped<LoginResponse>();
第三,我重命名了某些地方的变量名。
实现
该特性很简单
public class MapperPropertyAttribute : Attribute
{
public string Name { get; set; }
public MapperPropertyAttribute() { }
}
扩展方法已修改为提供两个 public
方法,它们共享一个公共的 private
实现。
public static class MapExtensionMethods
{
public static TTarget MapTo<TSource, TTarget>(this TSource source, TTarget target)
{
var ret = MapTo(source.GetType(), source, target);
return ret;
}
public static TTarget CreateMapped<TTarget>(this object source) where TTarget : new()
{
return MapTo(source.GetType(), source, new TTarget());
}
private static TTarget MapTo<TTarget>(Type tSource, object source, TTarget target)
{
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance |
BindingFlags.NonPublic;
var srcFields = (from PropertyInfo aProp in tSource.GetProperties(flags)
where aProp.CanRead //check if prop is readable
select new
{
Name = aProp.Name,
Alias = (string)null,
Type = Nullable.GetUnderlyingType(aProp.PropertyType) ?? aProp.PropertyType
}).ToList();
var trgFields = (from PropertyInfo aProp in target.GetType().GetProperties(flags)
where aProp.CanWrite //check if prop is writeable
select new
{
Name = aProp.Name,
Alias = aProp.GetCustomAttribute<MapperPropertyAttribute>()?.Name,
Type = Nullable.GetUnderlyingType(aProp.PropertyType) ?? aProp.PropertyType
}).ToList();
var commonFields = trgFields.In(srcFields, /* T1 */ t => t.Alias ??
t.Name, /* T2 */ t => t.Name).ToList();
foreach (var field in commonFields)
{
var value = tSource.GetProperty(field.Alias ?? field.Name).GetValue(source, null);
PropertyInfo propertyInfos = target.GetType().GetProperty(field.Name);
propertyInfos.SetValue(target, value, null);
}
return target;
}
}
"秘密武器"是在 select
语句返回的匿名对象中添加了 Alias
属性,以及使用 null 解决运算符 ??
来确定是使用别名还是源属性的属性名。 另一件有趣的事情是,由于这些是匿名属性,将 Alias
赋值为 null
需要将 null
强制转换为 string
:Alias = (string)null,
。 这并不常见。
什么是“In”扩展方法?
不幸的是,Linq 的 IntersectBy
仅在 .NET 6 中可用,所以我有一个自己的扩展方法,该方法修改自 CPian Mr.PoorInglish 在另一篇文章中发布的评论 的示例代码 。
// See Mr.PoorInglish's rework of my article here:
// https://codeproject.org.cn/Articles/5293576/A-Performant-Items-in-List-A-that-are-not-in-List?msg=5782421#xx5782421xx
public static IEnumerable<T1> In<T1, T2, TKey>(
this IEnumerable<T1> items1,
IEnumerable<T2> items2,
Func<T1, TKey> keySelector1, Func<T2, TKey> keySelector2)
{
var dict1 = items1.ToDictionary(keySelector1);
var k1s = dict1.Keys.Intersect(items2.Select(itm2 => keySelector2(itm2)));
var isIn = k1s.Select(k1 => dict1[k1]);
return isIn;
}
此外,.NET 6 对 IntersectedBy
的实现实际上并不是我想要的签名,而且我不想实现 iEqualityComparer
,所以我们将使用上面的扩展方法。
一个简单的测试程序
本文的下载包含一个示例程序,您可以运行该程序来演示此版本的映射器
public static void Main()
{
// We declare the epoch to be 1/1/1970.
var ts = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
var expiresSeconds = 24 * 60 * 60;
var user = new User()
{
Id = 1,
UserName = "fubar",
Password = "fizbin",
Salt = "pepper",
AccessToken = Guid.NewGuid().ToString(),
RefreshToken = Guid.NewGuid().ToString(),
ExpiresIn = expiresSeconds,
ExpiresOn = ts + expiresSeconds,
LastLogin = DateTime.Now,
};
var response = user.CreateMapped<LoginResponse>();
Console.WriteLine($"access_token: {response.access_token}");
Console.WriteLine($"refresh_token: {response.refresh_token}");
Console.WriteLine($"expires_in: {response.expires_in}");
Console.WriteLine($"expires_on: {response.expires_on}");
Console.WriteLine($"token_type: {response.token_type}");
}
输出
access_token: 86384067-9193-449a-a6ff-8023be5fe203 refresh_token: 12e04d46-882e-4a25-a777-d1440f4783cd expires_in: 86400 expires_on: 1644175047 token_type: Bearer
结论
这里没什么可总结的 - 这只是大约 8 年前编写的简短而有用的技巧的第三次实现!
历史
- 2022 年 2 月 5 日:初始版本