EF Core 规范模式添加所有列以使用自定义规范对数据进行排序

EF Core Specification pattern add all column for sorting data with custom specification

我为我的 .net 核心项目应用了规范模式。 我还为包含、排序、分页等创建了自定义规范

我使用 queryString 从 api url 获得 sort 值并传递给自定义规范 class。 在此 class 中,我添加了一些 switch case 以确定哪一列应 orderByorderByDescending

但是 table 中的列太多了。那么有什么方法可以将 sort 变量一次应用于所有列?还是我必须将所有列都写入 switch

这是我的自定义规格 class。

public class PersonsWithGroupsAndPrivileges : BaseSpecification<Person>
{
public PersonsWithGroupsAndPrivileges(string sort) : base()
{
    AddInclude(x => x.Group);
    AddInclude(x => x.Privilege);
    
    if(!string.IsNullOrEmpty(sort))
    {
        switch (sort)
        {
            case "gender": ApplyOrderBy(x => x.Gender); break;
            case "genderDesc": ApplyOrderByDescending(x => x.Gender); break;
            case "roomNo": ApplyOrderBy(x => x.RoomNo); break;
            case "roomNoDesc": ApplyOrderByDescending(x => x.RoomNo); break;
            default: ApplyOrderBy(x => x.Name); break;
        }
    }
}

}

ISpecification.cs 界面

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace XXXX.Core.Specifications
{
    public interface ISpecification<T>
    {
        Expression<Func<T, bool>> Criteria { get; }
        List<Expression<Func<T, object>>> Includes { get; }
        List<string> IncludeStrings { get; }
        Expression<Func<T, object>> OrderBy { get; }
        Expression<Func<T, object>> OrderByDescending { get; }
        Expression<Func<T, object>> GroupBy { get; }

        int Take { get; }
        int Skip { get; }
        bool IsPagingEnabled { get; }
    }
}

BaseSpecification.cs

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace XXXX.Core.Specifications
{
    public abstract class BaseSpecification<T> : ISpecification<T>
    {
    protected BaseSpecification(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }
    protected BaseSpecification()
    {

    }
    public Expression<Func<T, bool>> Criteria { get; }
    public List<Expression<Func<T, object>>> Includes { get; } = new List<Expression<Func<T, object>>>();
    public List<string> IncludeStrings { get; } = new List<string>();
    public Expression<Func<T, object>> OrderBy { get; private set; }
    public Expression<Func<T, object>> OrderByDescending { get; private set; }
    public Expression<Func<T, object>> GroupBy { get; private set; }

    public int Take { get; private set; }
    public int Skip { get; private set; }
    public bool IsPagingEnabled { get; private set; } = false;

    protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }

    protected virtual void AddInclude(string includeString)
    {
        IncludeStrings.Add(includeString);
    }

    protected virtual void ApplyPaging(int skip, int take)
    {
        Skip = skip;
        Take = take;
        IsPagingEnabled = true;
    }

    protected virtual void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
    {
        OrderBy = orderByExpression;
    }

    protected virtual void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescendingExpression)
    {
        OrderByDescending = orderByDescendingExpression;
    }

    protected virtual void ApplyGroupBy(Expression<Func<T, object>> groupByExpression)
    {
        GroupBy = groupByExpression;
    }
    }
}

SpecificationEvaluator.cs

using System.Linq;
using DesTech.Core.Entities;
using DesTech.Core.Specifications;
using Microsoft.EntityFrameworkCore;

namespace XXXX.Infrastructure.Data
{
public class SpecificationEvaluator<TEntity> where TEntity : BaseEntity
{
    public static IQueryable<TEntity> GetQuery(IQueryable<TEntity> inputQuery, ISpecification<TEntity> specification)
    {
        var query = inputQuery;

    if (specification.Criteria != null)
    {
        query = query.Where(specification.Criteria);
    }

    query = specification.Includes.Aggregate(query, (current, include) => current.Include(include));

    query = specification.IncludeStrings.Aggregate(query, (current, include) => current.Include(include));

    if (specification.OrderBy != null)
    {
        query = query.OrderBy(specification.OrderBy);
    }
    else if (specification.OrderByDescending != null)
    {
        query = query.OrderByDescending(specification.OrderByDescending);
    }

    if (specification.GroupBy != null)
    {
        query = query.GroupBy(specification.GroupBy).SelectMany(x => x);
    }
    if (specification.IsPagingEnabled)
    {
        query = query.Skip(specification.Skip)
                     .Take(specification.Take);
    }
    return query;
   }
}
}

一个简单的方法是使用反射按名称获取 属性,然后构建 Expression<Func<PersonWithGroupsAndPrivileges, object>> 表达式。

让我们假设这样一个规范 class:

public class BaseSpecification<T>
{
    public Expression<Func<T, object>> OrderBy { get; private set; }
    public Expression<Func<T, object>> OrderByDescending { get; private set; }
    
    protected virtual void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
        => OrderBy = orderByExpression;

    protected virtual void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescendingExpression)
        => OrderByDescending = orderByDescendingExpression;
}

这是一个完整的示例控制台项目,它实现了 PersonWithGroupsAndPrivileges<T> class 并将其用于 Person class:

using System;
using System.Linq.Expressions;
using System.Reflection;

namespace IssueConsoleTemplate
{
    public class Person
    {
        public int PersonId { get; set; }
        public string Gender { get; set; }
        public string RoomNo { get; set; }
    }

    public class BaseSpecification<T>
    {
        public Expression<Func<T, object>> OrderBy { get; private set; }
        public Expression<Func<T, object>> OrderByDescending { get; private set; }
        
        protected virtual void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
            => OrderBy = orderByExpression;

        protected virtual void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescendingExpression)
            => OrderByDescending = orderByDescendingExpression;

        protected void ApplySorting(string sort)
        {
            if(!string.IsNullOrEmpty(sort))
            {
                const string descendingSuffix = "Desc";

                var descending = sort.EndsWith(descendingSuffix, StringComparison.Ordinal);
                var propertyName = sort.Substring(0, 1).ToUpperInvariant() +
                                   sort.Substring(1, sort.Length - 1 - (descending ? descendingSuffix.Length : 0));

                var specificationType = GetType().BaseType;
                var targetType = specificationType.GenericTypeArguments[0];
                var property = targetType.GetRuntimeProperty(propertyName) ??
                               throw new InvalidOperationException($"Because the property {propertyName} does not exist it cannot be sorted.");

                // Create an Expression<Func<T, object>>.
                var lambdaParamX = Expression.Parameter(targetType, "x");

                var propertyReturningExpression = Expression.Lambda(
                    Expression.Convert(
                        Expression.Property(lambdaParamX, property),
                        typeof(object)),
                    lambdaParamX);

                if (descending)
                {
                    specificationType.GetMethod(
                            nameof(ApplyOrderByDescending),
                            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                        .Invoke(this, new object[]{propertyReturningExpression});
                }
                else
                {
                    specificationType.GetMethod(
                            nameof(ApplyOrderBy),
                            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                        .Invoke(this, new object[]{propertyReturningExpression});
                }
            }
        }
    }

    public class PersonsWithGroupsAndPrivileges<T> : BaseSpecification<T>
    {
        public PersonsWithGroupsAndPrivileges(string sort)
        {
            ApplySorting(sort);
        }
    }
    
    internal static class Program
    {
        private static void Main()
        {
            var p1 = new PersonsWithGroupsAndPrivileges<Person>("gender");
            var p2 = new PersonsWithGroupsAndPrivileges<Person>("genderDesc");
        }
    }
}

代码适用于任何 class T.

您基本上需要两个辅助方法 - 一个从排序字符串中提取排序信息(名称和降序),另一个动态构建并从中应用 Expression<Func<T, object>>。两者都转到基础泛型 class.

第一个处理像 {property}[[ ]{Desc}](不区分大小写)的模式可能是这样的:

protected virtual void ExtractSortInfo(string sort, out string propertyPath, out bool descending)
{
    const string Desc = "Desc";
    propertyPath = sort;
    descending = false;
    if (propertyPath.EndsWith(Desc, StringComparison.OrdinalIgnoreCase))
    {
        propertyPath = sort.Substring(0, sort.Length - Desc.Length).TrimEnd();
        descending = true;
    }
}

第二个像这样:

public virtual void ApplySort(string sort)
{
    if (string.IsNullOrEmpty(sort)) return;
    ExtractSortInfo(sort, out var propertyPath, out var descending);
    var parameter = Expression.Parameter(typeof(T), "x");
    var body = propertyPath.Split('.').Aggregate((Expression)parameter, Expression.Property);
    if (body.Type.IsValueType) body = Expression.Convert(body, typeof(object));
    var selector = Expression.Lambda<Func<T, object>>(body, parameter);
    if (descending)
        ApplyOrderByDescending(selector);
    else
        ApplyOrderBy(selector);
}

另外,它支持点分隔的嵌套属性,例如 blog.name for class Person having navigation 属性 Blog having 属性 Name.

Expression.Convert用于支持值类型(intdecimalDateTime等)属性