linq扩展——别再手写select new了,用Expression扩展IQueryable能让你少写很多代码

引言

在很多项目中,把数据库中的数据展示给客户端都会经历这样的过程:entity[-model]-viewModel。写select new实在耗时费力, 引入AutoMapper组件能让我们少写很多代码进而提高编码效率(节省出划水的时间[dog])。
使用AutoMapper后的操作是,把entity查询出来后再mapmodel。如果某个表列非常多,而客户端需要查询的仅仅是其中少部分列,我们希望的是仅查询需要的列,而不是全部。因为查询那些不必须的列会造成额外的数据库性能开销和额外的网络传输。

扩展IQueryable

  • IQueryableExtensions.cs
namespace System.Linq
{
	public static class IQueryableExtensions
    {
        public static IQueryable<TDestination> Select<TSource, TDestination>(this IQueryable<TSource> sources)
        {
            return sources.Select(CreateMemberMappingExpression<TSource, TDestination>());
        }
    }
}
  • ExpressionBuilder.cs
namespace System.Linq.Expressions
{
	public static class ExpressionBuilder
	{
		public static Expression<Func<TSource, TDestination>> BuildSelectExpression<TSource, TDestination>()
			where TDestination : new()
		{
			var sType = typeof(TSource);
			var dType = typeof(TDestination);
			var sProps = sType.GetProperties();
			var dMembers = dType.GetMembers();

			ParameterExpression right = BuildEntityParamExpression<TSource>();

			List<MemberBinding> bindings = new List<MemberBinding>();
			foreach (var dProp in dType.GetProperties())
			{
				if (!dProp.CanWrite) continue;

				/// [1] this part used AutoMapper
				//var memberFroms = dProp.GetCustomAttributes<SourceMemberAttribute>()?.Select(attr => attr.Name).Distinct();
				//var sProp = sProps.FirstOrDefault(p => p.CanRead && (memberFroms?.Contains(p.Name) == true || p.Name == dProp.Name));

				var sProp = sProps.FirstOrDefault(p => p.CanRead && p.Name == dProp.Name);
				if (sProp == null) { continue; }
				
				/// [2] TODO: Expression.Convert, convert type when destination property has a different type with source property
				MemberExpression memberExpression = Expression.PropertyOrField(right, sProp.Name);

				var member = Array.Find(dMembers, m => m.Name == dProp.Name);
				if (member == null) { continue; }

				bindings.Add(Expression.Bind(member, memberExpression));
			}

			Expression body = Expression.MemberInit(Expression.New(dType), bindings);

			Expression<Func<TSource, TDestination>> selector = (Expression<Func<TSource, TDestination>>)Expression.Lambda(body, right);
			return selector;
		}
	}
}
  • 上述代码省略了using部分
  • 注释[1]:这里用到了AutoMapperSourceMemberAttribute,用以兼容其成员映射规则。考虑到并不是所有人都会用AutoMapper,我把它注释掉了
  • 注释[2]:使用Expression.Bind绑定源属性和目标属性时,它严格要求二者的类型是相同的,否则会报错。如果你的目标属性的类型和源属性的类型并不总是相同,你需要做好类型转换

效果

我的Entity

[Table("City")]
public class City
{
	[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
	public int Id { get; set; }
	
	[Required]
	public int ProvinceId { get; set; }
	
	[StringLength(10), Column(TypeName = "varchar(10)")]
	public string Code { get; set; }
	
	[StringLength(64), Column(TypeName = "nvarchar(128)")]
	public string Name { get; set; }
}

我的ViewModel

public class CityViewModel
{
	public string Code { get; set; }
	
	[MemberFrom(nameof(City.Name))]
	public string CityName { get; set; }
}

原谅我吧,为了更好的测试效果,我使用了上文提到的SourceMemberAttribute。如果你真的不想用,就删除CityName属性的标签,并把它改为Name

如果之前你需要这样写:

var cities = _dbContext.Cities.Select(city=> new CityViewModel() {
	Code = city.Code,
	CityName = city.Name
}).ToList();

那么现在,愉快的使用它吧:

var cities = _dbContext.Cities.Select<City, CityViewModel>().ToList();

EFCore自动生成的脚本如下图(并没有查询IdProvinceId)
image

posted @ 2021-09-10 16:59  Theo·Chan  阅读(223)  评论(0编辑  收藏  举报