diff --git a/GrossesMitaines/GrossesMitainesAPI/Controllers/InventoryController.cs b/GrossesMitaines/GrossesMitainesAPI/Controllers/InventoryController.cs index 274fd81..166d4f9 100644 --- a/GrossesMitaines/GrossesMitainesAPI/Controllers/InventoryController.cs +++ b/GrossesMitaines/GrossesMitainesAPI/Controllers/InventoryController.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +namespace GrossesMitainesAPI.Controllers; + +#region Dependencies +using Microsoft.AspNetCore.Mvc; using GrossesMitainesAPI.Models; using System.Linq; using GrossesMitainesAPI.Data; @@ -7,30 +10,41 @@ using Microsoft.AspNetCore.Cors; using GrossesMitainesAPI.Services; using Microsoft.AspNetCore.Authorization; -namespace GrossesMitainesAPI.Controllers; +#endregion [EnableCors("_myAllowSpecificOrigins"), ApiController, Route("api/[controller]")] public class InventoryController : Controller { + #region Constants + private const int AMOUNT_SCROLL = 5; + + #endregion + + #region DI Fields private readonly ILogger _logger; private readonly InventoryContext _context; private readonly DatabaseCacheService _cache; - private const int AMOUNT_SCROLL = 5; + #endregion + + #region Ctor public InventoryController(ILogger logger, InventoryContext context, DatabaseCacheService cache) { _context = context; _logger = logger; _cache = cache; } + #endregion + + #region API Methods [EnableCors("_myAllowSpecificOrigins"), HttpGet(Name = "Inventory"), AllowAnonymous] // Pour faire des calls async par paquet de AMOUNT (5) (pour du loading en scrollant) public IEnumerable Get(int? lastId, string? order, string? filterPrice, string? filterState, bool? all) { bool iscache = false; - IQueryable ret; + IQueryable ret; if (_cache.isOk()) { - ret = _cache.queryCache(); + ret = _cache.queryCache().Select(x => new ProductViewModel(x)); iscache = true; } - else ret = _context.Products.AsQueryable(); + else ret = _context.Products.AsQueryable().Select(x => new ProductViewModel(x)); switch (filterPrice) { case "PriceUnder20": ret = ret.Where(x => x.Price < 20); @@ -63,16 +77,21 @@ public class InventoryController : Controller { ret = ret.Where(x => x.Status == Product.States.Discontinued); break; case "isPromoted": - ret = ret.Where(x => x.Status == Product.States.Clearance || x.Status == Product.States.Promotion); + ret = ret.Where(x => x.Status == Product.States.Clearance || + x.Status == Product.States.Promotion); break; default: break; } switch (order) { case "Price": - ret = ret.OrderBy(x => x.Status == Product.States.Promotion || x.Status == Product.States.Clearance ? x.PromoPrice : x.Price); + ret = ret.OrderBy(x => x.Status == Product.States.Promotion || + x.Status == Product.States.Clearance ? + x.PromoPrice : x.Price); break; case "PriceDesc": - ret = ret.OrderByDescending(x => x.Status == Product.States.Promotion || x.Status == Product.States.Clearance ? x.PromoPrice : x.Price); + ret = ret.OrderByDescending(x => x.Status == Product.States.Promotion || + x.Status == Product.States.Clearance ? + x.PromoPrice : x.Price); break; case "Title": ret = ret.OrderBy(x => x.Title); @@ -95,9 +114,9 @@ public class InventoryController : Controller { if (!lastId.HasValue || lastId == 0) yup = true; - foreach (Product prod in ret.ToList()) { + foreach (ProductViewModel prod in ret.ToList()) { if (yup && add < AMOUNT_SCROLL || (all.HasValue && all == true)) { - lst.Add(new ProductViewModel(prod)); + lst.Add(prod); add++; } if (!yup && prod.Id == lastId) @@ -143,5 +162,7 @@ public class InventoryController : Controller { _cache.askForRefresh(); return rid; } + + #endregion } diff --git a/GrossesMitaines/GrossesMitainesAPI/Controllers/ProductController.cs b/GrossesMitaines/GrossesMitainesAPI/Controllers/ProductController.cs index 01e36d0..fcc01d2 100644 --- a/GrossesMitaines/GrossesMitainesAPI/Controllers/ProductController.cs +++ b/GrossesMitaines/GrossesMitainesAPI/Controllers/ProductController.cs @@ -1,4 +1,7 @@ -using GrossesMitainesAPI.Models; +namespace GrossesMitainesAPI.Controllers; + +#region Dependencies +using GrossesMitainesAPI.Models; using System.Linq; using GrossesMitainesAPI.Data; using Microsoft.Extensions.Logging; @@ -8,7 +11,8 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using GrossesMitainesAPI.Services; -namespace GrossesMitainesAPI.Controllers; +#endregion + /// /// Ce contrôleur ne va pas chercher dans la cache, /// mais les changements dans celui-ci entrainera @@ -18,16 +22,23 @@ namespace GrossesMitainesAPI.Controllers; /// [EnableCors("_myAllowSpecificOrigins"), ApiController, Route("api/[controller]")] public class ProductController : ControllerBase { + #region DI Fields private readonly ILogger _logger; private readonly InventoryContext _context; private readonly DatabaseCacheService _cache; + #endregion + + #region Ctor public ProductController(ILogger logger, InventoryContext context, DatabaseCacheService cache) { _logger = logger; _context = context; _cache = cache; } + #endregion + + #region API Methods [EnableCors("_myAllowSpecificOrigins"), HttpGet(Name = "Product"), AllowAnonymous] public ActionResult Get(int id) { Product prod; @@ -85,4 +96,6 @@ public class ProductController : ControllerBase { _cache.askForRefresh(); return id; } + + #endregion } \ No newline at end of file diff --git a/GrossesMitaines/GrossesMitainesAPI/Controllers/SearchController.cs b/GrossesMitaines/GrossesMitainesAPI/Controllers/SearchController.cs index f94c71e..1a98775 100644 --- a/GrossesMitaines/GrossesMitainesAPI/Controllers/SearchController.cs +++ b/GrossesMitaines/GrossesMitainesAPI/Controllers/SearchController.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +namespace GrossesMitainesAPI.Controllers; + +#region Dependencies +using Microsoft.AspNetCore.Mvc; using GrossesMitainesAPI.Models; using System.Linq; using GrossesMitainesAPI.Data; @@ -8,118 +11,153 @@ using Microsoft.AspNetCore.Cors; using GrossesMitainesAPI.Services; using System.Collections.Immutable; -namespace GrossesMitainesAPI.Controllers; +#endregion [EnableCors("_myAllowSpecificOrigins"), ApiController, Route("api/[controller]")] public class SearchController : Controller { + #region Constants + private const int PREVIEW = 4; + + #endregion + + #region DI Fields private readonly ILogger _logger; private readonly InventoryContext _context; private readonly DatabaseCacheService _cache; - private Product[]? _searchCache = null; - private const int PREVIEW = 4; + private IQueryable? _searchCache = null; + #endregion + + #region Ctor public SearchController(ILogger logger, InventoryContext context, DatabaseCacheService cache) { _logger = logger; _context = context; _cache = cache; - if (_cache.isOk()) // Se fait une copie de la cache si elle est fonctionnelle. - _searchCache = _cache.GetCacheCopy(); + if (_cache.isOk()) + _searchCache = _cache.queryCache(); } + #endregion + + #region API Methods [EnableCors("_myAllowSpecificOrigins"), HttpGet(Name = "Search"), AllowAnonymous] - public IEnumerable Get(string query, bool? preview, bool? deep) { - if (_searchCache is not null) - return SearchCached(query, preview, deep); - else return SearchDirect(query, preview, deep); + public IEnumerable Get(string query, bool? preview, string? filterPrice, string? filterState, string? order = "Relevance") { + if (preview.HasValue && preview == true) + return _searchCache is null ? + _context.Products.Where(x => x.Title.Contains(query)).Take(PREVIEW).Select(x => new ProductViewModel(x)).ToList() : + _searchCache.Where(x => x.Title.Contains(query)).Take(PREVIEW).Select(x => new ProductViewModel(x)).ToList(); + return Search(query, filterPrice, filterState, order); } - private List SearchDirect(string query, bool? preview, bool? deep) { - List products = new(); + #endregion + + #region Private Methods + private List Search(string query, string? filterPrice, string? filterState, string? order) { + List products = new(); query = query.Trim(); - try { // Pour faire une liste priorisée. - if (preview.HasValue && preview == true) - products = _context.Products.Where(x => x.Title.Contains(query)).Take(PREVIEW).ToList(); - else { - if (deep.HasValue && deep == true) { - List title = new(), desc = new(), cat = new(); - query = query.ToLower(); - foreach (Product prod in _context.Products.ToArray()) { + try { + query = query.ToLower(); + IQueryable ret; + if (_searchCache is null) { + _logger.LogError(8, "Erreur de cache."); + ret = _context.Products.AsQueryable().Select(x => new ProductViewModel(x)); + } else ret = _searchCache.Select(x => new ProductViewModel(x)); + + switch (filterPrice) { + case "PriceUnder20": + ret = ret.Where(x => x.Price < 20); + break; + case "Price20to49": + ret = ret.Where(x => x.Price >= 20 && x.Price < 50); + break; + case "Price50to99": + ret = ret.Where(x => x.Price >= 50 && x.Price < 100); + break; + case "PriceOver100": + ret = ret.Where(x => x.Price >= 100); + break; + default: break; + } + switch (filterState) { + case "isAvailable": + ret = ret.Where(x => x.Status == Product.States.Available); + break; + case "isUnavailable": + ret = ret.Where(x => x.Status == Product.States.Unavailable); + break; + case "isBackOrder": + ret = ret.Where(x => x.Status == Product.States.BackOrder); + break; ; + case "isClearance": + ret = ret.Where(x => x.Status == Product.States.Clearance); + break; + case "isDiscontinued": + ret = ret.Where(x => x.Status == Product.States.Discontinued); + break; + case "isPromoted": + ret = ret.Where(x => x.Status == Product.States.Clearance || x.Status == Product.States.Promotion); + break; + default: break; + } + + switch (order) { + case "Relevance": + List title = new(), desc = new(), cat = new(); + foreach (ProductViewModel prod in ret.ToList()) { string sTitle = prod.Title.Replace(",", " ").ToLower() + " ", - sCat = prod.Category.ToLower() + " ", + sCat = " " + prod.Category.ToLower() + " ", sDesc = prod.Description.Replace(".", " ").Replace(",", " ").ToLower() + " "; if (sTitle.StartsWith(query)) products.Add(prod); else if (sTitle.Contains(" " + query + " ")) title.Add(prod); - else if (sDesc.StartsWith(query) || sDesc.Contains(" " + query + " ")) + else if (sDesc.Contains(" " + query + " ")) desc.Add(prod); - else if (sCat.Contains(query)) + else if (sCat.Contains(" " + query + " ")) cat.Add(prod); } products.AddRange(title); products.AddRange(desc); products.AddRange(cat); - } else { - products = _context.Products.Where(x => x.Title.Contains(query)).ToList(); - foreach (Product prod in _context.Products.Where(x => x.Description.Contains(query)).ToArray()) - if (!products.Contains(prod)) + break; + case "Price": + ret = ret.OrderBy(x => x.Status == Product.States.Promotion || x.Status == Product.States.Clearance ? x.PromoPrice : x.Price); + goto default; + case "PriceDesc": + ret = ret.OrderByDescending(x => x.Status == Product.States.Promotion || x.Status == Product.States.Clearance ? x.PromoPrice : x.Price); + goto default; + case "Title": + ret = ret.OrderBy(x => x.Title); + goto default; + case "TitleDesc": + ret = ret.OrderByDescending(x => x.Title); + goto default; + case "Category": + ret = ret.OrderBy(x => x.Category); + goto default; + case "CategoryDesc": + ret = ret.OrderByDescending(x => x.Category); + goto default; + default: + foreach (ProductViewModel prod in ret.ToList()) { + string sTitle = prod.Title.Replace(",", " ").ToLower() + " ", + sCat = " " + prod.Category.ToLower() + " ", + sDesc = prod.Description.Replace(".", " ").Replace(",", " ").ToLower() + " "; + if (sTitle.StartsWith(query) + || sTitle.Contains(" " + query + " ") + || sDesc.StartsWith(query) || sDesc.Contains(" " + query + " ") + || sCat.Contains(" " + query + " ")) products.Add(prod); - foreach (Product prod in _context.Products.Where(x => x.Category.Contains(query)).ToArray()) - if (!products.Contains(prod)) - products.Add(prod); - } + } + break; } } catch (Exception e) { _logger.LogError(8, e.Message); + return new List(); } return products; } - private List SearchCached(string query, bool? preview, bool? deep) { - List products = new(); - query = query.Trim(); - if (_searchCache is null) { - _logger.LogError(8, "Erreur de cache."); - return SearchDirect(query, preview, deep); // Fallback vers version non-cached en cas d'erreur. - } - try { // Pour faire une liste priorisée. - if (preview.HasValue && preview == true) - products = _searchCache.Where(x => x.Title.Contains(query)).Take(PREVIEW).ToList(); - else { - if (deep.HasValue && deep == true) { - List title = new(), desc = new(), cat = new(); - query = query.ToLower(); - foreach (Product prod in _searchCache) { - string sTitle = prod.Title.Replace(",", " ").ToLower() + " ", - sCat = prod.Category.ToLower() + " ", - sDesc = prod.Description.Replace(".", " ").Replace(",", " ").ToLower() + " "; - if (sTitle.StartsWith(query)) - products.Add(prod); - else if (sTitle.Contains(" " + query + " ")) - title.Add(prod); - else if (sDesc.StartsWith(query) || sDesc.Contains(" " + query + " ")) - desc.Add(prod); - else if (sCat.Contains(query)) - cat.Add(prod); - } - products.AddRange(title); - products.AddRange(desc); - products.AddRange(cat); - } else { - products = _searchCache.Where(x => x.Title.Contains(query)).ToList(); - foreach (Product prod in _searchCache.Where(x => x.Description.Contains(query)).ToArray()) - if (!products.Contains(prod)) - products.Add(prod); - foreach (Product prod in _searchCache.Where(x => x.Category.Contains(query)).ToArray()) - if (!products.Contains(prod)) - products.Add(prod); - } - } - } catch (Exception e) { - _logger.LogError(8, e.Message); - return SearchDirect(query, preview, deep); // Fallback vers version non-cached en cas d'erreur. - } - return products; - } + #endregion } diff --git a/GrossesMitaines/GrossesMitainesAPI/Services/DatabaseCacheService.cs b/GrossesMitaines/GrossesMitainesAPI/Services/DatabaseCacheService.cs index 39dc74d..1b0a2dc 100644 --- a/GrossesMitaines/GrossesMitainesAPI/Services/DatabaseCacheService.cs +++ b/GrossesMitaines/GrossesMitainesAPI/Services/DatabaseCacheService.cs @@ -1,117 +1,127 @@ -using GrossesMitainesAPI.Data; +namespace GrossesMitainesAPI.Services; + +#region Dependencies +using GrossesMitainesAPI.Data; using GrossesMitainesAPI.Models; using Microsoft.EntityFrameworkCore; -namespace GrossesMitainesAPI.Services { - public class DatabaseCacheService { - private readonly IServiceScopeFactory _contextFactory; // https://entityframeworkcore.com/knowledge-base/51939451/how-to-use-a-database-context-in-a-singleton-service- - private readonly ILogger _logger; +#endregion - private Product[] _cache = new Product[1]; - private Dictionary _hits = new(); - private bool _ok = false, _needUpd = true; - private PeriodicTimer _timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); +public class DatabaseCacheService { + #region DI + private readonly IServiceScopeFactory _contextFactory; // https://entityframeworkcore.com/knowledge-base/51939451/how-to-use-a-database-context-in-a-singleton-service- + private readonly ILogger _logger; - public DatabaseCacheService(ILogger logger, IServiceScopeFactory scopeFactory) { - _contextFactory = scopeFactory; - _logger = logger; - _ok = UpdateCache(); - _needUpd = !_ok; - UpdateJob(); - } + #endregion - private async void UpdateJob() { - while (await _timer.WaitForNextTickAsync()) { - if (_needUpd) { - _ok = UpdateCache(); - _needUpd = !_ok; - } - if (_hits.Count > 0 && _ok) { - UpdateMetrics(); - //_needUpd = true; - } - } - } - private bool UpdateCache() { - try { - Product[] prods; - using (var scope = _contextFactory.CreateScope()) { - var db = scope.ServiceProvider.GetRequiredService(); - prods = db.Products.ToArray(); - } - lock (_cache) { - _cache = prods; - } - } catch (Exception e) { - _logger.LogError(e, "Erreur de mise à jour de cache."); - return false; - } - return true; - } - - private bool UpdateMetrics() { - try { - Dictionary hits; - lock (_hits) { - hits = new(_hits); - _hits.Clear(); - } - List ids = hits.Keys.ToList(); - using (var scope = _contextFactory.CreateScope()) { - var db = scope.ServiceProvider.GetRequiredService(); - List lst = db.Products.Where(x => ids.Contains((uint)x.Id)).ToList(); - foreach (var x in hits) { - //Product prod = lst.First(x => x.Id == x.Id); - lst.First(x => x.Id == x.Id).Hits += x.Value; - // prod.Hits = prod.Hits + x.Value; - // db.Products.Update(prod); - } - db.UpdateRange(lst); - db.SaveChanges(); - } - } catch (Exception e) { - _logger.LogError(e, "Erreur de mise à jour de cache."); - return false; - } - return true; - } - - public bool isOk() { return _ok; } - public void askForRefresh() { _needUpd = true; } - public void addHit(uint id) { - lock (_hits) { - if (_hits.ContainsKey(id)) - _hits[id] = _hits[id] + 1; - else _hits[id] = 1; - } - } - - public Product[]? GetCacheCopy() { - if (!_ok) - return null; - - Product[] copy; - try { - lock (_cache) { - copy = new Product[_cache.Length]; - _cache.CopyTo(copy, 0); - } - } catch (Exception e) { - _logger.LogError(e, "Erreur de copie de cache."); - return null; - } - return copy; - } - - public IQueryable queryCache() { - if (!_ok) - return null; - try { - return _cache.AsQueryable(); - } catch (Exception e) { - _logger.LogError(e, "Erreur de cache."); - return null; + #region Fields + private Product[] _cache = new Product[1]; + private Dictionary _hits = new(); + private bool _ok = false, _needUpd = true; + private PeriodicTimer _timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + + #endregion + + #region Ctor + public DatabaseCacheService(ILogger logger, IServiceScopeFactory scopeFactory) { + _contextFactory = scopeFactory; + _logger = logger; + _ok = UpdateCache(); + _needUpd = !_ok; + UpdateJob(); + } + + #endregion + + #region Internal Methods + private async void UpdateJob() { + while (await _timer.WaitForNextTickAsync()) { + if (_needUpd) { + _ok = UpdateCache(); + _needUpd = !_ok; } + if (_hits.Count > 0 && _ok) + UpdateMetrics(); // les updates de metrics ne déclencheront pas d'update de cache + // puisque les clients ne voient pas les métriques. } } -} + private bool UpdateCache() { + try { + Product[] prods; + using (var scope = _contextFactory.CreateScope()) { + var db = scope.ServiceProvider.GetRequiredService(); + prods = db.Products.ToArray(); + } + lock (_cache) { + _cache = prods; + } + } catch (Exception e) { + _logger.LogError(e, "Erreur de mise à jour de cache."); + return false; + } + return true; + } + private bool UpdateMetrics() { + try { + Dictionary hits; + lock (_hits) { + hits = new(_hits); + _hits.Clear(); + } + List ids = hits.Keys.ToList(); + using (var scope = _contextFactory.CreateScope()) { + var db = scope.ServiceProvider.GetRequiredService(); + List lst = db.Products.Where(x => ids.Contains((uint)x.Id)).ToList(); + foreach (var x in hits) + lst.First(x => x.Id == x.Id).Hits += x.Value; + db.UpdateRange(lst); + db.SaveChanges(); + } + } catch (Exception e) { + _logger.LogError(e, "Erreur de mise à jour de cache."); + return false; + } + return true; + } + + #endregion + + #region Public Methods + public bool isOk() { return _ok; } + public void askForRefresh() { _needUpd = true; } + public void addHit(uint id) { + lock (_hits) { + if (_hits.ContainsKey(id)) + _hits[id] = _hits[id] + 1; + else _hits[id] = 1; + } + } + + public Product[]? GetCacheCopy() { + if (!_ok) + return null; + Product[] copy; + try { + lock (_cache) { + copy = new Product[_cache.Length]; + _cache.CopyTo(copy, 0); + } + } catch (Exception e) { + _logger.LogError(e, "Erreur de copie de cache."); + return null; + } + return copy; + } + public IQueryable queryCache() { + if (!_ok) + return null; + try { + return _cache.AsQueryable(); + } catch (Exception e) { + _logger.LogError(e, "Erreur de cache."); + return null; + } + } + + #endregion +} \ No newline at end of file