C# .NET Core中如何使用LINQ按展示模型列名对实体模型排序
解决方案
ProjectTo 不是唯一的解决方案,你可以根据自己的项目架构选择以下两种主流实现方式:
方案1:新增Dto字段到实体路径的映射规则(无需调整现有仓储返回逻辑)
适合已经写死仓储层返回List<TEntity>、不想调整原有架构的场景。核心思路是给Dto的字段配置和实体导航路径的映射关系,改造现有排序扩展方法,自动识别映射关系生成正确的排序表达式。
实现步骤:
- 定义映射标记Attribute
[AttributeUsage(AttributeTargets.Property)] public class SortMapAttribute : Attribute { /// <summary> /// 对应实体类的导航属性路径 /// </summary> public string EntityPropertyPath { get; } public SortMapAttribute(string entityPropertyPath) { EntityPropertyPath = entityPropertyPath; } }
- 给Dto字段加映射标记
public class UserDetailsViewDto { public long Id { get; set; } public string UserName { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string PhoneNumber { get; set; } public bool EmailConfirmed { get; set; } // 配置映射到实体的导航路径 [SortMap("UserRoles.FirstOrDefault().Role.Name")] public string Role { get; set; } }
- 改造排序扩展方法,支持嵌套路径解析和映射识别
首先新增路径解析逻辑,把类似A.FirstOrDefault().B.C的字符串转成表达式:
private static Expression GetPropertyPathExpression(ParameterExpression parameter, string propertyPath) { var properties = propertyPath.Split('.'); Expression propertyAccess = parameter; Type currentType = parameter.Type; foreach (var propName in properties) { // 处理FirstOrDefault()这类方法调用 if (propName.EndsWith("()")) { var methodName = propName.TrimEnd('(', ')'); var method = currentType.GetMethods() .First(m => m.Name == methodName && m.GetParameters().Length == 0); propertyAccess = Expression.Call(propertyAccess, method); currentType = method.ReturnType; continue; } // 普通属性访问 var property = currentType.GetProperty(propName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); propertyAccess = Expression.MakeMemberAccess(propertyAccess, property); currentType = property.PropertyType; } return propertyAccess; }
然后改造原有的OrderBy方法,优先读取Dto的映射配置:
// 新增重载,传入Dto类型用于读取映射配置 public static IQueryable<TEntity> OrderBy<TEntity, TDto>(this IQueryable<TEntity> source, string orderByProperty, bool desc) { string command = desc ? "OrderByDescending" : "OrderBy"; var entityType = typeof(TEntity); var dtoType = typeof(TDto); // 先查Dto对应字段有没有配置映射路径 var dtoProperty = dtoType.GetProperty(orderByProperty, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); var sortMapAttr = dtoProperty?.GetCustomAttribute<SortMapAttribute>(); string finalPath = sortMapAttr?.EntityPropertyPath ?? orderByProperty; var parameter = Expression.Parameter(entityType, "p"); var propertyAccess = GetPropertyPathExpression(parameter, finalPath); var orderByExpression = Expression.Lambda(propertyAccess, parameter); var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { entityType, propertyAccess.Type }, source.Expression, Expression.Quote(orderByExpression)); return source.Provider.CreateQuery<TEntity>(resultExpression); }
使用的时候只需要在调用排序时指定Dto类型即可:
var users = Context.Users .Include(user => user.UserRoles) .ThenInclude(ur => ur.Role) .OrderBy<User, UserDetailsViewDto>(query.SortColumn, query.SortDirection == "desc") .ToList(); // 后续再做Dto映射即可
方案2:使用ProjectTo提前完成投影(推荐,多页面场景维护成本更低)
如果可以调整仓储层返回IQueryable<TEntity>,或者直接在业务层操作DbContext,这个方案代码更简洁,不需要维护额外的映射Attribute,直接复用AutoMapper的字段映射配置即可。
实现步骤:
- 配置AutoMapper映射规则,和你现有的Dto映射规则完全一致
CreateMap<User, UserDetailsViewDto>() .ForMember(d => d.Role, opt => opt.MapFrom(s => s.UserRoles.FirstOrDefault().Role.Name));
- 直接在查询阶段完成投影,排序直接作用于Dto字段
var users = Context.Users .ProjectTo<UserDetailsViewDto>(_mapper.ConfigurationProvider) .ApplySort(query) .ToList();
这个方案不需要手动写Include,AutoMapper会自动根据映射关系生成关联查询,排序时直接传前端传的Role字段即可,EF会自动生成对应导航路径的排序SQL。
方案对比
| 方案 | 架构改造成本 | 维护成本 | 性能 |
|---|---|---|---|
| 映射Attribute方案 | 低,无需修改现有仓储返回逻辑 | 中,每个需要特殊映射的字段都要加Attribute | 略低,需要手动写Include加载关联数据 |
| ProjectTo方案 | 中,需要调整查询逻辑提前投影 | 低,直接复用现有Dto映射配置 | 高,自动生成最优SQL,只查需要的字段 |
你可以根据自己项目的实际架构选择合适的方案,不需要强制使用ProjectTo。
内容的提问来源于stack exchange,提问作者Stefan Petrović
相关产品推荐
相关产品推荐

