Add category browsing and product filtering with breadcrumbs
This commit is contained in:
45
src/FrontOffice.Main/Utilities/CategoryService.cs
Normal file
45
src/FrontOffice.Main/Utilities/CategoryService.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FrontOffice.BFF.Category.Protobuf.Protos.Category;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
public sealed record CategoryItem(long Id, string Title, long? ParentId, string? ImagePath, bool IsActive);
|
||||
|
||||
public class CategoryService
|
||||
{
|
||||
private readonly CategoryContract.CategoryContractClient _client;
|
||||
private List<CategoryItem>? _cache;
|
||||
|
||||
public CategoryService(CategoryContract.CategoryContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<List<CategoryItem>> GetAllAsync(bool forceReload = false)
|
||||
{
|
||||
if (!forceReload && _cache is { Count: > 0 })
|
||||
{
|
||||
return _cache;
|
||||
}
|
||||
|
||||
var response = await _client.GetAllCategoriesAsync(new GetCategoriesRequest());
|
||||
_cache = response.Categories
|
||||
.Select(dto => new CategoryItem(
|
||||
Id: dto.Id,
|
||||
Title: dto.Title ?? dto.Name ?? string.Empty,
|
||||
ParentId: dto.ParentId,
|
||||
ImagePath: dto.ImagePath,
|
||||
IsActive: dto.IsActive))
|
||||
.ToList();
|
||||
|
||||
return _cache;
|
||||
}
|
||||
|
||||
public async Task<CategoryItem?> GetByIdAsync(long id)
|
||||
{
|
||||
var categories = await GetAllAsync();
|
||||
return categories.FirstOrDefault(c => c.Id == id);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using FrontOffice.BFF.Products.Protobuf.Protos.Products;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
@@ -46,7 +49,8 @@ public record ProductCategoryPathInfo(
|
||||
|
||||
public class ProductService
|
||||
{
|
||||
private readonly ConcurrentDictionary<long, Product> _cache = new();
|
||||
private readonly ConcurrentDictionary<long, CacheEntry> _cache = new();
|
||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(1);
|
||||
private readonly ProductsContract.ProductsContractClient _client;
|
||||
|
||||
public ProductService(ProductsContract.ProductsContractClient client)
|
||||
@@ -54,11 +58,11 @@ public class ProductService
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<List<Product>> GetProductsAsync(string? query = null)
|
||||
public async Task<List<Product>> GetProductsAsync(string? query = null, long? categoryId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resp = await _client.GetAllProductsByFilterAsync(new GetAllProductsByFilterRequest
|
||||
var request = new GetAllProductsByFilterRequest
|
||||
{
|
||||
Filter = new GetAllProductsByFilterFilter
|
||||
{
|
||||
@@ -67,12 +71,19 @@ public class ProductService
|
||||
ShortInfomation = query ?? string.Empty,
|
||||
FullInformation = query ?? string.Empty
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (categoryId is { } value)
|
||||
{
|
||||
request.Filter.CategoryId = value ;
|
||||
}
|
||||
|
||||
var resp = await _client.GetAllProductsByFilterAsync(request);
|
||||
return MapAndCache(resp.Models);
|
||||
}
|
||||
catch
|
||||
{
|
||||
IEnumerable<Product> list = _cache.Values;
|
||||
IEnumerable<Product> list = GetValidCachedProducts();
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
var q = query.Trim();
|
||||
@@ -87,7 +98,7 @@ public class ProductService
|
||||
|
||||
public async Task<Product?> GetByIdAsync(long id)
|
||||
{
|
||||
if (_cache.TryGetValue(id, out var cached)) return cached;
|
||||
if (TryGetCachedProduct(id, out var cached)) return cached;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -101,7 +112,7 @@ public class ProductService
|
||||
}
|
||||
catch
|
||||
{
|
||||
_cache.TryGetValue(id, out var result);
|
||||
TryGetCachedProduct(id, out var result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -123,9 +134,7 @@ public class ProductService
|
||||
Rate: m.Rate,
|
||||
RemainingCount: m.RemainingCount
|
||||
);
|
||||
if (_cache.All(a => a.Key != p.Id))
|
||||
_cache[p.Id] = p;
|
||||
|
||||
CacheProduct(p);
|
||||
|
||||
list.Add(p);
|
||||
}
|
||||
@@ -159,10 +168,42 @@ public class ProductService
|
||||
Categories = MapCategoryPaths(model.Categories)
|
||||
};
|
||||
|
||||
_cache[product.Id] = product;
|
||||
CacheProduct(product);
|
||||
return product;
|
||||
}
|
||||
|
||||
private void CacheProduct(Product product)
|
||||
{
|
||||
var entry = new CacheEntry(product, DateTime.UtcNow.Add(CacheDuration));
|
||||
_cache.AddOrUpdate(product.Id, entry, (_, _) => entry);
|
||||
}
|
||||
|
||||
private bool TryGetCachedProduct(long id, [NotNullWhen(true)] out Product? product)
|
||||
{
|
||||
if (_cache.TryGetValue(id, out var entry))
|
||||
{
|
||||
if (entry.Expiration > DateTime.UtcNow)
|
||||
{
|
||||
product = entry.Product;
|
||||
return true;
|
||||
}
|
||||
|
||||
_cache.TryRemove(id, out _);
|
||||
}
|
||||
|
||||
product = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<Product> GetValidCachedProducts()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return _cache.Values
|
||||
.Where(entry => entry.Expiration > now)
|
||||
.Select(entry => entry.Product);
|
||||
}
|
||||
|
||||
private sealed record CacheEntry(Product Product, DateTime Expiration);
|
||||
private static string BuildUrl(string? path) =>
|
||||
string.IsNullOrWhiteSpace(path) ? string.Empty : UrlUtility.DownloadUrl + path;
|
||||
|
||||
|
||||
@@ -55,5 +55,6 @@ public static class RouteConstants
|
||||
public const string CheckoutSummary = "/checkout-summary";
|
||||
public const string Orders = "/orders";
|
||||
public const string OrderDetail = "/order/"; // usage: /order/{id}
|
||||
public const string Categories = "/categories";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user