Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,39 @@ public virtual async Task<IList<Product>> GetProductsByIds(string[] productIds,
if (productIds == null || productIds.Length == 0)
return new List<Product>();

var products = new List<Product>();
foreach (var id in productIds)
var products = new Dictionary<string, Product>();
var missing = new List<string>();

foreach (var id in productIds.Distinct())
{
var product = await GetProductById(id);
if (product != null && (showHidden || (_aclService.Authorize(product, _contextAccessor.WorkContext.CurrentCustomer) &&
_aclService.Authorize(product, _contextAccessor.StoreContext.CurrentStore.Id) &&
product.IsAvailable())))
products.Add(product);
if (_cacheBase.TryGetValue(string.Format(CacheKey.PRODUCTS_BY_ID_KEY, id), out Product cached))
products[id] = cached;
else
missing.Add(id);
}

return products;
if (missing.Count > 0)
{
var query = _productRepository.Table.Where(product => missing.Contains(product.Id));
var fromDb = (await _productRepository.ToListAsync(query)).ToDictionary(product => product.Id);

foreach (var id in missing)
{
//cache the miss as well, same as GetProductById does
fromDb.TryGetValue(id, out var product);
await _cacheBase.SetAsync(string.Format(CacheKey.PRODUCTS_BY_ID_KEY, id), () => Task.FromResult(product));
products[id] = product;
}
}

//keep the order of the identifiers given - recently viewed products rely on it
return productIds
.Select(id => products[id])
.Where(product => product != null && (showHidden ||
(_aclService.Authorize(product, _contextAccessor.WorkContext.CurrentCustomer) &&
_aclService.Authorize(product, _contextAccessor.StoreContext.CurrentStore.Id) &&
product.IsAvailable())))
.ToList();
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Core/Grand.Infrastructure/Caching/ICacheBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Grand.Infrastructure.Caching;
/// </summary>
public interface ICacheBase
{
bool TryGetValue<T>(string key, out T value);
T Get<T>(string key, Func<T> acquire);
T Get<T>(string key, Func<T> acquire, int cacheTime);
Task<T> GetAsync<T>(string key, Func<Task<T>> acquire);
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public MemoryCacheBase(IMemoryCache cache, IMediator mediator, CacheConfig cache

#region Methods

public virtual bool TryGetValue<T>(string key, out T value)
{
return _cache.TryGetValue(key, out value);
}

public virtual T Get<T>(string key, Func<T> acquire)
{
return Get(key, acquire, _cacheConfig.DefaultCacheTimeMinutes);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using Grand.Business.Catalog.Services.Products;
using Grand.Business.Common.Services.Security;
using Grand.Data;
using Grand.Domain.Catalog;
using Grand.Domain.Customers;
using Grand.Domain.Stores;
using Grand.Infrastructure;
using Grand.Infrastructure.Caching;
using Grand.Infrastructure.Configuration;
using Grand.Infrastructure.Tests.Caching;
using Grand.Mediator;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Grand.Business.Catalog.Tests.Services.Products;

[TestClass]
public class ProductServiceBatchTests
{
private readonly List<Product> _products = [
new() { Id = "1", Published = true, VisibleIndividually = true },
new() { Id = "2", Published = true, VisibleIndividually = true },
new() { Id = "3", Published = true, VisibleIndividually = true }
];

private MemoryCacheBase _cacheBase;
private ProductService _productService;
private List<string[]> _queries;

[TestInitialize]
public void InitializeTests()
{
_queries = [];
var repository = new Mock<IRepository<Product>>();
repository.Setup(x => x.Table).Returns(() => _products.AsQueryable());
repository.Setup(x => x.ToListAsync(It.IsAny<IQueryable<Product>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IQueryable<Product> query, CancellationToken _) =>
{
var result = query.ToList();
_queries.Add(result.Select(x => x.Id).ToArray());
return result;
});

var customer = new Customer { Id = "customer" };
var contextAccessor = new Mock<IContextAccessor>();
contextAccessor.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "store" });
contextAccessor.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer);
var mediator = new Mock<IMediator>();
_cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), mediator.Object,
new CacheConfig { DefaultCacheTimeMinutes = 1 });
_productService = new ProductService(_cacheBase, repository.Object, contextAccessor.Object,
mediator.Object, new AclService(new AccessControlConfig()));
}

[TestMethod]
public async Task ColdCache_ReadsEveryProductInOneQuery()
{
var result = await _productService.GetProductsByIds(["1", "2", "3"], true);

Assert.HasCount(3, result);
Assert.HasCount(1, _queries);
}

[TestMethod]
public async Task WarmCache_DoesNotQuery()
{
await _productService.GetProductsByIds(["1", "2", "3"], true);

var result = await _productService.GetProductsByIds(["1", "2", "3"], true);

Assert.HasCount(3, result);
Assert.HasCount(1, _queries);
}

[TestMethod]
public async Task PartlyWarmCache_QueriesOnlyTheMissingProducts()
{
await _productService.GetProductsByIds(["1"], true);

var result = await _productService.GetProductsByIds(["1", "2", "3"], true);

Assert.HasCount(3, result);
Assert.HasCount(2, _queries);
CollectionAssert.AreEquivalent(new[] { "2", "3" }, _queries[1]);
}

[TestMethod]
public async Task MissingProduct_IsCachedAndNotQueriedAgain()
{
await _productService.GetProductsByIds(["missing"], true);

var result = await _productService.GetProductsByIds(["missing"], true);

Assert.IsEmpty(result);
Assert.HasCount(1, _queries);
}

[TestMethod]
public async Task SharesCacheEntriesWithGetProductById()
{
await _productService.GetProductsByIds(["1"], true);
_products.Clear();

Assert.IsNotNull(await _productService.GetProductById("1"));
}

[TestMethod]
public async Task KeepsTheOrderOfTheIdentifiersGiven()
{
var result = await _productService.GetProductsByIds(["3", "1", "2"], true);

CollectionAssert.AreEqual(new[] { "3", "1", "2" }, result.Select(x => x.Id).ToArray());
}

[TestMethod]
public async Task SkipsAnIdentifierThatMatchesNothing()
{
var result = await _productService.GetProductsByIds(["1", "missing", "2"], true);

CollectionAssert.AreEqual(new[] { "1", "2" }, result.Select(x => x.Id).ToArray());
}

[TestMethod]
public async Task RepeatsAProductWhoseIdentifierRepeats()
{
var result = await _productService.GetProductsByIds(["1", "1"], true);

CollectionAssert.AreEqual(new[] { "1", "1" }, result.Select(x => x.Id).ToArray());
Assert.HasCount(1, _queries);
}

[TestMethod]
public async Task ReturnsNothingForAnEmptyRequest()
{
Assert.IsEmpty(await _productService.GetProductsByIds([], true));
Assert.IsEmpty(_queries);
}
}
Loading