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