diff --git a/.ai/knowledge/async.md b/.ai/knowledge/async.md index 7c69a6808c..dc03fdcf7f 100644 --- a/.ai/knowledge/async.md +++ b/.ai/knowledge/async.md @@ -63,16 +63,35 @@ var result = await _repository.GetByIdAsync(id); `ValueTask` avoids allocation for code that often returns a cached result without awaiting. Do not retrofit existing `Task`-returning code unless profiling shows allocation is a problem. -### Avoid `.ToList()` inside async lambdas passed to cache +### Never execute a query built on `Table` synchronously -`IQueryable.ToList()` is synchronous and blocks. Use LINQ's `ToList()` only on in-memory sequences, or use repository methods that return `Task>`. +`Table` is an `IQueryable` over the MongoDB driver. `ToList()`, `Count()`, `First()` and plain +enumeration on it each issue a **blocking** round trip and hold a thread pool thread for its +duration. Being MongoDB rather than EF does not make it synchronous-safe — it makes it a network +call with no async on that path unless you ask for one. + +Execute through the repository instead. `MongoRepository` runs the query on the driver's +asynchronous API. Hand it a sequence you already materialised and it throws — that is deliberate, +because quietly enumerating it would be the very pattern this rule exists to remove. ```csharp -// This is fine — the LINQ query is materialised synchronously here -// because Table is IQueryable against MongoDB, not EF +// wrong — blocks a thread for the whole round trip var productIds = query.Take(request.ProductsNumber).ToList(); +var total = query.Count(); +return new PagedList(query, pageIndex, pageSize); + +// correct +var productIds = await _productRepository.ToListAsync(query.Take(request.ProductsNumber)); +var total = await _productRepository.CountAsync(query); +return await _productRepository.PagedAsync(query, pageIndex, pageSize); ``` +The same applies inside a lambda passed to `ICacheBase.GetAsync` — make the lambda `async` and +await the repository, rather than returning `Task.FromResult(query.ToList())`. Blocking there is +worse than elsewhere, because the cache holds a lock while the acquire function runs. + +`ToList()` on a sequence that is already in memory is fine and needs no repository call. + ### Run independent operations concurrently with `Task.WhenAll` When two or more async operations are independent, start all of them before awaiting: diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetDiscountUsageHistoryQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetDiscountUsageHistoryQueryHandler.cs index d4c2302456..d35ee09739 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetDiscountUsageHistoryQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetDiscountUsageHistoryQueryHandler.cs @@ -33,6 +33,6 @@ public async Task> Handle(GetDiscountUsageHisto query = query.Where(duh => duh.Canceled == request.Canceled.Value); query = query.OrderByDescending(c => c.CreatedOnUtc); - return await PagedList.Create(query, request.PageIndex, request.PageSize); + return await _discountUsageHistoryRepository.PagedAsync(query, request.PageIndex, request.PageSize); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSearchProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSearchProductsQueryHandler.cs index 7326acf50e..d7aaeac939 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSearchProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSearchProductsQueryHandler.cs @@ -51,7 +51,7 @@ public GetSearchProductsQueryHandler( query = OrderByQueryable(request, query); // Create paged list - var products = await PagedList.Create(query, request.PageIndex, request.PageSize); + var products = await _productRepository.PagedAsync(query, request.PageIndex, request.PageSize); // Get filterable specification attributes if needed if (ShouldLoadFilterableSpecifications(request)) diff --git a/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs b/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs index 7e12b8a4d0..5b98c661d2 100644 --- a/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs @@ -91,7 +91,7 @@ public virtual async Task> GetAllBrands(string brandName, } query = query.OrderBy(m => m.DisplayOrder).ThenBy(m => m.Name); - return await PagedList.Create(query, pageIndex, pageSize); + return await _brandRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs b/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs index 92dcb5cc87..880e315b3e 100644 --- a/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs @@ -111,7 +111,7 @@ public virtual async Task> GetAllCategories(string parentId query = query.OrderBy(c => c.DisplayOrder).ThenBy(c => c.Name); //pagination - return await Task.FromResult(new PagedList(query, pageIndex, pageSize)); + return await _categoryRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs b/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs index 5411f405cc..e29adae7e2 100644 --- a/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs @@ -51,7 +51,7 @@ public virtual async Task> GetProductCategoriesByCa var key = string.Format(CacheKey.PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, showHidden, categoryId, pageIndex, pageSize, string.Join(",", _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds()), _contextAccessor.StoreContext.CurrentStore.Id); - return await _cacheBase.GetAsync(key, () => + return await _cacheBase.GetAsync(key, async () => { var query = _productRepository.Table.Where(x => x.ProductCategories.Any(y => y.CategoryId == categoryId)); @@ -91,7 +91,7 @@ from pc in prod.ProductCategories orderby pm.DisplayOrder select pm; - return Task.FromResult(new PagedList(queryProductCategories, pageIndex, pageSize)); + return await _productRepository.PagedAsync(queryProductCategories, pageIndex, pageSize); }); } diff --git a/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs b/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs index 0175e0f78f..2019df1608 100644 --- a/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs @@ -95,7 +95,7 @@ public virtual async Task> GetAllCollections(string colle } query = query.OrderBy(m => m.DisplayOrder).ThenBy(m => m.Name); - return await PagedList.Create(query, pageIndex, pageSize); + return await _collectionRepository.PagedAsync(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs b/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs index 528fe3310c..3f88c0651a 100644 --- a/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs @@ -46,7 +46,7 @@ public virtual async Task> GetProductCollectionsB { var key = string.Format(CacheKey.PRODUCTCOLLECTIONS_ALLBYCOLLECTIONID_KEY, showHidden, collectionId, pageIndex, pageSize, string.Join(",", _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds()), storeId); - return await _cacheBase.GetAsync(key, () => + return await _cacheBase.GetAsync(key, async () => { var query = _productRepository.Table.Where(x => x.ProductCollections.Any(y => y.CollectionId == collectionId)); @@ -84,7 +84,7 @@ from pm in prod.ProductCollections orderby pm.DisplayOrder select pm; - return Task.FromResult(new PagedList(queryProductCollection, pageIndex, pageSize)); + return await _productRepository.PagedAsync(queryProductCollection, pageIndex, pageSize); }); } diff --git a/src/Business/Grand.Business.Catalog/Services/Directory/SearchTermService.cs b/src/Business/Grand.Business.Catalog/Services/Directory/SearchTermService.cs index 7f79b3a21a..2fa55f7ca2 100644 --- a/src/Business/Grand.Business.Catalog/Services/Directory/SearchTermService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Directory/SearchTermService.cs @@ -94,7 +94,7 @@ into groupedResult Keyword = r.Keyword, Count = r.Count }); - return await PagedList.Create(query, pageIndex, pageSize); + return await _searchTermRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Catalog/Services/Discounts/DiscountService.cs b/src/Business/Grand.Business.Catalog/Services/Discounts/DiscountService.cs index c337856984..3fc47a86fd 100644 --- a/src/Business/Grand.Business.Catalog/Services/Discounts/DiscountService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Discounts/DiscountService.cs @@ -203,7 +203,7 @@ public virtual async Task> GetAllCouponCodesByDiscoun query = query.Where(duh => duh.DiscountId == discountId); query = query.OrderByDescending(c => c.CouponCode); - return await PagedList.Create(query, pageIndex, pageSize); + return await _discountCouponRepository.PagedAsync(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Catalog/Services/Products/AuctionService.cs b/src/Business/Grand.Business.Catalog/Services/Products/AuctionService.cs index 5ddd03063e..ad81bacfab 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/AuctionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/AuctionService.cs @@ -53,14 +53,14 @@ public virtual async Task> GetBidsByProductId(string productId, int pageSize = int.MaxValue) { var query = _bidRepository.Table.Where(x => x.ProductId == productId).OrderByDescending(x => x.Date); - return await Task.FromResult(new PagedList(query, pageIndex, pageSize)); + return await _bidRepository.PagedAsync(query, pageIndex, pageSize); } public virtual async Task> GetBidsByCustomerId(string customerId, int pageIndex = 0, int pageSize = int.MaxValue) { var query = _bidRepository.Table.Where(x => x.CustomerId == customerId); - return await Task.FromResult(new PagedList(query, pageIndex, pageSize)); + return await _bidRepository.PagedAsync(query, pageIndex, pageSize); } public virtual async Task InsertBid(Bid bid) diff --git a/src/Business/Grand.Business.Catalog/Services/Products/OutOfStockSubscriptionService.cs b/src/Business/Grand.Business.Catalog/Services/Products/OutOfStockSubscriptionService.cs index 94eaa9bf41..024342526f 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/OutOfStockSubscriptionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/OutOfStockSubscriptionService.cs @@ -58,7 +58,7 @@ public virtual async Task> GetAllSubscription query = query.OrderByDescending(x => x.CreatedOnUtc); - return await PagedList.Create(query, pageIndex, pageSize); + return await _outOfStockSubscriptionRepository.PagedAsync(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeService.cs index e35ebfe4ec..5345523913 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeService.cs @@ -61,7 +61,7 @@ public virtual async Task> GetAllProductAttributes( int pageSize = int.MaxValue) { var key = string.Format(CacheKey.PRODUCTATTRIBUTES_ALL_KEY, storeId, pageIndex, pageSize); - return await _cacheBase.GetAsync(key, () => + return await _cacheBase.GetAsync(key, async () => { var query = from pa in _productAttributeRepository.Table select pa; @@ -74,7 +74,7 @@ public virtual async Task> GetAllProductAttributes( query = query.OrderBy(pa => pa.Name); - return Task.FromResult(new PagedList(query, pageIndex, pageSize)); + return await _productAttributeRepository.PagedAsync(query, pageIndex, pageSize); }); diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductReservationService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductReservationService.cs index c2e3e2d932..9948cfeef9 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductReservationService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductReservationService.cs @@ -56,7 +56,7 @@ public virtual async Task> GetProductReservations } query = query.OrderBy(x => x.Date); - return await PagedList.Create(query, pageIndex, pageSize); + return await _productReservationRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductReviewService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductReviewService.cs index 432521fa1e..90803d9560 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductReviewService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductReviewService.cs @@ -60,7 +60,7 @@ public virtual async Task> GetAllProductReviews(string query = query.OrderByDescending(c => c.CreatedOnUtc); - return await PagedList.Create(query, pageIndex, pageSize); + return await _productReviewRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs index a72285354f..18e2e17a1e 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs @@ -153,7 +153,7 @@ public virtual async Task> GetProductsByDiscount(string disc where c.AppliedDiscounts.Any(x => x == discountId) select c; - return await PagedList.Create(query, pageIndex, pageSize); + return await _productRepository.PagedAsync(query, pageIndex, pageSize); } @@ -577,7 +577,7 @@ public virtual async Task> GetProductsByProductAttributeId(s query = query.OrderBy(x => x.Name); - return await PagedList.Create(query, pageIndex, pageSize); + return await _productRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Catalog/Services/Products/SpecificationAttributeService.cs b/src/Business/Grand.Business.Catalog/Services/Products/SpecificationAttributeService.cs index e2b1f8359e..cebaed7b8d 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/SpecificationAttributeService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/SpecificationAttributeService.cs @@ -98,7 +98,7 @@ public virtual async Task> GetSpecificationAt query = query.OrderBy(sa => sa.DisplayOrder).ThenBy(sa => sa.Name); - return await PagedList.Create(query, pageIndex, pageSize); + return await _specificationAttributeRepository.PagedAsync(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Checkout/Services/GiftVouchers/GiftVoucherService.cs b/src/Business/Grand.Business.Checkout/Services/GiftVouchers/GiftVoucherService.cs index 74e9793169..7951491ce0 100644 --- a/src/Business/Grand.Business.Checkout/Services/GiftVouchers/GiftVoucherService.cs +++ b/src/Business/Grand.Business.Checkout/Services/GiftVouchers/GiftVoucherService.cs @@ -79,7 +79,7 @@ public virtual async Task> GetAllGiftVouchers(string pur }; var query = await _mediator.Send(model); - return await PagedList.Create(query, pageIndex, pageSize); + return await _giftVoucherRepository.PagedAsync(query, pageIndex, pageSize); } public virtual async Task> GetAllGiftVoucherUsageHistory(string orderId = "") diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/MerchandiseReturnService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/MerchandiseReturnService.cs index 9c424c5832..22e95ddcf1 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/MerchandiseReturnService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/MerchandiseReturnService.cs @@ -111,7 +111,7 @@ public virtual async Task> SearchMerchandiseReturn }; var query = await _mediator.Send(model); - return await PagedList.Create(query, pageIndex, pageSize); + return await _merchandiseReturnRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/OrderReportService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/OrderReportService.cs index 4278f4bc9a..a27a190bb9 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/OrderReportService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/OrderReportService.cs @@ -560,7 +560,7 @@ orderby p.Name (showHidden || p.Published) select p; - return await PagedList.Create(qproducts, pageIndex, pageSize); + return await _productRepository.PagedAsync(qproducts, pageIndex, pageSize); } public class OrderStats diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/OrderService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/OrderService.cs index 7635f9941e..068f65c03e 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/OrderService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/OrderService.cs @@ -197,7 +197,7 @@ public virtual async Task> SearchOrders(string storeId = "", SalesEmployeeId = salesEmployeeId }; var query = await _mediator.Send(queryModel); - return await PagedList.Create(query, pageIndex, pageSize); + return await _orderRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Checkout/Services/Payments/PaymentTransactionService.cs b/src/Business/Grand.Business.Checkout/Services/Payments/PaymentTransactionService.cs index 3b97018ff8..29f150a66f 100644 --- a/src/Business/Grand.Business.Checkout/Services/Payments/PaymentTransactionService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Payments/PaymentTransactionService.cs @@ -149,7 +149,7 @@ public virtual async Task> SearchPaymentTransacti }; var query = await _mediator.Send(model); - return await PagedList.Create(query, pageIndex, pageSize); + return await _repositoryPaymentTransaction.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Checkout/Services/Shipping/DeliveryDateService.cs b/src/Business/Grand.Business.Checkout/Services/Shipping/DeliveryDateService.cs index 158739e31c..0abff69048 100644 --- a/src/Business/Grand.Business.Checkout/Services/Shipping/DeliveryDateService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Shipping/DeliveryDateService.cs @@ -64,7 +64,7 @@ public virtual async Task> GetAllDeliveryDates(string s query = query.Where(dd => dd.StoreId == storeId); query = query.OrderBy(dd => dd.DisplayOrder); - return await PagedList.Create(query, pageIndex, pageSize); + return await _deliveryDateRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Checkout/Services/Shipping/PickupPointService.cs b/src/Business/Grand.Business.Checkout/Services/Shipping/PickupPointService.cs index a006b4d313..914ed43b55 100644 --- a/src/Business/Grand.Business.Checkout/Services/Shipping/PickupPointService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Shipping/PickupPointService.cs @@ -64,7 +64,7 @@ public virtual async Task> GetAllPickupPoints(string sto query = query.Where(pp => pp.StoreId == storeId); query = query.OrderBy(pp => pp.DisplayOrder); - return await PagedList.Create(query, pageIndex, pageSize); + return await _pickupPointsRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Checkout/Services/Shipping/ShipmentService.cs b/src/Business/Grand.Business.Checkout/Services/Shipping/ShipmentService.cs index 6f2f81c64c..31d994e22a 100644 --- a/src/Business/Grand.Business.Checkout/Services/Shipping/ShipmentService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Shipping/ShipmentService.cs @@ -85,7 +85,7 @@ public virtual async Task> GetAllShipments(string storeId = query = query.Where(s => createdToUtc.Value >= s.CreatedOnUtc); query = query.OrderByDescending(x => x.CreatedOnUtc); - var shipments = await PagedList.Create(query, pageIndex, pageSize); + var shipments = await _shipmentRepository.PagedAsync(query, pageIndex, pageSize); return shipments; } diff --git a/src/Business/Grand.Business.Checkout/Services/Shipping/WarehouseService.cs b/src/Business/Grand.Business.Checkout/Services/Shipping/WarehouseService.cs index a720c13e9e..188a3d1507 100644 --- a/src/Business/Grand.Business.Checkout/Services/Shipping/WarehouseService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Shipping/WarehouseService.cs @@ -64,7 +64,7 @@ public virtual async Task> GetAllWarehouses(string storeId query = query.Where(wh => wh.StoreId == storeId); query = query.OrderBy(wh => wh.DisplayOrder); - return await PagedList.Create(query, pageIndex, pageSize); + return await _warehouseRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Cms/Services/BlogService.cs b/src/Business/Grand.Business.Cms/Services/BlogService.cs index 2562d5844d..647f757201 100644 --- a/src/Business/Grand.Business.Cms/Services/BlogService.cs +++ b/src/Business/Grand.Business.Cms/Services/BlogService.cs @@ -110,7 +110,7 @@ public virtual async Task> GetAllBlogPosts(string storeId = query = query.OrderByDescending(b => b.CreatedOnUtc); - return await PagedList.Create(query, pageIndex, pageSize); + return await _blogPostRepository.PagedAsync(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Cms/Services/NewsService.cs b/src/Business/Grand.Business.Cms/Services/NewsService.cs index 435b10b889..4abd31bd24 100644 --- a/src/Business/Grand.Business.Cms/Services/NewsService.cs +++ b/src/Business/Grand.Business.Cms/Services/NewsService.cs @@ -97,7 +97,7 @@ public virtual async Task> GetAllNews(string storeId = "", } query = query.OrderByDescending(n => n.CreatedOnUtc); - return await PagedList.Create(query, pageIndex, pageSize); + return await _newsItemRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Common/Services/Directory/GroupService.cs b/src/Business/Grand.Business.Common/Services/Directory/GroupService.cs index b36cdebc13..d95393ca8e 100644 --- a/src/Business/Grand.Business.Common/Services/Directory/GroupService.cs +++ b/src/Business/Grand.Business.Common/Services/Directory/GroupService.cs @@ -76,7 +76,7 @@ public virtual async Task> GetAllCustomerGroups(string query = query.OrderBy(m => m.DisplayOrder).ThenBy(m => m.Name); - return await PagedList.Create(query, pageIndex, pageSize); + return await _customerGroupRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Common/Services/Seo/SlugService.cs b/src/Business/Grand.Business.Common/Services/Seo/SlugService.cs index 626fd62231..7d0909c378 100644 --- a/src/Business/Grand.Business.Common/Services/Seo/SlugService.cs +++ b/src/Business/Grand.Business.Common/Services/Seo/SlugService.cs @@ -148,7 +148,7 @@ public virtual async Task> GetAllEntityUrl(string slug = " query = query.Where(ur => ur.IsActive == active.Value); query = query.OrderBy(ur => ur.Slug); - return await PagedList.Create(query, pageIndex, pageSize); + return await _urlEntityRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Core/Interfaces/Storage/IPictureService.cs b/src/Business/Grand.Business.Core/Interfaces/Storage/IPictureService.cs index cee91a5d7e..3e82f876bb 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Storage/IPictureService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Storage/IPictureService.cs @@ -113,7 +113,7 @@ Task GetPictureUrl(Picture picture, /// Current page /// Items on each page /// Paged list of pictures - IPagedList GetPictures(int pageIndex = 0, int pageSize = int.MaxValue); + Task> GetPictures(int pageIndex = 0, int pageSize = int.MaxValue); /// /// Inserts a picture diff --git a/src/Business/Grand.Business.Core/Interfaces/System/Reports/ICustomerReportService.cs b/src/Business/Grand.Business.Core/Interfaces/System/Reports/ICustomerReportService.cs index 58bc556a5d..7f92afe4f5 100644 --- a/src/Business/Grand.Business.Core/Interfaces/System/Reports/ICustomerReportService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/System/Reports/ICustomerReportService.cs @@ -24,7 +24,7 @@ public interface ICustomerReportService /// Page index /// Page size /// Report - IPagedList GetBestCustomersReport(string storeId = "", string vendorId = "", + Task> GetBestCustomersReport(string storeId = "", string vendorId = "", DateTime? createdFromUtc = null, DateTime? createdToUtc = null, int? os = null, PaymentStatus? ps = null, ShippingStatus? ss = null, int orderBy = 0, diff --git a/src/Business/Grand.Business.Customers/Services/AffiliateService.cs b/src/Business/Grand.Business.Customers/Services/AffiliateService.cs index 326b778b48..3ed7cc2c0b 100644 --- a/src/Business/Grand.Business.Customers/Services/AffiliateService.cs +++ b/src/Business/Grand.Business.Customers/Services/AffiliateService.cs @@ -108,7 +108,7 @@ public virtual async Task> GetAllAffiliates(string friendl query = query.OrderByDescending(a => a.Id); - return await PagedList.Create(query, pageIndex, pageSize); + return await _affiliateRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Customers/Services/CustomerReportService.cs b/src/Business/Grand.Business.Customers/Services/CustomerReportService.cs index 1f0c740e43..fe752a202d 100644 --- a/src/Business/Grand.Business.Customers/Services/CustomerReportService.cs +++ b/src/Business/Grand.Business.Customers/Services/CustomerReportService.cs @@ -62,7 +62,7 @@ public CustomerReportService(IRepository customerRepository, /// Page index /// Page size /// Report - public virtual IPagedList GetBestCustomersReport(string storeId = "", string vendorId = "", + public virtual async Task> GetBestCustomersReport(string storeId = "", string vendorId = "", DateTime? createdFromUtc = null, DateTime? createdToUtc = null, int? os = null, PaymentStatus? ps = null, ShippingStatus? ss = null, int orderBy = 0, @@ -101,7 +101,7 @@ into g _ => throw new ArgumentException("Wrong orderBy parameter", nameof(orderBy)) }; - var tmp = new PagedList(query2, pageIndex, pageSize); + var tmp = await _orderRepository.PagedAsync(query2, pageIndex, pageSize); return new PagedList(tmp.Select(x => new BestCustomerReportLine { CustomerId = x.CustomerId, OrderTotal = x.OrderTotal, @@ -135,7 +135,7 @@ into g _ => throw new ArgumentException("Wrong orderBy parameter", nameof(orderBy)) }; - var vendorReport = new PagedList(vendorQueryGroup, pageIndex, pageSize); + var vendorReport = await _orderRepository.PagedAsync(vendorQueryGroup, pageIndex, pageSize); return new PagedList(vendorReport.Select(x => new BestCustomerReportLine { CustomerId = x.CustomerId, OrderTotal = x.OrderTotal, diff --git a/src/Business/Grand.Business.Customers/Services/CustomerService.cs b/src/Business/Grand.Business.Customers/Services/CustomerService.cs index b1a7ea782c..7f31bd4998 100644 --- a/src/Business/Grand.Business.Customers/Services/CustomerService.cs +++ b/src/Business/Grand.Business.Customers/Services/CustomerService.cs @@ -114,7 +114,7 @@ public virtual async Task> GetAllCustomers(DateTime? create OrderBySelector = orderBySelector }; var query = await _mediator.Send(queryModel); - return await PagedList.Create(query, pageIndex, pageSize); + return await _customerRepository.PagedAsync(query, pageIndex, pageSize); } /// @@ -150,7 +150,7 @@ public virtual async Task> GetOnlineCustomers(DateTime last query = query.Where(c => c.SeId == salesEmployeeId); query = query.OrderByDescending(c => c.LastActivityDateUtc); - return await PagedList.Create(query, pageIndex, pageSize); + return await _customerRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Customers/Services/UserApiService.cs b/src/Business/Grand.Business.Customers/Services/UserApiService.cs index 73678e9655..6e40a2fd01 100644 --- a/src/Business/Grand.Business.Customers/Services/UserApiService.cs +++ b/src/Business/Grand.Business.Customers/Services/UserApiService.cs @@ -84,7 +84,7 @@ public virtual async Task> GetUsers(string email = "", int p if (!string.IsNullOrEmpty(email)) query = query.Where(x => x.Email.Contains(email.ToLowerInvariant())); - return await PagedList.Create(query, pageIndex, pageSize); + return await _userRepository.PagedAsync(query, pageIndex, pageSize); } #region Fields diff --git a/src/Business/Grand.Business.Customers/Services/VendorService.cs b/src/Business/Grand.Business.Customers/Services/VendorService.cs index 616cceb604..4000ace73e 100644 --- a/src/Business/Grand.Business.Customers/Services/VendorService.cs +++ b/src/Business/Grand.Business.Customers/Services/VendorService.cs @@ -76,7 +76,7 @@ public virtual async Task> GetAllVendors(string name = "", query = query.Where(v => v.Active); query = query.Where(v => !v.Deleted); query = query.OrderBy(v => v.DisplayOrder).ThenBy(v => v.Name); - return await PagedList.Create(query, pageIndex, pageSize); + return await _vendorRepository.PagedAsync(query, pageIndex, pageSize); } /// @@ -218,7 +218,7 @@ public virtual async Task> GetAllVendorReviews(string c if (!string.IsNullOrEmpty(vendorId)) query = query.Where(c => c.VendorId == vendorId); query = query.OrderByDescending(c => c.CreatedOnUtc); - return await PagedList.Create(query, pageIndex, pageSize); + return await _vendorReviewRepository.PagedAsync(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Marketing/Services/Campaigns/CampaignService.cs b/src/Business/Grand.Business.Marketing/Services/Campaigns/CampaignService.cs index e8eeb4ed8c..145b945c3f 100644 --- a/src/Business/Grand.Business.Marketing/Services/Campaigns/CampaignService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Campaigns/CampaignService.cs @@ -111,7 +111,7 @@ public virtual async Task> GetCampaignHistory(Campai where c.CampaignId == campaign.Id orderby c.CreatedDateUtc descending select c; - return await PagedList.Create(query, pageIndex, pageSize); + return await campaignHistoryRepository.PagedAsync(query, pageIndex, pageSize); } public virtual async Task> CustomerSubscriptions(Campaign campaign, @@ -119,7 +119,7 @@ public virtual async Task> CustomerSubscripti { ArgumentNullException.ThrowIfNull(campaign); - PagedList model; + IPagedList model; if (campaign.CustomerCreatedDateFrom.HasValue || campaign.CustomerCreatedDateTo.HasValue || campaign.CustomerHasShoppingCart is not (CampaignCondition.All and CampaignCondition.All) || campaign.CustomerLastActivityDateFrom.HasValue || campaign.CustomerLastActivityDateTo.HasValue || @@ -197,7 +197,7 @@ from customers in joined if (campaign.NewsletterCategories.Count > 0) foreach (var item in campaign.NewsletterCategories) query = query.Where(x => x.NewsletterCategories.Contains(item)); - model = await PagedList.Create( + model = await newsLetterSubscriptionRepository.PagedAsync( query.Select(x => new NewsLetterSubscription { CustomerId = x.CustomerId, Email = x.Email, NewsLetterSubscriptionGuid = x.NewsLetterSubscriptionGuid @@ -212,7 +212,7 @@ from customers in joined if (campaign.NewsletterCategories.Count > 0) foreach (var item in campaign.NewsletterCategories) query = query.Where(x => x.Categories.Contains(item)); - model = await PagedList.Create(query, pageIndex, pageSize); + model = await newsLetterSubscriptionRepository.PagedAsync(query, pageIndex, pageSize); } return await Task.FromResult(model); diff --git a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactUsService.cs b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactUsService.cs index 301b00e488..dc858f5d10 100644 --- a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactUsService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactUsService.cs @@ -79,7 +79,7 @@ public virtual async Task> GetAllContactUs(DateTime? fromU query = query.Where(l => l.Email.ToLower().Contains(email.ToLower())); query = query.OrderByDescending(x => x.CreatedOnUtc); - var contactus = await PagedList.Create(query, pageIndex, pageSize); + var contactus = await _contactusRepository.PagedAsync(query, pageIndex, pageSize); return contactus; } diff --git a/src/Business/Grand.Business.Marketing/Services/Courses/CourseService.cs b/src/Business/Grand.Business.Marketing/Services/Courses/CourseService.cs index bc2c6db226..d7761566b8 100644 --- a/src/Business/Grand.Business.Marketing/Services/Courses/CourseService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Courses/CourseService.cs @@ -45,7 +45,7 @@ public virtual async Task> GetAll(int pageIndex = 0, int page orderby q.DisplayOrder select q; - return await PagedList.Create(query, pageIndex, pageSize); + return await _courseRepository.PagedAsync(query, pageIndex, pageSize); } public virtual async Task> GetByCustomer(Customer customer, string storeId) diff --git a/src/Business/Grand.Business.Marketing/Services/Customers/CustomerProductService.cs b/src/Business/Grand.Business.Marketing/Services/Customers/CustomerProductService.cs index a61e431d96..c2cedfdd06 100644 --- a/src/Business/Grand.Business.Marketing/Services/Customers/CustomerProductService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Customers/CustomerProductService.cs @@ -126,7 +126,7 @@ public virtual async Task> GetProductsPriceByCu var query = from pp in _customerProductPriceRepository.Table where pp.CustomerId == customerId select pp; - return await PagedList.Create(query, pageIndex, pageSize); + return await _customerProductPriceRepository.PagedAsync(query, pageIndex, pageSize); } #endregion @@ -219,7 +219,7 @@ public virtual async Task> GetProductsByCustomer(str where pp.CustomerId == customerId orderby pp.DisplayOrder select pp; - return await PagedList.Create(query, pageIndex, pageSize); + return await _customerProductRepository.PagedAsync(query, pageIndex, pageSize); } #endregion diff --git a/src/Business/Grand.Business.Marketing/Services/Customers/CustomerTagService.cs b/src/Business/Grand.Business.Marketing/Services/Customers/CustomerTagService.cs index 52be576d23..fd223925e5 100644 --- a/src/Business/Grand.Business.Marketing/Services/Customers/CustomerTagService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Customers/CustomerTagService.cs @@ -45,7 +45,7 @@ public virtual async Task> GetCustomersByTag(string custome var query = from c in _customerRepository.Table where c.CustomerTags.Contains(customerTagId) select c; - return await PagedList.Create(query, pageIndex, pageSize); + return await _customerRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Marketing/Services/Documents/DocumentService.cs b/src/Business/Grand.Business.Marketing/Services/Documents/DocumentService.cs index 2bfae3c4d9..41d769a6e1 100644 --- a/src/Business/Grand.Business.Marketing/Services/Documents/DocumentService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Documents/DocumentService.cs @@ -61,7 +61,7 @@ public virtual async Task> GetAll(string name = "", string if (status >= 0) query = query.Where(d => d.StatusId == (DocumentStatus)status); - return await PagedList.Create(query, pageIndex, pageSize); + return await _documentRepository.PagedAsync(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Marketing/Services/Newsletters/NewsLetterSubscriptionService.cs b/src/Business/Grand.Business.Marketing/Services/Newsletters/NewsLetterSubscriptionService.cs index 1b2dc9704b..852911480a 100644 --- a/src/Business/Grand.Business.Marketing/Services/Newsletters/NewsLetterSubscriptionService.cs +++ b/src/Business/Grand.Business.Marketing/Services/Newsletters/NewsLetterSubscriptionService.cs @@ -238,7 +238,7 @@ public virtual async Task> GetAllNewsLetterSu query = query.Where(c => c.Categories.Any(x => categoryIds.Contains(x))); query = query.OrderBy(nls => nls.Email); - return await PagedList.Create(query, pageIndex, pageSize); + return await _subscriptionRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Marketing/Services/PushNotifications/PushNotificationsService.cs b/src/Business/Grand.Business.Marketing/Services/PushNotifications/PushNotificationsService.cs index 9821c1f577..dce379018a 100644 --- a/src/Business/Grand.Business.Marketing/Services/PushNotifications/PushNotificationsService.cs +++ b/src/Business/Grand.Business.Marketing/Services/PushNotifications/PushNotificationsService.cs @@ -116,7 +116,7 @@ public virtual async Task InsertPushMessage(PushMessage message) public virtual async Task> GetPushMessages(int pageIndex = 0, int pageSize = int.MaxValue) { var query = _pushMessagesRepository.Table.OrderByDescending(x => x.SentOn); - return await Task.FromResult(new PagedList(query, pageIndex, pageSize)); + return await _pushMessagesRepository.PagedAsync(query, pageIndex, pageSize); } /// @@ -126,7 +126,7 @@ public virtual async Task> GetPushReceivers(int pag int pageSize = int.MaxValue) { var query = _pushRegistrationRepository.Table.OrderByDescending(x => x.RegisteredOn); - return await Task.FromResult(new PagedList(query, pageIndex, pageSize)); + return await _pushRegistrationRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Messages/Services/EmailAccountService.cs b/src/Business/Grand.Business.Messages/Services/EmailAccountService.cs index 6b1a4a5759..4007053ba6 100644 --- a/src/Business/Grand.Business.Messages/Services/EmailAccountService.cs +++ b/src/Business/Grand.Business.Messages/Services/EmailAccountService.cs @@ -148,6 +148,6 @@ public virtual async Task> GetAllEmailAccounts(string s if (!string.IsNullOrEmpty(storeId)) query = query.Where(ea => ea.StoreId == storeId); - return await PagedList.Create(query, pageIndex, pageSize); + return await _emailAccountRepository.PagedAsync(query, pageIndex, pageSize); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs index 3467c6e51e..150c5f6d39 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs @@ -161,7 +161,7 @@ public virtual async Task> GetAllMessageTemplates(st query = query.OrderBy(t => t.Name); - return await PagedList.Create(query, pageIndex, pageSize); + return await _messageTemplateRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Messages/Services/QueuedEmailService.cs b/src/Business/Grand.Business.Messages/Services/QueuedEmailService.cs index a49a003c95..99bc1a2e2c 100644 --- a/src/Business/Grand.Business.Messages/Services/QueuedEmailService.cs +++ b/src/Business/Grand.Business.Messages/Services/QueuedEmailService.cs @@ -173,7 +173,7 @@ public virtual async Task> SearchEmails(string fromEmail : //load by priority query.OrderByDescending(qe => qe.PriorityId).ThenBy(qe => qe.CreatedOnUtc); - return await PagedList.Create(query, pageIndex, pageSize); + return await _queuedEmailRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Business/Grand.Business.Storage/Services/PictureService.cs b/src/Business/Grand.Business.Storage/Services/PictureService.cs index 2524e0f059..62d9a073fa 100644 --- a/src/Business/Grand.Business.Storage/Services/PictureService.cs +++ b/src/Business/Grand.Business.Storage/Services/PictureService.cs @@ -528,12 +528,11 @@ public virtual async Task ClearThumbs() /// Current page /// Items on each page /// Paged list of pictures - public virtual IPagedList GetPictures(int pageIndex = 0, int pageSize = int.MaxValue) + public virtual async Task> GetPictures(int pageIndex = 0, int pageSize = int.MaxValue) { var query = from p in _pictureRepository.Table select p; - var pictures = new PagedList(query, pageIndex, pageSize); - return pictures; + return await _pictureRepository.PagedAsync(query, pageIndex, pageSize); } /// diff --git a/src/Core/Grand.Data/IRepository.cs b/src/Core/Grand.Data/IRepository.cs index d692cf6bea..ccf97b0a4d 100644 --- a/src/Core/Grand.Data/IRepository.cs +++ b/src/Core/Grand.Data/IRepository.cs @@ -158,5 +158,33 @@ Task UpdateCollectionFieldItem(string id, Expression>> /// /// Gets a table collection /// - IQueryable TableCollection() where C : class; + IQueryable TableCollection() where C : class; + + /// + /// Executes the query and returns its results + /// + /// Type of the query result - the entity itself or a projection + /// Query built on top of or + /// Cancellation token + Task> ToListAsync(IQueryable query, + CancellationToken cancellationToken = default); + + /// + /// Executes the query and returns the number of matching documents + /// + /// Type of the query result - the entity itself or a projection + /// Query built on top of or + /// Cancellation token + Task CountAsync(IQueryable query, CancellationToken cancellationToken = default); + + /// + /// Executes the query and returns a single page of its results + /// + /// Type of the query result - the entity itself or a projection + /// Query built on top of or + /// Zero based page index + /// Page size + /// Cancellation token + Task> PagedAsync(IQueryable query, int pageIndex, int pageSize, + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Core/Grand.Data/LiteDb/LiteDBRepository.cs b/src/Core/Grand.Data/LiteDb/LiteDBRepository.cs index 2811e81db8..2773a2d168 100644 --- a/src/Core/Grand.Data/LiteDb/LiteDBRepository.cs +++ b/src/Core/Grand.Data/LiteDb/LiteDBRepository.cs @@ -419,6 +419,54 @@ public virtual IQueryable TableCollection() where C : class #endregion + #region Query execution + + //LiteDB is an embedded database with no asynchronous API, and Table already materialises the + //collection before the query runs. These complete synchronously by design - unlike the rest of + //the codebase, Task.FromResult here is honest rather than a fake async signature. + + /// + /// Executes the query and returns its results + /// + public virtual Task> ToListAsync(IQueryable query, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return Task.FromResult>(query.ToList()); + } + + /// + /// Executes the query and returns the number of matching documents + /// + public virtual Task CountAsync(IQueryable query, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return Task.FromResult(query.Count()); + } + + /// + /// Executes the query and returns a single page of its results + /// + public virtual Task> PagedAsync(IQueryable query, int pageIndex, + int pageSize, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + //keep the same normalization the paged list applies, so the skip matches the reported page size + if (pageSize <= 0) + pageSize = 1; + + var totalCount = query.Count(); + var items = query.Skip(pageIndex * pageSize).Take(pageSize).ToList(); + + return Task.FromResult>(new PagedList(items, pageIndex, pageSize, totalCount)); + } + + #endregion + #region Helpers private static string GetName(LambdaExpression lambdaexpression) diff --git a/src/Core/Grand.Data/Mongo/MongoRepository.cs b/src/Core/Grand.Data/Mongo/MongoRepository.cs index 3c0daab9a2..69c2116a08 100644 --- a/src/Core/Grand.Data/Mongo/MongoRepository.cs +++ b/src/Core/Grand.Data/Mongo/MongoRepository.cs @@ -1,5 +1,6 @@ using Grand.Domain; using MongoDB.Driver; +using MongoDB.Driver.Linq; using System.Linq.Expressions; namespace Grand.Data.Mongo; @@ -343,4 +344,48 @@ public virtual IQueryable TableCollection() where C : class } #endregion + + #region Query execution + + /// + /// Executes the query and returns its results + /// + public virtual async Task> ToListAsync(IQueryable query, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return await query.ToListAsync(cancellationToken); + } + + /// + /// Executes the query and returns the number of matching documents + /// + public virtual async Task CountAsync(IQueryable query, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + return await query.CountAsync(cancellationToken); + } + + /// + /// Executes the query and returns a single page of its results + /// + public virtual async Task> PagedAsync(IQueryable query, int pageIndex, + int pageSize, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + //keep the same normalization the paged list applies, so the skip matches the reported page size + if (pageSize <= 0) + pageSize = 1; + + var totalCount = await CountAsync(query, cancellationToken); + var items = await ToListAsync(query.Skip(pageIndex * pageSize).Take(pageSize), cancellationToken); + + return new PagedList(items, pageIndex, pageSize, totalCount); + } + + #endregion } \ No newline at end of file diff --git a/src/Core/Grand.Domain/PagedList.cs b/src/Core/Grand.Domain/PagedList.cs index a50031d64c..28d0ab3fcd 100644 --- a/src/Core/Grand.Domain/PagedList.cs +++ b/src/Core/Grand.Domain/PagedList.cs @@ -52,32 +52,4 @@ private void Initialize(IEnumerable source, int pageIndex, int pageSize, int? AddRange(source); } - private Task InitializeAsync(IQueryable source, int pageIndex, int pageSize, int? totalCount = null) - { - ArgumentNullException.ThrowIfNull(source); - if (pageSize <= 0) - pageSize = 1; - - TotalCount = totalCount ?? source.Count(); - source = totalCount == null ? source.Skip(pageIndex * pageSize).Take(pageSize) : source; - AddRange(source); - - if (pageSize > 0) - { - TotalPages = TotalCount / pageSize; - if (TotalCount % pageSize > 0) - TotalPages++; - } - - PageSize = pageSize; - PageIndex = pageIndex; - return Task.CompletedTask; - } - - public static async Task> Create(IQueryable source, int pageIndex, int pageSize) - { - var pagelist = new PagedList(); - await pagelist.InitializeAsync(source, pageIndex, pageSize); - return pagelist; - } } \ No newline at end of file diff --git a/src/Plugins/Shipping.ByWeight/Services/ShippingByWeightService.cs b/src/Plugins/Shipping.ByWeight/Services/ShippingByWeightService.cs index 3fea1f3dfd..737616e991 100644 --- a/src/Plugins/Shipping.ByWeight/Services/ShippingByWeightService.cs +++ b/src/Plugins/Shipping.ByWeight/Services/ShippingByWeightService.cs @@ -46,12 +46,12 @@ public virtual async Task DeleteShippingByWeightRecord(ShippingByWeightRecord sh public virtual async Task> GetAll(int pageIndex = 0, int pageSize = int.MaxValue) { var key = string.Format(SHIPPINGBYWEIGHT_ALL_KEY, pageIndex, pageSize); - return await _cacheBase.GetAsync(key, () => + return await _cacheBase.GetAsync(key, async () => { var query = from sbw in _sbwRepository.Table select sbw; - return Task.FromResult(new PagedList(query, pageIndex, pageSize)); + return await _sbwRepository.PagedAsync(query, pageIndex, pageSize); }); } diff --git a/src/Plugins/Tax.CountryStateZip/Services/TaxRateService.cs b/src/Plugins/Tax.CountryStateZip/Services/TaxRateService.cs index b7922872b5..f361e32d94 100644 --- a/src/Plugins/Tax.CountryStateZip/Services/TaxRateService.cs +++ b/src/Plugins/Tax.CountryStateZip/Services/TaxRateService.cs @@ -83,7 +83,7 @@ public virtual async Task> GetAllTaxRates(string storeId = " .OrderBy(tr => tr.StoreId).ThenBy(tr => tr.CountryId).ThenBy(tr => tr.StateProvinceId) .ThenBy(tr => tr.Zip).ThenBy(tr => tr.TaxCategoryId); - return await Task.FromResult(new PagedList(ordered, pageIndex, pageSize)); + return await _taxRateRepository.PagedAsync(ordered, pageIndex, pageSize); }); } diff --git a/src/Tests/Grand.Business.Customers.Tests/Services/CustomerServiceTests.cs b/src/Tests/Grand.Business.Customers.Tests/Services/CustomerServiceTests.cs index 152491ba39..ee12c930ae 100644 --- a/src/Tests/Grand.Business.Customers.Tests/Services/CustomerServiceTests.cs +++ b/src/Tests/Grand.Business.Customers.Tests/Services/CustomerServiceTests.cs @@ -232,7 +232,7 @@ public async Task GetCustomerByEmail_NullEmail_ReturnsNull() Assert.IsNull(result); } - [DataTestMethod] + [TestMethod] [DataRow("")] [DataRow(" ")] public async Task GetCustomerByEmail_EmptyOrWhitespaceEmail_ReturnsNull(string email) diff --git a/src/Tests/Grand.Data.Tests/MongoDb/MongoQueryableContractTests.cs b/src/Tests/Grand.Data.Tests/MongoDb/MongoQueryableContractTests.cs new file mode 100644 index 0000000000..024dae9e5a --- /dev/null +++ b/src/Tests/Grand.Data.Tests/MongoDb/MongoQueryableContractTests.cs @@ -0,0 +1,85 @@ +using Grand.Data.Mongo; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver; + +namespace Grand.Data.Tests.MongoDb; + +/// +/// executes whatever query it is handed straight on the driver, with no check +/// that the query is still server side. That is only safe because every shape the services compose on top of +/// stays a driver query - projections, groupings and sub-collection +/// flattening included. These tests pin that assumption: if a driver upgrade breaks one of them, the matching +/// service starts throwing instead of quietly materialising the whole collection. +/// +[TestClass] +public class MongoQueryableContractTests +{ + private IRepository _repository; + + [TestInitialize] + public void Init() + { + _repository = new MongoDBRepositoryTest(); + } + + private static void AssertServerSide(IQueryable query) + { + Assert.IsInstanceOfType>(query); + } + + [TestMethod] + public void Table_StaysServerSide() + { + AssertServerSide(_repository.Table); + } + + [TestMethod] + public void FilterSortPage_StaysServerSide() + { + AssertServerSide(_repository.Table.Where(x => x.Count > 0).OrderBy(x => x.Name).Skip(10).Take(10)); + } + + [TestMethod] + public void ProjectionToScalar_StaysServerSide() + { + AssertServerSide(_repository.Table.Select(x => x.Name)); + } + + /// Shape used by CustomerReportService.GetBestCustomersReport. + [TestMethod] + public void GroupingToAnonymousType_StaysServerSide() + { + var grouped = from item in _repository.Table + group item by item.Name + into g + select new { Name = g.Key, Total = g.Sum(x => x.Count), Lines = g.Count() }; + + AssertServerSide(grouped.OrderByDescending(x => x.Total)); + } + + /// Shape used by ProductCategoryService and ProductCollectionService. + [TestMethod] + public void FlattenedSubCollection_StaysServerSide() + { + var flattened = from sample in _repository.Table + from category in sample.Category + select new { sample.Id, category.Name, category.DisplayOrder }; + + AssertServerSide(flattened.Where(x => x.DisplayOrder > 0).OrderBy(x => x.DisplayOrder)); + } + + /// Shape used by OrderReportService - the closure list is sent as a filter, not enumerated locally. + [TestMethod] + public void FilterAgainstInMemoryList_StaysServerSide() + { + var excluded = new List { "a", "b" }; + + AssertServerSide(_repository.Table.Where(x => !excluded.Contains(x.Id))); + } + + [TestMethod] + public void PlainLinqToObjects_IsNotServerSide() + { + Assert.IsNotInstanceOfType>(new List().AsQueryable()); + } +} diff --git a/src/Tests/Grand.Data.Tests/RepositoryPagedQueryTests.cs b/src/Tests/Grand.Data.Tests/RepositoryPagedQueryTests.cs new file mode 100644 index 0000000000..fd9ebc3b31 --- /dev/null +++ b/src/Tests/Grand.Data.Tests/RepositoryPagedQueryTests.cs @@ -0,0 +1,182 @@ +using Grand.Data.Tests.LiteDb; +using Grand.Data.Tests.MongoDb; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Data.Tests; + +/// +/// Covers the query execution primitives on . The paging arithmetic has to stay +/// identical to what the paged list produced before execution moved out of the domain project, so every +/// assertion here is on TotalCount / TotalPages / page contents rather than on how the query ran. +/// +[TestClass] +public class RepositoryPagedQueryTests +{ + private const string Mongo = "MongoDB"; + private const string LiteDb = "LiteDB"; + + private IRepository _liteDb; + private IRepository _mongoRepository; + + [TestInitialize] + public async Task Init() + { + _mongoRepository = new MongoDBRepositoryTest(); + _liteDb = new LiteDBRepositoryMock(); + + foreach (var repository in new[] { _mongoRepository, _liteDb }) + for (var i = 1; i <= 25; i++) + await repository.InsertAsync(new SampleCollection { Name = $"sample {i:00}", Count = i }); + } + + private IRepository Repository(string provider) + { + return provider == Mongo ? _mongoRepository : _liteDb; + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task PagedAsync_FirstPage_ReturnsPageAndTotals(string provider) + { + var repository = Repository(provider); + var query = repository.Table.OrderBy(x => x.Count); + + var result = await repository.PagedAsync(query, 0, 10); + + Assert.AreEqual(25, result.TotalCount); + Assert.AreEqual(3, result.TotalPages); + Assert.AreEqual(0, result.PageIndex); + Assert.AreEqual(10, result.PageSize); + Assert.AreEqual(10, result.Count); + Assert.AreEqual(1, result.First().Count); + Assert.IsFalse(result.HasPreviousPage); + Assert.IsTrue(result.HasNextPage); + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task PagedAsync_LastPartialPage_ReturnsRemainder(string provider) + { + var repository = Repository(provider); + var query = repository.Table.OrderBy(x => x.Count); + + var result = await repository.PagedAsync(query, 2, 10); + + Assert.AreEqual(25, result.TotalCount); + Assert.AreEqual(5, result.Count); + Assert.AreEqual(21, result.First().Count); + Assert.IsTrue(result.HasPreviousPage); + Assert.IsFalse(result.HasNextPage); + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task PagedAsync_PageBeyondRange_ReturnsEmptyPageWithTotals(string provider) + { + var repository = Repository(provider); + var query = repository.Table.OrderBy(x => x.Count); + + var result = await repository.PagedAsync(query, 99, 10); + + Assert.AreEqual(0, result.Count); + Assert.AreEqual(25, result.TotalCount); + Assert.AreEqual(3, result.TotalPages); + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task PagedAsync_NoMatches_ReturnsEmpty(string provider) + { + var repository = Repository(provider); + var query = repository.Table.Where(x => x.Count > 1000); + + var result = await repository.PagedAsync(query, 0, 10); + + Assert.AreEqual(0, result.Count); + Assert.AreEqual(0, result.TotalCount); + Assert.AreEqual(0, result.TotalPages); + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task PagedAsync_NonPositivePageSize_NormalizesToOne(string provider) + { + var repository = Repository(provider); + var query = repository.Table.OrderBy(x => x.Count); + + var result = await repository.PagedAsync(query, 0, 0); + + Assert.AreEqual(1, result.PageSize); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(25, result.TotalPages); + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task PagedAsync_Projection_ReturnsProjectedPage(string provider) + { + var repository = Repository(provider); + var query = repository.Table.OrderBy(x => x.Count).Select(x => x.Name); + + var result = await repository.PagedAsync(query, 0, 5); + + Assert.AreEqual(25, result.TotalCount); + Assert.AreEqual(5, result.Count); + Assert.AreEqual("sample 01", result.First()); + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task ToListAsync_ReturnsAllMatches(string provider) + { + var repository = Repository(provider); + + var result = await repository.ToListAsync(repository.Table.Where(x => x.Count <= 3)); + + Assert.AreEqual(3, result.Count); + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task CountAsync_ReturnsNumberOfMatches(string provider) + { + var repository = Repository(provider); + + Assert.AreEqual(3, await repository.CountAsync(repository.Table.Where(x => x.Count <= 3))); + } + + [TestMethod] + [DataRow(Mongo)] + [DataRow(LiteDb)] + public async Task PagedAsync_NullQuery_Throws(string provider) + { + var repository = Repository(provider); + + await Assert.ThrowsExactlyAsync(() => + repository.PagedAsync(null, 0, 10)); + } + + /// + /// Handing the Mongo repository a sequence that is already in memory is a programming error - it means the + /// caller materialised the query before the repository could run it, which is the blocking pattern these + /// methods exist to remove. It has to fail loudly rather than quietly enumerate and look like it worked. + /// + [TestMethod] + public async Task MongoRepository_InMemoryQuery_ThrowsInsteadOfEnumerating() + { + var items = Enumerable.Range(1, 25).Select(i => new SampleCollection { Count = i }).ToList(); + + await Assert.ThrowsExactlyAsync(() => + _mongoRepository.ToListAsync(items.AsQueryable().Where(x => x.Count <= 4))); + await Assert.ThrowsExactlyAsync(() => + _mongoRepository.PagedAsync(items.AsQueryable().OrderBy(x => x.Count), 0, 10)); + } +} diff --git a/src/Web/Grand.Web.Admin/Controllers/MaintenanceController.cs b/src/Web/Grand.Web.Admin/Controllers/MaintenanceController.cs index e083dc0978..2f0d826619 100644 --- a/src/Web/Grand.Web.Admin/Controllers/MaintenanceController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/MaintenanceController.cs @@ -123,7 +123,7 @@ public async Task MaintenanceConvertPicture( var numberOfConvertItems = 0; if (storageSettings.PictureStoreInDb) { - var pictures = pictureService.GetPictures(); + var pictures = await pictureService.GetPictures(); foreach (var picture in pictures) try { diff --git a/src/Web/Grand.Web.Admin/Controllers/SettingController.cs b/src/Web/Grand.Web.Admin/Controllers/SettingController.cs index 321b657be6..1fcaf63df1 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SettingController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SettingController.cs @@ -936,7 +936,7 @@ private async Task SavePictureStorage(bool storeIdDb) const int pageSize = 100; while (true) { - var pictures = pictureService.GetPictures(pageIndex, pageSize); + var pictures = await pictureService.GetPictures(pageIndex, pageSize); pageIndex++; if (!pictures.Any()) break; diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerReportViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerReportViewModelService.cs index a20517609d..72813b9e32 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerReportViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerReportViewModelService.cs @@ -122,7 +122,7 @@ public virtual async Task> GetReportReg var paymentStatus = model.PaymentStatusId > 0 ? (PaymentStatus?)model.PaymentStatusId : null; var shippingStatus = model.ShippingStatusId > 0 ? (ShippingStatus?)model.ShippingStatusId : null; - var items = _customerReportService.GetBestCustomersReport( + var items = await _customerReportService.GetBestCustomersReport( model.StoreId, createdFromUtc: startDateValue, createdToUtc: endDateValue, diff --git a/src/Web/Grand.Web.Vendor/Controllers/ReportsController.cs b/src/Web/Grand.Web.Vendor/Controllers/ReportsController.cs index 6e21336563..50fa108a10 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ReportsController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ReportsController.cs @@ -357,7 +357,7 @@ public async Task ReportBestCustomersByOrderTotalList(DataSourceR var paymentStatus = model.PaymentStatusId > 0 ? (PaymentStatus?)model.PaymentStatusId : null; - var items = _customerReportService.GetBestCustomersReport("", _contextAccessor.WorkContext.CurrentVendor.Id, startDateValue, + var items = await _customerReportService.GetBestCustomersReport("", _contextAccessor.WorkContext.CurrentVendor.Id, startDateValue, endDateValue, null, paymentStatus, null, 2, command.Page - 1, command.PageSize);