EF Core中Linq Select内复用映射逻辑且不加载全实体的实现方法
实现方案
核心思路是把单个属性的映射逻辑封装为Expression<Func<AddressObj, GenericComboBox>>表达式树,再通过表达式展开的方式注入到整体的Select投影逻辑中,EF Core可以正常解析生成SQL,不会触发客户端评估,也不会加载全量字段。
步骤1:封装复用的映射表达式
定义公共的静态类存储可复用的映射逻辑:
// 此处AddressObj替换为你的PrimaryOffice对应的实体类型 public static class CommonMappings { public static readonly Expression<Func<AddressObj, GenericComboBox>> ToGenericComboBox = address => address == null ? null : new GenericComboBox() { Value = address.Id, Text = address.LocationName + (address.LocationBusinessName == null ? "" : " (" + address.LocationBusinessName + ") ") + "(" + (address.AddressLine1 != null ? address.AddressLine1 + ", " : null) + (address.AddressLine2 != null ? address.AddressLine2 + ", " : null) + (address.City != null ? address.City + ", " : null) + (address.State != null ? address.State.Name != null ? address.State.Name + ", " : null : null) + (address.ZipCode ?? null) + ")" }; }
步骤2:在Select中复用映射
有两种常用实现方式,按需选择即可:
方案A:使用LINQKit(最简实现)
- 安装对应EF Core版本的NuGet包
LinqKit.Microsoft.EntityFrameworkCore - 查询时调用
AsExpandable()展开表达式,直接调用封装的表达式:
using LinqKit; IQueryable<ExampleViewModel> list = query.AsExpandable() .Select(x => new ExampleViewModel() { Id = x.Id, BindDate = x.BindDate, CancellationDate = x.CancellationDate, ChangeDate = x.ChangeDate, CreateDate = x.CreateDate, Description = x.Description, // 直接调用封装好的表达式即可 PrimaryOffice = CommonMappings.ToGenericComboBox.Invoke(x.PrimaryOffice) });
AsExpandable()会自动将Invoke调用展开为完整的表达式树,EF Core可正常解析为SQL,仅查询需要的字段,无额外性能损耗。
方案B:无第三方依赖实现
如果不想引入额外NuGet包,可以自定义表达式访问者,将属性映射表达式替换到整体Select的表达式树中,实现代码如下:
public static class ExpressionExtensions { public static Expression<Func<TSource, TTarget>> Inject<TSource, TTarget, TProp>( this Expression<Func<TSource, TTarget>> mainExpr, Expression<Func<TSource, TProp>> propExpr, string propertyName) { var parameter = mainExpr.Parameters.First(); var memberInit = (MemberInitExpression)mainExpr.Body; var newBinding = Expression.Bind( typeof(TTarget).GetProperty(propertyName), Expression.Invoke(propExpr, parameter) ); var newBindings = memberInit.Bindings.Where(b => b.Member.Name != propertyName).Append(newBinding); var newMemberInit = Expression.MemberInit(memberInit.NewExpression, newBindings); return Expression.Lambda<Func<TSource, TTarget>>(newMemberInit, parameter); } public static IQueryable<TTarget> SelectWithInjection<TSource, TTarget>( this IQueryable<TSource> source, Expression<Func<TSource, TTarget>> baseSelect, params (string PropName, object MappingExpr)[] injections) { var expr = baseSelect; foreach (var (propName, mappingExpr) in injections) { var genericMethod = typeof(ExpressionExtensions).GetMethod(nameof(Inject)) ?.MakeGenericMethod(typeof(TSource), typeof(TTarget), mappingExpr.GetType().GetGenericArguments()[1]); if (genericMethod == null) continue; expr = (Expression<Func<TSource, TTarget>>)genericMethod.Invoke(null, new object[] { expr, mappingExpr, propName }); } return source.Select(expr); } }
使用方式:
// YourEntity替换为你的查询源实体类型 var baseSelect = (Expression<Func<YourEntity, ExampleViewModel>>)(x => new ExampleViewModel() { Id = x.Id, BindDate = x.BindDate, CancellationDate = x.CancellationDate, ChangeDate = x.ChangeDate, CreateDate = x.CreateDate, Description = x.Description, // 此处默认值会被替换,随便填写即可 PrimaryOffice = null }); IQueryable<ExampleViewModel> list = query.SelectWithInjection(baseSelect, (nameof(ExampleViewModel.PrimaryOffice), (Expression<Func<YourEntity, GenericComboBox>>)(x => CommonMappings.ToGenericComboBox.Invoke(x.PrimaryOffice))) );
验证
你可以通过EF Core日志输出生成的SQL,确认只会查询映射逻辑中用到的字段,不会加载全量实体数据,符合OData的性能要求。
内容的提问来源于stack exchange,提问作者Jackie
相关产品推荐
相关产品推荐

