简体   繁体   English

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

[英]Custom Sorting using c# Linq Expressions

I'm trying to make sorting easier for an C# Application and C# Web API. 我正在尝试使C#Application和C#Web API更容易排序。 I'm using Entity Framework Core for my persistence and testing. 我正在使用Entity Framework Core进行持久性和测试。

In my application or Web API I determine the Order, Descending or Ascending, Property Name. 在我的应用程序或Web API中,我确定了Order,Descending或Ascending,Property Name。

I pass this knowledge into my repository where a Linq query is created and executed. 我将这些知识传递到我的存储库中,其中创建并执行了Linq查询。 The problem is when I have a decimal column its doing a string order rather than a decimal order. 问题是,当我有一个十进制列时,它执行字符串顺序而不是十进制顺序。

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();
    }
}

Here is a test I wrote for it: 这是我为它写的测试:

[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;
    }
}

I expect it to sort the items into the order: 7m, 13.4m, 84.3m but instead it sorts it into 13.4m, 7m, 84.3m 我希望它能按顺序对物品进行分类:7米,13.4米,84.3米,但它将其分为13.4米,7米,84.3米

Can anyone help me understand why its doing this so I can fix it? 任何人都可以帮助我理解为什么它这样做,所以我可以解决它?

Thanks, Chris 谢谢,克里斯

First off, I've previously tried to re-invent the wheel like this myself, and it never ever really works as well as you'd like. 首先,我以前曾试图像这样重新发明这种轮子,它永远不会像你想的那样真正起作用。 If you need that sort of dynamic flexiblity, then either there is probably already a library somewhere, or you may as well drop to actually manually crafting SQL or something (it sucks, but sometimes it's the only pragmatic approach).. That aside... 如果你需要那种动态的灵活性,那么可能已经有一个库在某处, 或者你也可以实际手动制作SQL或其他东西(它很糟糕,但有时它是唯一实用的方法)..除此之外.. 。

I think your problem is actually related to SQLite - I couldn't get the SQLite stuff to work due to either typos or versions not being the same (eg The default nuget packages for SQLite have a SQLiteConnectionStringBuilder and NOT a SqliteConnectionStringBuilder , and this doesn't appear to have the same properties as your example) so I hacked your code somewhat to remove the SQL stuff and get rid of Async things (as I would hope that that's not relevant really), so I have this repository instead: 我认为你的问题实际上与SQLite有关 - 由于拼写错误或版本不相同,我无法让SQLite的东西工作(例如,SQLite的默认nuget包有一个SQLiteConnectionStringBuilder而不是一个SqliteConnectionStringBuilder ,这不是' t似乎与你的例子具有相同的属性)所以我在某种程度上破解了你的代码以删除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);            

    }
}

Which makes the test look like this 这使得测试看起来像这样

[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));
}

And left all your extension stuff the same, so it's still reflecting around etc. in the same way. 并且所有扩展内容都保持不变,因此它仍然以相同的方式反射等。

This test passes fine. 这个测试通过很好。 Now, this isn't really entirely valid as it's no longer quite the same of course, but it does to my mind mean it's probably not the framework mis-interpreting the type of the decimal property, or some sort of confusion related to boxing/unboxing meaning it can't work out the type and does a .ToString() for comparison. 现在,这并不是完全有效,因为它当然不再完全相同,但它确实意味着它可能不是框架错误解释小数属性的类型,或者与拳击相关的某种混乱/取消装箱意味着它无法计算出类型,并进行.ToString()进行比较。

Assuming the SQLite EF provider is correctly translating this into a SQL ORDER BY clause, have you checked this SQL? 假设SQLite EF提供程序正确地将其转换为SQL ORDER BY子句,您是否检查过此SQL? In the past I've done similar (used SQLite to write tests) and found it's not quite as complete in some obscure ways as SQL Server or similar. 在过去,我做过类似的事情(使用SQLite编写测试),并发现它不像SQL Server或类似的一些模糊方法那么完整。 Perhaps the provider has a bug, or there's a quirk in the generated expression tree that it can't quite understand well enough. 也许提供者有一个bug,或者生成的表达式树中有一个怪癖,它不能很好地理解。

So I'd look into that first rather than the code you've written.. 所以我先研究一下,而不是你写的代码。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM