diff --git a/src/Services/Catalog/Catalog.API/CatalogSettings.cs b/src/Services/Catalog/Catalog.API/CatalogSettings.cs index 297c68914..2fba164a0 100644 --- a/src/Services/Catalog/Catalog.API/CatalogSettings.cs +++ b/src/Services/Catalog/Catalog.API/CatalogSettings.cs @@ -1,13 +1,12 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API +namespace Microsoft.eShopOnContainers.Services.Catalog.API; + +public class CatalogSettings { - public class CatalogSettings - { - public string PicBaseUrl { get; set; } + public string PicBaseUrl { get; set; } - public string EventBusConnection { get; set; } + public string EventBusConnection { get; set; } - public bool UseCustomizationData { get; set; } + public bool UseCustomizationData { get; set; } - public bool AzureStorageEnabled { get; set; } - } + public bool AzureStorageEnabled { get; set; } } diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index 74a48a766..4dd1143f6 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -1,302 +1,300 @@ -using Microsoft.eShopOnContainers.Services.Catalog.API.Model; -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers; + +[Route("api/v1/[controller]")] +[ApiController] +public class CatalogController : ControllerBase { - [Route("api/v1/[controller]")] - [ApiController] - public class CatalogController : ControllerBase - { - private readonly CatalogContext _catalogContext; - private readonly CatalogSettings _settings; - private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; + private readonly CatalogContext _catalogContext; + private readonly CatalogSettings _settings; + private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; - public CatalogController(CatalogContext context, IOptionsSnapshot settings, ICatalogIntegrationEventService catalogIntegrationEventService) - { - _catalogContext = context ?? throw new ArgumentNullException(nameof(context)); - _catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); - _settings = settings.Value; + public CatalogController(CatalogContext context, IOptionsSnapshot settings, ICatalogIntegrationEventService catalogIntegrationEventService) + { + _catalogContext = context ?? throw new ArgumentNullException(nameof(context)); + _catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); + _settings = settings.Value; - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - } + context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + } - // GET api/v1/[controller]/items[?pageSize=3&pageIndex=10] - [HttpGet] - [Route("items")] - [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task ItemsAsync([FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0, string ids = null) + // GET api/v1/[controller]/items[?pageSize=3&pageIndex=10] + [HttpGet] + [Route("items")] + [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + public async Task ItemsAsync([FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0, string ids = null) + { + if (!string.IsNullOrEmpty(ids)) { - if (!string.IsNullOrEmpty(ids)) - { - var items = await GetItemsByIdsAsync(ids); - - if (!items.Any()) - { - return BadRequest("ids value invalid. Must be comma-separated list of numbers"); - } + var items = await GetItemsByIdsAsync(ids); - return Ok(items); + if (!items.Any()) + { + return BadRequest("ids value invalid. Must be comma-separated list of numbers"); } - var totalItems = await _catalogContext.CatalogItems - .LongCountAsync(); - - var itemsOnPage = await _catalogContext.CatalogItems - .OrderBy(c => c.Name) - .Skip(pageSize * pageIndex) - .Take(pageSize) - .ToListAsync(); - - /* The "awesome" fix for testing Devspaces */ - - /* - foreach (var pr in itemsOnPage) { - pr.Name = "Awesome " + pr.Name; - } + return Ok(items); + } - */ + var totalItems = await _catalogContext.CatalogItems + .LongCountAsync(); - itemsOnPage = ChangeUriPlaceholder(itemsOnPage); + var itemsOnPage = await _catalogContext.CatalogItems + .OrderBy(c => c.Name) + .Skip(pageSize * pageIndex) + .Take(pageSize) + .ToListAsync(); - var model = new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); + /* The "awesome" fix for testing Devspaces */ - return Ok(model); + /* + foreach (var pr in itemsOnPage) { + pr.Name = "Awesome " + pr.Name; } - private async Task> GetItemsByIdsAsync(string ids) - { - var numIds = ids.Split(',').Select(id => (Ok: int.TryParse(id, out int x), Value: x)); + */ - if (!numIds.All(nid => nid.Ok)) - { - return new List(); - } + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); - var idsToSelect = numIds - .Select(id => id.Value); + var model = new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); - var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); - - items = ChangeUriPlaceholder(items); + return Ok(model); + } - return items; - } + private async Task> GetItemsByIdsAsync(string ids) + { + var numIds = ids.Split(',').Select(id => (Ok: int.TryParse(id, out int x), Value: x)); - [HttpGet] - [Route("items/{id:int}")] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(CatalogItem), (int)HttpStatusCode.OK)] - public async Task> ItemByIdAsync(int id) + if (!numIds.All(nid => nid.Ok)) { - if (id <= 0) - { - return BadRequest(); - } + return new List(); + } - var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == id); + var idsToSelect = numIds + .Select(id => id.Value); - var baseUri = _settings.PicBaseUrl; - var azureStorageEnabled = _settings.AzureStorageEnabled; + var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); - item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); + items = ChangeUriPlaceholder(items); - if (item != null) - { - return item; - } + return items; + } - return NotFound(); + [HttpGet] + [Route("items/{id:int}")] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(CatalogItem), (int)HttpStatusCode.OK)] + public async Task> ItemByIdAsync(int id) + { + if (id <= 0) + { + return BadRequest(); } - // GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] - [HttpGet] - [Route("items/withname/{name:minlength(1)}")] - [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] - public async Task>> ItemsWithNameAsync(string name, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) - { - var totalItems = await _catalogContext.CatalogItems - .Where(c => c.Name.StartsWith(name)) - .LongCountAsync(); + var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == id); - var itemsOnPage = await _catalogContext.CatalogItems - .Where(c => c.Name.StartsWith(name)) - .Skip(pageSize * pageIndex) - .Take(pageSize) - .ToListAsync(); + var baseUri = _settings.PicBaseUrl; + var azureStorageEnabled = _settings.AzureStorageEnabled; - itemsOnPage = ChangeUriPlaceholder(itemsOnPage); + item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); - return new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); + if (item != null) + { + return item; } - // GET api/v1/[controller]/items/type/1/brand[?pageSize=3&pageIndex=10] - [HttpGet] - [Route("items/type/{catalogTypeId}/brand/{catalogBrandId:int?}")] - [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] - public async Task>> ItemsByTypeIdAndBrandIdAsync(int catalogTypeId, int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) - { - var root = (IQueryable)_catalogContext.CatalogItems; + return NotFound(); + } - root = root.Where(ci => ci.CatalogTypeId == catalogTypeId); + // GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] + [HttpGet] + [Route("items/withname/{name:minlength(1)}")] + [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] + public async Task>> ItemsWithNameAsync(string name, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) + { + var totalItems = await _catalogContext.CatalogItems + .Where(c => c.Name.StartsWith(name)) + .LongCountAsync(); - if (catalogBrandId.HasValue) - { - root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); - } + var itemsOnPage = await _catalogContext.CatalogItems + .Where(c => c.Name.StartsWith(name)) + .Skip(pageSize * pageIndex) + .Take(pageSize) + .ToListAsync(); - var totalItems = await root - .LongCountAsync(); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); - var itemsOnPage = await root - .Skip(pageSize * pageIndex) - .Take(pageSize) - .ToListAsync(); + return new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); + } - itemsOnPage = ChangeUriPlaceholder(itemsOnPage); + // GET api/v1/[controller]/items/type/1/brand[?pageSize=3&pageIndex=10] + [HttpGet] + [Route("items/type/{catalogTypeId}/brand/{catalogBrandId:int?}")] + [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] + public async Task>> ItemsByTypeIdAndBrandIdAsync(int catalogTypeId, int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) + { + var root = (IQueryable)_catalogContext.CatalogItems; - return new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); - } + root = root.Where(ci => ci.CatalogTypeId == catalogTypeId); - // GET api/v1/[controller]/items/type/all/brand[?pageSize=3&pageIndex=10] - [HttpGet] - [Route("items/type/all/brand/{catalogBrandId:int?}")] - [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] - public async Task>> ItemsByBrandIdAsync(int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) + if (catalogBrandId.HasValue) { - var root = (IQueryable)_catalogContext.CatalogItems; - - if (catalogBrandId.HasValue) - { - root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); - } + root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); + } - var totalItems = await root - .LongCountAsync(); + var totalItems = await root + .LongCountAsync(); - var itemsOnPage = await root - .Skip(pageSize * pageIndex) - .Take(pageSize) - .ToListAsync(); + var itemsOnPage = await root + .Skip(pageSize * pageIndex) + .Take(pageSize) + .ToListAsync(); - itemsOnPage = ChangeUriPlaceholder(itemsOnPage); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); - return new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); - } + return new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); + } - // GET api/v1/[controller]/CatalogTypes - [HttpGet] - [Route("catalogtypes")] - [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] - public async Task>> CatalogTypesAsync() - { - return await _catalogContext.CatalogTypes.ToListAsync(); - } + // GET api/v1/[controller]/items/type/all/brand[?pageSize=3&pageIndex=10] + [HttpGet] + [Route("items/type/all/brand/{catalogBrandId:int?}")] + [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] + public async Task>> ItemsByBrandIdAsync(int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) + { + var root = (IQueryable)_catalogContext.CatalogItems; - // GET api/v1/[controller]/CatalogBrands - [HttpGet] - [Route("catalogbrands")] - [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] - public async Task>> CatalogBrandsAsync() + if (catalogBrandId.HasValue) { - return await _catalogContext.CatalogBrands.ToListAsync(); + root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); } - //PUT api/v1/[controller]/items - [Route("items")] - [HttpPut] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - [ProducesResponseType((int)HttpStatusCode.Created)] - public async Task UpdateProductAsync([FromBody] CatalogItem productToUpdate) - { - var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); + var totalItems = await root + .LongCountAsync(); - if (catalogItem == null) - { - return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); - } + var itemsOnPage = await root + .Skip(pageSize * pageIndex) + .Take(pageSize) + .ToListAsync(); - var oldPrice = catalogItem.Price; - var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price; + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); - // Update current product - catalogItem = productToUpdate; - _catalogContext.CatalogItems.Update(catalogItem); + return new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); + } - if (raiseProductPriceChangedEvent) // Save product's data and publish integration event through the Event Bus if price has changed - { - //Create Integration Event to be published through the Event Bus - var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); + // GET api/v1/[controller]/CatalogTypes + [HttpGet] + [Route("catalogtypes")] + [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] + public async Task>> CatalogTypesAsync() + { + return await _catalogContext.CatalogTypes.ToListAsync(); + } - // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction - await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); + // GET api/v1/[controller]/CatalogBrands + [HttpGet] + [Route("catalogbrands")] + [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] + public async Task>> CatalogBrandsAsync() + { + return await _catalogContext.CatalogBrands.ToListAsync(); + } - // Publish through the Event Bus and mark the saved event as published - await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); - } - else // Just save the updated product because the Product's Price hasn't changed. - { - await _catalogContext.SaveChangesAsync(); - } + //PUT api/v1/[controller]/items + [Route("items")] + [HttpPut] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Created)] + public async Task UpdateProductAsync([FromBody] CatalogItem productToUpdate) + { + var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); - return CreatedAtAction(nameof(ItemByIdAsync), new { id = productToUpdate.Id }, null); + if (catalogItem == null) + { + return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); } - //POST api/v1/[controller]/items - [Route("items")] - [HttpPost] - [ProducesResponseType((int)HttpStatusCode.Created)] - public async Task CreateProductAsync([FromBody] CatalogItem product) + var oldPrice = catalogItem.Price; + var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price; + + // Update current product + catalogItem = productToUpdate; + _catalogContext.CatalogItems.Update(catalogItem); + + if (raiseProductPriceChangedEvent) // Save product's data and publish integration event through the Event Bus if price has changed { - var item = new CatalogItem - { - CatalogBrandId = product.CatalogBrandId, - CatalogTypeId = product.CatalogTypeId, - Description = product.Description, - Name = product.Name, - PictureFileName = product.PictureFileName, - Price = product.Price - }; + //Create Integration Event to be published through the Event Bus + var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); - _catalogContext.CatalogItems.Add(item); + // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction + await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); + // Publish through the Event Bus and mark the saved event as published + await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); + } + else // Just save the updated product because the Product's Price hasn't changed. + { await _catalogContext.SaveChangesAsync(); - - return CreatedAtAction(nameof(ItemByIdAsync), new { id = item.Id }, null); } - //DELETE api/v1/[controller]/id - [Route("{id}")] - [HttpDelete] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - public async Task DeleteProductAsync(int id) + return CreatedAtAction(nameof(ItemByIdAsync), new { id = productToUpdate.Id }, null); + } + + //POST api/v1/[controller]/items + [Route("items")] + [HttpPost] + [ProducesResponseType((int)HttpStatusCode.Created)] + public async Task CreateProductAsync([FromBody] CatalogItem product) + { + var item = new CatalogItem { - var product = _catalogContext.CatalogItems.SingleOrDefault(x => x.Id == id); + CatalogBrandId = product.CatalogBrandId, + CatalogTypeId = product.CatalogTypeId, + Description = product.Description, + Name = product.Name, + PictureFileName = product.PictureFileName, + Price = product.Price + }; - if (product == null) - { - return NotFound(); - } + _catalogContext.CatalogItems.Add(item); - _catalogContext.CatalogItems.Remove(product); + await _catalogContext.SaveChangesAsync(); - await _catalogContext.SaveChangesAsync(); + return CreatedAtAction(nameof(ItemByIdAsync), new { id = item.Id }, null); + } - return NoContent(); - } + //DELETE api/v1/[controller]/id + [Route("{id}")] + [HttpDelete] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task DeleteProductAsync(int id) + { + var product = _catalogContext.CatalogItems.SingleOrDefault(x => x.Id == id); - private List ChangeUriPlaceholder(List items) + if (product == null) { - var baseUri = _settings.PicBaseUrl; - var azureStorageEnabled = _settings.AzureStorageEnabled; + return NotFound(); + } - foreach (var item in items) - { - item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); - } + _catalogContext.CatalogItems.Remove(product); + + await _catalogContext.SaveChangesAsync(); + + return NoContent(); + } - return items; + private List ChangeUriPlaceholder(List items) + { + var baseUri = _settings.PicBaseUrl; + var azureStorageEnabled = _settings.AzureStorageEnabled; + + foreach (var item in items) + { + item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); } + + return items; } } diff --git a/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs b/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs index bfd73d4ac..cd86a2966 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs @@ -1,13 +1,11 @@ // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers; -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers +public class HomeController : Controller { - public class HomeController : Controller + // GET: // + public IActionResult Index() { - // GET: // - public IActionResult Index() - { - return new RedirectResult("~/swagger"); - } + return new RedirectResult("~/swagger"); } } diff --git a/src/Services/Catalog/Catalog.API/Controllers/PicController.cs b/src/Services/Catalog/Catalog.API/Controllers/PicController.cs index 3220a42f5..51ea6a7e4 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/PicController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/PicController.cs @@ -1,87 +1,86 @@ // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers; + +[ApiController] +public class PicController : ControllerBase { - [ApiController] - public class PicController : ControllerBase + private readonly IWebHostEnvironment _env; + private readonly CatalogContext _catalogContext; + + public PicController(IWebHostEnvironment env, + CatalogContext catalogContext) { - private readonly IWebHostEnvironment _env; - private readonly CatalogContext _catalogContext; + _env = env; + _catalogContext = catalogContext; + } - public PicController(IWebHostEnvironment env, - CatalogContext catalogContext) + [HttpGet] + [Route("api/v1/catalog/items/{catalogItemId:int}/pic")] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + // GET: // + public async Task GetImageAsync(int catalogItemId) + { + if (catalogItemId <= 0) { - _env = env; - _catalogContext = catalogContext; + return BadRequest(); } - [HttpGet] - [Route("api/v1/catalog/items/{catalogItemId:int}/pic")] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - // GET: // - public async Task GetImageAsync(int catalogItemId) - { - if (catalogItemId <= 0) - { - return BadRequest(); - } - - var item = await _catalogContext.CatalogItems - .SingleOrDefaultAsync(ci => ci.Id == catalogItemId); - - if (item != null) - { - var webRoot = _env.WebRootPath; - var path = Path.Combine(webRoot, item.PictureFileName); + var item = await _catalogContext.CatalogItems + .SingleOrDefaultAsync(ci => ci.Id == catalogItemId); - string imageFileExtension = Path.GetExtension(item.PictureFileName); - string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); + if (item != null) + { + var webRoot = _env.WebRootPath; + var path = Path.Combine(webRoot, item.PictureFileName); - var buffer = await System.IO.File.ReadAllBytesAsync(path); + string imageFileExtension = Path.GetExtension(item.PictureFileName); + string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); - return File(buffer, mimetype); - } + var buffer = await System.IO.File.ReadAllBytesAsync(path); - return NotFound(); + return File(buffer, mimetype); } - private string GetImageMimeTypeFromImageFileExtension(string extension) - { - string mimetype; + return NotFound(); + } - switch (extension) - { - case ".png": - mimetype = "image/png"; - break; - case ".gif": - mimetype = "image/gif"; - break; - case ".jpg": - case ".jpeg": - mimetype = "image/jpeg"; - break; - case ".bmp": - mimetype = "image/bmp"; - break; - case ".tiff": - mimetype = "image/tiff"; - break; - case ".wmf": - mimetype = "image/wmf"; - break; - case ".jp2": - mimetype = "image/jp2"; - break; - case ".svg": - mimetype = "image/svg+xml"; - break; - default: - mimetype = "application/octet-stream"; - break; - } + private string GetImageMimeTypeFromImageFileExtension(string extension) + { + string mimetype; - return mimetype; + switch (extension) + { + case ".png": + mimetype = "image/png"; + break; + case ".gif": + mimetype = "image/gif"; + break; + case ".jpg": + case ".jpeg": + mimetype = "image/jpeg"; + break; + case ".bmp": + mimetype = "image/bmp"; + break; + case ".tiff": + mimetype = "image/tiff"; + break; + case ".wmf": + mimetype = "image/wmf"; + break; + case ".jp2": + mimetype = "image/jp2"; + break; + case ".svg": + mimetype = "image/svg+xml"; + break; + default: + mimetype = "application/octet-stream"; + break; } + + return mimetype; } } diff --git a/src/Services/Catalog/Catalog.API/Extensions/CatalogItemExtensions.cs b/src/Services/Catalog/Catalog.API/Extensions/CatalogItemExtensions.cs index e953df581..efebc7615 100644 --- a/src/Services/Catalog/Catalog.API/Extensions/CatalogItemExtensions.cs +++ b/src/Services/Catalog/Catalog.API/Extensions/CatalogItemExtensions.cs @@ -1,15 +1,14 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; + +public static class CatalogItemExtensions { - public static class CatalogItemExtensions + public static void FillProductUrl(this CatalogItem item, string picBaseUrl, bool azureStorageEnabled) { - public static void FillProductUrl(this CatalogItem item, string picBaseUrl, bool azureStorageEnabled) + if (item != null) { - if (item != null) - { - item.PictureUri = azureStorageEnabled - ? picBaseUrl + item.PictureFileName - : picBaseUrl.Replace("[0]", item.Id.ToString()); - } + item.PictureUri = azureStorageEnabled + ? picBaseUrl + item.PictureFileName + : picBaseUrl.Replace("[0]", item.Id.ToString()); } } } diff --git a/src/Services/Catalog/Catalog.API/Extensions/HostExtensions.cs b/src/Services/Catalog/Catalog.API/Extensions/HostExtensions.cs index 4aff6009d..44b7e3d87 100644 --- a/src/Services/Catalog/Catalog.API/Extensions/HostExtensions.cs +++ b/src/Services/Catalog/Catalog.API/Extensions/HostExtensions.cs @@ -1,71 +1,70 @@ -namespace Catalog.API.Extensions +namespace Catalog.API.Extensions; + +public static class HostExtensions { - public static class HostExtensions + public static bool IsInKubernetes(this IHost host) { - public static bool IsInKubernetes(this IHost host) - { - var cfg = host.Services.GetService(); - var orchestratorType = cfg.GetValue("OrchestratorType"); - return orchestratorType?.ToUpper() == "K8S"; - } + var cfg = host.Services.GetService(); + var orchestratorType = cfg.GetValue("OrchestratorType"); + return orchestratorType?.ToUpper() == "K8S"; + } + + public static IHost MigrateDbContext(this IHost host, Action seeder) where TContext : DbContext + { + var underK8s = host.IsInKubernetes(); - public static IHost MigrateDbContext(this IHost host, Action seeder) where TContext : DbContext + using (var scope = host.Services.CreateScope()) { - var underK8s = host.IsInKubernetes(); + var services = scope.ServiceProvider; - using (var scope = host.Services.CreateScope()) - { - var services = scope.ServiceProvider; + var logger = services.GetRequiredService>(); - var logger = services.GetRequiredService>(); + var context = services.GetService(); - var context = services.GetService(); + try + { + logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); - try + if (underK8s) { - logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); - - if (underK8s) - { - InvokeSeeder(seeder, context, services); - } - else - { - var retry = Policy.Handle() - .WaitAndRetry(new TimeSpan[] - { - TimeSpan.FromSeconds(3), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(8), - }); - - //if the sql server container is not created on run docker compose this - //migration can't fail for network related exception. The retry options for DbContext only - //apply to transient exceptions - // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) - retry.Execute(() => InvokeSeeder(seeder, context, services)); - } + InvokeSeeder(seeder, context, services); + } + else + { + var retry = Policy.Handle() + .WaitAndRetry(new TimeSpan[] + { + TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(8), + }); - logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); + //if the sql server container is not created on run docker compose this + //migration can't fail for network related exception. The retry options for DbContext only + //apply to transient exceptions + // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) + retry.Execute(() => InvokeSeeder(seeder, context, services)); } - catch (Exception ex) + + logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); + if (underK8s) { - logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); - if (underK8s) - { - throw; // Rethrow under k8s because we rely on k8s to re-run the pod - } + throw; // Rethrow under k8s because we rely on k8s to re-run the pod } } - - return host; } - private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) - where TContext : DbContext - { - context.Database.Migrate(); - seeder(context, services); - } + return host; + } + + private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) + where TContext : DbContext + { + context.Database.Migrate(); + seeder(context, services); } } diff --git a/src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs b/src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs index a1b7c54a4..85fa9300c 100644 --- a/src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs +++ b/src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs @@ -1,46 +1,45 @@ -namespace Catalog.API.Extensions +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; + +public static class LinqSelectExtensions { - public static class LinqSelectExtensions + public static IEnumerable> SelectTry(this IEnumerable enumerable, Func selector) { - public static IEnumerable> SelectTry(this IEnumerable enumerable, Func selector) + foreach (TSource element in enumerable) { - foreach (TSource element in enumerable) + SelectTryResult returnedValue; + try + { + returnedValue = new SelectTryResult(element, selector(element), null); + } + catch (Exception ex) { - SelectTryResult returnedValue; - try - { - returnedValue = new SelectTryResult(element, selector(element), null); - } - catch (Exception ex) - { - returnedValue = new SelectTryResult(element, default(TResult), ex); - } - yield return returnedValue; + returnedValue = new SelectTryResult(element, default(TResult), ex); } + yield return returnedValue; } + } - public static IEnumerable OnCaughtException(this IEnumerable> enumerable, Func exceptionHandler) - { - return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.CaughtException)); - } + public static IEnumerable OnCaughtException(this IEnumerable> enumerable, Func exceptionHandler) + { + return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.CaughtException)); + } - public static IEnumerable OnCaughtException(this IEnumerable> enumerable, Func exceptionHandler) - { - return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.Source, x.CaughtException)); - } + public static IEnumerable OnCaughtException(this IEnumerable> enumerable, Func exceptionHandler) + { + return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.Source, x.CaughtException)); + } - public class SelectTryResult + public class SelectTryResult + { + internal SelectTryResult(TSource source, TResult result, Exception exception) { - internal SelectTryResult(TSource source, TResult result, Exception exception) - { - Source = source; - Result = result; - CaughtException = exception; - } - - public TSource Source { get; private set; } - public TResult Result { get; private set; } - public Exception CaughtException { get; private set; } + Source = source; + Result = result; + CaughtException = exception; } + + public TSource Source { get; private set; } + public TResult Result { get; private set; } + public Exception CaughtException { get; private set; } } } diff --git a/src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs b/src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs index 780704c58..b3ac16968 100644 --- a/src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs +++ b/src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs @@ -1,71 +1,70 @@ -namespace Catalog.API.Extensions +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; + +public static class WebHostExtensions { - public static class WebHostExtensions + public static bool IsInKubernetes(this IWebHost host) { - public static bool IsInKubernetes(this IWebHost host) - { - var cfg = host.Services.GetService(); - var orchestratorType = cfg.GetValue("OrchestratorType"); - return orchestratorType?.ToUpper() == "K8S"; - } + var cfg = host.Services.GetService(); + var orchestratorType = cfg.GetValue("OrchestratorType"); + return orchestratorType?.ToUpper() == "K8S"; + } + + public static IWebHost MigrateDbContext(this IWebHost host, Action seeder) where TContext : DbContext + { + var underK8s = host.IsInKubernetes(); - public static IWebHost MigrateDbContext(this IWebHost host, Action seeder) where TContext : DbContext + using (var scope = host.Services.CreateScope()) { - var underK8s = host.IsInKubernetes(); + var services = scope.ServiceProvider; - using (var scope = host.Services.CreateScope()) - { - var services = scope.ServiceProvider; + var logger = services.GetRequiredService>(); - var logger = services.GetRequiredService>(); + var context = services.GetService(); - var context = services.GetService(); + try + { + logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); - try + if (underK8s) { - logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); - - if (underK8s) - { - InvokeSeeder(seeder, context, services); - } - else - { - var retry = Policy.Handle() - .WaitAndRetry(new TimeSpan[] - { - TimeSpan.FromSeconds(3), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(8), - }); - - //if the sql server container is not created on run docker compose this - //migration can't fail for network related exception. The retry options for DbContext only - //apply to transient exceptions - // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) - retry.Execute(() => InvokeSeeder(seeder, context, services)); - } + InvokeSeeder(seeder, context, services); + } + else + { + var retry = Policy.Handle() + .WaitAndRetry(new TimeSpan[] + { + TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(8), + }); - logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); + //if the sql server container is not created on run docker compose this + //migration can't fail for network related exception. The retry options for DbContext only + //apply to transient exceptions + // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) + retry.Execute(() => InvokeSeeder(seeder, context, services)); } - catch (Exception ex) + + logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); + if (underK8s) { - logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); - if (underK8s) - { - throw; // Rethrow under k8s because we rely on k8s to re-run the pod - } + throw; // Rethrow under k8s because we rely on k8s to re-run the pod } } - - return host; } - private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) - where TContext : DbContext - { - context.Database.Migrate(); - seeder(context, services); - } + return host; + } + + private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) + where TContext : DbContext + { + context.Database.Migrate(); + seeder(context, services); } } diff --git a/src/Services/Catalog/Catalog.API/GlobalUsings.cs b/src/Services/Catalog/Catalog.API/GlobalUsings.cs index ea9b772d4..dc0b6f62a 100644 --- a/src/Services/Catalog/Catalog.API/GlobalUsings.cs +++ b/src/Services/Catalog/Catalog.API/GlobalUsings.cs @@ -2,10 +2,10 @@ global using Azure.Identity; global using Autofac.Extensions.DependencyInjection; global using Autofac; -global using Catalog.API.Extensions; -global using Catalog.API.Infrastructure.ActionResults; -global using Catalog.API.Infrastructure.Exceptions; -global using global::Catalog.API.IntegrationEvents; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; +global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; global using Grpc.Core; global using Microsoft.AspNetCore.Hosting; global using Microsoft.AspNetCore.Http; @@ -48,7 +48,7 @@ global using System.Net; global using System.Text.RegularExpressions; global using System.Threading.Tasks; global using System; -global using global::Catalog.API.Infrastructure.Filters; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; global using HealthChecks.UI.Client; global using Microsoft.AspNetCore.Diagnostics.HealthChecks; global using Microsoft.Azure.ServiceBus; @@ -59,4 +59,4 @@ global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents. global using Microsoft.Extensions.Diagnostics.HealthChecks; global using Microsoft.OpenApi.Models; global using RabbitMQ.Client; -global using System.Reflection; +global using System.Reflection; \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs b/src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs index f035c6524..9f657f28b 100644 --- a/src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs +++ b/src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs @@ -1,177 +1,176 @@ using CatalogApi; using static CatalogApi.Catalog; -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Grpc +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Grpc; + +public class CatalogService : CatalogBase { - public class CatalogService : CatalogBase + private readonly CatalogContext _catalogContext; + private readonly CatalogSettings _settings; + private readonly Microsoft.Extensions.Logging.ILogger _logger; + + public CatalogService(CatalogContext dbContext, IOptions settings, ILogger logger) { - private readonly CatalogContext _catalogContext; - private readonly CatalogSettings _settings; - private readonly Extensions.Logging.ILogger _logger; + _settings = settings.Value; + _catalogContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + _logger = logger; + } - public CatalogService(CatalogContext dbContext, IOptions settings, ILogger logger) + public override async Task GetItemById(CatalogItemRequest request, ServerCallContext context) + { + _logger.LogInformation("Begin grpc call CatalogService.GetItemById for product id {Id}", request.Id); + if (request.Id <= 0) { - _settings = settings.Value; - _catalogContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); - _logger = logger; + context.Status = new Status(StatusCode.FailedPrecondition, $"Id must be > 0 (received {request.Id})"); + return null; } - public override async Task GetItemById(CatalogItemRequest request, ServerCallContext context) - { - _logger.LogInformation("Begin grpc call CatalogService.GetItemById for product id {Id}", request.Id); - if (request.Id <= 0) - { - context.Status = new Status(StatusCode.FailedPrecondition, $"Id must be > 0 (received {request.Id})"); - return null; - } + var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == request.Id); + var baseUri = _settings.PicBaseUrl; + var azureStorageEnabled = _settings.AzureStorageEnabled; + item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); - var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == request.Id); - var baseUri = _settings.PicBaseUrl; - var azureStorageEnabled = _settings.AzureStorageEnabled; - item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); - - if (item != null) + if (item != null) + { + return new CatalogItemResponse() { - return new CatalogItemResponse() - { - AvailableStock = item.AvailableStock, - Description = item.Description, - Id = item.Id, - MaxStockThreshold = item.MaxStockThreshold, - Name = item.Name, - OnReorder = item.OnReorder, - PictureFileName = item.PictureFileName, - PictureUri = item.PictureUri, - Price = (double)item.Price, - RestockThreshold = item.RestockThreshold - }; - } - - context.Status = new Status(StatusCode.NotFound, $"Product with id {request.Id} do not exist"); - return null; + AvailableStock = item.AvailableStock, + Description = item.Description, + Id = item.Id, + MaxStockThreshold = item.MaxStockThreshold, + Name = item.Name, + OnReorder = item.OnReorder, + PictureFileName = item.PictureFileName, + PictureUri = item.PictureUri, + Price = (double)item.Price, + RestockThreshold = item.RestockThreshold + }; } - public override async Task GetItemsByIds(CatalogItemsRequest request, ServerCallContext context) + context.Status = new Status(StatusCode.NotFound, $"Product with id {request.Id} do not exist"); + return null; + } + + public override async Task GetItemsByIds(CatalogItemsRequest request, ServerCallContext context) + { + if (!string.IsNullOrEmpty(request.Ids)) { - if (!string.IsNullOrEmpty(request.Ids)) - { - var items = await GetItemsByIdsAsync(request.Ids); + var items = await GetItemsByIdsAsync(request.Ids); - context.Status = !items.Any() ? - new Status(StatusCode.NotFound, $"ids value invalid. Must be comma-separated list of numbers") : - new Status(StatusCode.OK, string.Empty); + context.Status = !items.Any() ? + new Status(StatusCode.NotFound, $"ids value invalid. Must be comma-separated list of numbers") : + new Status(StatusCode.OK, string.Empty); - return this.MapToResponse(items); - } + return this.MapToResponse(items); + } - var totalItems = await _catalogContext.CatalogItems - .LongCountAsync(); + var totalItems = await _catalogContext.CatalogItems + .LongCountAsync(); - var itemsOnPage = await _catalogContext.CatalogItems - .OrderBy(c => c.Name) - .Skip(request.PageSize * request.PageIndex) - .Take(request.PageSize) - .ToListAsync(); + var itemsOnPage = await _catalogContext.CatalogItems + .OrderBy(c => c.Name) + .Skip(request.PageSize * request.PageIndex) + .Take(request.PageSize) + .ToListAsync(); - /* The "awesome" fix for testing Devspaces */ + /* The "awesome" fix for testing Devspaces */ - /* - foreach (var pr in itemsOnPage) { - pr.Name = "Awesome " + pr.Name; - } + /* + foreach (var pr in itemsOnPage) { + pr.Name = "Awesome " + pr.Name; + } - */ + */ - itemsOnPage = ChangeUriPlaceholder(itemsOnPage); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); - var model = this.MapToResponse(itemsOnPage, totalItems, request.PageIndex, request.PageSize); - context.Status = new Status(StatusCode.OK, string.Empty); + var model = this.MapToResponse(itemsOnPage, totalItems, request.PageIndex, request.PageSize); + context.Status = new Status(StatusCode.OK, string.Empty); - return model; - } + return model; + } - private PaginatedItemsResponse MapToResponse(List items) - { - return this.MapToResponse(items, items.Count, 1, items.Count); - } + private PaginatedItemsResponse MapToResponse(List items) + { + return this.MapToResponse(items, items.Count, 1, items.Count); + } - private PaginatedItemsResponse MapToResponse(List items, long count, int pageIndex, int pageSize) + private PaginatedItemsResponse MapToResponse(List items, long count, int pageIndex, int pageSize) + { + var result = new PaginatedItemsResponse() { - var result = new PaginatedItemsResponse() - { - Count = count, - PageIndex = pageIndex, - PageSize = pageSize, - }; + Count = count, + PageIndex = pageIndex, + PageSize = pageSize, + }; - items.ForEach(i => + items.ForEach(i => + { + var brand = i.CatalogBrand == null + ? null + : new CatalogApi.CatalogBrand() + { + Id = i.CatalogBrand.Id, + Name = i.CatalogBrand.Brand, + }; + var catalogType = i.CatalogType == null + ? null + : new CatalogApi.CatalogType() + { + Id = i.CatalogType.Id, + Type = i.CatalogType.Type, + }; + + result.Data.Add(new CatalogItemResponse() { - var brand = i.CatalogBrand == null - ? null - : new CatalogApi.CatalogBrand() - { - Id = i.CatalogBrand.Id, - Name = i.CatalogBrand.Brand, - }; - var catalogType = i.CatalogType == null - ? null - : new CatalogApi.CatalogType() - { - Id = i.CatalogType.Id, - Type = i.CatalogType.Type, - }; - - result.Data.Add(new CatalogItemResponse() - { - AvailableStock = i.AvailableStock, - Description = i.Description, - Id = i.Id, - MaxStockThreshold = i.MaxStockThreshold, - Name = i.Name, - OnReorder = i.OnReorder, - PictureFileName = i.PictureFileName, - PictureUri = i.PictureUri, - RestockThreshold = i.RestockThreshold, - CatalogBrand = brand, - CatalogType = catalogType, - Price = (double)i.Price, - }); + AvailableStock = i.AvailableStock, + Description = i.Description, + Id = i.Id, + MaxStockThreshold = i.MaxStockThreshold, + Name = i.Name, + OnReorder = i.OnReorder, + PictureFileName = i.PictureFileName, + PictureUri = i.PictureUri, + RestockThreshold = i.RestockThreshold, + CatalogBrand = brand, + CatalogType = catalogType, + Price = (double)i.Price, }); + }); - return result; - } + return result; + } - private async Task> GetItemsByIdsAsync(string ids) + private async Task> GetItemsByIdsAsync(string ids) + { + var numIds = ids.Split(',').Select(id => (Ok: int.TryParse(id, out int x), Value: x)); + + if (!numIds.All(nid => nid.Ok)) { - var numIds = ids.Split(',').Select(id => (Ok: int.TryParse(id, out int x), Value: x)); + return new List(); + } - if (!numIds.All(nid => nid.Ok)) - { - return new List(); - } + var idsToSelect = numIds + .Select(id => id.Value); - var idsToSelect = numIds - .Select(id => id.Value); + var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); - var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); + items = ChangeUriPlaceholder(items); - items = ChangeUriPlaceholder(items); + return items; + } - return items; - } + private List ChangeUriPlaceholder(List items) + { + var baseUri = _settings.PicBaseUrl; + var azureStorageEnabled = _settings.AzureStorageEnabled; - private List ChangeUriPlaceholder(List items) + foreach (var item in items) { - var baseUri = _settings.PicBaseUrl; - var azureStorageEnabled = _settings.AzureStorageEnabled; - - foreach (var item in items) - { - item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); - } - - return items; + item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); } + + return items; } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs index 61954a7ab..53af0f0b8 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -1,11 +1,10 @@ -namespace Catalog.API.Infrastructure.ActionResults +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults; + +public class InternalServerErrorObjectResult : ObjectResult { - public class InternalServerErrorObjectResult : ObjectResult + public InternalServerErrorObjectResult(object error) + : base(error) { - public InternalServerErrorObjectResult(object error) - : base(error) - { - StatusCode = StatusCodes.Status500InternalServerError; - } + StatusCode = StatusCodes.Status500InternalServerError; } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs index 7741ac188..cf152f40f 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs @@ -1,33 +1,32 @@ using Microsoft.eShopOnContainers.Services.Catalog.API.Model; -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure -{ - public class CatalogContext : DbContext +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; + +public class CatalogContext : DbContext +{ + public CatalogContext(DbContextOptions options) : base(options) { - public CatalogContext(DbContextOptions options) : base(options) - { - } - public DbSet CatalogItems { get; set; } - public DbSet CatalogBrands { get; set; } - public DbSet CatalogTypes { get; set; } + } + public DbSet CatalogItems { get; set; } + public DbSet CatalogBrands { get; set; } + public DbSet CatalogTypes { get; set; } - protected override void OnModelCreating(ModelBuilder builder) - { - builder.ApplyConfiguration(new CatalogBrandEntityTypeConfiguration()); - builder.ApplyConfiguration(new CatalogTypeEntityTypeConfiguration()); - builder.ApplyConfiguration(new CatalogItemEntityTypeConfiguration()); - } + protected override void OnModelCreating(ModelBuilder builder) + { + builder.ApplyConfiguration(new CatalogBrandEntityTypeConfiguration()); + builder.ApplyConfiguration(new CatalogTypeEntityTypeConfiguration()); + builder.ApplyConfiguration(new CatalogItemEntityTypeConfiguration()); } +} - public class CatalogContextDesignFactory : IDesignTimeDbContextFactory +public class CatalogContextDesignFactory : IDesignTimeDbContextFactory +{ + public CatalogContext CreateDbContext(string[] args) { - public CatalogContext CreateDbContext(string[] args) - { - var optionsBuilder = new DbContextOptionsBuilder() - .UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true"); + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true"); - return new CatalogContext(optionsBuilder.Options); - } + return new CatalogContext(optionsBuilder.Options); } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs index 8d8bf0e71..ba7589293 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs @@ -1,371 +1,370 @@ using Microsoft.eShopOnContainers.Services.Catalog.API.Model; -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure -{ - public class CatalogContextSeed +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; + +public class CatalogContextSeed +{ + public async Task SeedAsync(CatalogContext context, IWebHostEnvironment env, IOptions settings, ILogger logger) { - public async Task SeedAsync(CatalogContext context, IWebHostEnvironment env, IOptions settings, ILogger logger) + var policy = CreatePolicy(logger, nameof(CatalogContextSeed)); + + await policy.ExecuteAsync(async () => { - var policy = CreatePolicy(logger, nameof(CatalogContextSeed)); + var useCustomizationData = settings.Value.UseCustomizationData; + var contentRootPath = env.ContentRootPath; + var picturePath = env.WebRootPath; - await policy.ExecuteAsync(async () => + if (!context.CatalogBrands.Any()) { - var useCustomizationData = settings.Value.UseCustomizationData; - var contentRootPath = env.ContentRootPath; - var picturePath = env.WebRootPath; + await context.CatalogBrands.AddRangeAsync(useCustomizationData + ? GetCatalogBrandsFromFile(contentRootPath, logger) + : GetPreconfiguredCatalogBrands()); - if (!context.CatalogBrands.Any()) - { - await context.CatalogBrands.AddRangeAsync(useCustomizationData - ? GetCatalogBrandsFromFile(contentRootPath, logger) - : GetPreconfiguredCatalogBrands()); + await context.SaveChangesAsync(); + } - await context.SaveChangesAsync(); - } + if (!context.CatalogTypes.Any()) + { + await context.CatalogTypes.AddRangeAsync(useCustomizationData + ? GetCatalogTypesFromFile(contentRootPath, logger) + : GetPreconfiguredCatalogTypes()); - if (!context.CatalogTypes.Any()) - { - await context.CatalogTypes.AddRangeAsync(useCustomizationData - ? GetCatalogTypesFromFile(contentRootPath, logger) - : GetPreconfiguredCatalogTypes()); + await context.SaveChangesAsync(); + } - await context.SaveChangesAsync(); - } + if (!context.CatalogItems.Any()) + { + await context.CatalogItems.AddRangeAsync(useCustomizationData + ? GetCatalogItemsFromFile(contentRootPath, context, logger) + : GetPreconfiguredItems()); - if (!context.CatalogItems.Any()) - { - await context.CatalogItems.AddRangeAsync(useCustomizationData - ? GetCatalogItemsFromFile(contentRootPath, context, logger) - : GetPreconfiguredItems()); + await context.SaveChangesAsync(); - await context.SaveChangesAsync(); + GetCatalogItemPictures(contentRootPath, picturePath); + } + }); + } - GetCatalogItemPictures(contentRootPath, picturePath); - } - }); + private IEnumerable GetCatalogBrandsFromFile(string contentRootPath, ILogger logger) + { + string csvFileCatalogBrands = Path.Combine(contentRootPath, "Setup", "CatalogBrands.csv"); + + if (!File.Exists(csvFileCatalogBrands)) + { + return GetPreconfiguredCatalogBrands(); } - private IEnumerable GetCatalogBrandsFromFile(string contentRootPath, ILogger logger) + string[] csvheaders; + try { - string csvFileCatalogBrands = Path.Combine(contentRootPath, "Setup", "CatalogBrands.csv"); + string[] requiredHeaders = { "catalogbrand" }; + csvheaders = GetHeaders(csvFileCatalogBrands, requiredHeaders); + } + catch (Exception ex) + { + logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); + return GetPreconfiguredCatalogBrands(); + } - if (!File.Exists(csvFileCatalogBrands)) - { - return GetPreconfiguredCatalogBrands(); - } + return File.ReadAllLines(csvFileCatalogBrands) + .Skip(1) // skip header row + .SelectTry(x => CreateCatalogBrand(x)) + .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) + .Where(x => x != null); + } - string[] csvheaders; - try - { - string[] requiredHeaders = { "catalogbrand" }; - csvheaders = GetHeaders(csvFileCatalogBrands, requiredHeaders); - } - catch (Exception ex) - { - logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); - return GetPreconfiguredCatalogBrands(); - } + private CatalogBrand CreateCatalogBrand(string brand) + { + brand = brand.Trim('"').Trim(); - return File.ReadAllLines(csvFileCatalogBrands) - .Skip(1) // skip header row - .SelectTry(x => CreateCatalogBrand(x)) - .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) - .Where(x => x != null); + if (String.IsNullOrEmpty(brand)) + { + throw new Exception("catalog Brand Name is empty"); } - private CatalogBrand CreateCatalogBrand(string brand) + return new CatalogBrand { - brand = brand.Trim('"').Trim(); + Brand = brand, + }; + } - if (String.IsNullOrEmpty(brand)) - { - throw new Exception("catalog Brand Name is empty"); - } + private IEnumerable GetPreconfiguredCatalogBrands() + { + return new List() + { + new CatalogBrand() { Brand = "Azure"}, + new CatalogBrand() { Brand = ".NET" }, + new CatalogBrand() { Brand = "Visual Studio" }, + new CatalogBrand() { Brand = "SQL Server" }, + new CatalogBrand() { Brand = "Other" } + }; + } - return new CatalogBrand - { - Brand = brand, - }; - } + private IEnumerable GetCatalogTypesFromFile(string contentRootPath, ILogger logger) + { + string csvFileCatalogTypes = Path.Combine(contentRootPath, "Setup", "CatalogTypes.csv"); - private IEnumerable GetPreconfiguredCatalogBrands() + if (!File.Exists(csvFileCatalogTypes)) { - return new List() - { - new CatalogBrand() { Brand = "Azure"}, - new CatalogBrand() { Brand = ".NET" }, - new CatalogBrand() { Brand = "Visual Studio" }, - new CatalogBrand() { Brand = "SQL Server" }, - new CatalogBrand() { Brand = "Other" } - }; + return GetPreconfiguredCatalogTypes(); } - private IEnumerable GetCatalogTypesFromFile(string contentRootPath, ILogger logger) + string[] csvheaders; + try { - string csvFileCatalogTypes = Path.Combine(contentRootPath, "Setup", "CatalogTypes.csv"); + string[] requiredHeaders = { "catalogtype" }; + csvheaders = GetHeaders(csvFileCatalogTypes, requiredHeaders); + } + catch (Exception ex) + { + logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); + return GetPreconfiguredCatalogTypes(); + } - if (!File.Exists(csvFileCatalogTypes)) - { - return GetPreconfiguredCatalogTypes(); - } + return File.ReadAllLines(csvFileCatalogTypes) + .Skip(1) // skip header row + .SelectTry(x => CreateCatalogType(x)) + .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) + .Where(x => x != null); + } - string[] csvheaders; - try - { - string[] requiredHeaders = { "catalogtype" }; - csvheaders = GetHeaders(csvFileCatalogTypes, requiredHeaders); - } - catch (Exception ex) - { - logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); - return GetPreconfiguredCatalogTypes(); - } + private CatalogType CreateCatalogType(string type) + { + type = type.Trim('"').Trim(); - return File.ReadAllLines(csvFileCatalogTypes) - .Skip(1) // skip header row - .SelectTry(x => CreateCatalogType(x)) - .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) - .Where(x => x != null); + if (String.IsNullOrEmpty(type)) + { + throw new Exception("catalog Type Name is empty"); } - private CatalogType CreateCatalogType(string type) + return new CatalogType + { + Type = type, + }; + } + + private IEnumerable GetPreconfiguredCatalogTypes() + { + return new List() { - type = type.Trim('"').Trim(); + new CatalogType() { Type = "Mug"}, + new CatalogType() { Type = "T-Shirt" }, + new CatalogType() { Type = "Sheet" }, + new CatalogType() { Type = "USB Memory Stick" } + }; + } - if (String.IsNullOrEmpty(type)) - { - throw new Exception("catalog Type Name is empty"); - } + private IEnumerable GetCatalogItemsFromFile(string contentRootPath, CatalogContext context, ILogger logger) + { + string csvFileCatalogItems = Path.Combine(contentRootPath, "Setup", "CatalogItems.csv"); - return new CatalogType - { - Type = type, - }; + if (!File.Exists(csvFileCatalogItems)) + { + return GetPreconfiguredItems(); } - private IEnumerable GetPreconfiguredCatalogTypes() + string[] csvheaders; + try { - return new List() - { - new CatalogType() { Type = "Mug"}, - new CatalogType() { Type = "T-Shirt" }, - new CatalogType() { Type = "Sheet" }, - new CatalogType() { Type = "USB Memory Stick" } - }; + string[] requiredHeaders = { "catalogtypename", "catalogbrandname", "description", "name", "price", "picturefilename" }; + string[] optionalheaders = { "availablestock", "restockthreshold", "maxstockthreshold", "onreorder" }; + csvheaders = GetHeaders(csvFileCatalogItems, requiredHeaders, optionalheaders); } - - private IEnumerable GetCatalogItemsFromFile(string contentRootPath, CatalogContext context, ILogger logger) + catch (Exception ex) { - string csvFileCatalogItems = Path.Combine(contentRootPath, "Setup", "CatalogItems.csv"); + logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); + return GetPreconfiguredItems(); + } - if (!File.Exists(csvFileCatalogItems)) - { - return GetPreconfiguredItems(); - } + var catalogTypeIdLookup = context.CatalogTypes.ToDictionary(ct => ct.Type, ct => ct.Id); + var catalogBrandIdLookup = context.CatalogBrands.ToDictionary(ct => ct.Brand, ct => ct.Id); - string[] csvheaders; - try - { - string[] requiredHeaders = { "catalogtypename", "catalogbrandname", "description", "name", "price", "picturefilename" }; - string[] optionalheaders = { "availablestock", "restockthreshold", "maxstockthreshold", "onreorder" }; - csvheaders = GetHeaders(csvFileCatalogItems, requiredHeaders, optionalheaders); - } - catch (Exception ex) - { - logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); - return GetPreconfiguredItems(); - } - - var catalogTypeIdLookup = context.CatalogTypes.ToDictionary(ct => ct.Type, ct => ct.Id); - var catalogBrandIdLookup = context.CatalogBrands.ToDictionary(ct => ct.Brand, ct => ct.Id); + return File.ReadAllLines(csvFileCatalogItems) + .Skip(1) // skip header row + .Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) + .SelectTry(column => CreateCatalogItem(column, csvheaders, catalogTypeIdLookup, catalogBrandIdLookup)) + .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) + .Where(x => x != null); + } - return File.ReadAllLines(csvFileCatalogItems) - .Skip(1) // skip header row - .Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) - .SelectTry(column => CreateCatalogItem(column, csvheaders, catalogTypeIdLookup, catalogBrandIdLookup)) - .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) - .Where(x => x != null); + private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary catalogTypeIdLookup, Dictionary catalogBrandIdLookup) + { + if (column.Count() != headers.Count()) + { + throw new Exception($"column count '{column.Count()}' not the same as headers count'{headers.Count()}'"); } - private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary catalogTypeIdLookup, Dictionary catalogBrandIdLookup) + string catalogTypeName = column[Array.IndexOf(headers, "catalogtypename")].Trim('"').Trim(); + if (!catalogTypeIdLookup.ContainsKey(catalogTypeName)) { - if (column.Count() != headers.Count()) - { - throw new Exception($"column count '{column.Count()}' not the same as headers count'{headers.Count()}'"); - } - - string catalogTypeName = column[Array.IndexOf(headers, "catalogtypename")].Trim('"').Trim(); - if (!catalogTypeIdLookup.ContainsKey(catalogTypeName)) - { - throw new Exception($"type={catalogTypeName} does not exist in catalogTypes"); - } + throw new Exception($"type={catalogTypeName} does not exist in catalogTypes"); + } - string catalogBrandName = column[Array.IndexOf(headers, "catalogbrandname")].Trim('"').Trim(); - if (!catalogBrandIdLookup.ContainsKey(catalogBrandName)) - { - throw new Exception($"type={catalogTypeName} does not exist in catalogTypes"); - } + string catalogBrandName = column[Array.IndexOf(headers, "catalogbrandname")].Trim('"').Trim(); + if (!catalogBrandIdLookup.ContainsKey(catalogBrandName)) + { + throw new Exception($"type={catalogTypeName} does not exist in catalogTypes"); + } - string priceString = column[Array.IndexOf(headers, "price")].Trim('"').Trim(); - if (!Decimal.TryParse(priceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out Decimal price)) - { - throw new Exception($"price={priceString}is not a valid decimal number"); - } + string priceString = column[Array.IndexOf(headers, "price")].Trim('"').Trim(); + if (!Decimal.TryParse(priceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out Decimal price)) + { + throw new Exception($"price={priceString}is not a valid decimal number"); + } - var catalogItem = new CatalogItem() - { - CatalogTypeId = catalogTypeIdLookup[catalogTypeName], - CatalogBrandId = catalogBrandIdLookup[catalogBrandName], - Description = column[Array.IndexOf(headers, "description")].Trim('"').Trim(), - Name = column[Array.IndexOf(headers, "name")].Trim('"').Trim(), - Price = price, - PictureFileName = column[Array.IndexOf(headers, "picturefilename")].Trim('"').Trim(), - }; - - int availableStockIndex = Array.IndexOf(headers, "availablestock"); - if (availableStockIndex != -1) + var catalogItem = new CatalogItem() + { + CatalogTypeId = catalogTypeIdLookup[catalogTypeName], + CatalogBrandId = catalogBrandIdLookup[catalogBrandName], + Description = column[Array.IndexOf(headers, "description")].Trim('"').Trim(), + Name = column[Array.IndexOf(headers, "name")].Trim('"').Trim(), + Price = price, + PictureFileName = column[Array.IndexOf(headers, "picturefilename")].Trim('"').Trim(), + }; + + int availableStockIndex = Array.IndexOf(headers, "availablestock"); + if (availableStockIndex != -1) + { + string availableStockString = column[availableStockIndex].Trim('"').Trim(); + if (!String.IsNullOrEmpty(availableStockString)) { - string availableStockString = column[availableStockIndex].Trim('"').Trim(); - if (!String.IsNullOrEmpty(availableStockString)) + if (int.TryParse(availableStockString, out int availableStock)) { - if (int.TryParse(availableStockString, out int availableStock)) - { - catalogItem.AvailableStock = availableStock; - } - else - { - throw new Exception($"availableStock={availableStockString} is not a valid integer"); - } + catalogItem.AvailableStock = availableStock; + } + else + { + throw new Exception($"availableStock={availableStockString} is not a valid integer"); } } + } - int restockThresholdIndex = Array.IndexOf(headers, "restockthreshold"); - if (restockThresholdIndex != -1) + int restockThresholdIndex = Array.IndexOf(headers, "restockthreshold"); + if (restockThresholdIndex != -1) + { + string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); + if (!String.IsNullOrEmpty(restockThresholdString)) { - string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); - if (!String.IsNullOrEmpty(restockThresholdString)) + if (int.TryParse(restockThresholdString, out int restockThreshold)) { - if (int.TryParse(restockThresholdString, out int restockThreshold)) - { - catalogItem.RestockThreshold = restockThreshold; - } - else - { - throw new Exception($"restockThreshold={restockThreshold} is not a valid integer"); - } + catalogItem.RestockThreshold = restockThreshold; + } + else + { + throw new Exception($"restockThreshold={restockThreshold} is not a valid integer"); } } + } - int maxStockThresholdIndex = Array.IndexOf(headers, "maxstockthreshold"); - if (maxStockThresholdIndex != -1) + int maxStockThresholdIndex = Array.IndexOf(headers, "maxstockthreshold"); + if (maxStockThresholdIndex != -1) + { + string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); + if (!String.IsNullOrEmpty(maxStockThresholdString)) { - string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); - if (!String.IsNullOrEmpty(maxStockThresholdString)) + if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) { - if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) - { - catalogItem.MaxStockThreshold = maxStockThreshold; - } - else - { - throw new Exception($"maxStockThreshold={maxStockThreshold} is not a valid integer"); - } + catalogItem.MaxStockThreshold = maxStockThreshold; + } + else + { + throw new Exception($"maxStockThreshold={maxStockThreshold} is not a valid integer"); } } + } - int onReorderIndex = Array.IndexOf(headers, "onreorder"); - if (onReorderIndex != -1) + int onReorderIndex = Array.IndexOf(headers, "onreorder"); + if (onReorderIndex != -1) + { + string onReorderString = column[onReorderIndex].Trim('"').Trim(); + if (!String.IsNullOrEmpty(onReorderString)) { - string onReorderString = column[onReorderIndex].Trim('"').Trim(); - if (!String.IsNullOrEmpty(onReorderString)) + if (bool.TryParse(onReorderString, out bool onReorder)) { - if (bool.TryParse(onReorderString, out bool onReorder)) - { - catalogItem.OnReorder = onReorder; - } - else - { - throw new Exception($"onReorder={onReorderString} is not a valid boolean"); - } + catalogItem.OnReorder = onReorder; + } + else + { + throw new Exception($"onReorder={onReorderString} is not a valid boolean"); } } - - return catalogItem; } - private IEnumerable GetPreconfiguredItems() + return catalogItem; + } + + private IEnumerable GetPreconfiguredItems() + { + return new List() { - return new List() - { - new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureFileName = "1.png" }, - new CatalogItem { CatalogTypeId = 1, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureFileName = "2.png" }, - new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureFileName = "3.png" }, - new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureFileName = "4.png" }, - new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureFileName = "5.png" }, - new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureFileName = "6.png" }, - new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureFileName = "7.png" }, - new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureFileName = "8.png" }, - new CatalogItem { CatalogTypeId = 1, CatalogBrandId = 5, AvailableStock = 100, Description = "Cup White Mug", Name = "Cup White Mug", Price = 12, PictureFileName = "9.png" }, - new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureFileName = "10.png" }, - new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = "Cup Sheet", Name = "Cup Sheet", Price = 8.5M, PictureFileName = "11.png" }, - new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureFileName = "12.png" }, - }; - } + new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureFileName = "1.png" }, + new CatalogItem { CatalogTypeId = 1, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureFileName = "2.png" }, + new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureFileName = "3.png" }, + new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureFileName = "4.png" }, + new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureFileName = "5.png" }, + new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureFileName = "6.png" }, + new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureFileName = "7.png" }, + new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureFileName = "8.png" }, + new CatalogItem { CatalogTypeId = 1, CatalogBrandId = 5, AvailableStock = 100, Description = "Cup White Mug", Name = "Cup White Mug", Price = 12, PictureFileName = "9.png" }, + new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureFileName = "10.png" }, + new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = "Cup Sheet", Name = "Cup Sheet", Price = 8.5M, PictureFileName = "11.png" }, + new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureFileName = "12.png" }, + }; + } - private string[] GetHeaders(string csvfile, string[] requiredHeaders, string[] optionalHeaders = null) + private string[] GetHeaders(string csvfile, string[] requiredHeaders, string[] optionalHeaders = null) + { + string[] csvheaders = File.ReadLines(csvfile).First().ToLowerInvariant().Split(','); + + if (csvheaders.Count() < requiredHeaders.Count()) { - string[] csvheaders = File.ReadLines(csvfile).First().ToLowerInvariant().Split(','); + throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is bigger then csv header count '{csvheaders.Count()}' "); + } - if (csvheaders.Count() < requiredHeaders.Count()) + if (optionalHeaders != null) + { + if (csvheaders.Count() > (requiredHeaders.Count() + optionalHeaders.Count())) { - throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is bigger then csv header count '{csvheaders.Count()}' "); + throw new Exception($"csv header count '{csvheaders.Count()}' is larger then required '{requiredHeaders.Count()}' and optional '{optionalHeaders.Count()}' headers count"); } + } - if (optionalHeaders != null) + foreach (var requiredHeader in requiredHeaders) + { + if (!csvheaders.Contains(requiredHeader)) { - if (csvheaders.Count() > (requiredHeaders.Count() + optionalHeaders.Count())) - { - throw new Exception($"csv header count '{csvheaders.Count()}' is larger then required '{requiredHeaders.Count()}' and optional '{optionalHeaders.Count()}' headers count"); - } + throw new Exception($"does not contain required header '{requiredHeader}'"); } + } + + return csvheaders; + } - foreach (var requiredHeader in requiredHeaders) + private void GetCatalogItemPictures(string contentRootPath, string picturePath) + { + if (picturePath != null) + { + DirectoryInfo directory = new DirectoryInfo(picturePath); + foreach (FileInfo file in directory.GetFiles()) { - if (!csvheaders.Contains(requiredHeader)) - { - throw new Exception($"does not contain required header '{requiredHeader}'"); - } + file.Delete(); } - return csvheaders; + string zipFileCatalogItemPictures = Path.Combine(contentRootPath, "Setup", "CatalogItems.zip"); + ZipFile.ExtractToDirectory(zipFileCatalogItemPictures, picturePath); } + } - private void GetCatalogItemPictures(string contentRootPath, string picturePath) - { - if (picturePath != null) - { - DirectoryInfo directory = new DirectoryInfo(picturePath); - foreach (FileInfo file in directory.GetFiles()) + private AsyncRetryPolicy CreatePolicy(ILogger logger, string prefix, int retries = 3) + { + return Policy.Handle(). + WaitAndRetryAsync( + retryCount: retries, + sleepDurationProvider: retry => TimeSpan.FromSeconds(5), + onRetry: (exception, timeSpan, retry, ctx) => { - file.Delete(); + logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", prefix, exception.GetType().Name, exception.Message, retry, retries); } - - string zipFileCatalogItemPictures = Path.Combine(contentRootPath, "Setup", "CatalogItems.zip"); - ZipFile.ExtractToDirectory(zipFileCatalogItemPictures, picturePath); - } - } - - private AsyncRetryPolicy CreatePolicy(ILogger logger, string prefix, int retries = 3) - { - return Policy.Handle(). - WaitAndRetryAsync( - retryCount: retries, - sleepDurationProvider: retry => TimeSpan.FromSeconds(5), - onRetry: (exception, timeSpan, retry, ctx) => - { - logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", prefix, exception.GetType().Name, exception.Message, retry, retries); - } - ); - } + ); } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogBrandEntityTypeConfiguration.cs b/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogBrandEntityTypeConfiguration.cs index be089a025..1263891ad 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogBrandEntityTypeConfiguration.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogBrandEntityTypeConfiguration.cs @@ -1,21 +1,20 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; + +class CatalogBrandEntityTypeConfiguration + : IEntityTypeConfiguration { - class CatalogBrandEntityTypeConfiguration - : IEntityTypeConfiguration + public void Configure(EntityTypeBuilder builder) { - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("CatalogBrand"); + builder.ToTable("CatalogBrand"); - builder.HasKey(ci => ci.Id); + builder.HasKey(ci => ci.Id); - builder.Property(ci => ci.Id) - .UseHiLo("catalog_brand_hilo") - .IsRequired(); + builder.Property(ci => ci.Id) + .UseHiLo("catalog_brand_hilo") + .IsRequired(); - builder.Property(cb => cb.Brand) - .IsRequired() - .HasMaxLength(100); - } + builder.Property(cb => cb.Brand) + .IsRequired() + .HasMaxLength(100); } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogItemEntityTypeConfiguration.cs b/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogItemEntityTypeConfiguration.cs index 647f23787..d7cc8b2d5 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogItemEntityTypeConfiguration.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogItemEntityTypeConfiguration.cs @@ -1,35 +1,34 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; + +class CatalogItemEntityTypeConfiguration + : IEntityTypeConfiguration { - class CatalogItemEntityTypeConfiguration - : IEntityTypeConfiguration + public void Configure(EntityTypeBuilder builder) { - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("Catalog"); + builder.ToTable("Catalog"); - builder.Property(ci => ci.Id) - .UseHiLo("catalog_hilo") - .IsRequired(); + builder.Property(ci => ci.Id) + .UseHiLo("catalog_hilo") + .IsRequired(); - builder.Property(ci => ci.Name) - .IsRequired(true) - .HasMaxLength(50); + builder.Property(ci => ci.Name) + .IsRequired(true) + .HasMaxLength(50); - builder.Property(ci => ci.Price) - .IsRequired(true); + builder.Property(ci => ci.Price) + .IsRequired(true); - builder.Property(ci => ci.PictureFileName) - .IsRequired(false); + builder.Property(ci => ci.PictureFileName) + .IsRequired(false); - builder.Ignore(ci => ci.PictureUri); + builder.Ignore(ci => ci.PictureUri); - builder.HasOne(ci => ci.CatalogBrand) - .WithMany() - .HasForeignKey(ci => ci.CatalogBrandId); + builder.HasOne(ci => ci.CatalogBrand) + .WithMany() + .HasForeignKey(ci => ci.CatalogBrandId); - builder.HasOne(ci => ci.CatalogType) - .WithMany() - .HasForeignKey(ci => ci.CatalogTypeId); - } + builder.HasOne(ci => ci.CatalogType) + .WithMany() + .HasForeignKey(ci => ci.CatalogTypeId); } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogTypeEntityTypeConfiguration.cs b/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogTypeEntityTypeConfiguration.cs index 05b860262..698de0d8f 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogTypeEntityTypeConfiguration.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogTypeEntityTypeConfiguration.cs @@ -1,21 +1,20 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; + +class CatalogTypeEntityTypeConfiguration + : IEntityTypeConfiguration { - class CatalogTypeEntityTypeConfiguration - : IEntityTypeConfiguration + public void Configure(EntityTypeBuilder builder) { - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("CatalogType"); + builder.ToTable("CatalogType"); - builder.HasKey(ci => ci.Id); + builder.HasKey(ci => ci.Id); - builder.Property(ci => ci.Id) - .UseHiLo("catalog_type_hilo") - .IsRequired(); + builder.Property(ci => ci.Id) + .UseHiLo("catalog_type_hilo") + .IsRequired(); - builder.Property(cb => cb.Type) - .IsRequired() - .HasMaxLength(100); - } + builder.Property(cb => cb.Type) + .IsRequired() + .HasMaxLength(100); } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs b/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs index 1d0905935..4bc8db208 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs @@ -1,19 +1,18 @@ -namespace Catalog.API.Infrastructure.Exceptions +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; + +/// +/// Exception type for app exceptions +/// +public class CatalogDomainException : Exception { - /// - /// Exception type for app exceptions - /// - public class CatalogDomainException : Exception - { - public CatalogDomainException() - { } + public CatalogDomainException() + { } - public CatalogDomainException(string message) - : base(message) - { } + public CatalogDomainException(string message) + : base(message) + { } - public CatalogDomainException(string message, Exception innerException) - : base(message, innerException) - { } - } + public CatalogDomainException(string message, Exception innerException) + : base(message, innerException) + { } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs index d69a729e6..66ee35e7b 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -1,59 +1,58 @@ -namespace Catalog.API.Infrastructure.Filters +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; + +public class HttpGlobalExceptionFilter : IExceptionFilter { - public class HttpGlobalExceptionFilter : IExceptionFilter + private readonly IWebHostEnvironment env; + private readonly ILogger logger; + + public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) { - private readonly IWebHostEnvironment env; - private readonly ILogger logger; + this.env = env; + this.logger = logger; + } - public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) - { - this.env = env; - this.logger = logger; - } + public void OnException(ExceptionContext context) + { + logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); - public void OnException(ExceptionContext context) + if (context.Exception.GetType() == typeof(CatalogDomainException)) { - logger.LogError(new EventId(context.Exception.HResult), - context.Exception, - context.Exception.Message); - - if (context.Exception.GetType() == typeof(CatalogDomainException)) + var problemDetails = new ValidationProblemDetails() { - var problemDetails = new ValidationProblemDetails() - { - Instance = context.HttpContext.Request.Path, - Status = StatusCodes.Status400BadRequest, - Detail = "Please refer to the errors property for additional details." - }; + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; - problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); + problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); - context.Result = new BadRequestObjectResult(problemDetails); - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; - } - else + context.Result = new BadRequestObjectResult(problemDetails); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An error ocurred." } + }; + + if (env.IsDevelopment()) { - var json = new JsonErrorResponse - { - Messages = new[] { "An error ocurred." } - }; - - if (env.IsDevelopment()) - { - json.DeveloperMessage = context.Exception; - } - - context.Result = new InternalServerErrorObjectResult(json); - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + json.DeveloperMessage = context.Exception; } - context.ExceptionHandled = true; + + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; } + context.ExceptionHandled = true; + } - private class JsonErrorResponse - { - public string[] Messages { get; set; } + private class JsonErrorResponse + { + public string[] Messages { get; set; } - public object DeveloperMessage { get; set; } - } + public object DeveloperMessage { get; set; } } } diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs index f12ea951d..282bea5b3 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs @@ -1,75 +1,74 @@ -namespace Catalog.API.IntegrationEvents +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; + +public class CatalogIntegrationEventService : ICatalogIntegrationEventService, IDisposable { - public class CatalogIntegrationEventService : ICatalogIntegrationEventService, IDisposable + private readonly Func _integrationEventLogServiceFactory; + private readonly IEventBus _eventBus; + private readonly CatalogContext _catalogContext; + private readonly IIntegrationEventLogService _eventLogService; + private readonly ILogger _logger; + private volatile bool disposedValue; + + public CatalogIntegrationEventService( + ILogger logger, + IEventBus eventBus, + CatalogContext catalogContext, + Func integrationEventLogServiceFactory) { - private readonly Func _integrationEventLogServiceFactory; - private readonly IEventBus _eventBus; - private readonly CatalogContext _catalogContext; - private readonly IIntegrationEventLogService _eventLogService; - private readonly ILogger _logger; - private volatile bool disposedValue; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _catalogContext = catalogContext ?? throw new ArgumentNullException(nameof(catalogContext)); + _integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory)); + _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); + _eventLogService = _integrationEventLogServiceFactory(_catalogContext.Database.GetDbConnection()); + } - public CatalogIntegrationEventService( - ILogger logger, - IEventBus eventBus, - CatalogContext catalogContext, - Func integrationEventLogServiceFactory) + public async Task PublishThroughEventBusAsync(IntegrationEvent evt) + { + try { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _catalogContext = catalogContext ?? throw new ArgumentNullException(nameof(catalogContext)); - _integrationEventLogServiceFactory = integrationEventLogServiceFactory ?? throw new ArgumentNullException(nameof(integrationEventLogServiceFactory)); - _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus)); - _eventLogService = _integrationEventLogServiceFactory(_catalogContext.Database.GetDbConnection()); - } + _logger.LogInformation("----- Publishing integration event: {IntegrationEventId_published} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); - public async Task PublishThroughEventBusAsync(IntegrationEvent evt) + await _eventLogService.MarkEventAsInProgressAsync(evt.Id); + _eventBus.Publish(evt); + await _eventLogService.MarkEventAsPublishedAsync(evt.Id); + } + catch (Exception ex) { - try - { - _logger.LogInformation("----- Publishing integration event: {IntegrationEventId_published} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); - - await _eventLogService.MarkEventAsInProgressAsync(evt.Id); - _eventBus.Publish(evt); - await _eventLogService.MarkEventAsPublishedAsync(evt.Id); - } - catch (Exception ex) - { - _logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); - await _eventLogService.MarkEventAsFailedAsync(evt.Id); - } + _logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); + await _eventLogService.MarkEventAsFailedAsync(evt.Id); } + } - public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) - { - _logger.LogInformation("----- CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}", evt.Id); + public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) + { + _logger.LogInformation("----- CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}", evt.Id); - //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): - //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - await ResilientTransaction.New(_catalogContext).ExecuteAsync(async () => - { - // Achieving atomicity between original catalog database operation and the IntegrationEventLog thanks to a local transaction - await _catalogContext.SaveChangesAsync(); - await _eventLogService.SaveEventAsync(evt, _catalogContext.Database.CurrentTransaction); - }); - } + //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): + //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + await ResilientTransaction.New(_catalogContext).ExecuteAsync(async () => + { + // Achieving atomicity between original catalog database operation and the IntegrationEventLog thanks to a local transaction + await _catalogContext.SaveChangesAsync(); + await _eventLogService.SaveEventAsync(evt, _catalogContext.Database.CurrentTransaction); + }); + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) { - if (!disposedValue) + if (disposing) { - if (disposing) - { - (_eventLogService as IDisposable)?.Dispose(); - } - - disposedValue = true; + (_eventLogService as IDisposable)?.Dispose(); } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + disposedValue = true; } } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs index 5b627370f..35c523c39 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs @@ -1,48 +1,46 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling -{ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; - public class OrderStatusChangedToAwaitingValidationIntegrationEventHandler : - IIntegrationEventHandler +public class OrderStatusChangedToAwaitingValidationIntegrationEventHandler : + IIntegrationEventHandler +{ + private readonly CatalogContext _catalogContext; + private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; + private readonly ILogger _logger; + + public OrderStatusChangedToAwaitingValidationIntegrationEventHandler( + CatalogContext catalogContext, + ICatalogIntegrationEventService catalogIntegrationEventService, + ILogger logger) { - private readonly CatalogContext _catalogContext; - private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; - private readonly ILogger _logger; - - public OrderStatusChangedToAwaitingValidationIntegrationEventHandler( - CatalogContext catalogContext, - ICatalogIntegrationEventService catalogIntegrationEventService, - ILogger logger) - { - _catalogContext = catalogContext; - _catalogIntegrationEventService = catalogIntegrationEventService; - _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); - } + _catalogContext = catalogContext; + _catalogIntegrationEventService = catalogIntegrationEventService; + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); + } - public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) + public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) + { + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) - { - _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); - var confirmedOrderStockItems = new List(); + var confirmedOrderStockItems = new List(); - foreach (var orderStockItem in @event.OrderStockItems) - { - var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); - var hasStock = catalogItem.AvailableStock >= orderStockItem.Units; - var confirmedOrderStockItem = new ConfirmedOrderStockItem(catalogItem.Id, hasStock); + foreach (var orderStockItem in @event.OrderStockItems) + { + var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); + var hasStock = catalogItem.AvailableStock >= orderStockItem.Units; + var confirmedOrderStockItem = new ConfirmedOrderStockItem(catalogItem.Id, hasStock); - confirmedOrderStockItems.Add(confirmedOrderStockItem); - } + confirmedOrderStockItems.Add(confirmedOrderStockItem); + } - var confirmedIntegrationEvent = confirmedOrderStockItems.Any(c => !c.HasStock) - ? (IntegrationEvent)new OrderStockRejectedIntegrationEvent(@event.OrderId, confirmedOrderStockItems) - : new OrderStockConfirmedIntegrationEvent(@event.OrderId); + var confirmedIntegrationEvent = confirmedOrderStockItems.Any(c => !c.HasStock) + ? (IntegrationEvent)new OrderStockRejectedIntegrationEvent(@event.OrderId, confirmedOrderStockItems) + : new OrderStockConfirmedIntegrationEvent(@event.OrderId); - await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(confirmedIntegrationEvent); - await _catalogIntegrationEventService.PublishThroughEventBusAsync(confirmedIntegrationEvent); + await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(confirmedIntegrationEvent); + await _catalogIntegrationEventService.PublishThroughEventBusAsync(confirmedIntegrationEvent); - } } } -} \ No newline at end of file +} diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs index 6eaa8a509..8882e78a6 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs @@ -1,36 +1,35 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling -{ - public class OrderStatusChangedToPaidIntegrationEventHandler : - IIntegrationEventHandler +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; + +public class OrderStatusChangedToPaidIntegrationEventHandler : + IIntegrationEventHandler +{ + private readonly CatalogContext _catalogContext; + private readonly ILogger _logger; + + public OrderStatusChangedToPaidIntegrationEventHandler( + CatalogContext catalogContext, + ILogger logger) { - private readonly CatalogContext _catalogContext; - private readonly ILogger _logger; + _catalogContext = catalogContext; + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); + } - public OrderStatusChangedToPaidIntegrationEventHandler( - CatalogContext catalogContext, - ILogger logger) + public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event) + { + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _catalogContext = catalogContext; - _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); - } + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); - public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event) - { - using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) + //we're not blocking stock/inventory + foreach (var orderStockItem in @event.OrderStockItems) { - _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); - - //we're not blocking stock/inventory - foreach (var orderStockItem in @event.OrderStockItems) - { - var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); + var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); - catalogItem.RemoveStock(orderStockItem.Units); - } + catalogItem.RemoveStock(orderStockItem.Units); + } - await _catalogContext.SaveChangesAsync(); + await _catalogContext.SaveChangesAsync(); - } } } -} \ No newline at end of file +} diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs index 4ae24de24..925b5fbcc 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs @@ -1,27 +1,26 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events -{ - public record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent - { - public int OrderId { get; } - public IEnumerable OrderStockItems { get; } +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; - public OrderStatusChangedToAwaitingValidationIntegrationEvent(int orderId, - IEnumerable orderStockItems) - { - OrderId = orderId; - OrderStockItems = orderStockItems; - } - } +public record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent +{ + public int OrderId { get; } + public IEnumerable OrderStockItems { get; } - public record OrderStockItem + public OrderStatusChangedToAwaitingValidationIntegrationEvent(int orderId, + IEnumerable orderStockItems) { - public int ProductId { get; } - public int Units { get; } + OrderId = orderId; + OrderStockItems = orderStockItems; + } +} - public OrderStockItem(int productId, int units) - { - ProductId = productId; - Units = units; - } +public record OrderStockItem +{ + public int ProductId { get; } + public int Units { get; } + + public OrderStockItem(int productId, int units) + { + ProductId = productId; + Units = units; } -} \ No newline at end of file +} diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs index b89465f7a..1ca5cc6db 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs @@ -1,18 +1,14 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; + +public record OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent { - using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; - using System.Collections.Generic; + public int OrderId { get; } + public IEnumerable OrderStockItems { get; } - public record OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent + public OrderStatusChangedToPaidIntegrationEvent(int orderId, + IEnumerable orderStockItems) { - public int OrderId { get; } - public IEnumerable OrderStockItems { get; } - - public OrderStatusChangedToPaidIntegrationEvent(int orderId, - IEnumerable orderStockItems) - { - OrderId = orderId; - OrderStockItems = orderStockItems; - } + OrderId = orderId; + OrderStockItems = orderStockItems; } -} \ No newline at end of file +} diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs index 30cfee3c7..30e4dfce2 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs @@ -1,9 +1,8 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events -{ - public record OrderStockConfirmedIntegrationEvent : IntegrationEvent - { - public int OrderId { get; } +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; - public OrderStockConfirmedIntegrationEvent(int orderId) => OrderId = orderId; - } -} \ No newline at end of file +public record OrderStockConfirmedIntegrationEvent : IntegrationEvent +{ + public int OrderId { get; } + + public OrderStockConfirmedIntegrationEvent(int orderId) => OrderId = orderId; +} diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs index 95974dabd..7c8d03b18 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs @@ -1,28 +1,27 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events -{ - public record OrderStockRejectedIntegrationEvent : IntegrationEvent - { - public int OrderId { get; } +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; - public List OrderStockItems { get; } +public record OrderStockRejectedIntegrationEvent : IntegrationEvent +{ + public int OrderId { get; } - public OrderStockRejectedIntegrationEvent(int orderId, - List orderStockItems) - { - OrderId = orderId; - OrderStockItems = orderStockItems; - } - } + public List OrderStockItems { get; } - public record ConfirmedOrderStockItem + public OrderStockRejectedIntegrationEvent(int orderId, + List orderStockItems) { - public int ProductId { get; } - public bool HasStock { get; } + OrderId = orderId; + OrderStockItems = orderStockItems; + } +} - public ConfirmedOrderStockItem(int productId, bool hasStock) - { - ProductId = productId; - HasStock = hasStock; - } +public record ConfirmedOrderStockItem +{ + public int ProductId { get; } + public bool HasStock { get; } + + public ConfirmedOrderStockItem(int productId, bool hasStock) + { + ProductId = productId; + HasStock = hasStock; } -} \ No newline at end of file +} diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs index 978f3f57e..69a821935 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs @@ -1,21 +1,20 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events -{ - // Integration Events notes: - // An Event is “something that has happened in the past”, therefore its name has to be past tense - // An Integration Event is an event that can cause side effects to other microservices, Bounded-Contexts or external systems. - public record ProductPriceChangedIntegrationEvent : IntegrationEvent - { - public int ProductId { get; private init; } +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; + +// Integration Events notes: +// An Event is “something that has happened in the past”, therefore its name has to be past tense +// An Integration Event is an event that can cause side effects to other microservices, Bounded-Contexts or external systems. +public record ProductPriceChangedIntegrationEvent : IntegrationEvent +{ + public int ProductId { get; private init; } - public decimal NewPrice { get; private init; } + public decimal NewPrice { get; private init; } - public decimal OldPrice { get; private init; } + public decimal OldPrice { get; private init; } - public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice) - { - ProductId = productId; - NewPrice = newPrice; - OldPrice = oldPrice; - } + public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice) + { + ProductId = productId; + NewPrice = newPrice; + OldPrice = oldPrice; } } diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs index f53b92cce..f5e78b803 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs @@ -1,8 +1,7 @@ -namespace Catalog.API.IntegrationEvents +namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; + +public interface ICatalogIntegrationEventService { - public interface ICatalogIntegrationEventService - { - Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt); - Task PublishThroughEventBusAsync(IntegrationEvent evt); - } + Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt); + Task PublishThroughEventBusAsync(IntegrationEvent evt); } diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs b/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs index 84d72899e..68223177e 100644 --- a/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs +++ b/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs @@ -1,9 +1,8 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; + +public class CatalogBrand { - public class CatalogBrand - { - public int Id { get; set; } + public int Id { get; set; } - public string Brand { get; set; } - } + public string Brand { get; set; } } diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs b/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs index 0c41b5d77..f5a4f9654 100644 --- a/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs +++ b/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs @@ -1,100 +1,99 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; + +public class CatalogItem { - public class CatalogItem - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string Description { get; set; } + public string Description { get; set; } - public decimal Price { get; set; } + public decimal Price { get; set; } - public string PictureFileName { get; set; } + public string PictureFileName { get; set; } - public string PictureUri { get; set; } + public string PictureUri { get; set; } - public int CatalogTypeId { get; set; } + public int CatalogTypeId { get; set; } - public CatalogType CatalogType { get; set; } + public CatalogType CatalogType { get; set; } - public int CatalogBrandId { get; set; } + public int CatalogBrandId { get; set; } - public CatalogBrand CatalogBrand { get; set; } + public CatalogBrand CatalogBrand { get; set; } - // Quantity in stock - public int AvailableStock { get; set; } + // Quantity in stock + public int AvailableStock { get; set; } - // Available stock at which we should reorder - public int RestockThreshold { get; set; } + // Available stock at which we should reorder + public int RestockThreshold { get; set; } - // Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses) - public int MaxStockThreshold { get; set; } + // Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses) + public int MaxStockThreshold { get; set; } - /// - /// True if item is on reorder - /// - public bool OnReorder { get; set; } + /// + /// True if item is on reorder + /// + public bool OnReorder { get; set; } - public CatalogItem() { } + public CatalogItem() { } - /// - /// Decrements the quantity of a particular item in inventory and ensures the restockThreshold hasn't - /// been breached. If so, a RestockRequest is generated in CheckThreshold. - /// - /// If there is sufficient stock of an item, then the integer returned at the end of this call should be the same as quantityDesired. - /// In the event that there is not sufficient stock available, the method will remove whatever stock is available and return that quantity to the client. - /// In this case, it is the responsibility of the client to determine if the amount that is returned is the same as quantityDesired. - /// It is invalid to pass in a negative number. - /// - /// - /// int: Returns the number actually removed from stock. - /// - public int RemoveStock(int quantityDesired) + /// + /// Decrements the quantity of a particular item in inventory and ensures the restockThreshold hasn't + /// been breached. If so, a RestockRequest is generated in CheckThreshold. + /// + /// If there is sufficient stock of an item, then the integer returned at the end of this call should be the same as quantityDesired. + /// In the event that there is not sufficient stock available, the method will remove whatever stock is available and return that quantity to the client. + /// In this case, it is the responsibility of the client to determine if the amount that is returned is the same as quantityDesired. + /// It is invalid to pass in a negative number. + /// + /// + /// int: Returns the number actually removed from stock. + /// + public int RemoveStock(int quantityDesired) + { + if (AvailableStock == 0) { - if (AvailableStock == 0) - { - throw new CatalogDomainException($"Empty stock, product item {Name} is sold out"); - } + throw new CatalogDomainException($"Empty stock, product item {Name} is sold out"); + } - if (quantityDesired <= 0) - { - throw new CatalogDomainException($"Item units desired should be greater than zero"); - } + if (quantityDesired <= 0) + { + throw new CatalogDomainException($"Item units desired should be greater than zero"); + } - int removed = Math.Min(quantityDesired, this.AvailableStock); + int removed = Math.Min(quantityDesired, this.AvailableStock); - this.AvailableStock -= removed; + this.AvailableStock -= removed; - return removed; - } + return removed; + } + + /// + /// Increments the quantity of a particular item in inventory. + /// + /// int: Returns the quantity that has been added to stock + /// + public int AddStock(int quantity) + { + int original = this.AvailableStock; - /// - /// Increments the quantity of a particular item in inventory. - /// - /// int: Returns the quantity that has been added to stock - /// - public int AddStock(int quantity) + // The quantity that the client is trying to add to stock is greater than what can be physically accommodated in the Warehouse + if ((this.AvailableStock + quantity) > this.MaxStockThreshold) { - int original = this.AvailableStock; - - // The quantity that the client is trying to add to stock is greater than what can be physically accommodated in the Warehouse - if ((this.AvailableStock + quantity) > this.MaxStockThreshold) - { - // For now, this method only adds new units up maximum stock threshold. In an expanded version of this application, we - //could include tracking for the remaining units and store information about overstock elsewhere. - this.AvailableStock += (this.MaxStockThreshold - this.AvailableStock); - } - else - { - this.AvailableStock += quantity; - } - - this.OnReorder = false; - - return this.AvailableStock - original; + // For now, this method only adds new units up maximum stock threshold. In an expanded version of this application, we + //could include tracking for the remaining units and store information about overstock elsewhere. + this.AvailableStock += (this.MaxStockThreshold - this.AvailableStock); } + else + { + this.AvailableStock += quantity; + } + + this.OnReorder = false; + + return this.AvailableStock - original; } -} \ No newline at end of file +} diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogType.cs b/src/Services/Catalog/Catalog.API/Model/CatalogType.cs index 0bc640dee..17754536e 100644 --- a/src/Services/Catalog/Catalog.API/Model/CatalogType.cs +++ b/src/Services/Catalog/Catalog.API/Model/CatalogType.cs @@ -1,9 +1,8 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; + +public class CatalogType { - public class CatalogType - { - public int Id { get; set; } + public int Id { get; set; } - public string Type { get; set; } - } + public string Type { get; set; } } diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index a7b828414..f93877562 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -1,334 +1,333 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API +namespace Microsoft.eShopOnContainers.Services.Catalog.API; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - public IServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddAppInsight(Configuration) - .AddGrpc().Services - .AddCustomMVC(Configuration) - .AddCustomDbContext(Configuration) - .AddCustomOptions(Configuration) - .AddIntegrationServices(Configuration) - .AddEventBus(Configuration) - .AddSwagger(Configuration) - .AddCustomHealthCheck(Configuration); - - var container = new ContainerBuilder(); - container.Populate(services); - - return new AutofacServiceProvider(container.Build()); - } + public IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddAppInsight(Configuration) + .AddGrpc().Services + .AddCustomMVC(Configuration) + .AddCustomDbContext(Configuration) + .AddCustomOptions(Configuration) + .AddIntegrationServices(Configuration) + .AddEventBus(Configuration) + .AddSwagger(Configuration) + .AddCustomHealthCheck(Configuration); + + var container = new ContainerBuilder(); + container.Populate(services); + + return new AutofacServiceProvider(container.Build()); + } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) - { - //Configure logs + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + //Configure logs - //loggerFactory.AddAzureWebAppDiagnostics(); - //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); + //loggerFactory.AddAzureWebAppDiagnostics(); + //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); - var pathBase = Configuration["PATH_BASE"]; + var pathBase = Configuration["PATH_BASE"]; - if (!string.IsNullOrEmpty(pathBase)) + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger().LogDebug("Using PATH BASE '{pathBase}'", pathBase); + app.UsePathBase(pathBase); + } + + app.UseSwagger() + .UseSwaggerUI(c => { - loggerFactory.CreateLogger().LogDebug("Using PATH BASE '{pathBase}'", pathBase); - app.UsePathBase(pathBase); - } - - app.UseSwagger() - .UseSwaggerUI(c => - { - c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Catalog.API V1"); - }); - - app.UseRouting(); - app.UseCors("CorsPolicy"); - app.UseEndpoints(endpoints => + c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Catalog.API V1"); + }); + + app.UseRouting(); + app.UseCors("CorsPolicy"); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + endpoints.MapGet("/_proto/", async ctx => { - endpoints.MapDefaultControllerRoute(); - endpoints.MapControllers(); - endpoints.MapGet("/_proto/", async ctx => + ctx.Response.ContentType = "text/plain"; + using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "catalog.proto"), FileMode.Open, FileAccess.Read); + using var sr = new StreamReader(fs); + while (!sr.EndOfStream) { - ctx.Response.ContentType = "text/plain"; - using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "catalog.proto"), FileMode.Open, FileAccess.Read); - using var sr = new StreamReader(fs); - while (!sr.EndOfStream) + var line = await sr.ReadLineAsync(); + if (line != "/* >>" || line != "<< */") { - var line = await sr.ReadLineAsync(); - if (line != "/* >>" || line != "<< */") - { - await ctx.Response.WriteAsync(line); - } + await ctx.Response.WriteAsync(line); } - }); - endpoints.MapGrpcService(); - endpoints.MapHealthChecks("/hc", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); - endpoints.MapHealthChecks("/liveness", new HealthCheckOptions - { - Predicate = r => r.Name.Contains("self") - }); + } + }); + endpoints.MapGrpcService(); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") }); + }); - ConfigureEventBus(app); - } + ConfigureEventBus(app); + } - protected virtual void ConfigureEventBus(IApplicationBuilder app) - { - var eventBus = app.ApplicationServices.GetRequiredService(); - eventBus.Subscribe(); - eventBus.Subscribe(); - } + protected virtual void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe(); + eventBus.Subscribe(); } +} - public static class CustomExtensionMethods +public static class CustomExtensionMethods +{ + public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) { - public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) - { - services.AddApplicationInsightsTelemetry(configuration); - services.AddApplicationInsightsKubernetesEnricher(); + services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsKubernetesEnricher(); - return services; - } + return services; + } - public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) + { + services.AddControllers(options => { - services.AddControllers(options => - { - options.Filters.Add(typeof(HttpGlobalExceptionFilter)); - }) - .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + }) + .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); - services.AddCors(options => - { - options.AddPolicy("CorsPolicy", - builder => builder - .SetIsOriginAllowed((host) => true) - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials()); - }); + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .SetIsOriginAllowed((host) => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + + return services; + } - return services; - } + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var accountName = configuration.GetValue("AzureStorageAccountName"); + var accountKey = configuration.GetValue("AzureStorageAccountKey"); - public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) - { - var accountName = configuration.GetValue("AzureStorageAccountName"); - var accountKey = configuration.GetValue("AzureStorageAccountKey"); + var hcBuilder = services.AddHealthChecks(); - var hcBuilder = services.AddHealthChecks(); + hcBuilder + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddSqlServer( + configuration["ConnectionString"], + name: "CatalogDB-check", + tags: new string[] { "catalogdb" }); + if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) + { hcBuilder - .AddCheck("self", () => HealthCheckResult.Healthy()) - .AddSqlServer( - configuration["ConnectionString"], - name: "CatalogDB-check", - tags: new string[] { "catalogdb" }); - - if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) - { - hcBuilder - .AddAzureBlobStorage( - $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", - name: "catalog-storage-check", - tags: new string[] { "catalogstorage" }); - } - - if (configuration.GetValue("AzureServiceBusEnabled")) - { - hcBuilder - .AddAzureServiceBusTopic( - configuration["EventBusConnection"], - topicName: "eshop_event_bus", - name: "catalog-servicebus-check", - tags: new string[] { "servicebus" }); - } - else - { - hcBuilder - .AddRabbitMQ( - $"amqp://{configuration["EventBusConnection"]}", - name: "catalog-rabbitmqbus-check", - tags: new string[] { "rabbitmqbus" }); - } - - return services; + .AddAzureBlobStorage( + $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", + name: "catalog-storage-check", + tags: new string[] { "catalogstorage" }); } - public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) + if (configuration.GetValue("AzureServiceBusEnabled")) { - services.AddEntityFrameworkSqlServer() - .AddDbContext(options => - { - options.UseSqlServer(configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); - //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }); - }); + hcBuilder + .AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "catalog-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder + .AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "catalog-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } - services.AddDbContext(options => - { - options.UseSqlServer(configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); - //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }); - }); + return services; + } - return services; - } + public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) + { + services.AddEntityFrameworkSqlServer() + .AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + services.AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + return services; + } - public static IServiceCollection AddCustomOptions(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddCustomOptions(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + services.Configure(options => { - services.Configure(configuration); - services.Configure(options => + options.InvalidModelStateResponseFactory = context => { - options.InvalidModelStateResponseFactory = context => + var problemDetails = new ValidationProblemDetails(context.ModelState) { - var problemDetails = new ValidationProblemDetails(context.ModelState) - { - Instance = context.HttpContext.Request.Path, - Status = StatusCodes.Status400BadRequest, - Detail = "Please refer to the errors property for additional details." - }; + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; - return new BadRequestObjectResult(problemDetails) - { - ContentTypes = { "application/problem+json", "application/problem+xml" } - }; + return new BadRequestObjectResult(problemDetails) + { + ContentTypes = { "application/problem+json", "application/problem+xml" } }; - }); + }; + }); - return services; - } + return services; + } - public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) - { - services.AddSwaggerGen(options => - { - options.SwaggerDoc("v1", new OpenApiInfo - { - Title = "eShopOnContainers - Catalog HTTP API", - Version = "v1", - Description = "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample" - }); + public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) + { + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "eShopOnContainers - Catalog HTTP API", + Version = "v1", + Description = "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample" }); + }); - return services; + return services; - } + } - public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) - { - services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); + public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); - services.AddTransient(); + services.AddTransient(); - if (configuration.GetValue("AzureServiceBusEnabled")) - { - services.AddSingleton(sp => - { - var settings = sp.GetRequiredService>().Value; - var serviceBusConnection = new ServiceBusConnectionStringBuilder(settings.EventBusConnection); - var subscriptionClientName = configuration["SubscriptionClientName"]; - - return new DefaultServiceBusPersisterConnection(serviceBusConnection, subscriptionClientName); - }); - } - else + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => { - services.AddSingleton(sp => - { - var settings = sp.GetRequiredService>().Value; - var logger = sp.GetRequiredService>(); + var settings = sp.GetRequiredService>().Value; + var serviceBusConnection = new ServiceBusConnectionStringBuilder(settings.EventBusConnection); + var subscriptionClientName = configuration["SubscriptionClientName"]; - var factory = new ConnectionFactory() - { - HostName = configuration["EventBusConnection"], - DispatchConsumersAsync = true - }; + return new DefaultServiceBusPersisterConnection(serviceBusConnection, subscriptionClientName); + }); + } + else + { + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var logger = sp.GetRequiredService>(); - if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) - { - factory.UserName = configuration["EventBusUserName"]; - } + var factory = new ConnectionFactory() + { + HostName = configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; - if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) - { - factory.Password = configuration["EventBusPassword"]; - } + if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) + { + factory.UserName = configuration["EventBusUserName"]; + } - var retryCount = 5; - if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) - { - retryCount = int.Parse(configuration["EventBusRetryCount"]); - } + if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) + { + factory.Password = configuration["EventBusPassword"]; + } - return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); - }); - } + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } - return services; + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); } - public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) + return services; + } + + public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("AzureServiceBusEnabled")) { - if (configuration.GetValue("AzureServiceBusEnabled")) + services.AddSingleton(sp => { - services.AddSingleton(sp => - { - var serviceBusPersisterConnection = sp.GetRequiredService(); - var iLifetimeScope = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubcriptionsManager = sp.GetRequiredService(); + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); - return new EventBusServiceBus(serviceBusPersisterConnection, logger, - eventBusSubcriptionsManager, iLifetimeScope); - }); + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubcriptionsManager, iLifetimeScope); + }); - } - else + } + else + { + services.AddSingleton(sp => { - services.AddSingleton(sp => + var subscriptionClientName = configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) { - var subscriptionClientName = configuration["SubscriptionClientName"]; - var rabbitMQPersistentConnection = sp.GetRequiredService(); - var iLifetimeScope = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubcriptionsManager = sp.GetRequiredService(); - - var retryCount = 5; - if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) - { - retryCount = int.Parse(configuration["EventBusRetryCount"]); - } + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } - return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); - }); - } + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + } - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); - return services; - } + return services; } } diff --git a/src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs b/src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs index 177b428dc..d6673012b 100644 --- a/src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs +++ b/src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs @@ -1,21 +1,20 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel -{ - public class PaginatedItemsViewModel where TEntity : class - { - public int PageIndex { get; private set; } +namespace Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; + +public class PaginatedItemsViewModel where TEntity : class +{ + public int PageIndex { get; private set; } - public int PageSize { get; private set; } + public int PageSize { get; private set; } - public long Count { get; private set; } + public long Count { get; private set; } - public IEnumerable Data { get; private set; } + public IEnumerable Data { get; private set; } - public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable data) - { - PageIndex = pageIndex; - PageSize = pageSize; - Count = count; - Data = data; - } + public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable data) + { + PageIndex = pageIndex; + PageSize = pageSize; + Count = count; + Data = data; } }