【问题标题】:Custom Sorting using c# Linq Expressions使用 c# Linq 表达式自定义排序
【发布时间】:2017-11-09 10:35:54
【问题描述】:

我正在尝试使 C# 应用程序和 C# Web API 的排序更容易。我正在使用 Entity Framework Core 进行持久性和测试。

在我的应用程序或 Web API 中,我确定顺序、降序或升序、属性名称。

我将这些知识传递到我的存储库中,在该存储库中创建和执行 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;
    }
}

我希望它将项目按以下顺序排序:7m、13.4m、84.3m,但它会将其排序为 13.4m、7m、84.3m

谁能帮我理解为什么会这样,以便我修复它?

谢谢, 克里斯

【问题讨论】:

  • 它的输出顺序与输入顺序相同,这表明没有进行排序。
  • 我刚刚将十进制数组重新排序为{7m, 84.3m, 13.4m},它仍然以{13.4m, 7m, 84.3m} 出现
  • 它适用于十进制以外的类型?
  • 这里有很多代码。你能用最少的代码举例说明你的问题吗? stackoverflow.com/help/mcve
  • 包含所有代码以重现问题是一件好事,但如果您尝试从头开始并一次包含一小段代码,我怀疑您将获得更少的代码重现问题,也许你会意识到问题出在哪里。

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


【解决方案1】:

首先,我以前曾尝试过自己重新发明这样的轮子,但它从来没有像你想要的那样真正有效。如果你需要那种动态的灵活性,那么要么可能已经有一个库,或者你还不如真正手动制作 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 或类似的完整。也许提供者有一个错误,或者生成的表达式树中有一个怪癖,它不能很好地理解。

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

【讨论】:

  • 在你回复之后,我回去写了一个单元测试来做一个简单的OrderBy(x =&gt; x.Decimal),它表明问题是框架。感谢您的帮助
  • 没问题 - 我还建议在 SQL Server 上尝试一下(假设这是实际的最终用例)只是为了看看;它可能在那里工作得很好(正如我在上面暗示的那样,这是我过去见过的那种事情)。内存中的 SQLite 可以用于测试基本的存储库操作,但上次我做这样的事情时,我实际上最终使用 LocalDB 来测试 IIRC。有点痛苦,因为它必须创建一个物理数据库文件,这大大减慢了它们的速度,但我想我只是写了一些自定义的“清空数据库”代码来在 SetUp() 或其他东西中运行。仍然不是 100% 确定这值得麻烦:)
  • 这里的问题指出了 SQLite 中排序不同的原因。 github.com/aspnet/EntityFrameworkCore/issues/10249
猜你喜欢
  • 2019-06-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多