使用c#Linq表达式进行自定义排序

时间:2017-11-09 10:35:55

标签: c# linq sorting lambda entity-framework-core

我试图让C#Application和C#Web API更容易排序。我使用Entity Framework Core进行持久性和测试。

在我的应用程序或Web API中,我确定了Order,Descending或Ascending,Property Name。

我将这些知识传递到我的存储库中,其中创建并执行了Linq查询。问题是,当我有一个十进制列时,它执行字符串顺序而不是十进制顺序。

public static class SortingExtensions
{
    public static IQueryable<T> SortBy<T>(
        this IQueryable<T> queryable,
        Sorting sorting)
    {
        IOrderedQueryable<T> orderedQueryable = null;

        sorting.SortableEntities
            .OrderBy(x => x.Order)
            .ToList()
            .ForEach(sortableEntity =>
            {
                Expression<Func<T, object>> expression = QueryHelper.GetDefaultSortExpression<T>(sortableEntity);
                if (expression != null)
                {
                    orderedQueryable = orderedQueryable == null
                        ? queryable.OrderBy(expression, sortableEntity.Descending)
                        : orderedQueryable.OrderBy(expression, sortableEntity.Descending);
                }
            });

        return orderedQueryable;
    }

    private static IOrderedQueryable<T> OrderBy<T, TKey>(
        this IOrderedQueryable<T> query,
        Expression<Func<T, TKey>> keySelector,
        bool descending) => descending ? query.ThenByDescending(keySelector) : query.ThenBy(keySelector);

    private static IOrderedQueryable<T> OrderBy<T, TKey>(
        this IQueryable<T> query,
        Expression<Func<T, TKey>> keySelector,
        bool descending) => descending ? query.OrderByDescending(keySelector) : query.OrderBy(keySelector);
}

public static class QueryHelper
{
    public static Expression<Func<T, object>> GetDefaultSortExpression<T>(SortableEntity sortableEntity)
    {
        Type entityType = typeof(T);
        ParameterExpression arg = Expression.Parameter(entityType, "x");

        string[] fieldNames = sortableEntity.Name.Split('.');
        MemberExpression memberExpression = null;
        foreach (string name in fieldNames)
        {
            Expression expressionToUse = memberExpression ?? (Expression) arg;
            memberExpression = Expression.Property(expressionToUse, name.ToProperCase());
        }

        Expression propertyExpression = Expression.Convert(memberExpression, typeof(object));
        Expression<Func<T, object>>
            complexExpression = Expression.Lambda<Func<T, object>>(propertyExpression, arg);
        return complexExpression;
    }
}

public class SortableEntity
{
    public int Order { get; set; }
    public bool Descending { get; set; }
    public string Name { get; set; }
}

public class Sorting
{
    IEnumerable<SortableEntity> SortableEntities { get; }
}

public class TestDecimalPropertyClass : Entity
{
    public TestDecimalPropertyClass(decimal @decimal) => Decimal = @decimal;

    protected TestDecimalPropertyClass()
    {
    }

    public decimal Decimal { get; set; }
}

public class TestDecimalPropertyClassRepository
{
    private readonly DbContext _dbContext;

    public TestDecimalPropertyClassRepository(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<IEnumerable<TestDecimalPropertyClass>> GetAllAsync(Sorting sorting)
    {
        List<TestDecimalPropertyClass> entities = await _dbContext.Set<TestDecimalPropertyClass>()
            .SortBy(sorting)
            .ToListAsync();

        return entities;
    }

    public async Task SaveAsync(TestDecimalPropertyClass testDecimalPropertyClass)
    {
        _dbContext.Set<TestDecimalPropertyClass>().Add(testDecimalPropertyClass);
        await _dbContext.SaveChangesAsync();
    }
}

这是我为它写的测试:

[TestFixture]
public class GenericSortingTests
{
    private SqliteConnection SqliteConnection { get; set; }

    [SetUp]
    public void DbSetup()
    {
        SqliteConnectionStringBuilder sqliteConnectionStringBuilder = new SqliteConnectionStringBuilder
        {
            Mode = SqliteOpenMode.Memory,
            Cache = SqliteCacheMode.Private
        };
        SqliteConnection = new SqliteConnection(sqliteConnectionStringBuilder.ToString());
        SqliteConnection.Open();
    }

    [TearDown]
    public void DbTearDown()
    {
        SqliteConnection.Close();
    }
    [Test]
    public async Task GivenADecimalProperty_WhenISortByColumn_ThenItSorts()
    {
        decimal[] decimals = new[] {7m, 84.3m, 13.4m};

        using (DbContext dbContext = GetDbContext())
        {
            TestDecimalPropertyClassRepository testRepository = new TestDecimalPropertyClassRepository(dbContext);

            foreach (decimal @decimal in decimals)
            {
                TestDecimalPropertyClass entity = new TestDecimalPropertyClass(@decimal);
                await testRepository.SaveAsync(entity);
            }
        }

        IEnumerable<TestDecimalPropertyClass> entities;
        using (DbContext dbContext = GetDbContext())
        {
            TestDecimalPropertyClassRepository testRepository = new TestDecimalPropertyClassRepository(dbContext);

            entities = await testRepository.GetAllAsync(new Sorting
            {
                SortableEntities = new[]
                {
                    new SortableEntity
                    {
                        Descending = false,
                        Name = "decimal",
                        Order = 0
                    }
                }
            });
        }

        List<TestDecimalPropertyClass> list = entities.ToList();
        Assert.That(list.Count(), Is.EqualTo(decimals.Length));
        Assert.That(list.ToArray()[0].Decimal, Is.EqualTo(7m));
        Assert.That(list.ToArray()[1].Decimal, Is.EqualTo(13.4m));
        Assert.That(list.ToArray()[2].Decimal, Is.EqualTo(84.3m));
    }

    private class TestDbContext : DbContext
    {
        public TestDbContext(DbContextOptions options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestDecimalPropertyClass>();
            base.OnModelCreating(modelBuilder);
        }
    }

    private DbContext GetDbContext()
    {
        DbContextOptions<TestDbContext> options = new DbContextOptionsBuilder<TestDbContext>()
            .UseSqlite(SqliteConnection)
            .EnableSensitiveDataLogging()
            .Options;

        TestDbContext dbContext = new TestDbContext(options);
        dbContext.Database.EnsureCreated();
        return dbContext;
    }
}

我希望它将物品分类为顺序:7米,13.4米,84.3米,但它将其分类为13.4米,7米,84.3米

任何人都可以帮助我理解为什么这样做我可以解决它吗?

谢谢, 克里斯

1 个答案:

答案 0 :(得分:3)

首先,我以前曾试图像这样重新发明这种轮子,但它永远不会像你想的那样真正起作用。如果您需要那种动态灵活性,那么可能已经存在某个库,您可能会实际手动制作SQL或其他东西(很糟糕,但有时这是唯一实用的方法) )......除此之外......

我认为你的问题实际与SQLite有关 - 由于拼写错误或版本不相同,我无法使SQLite工作正常(例如,SQLite的默认nuget包有一个SQLiteConnectionStringBuilder而不是SqliteConnectionStringBuilder,这似乎没有与您的示例相同的属性)所以我在某种程度上破解了你的代码以删除SQL的东西并摆脱异步的东西(就像我想的那样) 希望这真的不相关),所以我改为使用这个存储库:

public class TestDecimalPropertyClassRepository
{
    private readonly IList<TestDecimalPropertyClass> list;

    public TestDecimalPropertyClassRepository(IEnumerable<TestDecimalPropertyClass> repo)
    {
        list = repo.ToList();
    }

    public IEnumerable<TestDecimalPropertyClass> GetAll(Sorting sorting)
    {
        List<TestDecimalPropertyClass> entities = list
            .AsQueryable()
            .SortBy(sorting)
            .ToList();

        return entities;
    }

    public void Save(TestDecimalPropertyClass testDecimalPropertyClass)
    {
        list.Add(testDecimalPropertyClass);            

    }
}

这使测试看起来像这样

[Test]
public void GivenADecimalProperty_WhenISortByColumn_ThenItSorts()
{
    decimal[] decimals = new[] { 7m, 84.3m, 13.4m };
    var repo = decimals.Select(x => new TestDecimalPropertyClass(x));

    TestDecimalPropertyClassRepository testRepository = new TestDecimalPropertyClassRepository(repo);

    var entities = testRepository.GetAll(new Sorting
    {
        SortableEntities = new[]
            {
            new SortableEntity
            {
                Descending = false,
                Name = "decimal",
                Order = 0
            }
        }
    });

    List<TestDecimalPropertyClass> list = entities.ToList();
    Assert.That(list.Count(), Is.EqualTo(decimals.Length));
    Assert.That(list.ToArray()[0].Decimal, Is.EqualTo(7m));
    Assert.That(list.ToArray()[1].Decimal, Is.EqualTo(13.4m));
    Assert.That(list.ToArray()[2].Decimal, Is.EqualTo(84.3m));
}

并且所有的扩展程序都是相同的,所以它仍然以相同的方式反射等等。

此测试通过正常。现在,这并不完全有效,因为它当然不再完全相同,但它在我看来意味着它可能可能不是框架错误解释小数属性的类型,或者一些与装箱/拆箱相关的一些混淆意味着它无法计算出类型,并进行.ToString()进行比较。

假设SQLite EF提供程序正确地将其转换为SQL ORDER BY子句,您是否检查过此SQL?在过去,我做过类似的事情(使用SQLite编写测试),并发现它不像SQL Server或类似的一些模糊方法那么完整。也许提供者有一个bug,或者生成的表达式树中有一个怪癖,它不能很好地理解。

所以我先研究一下,而不是你写的代码......