如何为EF6创建可重用的where子句

时间:2017-04-06 15:38:40

标签: c# entity-framework linq

我最近从Java编码转到c#,我仍在学习c#的各种元素。

要访问我无法重新设计的现有数据库,我使用实体框架6和“数据库中的代码优先”来生成表示数据库表的上下文和类型。我正在使用Ling-To-SQL从数据库中检索严重非规范化的数据。

我当前的任务是创建一个报告,其中从各个表中读取每个部分,这些表都与一个基表有关系。

这是我的工作范例:

using(var db = new PaymentContext()) 
{
    var out = from pay in db.Payment
              join typ in db.Type on new { pay.ID, pay.TypeID } equals 
                                     new { typ.ID, typ.TypeID }
              join base in db.BaseTable on 
                  new { pay.Key1, pay.Key2, pay.Key3, pay.Key4, pay.Key5 } equals 
                  new { base.Key1, base.Key2, base.Key3, base.Key4, base.Key5 }
              where 
              base.Cancelled.Equals("0") &&
              base.TimeStamp.CompareTo(startTime) > 0 &&
              base.TimeStamp.CompareTo(endTime) < 1 &&
              .
              (other conditions)
              .
              group new { pay, typ } by new { typ.PaymentType } into grp
              select new
              {
                  name = grp.Key,
                  count = grp.Count(),
                  total = grp.Sum(x => x.pay.Amount)
              };
}

报告中会有大量的部分,每个部分都会生成一个where子句,其中包含显示的条件。在某些部分中,所需数据将从表格中提取,最多可在BaseTable下面的五个级别中提取。

我想要做的是为每个报告部分创建一个resuable where子句,以避免大量重复的代码。

经过大量搜索,我尝试使用建议的here解决方案,但这已在Entity Framework 6中取代。

如何避免不必要地重复代码?

2 个答案:

答案 0 :(得分:0)

The nice thing about LINQ is that methods like Where() return an IEnumerable<T> that you can feed into the next method.

You could refactor the where clauses into extension methods akin to:

public static class PaymentQueryExtensions {

    public static IQueryable<T> ApplyNotCancelledFilter(
        this IQueryable<T> payments) 
        where T : BaseTable {

        // no explicit 'join' needed to access properties of base class in EF Model
        return payments.Where(p => p.Cancelled.Equals("0"));
    }

    public static IQueryable<T> ApplyTimeFilter(
        this IQueryable<T> payments, DateTime startTime, DateTime endTime) 
        where T: BaseTable {

        return payments.Where(p => p.TimeStamp.CompareTo(startTime) > 0 
                                && p.TimeStamp.CompareTo(endTime) < 1);
    }

    public static IGrouping<Typ, T> GroupByType(
         this IQueryable<T> payments) 
         where T: BaseTable {

        // assuming the relationship Payment -> Typ has been set up with a backlink property Payment.Typ
        // e.g. for EF fluent API: 
        //  ModelBuilder.Entity<Typ>().HasMany(t => t.Payment).WithRequired(p => p.Typ);
        return payments.GroupBy(p => p.Typ);
    }
}

And then compose your queries using these building blocks:

IEnumerable<Payment> payments = db.Payment
    .ApplyNotCancelledFilter()
    .ApplyTimeFilter(startTime, endTime);

if (renderSectionOne) {
    payments = payments.ApplySectionOneFilter();
}

var paymentsByType = payments.GroupByType();

var result = paymentsByType.Select(new
          {
              name = grp.Key,
              count = grp.Count(),
              total = grp.Sum(x => x.pay.Amount)
          }
);

Now that you have composed the query, execute it by enumerating. No DB access has happened until now.

var output = result.ToArray(); // <- DB access happens here

Edit After the suggestion of Ivan, I looked at our codebase. As he mentioned, the Extension methods should work on IQueryable instead of IEnumerable. Just take care that you only use expressions that can be translated to SQL, i.e. do not call any custom code like an overriden ToString() method.

Edit 2 If Payment and other model classes inherit BaseTable, the filter methods can be written as generic methods that accept any child type of BaseTable. Also added example for grouping method.

答案 1 :(得分:0)

我确实尝试使用你建议的扩展子句,但是我生成的类没有扩展BaseTable,所以我必须通过navigation属性显式定义链接​​。由于查询中只有少量表是常见的,因此我决定根据需要将过滤器直接应用于每个表。我将根据需要定义这些。

krillgar建议采用直接的LINQ语法,这似乎是一个很好的建议。我们打算在不久的将来重新设计我们的数据库,这将删除一些SQL依赖项。我合并了建议的过滤器和完整的LINQ语法来访问我的数据。

// A class to hold all the possible conditions applied for the report
// Can be applied at various levels within the select
public class WhereConditions
{
    public string CancelledFlag { get; set; } = "0";  // <= default setting
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
}

// Class to define all the filters to be applied to any level of table
public static class QueryExtensions
{
    public static IQueryable<BaseTable> ApplyCancellationFilter(this IQueryable<BaseTable> base, WhereConditions clause)
    {
        return base.Where(bse => bse.CancelFlag.Equals(clause.CancelledFlag));
    }

    public static IQueryable<BaseTable> ApplyTimeFilter(this IQueryable<BaseTable> base, WhereConditions clause)
    {
        return base.Where(bse => bse.TimeStamp.CompareTo(clause.StartTime) > 0 &&
                                 bse.TimeStamp.CompareTo(clause.EndTime) < 1);
    }
}

查询的组成如下:

using (var db = new PaymentContext())
{
    IEnumerable<BaseTable> filter = db.BaseTable.ApplyCancellationFilter(clause).ApplyTimeFilter(clause);

    var result = db.Payment.
                Join(
                    filter,
                    pay => new { pay.Key1, pay.Key2, pay.Key3, pay.Key4, pay.Key5 },
                    bse => new { bse.Key1, bse.Key2, bse.Key3, bse.Key4, bse.Key5 },
                    (pay, bse) => new { Payment = pay, BaseTable = bse }).
                Join(
                    db.Type,
                    pay => new { pay.Payment.TypeKey1, pay.Payment.TypeKey2 },
                    typ => new { typ.TypeKey1, typ.TypeKey2 },
                    (pay, typ) => new { name = typ.Description, amount = pay.Amount }).
                GroupBy(x => x.name).
                Select(y => new { name = y.Key, 
                                  count = y.Count(), 
                                  amount = y.Sum(z => z.amount)});
}

然后最终执行撰写的查询。

var reportDetail = result.ToArray(); // <= Access database here

由于此查询是我将要应用的最简单的查询,因此将来的查询将变得更加复杂。