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