65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2022 年 2 月 5 日

CPOL

2分钟阅读

viewsIcon

5692

downloadIcon

72

对代码进行了一些调整,包括能够为映射的属性名称添加别名

引言

这是 CPian DiponRoy 的技巧文章 C# 简单模型/实体映射器 的第三次修改版本,该文章发表于 2014 年 9 月 2 日。CPian ThiagoTane 于 2015 年 3 月 12 日发布了修订版。当然,如果您想要包括用于优化的生成的 IL 代码的完整功能,GitHub 上有 AutoMapper

我对 Code Project 上的前两篇文章进行修订的目的是因为我想做一些有用的更改,并且使用 AutoMapper 这样的东西有点过头了。 虽然我欣赏 IL 生成,但我真的不想在 MapperConfiguration 对象中使用 CreateMap 注册我的映射。 我只是想在需要时映射两个对象,带有一些小功能,不需要复杂的配置。 我生活中的一个主题似乎是 KISS 原则,这就是为什么我经常自己动手的原因!

代码更改

我对代码做了三个改动

首先,映射方法 CreateMappedthis 对象确定源类型,所以不需要写

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 强制转换为 stringAlias = (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 日:初始版本
© . All rights reserved.