C# 表达式树实现匿名类型到列表返回模型的自动映射

前言:

在我们的业务中,展示列表时经常会联表查询,比如说我们有学生表和班级表,表结构如下:包含了学生表、班级表以及列表返回模型.

/// <summary>/// 学生表/// </summary>public class StudentInfo{    /// <summary>    /// 标识    /// </summary>    public Guid Id { get; set; }     /// <summary>    /// 学号    /// </summary>    public string Number { get; set; }     /// <summary>    /// 姓名    /// </summary>    public string Name { get; set; }     /// <summary>    /// 班级标识    /// </summary>    public Guid ClassId { get; set; }} /// <summary>/// 班级表/// </summary>public class ClassInfo{    /// <summary>    /// 标识    /// </summary>    public Guid Id { get; set; }      /// <summary>    /// 班级总人数    /// </summary>    public int TotalNumber { get; set; }} /// <summary>/// 列表返回模型/// </summary>public class ClassStudentModel{    /// <summary>    /// 学生标识    /// </summary>    public Guid Id { get; set; }     /// <summary>    /// 学号    /// </summary>    public string Number { get; set; }     /// <summary>    /// 学生名称    /// </summary>    public string Name { get; set; }     /// <summary>    /// 班级总人数    /// </summary>    public int TotalNumber { get; set; }}

普通的查询语句,例子中的返回模型字段并不多,所以写4个字段也不费劲,但如果是十几二十个那就挺麻烦的了,如下:

var query = from s in dbStore.Set<StudentInfo>()                   from c in dbStore.Set<ClassInfo>().Where(c => c.Id == s.ClassId).DefaultIfEmpty()                   select new { s, c }; // 一般一些业务中应该会有更多的查询条件,这里就省略了        // 比如:query = query.Where(e => e.s.Name == options.Name);        var result = query.Select(e => new ClassStudentModel        {            Id = e.s.Id,            Number = e.s.Number,            Name = e.s.Name,            TotalNumber = e.c.TotalNumber        }).ToList();

所以我想有没有什么方法能够让这个匿名类型中的字段自动映射到列表的返回模型中呢?

正文:

使用表达式树+反射可以实现此需求,通过反射将各模型中的字段名与列表返回模型中的各字段进行对应,再利用表达式树进行拼接构造函数。有了这个思路之后呢,还有个问题,就是可能各模型中有很多相同的字段都可以和返回模型中的字段对应,比如说学生表里有Id,班级表中也有Id,那么到底是哪个Id和返回模型中的Id呢,为此,我又加了一个特性自动映射ParentAnonymousAttribute,通过此特性来判别取哪个模型中的Id,自动映射方法和特性代码如下:

/// <summary>/// 父级匿名特性/// </summary>public class ParentAnonymousAttribute : Attribute{    /// <summary>    /// 构造函数    /// </summary>    public ParentAnonymousAttribute(string parentName)    {        ParentName = parentName;    }     /// <summary>    ///    /// </summary>    /// <param name="parentName"></param>    /// <param name="name"></param>    public ParentAnonymousAttribute(string parentName, string name)    {        ParentName = parentName;        Name = name;    }     /// <summary>    /// 父级匿名名称    /// </summary>    public string ParentName { get; set; }     /// <summary>    /// 属性名称    /// </summary>    public string Name { get; set; }}
/// <summary>/// 自动select/// </summary>/// <typeparam name="T"></typeparam>/// <typeparam name="TGraph"></typeparam>/// <param name="query"></param>/// <param name="graph"></param>/// <returns></returns>public static IQueryable<TGraph> AutoSelect<T, TGraph>(this IQueryable<T> query, TGraph graph){    var defaultCtor = typeof(TGraph).GetConstructor(Type.EmptyTypes);    if (defaultCtor == null)    {        throw new Exception();    }    var constructor = Expression.New(defaultCtor);    var bindings = new List<MemberAssignment>();    var tExp = Expression.Parameter(typeof(T), "t");    var typeT = typeof(T);    var tProperties = typeT.GetProperties();    var graphProperties = typeof(TGraph).GetProperties().Where(e => e.SetMethod != null).ToList();    var graphPointParentProps = graphProperties.Where(e => e.GetCustomAttribute<ParentAnonymousAttribute>() != null).ToList();    var graphNoPointParentProps = graphProperties.Where(e => e.GetCustomAttribute<ParentAnonymousAttribute>() == null).ToList();    foreach (var item in graphPointParentProps)    {        var attr = item.GetCustomAttribute<ParentAnonymousAttribute>();        var pExp = Expression.Property(tExp, attr.ParentName);        var realExp = Expression.Property(pExp, attr.Name ?? item.Name);        bindings.Add(Expression.Bind(item, realExp));    }    foreach (var prop in tProperties)    {        var anoyClass = prop.PropertyType.GetProperties();        foreach (var item in anoyClass)        {            if (graphNoPointParentProps.Any(a => a.Name == item.Name))            {                var property = graphNoPointParentProps.Where(a => a.Name == item.Name).FirstOrDefault();                if (property.PropertyType != item.PropertyType)                {                    continue;                }                if (bindings.Any(e => e.Member.Name == property.Name))                {                    continue;                }                var pExp = Expression.Property(tExp, prop.Name);                var realExp = Expression.Property(pExp, item.Name);                bindings.Add(Expression.Bind(property, realExp));            }        }    }     var init = Expression.MemberInit(constructor, bindings);    var final = Expression.Lambda<Func<T, TGraph>>(init, tExp);    return query.Select(final);}

先给列表返回模型的字段加上特性:

/// <summary>/// 列表返回模型/// </summary>public class ClassStudentModel{    /// <summary>    /// 学生标识    /// </summary>    [ParentAnonymous("s")]    public Guid Id { get; set; }     /// <summary>    /// 学号    /// </summary>    public string Number { get; set; }     /// <summary>    /// 学生名称    /// </summary>    public string Name { get; set; }     /// <summary>    /// 班级总人数    /// </summary>    public int TotalNumber { get; set; }}

然后再改一下查询方法:

var query = from s in dbStore.Set<StudentInfo>()            from c in dbStore.Set<ClassInfo>().Where(c => c.Id == s.ClassId).DefaultIfEmpty()            select new { s, c }; var result = query.AutoSelect(new ClassStudentModel()).ToList();

至此,自动映射,大功告成