你应该自己想一想:我为什么要添加 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<TResult>。缺点:每次创建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<TResult> 而不是IReadOnlyList<TResult>。缺点:你不能问:“给我页面[4]”
要创建 PageCollection,您需要使用返回页面集合的方法扩展 IQueryable<TResult>,而不执行查询。如果你对扩展方法不熟悉,请参阅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];