【问题标题】:Requiring Skip and Take When Querying DbSet (C#)查询 DbSet (C#) 时要求 Skip 和 Take
【发布时间】:2020-12-31 17:44:24
【问题描述】:

我正在试验,我试图为每个 DbSet 查询要求 Skip()Take()

例如,如果我尝试这样做:

var result = _context.Employee.Include(x => x.Person).ToList();

我收到一个编译时错误,例如缺少 Skip()Take() 值。

编辑:我面临的一个障碍是需要IQueryable 才能调用Skip()Take()。如果我能够在DbSet 上使用它们,那么我只需要修改Context 构造函数。

欢迎任何其他自动减少记录数的建议。

【问题讨论】:

  • 听起来你想要一个 roslyn 分析仪。否则,我建议将您的上下文和数据库集隐藏在私有变量中,并且只能通过带有 take / skip 参数的方法访问它们。
  • 这是 EF 6 吗?如果您想说应用默认限制,这将是相关的。
  • @JeremyLakeman 这些是一些很棒的建议。关于最后一条语句,skip 和 take 需要一个 Iqueryable,所以我必须解决这个问题
  • @AluanHaddad 你是对的。
  • 最接近的可能是应用某个最大数量的结果。您可以使用分析器路线,但在不同的上下文中调用它们是非常不同的,例如在 where 子句之前或之后。如果您需要在使用 tolist 或其他方式实现之前调用它们,您怎么知道我还没有调用 take?

标签: c# asp.net .net linq entity-framework-6


【解决方案1】:

你应该自己想一想:我为什么要添加 Skip(...).Take(...)

我不希望任何人要求一百万客户!

您可能希望防止用户(= 软件,而不是操作员)查询所有 1000 万客户:

var customers = repository.Customers.Skip(0).Take(Int2.MaxValue);

问题是,只要您允许用户访问您的原始 DbContext,您就不能禁止他们这样做。

即使您编写自己的ToList(...),添加一个Skip / Take,您也无法阻止他们使用原始ToList,或使用foreach,甚至在最低级别使用@987654326 @。

如果你想做出这个完整的证明,你必须创建你自己的 Repository 类,它隐藏了 DbContext。如果他们要求IDbSet<...>,您不会将 DbContext 中的 DbSet 提供给他们,而是您自己的对象会保存 DbContext 的 DbSet。

如果用户向此 DbSet 询问 IQueryable<...>,您不会给它 DbContext 的 IQueryable,而是您自己的。

当用户开始枚举这个 IQueryable 时,你添加 Skip / Take

除此之外,您还必须执行 IQueryable 操作,还必须编写项目以添加/删除/更新元素。

这是一项相当多的工作,问题是:它几乎不能完全证明。恶意用户仍然可以在您添加的 Skip/Take 无法阻止获取太多项目的情况下发明查询。

例如,如果您想防止用户一次请求超过 1000 个客户,那么恶意用户仍然可以创建用户组,每个组有 1000 个客户。添加 Skip(0).Take(1000),他仍然获得了他的 100 万客户。

所以我认为你不能轻易地完成这个完整的证明,同时仍然让用户可以访问 DbSet 的所有可能性。

我想鼓励人们购买数量有限的商品

这个要求要求不高,但更容易实现。鼓励用户询问“每页”数据的可能性。

您创建一个 PageCollection,其中每个 Page 都有许多项目。如果用户请求 Page[4],他会得到在查询中添加了 Skip / Take 的页面。

要创建此集合类的对象,不必执行查询。即使您获取页面,也不必执行查询。只有当您请求页面上的元素时,才会执行添加了 Skip / Take 的查询。

这个怎么样:

interface IPage<TResult> : IReadOnlyCollection<TResult>
{
    int PageCount {get;}
    int PageNr {get;}
    int PageSize {get;}
}

internal class Page<TResult> : IPage<TResult>
{
    internal IQueryable<TResult> Query {get; internal set;}
    public int PageCount {get; internal set;}
    public int PageNr {get; internal set; }
    public int PageSize {get; internal set;}

    // Implementation IReadOnlyCollection<TResult>
    public int Count => this.PageSize;
    public IEnumerator<TResult> GetEnumerator()
    {
         return this.Query.GetEnumerator();
    }
    
    IEnumerator IEnumerator.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

如果您这样做,则创建页面不会执行查询。如果您开始枚举页面上的项目,则会执行它。所以获取一个页面并不昂贵。

如果您认为获取的页面几乎总是会被完全枚举,请考虑在创建页面时执行查询并将数据保存在列表中。这样你就可以很容易地实现IReadOnlyList&lt;TResult&gt;。缺点:每次创建Page[4] 时都会执行查询,即使没有人要求页面中的元素。

private class PageCollection<TResult> : IReadOnlyList<TResult>
{
    private readonly IQueryable<Query> query;
    private readonly int pageSize;
    private readonly int pageCount;

    public PageCollection(IQueryable<TResult> query, int pageSize)
    {
        this.query = query;
        this.pageSize = pageSize;
        int totalCount = query.Count();
        this.pageCount = totalCount / pageSize;
    }

    public int PageSize => this.pageSize;
    public int PageCount => this.pageCount;
    public IPage<TResult> GetPage(int pageNr)
    {
        if (pageNr >= this.PageCount)
        {
             return null; // consider to return empty page
        }

        return new Page<TResult>
        {
            PageCount = this.PageCount,
            PageNr = this.PageNr,
            PageSize = this.PageSize,

            Query = this.Query.Skip(this.PageSize * pageNr).Take(this.PageSize),
        }
    }
}

通过PageCount和GetPage实现IReadOnlyList和IReadOnlyCollection

注意:计算我需要获取Count() 的页数。这是一个相当便宜的操作,不会经常执行。

如果您真的根本不想执行任何操作,请考虑实现IReadOnlyCollection&lt;TResult&gt; 而不是IReadOnlyList&lt;TResult&gt;。缺点:你不能问:“给我页面[4]”

要创建 PageCollection,您需要使用返回页面集合的方法扩展 IQueryable&lt;TResult&gt;,而不执行查询。如果你对扩展方法不熟悉,请参阅extension methods demystified

public IReadOnlyList<IPage<TResult>> ToPageCollection<TResult>(
    this IQueryable<TResult> query,
    int pageSize)
{
    // TODO: decide what to do if query == null. Exception? return empty collection?
    // TODO: what if pageSize <= 0? What if PageSize is too large?

    return new PageCollection<TResult>(query, pageSize);
}

用法:

int pageSize = this.DataGridView.GetVisibleRows.Count;
var oldCustomerPages = dbContext.Customers
    .Where(customer => customer.Birthday.Year <= 1980)
    .ToPageCollection(pageSize);

注意:只有query.Count() 被执行了!这比获取客户快得多。

int pageNrToShow = Int32.Parse(this.textBoxPageNr.Text);
var pageToShow = oldCustomerPages[pageNrToShow];

仍然没有获取数据!

foreach (var customer in pageToShow)
{
    ...
}

这将最终执行查询。

获取下一页和上一页的成本很低,因为它不会执行查询。

var pageCollection = ...
IPage<Customer> currentPage;

IPage<Customer> NextPage => pageCollection[currentPage.PageNr+1];
IPage<Customer> PreviousPage => pageCollection[currentPage.PageNr-1];

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-02-03
    • 2011-11-13
    • 2014-02-11
    • 1970-01-01
    • 2012-06-12
    • 1970-01-01
    • 2013-11-26
    相关资源
    最近更新 更多