Browse Source

Refactored namespace statements in catalog.api project

pull/1770/head
Sumit Ghosh 3 years ago
parent
commit
986262a833
32 changed files with 1555 additions and 1592 deletions
  1. +7
    -8
      src/Services/Catalog/Catalog.API/CatalogSettings.cs
  2. +224
    -226
      src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs
  3. +5
    -7
      src/Services/Catalog/Catalog.API/Controllers/HomeController.cs
  4. +68
    -69
      src/Services/Catalog/Catalog.API/Controllers/PicController.cs
  5. +8
    -9
      src/Services/Catalog/Catalog.API/Extensions/CatalogItemExtensions.cs
  6. +52
    -53
      src/Services/Catalog/Catalog.API/Extensions/HostExtensions.cs
  7. +32
    -33
      src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs
  8. +52
    -53
      src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs
  9. +6
    -6
      src/Services/Catalog/Catalog.API/GlobalUsings.cs
  10. +132
    -133
      src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs
  11. +6
    -7
      src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs
  12. +21
    -22
      src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs
  13. +279
    -280
      src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs
  14. +13
    -14
      src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogBrandEntityTypeConfiguration.cs
  15. +23
    -24
      src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogItemEntityTypeConfiguration.cs
  16. +13
    -14
      src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogTypeEntityTypeConfiguration.cs
  17. +14
    -15
      src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs
  18. +43
    -44
      src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs
  19. +57
    -58
      src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs
  20. +34
    -36
      src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs
  21. +25
    -26
      src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs
  22. +21
    -22
      src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs
  23. +10
    -14
      src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs
  24. +7
    -8
      src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs
  25. +21
    -22
      src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs
  26. +15
    -16
      src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs
  27. +5
    -6
      src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs
  28. +5
    -6
      src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs
  29. +72
    -73
      src/Services/Catalog/Catalog.API/Model/CatalogItem.cs
  30. +5
    -6
      src/Services/Catalog/Catalog.API/Model/CatalogType.cs
  31. +266
    -267
      src/Services/Catalog/Catalog.API/Startup.cs
  32. +14
    -15
      src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs

+ 7
- 8
src/Services/Catalog/Catalog.API/CatalogSettings.cs View File

@ -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; }
} }

+ 224
- 226
src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs View File

@ -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;
} }
} }

+ 5
- 7
src/Services/Catalog/Catalog.API/Controllers/HomeController.cs View File

@ -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");
} }
} }

+ 68
- 69
src/Services/Catalog/Catalog.API/Controllers/PicController.cs View File

@ -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;
} }
} }

+ 8
- 9
src/Services/Catalog/Catalog.API/Extensions/CatalogItemExtensions.cs View File

@ -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());
} }
} }
} }

+ 52
- 53
src/Services/Catalog/Catalog.API/Extensions/HostExtensions.cs View File

@ -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);
} }
} }

+ 32
- 33
src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs View File

@ -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; }
} }
} }

+ 52
- 53
src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs View File

@ -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);
} }
} }

+ 6
- 6
src/Services/Catalog/Catalog.API/GlobalUsings.cs View File

@ -2,10 +2,10 @@
global using Azure.Identity; global using Azure.Identity;
global using Autofac.Extensions.DependencyInjection; global using Autofac.Extensions.DependencyInjection;
global using Autofac; global using Autofac;
global using Catalog.API.Extensions;
global using Catalog.API.Infrastructure.ActionResults;
global using Catalog.API.Infrastructure.Exceptions;
global using global::Catalog.API.IntegrationEvents;
global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions;
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults;
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions;
global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents;
global using Grpc.Core; global using Grpc.Core;
global using Microsoft.AspNetCore.Hosting; global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Http;
@ -48,7 +48,7 @@ global using System.Net;
global using System.Text.RegularExpressions; global using System.Text.RegularExpressions;
global using System.Threading.Tasks; global using System.Threading.Tasks;
global using System; global using System;
global using global::Catalog.API.Infrastructure.Filters;
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters;
global using HealthChecks.UI.Client; global using HealthChecks.UI.Client;
global using Microsoft.AspNetCore.Diagnostics.HealthChecks; global using Microsoft.AspNetCore.Diagnostics.HealthChecks;
global using Microsoft.Azure.ServiceBus; global using Microsoft.Azure.ServiceBus;
@ -59,4 +59,4 @@ global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.
global using Microsoft.Extensions.Diagnostics.HealthChecks; global using Microsoft.Extensions.Diagnostics.HealthChecks;
global using Microsoft.OpenApi.Models; global using Microsoft.OpenApi.Models;
global using RabbitMQ.Client; global using RabbitMQ.Client;
global using System.Reflection;
global using System.Reflection;

+ 132
- 133
src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs View File

@ -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;
} }
} }

+ 6
- 7
src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs View File

@ -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;
} }
} }

+ 21
- 22
src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs View File

@ -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);
} }
} }

+ 279
- 280
src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs View File

@ -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);
}
);
}
);
} }
} }

+ 13
- 14
src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogBrandEntityTypeConfiguration.cs View File

@ -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);
} }
} }

+ 23
- 24
src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogItemEntityTypeConfiguration.cs View File

@ -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);
} }
} }

+ 13
- 14
src/Services/Catalog/Catalog.API/Infrastructure/EntityConfigurations/CatalogTypeEntityTypeConfiguration.cs View File

@ -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);
} }
} }

+ 14
- 15
src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs View File

@ -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)
{ }
} }

+ 43
- 44
src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs View File

@ -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; }
} }
} }

+ 57
- 58
src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs View File

@ -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);
}
} }

+ 34
- 36
src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs View File

@ -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);
}
} }
} }
}
}

+ 25
- 26
src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs View File

@ -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();
}
} }
} }
}
}

+ 21
- 22
src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs View File

@ -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;
} }
}
}

+ 10
- 14
src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs View File

@ -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;
} }
}
}

+ 7
- 8
src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs View File

@ -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;
}

+ 21
- 22
src/Services/Catalog/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs View File

@ -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;
} }
}
}

+ 15
- 16
src/Services/Catalog/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs View File

@ -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;
} }
} }

+ 5
- 6
src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs View File

@ -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);
} }

+ 5
- 6
src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs View File

@ -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; }
} }

+ 72
- 73
src/Services/Catalog/Catalog.API/Model/CatalogItem.cs View File

@ -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;
} }
}
}

+ 5
- 6
src/Services/Catalog/Catalog.API/Model/CatalogType.cs View File

@ -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; }
} }

+ 266
- 267
src/Services/Catalog/Catalog.API/Startup.cs View File

@ -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;
} }
} }

+ 14
- 15
src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs View File

@ -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;
} }
} }

Loading…
Cancel
Save