Refactored namespace statements in catalog.api project
This commit is contained in:
		
							parent
							
								
									a598bfde6c
								
							
						
					
					
						commit
						986262a833
					
				| @ -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; } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|     public CatalogController(CatalogContext context, IOptionsSnapshot<CatalogSettings> settings, ICatalogIntegrationEventService catalogIntegrationEventService) | ||||
|     { | ||||
|         private readonly CatalogContext _catalogContext; | ||||
|         private readonly CatalogSettings _settings; | ||||
|         private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; | ||||
|         _catalogContext = context ?? throw new ArgumentNullException(nameof(context)); | ||||
|         _catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); | ||||
|         _settings = settings.Value; | ||||
| 
 | ||||
|         public CatalogController(CatalogContext context, IOptionsSnapshot<CatalogSettings> settings, ICatalogIntegrationEventService catalogIntegrationEventService) | ||||
|         context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; | ||||
|     } | ||||
| 
 | ||||
|     // GET api/v1/[controller]/items[?pageSize=3&pageIndex=10] | ||||
|     [HttpGet] | ||||
|     [Route("items")] | ||||
|     [ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|     [ProducesResponseType(typeof(IEnumerable<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|     [ProducesResponseType((int)HttpStatusCode.BadRequest)] | ||||
|     public async Task<IActionResult> ItemsAsync([FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0, string ids = null) | ||||
|     { | ||||
|         if (!string.IsNullOrEmpty(ids)) | ||||
|         { | ||||
|             _catalogContext = context ?? throw new ArgumentNullException(nameof(context)); | ||||
|             _catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); | ||||
|             _settings = settings.Value; | ||||
|             var items = await GetItemsByIdsAsync(ids); | ||||
| 
 | ||||
|             context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; | ||||
|             if (!items.Any()) | ||||
|             { | ||||
|                 return BadRequest("ids value invalid. Must be comma-separated list of numbers"); | ||||
|             } | ||||
| 
 | ||||
|             return Ok(items); | ||||
|         } | ||||
| 
 | ||||
|         // GET api/v1/[controller]/items[?pageSize=3&pageIndex=10] | ||||
|         [HttpGet] | ||||
|         [Route("items")] | ||||
|         [ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|         [ProducesResponseType(typeof(IEnumerable<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|         [ProducesResponseType((int)HttpStatusCode.BadRequest)] | ||||
|         public async Task<IActionResult> ItemsAsync([FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0, string ids = null) | ||||
|         { | ||||
|             if (!string.IsNullOrEmpty(ids)) | ||||
|             { | ||||
|                 var items = await GetItemsByIdsAsync(ids); | ||||
|         var totalItems = await _catalogContext.CatalogItems | ||||
|             .LongCountAsync(); | ||||
| 
 | ||||
|                 if (!items.Any()) | ||||
|                 { | ||||
|                     return BadRequest("ids value invalid. Must be comma-separated list of numbers"); | ||||
|                 } | ||||
|         var itemsOnPage = await _catalogContext.CatalogItems | ||||
|             .OrderBy(c => c.Name) | ||||
|             .Skip(pageSize * pageIndex) | ||||
|             .Take(pageSize) | ||||
|             .ToListAsync(); | ||||
| 
 | ||||
|                 return Ok(items); | ||||
|             } | ||||
|         /* The "awesome" fix for testing Devspaces */ | ||||
| 
 | ||||
|             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; | ||||
|             } | ||||
| 
 | ||||
|             */ | ||||
| 
 | ||||
|             itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|             var model = new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | ||||
| 
 | ||||
|             return Ok(model); | ||||
|         /* | ||||
|         foreach (var pr in itemsOnPage) { | ||||
|             pr.Name = "Awesome " + pr.Name; | ||||
|         } | ||||
| 
 | ||||
|         private async Task<List<CatalogItem>> GetItemsByIdsAsync(string ids) | ||||
|         */ | ||||
| 
 | ||||
|         itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|         var model = new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | ||||
| 
 | ||||
|         return Ok(model); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<List<CatalogItem>> 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)); | ||||
| 
 | ||||
|             if (!numIds.All(nid => nid.Ok)) | ||||
|             { | ||||
|                 return new List<CatalogItem>(); | ||||
|             } | ||||
| 
 | ||||
|             var idsToSelect = numIds | ||||
|                 .Select(id => id.Value); | ||||
| 
 | ||||
|             var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); | ||||
| 
 | ||||
|             items = ChangeUriPlaceholder(items); | ||||
| 
 | ||||
|             return items; | ||||
|             return new List<CatalogItem>(); | ||||
|         } | ||||
| 
 | ||||
|         [HttpGet] | ||||
|         [Route("items/{id:int}")] | ||||
|         [ProducesResponseType((int)HttpStatusCode.NotFound)] | ||||
|         [ProducesResponseType((int)HttpStatusCode.BadRequest)] | ||||
|         [ProducesResponseType(typeof(CatalogItem), (int)HttpStatusCode.OK)] | ||||
|         public async Task<ActionResult<CatalogItem>> ItemByIdAsync(int id) | ||||
|         var idsToSelect = numIds | ||||
|             .Select(id => id.Value); | ||||
| 
 | ||||
|         var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); | ||||
| 
 | ||||
|         items = ChangeUriPlaceholder(items); | ||||
| 
 | ||||
|         return items; | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet] | ||||
|     [Route("items/{id:int}")] | ||||
|     [ProducesResponseType((int)HttpStatusCode.NotFound)] | ||||
|     [ProducesResponseType((int)HttpStatusCode.BadRequest)] | ||||
|     [ProducesResponseType(typeof(CatalogItem), (int)HttpStatusCode.OK)] | ||||
|     public async Task<ActionResult<CatalogItem>> ItemByIdAsync(int id) | ||||
|     { | ||||
|         if (id <= 0) | ||||
|         { | ||||
|             if (id <= 0) | ||||
|             { | ||||
|                 return BadRequest(); | ||||
|             } | ||||
|             return BadRequest(); | ||||
|         } | ||||
| 
 | ||||
|             var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == id); | ||||
|         var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == id); | ||||
| 
 | ||||
|             var baseUri = _settings.PicBaseUrl; | ||||
|             var azureStorageEnabled = _settings.AzureStorageEnabled; | ||||
|         var baseUri = _settings.PicBaseUrl; | ||||
|         var azureStorageEnabled = _settings.AzureStorageEnabled; | ||||
| 
 | ||||
|             item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | ||||
|         item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | ||||
| 
 | ||||
|             if (item != null) | ||||
|             { | ||||
|                 return item; | ||||
|             } | ||||
|         if (item != null) | ||||
|         { | ||||
|             return item; | ||||
|         } | ||||
| 
 | ||||
|         return NotFound(); | ||||
|     } | ||||
| 
 | ||||
|     // GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] | ||||
|     [HttpGet] | ||||
|     [Route("items/withname/{name:minlength(1)}")] | ||||
|     [ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|     public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsWithNameAsync(string name, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) | ||||
|     { | ||||
|         var totalItems = await _catalogContext.CatalogItems | ||||
|             .Where(c => c.Name.StartsWith(name)) | ||||
|             .LongCountAsync(); | ||||
| 
 | ||||
|         var itemsOnPage = await _catalogContext.CatalogItems | ||||
|             .Where(c => c.Name.StartsWith(name)) | ||||
|             .Skip(pageSize * pageIndex) | ||||
|             .Take(pageSize) | ||||
|             .ToListAsync(); | ||||
| 
 | ||||
|         itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|         return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | ||||
|     } | ||||
| 
 | ||||
|     // GET api/v1/[controller]/items/type/1/brand[?pageSize=3&pageIndex=10] | ||||
|     [HttpGet] | ||||
|     [Route("items/type/{catalogTypeId}/brand/{catalogBrandId:int?}")] | ||||
|     [ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|     public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsByTypeIdAndBrandIdAsync(int catalogTypeId, int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) | ||||
|     { | ||||
|         var root = (IQueryable<CatalogItem>)_catalogContext.CatalogItems; | ||||
| 
 | ||||
|         root = root.Where(ci => ci.CatalogTypeId == catalogTypeId); | ||||
| 
 | ||||
|         if (catalogBrandId.HasValue) | ||||
|         { | ||||
|             root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); | ||||
|         } | ||||
| 
 | ||||
|         var totalItems = await root | ||||
|             .LongCountAsync(); | ||||
| 
 | ||||
|         var itemsOnPage = await root | ||||
|             .Skip(pageSize * pageIndex) | ||||
|             .Take(pageSize) | ||||
|             .ToListAsync(); | ||||
| 
 | ||||
|         itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|         return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | ||||
|     } | ||||
| 
 | ||||
|     // GET api/v1/[controller]/items/type/all/brand[?pageSize=3&pageIndex=10] | ||||
|     [HttpGet] | ||||
|     [Route("items/type/all/brand/{catalogBrandId:int?}")] | ||||
|     [ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|     public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsByBrandIdAsync(int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) | ||||
|     { | ||||
|         var root = (IQueryable<CatalogItem>)_catalogContext.CatalogItems; | ||||
| 
 | ||||
|         if (catalogBrandId.HasValue) | ||||
|         { | ||||
|             root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); | ||||
|         } | ||||
| 
 | ||||
|         var totalItems = await root | ||||
|             .LongCountAsync(); | ||||
| 
 | ||||
|         var itemsOnPage = await root | ||||
|             .Skip(pageSize * pageIndex) | ||||
|             .Take(pageSize) | ||||
|             .ToListAsync(); | ||||
| 
 | ||||
|         itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|         return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | ||||
|     } | ||||
| 
 | ||||
|     // GET api/v1/[controller]/CatalogTypes | ||||
|     [HttpGet] | ||||
|     [Route("catalogtypes")] | ||||
|     [ProducesResponseType(typeof(List<CatalogType>), (int)HttpStatusCode.OK)] | ||||
|     public async Task<ActionResult<List<CatalogType>>> CatalogTypesAsync() | ||||
|     { | ||||
|         return await _catalogContext.CatalogTypes.ToListAsync(); | ||||
|     } | ||||
| 
 | ||||
|     // GET api/v1/[controller]/CatalogBrands | ||||
|     [HttpGet] | ||||
|     [Route("catalogbrands")] | ||||
|     [ProducesResponseType(typeof(List<CatalogBrand>), (int)HttpStatusCode.OK)] | ||||
|     public async Task<ActionResult<List<CatalogBrand>>> CatalogBrandsAsync() | ||||
|     { | ||||
|         return await _catalogContext.CatalogBrands.ToListAsync(); | ||||
|     } | ||||
| 
 | ||||
|     //PUT api/v1/[controller]/items | ||||
|     [Route("items")] | ||||
|     [HttpPut] | ||||
|     [ProducesResponseType((int)HttpStatusCode.NotFound)] | ||||
|     [ProducesResponseType((int)HttpStatusCode.Created)] | ||||
|     public async Task<ActionResult> UpdateProductAsync([FromBody] CatalogItem productToUpdate) | ||||
|     { | ||||
|         var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); | ||||
| 
 | ||||
|         if (catalogItem == null) | ||||
|         { | ||||
|             return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); | ||||
|         } | ||||
| 
 | ||||
|         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 | ||||
|         { | ||||
|             //Create Integration Event to be published through the Event Bus | ||||
|             var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); | ||||
| 
 | ||||
|             // 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 = productToUpdate.Id }, null); | ||||
|     } | ||||
| 
 | ||||
|     //POST api/v1/[controller]/items | ||||
|     [Route("items")] | ||||
|     [HttpPost] | ||||
|     [ProducesResponseType((int)HttpStatusCode.Created)] | ||||
|     public async Task<ActionResult> CreateProductAsync([FromBody] CatalogItem product) | ||||
|     { | ||||
|         var item = new CatalogItem | ||||
|         { | ||||
|             CatalogBrandId = product.CatalogBrandId, | ||||
|             CatalogTypeId = product.CatalogTypeId, | ||||
|             Description = product.Description, | ||||
|             Name = product.Name, | ||||
|             PictureFileName = product.PictureFileName, | ||||
|             Price = product.Price | ||||
|         }; | ||||
| 
 | ||||
|         _catalogContext.CatalogItems.Add(item); | ||||
| 
 | ||||
|         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<ActionResult> DeleteProductAsync(int id) | ||||
|     { | ||||
|         var product = _catalogContext.CatalogItems.SingleOrDefault(x => x.Id == id); | ||||
| 
 | ||||
|         if (product == null) | ||||
|         { | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         // GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] | ||||
|         [HttpGet] | ||||
|         [Route("items/withname/{name:minlength(1)}")] | ||||
|         [ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|         public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsWithNameAsync(string name, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) | ||||
|         _catalogContext.CatalogItems.Remove(product); | ||||
| 
 | ||||
|         await _catalogContext.SaveChangesAsync(); | ||||
| 
 | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items) | ||||
|     { | ||||
|         var baseUri = _settings.PicBaseUrl; | ||||
|         var azureStorageEnabled = _settings.AzureStorageEnabled; | ||||
| 
 | ||||
|         foreach (var item in items) | ||||
|         { | ||||
|             var totalItems = await _catalogContext.CatalogItems | ||||
|                 .Where(c => c.Name.StartsWith(name)) | ||||
|                 .LongCountAsync(); | ||||
| 
 | ||||
|             var itemsOnPage = await _catalogContext.CatalogItems | ||||
|                 .Where(c => c.Name.StartsWith(name)) | ||||
|                 .Skip(pageSize * pageIndex) | ||||
|                 .Take(pageSize) | ||||
|                 .ToListAsync(); | ||||
| 
 | ||||
|             itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|             return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | ||||
|             item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | ||||
|         } | ||||
| 
 | ||||
|         // GET api/v1/[controller]/items/type/1/brand[?pageSize=3&pageIndex=10] | ||||
|         [HttpGet] | ||||
|         [Route("items/type/{catalogTypeId}/brand/{catalogBrandId:int?}")] | ||||
|         [ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|         public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsByTypeIdAndBrandIdAsync(int catalogTypeId, int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) | ||||
|         { | ||||
|             var root = (IQueryable<CatalogItem>)_catalogContext.CatalogItems; | ||||
| 
 | ||||
|             root = root.Where(ci => ci.CatalogTypeId == catalogTypeId); | ||||
| 
 | ||||
|             if (catalogBrandId.HasValue) | ||||
|             { | ||||
|                 root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); | ||||
|             } | ||||
| 
 | ||||
|             var totalItems = await root | ||||
|                 .LongCountAsync(); | ||||
| 
 | ||||
|             var itemsOnPage = await root | ||||
|                 .Skip(pageSize * pageIndex) | ||||
|                 .Take(pageSize) | ||||
|                 .ToListAsync(); | ||||
| 
 | ||||
|             itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|             return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | ||||
|         } | ||||
| 
 | ||||
|         // GET api/v1/[controller]/items/type/all/brand[?pageSize=3&pageIndex=10] | ||||
|         [HttpGet] | ||||
|         [Route("items/type/all/brand/{catalogBrandId:int?}")] | ||||
|         [ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | ||||
|         public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsByBrandIdAsync(int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) | ||||
|         { | ||||
|             var root = (IQueryable<CatalogItem>)_catalogContext.CatalogItems; | ||||
| 
 | ||||
|             if (catalogBrandId.HasValue) | ||||
|             { | ||||
|                 root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); | ||||
|             } | ||||
| 
 | ||||
|             var totalItems = await root | ||||
|                 .LongCountAsync(); | ||||
| 
 | ||||
|             var itemsOnPage = await root | ||||
|                 .Skip(pageSize * pageIndex) | ||||
|                 .Take(pageSize) | ||||
|                 .ToListAsync(); | ||||
| 
 | ||||
|             itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|             return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | ||||
|         } | ||||
| 
 | ||||
|         // GET api/v1/[controller]/CatalogTypes | ||||
|         [HttpGet] | ||||
|         [Route("catalogtypes")] | ||||
|         [ProducesResponseType(typeof(List<CatalogType>), (int)HttpStatusCode.OK)] | ||||
|         public async Task<ActionResult<List<CatalogType>>> CatalogTypesAsync() | ||||
|         { | ||||
|             return await _catalogContext.CatalogTypes.ToListAsync(); | ||||
|         } | ||||
| 
 | ||||
|         // GET api/v1/[controller]/CatalogBrands | ||||
|         [HttpGet] | ||||
|         [Route("catalogbrands")] | ||||
|         [ProducesResponseType(typeof(List<CatalogBrand>), (int)HttpStatusCode.OK)] | ||||
|         public async Task<ActionResult<List<CatalogBrand>>> CatalogBrandsAsync() | ||||
|         { | ||||
|             return await _catalogContext.CatalogBrands.ToListAsync(); | ||||
|         } | ||||
| 
 | ||||
|         //PUT api/v1/[controller]/items | ||||
|         [Route("items")] | ||||
|         [HttpPut] | ||||
|         [ProducesResponseType((int)HttpStatusCode.NotFound)] | ||||
|         [ProducesResponseType((int)HttpStatusCode.Created)] | ||||
|         public async Task<ActionResult> UpdateProductAsync([FromBody] CatalogItem productToUpdate) | ||||
|         { | ||||
|             var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); | ||||
| 
 | ||||
|             if (catalogItem == null) | ||||
|             { | ||||
|                 return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); | ||||
|             } | ||||
| 
 | ||||
|             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 | ||||
|             { | ||||
|                 //Create Integration Event to be published through the Event Bus | ||||
|                 var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); | ||||
| 
 | ||||
|                 // 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 = productToUpdate.Id }, null); | ||||
|         } | ||||
| 
 | ||||
|         //POST api/v1/[controller]/items | ||||
|         [Route("items")] | ||||
|         [HttpPost] | ||||
|         [ProducesResponseType((int)HttpStatusCode.Created)] | ||||
|         public async Task<ActionResult> CreateProductAsync([FromBody] CatalogItem product) | ||||
|         { | ||||
|             var item = new CatalogItem | ||||
|             { | ||||
|                 CatalogBrandId = product.CatalogBrandId, | ||||
|                 CatalogTypeId = product.CatalogTypeId, | ||||
|                 Description = product.Description, | ||||
|                 Name = product.Name, | ||||
|                 PictureFileName = product.PictureFileName, | ||||
|                 Price = product.Price | ||||
|             }; | ||||
| 
 | ||||
|             _catalogContext.CatalogItems.Add(item); | ||||
| 
 | ||||
|             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<ActionResult> DeleteProductAsync(int id) | ||||
|         { | ||||
|             var product = _catalogContext.CatalogItems.SingleOrDefault(x => x.Id == id); | ||||
| 
 | ||||
|             if (product == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             _catalogContext.CatalogItems.Remove(product); | ||||
| 
 | ||||
|             await _catalogContext.SaveChangesAsync(); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items) | ||||
|         { | ||||
|             var baseUri = _settings.PicBaseUrl; | ||||
|             var azureStorageEnabled = _settings.AzureStorageEnabled; | ||||
| 
 | ||||
|             foreach (var item in items) | ||||
|             { | ||||
|                 item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | ||||
|             } | ||||
| 
 | ||||
|             return items; | ||||
|         } | ||||
|         return items; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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: /<controller>/ | ||||
|     public IActionResult Index() | ||||
|     { | ||||
|         // GET: /<controller>/ | ||||
|         public IActionResult Index() | ||||
|         { | ||||
|             return new RedirectResult("~/swagger"); | ||||
|         } | ||||
|         return new RedirectResult("~/swagger"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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: /<controller>/ | ||||
|     public async Task<ActionResult> 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: /<controller>/ | ||||
|         public async Task<ActionResult> GetImageAsync(int catalogItemId) | ||||
|         var item = await _catalogContext.CatalogItems | ||||
|             .SingleOrDefaultAsync(ci => ci.Id == catalogItemId); | ||||
| 
 | ||||
|         if (item != null) | ||||
|         { | ||||
|             if (catalogItemId <= 0) | ||||
|             { | ||||
|                 return BadRequest(); | ||||
|             } | ||||
|             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); | ||||
| 
 | ||||
|                 var buffer = await System.IO.File.ReadAllBytesAsync(path); | ||||
| 
 | ||||
|                 return File(buffer, mimetype); | ||||
|             } | ||||
| 
 | ||||
|             return NotFound(); | ||||
|             return File(buffer, mimetype); | ||||
|         } | ||||
| 
 | ||||
|         private string GetImageMimeTypeFromImageFileExtension(string extension) | ||||
|         return NotFound(); | ||||
|     } | ||||
| 
 | ||||
|     private string GetImageMimeTypeFromImageFileExtension(string extension) | ||||
|     { | ||||
|         string mimetype; | ||||
| 
 | ||||
|         switch (extension) | ||||
|         { | ||||
|             string 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; | ||||
|             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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<IConfiguration>(); | ||||
|             var orchestratorType = cfg.GetValue<string>("OrchestratorType"); | ||||
|             return orchestratorType?.ToUpper() == "K8S"; | ||||
|         } | ||||
|         var cfg = host.Services.GetService<IConfiguration>(); | ||||
|         var orchestratorType = cfg.GetValue<string>("OrchestratorType"); | ||||
|         return orchestratorType?.ToUpper() == "K8S"; | ||||
|     } | ||||
| 
 | ||||
|         public static IHost MigrateDbContext<TContext>(this IHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext | ||||
|         { | ||||
|             var underK8s = host.IsInKubernetes(); | ||||
|     public static IHost MigrateDbContext<TContext>(this IHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext | ||||
|     { | ||||
|         var underK8s = host.IsInKubernetes(); | ||||
| 
 | ||||
|             using (var scope = host.Services.CreateScope()) | ||||
|         using (var scope = host.Services.CreateScope()) | ||||
|         { | ||||
|             var services = scope.ServiceProvider; | ||||
| 
 | ||||
|             var logger = services.GetRequiredService<ILogger<TContext>>(); | ||||
| 
 | ||||
|             var context = services.GetService<TContext>(); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 var services = scope.ServiceProvider; | ||||
|                 logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); | ||||
| 
 | ||||
|                 var logger = services.GetRequiredService<ILogger<TContext>>(); | ||||
| 
 | ||||
|                 var context = services.GetService<TContext>(); | ||||
| 
 | ||||
|                 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<SqlException>() | ||||
|                              .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)); | ||||
|                     } | ||||
| 
 | ||||
|                     logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); | ||||
|                     InvokeSeeder(seeder, context, services); | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 else | ||||
|                 { | ||||
|                     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 | ||||
|                     } | ||||
|                     var retry = Policy.Handle<SqlException>() | ||||
|                             .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)); | ||||
|                 } | ||||
| 
 | ||||
|                 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) | ||||
|                 { | ||||
|                     throw;          // Rethrow under k8s because we rely on k8s to re-run the pod | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return host; | ||||
|         } | ||||
| 
 | ||||
|         private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) | ||||
|             where TContext : DbContext | ||||
|         { | ||||
|             context.Database.Migrate(); | ||||
|             seeder(context, services); | ||||
|         } | ||||
|         return host; | ||||
|     } | ||||
| 
 | ||||
|     private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) | ||||
|         where TContext : DbContext | ||||
|     { | ||||
|         context.Database.Migrate(); | ||||
|         seeder(context, services); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<SelectTryResult<TSource, TResult>> SelectTry<TSource, TResult>(this IEnumerable<TSource> enumerable, Func<TSource, TResult> selector) | ||||
|     { | ||||
|         public static IEnumerable<SelectTryResult<TSource, TResult>> SelectTry<TSource, TResult>(this IEnumerable<TSource> enumerable, Func<TSource, TResult> selector) | ||||
|         foreach (TSource element in enumerable) | ||||
|         { | ||||
|             foreach (TSource element in enumerable) | ||||
|             SelectTryResult<TSource, TResult> returnedValue; | ||||
|             try | ||||
|             { | ||||
|                 SelectTryResult<TSource, TResult> returnedValue; | ||||
|                 try | ||||
|                 { | ||||
|                     returnedValue = new SelectTryResult<TSource, TResult>(element, selector(element), null); | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     returnedValue = new SelectTryResult<TSource, TResult>(element, default(TResult), ex); | ||||
|                 } | ||||
|                 yield return returnedValue; | ||||
|                 returnedValue = new SelectTryResult<TSource, TResult>(element, selector(element), null); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<Exception, TResult> exceptionHandler) | ||||
|         { | ||||
|             return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.CaughtException)); | ||||
|         } | ||||
| 
 | ||||
|         public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<TSource, Exception, TResult> exceptionHandler) | ||||
|         { | ||||
|             return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.Source, x.CaughtException)); | ||||
|         } | ||||
| 
 | ||||
|         public class SelectTryResult<TSource, TResult> | ||||
|         { | ||||
|             internal SelectTryResult(TSource source, TResult result, Exception exception) | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 Source = source; | ||||
|                 Result = result; | ||||
|                 CaughtException = exception; | ||||
|                 returnedValue = new SelectTryResult<TSource, TResult>(element, default(TResult), ex); | ||||
|             } | ||||
| 
 | ||||
|             public TSource Source { get; private set; } | ||||
|             public TResult Result { get; private set; } | ||||
|             public Exception CaughtException { get; private set; } | ||||
|             yield return returnedValue; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<Exception, TResult> exceptionHandler) | ||||
|     { | ||||
|         return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.CaughtException)); | ||||
|     } | ||||
| 
 | ||||
|     public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<TSource, Exception, TResult> exceptionHandler) | ||||
|     { | ||||
|         return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.Source, x.CaughtException)); | ||||
|     } | ||||
| 
 | ||||
|     public class SelectTryResult<TSource, TResult> | ||||
|     { | ||||
|         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; } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<IConfiguration>(); | ||||
|             var orchestratorType = cfg.GetValue<string>("OrchestratorType"); | ||||
|             return orchestratorType?.ToUpper() == "K8S"; | ||||
|         } | ||||
|         var cfg = host.Services.GetService<IConfiguration>(); | ||||
|         var orchestratorType = cfg.GetValue<string>("OrchestratorType"); | ||||
|         return orchestratorType?.ToUpper() == "K8S"; | ||||
|     } | ||||
| 
 | ||||
|         public static IWebHost MigrateDbContext<TContext>(this IWebHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext | ||||
|         { | ||||
|             var underK8s = host.IsInKubernetes(); | ||||
|     public static IWebHost MigrateDbContext<TContext>(this IWebHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext | ||||
|     { | ||||
|         var underK8s = host.IsInKubernetes(); | ||||
| 
 | ||||
|             using (var scope = host.Services.CreateScope()) | ||||
|         using (var scope = host.Services.CreateScope()) | ||||
|         { | ||||
|             var services = scope.ServiceProvider; | ||||
| 
 | ||||
|             var logger = services.GetRequiredService<ILogger<TContext>>(); | ||||
| 
 | ||||
|             var context = services.GetService<TContext>(); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 var services = scope.ServiceProvider; | ||||
|                 logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); | ||||
| 
 | ||||
|                 var logger = services.GetRequiredService<ILogger<TContext>>(); | ||||
| 
 | ||||
|                 var context = services.GetService<TContext>(); | ||||
| 
 | ||||
|                 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<SqlException>() | ||||
|                              .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)); | ||||
|                     } | ||||
| 
 | ||||
|                     logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); | ||||
|                     InvokeSeeder(seeder, context, services); | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 else | ||||
|                 { | ||||
|                     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 | ||||
|                     } | ||||
|                     var retry = Policy.Handle<SqlException>() | ||||
|                             .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)); | ||||
|                 } | ||||
| 
 | ||||
|                 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) | ||||
|                 { | ||||
|                     throw;          // Rethrow under k8s because we rely on k8s to re-run the pod | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return host; | ||||
|         } | ||||
| 
 | ||||
|         private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) | ||||
|             where TContext : DbContext | ||||
|         { | ||||
|             context.Database.Migrate(); | ||||
|             seeder(context, services); | ||||
|         } | ||||
|         return host; | ||||
|     } | ||||
| 
 | ||||
|     private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) | ||||
|         where TContext : DbContext | ||||
|     { | ||||
|         context.Database.Migrate(); | ||||
|         seeder(context, services); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| @ -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<CatalogSettings> settings, ILogger<CatalogService> 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<CatalogSettings> settings, ILogger<CatalogService> logger) | ||||
|     public override async Task<CatalogItemResponse> 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; | ||||
|         } | ||||
| 
 | ||||
|         public override async Task<CatalogItemResponse> 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); | ||||
| 
 | ||||
|             if (item != null) | ||||
|             { | ||||
|                 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"); | ||||
|             context.Status = new Status(StatusCode.FailedPrecondition, $"Id must be > 0 (received {request.Id})"); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         public override async Task<PaginatedItemsResponse> GetItemsByIds(CatalogItemsRequest request, ServerCallContext context) | ||||
|         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 (!string.IsNullOrEmpty(request.Ids)) | ||||
|             return new CatalogItemResponse() | ||||
|             { | ||||
|                 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); | ||||
| 
 | ||||
|                 return this.MapToResponse(items); | ||||
|             } | ||||
| 
 | ||||
|             var totalItems = await _catalogContext.CatalogItems | ||||
|                 .LongCountAsync(); | ||||
| 
 | ||||
|             var itemsOnPage = await _catalogContext.CatalogItems | ||||
|                 .OrderBy(c => c.Name) | ||||
|                 .Skip(request.PageSize * request.PageIndex) | ||||
|                 .Take(request.PageSize) | ||||
|                 .ToListAsync(); | ||||
| 
 | ||||
|             /* The "awesome" fix for testing Devspaces */ | ||||
| 
 | ||||
|             /* | ||||
|             foreach (var pr in itemsOnPage) { | ||||
|                 pr.Name = "Awesome " + pr.Name; | ||||
|             } | ||||
| 
 | ||||
|             */ | ||||
| 
 | ||||
|             itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|             var model = this.MapToResponse(itemsOnPage, totalItems, request.PageIndex, request.PageSize); | ||||
|             context.Status = new Status(StatusCode.OK, string.Empty); | ||||
| 
 | ||||
|             return model; | ||||
|         } | ||||
| 
 | ||||
|         private PaginatedItemsResponse MapToResponse(List<CatalogItem> items) | ||||
|         { | ||||
|             return this.MapToResponse(items, items.Count, 1, items.Count); | ||||
|         } | ||||
| 
 | ||||
|         private PaginatedItemsResponse MapToResponse(List<CatalogItem> items, long count, int pageIndex, int pageSize) | ||||
|         { | ||||
|             var result = new PaginatedItemsResponse() | ||||
|             { | ||||
|                 Count = count, | ||||
|                 PageIndex = pageIndex, | ||||
|                 PageSize = pageSize, | ||||
|                 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 | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|             items.ForEach(i => | ||||
|         context.Status = new Status(StatusCode.NotFound, $"Product with id {request.Id} do not exist"); | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public override async Task<PaginatedItemsResponse> GetItemsByIds(CatalogItemsRequest request, ServerCallContext context) | ||||
|     { | ||||
|         if (!string.IsNullOrEmpty(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); | ||||
| 
 | ||||
|             return this.MapToResponse(items); | ||||
|         } | ||||
| 
 | ||||
|         var totalItems = await _catalogContext.CatalogItems | ||||
|             .LongCountAsync(); | ||||
| 
 | ||||
|         var itemsOnPage = await _catalogContext.CatalogItems | ||||
|             .OrderBy(c => c.Name) | ||||
|             .Skip(request.PageSize * request.PageIndex) | ||||
|             .Take(request.PageSize) | ||||
|             .ToListAsync(); | ||||
| 
 | ||||
|         /* The "awesome" fix for testing Devspaces */ | ||||
| 
 | ||||
|         /* | ||||
|         foreach (var pr in itemsOnPage) { | ||||
|             pr.Name = "Awesome " + pr.Name; | ||||
|         } | ||||
| 
 | ||||
|         */ | ||||
| 
 | ||||
|         itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | ||||
| 
 | ||||
|         var model = this.MapToResponse(itemsOnPage, totalItems, request.PageIndex, request.PageSize); | ||||
|         context.Status = new Status(StatusCode.OK, string.Empty); | ||||
| 
 | ||||
|         return model; | ||||
|     } | ||||
| 
 | ||||
|     private PaginatedItemsResponse MapToResponse(List<CatalogItem> items) | ||||
|     { | ||||
|         return this.MapToResponse(items, items.Count, 1, items.Count); | ||||
|     } | ||||
| 
 | ||||
|     private PaginatedItemsResponse MapToResponse(List<CatalogItem> items, long count, int pageIndex, int pageSize) | ||||
|     { | ||||
|         var result = new PaginatedItemsResponse() | ||||
|         { | ||||
|             Count = count, | ||||
|             PageIndex = pageIndex, | ||||
|             PageSize = pageSize, | ||||
|         }; | ||||
| 
 | ||||
|         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<List<CatalogItem>> GetItemsByIdsAsync(string ids) | ||||
|     private async Task<List<CatalogItem>> 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)); | ||||
| 
 | ||||
|             if (!numIds.All(nid => nid.Ok)) | ||||
|             { | ||||
|                 return new List<CatalogItem>(); | ||||
|             } | ||||
| 
 | ||||
|             var idsToSelect = numIds | ||||
|                 .Select(id => id.Value); | ||||
| 
 | ||||
|             var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); | ||||
| 
 | ||||
|             items = ChangeUriPlaceholder(items); | ||||
| 
 | ||||
|             return items; | ||||
|             return new List<CatalogItem>(); | ||||
|         } | ||||
| 
 | ||||
|         private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items) | ||||
|         var idsToSelect = numIds | ||||
|             .Select(id => id.Value); | ||||
| 
 | ||||
|         var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); | ||||
| 
 | ||||
|         items = ChangeUriPlaceholder(items); | ||||
| 
 | ||||
|         return items; | ||||
|     } | ||||
| 
 | ||||
|     private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items) | ||||
|     { | ||||
|         var baseUri = _settings.PicBaseUrl; | ||||
|         var azureStorageEnabled = _settings.AzureStorageEnabled; | ||||
| 
 | ||||
|         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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,33 +1,32 @@ | ||||
| using Microsoft.eShopOnContainers.Services.Catalog.API.Model; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure | ||||
| {    | ||||
|     public class CatalogContext : DbContext | ||||
|     { | ||||
|         public CatalogContext(DbContextOptions<CatalogContext> options) : base(options) | ||||
|         { | ||||
|         } | ||||
|         public DbSet<CatalogItem> CatalogItems { get; set; } | ||||
|         public DbSet<CatalogBrand> CatalogBrands { get; set; } | ||||
|         public DbSet<CatalogType> CatalogTypes { get; set; } | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | ||||
| 
 | ||||
|         protected override void OnModelCreating(ModelBuilder builder) | ||||
|         { | ||||
|             builder.ApplyConfiguration(new CatalogBrandEntityTypeConfiguration()); | ||||
|             builder.ApplyConfiguration(new CatalogTypeEntityTypeConfiguration()); | ||||
|             builder.ApplyConfiguration(new CatalogItemEntityTypeConfiguration()); | ||||
|         } | ||||
| public class CatalogContext : DbContext | ||||
| { | ||||
|     public CatalogContext(DbContextOptions<CatalogContext> options) : base(options) | ||||
|     { | ||||
|     } | ||||
|     public DbSet<CatalogItem> CatalogItems { get; set; } | ||||
|     public DbSet<CatalogBrand> CatalogBrands { get; set; } | ||||
|     public DbSet<CatalogType> CatalogTypes { get; set; } | ||||
| 
 | ||||
| 
 | ||||
|     public class CatalogContextDesignFactory : IDesignTimeDbContextFactory<CatalogContext> | ||||
|     protected override void OnModelCreating(ModelBuilder builder) | ||||
|     { | ||||
|         public CatalogContext CreateDbContext(string[] args) | ||||
|         { | ||||
|             var optionsBuilder = new DbContextOptionsBuilder<CatalogContext>() | ||||
|                 .UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true"); | ||||
| 
 | ||||
|             return new CatalogContext(optionsBuilder.Options); | ||||
|         } | ||||
|         builder.ApplyConfiguration(new CatalogBrandEntityTypeConfiguration()); | ||||
|         builder.ApplyConfiguration(new CatalogTypeEntityTypeConfiguration()); | ||||
|         builder.ApplyConfiguration(new CatalogItemEntityTypeConfiguration()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| public class CatalogContextDesignFactory : IDesignTimeDbContextFactory<CatalogContext> | ||||
| { | ||||
|     public CatalogContext CreateDbContext(string[] args) | ||||
|     { | ||||
|         var optionsBuilder = new DbContextOptionsBuilder<CatalogContext>() | ||||
|             .UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true"); | ||||
| 
 | ||||
|         return new CatalogContext(optionsBuilder.Options); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<CatalogSettings> settings, ILogger<CatalogContextSeed> logger) | ||||
|     { | ||||
|         public async Task SeedAsync(CatalogContext context, IWebHostEnvironment env, IOptions<CatalogSettings> settings, ILogger<CatalogContextSeed> 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.SaveChangesAsync(); | ||||
|             } | ||||
| 
 | ||||
|             if (!context.CatalogTypes.Any()) | ||||
|             { | ||||
|                 await context.CatalogTypes.AddRangeAsync(useCustomizationData | ||||
|                     ? GetCatalogTypesFromFile(contentRootPath, logger) | ||||
|                     : GetPreconfiguredCatalogTypes()); | ||||
| 
 | ||||
|                 await context.SaveChangesAsync(); | ||||
|             } | ||||
| 
 | ||||
|             if (!context.CatalogItems.Any()) | ||||
|             { | ||||
|                 await context.CatalogItems.AddRangeAsync(useCustomizationData | ||||
|                     ? GetCatalogItemsFromFile(contentRootPath, context, logger) | ||||
|                     : GetPreconfiguredItems()); | ||||
| 
 | ||||
|                 await context.SaveChangesAsync(); | ||||
| 
 | ||||
|                 GetCatalogItemPictures(contentRootPath, picturePath); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<CatalogBrand> GetCatalogBrandsFromFile(string contentRootPath, ILogger<CatalogContextSeed> logger) | ||||
|     { | ||||
|         string csvFileCatalogBrands = Path.Combine(contentRootPath, "Setup", "CatalogBrands.csv"); | ||||
| 
 | ||||
|         if (!File.Exists(csvFileCatalogBrands)) | ||||
|         { | ||||
|             return GetPreconfiguredCatalogBrands(); | ||||
|         } | ||||
| 
 | ||||
|         string[] csvheaders; | ||||
|         try | ||||
|         { | ||||
|             string[] requiredHeaders = { "catalogbrand" }; | ||||
|             csvheaders = GetHeaders(csvFileCatalogBrands, requiredHeaders); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | ||||
|             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); | ||||
|     } | ||||
| 
 | ||||
|     private CatalogBrand CreateCatalogBrand(string brand) | ||||
|     { | ||||
|         brand = brand.Trim('"').Trim(); | ||||
| 
 | ||||
|         if (String.IsNullOrEmpty(brand)) | ||||
|         { | ||||
|             throw new Exception("catalog Brand Name is empty"); | ||||
|         } | ||||
| 
 | ||||
|         return new CatalogBrand | ||||
|         { | ||||
|             Brand = brand, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<CatalogBrand> GetPreconfiguredCatalogBrands() | ||||
|     { | ||||
|         return new List<CatalogBrand>() | ||||
|         { | ||||
|             new CatalogBrand() { Brand = "Azure"}, | ||||
|             new CatalogBrand() { Brand = ".NET" }, | ||||
|             new CatalogBrand() { Brand = "Visual Studio" }, | ||||
|             new CatalogBrand() { Brand = "SQL Server" }, | ||||
|             new CatalogBrand() { Brand = "Other" } | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<CatalogType> GetCatalogTypesFromFile(string contentRootPath, ILogger<CatalogContextSeed> logger) | ||||
|     { | ||||
|         string csvFileCatalogTypes = Path.Combine(contentRootPath, "Setup", "CatalogTypes.csv"); | ||||
| 
 | ||||
|         if (!File.Exists(csvFileCatalogTypes)) | ||||
|         { | ||||
|             return GetPreconfiguredCatalogTypes(); | ||||
|         } | ||||
| 
 | ||||
|         string[] csvheaders; | ||||
|         try | ||||
|         { | ||||
|             string[] requiredHeaders = { "catalogtype" }; | ||||
|             csvheaders = GetHeaders(csvFileCatalogTypes, requiredHeaders); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | ||||
|             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); | ||||
|     } | ||||
| 
 | ||||
|     private CatalogType CreateCatalogType(string type) | ||||
|     { | ||||
|         type = type.Trim('"').Trim(); | ||||
| 
 | ||||
|         if (String.IsNullOrEmpty(type)) | ||||
|         { | ||||
|             throw new Exception("catalog Type Name is empty"); | ||||
|         } | ||||
| 
 | ||||
|         return new CatalogType | ||||
|         { | ||||
|             Type = type, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<CatalogType> GetPreconfiguredCatalogTypes() | ||||
|     { | ||||
|         return new List<CatalogType>() | ||||
|         { | ||||
|             new CatalogType() { Type = "Mug"}, | ||||
|             new CatalogType() { Type = "T-Shirt" }, | ||||
|             new CatalogType() { Type = "Sheet" }, | ||||
|             new CatalogType() { Type = "USB Memory Stick" } | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<CatalogItem> GetCatalogItemsFromFile(string contentRootPath, CatalogContext context, ILogger<CatalogContextSeed> logger) | ||||
|     { | ||||
|         string csvFileCatalogItems = Path.Combine(contentRootPath, "Setup", "CatalogItems.csv"); | ||||
| 
 | ||||
|         if (!File.Exists(csvFileCatalogItems)) | ||||
|         { | ||||
|             return GetPreconfiguredItems(); | ||||
|         } | ||||
| 
 | ||||
|         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); | ||||
|     } | ||||
| 
 | ||||
|     private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary<String, int> catalogTypeIdLookup, Dictionary<String, int> catalogBrandIdLookup) | ||||
|     { | ||||
|         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"); | ||||
|         } | ||||
| 
 | ||||
|         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"); | ||||
|         } | ||||
| 
 | ||||
|         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)) | ||||
|             { | ||||
|                 if (int.TryParse(availableStockString, out int availableStock)) | ||||
|                 { | ||||
|                     await context.CatalogBrands.AddRangeAsync(useCustomizationData | ||||
|                         ? GetCatalogBrandsFromFile(contentRootPath, logger) | ||||
|                         : GetPreconfiguredCatalogBrands()); | ||||
| 
 | ||||
|                     await context.SaveChangesAsync(); | ||||
|                     catalogItem.AvailableStock = availableStock; | ||||
|                 } | ||||
| 
 | ||||
|                 if (!context.CatalogTypes.Any()) | ||||
|                 else | ||||
|                 { | ||||
|                     await context.CatalogTypes.AddRangeAsync(useCustomizationData | ||||
|                         ? GetCatalogTypesFromFile(contentRootPath, logger) | ||||
|                         : GetPreconfiguredCatalogTypes()); | ||||
| 
 | ||||
|                     await context.SaveChangesAsync(); | ||||
|                 } | ||||
| 
 | ||||
|                 if (!context.CatalogItems.Any()) | ||||
|                 { | ||||
|                     await context.CatalogItems.AddRangeAsync(useCustomizationData | ||||
|                         ? GetCatalogItemsFromFile(contentRootPath, context, logger) | ||||
|                         : GetPreconfiguredItems()); | ||||
| 
 | ||||
|                     await context.SaveChangesAsync(); | ||||
| 
 | ||||
|                     GetCatalogItemPictures(contentRootPath, picturePath); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<CatalogBrand> GetCatalogBrandsFromFile(string contentRootPath, ILogger<CatalogContextSeed> logger) | ||||
|         { | ||||
|             string csvFileCatalogBrands = Path.Combine(contentRootPath, "Setup", "CatalogBrands.csv"); | ||||
| 
 | ||||
|             if (!File.Exists(csvFileCatalogBrands)) | ||||
|             { | ||||
|                 return GetPreconfiguredCatalogBrands(); | ||||
|             } | ||||
| 
 | ||||
|             string[] csvheaders; | ||||
|             try | ||||
|             { | ||||
|                 string[] requiredHeaders = { "catalogbrand" }; | ||||
|                 csvheaders = GetHeaders(csvFileCatalogBrands, requiredHeaders); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | ||||
|                 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); | ||||
|         } | ||||
| 
 | ||||
|         private CatalogBrand CreateCatalogBrand(string brand) | ||||
|         { | ||||
|             brand = brand.Trim('"').Trim(); | ||||
| 
 | ||||
|             if (String.IsNullOrEmpty(brand)) | ||||
|             { | ||||
|                 throw new Exception("catalog Brand Name is empty"); | ||||
|             } | ||||
| 
 | ||||
|             return new CatalogBrand | ||||
|             { | ||||
|                 Brand = brand, | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<CatalogBrand> GetPreconfiguredCatalogBrands() | ||||
|         { | ||||
|             return new List<CatalogBrand>() | ||||
|             { | ||||
|                 new CatalogBrand() { Brand = "Azure"}, | ||||
|                 new CatalogBrand() { Brand = ".NET" }, | ||||
|                 new CatalogBrand() { Brand = "Visual Studio" }, | ||||
|                 new CatalogBrand() { Brand = "SQL Server" }, | ||||
|                 new CatalogBrand() { Brand = "Other" } | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<CatalogType> GetCatalogTypesFromFile(string contentRootPath, ILogger<CatalogContextSeed> logger) | ||||
|         { | ||||
|             string csvFileCatalogTypes = Path.Combine(contentRootPath, "Setup", "CatalogTypes.csv"); | ||||
| 
 | ||||
|             if (!File.Exists(csvFileCatalogTypes)) | ||||
|             { | ||||
|                 return GetPreconfiguredCatalogTypes(); | ||||
|             } | ||||
| 
 | ||||
|             string[] csvheaders; | ||||
|             try | ||||
|             { | ||||
|                 string[] requiredHeaders = { "catalogtype" }; | ||||
|                 csvheaders = GetHeaders(csvFileCatalogTypes, requiredHeaders); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | ||||
|                 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); | ||||
|         } | ||||
| 
 | ||||
|         private CatalogType CreateCatalogType(string type) | ||||
|         { | ||||
|             type = type.Trim('"').Trim(); | ||||
| 
 | ||||
|             if (String.IsNullOrEmpty(type)) | ||||
|             { | ||||
|                 throw new Exception("catalog Type Name is empty"); | ||||
|             } | ||||
| 
 | ||||
|             return new CatalogType | ||||
|             { | ||||
|                 Type = type, | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<CatalogType> GetPreconfiguredCatalogTypes() | ||||
|         { | ||||
|             return new List<CatalogType>() | ||||
|             { | ||||
|                 new CatalogType() { Type = "Mug"}, | ||||
|                 new CatalogType() { Type = "T-Shirt" }, | ||||
|                 new CatalogType() { Type = "Sheet" }, | ||||
|                 new CatalogType() { Type = "USB Memory Stick" } | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<CatalogItem> GetCatalogItemsFromFile(string contentRootPath, CatalogContext context, ILogger<CatalogContextSeed> logger) | ||||
|         { | ||||
|             string csvFileCatalogItems = Path.Combine(contentRootPath, "Setup", "CatalogItems.csv"); | ||||
| 
 | ||||
|             if (!File.Exists(csvFileCatalogItems)) | ||||
|             { | ||||
|                 return GetPreconfiguredItems(); | ||||
|             } | ||||
| 
 | ||||
|             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); | ||||
|         } | ||||
| 
 | ||||
|         private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary<String, int> catalogTypeIdLookup, Dictionary<String, int> catalogBrandIdLookup) | ||||
|         { | ||||
|             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"); | ||||
|             } | ||||
| 
 | ||||
|             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"); | ||||
|             } | ||||
| 
 | ||||
|             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)) | ||||
|                 { | ||||
|                     if (int.TryParse(availableStockString, out int availableStock)) | ||||
|                     { | ||||
|                         catalogItem.AvailableStock = availableStock; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         throw new Exception($"availableStock={availableStockString} is not a valid integer"); | ||||
|                     } | ||||
|                     throw new Exception($"availableStock={availableStockString} is not a valid integer"); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             int restockThresholdIndex = Array.IndexOf(headers, "restockthreshold"); | ||||
|             if (restockThresholdIndex != -1) | ||||
|             { | ||||
|                 string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); | ||||
|                 if (!String.IsNullOrEmpty(restockThresholdString)) | ||||
|                 { | ||||
|                     if (int.TryParse(restockThresholdString, out int restockThreshold)) | ||||
|                     { | ||||
|                         catalogItem.RestockThreshold = restockThreshold; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         throw new Exception($"restockThreshold={restockThreshold} is not a valid integer"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             int maxStockThresholdIndex = Array.IndexOf(headers, "maxstockthreshold"); | ||||
|             if (maxStockThresholdIndex != -1) | ||||
|             { | ||||
|                 string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); | ||||
|                 if (!String.IsNullOrEmpty(maxStockThresholdString)) | ||||
|                 { | ||||
|                     if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) | ||||
|                     { | ||||
|                         catalogItem.MaxStockThreshold = maxStockThreshold; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         throw new Exception($"maxStockThreshold={maxStockThreshold} is not a valid integer"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             int onReorderIndex = Array.IndexOf(headers, "onreorder"); | ||||
|             if (onReorderIndex != -1) | ||||
|             { | ||||
|                 string onReorderString = column[onReorderIndex].Trim('"').Trim(); | ||||
|                 if (!String.IsNullOrEmpty(onReorderString)) | ||||
|                 { | ||||
|                     if (bool.TryParse(onReorderString, out bool onReorder)) | ||||
|                     { | ||||
|                         catalogItem.OnReorder = onReorder; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         throw new Exception($"onReorder={onReorderString} is not a valid boolean"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return catalogItem; | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<CatalogItem> GetPreconfiguredItems() | ||||
|         int restockThresholdIndex = Array.IndexOf(headers, "restockthreshold"); | ||||
|         if (restockThresholdIndex != -1) | ||||
|         { | ||||
|             return new List<CatalogItem>() | ||||
|             string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); | ||||
|             if (!String.IsNullOrEmpty(restockThresholdString)) | ||||
|             { | ||||
|                 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<T> White Mug", Name = "Cup<T> 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<T> Sheet", Name = "Cup<T> 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) | ||||
|         { | ||||
|             string[] csvheaders = File.ReadLines(csvfile).First().ToLowerInvariant().Split(','); | ||||
| 
 | ||||
|             if (csvheaders.Count() < requiredHeaders.Count()) | ||||
|             { | ||||
|                 throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is bigger then csv header count '{csvheaders.Count()}' "); | ||||
|             } | ||||
| 
 | ||||
|             if (optionalHeaders != null) | ||||
|             { | ||||
|                 if (csvheaders.Count() > (requiredHeaders.Count() + optionalHeaders.Count())) | ||||
|                 if (int.TryParse(restockThresholdString, out int restockThreshold)) | ||||
|                 { | ||||
|                     throw new Exception($"csv header count '{csvheaders.Count()}'  is larger then required '{requiredHeaders.Count()}' and optional '{optionalHeaders.Count()}' headers count"); | ||||
|                     catalogItem.RestockThreshold = restockThreshold; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             foreach (var requiredHeader in requiredHeaders) | ||||
|             { | ||||
|                 if (!csvheaders.Contains(requiredHeader)) | ||||
|                 else | ||||
|                 { | ||||
|                     throw new Exception($"does not contain required header '{requiredHeader}'"); | ||||
|                     throw new Exception($"restockThreshold={restockThreshold} is not a valid integer"); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return csvheaders; | ||||
|         } | ||||
| 
 | ||||
|         private void GetCatalogItemPictures(string contentRootPath, string picturePath) | ||||
|         { | ||||
|             if (picturePath != null) | ||||
|             { | ||||
|                 DirectoryInfo directory = new DirectoryInfo(picturePath); | ||||
|                 foreach (FileInfo file in directory.GetFiles()) | ||||
|                 { | ||||
|                     file.Delete(); | ||||
|                 } | ||||
| 
 | ||||
|                 string zipFileCatalogItemPictures = Path.Combine(contentRootPath, "Setup", "CatalogItems.zip"); | ||||
|                 ZipFile.ExtractToDirectory(zipFileCatalogItemPictures, picturePath); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private AsyncRetryPolicy CreatePolicy(ILogger<CatalogContextSeed> logger, string prefix, int retries = 3) | ||||
|         int maxStockThresholdIndex = Array.IndexOf(headers, "maxstockthreshold"); | ||||
|         if (maxStockThresholdIndex != -1) | ||||
|         { | ||||
|             return Policy.Handle<SqlException>(). | ||||
|                 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); | ||||
|                     } | ||||
|                 ); | ||||
|             string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); | ||||
|             if (!String.IsNullOrEmpty(maxStockThresholdString)) | ||||
|             { | ||||
|                 if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) | ||||
|                 { | ||||
|                     catalogItem.MaxStockThreshold = maxStockThreshold; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw new Exception($"maxStockThreshold={maxStockThreshold} is not a valid integer"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         int onReorderIndex = Array.IndexOf(headers, "onreorder"); | ||||
|         if (onReorderIndex != -1) | ||||
|         { | ||||
|             string onReorderString = column[onReorderIndex].Trim('"').Trim(); | ||||
|             if (!String.IsNullOrEmpty(onReorderString)) | ||||
|             { | ||||
|                 if (bool.TryParse(onReorderString, out bool onReorder)) | ||||
|                 { | ||||
|                     catalogItem.OnReorder = onReorder; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw new Exception($"onReorder={onReorderString} is not a valid boolean"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return catalogItem; | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<CatalogItem> GetPreconfiguredItems() | ||||
|     { | ||||
|         return new List<CatalogItem>() | ||||
|         { | ||||
|             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<T> White Mug", Name = "Cup<T> 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<T> Sheet", Name = "Cup<T> 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) | ||||
|     { | ||||
|         string[] csvheaders = File.ReadLines(csvfile).First().ToLowerInvariant().Split(','); | ||||
| 
 | ||||
|         if (csvheaders.Count() < requiredHeaders.Count()) | ||||
|         { | ||||
|             throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is bigger then csv header count '{csvheaders.Count()}' "); | ||||
|         } | ||||
| 
 | ||||
|         if (optionalHeaders != null) | ||||
|         { | ||||
|             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"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         foreach (var requiredHeader in requiredHeaders) | ||||
|         { | ||||
|             if (!csvheaders.Contains(requiredHeader)) | ||||
|             { | ||||
|                 throw new Exception($"does not contain required header '{requiredHeader}'"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return csvheaders; | ||||
|     } | ||||
| 
 | ||||
|     private void GetCatalogItemPictures(string contentRootPath, string picturePath) | ||||
|     { | ||||
|         if (picturePath != null) | ||||
|         { | ||||
|             DirectoryInfo directory = new DirectoryInfo(picturePath); | ||||
|             foreach (FileInfo file in directory.GetFiles()) | ||||
|             { | ||||
|                 file.Delete(); | ||||
|             } | ||||
| 
 | ||||
|             string zipFileCatalogItemPictures = Path.Combine(contentRootPath, "Setup", "CatalogItems.zip"); | ||||
|             ZipFile.ExtractToDirectory(zipFileCatalogItemPictures, picturePath); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private AsyncRetryPolicy CreatePolicy(ILogger<CatalogContextSeed> logger, string prefix, int retries = 3) | ||||
|     { | ||||
|         return Policy.Handle<SqlException>(). | ||||
|             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); | ||||
|                 } | ||||
|             ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,21 +1,20 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; | ||||
| 
 | ||||
| class CatalogBrandEntityTypeConfiguration | ||||
|     : IEntityTypeConfiguration<CatalogBrand> | ||||
| { | ||||
|     class CatalogBrandEntityTypeConfiguration | ||||
|         : IEntityTypeConfiguration<CatalogBrand> | ||||
|     public void Configure(EntityTypeBuilder<CatalogBrand> builder) | ||||
|     { | ||||
|         public void Configure(EntityTypeBuilder<CatalogBrand> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,35 +1,34 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; | ||||
| 
 | ||||
| class CatalogItemEntityTypeConfiguration | ||||
|     : IEntityTypeConfiguration<CatalogItem> | ||||
| { | ||||
|     class CatalogItemEntityTypeConfiguration | ||||
|         : IEntityTypeConfiguration<CatalogItem> | ||||
|     public void Configure(EntityTypeBuilder<CatalogItem> builder) | ||||
|     { | ||||
|         public void Configure(EntityTypeBuilder<CatalogItem> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,21 +1,20 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; | ||||
| 
 | ||||
| class CatalogTypeEntityTypeConfiguration | ||||
|     : IEntityTypeConfiguration<CatalogType> | ||||
| { | ||||
|     class CatalogTypeEntityTypeConfiguration | ||||
|         : IEntityTypeConfiguration<CatalogType> | ||||
|     public void Configure(EntityTypeBuilder<CatalogType> builder) | ||||
|     { | ||||
|         public void Configure(EntityTypeBuilder<CatalogType> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,19 +1,18 @@ | ||||
| namespace Catalog.API.Infrastructure.Exceptions | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Exception type for app exceptions | ||||
| /// </summary> | ||||
| public class CatalogDomainException : Exception | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Exception type for app exceptions | ||||
|     /// </summary> | ||||
|     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) | ||||
|     { } | ||||
| } | ||||
|  | ||||
| @ -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<HttpGlobalExceptionFilter> logger; | ||||
| 
 | ||||
|     public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger) | ||||
|     { | ||||
|         private readonly IWebHostEnvironment env; | ||||
|         private readonly ILogger<HttpGlobalExceptionFilter> logger; | ||||
|         this.env = env; | ||||
|         this.logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger) | ||||
|     public void OnException(ExceptionContext context) | ||||
|     { | ||||
|         logger.LogError(new EventId(context.Exception.HResult), | ||||
|             context.Exception, | ||||
|             context.Exception.Message); | ||||
| 
 | ||||
|         if (context.Exception.GetType() == typeof(CatalogDomainException)) | ||||
|         { | ||||
|             this.env = env; | ||||
|             this.logger = logger; | ||||
|         } | ||||
| 
 | ||||
|         public void OnException(ExceptionContext context) | ||||
|         { | ||||
|             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 | ||||
|             { | ||||
|                 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; | ||||
|             } | ||||
|             context.ExceptionHandled = true; | ||||
|             context.Result = new BadRequestObjectResult(problemDetails); | ||||
|             context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; | ||||
|         } | ||||
| 
 | ||||
|         private class JsonErrorResponse | ||||
|         else | ||||
|         { | ||||
|             public string[] Messages { get; set; } | ||||
|             var json = new JsonErrorResponse | ||||
|             { | ||||
|                 Messages = new[] { "An error ocurred." } | ||||
|             }; | ||||
| 
 | ||||
|             public object DeveloperMessage { get; set; } | ||||
|             if (env.IsDevelopment()) | ||||
|             { | ||||
|                 json.DeveloperMessage = context.Exception; | ||||
|             } | ||||
| 
 | ||||
|             context.Result = new InternalServerErrorObjectResult(json); | ||||
|             context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | ||||
|         } | ||||
|         context.ExceptionHandled = true; | ||||
|     } | ||||
| 
 | ||||
|     private class JsonErrorResponse | ||||
|     { | ||||
|         public string[] Messages { get; set; } | ||||
| 
 | ||||
|         public object DeveloperMessage { get; set; } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<DbConnection, IIntegrationEventLogService> _integrationEventLogServiceFactory; | ||||
|     private readonly IEventBus _eventBus; | ||||
|     private readonly CatalogContext _catalogContext; | ||||
|     private readonly IIntegrationEventLogService _eventLogService; | ||||
|     private readonly ILogger<CatalogIntegrationEventService> _logger; | ||||
|     private volatile bool disposedValue; | ||||
| 
 | ||||
|     public CatalogIntegrationEventService( | ||||
|         ILogger<CatalogIntegrationEventService> logger, | ||||
|         IEventBus eventBus, | ||||
|         CatalogContext catalogContext, | ||||
|         Func<DbConnection, IIntegrationEventLogService> integrationEventLogServiceFactory) | ||||
|     { | ||||
|         private readonly Func<DbConnection, IIntegrationEventLogService> _integrationEventLogServiceFactory; | ||||
|         private readonly IEventBus _eventBus; | ||||
|         private readonly CatalogContext _catalogContext; | ||||
|         private readonly IIntegrationEventLogService _eventLogService; | ||||
|         private readonly ILogger<CatalogIntegrationEventService> _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<CatalogIntegrationEventService> logger, | ||||
|             IEventBus eventBus, | ||||
|             CatalogContext catalogContext, | ||||
|             Func<DbConnection, IIntegrationEventLogService> 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); | ||||
| 
 | ||||
|             await _eventLogService.MarkEventAsInProgressAsync(evt.Id); | ||||
|             _eventBus.Publish(evt); | ||||
|             await _eventLogService.MarkEventAsPublishedAsync(evt.Id); | ||||
|         } | ||||
| 
 | ||||
|         public async Task PublishThroughEventBusAsync(IntegrationEvent evt) | ||||
|         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); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         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); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         protected virtual void Dispose(bool disposing) | ||||
|         { | ||||
|             if (!disposedValue) | ||||
|             { | ||||
|                 if (disposing) | ||||
|                 { | ||||
|                     (_eventLogService as IDisposable)?.Dispose(); | ||||
|                 } | ||||
| 
 | ||||
|                 disposedValue = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public void Dispose() | ||||
|         { | ||||
|             Dispose(disposing: true); | ||||
|             GC.SuppressFinalize(this); | ||||
|             _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); | ||||
| 
 | ||||
|         //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) | ||||
|     { | ||||
|         if (!disposedValue) | ||||
|         { | ||||
|             if (disposing) | ||||
|             { | ||||
|                 (_eventLogService as IDisposable)?.Dispose(); | ||||
|             } | ||||
| 
 | ||||
|             disposedValue = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void Dispose() | ||||
|     { | ||||
|         Dispose(disposing: true); | ||||
|         GC.SuppressFinalize(this); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,48 +1,46 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling | ||||
| { | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; | ||||
|      | ||||
|     public class OrderStatusChangedToAwaitingValidationIntegrationEventHandler : | ||||
|         IIntegrationEventHandler<OrderStatusChangedToAwaitingValidationIntegrationEvent> | ||||
| public class OrderStatusChangedToAwaitingValidationIntegrationEventHandler : | ||||
|     IIntegrationEventHandler<OrderStatusChangedToAwaitingValidationIntegrationEvent> | ||||
| { | ||||
|     private readonly CatalogContext _catalogContext; | ||||
|     private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; | ||||
|     private readonly ILogger<OrderStatusChangedToAwaitingValidationIntegrationEventHandler> _logger; | ||||
| 
 | ||||
|     public OrderStatusChangedToAwaitingValidationIntegrationEventHandler( | ||||
|         CatalogContext catalogContext, | ||||
|         ICatalogIntegrationEventService catalogIntegrationEventService, | ||||
|         ILogger<OrderStatusChangedToAwaitingValidationIntegrationEventHandler> logger) | ||||
|     { | ||||
|         private readonly CatalogContext _catalogContext; | ||||
|         private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; | ||||
|         private readonly ILogger<OrderStatusChangedToAwaitingValidationIntegrationEventHandler> _logger; | ||||
|         _catalogContext = catalogContext; | ||||
|         _catalogIntegrationEventService = catalogIntegrationEventService; | ||||
|         _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); | ||||
|     } | ||||
| 
 | ||||
|         public OrderStatusChangedToAwaitingValidationIntegrationEventHandler( | ||||
|             CatalogContext catalogContext, | ||||
|             ICatalogIntegrationEventService catalogIntegrationEventService, | ||||
|             ILogger<OrderStatusChangedToAwaitingValidationIntegrationEventHandler> logger) | ||||
|     public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) | ||||
|     { | ||||
|         using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) | ||||
|         { | ||||
|             _catalogContext = catalogContext; | ||||
|             _catalogIntegrationEventService = catalogIntegrationEventService; | ||||
|             _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(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) | ||||
|         { | ||||
|             using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) | ||||
|             var confirmedOrderStockItems = new List<ConfirmedOrderStockItem>(); | ||||
| 
 | ||||
|             foreach (var orderStockItem in @event.OrderStockItems) | ||||
|             { | ||||
|                 _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); | ||||
| 
 | ||||
|                 var confirmedOrderStockItems = new List<ConfirmedOrderStockItem>(); | ||||
| 
 | ||||
|                 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); | ||||
|                 } | ||||
| 
 | ||||
|                 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); | ||||
|                 var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); | ||||
|                 var hasStock = catalogItem.AvailableStock >= orderStockItem.Units; | ||||
|                 var confirmedOrderStockItem = new ConfirmedOrderStockItem(catalogItem.Id, hasStock); | ||||
| 
 | ||||
|                 confirmedOrderStockItems.Add(confirmedOrderStockItem); | ||||
|             } | ||||
| 
 | ||||
|             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); | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -1,36 +1,35 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling | ||||
| {     | ||||
|     public class OrderStatusChangedToPaidIntegrationEventHandler : | ||||
|         IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent> | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; | ||||
| 
 | ||||
| public class OrderStatusChangedToPaidIntegrationEventHandler : | ||||
|     IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent> | ||||
| { | ||||
|     private readonly CatalogContext _catalogContext; | ||||
|     private readonly ILogger<OrderStatusChangedToPaidIntegrationEventHandler> _logger; | ||||
| 
 | ||||
|     public OrderStatusChangedToPaidIntegrationEventHandler( | ||||
|         CatalogContext catalogContext, | ||||
|         ILogger<OrderStatusChangedToPaidIntegrationEventHandler> logger) | ||||
|     { | ||||
|         private readonly CatalogContext _catalogContext; | ||||
|         private readonly ILogger<OrderStatusChangedToPaidIntegrationEventHandler> _logger; | ||||
|         _catalogContext = catalogContext; | ||||
|         _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); | ||||
|     } | ||||
| 
 | ||||
|         public OrderStatusChangedToPaidIntegrationEventHandler( | ||||
|             CatalogContext catalogContext, | ||||
|             ILogger<OrderStatusChangedToPaidIntegrationEventHandler> 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); | ||||
| 
 | ||||
|                     catalogItem.RemoveStock(orderStockItem.Units); | ||||
|                 } | ||||
| 
 | ||||
|                 await _catalogContext.SaveChangesAsync(); | ||||
|                 var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); | ||||
| 
 | ||||
|                 catalogItem.RemoveStock(orderStockItem.Units); | ||||
|             } | ||||
| 
 | ||||
|             await _catalogContext.SaveChangesAsync(); | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -1,27 +1,26 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | ||||
| {     | ||||
|     public record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; | ||||
| 
 | ||||
| public record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent | ||||
| { | ||||
|     public int OrderId { get; } | ||||
|     public IEnumerable<OrderStockItem> OrderStockItems { get; } | ||||
| 
 | ||||
|     public OrderStatusChangedToAwaitingValidationIntegrationEvent(int orderId, | ||||
|         IEnumerable<OrderStockItem> orderStockItems) | ||||
|     { | ||||
|         public int OrderId { get; } | ||||
|         public IEnumerable<OrderStockItem> OrderStockItems { get; } | ||||
| 
 | ||||
|         public OrderStatusChangedToAwaitingValidationIntegrationEvent(int orderId, | ||||
|             IEnumerable<OrderStockItem> orderStockItems) | ||||
|         { | ||||
|             OrderId = orderId; | ||||
|             OrderStockItems = orderStockItems; | ||||
|         } | ||||
|         OrderId = orderId; | ||||
|         OrderStockItems = orderStockItems; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     public record OrderStockItem | ||||
| public record OrderStockItem | ||||
| { | ||||
|     public int ProductId { get; } | ||||
|     public int Units { get; } | ||||
| 
 | ||||
|     public OrderStockItem(int productId, int units) | ||||
|     { | ||||
|         public int ProductId { get; } | ||||
|         public int Units { get; } | ||||
| 
 | ||||
|         public OrderStockItem(int productId, int units) | ||||
|         { | ||||
|             ProductId = productId; | ||||
|             Units = units; | ||||
|         } | ||||
|         ProductId = productId; | ||||
|         Units = units; | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -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<OrderStockItem> OrderStockItems { get; } | ||||
| 
 | ||||
|     public record OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent | ||||
|     public OrderStatusChangedToPaidIntegrationEvent(int orderId, | ||||
|         IEnumerable<OrderStockItem> orderStockItems) | ||||
|     { | ||||
|         public int OrderId { get; } | ||||
|         public IEnumerable<OrderStockItem> OrderStockItems { get; } | ||||
| 
 | ||||
|         public OrderStatusChangedToPaidIntegrationEvent(int orderId, | ||||
|             IEnumerable<OrderStockItem> orderStockItems) | ||||
|         { | ||||
|             OrderId = orderId; | ||||
|             OrderStockItems = orderStockItems; | ||||
|         } | ||||
|         OrderId = orderId; | ||||
|         OrderStockItems = orderStockItems; | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
| public record OrderStockConfirmedIntegrationEvent : IntegrationEvent | ||||
| { | ||||
|     public int OrderId { get; } | ||||
| 
 | ||||
|     public OrderStockConfirmedIntegrationEvent(int orderId) => OrderId = orderId; | ||||
| } | ||||
|  | ||||
| @ -1,28 +1,27 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | ||||
| {     | ||||
|     public record OrderStockRejectedIntegrationEvent : IntegrationEvent | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; | ||||
| 
 | ||||
| public record OrderStockRejectedIntegrationEvent : IntegrationEvent | ||||
| { | ||||
|     public int OrderId { get; } | ||||
| 
 | ||||
|     public List<ConfirmedOrderStockItem> OrderStockItems { get; } | ||||
| 
 | ||||
|     public OrderStockRejectedIntegrationEvent(int orderId, | ||||
|         List<ConfirmedOrderStockItem> orderStockItems) | ||||
|     { | ||||
|         public int OrderId { get; } | ||||
| 
 | ||||
|         public List<ConfirmedOrderStockItem> OrderStockItems { get; } | ||||
| 
 | ||||
|         public OrderStockRejectedIntegrationEvent(int orderId, | ||||
|             List<ConfirmedOrderStockItem> orderStockItems) | ||||
|         { | ||||
|             OrderId = orderId; | ||||
|             OrderStockItems = orderStockItems; | ||||
|         } | ||||
|         OrderId = orderId; | ||||
|         OrderStockItems = orderStockItems; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     public record ConfirmedOrderStockItem | ||||
| public record ConfirmedOrderStockItem | ||||
| { | ||||
|     public int ProductId { get; } | ||||
|     public bool HasStock { get; } | ||||
| 
 | ||||
|     public ConfirmedOrderStockItem(int productId, bool hasStock) | ||||
|     { | ||||
|         public int ProductId { get; } | ||||
|         public bool HasStock { get; } | ||||
| 
 | ||||
|         public ConfirmedOrderStockItem(int productId, bool hasStock) | ||||
|         { | ||||
|             ProductId = productId; | ||||
|             HasStock = hasStock; | ||||
|         } | ||||
|         ProductId = productId; | ||||
|         HasStock = hasStock; | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| 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 OldPrice { get; private init; } | ||||
| 
 | ||||
|     public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice) | ||||
|     { | ||||
|         public int ProductId { get; private init; } | ||||
| 
 | ||||
|         public decimal NewPrice { get; private init; } | ||||
| 
 | ||||
|         public decimal OldPrice { get; private init; } | ||||
| 
 | ||||
|         public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice) | ||||
|         { | ||||
|             ProductId = productId; | ||||
|             NewPrice = newPrice; | ||||
|             OldPrice = oldPrice; | ||||
|         } | ||||
|         ProductId = productId; | ||||
|         NewPrice = newPrice; | ||||
|         OldPrice = oldPrice; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model | ||||
| { | ||||
|     public class CatalogBrand | ||||
|     { | ||||
|         public int Id { get; set; } | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; | ||||
| 
 | ||||
|         public string Brand { get; set; } | ||||
|     } | ||||
| public class CatalogBrand | ||||
| { | ||||
|     public int Id { get; set; } | ||||
| 
 | ||||
|     public string Brand { get; set; } | ||||
| } | ||||
|  | ||||
| @ -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 string Name { get; set; } | ||||
| 
 | ||||
|     public string Description { get; set; } | ||||
| 
 | ||||
|     public decimal Price { get; set; } | ||||
| 
 | ||||
|     public string PictureFileName { get; set; } | ||||
| 
 | ||||
|     public string PictureUri { get; set; } | ||||
| 
 | ||||
|     public int CatalogTypeId { get; set; } | ||||
| 
 | ||||
|     public CatalogType CatalogType { get; set; } | ||||
| 
 | ||||
|     public int CatalogBrandId { get; set; } | ||||
| 
 | ||||
|     public CatalogBrand CatalogBrand { get; set; } | ||||
| 
 | ||||
|     // Quantity in stock | ||||
|     public int AvailableStock { 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; } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// True if item is on reorder | ||||
|     /// </summary> | ||||
|     public bool OnReorder { get; set; } | ||||
| 
 | ||||
|     public CatalogItem() { } | ||||
| 
 | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// 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.  | ||||
|     /// </summary> | ||||
|     /// <param name="quantityDesired"></param> | ||||
|     /// <returns>int: Returns the number actually removed from stock. </returns> | ||||
|     ///  | ||||
|     public int RemoveStock(int quantityDesired) | ||||
|     { | ||||
|         public int Id { get; set; } | ||||
| 
 | ||||
|         public string Name { get; set; } | ||||
| 
 | ||||
|         public string Description { get; set; } | ||||
| 
 | ||||
|         public decimal Price { get; set; } | ||||
| 
 | ||||
|         public string PictureFileName { get; set; } | ||||
| 
 | ||||
|         public string PictureUri { get; set; } | ||||
| 
 | ||||
|         public int CatalogTypeId { get; set; } | ||||
| 
 | ||||
|         public CatalogType CatalogType { get; set; } | ||||
| 
 | ||||
|         public int CatalogBrandId { get; set; } | ||||
| 
 | ||||
|         public CatalogBrand CatalogBrand { get; set; } | ||||
| 
 | ||||
|         // Quantity in stock | ||||
|         public int AvailableStock { 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; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// True if item is on reorder | ||||
|         /// </summary> | ||||
|         public bool OnReorder { get; set; } | ||||
| 
 | ||||
|         public CatalogItem() { } | ||||
| 
 | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 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.  | ||||
|         /// </summary> | ||||
|         /// <param name="quantityDesired"></param> | ||||
|         /// <returns>int: Returns the number actually removed from stock. </returns> | ||||
|         ///  | ||||
|         public int RemoveStock(int quantityDesired) | ||||
|         if (AvailableStock == 0) | ||||
|         { | ||||
|             if (AvailableStock == 0) | ||||
|             { | ||||
|                 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"); | ||||
|             } | ||||
| 
 | ||||
|             int removed = Math.Min(quantityDesired, this.AvailableStock); | ||||
| 
 | ||||
|             this.AvailableStock -= removed; | ||||
| 
 | ||||
|             return removed; | ||||
|             throw new CatalogDomainException($"Empty stock, product item {Name} is sold out"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Increments the quantity of a particular item in inventory. | ||||
|         /// <param name="quantity"></param> | ||||
|         /// <returns>int: Returns the quantity that has been added to stock</returns> | ||||
|         /// </summary> | ||||
|         public int AddStock(int quantity) | ||||
|         if (quantityDesired <= 0) | ||||
|         { | ||||
|             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; | ||||
|             throw new CatalogDomainException($"Item units desired should be greater than zero"); | ||||
|         } | ||||
| 
 | ||||
|         int removed = Math.Min(quantityDesired, this.AvailableStock); | ||||
| 
 | ||||
|         this.AvailableStock -= removed; | ||||
| 
 | ||||
|         return removed; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Increments the quantity of a particular item in inventory. | ||||
|     /// <param name="quantity"></param> | ||||
|     /// <returns>int: Returns the quantity that has been added to stock</returns> | ||||
|     /// </summary> | ||||
|     public int AddStock(int quantity) | ||||
|     { | ||||
|         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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model | ||||
| { | ||||
|     public class CatalogType | ||||
|     { | ||||
|         public int Id { get; set; } | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; | ||||
| 
 | ||||
|         public string Type { get; set; } | ||||
|     } | ||||
| public class CatalogType | ||||
| { | ||||
|     public int Id { get; set; } | ||||
| 
 | ||||
|     public string Type { get; set; } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|         } | ||||
| 
 | ||||
|         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 void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | ||||
|         { | ||||
|             //Configure logs | ||||
| 
 | ||||
|             //loggerFactory.AddAzureWebAppDiagnostics(); | ||||
|             //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); | ||||
| 
 | ||||
|             var pathBase = Configuration["PATH_BASE"]; | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(pathBase)) | ||||
|             { | ||||
|                 loggerFactory.CreateLogger<Startup>().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 => | ||||
|             { | ||||
|                 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) | ||||
|                     { | ||||
|                         var line = await sr.ReadLineAsync(); | ||||
|                         if (line != "/* >>" || line != "<< */") | ||||
|                         { | ||||
|                             await ctx.Response.WriteAsync(line); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|                 endpoints.MapGrpcService<CatalogService>(); | ||||
|                 endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | ||||
|                 { | ||||
|                     Predicate = _ => true, | ||||
|                     ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | ||||
|                 }); | ||||
|                 endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | ||||
|                 { | ||||
|                     Predicate = r => r.Name.Contains("self") | ||||
|                 }); | ||||
|             }); | ||||
| 
 | ||||
|             ConfigureEventBus(app); | ||||
|         } | ||||
| 
 | ||||
|         protected virtual void ConfigureEventBus(IApplicationBuilder app) | ||||
|         { | ||||
|             var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); | ||||
|             eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | ||||
|             eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>(); | ||||
|         } | ||||
|         Configuration = configuration; | ||||
|     } | ||||
| 
 | ||||
|     public static class CustomExtensionMethods | ||||
|     public IConfiguration Configuration { get; } | ||||
| 
 | ||||
|     public IServiceProvider ConfigureServices(IServiceCollection services) | ||||
|     { | ||||
|         public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|             services.AddApplicationInsightsTelemetry(configuration); | ||||
|             services.AddApplicationInsightsKubernetesEnricher(); | ||||
|         services.AddAppInsight(Configuration) | ||||
|             .AddGrpc().Services | ||||
|             .AddCustomMVC(Configuration) | ||||
|             .AddCustomDbContext(Configuration) | ||||
|             .AddCustomOptions(Configuration) | ||||
|             .AddIntegrationServices(Configuration) | ||||
|             .AddEventBus(Configuration) | ||||
|             .AddSwagger(Configuration) | ||||
|             .AddCustomHealthCheck(Configuration); | ||||
| 
 | ||||
|             return services; | ||||
|         var container = new ContainerBuilder(); | ||||
|         container.Populate(services); | ||||
| 
 | ||||
|         return new AutofacServiceProvider(container.Build()); | ||||
|     } | ||||
| 
 | ||||
|     public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | ||||
|     { | ||||
|         //Configure logs | ||||
| 
 | ||||
|         //loggerFactory.AddAzureWebAppDiagnostics(); | ||||
|         //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); | ||||
| 
 | ||||
|         var pathBase = Configuration["PATH_BASE"]; | ||||
| 
 | ||||
|         if (!string.IsNullOrEmpty(pathBase)) | ||||
|         { | ||||
|             loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | ||||
|             app.UsePathBase(pathBase); | ||||
|         } | ||||
| 
 | ||||
|         public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|             services.AddControllers(options => | ||||
|         app.UseSwagger() | ||||
|             .UseSwaggerUI(c => | ||||
|             { | ||||
|                 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()); | ||||
|                 c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Catalog.API V1"); | ||||
|             }); | ||||
| 
 | ||||
|             return services; | ||||
|         } | ||||
| 
 | ||||
|         public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) | ||||
|         app.UseRouting(); | ||||
|         app.UseCors("CorsPolicy"); | ||||
|         app.UseEndpoints(endpoints => | ||||
|         { | ||||
|             var accountName = configuration.GetValue<string>("AzureStorageAccountName"); | ||||
|             var accountKey = configuration.GetValue<string>("AzureStorageAccountKey"); | ||||
| 
 | ||||
|             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)) | ||||
|             endpoints.MapDefaultControllerRoute(); | ||||
|             endpoints.MapControllers(); | ||||
|             endpoints.MapGet("/_proto/", async ctx => | ||||
|             { | ||||
|                 hcBuilder | ||||
|                     .AddAzureBlobStorage( | ||||
|                         $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", | ||||
|                         name: "catalog-storage-check", | ||||
|                         tags: new string[] { "catalogstorage" }); | ||||
|             } | ||||
| 
 | ||||
|             if (configuration.GetValue<bool>("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; | ||||
|         } | ||||
| 
 | ||||
|         public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|             services.AddEntityFrameworkSqlServer() | ||||
|                 .AddDbContext<CatalogContext>(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<IntegrationEventLogContext>(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) | ||||
|         { | ||||
|             services.Configure<CatalogSettings>(configuration); | ||||
|             services.Configure<ApiBehaviorOptions>(options => | ||||
|             { | ||||
|                 options.InvalidModelStateResponseFactory = context => | ||||
|                 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 problemDetails = new ValidationProblemDetails(context.ModelState) | ||||
|                     var line = await sr.ReadLineAsync(); | ||||
|                     if (line != "/* >>" || line != "<< */") | ||||
|                     { | ||||
|                         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 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" | ||||
|                 }); | ||||
|             }); | ||||
| 
 | ||||
|             return services; | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|             services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>( | ||||
|                 sp => (DbConnection c) => new IntegrationEventLogService(c)); | ||||
| 
 | ||||
|             services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>(); | ||||
| 
 | ||||
|             if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | ||||
|             { | ||||
|                 services.AddSingleton<IServiceBusPersisterConnection>(sp => | ||||
|                 { | ||||
|                     var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | ||||
|                     var serviceBusConnection = new ServiceBusConnectionStringBuilder(settings.EventBusConnection); | ||||
|                     var subscriptionClientName = configuration["SubscriptionClientName"]; | ||||
| 
 | ||||
|                     return new DefaultServiceBusPersisterConnection(serviceBusConnection, subscriptionClientName); | ||||
|                 }); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 services.AddSingleton<IRabbitMQPersistentConnection>(sp => | ||||
|                 { | ||||
|                     var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | ||||
|                     var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>(); | ||||
| 
 | ||||
|                     var factory = new ConnectionFactory() | ||||
|                     { | ||||
|                         HostName = configuration["EventBusConnection"], | ||||
|                         DispatchConsumersAsync = true | ||||
|                     }; | ||||
| 
 | ||||
|                     if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) | ||||
|                     { | ||||
|                         factory.UserName = configuration["EventBusUserName"]; | ||||
|                         await ctx.Response.WriteAsync(line); | ||||
|                     } | ||||
| 
 | ||||
|                     if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) | ||||
|                     { | ||||
|                         factory.Password = configuration["EventBusPassword"]; | ||||
|                     } | ||||
| 
 | ||||
|                     var retryCount = 5; | ||||
|                     if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | ||||
|                     { | ||||
|                         retryCount = int.Parse(configuration["EventBusRetryCount"]); | ||||
|                     } | ||||
| 
 | ||||
|                     return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return services; | ||||
|         } | ||||
| 
 | ||||
|         public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|             if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | ||||
|                 } | ||||
|             }); | ||||
|             endpoints.MapGrpcService<CatalogService>(); | ||||
|             endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | ||||
|             { | ||||
|                 services.AddSingleton<IEventBus, EventBusServiceBus>(sp => | ||||
|                 { | ||||
|                     var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>(); | ||||
|                     var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>(); | ||||
|                     var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>(); | ||||
|                     var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | ||||
| 
 | ||||
|                     return new EventBusServiceBus(serviceBusPersisterConnection, logger, | ||||
|                         eventBusSubcriptionsManager, iLifetimeScope); | ||||
|                 }); | ||||
| 
 | ||||
|             } | ||||
|             else | ||||
|                 Predicate = _ => true, | ||||
|                 ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | ||||
|             }); | ||||
|             endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | ||||
|             { | ||||
|                 services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp => | ||||
|                 { | ||||
|                     var subscriptionClientName = configuration["SubscriptionClientName"]; | ||||
|                     var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>(); | ||||
|                     var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>(); | ||||
|                     var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>(); | ||||
|                     var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | ||||
|                 Predicate = r => r.Name.Contains("self") | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|                     var retryCount = 5; | ||||
|                     if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | ||||
|                     { | ||||
|                         retryCount = int.Parse(configuration["EventBusRetryCount"]); | ||||
|                     } | ||||
|         ConfigureEventBus(app); | ||||
|     } | ||||
| 
 | ||||
|                     return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>(); | ||||
|             services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | ||||
|             services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); | ||||
| 
 | ||||
|             return services; | ||||
|         } | ||||
|     protected virtual void ConfigureEventBus(IApplicationBuilder app) | ||||
|     { | ||||
|         var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); | ||||
|         eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | ||||
|         eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public static class CustomExtensionMethods | ||||
| { | ||||
|     public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddApplicationInsightsTelemetry(configuration); | ||||
|         services.AddApplicationInsightsKubernetesEnricher(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddControllers(options => | ||||
|         { | ||||
|             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()); | ||||
|         }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         var accountName = configuration.GetValue<string>("AzureStorageAccountName"); | ||||
|         var accountKey = configuration.GetValue<string>("AzureStorageAccountKey"); | ||||
| 
 | ||||
|         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 | ||||
|                 .AddAzureBlobStorage( | ||||
|                     $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", | ||||
|                     name: "catalog-storage-check", | ||||
|                     tags: new string[] { "catalogstorage" }); | ||||
|         } | ||||
| 
 | ||||
|         if (configuration.GetValue<bool>("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; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddEntityFrameworkSqlServer() | ||||
|             .AddDbContext<CatalogContext>(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<IntegrationEventLogContext>(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) | ||||
|     { | ||||
|         services.Configure<CatalogSettings>(configuration); | ||||
|         services.Configure<ApiBehaviorOptions>(options => | ||||
|         { | ||||
|             options.InvalidModelStateResponseFactory = context => | ||||
|             { | ||||
|                 var problemDetails = new ValidationProblemDetails(context.ModelState) | ||||
|                 { | ||||
|                     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 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" | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         return services; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>( | ||||
|             sp => (DbConnection c) => new IntegrationEventLogService(c)); | ||||
| 
 | ||||
|         services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>(); | ||||
| 
 | ||||
|         if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | ||||
|         { | ||||
|             services.AddSingleton<IServiceBusPersisterConnection>(sp => | ||||
|             { | ||||
|                 var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | ||||
|                 var serviceBusConnection = new ServiceBusConnectionStringBuilder(settings.EventBusConnection); | ||||
|                 var subscriptionClientName = configuration["SubscriptionClientName"]; | ||||
| 
 | ||||
|                 return new DefaultServiceBusPersisterConnection(serviceBusConnection, subscriptionClientName); | ||||
|             }); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             services.AddSingleton<IRabbitMQPersistentConnection>(sp => | ||||
|             { | ||||
|                 var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | ||||
|                 var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>(); | ||||
| 
 | ||||
|                 var factory = new ConnectionFactory() | ||||
|                 { | ||||
|                     HostName = configuration["EventBusConnection"], | ||||
|                     DispatchConsumersAsync = true | ||||
|                 }; | ||||
| 
 | ||||
|                 if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) | ||||
|                 { | ||||
|                     factory.UserName = configuration["EventBusUserName"]; | ||||
|                 } | ||||
| 
 | ||||
|                 if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) | ||||
|                 { | ||||
|                     factory.Password = configuration["EventBusPassword"]; | ||||
|                 } | ||||
| 
 | ||||
|                 var retryCount = 5; | ||||
|                 if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | ||||
|                 { | ||||
|                     retryCount = int.Parse(configuration["EventBusRetryCount"]); | ||||
|                 } | ||||
| 
 | ||||
|                 return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | ||||
|         { | ||||
|             services.AddSingleton<IEventBus, EventBusServiceBus>(sp => | ||||
|             { | ||||
|                 var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>(); | ||||
|                 var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>(); | ||||
|                 var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>(); | ||||
|                 var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | ||||
| 
 | ||||
|                 return new EventBusServiceBus(serviceBusPersisterConnection, logger, | ||||
|                     eventBusSubcriptionsManager, iLifetimeScope); | ||||
|             }); | ||||
| 
 | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp => | ||||
|             { | ||||
|                 var subscriptionClientName = configuration["SubscriptionClientName"]; | ||||
|                 var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>(); | ||||
|                 var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>(); | ||||
|                 var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>(); | ||||
|                 var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | ||||
| 
 | ||||
|                 var retryCount = 5; | ||||
|                 if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | ||||
|                 { | ||||
|                     retryCount = int.Parse(configuration["EventBusRetryCount"]); | ||||
|                 } | ||||
| 
 | ||||
|                 return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>(); | ||||
|         services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | ||||
|         services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,21 +1,20 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel | ||||
| {     | ||||
|     public class PaginatedItemsViewModel<TEntity> where TEntity : class | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; | ||||
| 
 | ||||
| public class PaginatedItemsViewModel<TEntity> where TEntity : class | ||||
| { | ||||
|     public int PageIndex { get; private set; } | ||||
| 
 | ||||
|     public int PageSize { get; private set; } | ||||
| 
 | ||||
|     public long Count { get; private set; } | ||||
| 
 | ||||
|     public IEnumerable<TEntity> Data { get; private set; } | ||||
| 
 | ||||
|     public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable<TEntity> data) | ||||
|     { | ||||
|         public int PageIndex { get; private set; } | ||||
| 
 | ||||
|         public int PageSize { get; private set; } | ||||
| 
 | ||||
|         public long Count { get; private set; } | ||||
| 
 | ||||
|         public IEnumerable<TEntity> Data { get; private set; } | ||||
| 
 | ||||
|         public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable<TEntity> data) | ||||
|         { | ||||
|             PageIndex = pageIndex; | ||||
|             PageSize = pageSize; | ||||
|             Count = count; | ||||
|             Data = data; | ||||
|         } | ||||
|         PageIndex = pageIndex; | ||||
|         PageSize = pageSize; | ||||
|         Count = count; | ||||
|         Data = data; | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user