@ -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; | |||
private readonly CatalogContext _catalogContext; | |||
private readonly CatalogSettings _settings; | |||
private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; | |||
public CatalogController(CatalogContext context, IOptionsSnapshot<CatalogSettings> settings, ICatalogIntegrationEventService catalogIntegrationEventService) | |||
{ | |||
_catalogContext = context ?? throw new ArgumentNullException(nameof(context)); | |||
_catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); | |||
_settings = settings.Value; | |||
public CatalogController(CatalogContext context, IOptionsSnapshot<CatalogSettings> settings, ICatalogIntegrationEventService catalogIntegrationEventService) | |||
{ | |||
_catalogContext = context ?? throw new ArgumentNullException(nameof(context)); | |||
_catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); | |||
_settings = settings.Value; | |||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; | |||
} | |||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; | |||
} | |||
// GET api/v1/[controller]/items[?pageSize=3&pageIndex=10] | |||
[HttpGet] | |||
[Route("items")] | |||
[ProducesResponseType(typeof(PaginatedItemsViewModel<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) | |||
// 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)) | |||
{ | |||
if (!string.IsNullOrEmpty(ids)) | |||
{ | |||
var items = await GetItemsByIdsAsync(ids); | |||
if (!items.Any()) | |||
{ | |||
return BadRequest("ids value invalid. Must be comma-separated list of numbers"); | |||
} | |||
var items = await GetItemsByIdsAsync(ids); | |||
return Ok(items); | |||
if (!items.Any()) | |||
{ | |||
return BadRequest("ids value invalid. Must be comma-separated list of numbers"); | |||
} | |||
var totalItems = await _catalogContext.CatalogItems | |||
.LongCountAsync(); | |||
var itemsOnPage = await _catalogContext.CatalogItems | |||
.OrderBy(c => c.Name) | |||
.Skip(pageSize * pageIndex) | |||
.Take(pageSize) | |||
.ToListAsync(); | |||
/* The "awesome" fix for testing Devspaces */ | |||
/* | |||
foreach (var pr in itemsOnPage) { | |||
pr.Name = "Awesome " + pr.Name; | |||
} | |||
return Ok(items); | |||
} | |||
*/ | |||
var totalItems = await _catalogContext.CatalogItems | |||
.LongCountAsync(); | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
var itemsOnPage = await _catalogContext.CatalogItems | |||
.OrderBy(c => c.Name) | |||
.Skip(pageSize * pageIndex) | |||
.Take(pageSize) | |||
.ToListAsync(); | |||
var model = new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | |||
/* The "awesome" fix for testing Devspaces */ | |||
return Ok(model); | |||
/* | |||
foreach (var pr in itemsOnPage) { | |||
pr.Name = "Awesome " + pr.Name; | |||
} | |||
private async Task<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)) | |||
{ | |||
return new List<CatalogItem>(); | |||
} | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
var idsToSelect = numIds | |||
.Select(id => id.Value); | |||
var model = new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | |||
var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); | |||
items = ChangeUriPlaceholder(items); | |||
return Ok(model); | |||
} | |||
return items; | |||
} | |||
private async Task<List<CatalogItem>> GetItemsByIdsAsync(string ids) | |||
{ | |||
var numIds = ids.Split(',').Select(id => (Ok: int.TryParse(id, out int x), Value: x)); | |||
[HttpGet] | |||
[Route("items/{id:int}")] | |||
[ProducesResponseType((int)HttpStatusCode.NotFound)] | |||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||
[ProducesResponseType(typeof(CatalogItem), (int)HttpStatusCode.OK)] | |||
public async Task<ActionResult<CatalogItem>> ItemByIdAsync(int id) | |||
if (!numIds.All(nid => nid.Ok)) | |||
{ | |||
if (id <= 0) | |||
{ | |||
return BadRequest(); | |||
} | |||
return new List<CatalogItem>(); | |||
} | |||
var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == id); | |||
var idsToSelect = numIds | |||
.Select(id => id.Value); | |||
var baseUri = _settings.PicBaseUrl; | |||
var azureStorageEnabled = _settings.AzureStorageEnabled; | |||
var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); | |||
item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | |||
items = ChangeUriPlaceholder(items); | |||
if (item != null) | |||
{ | |||
return item; | |||
} | |||
return items; | |||
} | |||
return NotFound(); | |||
[HttpGet] | |||
[Route("items/{id:int}")] | |||
[ProducesResponseType((int)HttpStatusCode.NotFound)] | |||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||
[ProducesResponseType(typeof(CatalogItem), (int)HttpStatusCode.OK)] | |||
public async Task<ActionResult<CatalogItem>> ItemByIdAsync(int id) | |||
{ | |||
if (id <= 0) | |||
{ | |||
return BadRequest(); | |||
} | |||
// GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] | |||
[HttpGet] | |||
[Route("items/withname/{name:minlength(1)}")] | |||
[ProducesResponseType(typeof(PaginatedItemsViewModel<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 item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == id); | |||
var itemsOnPage = await _catalogContext.CatalogItems | |||
.Where(c => c.Name.StartsWith(name)) | |||
.Skip(pageSize * pageIndex) | |||
.Take(pageSize) | |||
.ToListAsync(); | |||
var baseUri = _settings.PicBaseUrl; | |||
var azureStorageEnabled = _settings.AzureStorageEnabled; | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | |||
return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | |||
if (item != null) | |||
{ | |||
return item; | |||
} | |||
// GET api/v1/[controller]/items/type/1/brand[?pageSize=3&pageIndex=10] | |||
[HttpGet] | |||
[Route("items/type/{catalogTypeId}/brand/{catalogBrandId:int?}")] | |||
[ProducesResponseType(typeof(PaginatedItemsViewModel<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; | |||
return NotFound(); | |||
} | |||
root = root.Where(ci => ci.CatalogTypeId == catalogTypeId); | |||
// GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] | |||
[HttpGet] | |||
[Route("items/withname/{name:minlength(1)}")] | |||
[ProducesResponseType(typeof(PaginatedItemsViewModel<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(); | |||
if (catalogBrandId.HasValue) | |||
{ | |||
root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); | |||
} | |||
var itemsOnPage = await _catalogContext.CatalogItems | |||
.Where(c => c.Name.StartsWith(name)) | |||
.Skip(pageSize * pageIndex) | |||
.Take(pageSize) | |||
.ToListAsync(); | |||
var totalItems = await root | |||
.LongCountAsync(); | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
var itemsOnPage = await root | |||
.Skip(pageSize * pageIndex) | |||
.Take(pageSize) | |||
.ToListAsync(); | |||
return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | |||
} | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
// GET api/v1/[controller]/items/type/1/brand[?pageSize=3&pageIndex=10] | |||
[HttpGet] | |||
[Route("items/type/{catalogTypeId}/brand/{catalogBrandId:int?}")] | |||
[ProducesResponseType(typeof(PaginatedItemsViewModel<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; | |||
return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | |||
} | |||
root = root.Where(ci => ci.CatalogTypeId == catalogTypeId); | |||
// GET api/v1/[controller]/items/type/all/brand[?pageSize=3&pageIndex=10] | |||
[HttpGet] | |||
[Route("items/type/all/brand/{catalogBrandId:int?}")] | |||
[ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)] | |||
public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsByBrandIdAsync(int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) | |||
if (catalogBrandId.HasValue) | |||
{ | |||
var root = (IQueryable<CatalogItem>)_catalogContext.CatalogItems; | |||
if (catalogBrandId.HasValue) | |||
{ | |||
root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); | |||
} | |||
root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); | |||
} | |||
var totalItems = await root | |||
.LongCountAsync(); | |||
var totalItems = await root | |||
.LongCountAsync(); | |||
var itemsOnPage = await root | |||
.Skip(pageSize * pageIndex) | |||
.Take(pageSize) | |||
.ToListAsync(); | |||
var itemsOnPage = await root | |||
.Skip(pageSize * pageIndex) | |||
.Take(pageSize) | |||
.ToListAsync(); | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, 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]/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; | |||
// GET api/v1/[controller]/CatalogBrands | |||
[HttpGet] | |||
[Route("catalogbrands")] | |||
[ProducesResponseType(typeof(List<CatalogBrand>), (int)HttpStatusCode.OK)] | |||
public async Task<ActionResult<List<CatalogBrand>>> CatalogBrandsAsync() | |||
if (catalogBrandId.HasValue) | |||
{ | |||
return await _catalogContext.CatalogBrands.ToListAsync(); | |||
root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); | |||
} | |||
//PUT api/v1/[controller]/items | |||
[Route("items")] | |||
[HttpPut] | |||
[ProducesResponseType((int)HttpStatusCode.NotFound)] | |||
[ProducesResponseType((int)HttpStatusCode.Created)] | |||
public async Task<ActionResult> UpdateProductAsync([FromBody] CatalogItem productToUpdate) | |||
{ | |||
var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); | |||
var totalItems = await root | |||
.LongCountAsync(); | |||
if (catalogItem == null) | |||
{ | |||
return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); | |||
} | |||
var itemsOnPage = await root | |||
.Skip(pageSize * pageIndex) | |||
.Take(pageSize) | |||
.ToListAsync(); | |||
var oldPrice = catalogItem.Price; | |||
var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price; | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
// Update current product | |||
catalogItem = productToUpdate; | |||
_catalogContext.CatalogItems.Update(catalogItem); | |||
return new PaginatedItemsViewModel<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); | |||
} | |||
if (raiseProductPriceChangedEvent) // Save product's data and publish integration event through the Event Bus if price has changed | |||
{ | |||
//Create Integration Event to be published through the Event Bus | |||
var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); | |||
// GET api/v1/[controller]/CatalogTypes | |||
[HttpGet] | |||
[Route("catalogtypes")] | |||
[ProducesResponseType(typeof(List<CatalogType>), (int)HttpStatusCode.OK)] | |||
public async Task<ActionResult<List<CatalogType>>> CatalogTypesAsync() | |||
{ | |||
return await _catalogContext.CatalogTypes.ToListAsync(); | |||
} | |||
// Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction | |||
await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); | |||
// GET api/v1/[controller]/CatalogBrands | |||
[HttpGet] | |||
[Route("catalogbrands")] | |||
[ProducesResponseType(typeof(List<CatalogBrand>), (int)HttpStatusCode.OK)] | |||
public async Task<ActionResult<List<CatalogBrand>>> CatalogBrandsAsync() | |||
{ | |||
return await _catalogContext.CatalogBrands.ToListAsync(); | |||
} | |||
// Publish through the Event Bus and mark the saved event as published | |||
await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); | |||
} | |||
else // Just save the updated product because the Product's Price hasn't changed. | |||
{ | |||
await _catalogContext.SaveChangesAsync(); | |||
} | |||
//PUT api/v1/[controller]/items | |||
[Route("items")] | |||
[HttpPut] | |||
[ProducesResponseType((int)HttpStatusCode.NotFound)] | |||
[ProducesResponseType((int)HttpStatusCode.Created)] | |||
public async Task<ActionResult> UpdateProductAsync([FromBody] CatalogItem productToUpdate) | |||
{ | |||
var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); | |||
return CreatedAtAction(nameof(ItemByIdAsync), new { id = productToUpdate.Id }, null); | |||
if (catalogItem == null) | |||
{ | |||
return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); | |||
} | |||
//POST api/v1/[controller]/items | |||
[Route("items")] | |||
[HttpPost] | |||
[ProducesResponseType((int)HttpStatusCode.Created)] | |||
public async Task<ActionResult> CreateProductAsync([FromBody] CatalogItem product) | |||
var oldPrice = catalogItem.Price; | |||
var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price; | |||
// Update current product | |||
catalogItem = productToUpdate; | |||
_catalogContext.CatalogItems.Update(catalogItem); | |||
if (raiseProductPriceChangedEvent) // Save product's data and publish integration event through the Event Bus if price has changed | |||
{ | |||
var item = new CatalogItem | |||
{ | |||
CatalogBrandId = product.CatalogBrandId, | |||
CatalogTypeId = product.CatalogTypeId, | |||
Description = product.Description, | |||
Name = product.Name, | |||
PictureFileName = product.PictureFileName, | |||
Price = product.Price | |||
}; | |||
//Create Integration Event to be published through the Event Bus | |||
var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); | |||
_catalogContext.CatalogItems.Add(item); | |||
// Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction | |||
await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); | |||
// Publish through the Event Bus and mark the saved event as published | |||
await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); | |||
} | |||
else // Just save the updated product because the Product's Price hasn't changed. | |||
{ | |||
await _catalogContext.SaveChangesAsync(); | |||
return CreatedAtAction(nameof(ItemByIdAsync), new { id = item.Id }, null); | |||
} | |||
//DELETE api/v1/[controller]/id | |||
[Route("{id}")] | |||
[HttpDelete] | |||
[ProducesResponseType((int)HttpStatusCode.NoContent)] | |||
[ProducesResponseType((int)HttpStatusCode.NotFound)] | |||
public async Task<ActionResult> DeleteProductAsync(int id) | |||
return CreatedAtAction(nameof(ItemByIdAsync), new { id = productToUpdate.Id }, null); | |||
} | |||
//POST api/v1/[controller]/items | |||
[Route("items")] | |||
[HttpPost] | |||
[ProducesResponseType((int)HttpStatusCode.Created)] | |||
public async Task<ActionResult> CreateProductAsync([FromBody] CatalogItem product) | |||
{ | |||
var item = new CatalogItem | |||
{ | |||
var product = _catalogContext.CatalogItems.SingleOrDefault(x => x.Id == id); | |||
CatalogBrandId = product.CatalogBrandId, | |||
CatalogTypeId = product.CatalogTypeId, | |||
Description = product.Description, | |||
Name = product.Name, | |||
PictureFileName = product.PictureFileName, | |||
Price = product.Price | |||
}; | |||
if (product == null) | |||
{ | |||
return NotFound(); | |||
} | |||
_catalogContext.CatalogItems.Add(item); | |||
_catalogContext.CatalogItems.Remove(product); | |||
await _catalogContext.SaveChangesAsync(); | |||
await _catalogContext.SaveChangesAsync(); | |||
return CreatedAtAction(nameof(ItemByIdAsync), new { id = item.Id }, null); | |||
} | |||
return NoContent(); | |||
} | |||
//DELETE api/v1/[controller]/id | |||
[Route("{id}")] | |||
[HttpDelete] | |||
[ProducesResponseType((int)HttpStatusCode.NoContent)] | |||
[ProducesResponseType((int)HttpStatusCode.NotFound)] | |||
public async Task<ActionResult> DeleteProductAsync(int id) | |||
{ | |||
var product = _catalogContext.CatalogItems.SingleOrDefault(x => x.Id == id); | |||
private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items) | |||
if (product == null) | |||
{ | |||
var baseUri = _settings.PicBaseUrl; | |||
var azureStorageEnabled = _settings.AzureStorageEnabled; | |||
return NotFound(); | |||
} | |||
foreach (var item in items) | |||
{ | |||
item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | |||
} | |||
_catalogContext.CatalogItems.Remove(product); | |||
await _catalogContext.SaveChangesAsync(); | |||
return NoContent(); | |||
} | |||
return items; | |||
private List<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; | |||
} | |||
} |
@ -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) | |||
{ | |||
if (catalogItemId <= 0) | |||
{ | |||
return BadRequest(); | |||
} | |||
var item = await _catalogContext.CatalogItems | |||
.SingleOrDefaultAsync(ci => ci.Id == catalogItemId); | |||
if (item != null) | |||
{ | |||
var webRoot = _env.WebRootPath; | |||
var path = Path.Combine(webRoot, item.PictureFileName); | |||
var item = await _catalogContext.CatalogItems | |||
.SingleOrDefaultAsync(ci => ci.Id == catalogItemId); | |||
string imageFileExtension = Path.GetExtension(item.PictureFileName); | |||
string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); | |||
if (item != null) | |||
{ | |||
var webRoot = _env.WebRootPath; | |||
var path = Path.Combine(webRoot, item.PictureFileName); | |||
var buffer = await System.IO.File.ReadAllBytesAsync(path); | |||
string imageFileExtension = Path.GetExtension(item.PictureFileName); | |||
string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); | |||
return File(buffer, mimetype); | |||
} | |||
var buffer = await System.IO.File.ReadAllBytesAsync(path); | |||
return NotFound(); | |||
return File(buffer, mimetype); | |||
} | |||
private string GetImageMimeTypeFromImageFileExtension(string extension) | |||
{ | |||
string mimetype; | |||
return NotFound(); | |||
} | |||
switch (extension) | |||
{ | |||
case ".png": | |||
mimetype = "image/png"; | |||
break; | |||
case ".gif": | |||
mimetype = "image/gif"; | |||
break; | |||
case ".jpg": | |||
case ".jpeg": | |||
mimetype = "image/jpeg"; | |||
break; | |||
case ".bmp": | |||
mimetype = "image/bmp"; | |||
break; | |||
case ".tiff": | |||
mimetype = "image/tiff"; | |||
break; | |||
case ".wmf": | |||
mimetype = "image/wmf"; | |||
break; | |||
case ".jp2": | |||
mimetype = "image/jp2"; | |||
break; | |||
case ".svg": | |||
mimetype = "image/svg+xml"; | |||
break; | |||
default: | |||
mimetype = "application/octet-stream"; | |||
break; | |||
} | |||
private string GetImageMimeTypeFromImageFileExtension(string extension) | |||
{ | |||
string mimetype; | |||
return mimetype; | |||
switch (extension) | |||
{ | |||
case ".png": | |||
mimetype = "image/png"; | |||
break; | |||
case ".gif": | |||
mimetype = "image/gif"; | |||
break; | |||
case ".jpg": | |||
case ".jpeg": | |||
mimetype = "image/jpeg"; | |||
break; | |||
case ".bmp": | |||
mimetype = "image/bmp"; | |||
break; | |||
case ".tiff": | |||
mimetype = "image/tiff"; | |||
break; | |||
case ".wmf": | |||
mimetype = "image/wmf"; | |||
break; | |||
case ".jp2": | |||
mimetype = "image/jp2"; | |||
break; | |||
case ".svg": | |||
mimetype = "image/svg+xml"; | |||
break; | |||
default: | |||
mimetype = "application/octet-stream"; | |||
break; | |||
} | |||
return mimetype; | |||
} | |||
} |
@ -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 | |||
using (var scope = host.Services.CreateScope()) | |||
{ | |||
var underK8s = host.IsInKubernetes(); | |||
var services = scope.ServiceProvider; | |||
using (var scope = host.Services.CreateScope()) | |||
{ | |||
var services = scope.ServiceProvider; | |||
var logger = services.GetRequiredService<ILogger<TContext>>(); | |||
var logger = services.GetRequiredService<ILogger<TContext>>(); | |||
var context = services.GetService<TContext>(); | |||
var context = services.GetService<TContext>(); | |||
try | |||
{ | |||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); | |||
try | |||
if (underK8s) | |||
{ | |||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); | |||
if (underK8s) | |||
{ | |||
InvokeSeeder(seeder, context, services); | |||
} | |||
else | |||
{ | |||
var retry = Policy.Handle<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)); | |||
} | |||
InvokeSeeder(seeder, context, services); | |||
} | |||
else | |||
{ | |||
var retry = Policy.Handle<SqlException>() | |||
.WaitAndRetry(new TimeSpan[] | |||
{ | |||
TimeSpan.FromSeconds(3), | |||
TimeSpan.FromSeconds(5), | |||
TimeSpan.FromSeconds(8), | |||
}); | |||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); | |||
//if the sql server container is not created on run docker compose this | |||
//migration can't fail for network related exception. The retry options for DbContext only | |||
//apply to transient exceptions | |||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) | |||
retry.Execute(() => InvokeSeeder(seeder, context, services)); | |||
} | |||
catch (Exception ex) | |||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); | |||
if (underK8s) | |||
{ | |||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); | |||
if (underK8s) | |||
{ | |||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod | |||
} | |||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod | |||
} | |||
} | |||
return host; | |||
} | |||
private static void InvokeSeeder<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 | |||
{ | |||
returnedValue = new SelectTryResult<TSource, TResult>(element, selector(element), null); | |||
} | |||
catch (Exception ex) | |||
{ | |||
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, default(TResult), ex); | |||
} | |||
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<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 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> | |||
public class SelectTryResult<TSource, TResult> | |||
{ | |||
internal SelectTryResult(TSource source, TResult result, Exception exception) | |||
{ | |||
internal SelectTryResult(TSource source, TResult result, Exception exception) | |||
{ | |||
Source = source; | |||
Result = result; | |||
CaughtException = exception; | |||
} | |||
public TSource Source { get; private set; } | |||
public TResult Result { get; private set; } | |||
public Exception CaughtException { get; private set; } | |||
Source = source; | |||
Result = result; | |||
CaughtException = exception; | |||
} | |||
public TSource Source { get; private set; } | |||
public TResult Result { get; private set; } | |||
public Exception CaughtException { get; private set; } | |||
} | |||
} |
@ -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 | |||
using (var scope = host.Services.CreateScope()) | |||
{ | |||
var underK8s = host.IsInKubernetes(); | |||
var services = scope.ServiceProvider; | |||
using (var scope = host.Services.CreateScope()) | |||
{ | |||
var services = scope.ServiceProvider; | |||
var logger = services.GetRequiredService<ILogger<TContext>>(); | |||
var logger = services.GetRequiredService<ILogger<TContext>>(); | |||
var context = services.GetService<TContext>(); | |||
var context = services.GetService<TContext>(); | |||
try | |||
{ | |||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); | |||
try | |||
if (underK8s) | |||
{ | |||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); | |||
if (underK8s) | |||
{ | |||
InvokeSeeder(seeder, context, services); | |||
} | |||
else | |||
{ | |||
var retry = Policy.Handle<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)); | |||
} | |||
InvokeSeeder(seeder, context, services); | |||
} | |||
else | |||
{ | |||
var retry = Policy.Handle<SqlException>() | |||
.WaitAndRetry(new TimeSpan[] | |||
{ | |||
TimeSpan.FromSeconds(3), | |||
TimeSpan.FromSeconds(5), | |||
TimeSpan.FromSeconds(8), | |||
}); | |||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); | |||
//if the sql server container is not created on run docker compose this | |||
//migration can't fail for network related exception. The retry options for DbContext only | |||
//apply to transient exceptions | |||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) | |||
retry.Execute(() => InvokeSeeder(seeder, context, services)); | |||
} | |||
catch (Exception ex) | |||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); | |||
if (underK8s) | |||
{ | |||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); | |||
if (underK8s) | |||
{ | |||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod | |||
} | |||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod | |||
} | |||
} | |||
return host; | |||
} | |||
private static void InvokeSeeder<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,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; | |||
context.Status = new Status(StatusCode.FailedPrecondition, $"Id must be > 0 (received {request.Id})"); | |||
return null; | |||
} | |||
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); | |||
var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == request.Id); | |||
var baseUri = _settings.PicBaseUrl; | |||
var azureStorageEnabled = _settings.AzureStorageEnabled; | |||
item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | |||
if (item != null) | |||
if (item != null) | |||
{ | |||
return new CatalogItemResponse() | |||
{ | |||
return new CatalogItemResponse() | |||
{ | |||
AvailableStock = item.AvailableStock, | |||
Description = item.Description, | |||
Id = item.Id, | |||
MaxStockThreshold = item.MaxStockThreshold, | |||
Name = item.Name, | |||
OnReorder = item.OnReorder, | |||
PictureFileName = item.PictureFileName, | |||
PictureUri = item.PictureUri, | |||
Price = (double)item.Price, | |||
RestockThreshold = item.RestockThreshold | |||
}; | |||
} | |||
context.Status = new Status(StatusCode.NotFound, $"Product with id {request.Id} do not exist"); | |||
return null; | |||
AvailableStock = item.AvailableStock, | |||
Description = item.Description, | |||
Id = item.Id, | |||
MaxStockThreshold = item.MaxStockThreshold, | |||
Name = item.Name, | |||
OnReorder = item.OnReorder, | |||
PictureFileName = item.PictureFileName, | |||
PictureUri = item.PictureUri, | |||
Price = (double)item.Price, | |||
RestockThreshold = item.RestockThreshold | |||
}; | |||
} | |||
public override async Task<PaginatedItemsResponse> GetItemsByIds(CatalogItemsRequest request, ServerCallContext context) | |||
context.Status = new Status(StatusCode.NotFound, $"Product with id {request.Id} do not exist"); | |||
return null; | |||
} | |||
public override async Task<PaginatedItemsResponse> GetItemsByIds(CatalogItemsRequest request, ServerCallContext context) | |||
{ | |||
if (!string.IsNullOrEmpty(request.Ids)) | |||
{ | |||
if (!string.IsNullOrEmpty(request.Ids)) | |||
{ | |||
var items = await GetItemsByIdsAsync(request.Ids); | |||
var items = await GetItemsByIdsAsync(request.Ids); | |||
context.Status = !items.Any() ? | |||
new Status(StatusCode.NotFound, $"ids value invalid. Must be comma-separated list of numbers") : | |||
new Status(StatusCode.OK, string.Empty); | |||
context.Status = !items.Any() ? | |||
new Status(StatusCode.NotFound, $"ids value invalid. Must be comma-separated list of numbers") : | |||
new Status(StatusCode.OK, string.Empty); | |||
return this.MapToResponse(items); | |||
} | |||
return this.MapToResponse(items); | |||
} | |||
var totalItems = await _catalogContext.CatalogItems | |||
.LongCountAsync(); | |||
var totalItems = await _catalogContext.CatalogItems | |||
.LongCountAsync(); | |||
var itemsOnPage = await _catalogContext.CatalogItems | |||
.OrderBy(c => c.Name) | |||
.Skip(request.PageSize * request.PageIndex) | |||
.Take(request.PageSize) | |||
.ToListAsync(); | |||
var itemsOnPage = await _catalogContext.CatalogItems | |||
.OrderBy(c => c.Name) | |||
.Skip(request.PageSize * request.PageIndex) | |||
.Take(request.PageSize) | |||
.ToListAsync(); | |||
/* The "awesome" fix for testing Devspaces */ | |||
/* The "awesome" fix for testing Devspaces */ | |||
/* | |||
foreach (var pr in itemsOnPage) { | |||
pr.Name = "Awesome " + pr.Name; | |||
} | |||
/* | |||
foreach (var pr in itemsOnPage) { | |||
pr.Name = "Awesome " + pr.Name; | |||
} | |||
*/ | |||
*/ | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
itemsOnPage = ChangeUriPlaceholder(itemsOnPage); | |||
var model = this.MapToResponse(itemsOnPage, totalItems, request.PageIndex, request.PageSize); | |||
context.Status = new Status(StatusCode.OK, string.Empty); | |||
var model = this.MapToResponse(itemsOnPage, totalItems, request.PageIndex, request.PageSize); | |||
context.Status = new Status(StatusCode.OK, string.Empty); | |||
return model; | |||
} | |||
return model; | |||
} | |||
private PaginatedItemsResponse MapToResponse(List<CatalogItem> items) | |||
{ | |||
return this.MapToResponse(items, items.Count, 1, items.Count); | |||
} | |||
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) | |||
private PaginatedItemsResponse MapToResponse(List<CatalogItem> items, long count, int pageIndex, int pageSize) | |||
{ | |||
var result = new PaginatedItemsResponse() | |||
{ | |||
var result = new PaginatedItemsResponse() | |||
{ | |||
Count = count, | |||
PageIndex = pageIndex, | |||
PageSize = pageSize, | |||
}; | |||
Count = count, | |||
PageIndex = pageIndex, | |||
PageSize = pageSize, | |||
}; | |||
items.ForEach(i => | |||
items.ForEach(i => | |||
{ | |||
var brand = i.CatalogBrand == null | |||
? null | |||
: new CatalogApi.CatalogBrand() | |||
{ | |||
Id = i.CatalogBrand.Id, | |||
Name = i.CatalogBrand.Brand, | |||
}; | |||
var catalogType = i.CatalogType == null | |||
? null | |||
: new CatalogApi.CatalogType() | |||
{ | |||
Id = i.CatalogType.Id, | |||
Type = i.CatalogType.Type, | |||
}; | |||
result.Data.Add(new CatalogItemResponse() | |||
{ | |||
var brand = i.CatalogBrand == null | |||
? null | |||
: new CatalogApi.CatalogBrand() | |||
{ | |||
Id = i.CatalogBrand.Id, | |||
Name = i.CatalogBrand.Brand, | |||
}; | |||
var catalogType = i.CatalogType == null | |||
? null | |||
: new CatalogApi.CatalogType() | |||
{ | |||
Id = i.CatalogType.Id, | |||
Type = i.CatalogType.Type, | |||
}; | |||
result.Data.Add(new CatalogItemResponse() | |||
{ | |||
AvailableStock = i.AvailableStock, | |||
Description = i.Description, | |||
Id = i.Id, | |||
MaxStockThreshold = i.MaxStockThreshold, | |||
Name = i.Name, | |||
OnReorder = i.OnReorder, | |||
PictureFileName = i.PictureFileName, | |||
PictureUri = i.PictureUri, | |||
RestockThreshold = i.RestockThreshold, | |||
CatalogBrand = brand, | |||
CatalogType = catalogType, | |||
Price = (double)i.Price, | |||
}); | |||
AvailableStock = i.AvailableStock, | |||
Description = i.Description, | |||
Id = i.Id, | |||
MaxStockThreshold = i.MaxStockThreshold, | |||
Name = i.Name, | |||
OnReorder = i.OnReorder, | |||
PictureFileName = i.PictureFileName, | |||
PictureUri = i.PictureUri, | |||
RestockThreshold = i.RestockThreshold, | |||
CatalogBrand = brand, | |||
CatalogType = catalogType, | |||
Price = (double)i.Price, | |||
}); | |||
}); | |||
return result; | |||
} | |||
return result; | |||
} | |||
private async Task<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)); | |||
return new List<CatalogItem>(); | |||
} | |||
if (!numIds.All(nid => nid.Ok)) | |||
{ | |||
return new List<CatalogItem>(); | |||
} | |||
var idsToSelect = numIds | |||
.Select(id => id.Value); | |||
var idsToSelect = numIds | |||
.Select(id => id.Value); | |||
var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); | |||
var items = await _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToListAsync(); | |||
items = ChangeUriPlaceholder(items); | |||
items = ChangeUriPlaceholder(items); | |||
return items; | |||
} | |||
return items; | |||
} | |||
private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items) | |||
{ | |||
var baseUri = _settings.PicBaseUrl; | |||
var azureStorageEnabled = _settings.AzureStorageEnabled; | |||
private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items) | |||
foreach (var item in items) | |||
{ | |||
var baseUri = _settings.PicBaseUrl; | |||
var azureStorageEnabled = _settings.AzureStorageEnabled; | |||
foreach (var item in items) | |||
{ | |||
item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | |||
} | |||
return items; | |||
item.FillProductUrl(baseUri, azureStorageEnabled: azureStorageEnabled); | |||
} | |||
return items; | |||
} | |||
} |
@ -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 | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | |||
public class CatalogContext : DbContext | |||
{ | |||
public CatalogContext(DbContextOptions<CatalogContext> options) : base(options) | |||
{ | |||
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 DbSet<CatalogItem> CatalogItems { get; set; } | |||
public DbSet<CatalogBrand> CatalogBrands { get; set; } | |||
public DbSet<CatalogType> CatalogTypes { get; set; } | |||
protected override void OnModelCreating(ModelBuilder builder) | |||
{ | |||
builder.ApplyConfiguration(new CatalogBrandEntityTypeConfiguration()); | |||
builder.ApplyConfiguration(new CatalogTypeEntityTypeConfiguration()); | |||
builder.ApplyConfiguration(new CatalogItemEntityTypeConfiguration()); | |||
} | |||
protected override void OnModelCreating(ModelBuilder builder) | |||
{ | |||
builder.ApplyConfiguration(new CatalogBrandEntityTypeConfiguration()); | |||
builder.ApplyConfiguration(new CatalogTypeEntityTypeConfiguration()); | |||
builder.ApplyConfiguration(new CatalogItemEntityTypeConfiguration()); | |||
} | |||
} | |||
public class CatalogContextDesignFactory : IDesignTimeDbContextFactory<CatalogContext> | |||
public class CatalogContextDesignFactory : IDesignTimeDbContextFactory<CatalogContext> | |||
{ | |||
public CatalogContext CreateDbContext(string[] args) | |||
{ | |||
public CatalogContext CreateDbContext(string[] args) | |||
{ | |||
var optionsBuilder = new DbContextOptionsBuilder<CatalogContext>() | |||
.UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true"); | |||
var optionsBuilder = new DbContextOptionsBuilder<CatalogContext>() | |||
.UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true"); | |||
return new CatalogContext(optionsBuilder.Options); | |||
} | |||
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.CatalogBrands.AddRangeAsync(useCustomizationData | |||
? GetCatalogBrandsFromFile(contentRootPath, logger) | |||
: GetPreconfiguredCatalogBrands()); | |||
await context.SaveChangesAsync(); | |||
} | |||
await context.SaveChangesAsync(); | |||
} | |||
if (!context.CatalogTypes.Any()) | |||
{ | |||
await context.CatalogTypes.AddRangeAsync(useCustomizationData | |||
? GetCatalogTypesFromFile(contentRootPath, logger) | |||
: GetPreconfiguredCatalogTypes()); | |||
if (!context.CatalogTypes.Any()) | |||
{ | |||
await context.CatalogTypes.AddRangeAsync(useCustomizationData | |||
? GetCatalogTypesFromFile(contentRootPath, logger) | |||
: GetPreconfiguredCatalogTypes()); | |||
await context.SaveChangesAsync(); | |||
} | |||
await context.SaveChangesAsync(); | |||
} | |||
if (!context.CatalogItems.Any()) | |||
{ | |||
await context.CatalogItems.AddRangeAsync(useCustomizationData | |||
? GetCatalogItemsFromFile(contentRootPath, context, logger) | |||
: GetPreconfiguredItems()); | |||
if (!context.CatalogItems.Any()) | |||
{ | |||
await context.CatalogItems.AddRangeAsync(useCustomizationData | |||
? GetCatalogItemsFromFile(contentRootPath, context, logger) | |||
: GetPreconfiguredItems()); | |||
await context.SaveChangesAsync(); | |||
await context.SaveChangesAsync(); | |||
GetCatalogItemPictures(contentRootPath, picturePath); | |||
} | |||
}); | |||
} | |||
GetCatalogItemPictures(contentRootPath, picturePath); | |||
} | |||
}); | |||
private IEnumerable<CatalogBrand> GetCatalogBrandsFromFile(string contentRootPath, ILogger<CatalogContextSeed> logger) | |||
{ | |||
string csvFileCatalogBrands = Path.Combine(contentRootPath, "Setup", "CatalogBrands.csv"); | |||
if (!File.Exists(csvFileCatalogBrands)) | |||
{ | |||
return GetPreconfiguredCatalogBrands(); | |||
} | |||
private IEnumerable<CatalogBrand> GetCatalogBrandsFromFile(string contentRootPath, ILogger<CatalogContextSeed> logger) | |||
string[] csvheaders; | |||
try | |||
{ | |||
string csvFileCatalogBrands = Path.Combine(contentRootPath, "Setup", "CatalogBrands.csv"); | |||
string[] requiredHeaders = { "catalogbrand" }; | |||
csvheaders = GetHeaders(csvFileCatalogBrands, requiredHeaders); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | |||
return GetPreconfiguredCatalogBrands(); | |||
} | |||
if (!File.Exists(csvFileCatalogBrands)) | |||
{ | |||
return GetPreconfiguredCatalogBrands(); | |||
} | |||
return File.ReadAllLines(csvFileCatalogBrands) | |||
.Skip(1) // skip header row | |||
.SelectTry(x => CreateCatalogBrand(x)) | |||
.OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) | |||
.Where(x => x != null); | |||
} | |||
string[] csvheaders; | |||
try | |||
{ | |||
string[] requiredHeaders = { "catalogbrand" }; | |||
csvheaders = GetHeaders(csvFileCatalogBrands, requiredHeaders); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | |||
return GetPreconfiguredCatalogBrands(); | |||
} | |||
private CatalogBrand CreateCatalogBrand(string brand) | |||
{ | |||
brand = brand.Trim('"').Trim(); | |||
return File.ReadAllLines(csvFileCatalogBrands) | |||
.Skip(1) // skip header row | |||
.SelectTry(x => CreateCatalogBrand(x)) | |||
.OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) | |||
.Where(x => x != null); | |||
if (String.IsNullOrEmpty(brand)) | |||
{ | |||
throw new Exception("catalog Brand Name is empty"); | |||
} | |||
private CatalogBrand CreateCatalogBrand(string brand) | |||
return new CatalogBrand | |||
{ | |||
brand = brand.Trim('"').Trim(); | |||
Brand = brand, | |||
}; | |||
} | |||
if (String.IsNullOrEmpty(brand)) | |||
{ | |||
throw new Exception("catalog Brand Name is empty"); | |||
} | |||
private IEnumerable<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" } | |||
}; | |||
} | |||
return new CatalogBrand | |||
{ | |||
Brand = brand, | |||
}; | |||
} | |||
private IEnumerable<CatalogType> GetCatalogTypesFromFile(string contentRootPath, ILogger<CatalogContextSeed> logger) | |||
{ | |||
string csvFileCatalogTypes = Path.Combine(contentRootPath, "Setup", "CatalogTypes.csv"); | |||
private IEnumerable<CatalogBrand> GetPreconfiguredCatalogBrands() | |||
if (!File.Exists(csvFileCatalogTypes)) | |||
{ | |||
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" } | |||
}; | |||
return GetPreconfiguredCatalogTypes(); | |||
} | |||
private IEnumerable<CatalogType> GetCatalogTypesFromFile(string contentRootPath, ILogger<CatalogContextSeed> logger) | |||
string[] csvheaders; | |||
try | |||
{ | |||
string csvFileCatalogTypes = Path.Combine(contentRootPath, "Setup", "CatalogTypes.csv"); | |||
string[] requiredHeaders = { "catalogtype" }; | |||
csvheaders = GetHeaders(csvFileCatalogTypes, requiredHeaders); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | |||
return GetPreconfiguredCatalogTypes(); | |||
} | |||
if (!File.Exists(csvFileCatalogTypes)) | |||
{ | |||
return GetPreconfiguredCatalogTypes(); | |||
} | |||
return File.ReadAllLines(csvFileCatalogTypes) | |||
.Skip(1) // skip header row | |||
.SelectTry(x => CreateCatalogType(x)) | |||
.OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) | |||
.Where(x => x != null); | |||
} | |||
string[] csvheaders; | |||
try | |||
{ | |||
string[] requiredHeaders = { "catalogtype" }; | |||
csvheaders = GetHeaders(csvFileCatalogTypes, requiredHeaders); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | |||
return GetPreconfiguredCatalogTypes(); | |||
} | |||
private CatalogType CreateCatalogType(string type) | |||
{ | |||
type = type.Trim('"').Trim(); | |||
return File.ReadAllLines(csvFileCatalogTypes) | |||
.Skip(1) // skip header row | |||
.SelectTry(x => CreateCatalogType(x)) | |||
.OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) | |||
.Where(x => x != null); | |||
if (String.IsNullOrEmpty(type)) | |||
{ | |||
throw new Exception("catalog Type Name is empty"); | |||
} | |||
private CatalogType CreateCatalogType(string type) | |||
return new CatalogType | |||
{ | |||
Type = type, | |||
}; | |||
} | |||
private IEnumerable<CatalogType> GetPreconfiguredCatalogTypes() | |||
{ | |||
return new List<CatalogType>() | |||
{ | |||
type = type.Trim('"').Trim(); | |||
new CatalogType() { Type = "Mug"}, | |||
new CatalogType() { Type = "T-Shirt" }, | |||
new CatalogType() { Type = "Sheet" }, | |||
new CatalogType() { Type = "USB Memory Stick" } | |||
}; | |||
} | |||
if (String.IsNullOrEmpty(type)) | |||
{ | |||
throw new Exception("catalog Type Name is empty"); | |||
} | |||
private IEnumerable<CatalogItem> GetCatalogItemsFromFile(string contentRootPath, CatalogContext context, ILogger<CatalogContextSeed> logger) | |||
{ | |||
string csvFileCatalogItems = Path.Combine(contentRootPath, "Setup", "CatalogItems.csv"); | |||
return new CatalogType | |||
{ | |||
Type = type, | |||
}; | |||
if (!File.Exists(csvFileCatalogItems)) | |||
{ | |||
return GetPreconfiguredItems(); | |||
} | |||
private IEnumerable<CatalogType> GetPreconfiguredCatalogTypes() | |||
string[] csvheaders; | |||
try | |||
{ | |||
return new List<CatalogType>() | |||
{ | |||
new CatalogType() { Type = "Mug"}, | |||
new CatalogType() { Type = "T-Shirt" }, | |||
new CatalogType() { Type = "Sheet" }, | |||
new CatalogType() { Type = "USB Memory Stick" } | |||
}; | |||
string[] requiredHeaders = { "catalogtypename", "catalogbrandname", "description", "name", "price", "picturefilename" }; | |||
string[] optionalheaders = { "availablestock", "restockthreshold", "maxstockthreshold", "onreorder" }; | |||
csvheaders = GetHeaders(csvFileCatalogItems, requiredHeaders, optionalheaders); | |||
} | |||
private IEnumerable<CatalogItem> GetCatalogItemsFromFile(string contentRootPath, CatalogContext context, ILogger<CatalogContextSeed> logger) | |||
catch (Exception ex) | |||
{ | |||
string csvFileCatalogItems = Path.Combine(contentRootPath, "Setup", "CatalogItems.csv"); | |||
logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | |||
return GetPreconfiguredItems(); | |||
} | |||
if (!File.Exists(csvFileCatalogItems)) | |||
{ | |||
return GetPreconfiguredItems(); | |||
} | |||
var catalogTypeIdLookup = context.CatalogTypes.ToDictionary(ct => ct.Type, ct => ct.Id); | |||
var catalogBrandIdLookup = context.CatalogBrands.ToDictionary(ct => ct.Brand, ct => ct.Id); | |||
string[] csvheaders; | |||
try | |||
{ | |||
string[] requiredHeaders = { "catalogtypename", "catalogbrandname", "description", "name", "price", "picturefilename" }; | |||
string[] optionalheaders = { "availablestock", "restockthreshold", "maxstockthreshold", "onreorder" }; | |||
csvheaders = GetHeaders(csvFileCatalogItems, requiredHeaders, optionalheaders); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | |||
return GetPreconfiguredItems(); | |||
} | |||
var catalogTypeIdLookup = context.CatalogTypes.ToDictionary(ct => ct.Type, ct => ct.Id); | |||
var catalogBrandIdLookup = context.CatalogBrands.ToDictionary(ct => ct.Brand, ct => ct.Id); | |||
return File.ReadAllLines(csvFileCatalogItems) | |||
.Skip(1) // skip header row | |||
.Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) | |||
.SelectTry(column => CreateCatalogItem(column, csvheaders, catalogTypeIdLookup, catalogBrandIdLookup)) | |||
.OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) | |||
.Where(x => x != null); | |||
} | |||
return File.ReadAllLines(csvFileCatalogItems) | |||
.Skip(1) // skip header row | |||
.Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) | |||
.SelectTry(column => CreateCatalogItem(column, csvheaders, catalogTypeIdLookup, catalogBrandIdLookup)) | |||
.OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) | |||
.Where(x => x != null); | |||
private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary<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()}'"); | |||
} | |||
private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary<String, int> catalogTypeIdLookup, Dictionary<String, int> catalogBrandIdLookup) | |||
string catalogTypeName = column[Array.IndexOf(headers, "catalogtypename")].Trim('"').Trim(); | |||
if (!catalogTypeIdLookup.ContainsKey(catalogTypeName)) | |||
{ | |||
if (column.Count() != headers.Count()) | |||
{ | |||
throw new Exception($"column count '{column.Count()}' not the same as headers count'{headers.Count()}'"); | |||
} | |||
string catalogTypeName = column[Array.IndexOf(headers, "catalogtypename")].Trim('"').Trim(); | |||
if (!catalogTypeIdLookup.ContainsKey(catalogTypeName)) | |||
{ | |||
throw new Exception($"type={catalogTypeName} does not exist in catalogTypes"); | |||
} | |||
throw new Exception($"type={catalogTypeName} does not exist in catalogTypes"); | |||
} | |||
string catalogBrandName = column[Array.IndexOf(headers, "catalogbrandname")].Trim('"').Trim(); | |||
if (!catalogBrandIdLookup.ContainsKey(catalogBrandName)) | |||
{ | |||
throw new Exception($"type={catalogTypeName} does not exist in catalogTypes"); | |||
} | |||
string catalogBrandName = column[Array.IndexOf(headers, "catalogbrandname")].Trim('"').Trim(); | |||
if (!catalogBrandIdLookup.ContainsKey(catalogBrandName)) | |||
{ | |||
throw new Exception($"type={catalogTypeName} does not exist in catalogTypes"); | |||
} | |||
string priceString = column[Array.IndexOf(headers, "price")].Trim('"').Trim(); | |||
if (!Decimal.TryParse(priceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out Decimal price)) | |||
{ | |||
throw new Exception($"price={priceString}is not a valid decimal number"); | |||
} | |||
string priceString = column[Array.IndexOf(headers, "price")].Trim('"').Trim(); | |||
if (!Decimal.TryParse(priceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out Decimal price)) | |||
{ | |||
throw new Exception($"price={priceString}is not a valid decimal number"); | |||
} | |||
var catalogItem = new CatalogItem() | |||
{ | |||
CatalogTypeId = catalogTypeIdLookup[catalogTypeName], | |||
CatalogBrandId = catalogBrandIdLookup[catalogBrandName], | |||
Description = column[Array.IndexOf(headers, "description")].Trim('"').Trim(), | |||
Name = column[Array.IndexOf(headers, "name")].Trim('"').Trim(), | |||
Price = price, | |||
PictureFileName = column[Array.IndexOf(headers, "picturefilename")].Trim('"').Trim(), | |||
}; | |||
int availableStockIndex = Array.IndexOf(headers, "availablestock"); | |||
if (availableStockIndex != -1) | |||
var catalogItem = new CatalogItem() | |||
{ | |||
CatalogTypeId = catalogTypeIdLookup[catalogTypeName], | |||
CatalogBrandId = catalogBrandIdLookup[catalogBrandName], | |||
Description = column[Array.IndexOf(headers, "description")].Trim('"').Trim(), | |||
Name = column[Array.IndexOf(headers, "name")].Trim('"').Trim(), | |||
Price = price, | |||
PictureFileName = column[Array.IndexOf(headers, "picturefilename")].Trim('"').Trim(), | |||
}; | |||
int availableStockIndex = Array.IndexOf(headers, "availablestock"); | |||
if (availableStockIndex != -1) | |||
{ | |||
string availableStockString = column[availableStockIndex].Trim('"').Trim(); | |||
if (!String.IsNullOrEmpty(availableStockString)) | |||
{ | |||
string availableStockString = column[availableStockIndex].Trim('"').Trim(); | |||
if (!String.IsNullOrEmpty(availableStockString)) | |||
if (int.TryParse(availableStockString, out int availableStock)) | |||
{ | |||
if (int.TryParse(availableStockString, out int availableStock)) | |||
{ | |||
catalogItem.AvailableStock = availableStock; | |||
} | |||
else | |||
{ | |||
throw new Exception($"availableStock={availableStockString} is not a valid integer"); | |||
} | |||
catalogItem.AvailableStock = availableStock; | |||
} | |||
else | |||
{ | |||
throw new Exception($"availableStock={availableStockString} is not a valid integer"); | |||
} | |||
} | |||
} | |||
int restockThresholdIndex = Array.IndexOf(headers, "restockthreshold"); | |||
if (restockThresholdIndex != -1) | |||
int restockThresholdIndex = Array.IndexOf(headers, "restockthreshold"); | |||
if (restockThresholdIndex != -1) | |||
{ | |||
string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); | |||
if (!String.IsNullOrEmpty(restockThresholdString)) | |||
{ | |||
string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); | |||
if (!String.IsNullOrEmpty(restockThresholdString)) | |||
if (int.TryParse(restockThresholdString, out int restockThreshold)) | |||
{ | |||
if (int.TryParse(restockThresholdString, out int restockThreshold)) | |||
{ | |||
catalogItem.RestockThreshold = restockThreshold; | |||
} | |||
else | |||
{ | |||
throw new Exception($"restockThreshold={restockThreshold} is not a valid integer"); | |||
} | |||
catalogItem.RestockThreshold = restockThreshold; | |||
} | |||
else | |||
{ | |||
throw new Exception($"restockThreshold={restockThreshold} is not a valid integer"); | |||
} | |||
} | |||
} | |||
int maxStockThresholdIndex = Array.IndexOf(headers, "maxstockthreshold"); | |||
if (maxStockThresholdIndex != -1) | |||
int maxStockThresholdIndex = Array.IndexOf(headers, "maxstockthreshold"); | |||
if (maxStockThresholdIndex != -1) | |||
{ | |||
string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); | |||
if (!String.IsNullOrEmpty(maxStockThresholdString)) | |||
{ | |||
string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); | |||
if (!String.IsNullOrEmpty(maxStockThresholdString)) | |||
if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) | |||
{ | |||
if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) | |||
{ | |||
catalogItem.MaxStockThreshold = maxStockThreshold; | |||
} | |||
else | |||
{ | |||
throw new Exception($"maxStockThreshold={maxStockThreshold} is not a valid integer"); | |||
} | |||
catalogItem.MaxStockThreshold = maxStockThreshold; | |||
} | |||
else | |||
{ | |||
throw new Exception($"maxStockThreshold={maxStockThreshold} is not a valid integer"); | |||
} | |||
} | |||
} | |||
int onReorderIndex = Array.IndexOf(headers, "onreorder"); | |||
if (onReorderIndex != -1) | |||
int onReorderIndex = Array.IndexOf(headers, "onreorder"); | |||
if (onReorderIndex != -1) | |||
{ | |||
string onReorderString = column[onReorderIndex].Trim('"').Trim(); | |||
if (!String.IsNullOrEmpty(onReorderString)) | |||
{ | |||
string onReorderString = column[onReorderIndex].Trim('"').Trim(); | |||
if (!String.IsNullOrEmpty(onReorderString)) | |||
if (bool.TryParse(onReorderString, out bool onReorder)) | |||
{ | |||
if (bool.TryParse(onReorderString, out bool onReorder)) | |||
{ | |||
catalogItem.OnReorder = onReorder; | |||
} | |||
else | |||
{ | |||
throw new Exception($"onReorder={onReorderString} is not a valid boolean"); | |||
} | |||
catalogItem.OnReorder = onReorder; | |||
} | |||
else | |||
{ | |||
throw new Exception($"onReorder={onReorderString} is not a valid boolean"); | |||
} | |||
} | |||
return catalogItem; | |||
} | |||
private IEnumerable<CatalogItem> GetPreconfiguredItems() | |||
return catalogItem; | |||
} | |||
private IEnumerable<CatalogItem> GetPreconfiguredItems() | |||
{ | |||
return new List<CatalogItem>() | |||
{ | |||
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" }, | |||
}; | |||
} | |||
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) | |||
private string[] GetHeaders(string csvfile, string[] requiredHeaders, string[] optionalHeaders = null) | |||
{ | |||
string[] csvheaders = File.ReadLines(csvfile).First().ToLowerInvariant().Split(','); | |||
if (csvheaders.Count() < requiredHeaders.Count()) | |||
{ | |||
string[] csvheaders = File.ReadLines(csvfile).First().ToLowerInvariant().Split(','); | |||
throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is bigger then csv header count '{csvheaders.Count()}' "); | |||
} | |||
if (csvheaders.Count() < requiredHeaders.Count()) | |||
if (optionalHeaders != null) | |||
{ | |||
if (csvheaders.Count() > (requiredHeaders.Count() + optionalHeaders.Count())) | |||
{ | |||
throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is bigger then csv header count '{csvheaders.Count()}' "); | |||
throw new Exception($"csv header count '{csvheaders.Count()}' is larger then required '{requiredHeaders.Count()}' and optional '{optionalHeaders.Count()}' headers count"); | |||
} | |||
} | |||
if (optionalHeaders != null) | |||
foreach (var requiredHeader in requiredHeaders) | |||
{ | |||
if (!csvheaders.Contains(requiredHeader)) | |||
{ | |||
if (csvheaders.Count() > (requiredHeaders.Count() + optionalHeaders.Count())) | |||
{ | |||
throw new Exception($"csv header count '{csvheaders.Count()}' is larger then required '{requiredHeaders.Count()}' and optional '{optionalHeaders.Count()}' headers count"); | |||
} | |||
throw new Exception($"does not contain required header '{requiredHeader}'"); | |||
} | |||
} | |||
return csvheaders; | |||
} | |||
foreach (var requiredHeader in requiredHeaders) | |||
private void GetCatalogItemPictures(string contentRootPath, string picturePath) | |||
{ | |||
if (picturePath != null) | |||
{ | |||
DirectoryInfo directory = new DirectoryInfo(picturePath); | |||
foreach (FileInfo file in directory.GetFiles()) | |||
{ | |||
if (!csvheaders.Contains(requiredHeader)) | |||
{ | |||
throw new Exception($"does not contain required header '{requiredHeader}'"); | |||
} | |||
file.Delete(); | |||
} | |||
return csvheaders; | |||
string zipFileCatalogItemPictures = Path.Combine(contentRootPath, "Setup", "CatalogItems.zip"); | |||
ZipFile.ExtractToDirectory(zipFileCatalogItemPictures, picturePath); | |||
} | |||
} | |||
private void GetCatalogItemPictures(string contentRootPath, string picturePath) | |||
{ | |||
if (picturePath != null) | |||
{ | |||
DirectoryInfo directory = new DirectoryInfo(picturePath); | |||
foreach (FileInfo file in directory.GetFiles()) | |||
private AsyncRetryPolicy CreatePolicy(ILogger<CatalogContextSeed> logger, string prefix, int retries = 3) | |||
{ | |||
return Policy.Handle<SqlException>(). | |||
WaitAndRetryAsync( | |||
retryCount: retries, | |||
sleepDurationProvider: retry => TimeSpan.FromSeconds(5), | |||
onRetry: (exception, timeSpan, retry, ctx) => | |||
{ | |||
file.Delete(); | |||
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", prefix, exception.GetType().Name, exception.Message, retry, retries); | |||
} | |||
string zipFileCatalogItemPictures = Path.Combine(contentRootPath, "Setup", "CatalogItems.zip"); | |||
ZipFile.ExtractToDirectory(zipFileCatalogItemPictures, picturePath); | |||
} | |||
} | |||
private AsyncRetryPolicy CreatePolicy(ILogger<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) | |||
{ | |||
this.env = env; | |||
this.logger = logger; | |||
} | |||
public void OnException(ExceptionContext context) | |||
{ | |||
logger.LogError(new EventId(context.Exception.HResult), | |||
context.Exception, | |||
context.Exception.Message); | |||
public void OnException(ExceptionContext context) | |||
if (context.Exception.GetType() == typeof(CatalogDomainException)) | |||
{ | |||
logger.LogError(new EventId(context.Exception.HResult), | |||
context.Exception, | |||
context.Exception.Message); | |||
if (context.Exception.GetType() == typeof(CatalogDomainException)) | |||
var problemDetails = new ValidationProblemDetails() | |||
{ | |||
var problemDetails = new ValidationProblemDetails() | |||
{ | |||
Instance = context.HttpContext.Request.Path, | |||
Status = StatusCodes.Status400BadRequest, | |||
Detail = "Please refer to the errors property for additional details." | |||
}; | |||
Instance = context.HttpContext.Request.Path, | |||
Status = StatusCodes.Status400BadRequest, | |||
Detail = "Please refer to the errors property for additional details." | |||
}; | |||
problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); | |||
problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); | |||
context.Result = new BadRequestObjectResult(problemDetails); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; | |||
} | |||
else | |||
context.Result = new BadRequestObjectResult(problemDetails); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; | |||
} | |||
else | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { "An error ocurred." } | |||
}; | |||
if (env.IsDevelopment()) | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { "An error ocurred." } | |||
}; | |||
if (env.IsDevelopment()) | |||
{ | |||
json.DeveloperMessage = context.Exception; | |||
} | |||
context.Result = new InternalServerErrorObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||
json.DeveloperMessage = context.Exception; | |||
} | |||
context.ExceptionHandled = true; | |||
context.Result = new InternalServerErrorObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||
} | |||
context.ExceptionHandled = true; | |||
} | |||
private class JsonErrorResponse | |||
{ | |||
public string[] Messages { get; set; } | |||
private class JsonErrorResponse | |||
{ | |||
public string[] Messages { get; set; } | |||
public object DeveloperMessage { get; set; } | |||
} | |||
public object DeveloperMessage { get; set; } | |||
} | |||
} |
@ -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); | |||
public async Task PublishThroughEventBusAsync(IntegrationEvent evt) | |||
await _eventLogService.MarkEventAsInProgressAsync(evt.Id); | |||
_eventBus.Publish(evt); | |||
await _eventLogService.MarkEventAsPublishedAsync(evt.Id); | |||
} | |||
catch (Exception ex) | |||
{ | |||
try | |||
{ | |||
_logger.LogInformation("----- Publishing integration event: {IntegrationEventId_published} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); | |||
await _eventLogService.MarkEventAsInProgressAsync(evt.Id); | |||
_eventBus.Publish(evt); | |||
await _eventLogService.MarkEventAsPublishedAsync(evt.Id); | |||
} | |||
catch (Exception ex) | |||
{ | |||
_logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); | |||
await _eventLogService.MarkEventAsFailedAsync(evt.Id); | |||
} | |||
_logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); | |||
await _eventLogService.MarkEventAsFailedAsync(evt.Id); | |||
} | |||
} | |||
public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) | |||
{ | |||
_logger.LogInformation("----- CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}", evt.Id); | |||
public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) | |||
{ | |||
_logger.LogInformation("----- CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}", evt.Id); | |||
//Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): | |||
//See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | |||
await ResilientTransaction.New(_catalogContext).ExecuteAsync(async () => | |||
{ | |||
// Achieving atomicity between original catalog database operation and the IntegrationEventLog thanks to a local transaction | |||
await _catalogContext.SaveChangesAsync(); | |||
await _eventLogService.SaveEventAsync(evt, _catalogContext.Database.CurrentTransaction); | |||
}); | |||
} | |||
//Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): | |||
//See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | |||
await ResilientTransaction.New(_catalogContext).ExecuteAsync(async () => | |||
{ | |||
// Achieving atomicity between original catalog database operation and the IntegrationEventLog thanks to a local transaction | |||
await _catalogContext.SaveChangesAsync(); | |||
await _eventLogService.SaveEventAsync(evt, _catalogContext.Database.CurrentTransaction); | |||
}); | |||
} | |||
protected virtual void Dispose(bool disposing) | |||
protected virtual void Dispose(bool disposing) | |||
{ | |||
if (!disposedValue) | |||
{ | |||
if (!disposedValue) | |||
if (disposing) | |||
{ | |||
if (disposing) | |||
{ | |||
(_eventLogService as IDisposable)?.Dispose(); | |||
} | |||
disposedValue = true; | |||
(_eventLogService as IDisposable)?.Dispose(); | |||
} | |||
} | |||
public void Dispose() | |||
{ | |||
Dispose(disposing: true); | |||
GC.SuppressFinalize(this); | |||
disposedValue = true; | |||
} | |||
} | |||
public void Dispose() | |||
{ | |||
Dispose(disposing: true); | |||
GC.SuppressFinalize(this); | |||
} | |||
} |
@ -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; | |||
public OrderStatusChangedToAwaitingValidationIntegrationEventHandler( | |||
CatalogContext catalogContext, | |||
ICatalogIntegrationEventService catalogIntegrationEventService, | |||
ILogger<OrderStatusChangedToAwaitingValidationIntegrationEventHandler> logger) | |||
{ | |||
_catalogContext = catalogContext; | |||
_catalogIntegrationEventService = catalogIntegrationEventService; | |||
_logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); | |||
} | |||
_catalogContext = catalogContext; | |||
_catalogIntegrationEventService = catalogIntegrationEventService; | |||
_logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); | |||
} | |||
public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) | |||
public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) | |||
{ | |||
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) | |||
{ | |||
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) | |||
{ | |||
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); | |||
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); | |||
var confirmedOrderStockItems = new List<ConfirmedOrderStockItem>(); | |||
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); | |||
foreach (var orderStockItem in @event.OrderStockItems) | |||
{ | |||
var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); | |||
var hasStock = catalogItem.AvailableStock >= orderStockItem.Units; | |||
var confirmedOrderStockItem = new ConfirmedOrderStockItem(catalogItem.Id, hasStock); | |||
confirmedOrderStockItems.Add(confirmedOrderStockItem); | |||
} | |||
confirmedOrderStockItems.Add(confirmedOrderStockItem); | |||
} | |||
var confirmedIntegrationEvent = confirmedOrderStockItems.Any(c => !c.HasStock) | |||
? (IntegrationEvent)new OrderStockRejectedIntegrationEvent(@event.OrderId, confirmedOrderStockItems) | |||
: new OrderStockConfirmedIntegrationEvent(@event.OrderId); | |||
var confirmedIntegrationEvent = confirmedOrderStockItems.Any(c => !c.HasStock) | |||
? (IntegrationEvent)new OrderStockRejectedIntegrationEvent(@event.OrderId, confirmedOrderStockItems) | |||
: new OrderStockConfirmedIntegrationEvent(@event.OrderId); | |||
await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(confirmedIntegrationEvent); | |||
await _catalogIntegrationEventService.PublishThroughEventBusAsync(confirmedIntegrationEvent); | |||
await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(confirmedIntegrationEvent); | |||
await _catalogIntegrationEventService.PublishThroughEventBusAsync(confirmedIntegrationEvent); | |||
} | |||
} | |||
} | |||
} | |||
} |
@ -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); | |||
var catalogItem = _catalogContext.CatalogItems.Find(orderStockItem.ProductId); | |||
catalogItem.RemoveStock(orderStockItem.Units); | |||
} | |||
catalogItem.RemoveStock(orderStockItem.Units); | |||
} | |||
await _catalogContext.SaveChangesAsync(); | |||
await _catalogContext.SaveChangesAsync(); | |||
} | |||
} | |||
} | |||
} | |||
} |
@ -1,27 +1,26 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | |||
{ | |||
public record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public IEnumerable<OrderStockItem> OrderStockItems { get; } | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; | |||
public OrderStatusChangedToAwaitingValidationIntegrationEvent(int orderId, | |||
IEnumerable<OrderStockItem> orderStockItems) | |||
{ | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
public record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public IEnumerable<OrderStockItem> OrderStockItems { get; } | |||
public record OrderStockItem | |||
public OrderStatusChangedToAwaitingValidationIntegrationEvent(int orderId, | |||
IEnumerable<OrderStockItem> orderStockItems) | |||
{ | |||
public int ProductId { get; } | |||
public int Units { get; } | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
public OrderStockItem(int productId, int units) | |||
{ | |||
ProductId = productId; | |||
Units = units; | |||
} | |||
public record OrderStockItem | |||
{ | |||
public int ProductId { get; } | |||
public int Units { get; } | |||
public OrderStockItem(int productId, int units) | |||
{ | |||
ProductId = productId; | |||
Units = units; | |||
} | |||
} | |||
} |
@ -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 | |||
{ | |||
public int OrderId { get; } | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; | |||
public List<ConfirmedOrderStockItem> OrderStockItems { get; } | |||
public record OrderStockRejectedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int OrderId { get; } | |||
public OrderStockRejectedIntegrationEvent(int orderId, | |||
List<ConfirmedOrderStockItem> orderStockItems) | |||
{ | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
public List<ConfirmedOrderStockItem> OrderStockItems { get; } | |||
public record ConfirmedOrderStockItem | |||
public OrderStockRejectedIntegrationEvent(int orderId, | |||
List<ConfirmedOrderStockItem> orderStockItems) | |||
{ | |||
public int ProductId { get; } | |||
public bool HasStock { get; } | |||
OrderId = orderId; | |||
OrderStockItems = orderStockItems; | |||
} | |||
} | |||
public ConfirmedOrderStockItem(int productId, bool hasStock) | |||
{ | |||
ProductId = productId; | |||
HasStock = hasStock; | |||
} | |||
public record ConfirmedOrderStockItem | |||
{ | |||
public int ProductId { get; } | |||
public bool HasStock { get; } | |||
public ConfirmedOrderStockItem(int productId, bool hasStock) | |||
{ | |||
ProductId = productId; | |||
HasStock = hasStock; | |||
} | |||
} | |||
} |
@ -1,21 +1,20 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | |||
{ | |||
// Integration Events notes: | |||
// An Event is “something that has happened in the past”, therefore its name has to be past tense | |||
// An Integration Event is an event that can cause side effects to other microservices, Bounded-Contexts or external systems. | |||
public record ProductPriceChangedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int ProductId { get; private init; } | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; | |||
// Integration Events notes: | |||
// An Event is “something that has happened in the past”, therefore its name has to be past tense | |||
// An Integration Event is an event that can cause side effects to other microservices, Bounded-Contexts or external systems. | |||
public record ProductPriceChangedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int ProductId { get; private init; } | |||
public decimal NewPrice { get; private init; } | |||
public decimal NewPrice { get; private init; } | |||
public decimal OldPrice { get; private init; } | |||
public decimal OldPrice { get; private init; } | |||
public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice) | |||
{ | |||
ProductId = productId; | |||
NewPrice = newPrice; | |||
OldPrice = oldPrice; | |||
} | |||
public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice) | |||
{ | |||
ProductId = productId; | |||
NewPrice = newPrice; | |||
OldPrice = oldPrice; | |||
} | |||
} |
@ -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 | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; | |||
public class CatalogBrand | |||
{ | |||
public class CatalogBrand | |||
{ | |||
public int Id { get; set; } | |||
public int Id { get; set; } | |||
public string Brand { get; set; } | |||
} | |||
public string Brand { get; set; } | |||
} |
@ -1,100 +1,99 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; | |||
public class CatalogItem | |||
{ | |||
public class CatalogItem | |||
{ | |||
public int Id { get; set; } | |||
public int Id { get; set; } | |||
public string Name { get; set; } | |||
public string Name { get; set; } | |||
public string Description { get; set; } | |||
public string Description { get; set; } | |||
public decimal Price { get; set; } | |||
public decimal Price { get; set; } | |||
public string PictureFileName { get; set; } | |||
public string PictureFileName { get; set; } | |||
public string PictureUri { get; set; } | |||
public string PictureUri { get; set; } | |||
public int CatalogTypeId { get; set; } | |||
public int CatalogTypeId { get; set; } | |||
public CatalogType CatalogType { get; set; } | |||
public CatalogType CatalogType { get; set; } | |||
public int CatalogBrandId { get; set; } | |||
public int CatalogBrandId { get; set; } | |||
public CatalogBrand CatalogBrand { get; set; } | |||
public CatalogBrand CatalogBrand { get; set; } | |||
// Quantity in stock | |||
public int AvailableStock { get; set; } | |||
// Quantity in stock | |||
public int AvailableStock { get; set; } | |||
// Available stock at which we should reorder | |||
public int RestockThreshold { get; set; } | |||
// Available stock at which we should reorder | |||
public int RestockThreshold { get; set; } | |||
// Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses) | |||
public int MaxStockThreshold { get; set; } | |||
// Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses) | |||
public int MaxStockThreshold { get; set; } | |||
/// <summary> | |||
/// True if item is on reorder | |||
/// </summary> | |||
public bool OnReorder { get; set; } | |||
/// <summary> | |||
/// True if item is on reorder | |||
/// </summary> | |||
public bool OnReorder { get; set; } | |||
public CatalogItem() { } | |||
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) | |||
/// <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"); | |||
} | |||
throw new CatalogDomainException($"Empty stock, product item {Name} is sold out"); | |||
} | |||
if (quantityDesired <= 0) | |||
{ | |||
throw new CatalogDomainException($"Item units desired should be greater than zero"); | |||
} | |||
if (quantityDesired <= 0) | |||
{ | |||
throw new CatalogDomainException($"Item units desired should be greater than zero"); | |||
} | |||
int removed = Math.Min(quantityDesired, this.AvailableStock); | |||
int removed = Math.Min(quantityDesired, this.AvailableStock); | |||
this.AvailableStock -= removed; | |||
this.AvailableStock -= removed; | |||
return removed; | |||
} | |||
return removed; | |||
} | |||
/// <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; | |||
/// <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) | |||
// The quantity that the client is trying to add to stock is greater than what can be physically accommodated in the Warehouse | |||
if ((this.AvailableStock + quantity) > this.MaxStockThreshold) | |||
{ | |||
int original = this.AvailableStock; | |||
// The quantity that the client is trying to add to stock is greater than what can be physically accommodated in the Warehouse | |||
if ((this.AvailableStock + quantity) > this.MaxStockThreshold) | |||
{ | |||
// For now, this method only adds new units up maximum stock threshold. In an expanded version of this application, we | |||
//could include tracking for the remaining units and store information about overstock elsewhere. | |||
this.AvailableStock += (this.MaxStockThreshold - this.AvailableStock); | |||
} | |||
else | |||
{ | |||
this.AvailableStock += quantity; | |||
} | |||
this.OnReorder = false; | |||
return this.AvailableStock - original; | |||
// For now, this method only adds new units up maximum stock threshold. In an expanded version of this application, we | |||
//could include tracking for the remaining units and store information about overstock elsewhere. | |||
this.AvailableStock += (this.MaxStockThreshold - this.AvailableStock); | |||
} | |||
else | |||
{ | |||
this.AvailableStock += quantity; | |||
} | |||
this.OnReorder = false; | |||
return this.AvailableStock - original; | |||
} | |||
} | |||
} |
@ -1,9 +1,8 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model; | |||
public class CatalogType | |||
{ | |||
public class CatalogType | |||
{ | |||
public int Id { get; set; } | |||
public int Id { get; set; } | |||
public string Type { get; set; } | |||
} | |||
public string Type { get; set; } | |||
} |
@ -1,334 +1,333 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API; | |||
public class Startup | |||
{ | |||
public class Startup | |||
public Startup(IConfiguration configuration) | |||
{ | |||
public Startup(IConfiguration configuration) | |||
{ | |||
Configuration = configuration; | |||
} | |||
Configuration = configuration; | |||
} | |||
public IConfiguration Configuration { get; } | |||
public IConfiguration Configuration { get; } | |||
public IServiceProvider ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddAppInsight(Configuration) | |||
.AddGrpc().Services | |||
.AddCustomMVC(Configuration) | |||
.AddCustomDbContext(Configuration) | |||
.AddCustomOptions(Configuration) | |||
.AddIntegrationServices(Configuration) | |||
.AddEventBus(Configuration) | |||
.AddSwagger(Configuration) | |||
.AddCustomHealthCheck(Configuration); | |||
var container = new ContainerBuilder(); | |||
container.Populate(services); | |||
return new AutofacServiceProvider(container.Build()); | |||
} | |||
public IServiceProvider ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddAppInsight(Configuration) | |||
.AddGrpc().Services | |||
.AddCustomMVC(Configuration) | |||
.AddCustomDbContext(Configuration) | |||
.AddCustomOptions(Configuration) | |||
.AddIntegrationServices(Configuration) | |||
.AddEventBus(Configuration) | |||
.AddSwagger(Configuration) | |||
.AddCustomHealthCheck(Configuration); | |||
var container = new ContainerBuilder(); | |||
container.Populate(services); | |||
return new AutofacServiceProvider(container.Build()); | |||
} | |||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||
{ | |||
//Configure logs | |||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||
{ | |||
//Configure logs | |||
//loggerFactory.AddAzureWebAppDiagnostics(); | |||
//loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); | |||
//loggerFactory.AddAzureWebAppDiagnostics(); | |||
//loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); | |||
var pathBase = Configuration["PATH_BASE"]; | |||
var pathBase = Configuration["PATH_BASE"]; | |||
if (!string.IsNullOrEmpty(pathBase)) | |||
if (!string.IsNullOrEmpty(pathBase)) | |||
{ | |||
loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | |||
app.UsePathBase(pathBase); | |||
} | |||
app.UseSwagger() | |||
.UseSwaggerUI(c => | |||
{ | |||
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 => | |||
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Catalog.API V1"); | |||
}); | |||
app.UseRouting(); | |||
app.UseCors("CorsPolicy"); | |||
app.UseEndpoints(endpoints => | |||
{ | |||
endpoints.MapDefaultControllerRoute(); | |||
endpoints.MapControllers(); | |||
endpoints.MapGet("/_proto/", async ctx => | |||
{ | |||
endpoints.MapDefaultControllerRoute(); | |||
endpoints.MapControllers(); | |||
endpoints.MapGet("/_proto/", async ctx => | |||
ctx.Response.ContentType = "text/plain"; | |||
using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "catalog.proto"), FileMode.Open, FileAccess.Read); | |||
using var sr = new StreamReader(fs); | |||
while (!sr.EndOfStream) | |||
{ | |||
ctx.Response.ContentType = "text/plain"; | |||
using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "catalog.proto"), FileMode.Open, FileAccess.Read); | |||
using var sr = new StreamReader(fs); | |||
while (!sr.EndOfStream) | |||
var line = await sr.ReadLineAsync(); | |||
if (line != "/* >>" || line != "<< */") | |||
{ | |||
var line = await sr.ReadLineAsync(); | |||
if (line != "/* >>" || line != "<< */") | |||
{ | |||
await ctx.Response.WriteAsync(line); | |||
} | |||
await ctx.Response.WriteAsync(line); | |||
} | |||
}); | |||
endpoints.MapGrpcService<CatalogService>(); | |||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | |||
{ | |||
Predicate = _ => true, | |||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||
}); | |||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | |||
{ | |||
Predicate = r => r.Name.Contains("self") | |||
}); | |||
} | |||
}); | |||
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); | |||
} | |||
ConfigureEventBus(app); | |||
} | |||
protected virtual void ConfigureEventBus(IApplicationBuilder app) | |||
{ | |||
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); | |||
eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | |||
eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>(); | |||
} | |||
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 class CustomExtensionMethods | |||
{ | |||
public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddApplicationInsightsTelemetry(configuration); | |||
services.AddApplicationInsightsKubernetesEnricher(); | |||
services.AddApplicationInsightsTelemetry(configuration); | |||
services.AddApplicationInsightsKubernetesEnricher(); | |||
return services; | |||
} | |||
return services; | |||
} | |||
public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) | |||
public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddControllers(options => | |||
{ | |||
services.AddControllers(options => | |||
{ | |||
options.Filters.Add(typeof(HttpGlobalExceptionFilter)); | |||
}) | |||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||
options.Filters.Add(typeof(HttpGlobalExceptionFilter)); | |||
}) | |||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||
services.AddCors(options => | |||
{ | |||
options.AddPolicy("CorsPolicy", | |||
builder => builder | |||
.SetIsOriginAllowed((host) => true) | |||
.AllowAnyMethod() | |||
.AllowAnyHeader() | |||
.AllowCredentials()); | |||
}); | |||
services.AddCors(options => | |||
{ | |||
options.AddPolicy("CorsPolicy", | |||
builder => builder | |||
.SetIsOriginAllowed((host) => true) | |||
.AllowAnyMethod() | |||
.AllowAnyHeader() | |||
.AllowCredentials()); | |||
}); | |||
return services; | |||
} | |||
return services; | |||
} | |||
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
var accountName = configuration.GetValue<string>("AzureStorageAccountName"); | |||
var accountKey = configuration.GetValue<string>("AzureStorageAccountKey"); | |||
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(); | |||
var hcBuilder = services.AddHealthChecks(); | |||
hcBuilder | |||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||
.AddSqlServer( | |||
configuration["ConnectionString"], | |||
name: "CatalogDB-check", | |||
tags: new string[] { "catalogdb" }); | |||
if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) | |||
{ | |||
hcBuilder | |||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||
.AddSqlServer( | |||
configuration["ConnectionString"], | |||
name: "CatalogDB-check", | |||
tags: new string[] { "catalogdb" }); | |||
if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) | |||
{ | |||
hcBuilder | |||
.AddAzureBlobStorage( | |||
$"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", | |||
name: "catalog-storage-check", | |||
tags: new string[] { "catalogstorage" }); | |||
} | |||
if (configuration.GetValue<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; | |||
.AddAzureBlobStorage( | |||
$"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", | |||
name: "catalog-storage-check", | |||
tags: new string[] { "catalogstorage" }); | |||
} | |||
public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) | |||
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||
{ | |||
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); | |||
}); | |||
}); | |||
hcBuilder | |||
.AddAzureServiceBusTopic( | |||
configuration["EventBusConnection"], | |||
topicName: "eshop_event_bus", | |||
name: "catalog-servicebus-check", | |||
tags: new string[] { "servicebus" }); | |||
} | |||
else | |||
{ | |||
hcBuilder | |||
.AddRabbitMQ( | |||
$"amqp://{configuration["EventBusConnection"]}", | |||
name: "catalog-rabbitmqbus-check", | |||
tags: new string[] { "rabbitmqbus" }); | |||
} | |||
services.AddDbContext<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; | |||
} | |||
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) | |||
public static IServiceCollection AddCustomOptions(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.Configure<CatalogSettings>(configuration); | |||
services.Configure<ApiBehaviorOptions>(options => | |||
{ | |||
services.Configure<CatalogSettings>(configuration); | |||
services.Configure<ApiBehaviorOptions>(options => | |||
options.InvalidModelStateResponseFactory = context => | |||
{ | |||
options.InvalidModelStateResponseFactory = context => | |||
var problemDetails = new ValidationProblemDetails(context.ModelState) | |||
{ | |||
var problemDetails = new ValidationProblemDetails(context.ModelState) | |||
{ | |||
Instance = context.HttpContext.Request.Path, | |||
Status = StatusCodes.Status400BadRequest, | |||
Detail = "Please refer to the errors property for additional details." | |||
}; | |||
Instance = context.HttpContext.Request.Path, | |||
Status = StatusCodes.Status400BadRequest, | |||
Detail = "Please refer to the errors property for additional details." | |||
}; | |||
return new BadRequestObjectResult(problemDetails) | |||
{ | |||
ContentTypes = { "application/problem+json", "application/problem+xml" } | |||
}; | |||
return new BadRequestObjectResult(problemDetails) | |||
{ | |||
ContentTypes = { "application/problem+json", "application/problem+xml" } | |||
}; | |||
}); | |||
}; | |||
}); | |||
return services; | |||
} | |||
return services; | |||
} | |||
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddSwaggerGen(options => | |||
{ | |||
options.SwaggerDoc("v1", new OpenApiInfo | |||
{ | |||
Title = "eShopOnContainers - Catalog HTTP API", | |||
Version = "v1", | |||
Description = "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample" | |||
}); | |||
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddSwaggerGen(options => | |||
{ | |||
options.SwaggerDoc("v1", new OpenApiInfo | |||
{ | |||
Title = "eShopOnContainers - Catalog HTTP API", | |||
Version = "v1", | |||
Description = "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample" | |||
}); | |||
}); | |||
return services; | |||
return services; | |||
} | |||
} | |||
public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>( | |||
sp => (DbConnection c) => new IntegrationEventLogService(c)); | |||
public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>( | |||
sp => (DbConnection c) => new IntegrationEventLogService(c)); | |||
services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>(); | |||
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 | |||
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||
{ | |||
services.AddSingleton<IServiceBusPersisterConnection>(sp => | |||
{ | |||
services.AddSingleton<IRabbitMQPersistentConnection>(sp => | |||
{ | |||
var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | |||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>(); | |||
var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | |||
var serviceBusConnection = new ServiceBusConnectionStringBuilder(settings.EventBusConnection); | |||
var subscriptionClientName = configuration["SubscriptionClientName"]; | |||
var factory = new ConnectionFactory() | |||
{ | |||
HostName = configuration["EventBusConnection"], | |||
DispatchConsumersAsync = true | |||
}; | |||
return new DefaultServiceBusPersisterConnection(serviceBusConnection, subscriptionClientName); | |||
}); | |||
} | |||
else | |||
{ | |||
services.AddSingleton<IRabbitMQPersistentConnection>(sp => | |||
{ | |||
var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | |||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>(); | |||
if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) | |||
{ | |||
factory.UserName = configuration["EventBusUserName"]; | |||
} | |||
var factory = new ConnectionFactory() | |||
{ | |||
HostName = configuration["EventBusConnection"], | |||
DispatchConsumersAsync = true | |||
}; | |||
if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) | |||
{ | |||
factory.Password = configuration["EventBusPassword"]; | |||
} | |||
if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) | |||
{ | |||
factory.UserName = configuration["EventBusUserName"]; | |||
} | |||
var retryCount = 5; | |||
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | |||
{ | |||
retryCount = int.Parse(configuration["EventBusRetryCount"]); | |||
} | |||
if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) | |||
{ | |||
factory.Password = configuration["EventBusPassword"]; | |||
} | |||
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); | |||
}); | |||
} | |||
var retryCount = 5; | |||
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | |||
{ | |||
retryCount = int.Parse(configuration["EventBusRetryCount"]); | |||
} | |||
return services; | |||
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); | |||
}); | |||
} | |||
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) | |||
return services; | |||
} | |||
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||
{ | |||
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||
services.AddSingleton<IEventBus, EventBusServiceBus>(sp => | |||
{ | |||
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>(); | |||
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); | |||
}); | |||
return new EventBusServiceBus(serviceBusPersisterConnection, logger, | |||
eventBusSubcriptionsManager, iLifetimeScope); | |||
}); | |||
} | |||
else | |||
} | |||
else | |||
{ | |||
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp => | |||
{ | |||
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"])) | |||
{ | |||
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"]); | |||
} | |||
retryCount = int.Parse(configuration["EventBusRetryCount"]); | |||
} | |||
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); | |||
}); | |||
} | |||
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); | |||
}); | |||
} | |||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>(); | |||
services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | |||
services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); | |||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>(); | |||
services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | |||
services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); | |||
return services; | |||
} | |||
return services; | |||
} | |||
} |
@ -1,21 +1,20 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel | |||
{ | |||
public class PaginatedItemsViewModel<TEntity> where TEntity : class | |||
{ | |||
public int PageIndex { get; private set; } | |||
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 int PageSize { get; private set; } | |||
public long Count { get; private set; } | |||
public long Count { get; private set; } | |||
public IEnumerable<TEntity> Data { 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; | |||
} | |||
public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable<TEntity> data) | |||
{ | |||
PageIndex = pageIndex; | |||
PageSize = pageSize; | |||
Count = count; | |||
Data = data; | |||
} | |||
} |