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