创建匿名类型时如何使用临时本地数据?

时间:2018-07-18 09:48:32

标签: c# linq anonymous-types

我从EF获取了一些实体,并对其进行了迭代并创建了匿名类型对象,例如:

var payments = ctx.Payments.ToList();
var data = ctx.Activities.OrderBy(p => p.ID).ToList().Select(p => new
{
    ID = p.ID,
    Date = p.Date?.ToString("dd/MM/yyyy"),
    PaymentMethod = p.PaymentMethods != null ? p.PaymentMethods.Description : "",
    ActivityStatusID = payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() == 0 ? 1 : .. // I need some other check
}).ToList();

现在,我想先检查payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count(),然后再设置一个值。

例如:

  1. 如果.Count()== 0,则值为1
  2. 如果.Count()== 1,值8
  3. 如果.Count()> 1,值为10
  4. 其他87

,依此类推。我不会做这样的事情:

ActivityStatusID = payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() == 0 ? 1 : payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() == 1 ? 8 : payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() > 1 ? 10 : 87

是否有任何方法可以对每个payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count()做一次p,而不是评估条件?

2 个答案:

答案 0 :(得分:1)

我会将查询从lambda表达式更改为普通查询。然后使用let语法为每次迭代设置一个变量。像这样:

var payments = ctx.Payments.ToList();
var data = (
    from p in ctx.Activities.ToList()
    orderby p.ID
    let paymentCount = payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count()
    select new
    {
        ID = p.ID,
        Date = p.Date?.ToString("dd/MM/yyyy"),
        PaymentMethod = p.PaymentMethods != null ? p.PaymentMethods.Description : "",
        ActivityStatusID = paymentCount == 0 ? 1 : .. // I need some other check
    }
).ToList();

顺便说一句,您也可以不同地做这部分。这个:

payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count()

您可以这样写:

payments.Count(q => q.ActivityID == p.ID && !q.Paid)

答案 1 :(得分:1)

人们建议将查询转换为查询语法,这将使您能够使用let语句创建一个可以保存计数的变量。

如果您调查let的作用,它将为您的结果添加一列值。结果的每一行在此列中具有相同的let值。 See this answer on stackoverflow

将查询保留在方法语法中

如果要使查询保持方法语法,只需将Count添加到您的匿名类型中即可:

var result = ctx.Activities
    .OrderBy(p => p.ID)
    .Select(p => new
    {
        Id = p.Id,
        Date = p.Date?.ToString("dd/MM/yyyy"),
        PaymentMethod = p.PaymentMethods != null ? p.PaymentMethods.Description : "",

        PaidCount = payments
            .Where(q => q.ActivityID == p.ID && !q.Paid)
            .Count();
    })
    .Select(p => new
    {
        Id = p.Id,
        Date = p.Date,
        ActivityStatusId = 
        {
             // count == 0 => 1
             // count == 1 => 8
             // count >  1 => 10
             // count <  0 => 87 
             if (p.PaidCount < 0) return 87;

             switch (p.PaidCount)
             {
                 case 0:
                    return 0;
                 case 1:
                    return 8;
                 default:
                    return 10;
             }
        },
    });

请注意,仅当您使用ToList将完整的ctx.Activities带到本地内存后,才能进行切换。

效率提高

您先进行ToList的选择。这意味着在您开始选择和计算序列中的项目之前,您的完整ctx.payments会在本地存储器中实现为List

如果ctx.Payments来自外部来源,例如数据库或文件,则ctx.Payments是IQueryable而不是IEnumerable。将完整的付款提取到本地内存不是一种有效的方法。

建议:每当您有一个IQueryable时,请尝试使其保持IQueryable尽可能长的时间。您的源数据提供者可以比本地处理器更有效地处理查询。仅当您的源数据提供者无法处理它时,才将其具体化到本地内存中,例如,因为您需要调用本地过程,或者因为不再需要处理任何东西。

此外,不要将值移动到您不打算使用的本地内存中。仅选择您实际将在本地内存中使用的属性。

一项改进将是:

var result = ctx.Payments.Select(payment => new
{    // select only the properties you plan to use locally:
     Id = payment.Id,
     Date = payment.Date,
     PaymentMethod = payment.PaymentMethods?.Description,
     PaidCount = ctx.Payments
         .Where(q => q.ActivityID == p.ID && !q.Paid)
         .Count(),
})
.OrderBy(fetchedPaymentData => fetchedPaymentData.Id)

// from here you need to move it to local memory
// Use AsEnumerable instead of ToList
.AsEnumerable()
.Select(fetchedPaymentData => new
{
     Id = fetchedPaymentData.Id,
     PaymentMethod = fetchedPaymentData.PaymentMethod ?? String.Empty,
     ActivityStatusId = {...}
});

AsEnumerable比ToList效率更高,尤其是当您一次不需要所有项目时。例如,如果您将以FirstOrDefault或仅以Take(5)结尾,那么将所有项目移动到本地内存将是一种浪费。

最后:通过一些尝试,您可以摆脱switch语句,从而使DBMS可以计算ActivityStatusId。但是,由于选择源数据并将所选数据传输到本地内存是完整查询的较慢部分,因此我怀疑这是否会缩短执行时间。此开关肯定会使您的要求更易读,特别是如果将数字1/8/87放在枚举中。