Fix/issue 826 support cross thread minimal api (#827)

* remove repository global variable to suuport muliple thread (call by handle method)

* add parallel call tests

Co-authored-by: cedri <cedri@BAS>
This commit is contained in:
Cédric Michel
2022-12-20 04:24:46 +01:00
committed by GitHub
parent a72dd775ee
commit 707f8696f9
8 changed files with 64 additions and 54 deletions

View File

@@ -13,9 +13,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints;
/// <summary> /// <summary>
/// List Catalog Brands /// List Catalog Brands
/// </summary> /// </summary>
public class CatalogBrandListEndpoint : IEndpoint<IResult> public class CatalogBrandListEndpoint : IEndpoint<IResult, IRepository<CatalogBrand>>
{ {
private IRepository<CatalogBrand> _catalogBrandRepository;
private readonly IMapper _mapper; private readonly IMapper _mapper;
public CatalogBrandListEndpoint(IMapper mapper) public CatalogBrandListEndpoint(IMapper mapper)
@@ -28,18 +27,17 @@ public class CatalogBrandListEndpoint : IEndpoint<IResult>
app.MapGet("api/catalog-brands", app.MapGet("api/catalog-brands",
async (IRepository<CatalogBrand> catalogBrandRepository) => async (IRepository<CatalogBrand> catalogBrandRepository) =>
{ {
_catalogBrandRepository = catalogBrandRepository; return await HandleAsync(catalogBrandRepository);
return await HandleAsync();
}) })
.Produces<ListCatalogBrandsResponse>() .Produces<ListCatalogBrandsResponse>()
.WithTags("CatalogBrandEndpoints"); .WithTags("CatalogBrandEndpoints");
} }
public async Task<IResult> HandleAsync() public async Task<IResult> HandleAsync(IRepository<CatalogBrand> catalogBrandRepository)
{ {
var response = new ListCatalogBrandsResponse(); var response = new ListCatalogBrandsResponse();
var items = await _catalogBrandRepository.ListAsync(); var items = await catalogBrandRepository.ListAsync();
response.CatalogBrands.AddRange(items.Select(_mapper.Map<CatalogBrandDto>)); response.CatalogBrands.AddRange(items.Select(_mapper.Map<CatalogBrandDto>));

View File

@@ -11,9 +11,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
/// <summary> /// <summary>
/// Get a Catalog Item by Id /// Get a Catalog Item by Id
/// </summary> /// </summary>
public class CatalogItemGetByIdEndpoint : IEndpoint<IResult, GetByIdCatalogItemRequest> public class CatalogItemGetByIdEndpoint : IEndpoint<IResult, GetByIdCatalogItemRequest, IRepository<CatalogItem>>
{ {
private IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer; private readonly IUriComposer _uriComposer;
public CatalogItemGetByIdEndpoint(IUriComposer uriComposer) public CatalogItemGetByIdEndpoint(IUriComposer uriComposer)
@@ -26,18 +25,17 @@ public class CatalogItemGetByIdEndpoint : IEndpoint<IResult, GetByIdCatalogItemR
app.MapGet("api/catalog-items/{catalogItemId}", app.MapGet("api/catalog-items/{catalogItemId}",
async (int catalogItemId, IRepository<CatalogItem> itemRepository) => async (int catalogItemId, IRepository<CatalogItem> itemRepository) =>
{ {
_itemRepository = itemRepository; return await HandleAsync(new GetByIdCatalogItemRequest(catalogItemId), itemRepository);
return await HandleAsync(new GetByIdCatalogItemRequest(catalogItemId));
}) })
.Produces<GetByIdCatalogItemResponse>() .Produces<GetByIdCatalogItemResponse>()
.WithTags("CatalogItemEndpoints"); .WithTags("CatalogItemEndpoints");
} }
public async Task<IResult> HandleAsync(GetByIdCatalogItemRequest request) public async Task<IResult> HandleAsync(GetByIdCatalogItemRequest request, IRepository<CatalogItem> itemRepository)
{ {
var response = new GetByIdCatalogItemResponse(request.CorrelationId()); var response = new GetByIdCatalogItemResponse(request.CorrelationId());
var item = await _itemRepository.GetByIdAsync(request.CatalogItemId); var item = await itemRepository.GetByIdAsync(request.CatalogItemId);
if (item is null) if (item is null)
return Results.NotFound(); return Results.NotFound();

View File

@@ -15,9 +15,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
/// <summary> /// <summary>
/// List Catalog Items (paged) /// List Catalog Items (paged)
/// </summary> /// </summary>
public class CatalogItemListPagedEndpoint : IEndpoint<IResult, ListPagedCatalogItemRequest> public class CatalogItemListPagedEndpoint : IEndpoint<IResult, ListPagedCatalogItemRequest, IRepository<CatalogItem>>
{ {
private IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer; private readonly IUriComposer _uriComposer;
private readonly IMapper _mapper; private readonly IMapper _mapper;
@@ -32,19 +31,19 @@ public class CatalogItemListPagedEndpoint : IEndpoint<IResult, ListPagedCatalogI
app.MapGet("api/catalog-items", app.MapGet("api/catalog-items",
async (int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId, IRepository<CatalogItem> itemRepository) => async (int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId, IRepository<CatalogItem> itemRepository) =>
{ {
_itemRepository = itemRepository; return await HandleAsync(new ListPagedCatalogItemRequest(pageSize, pageIndex, catalogBrandId, catalogTypeId), itemRepository);
return await HandleAsync(new ListPagedCatalogItemRequest(pageSize, pageIndex, catalogBrandId, catalogTypeId)); })
})
.Produces<ListPagedCatalogItemResponse>() .Produces<ListPagedCatalogItemResponse>()
.WithTags("CatalogItemEndpoints"); .WithTags("CatalogItemEndpoints");
} }
public async Task<IResult> HandleAsync(ListPagedCatalogItemRequest request) public async Task<IResult> HandleAsync(ListPagedCatalogItemRequest request, IRepository<CatalogItem> itemRepository)
{ {
await Task.Delay(1000);
var response = new ListPagedCatalogItemResponse(request.CorrelationId()); var response = new ListPagedCatalogItemResponse(request.CorrelationId());
var filterSpec = new CatalogFilterSpecification(request.CatalogBrandId, request.CatalogTypeId); var filterSpec = new CatalogFilterSpecification(request.CatalogBrandId, request.CatalogTypeId);
int totalItems = await _itemRepository.CountAsync(filterSpec); int totalItems = await itemRepository.CountAsync(filterSpec);
var pagedSpec = new CatalogFilterPaginatedSpecification( var pagedSpec = new CatalogFilterPaginatedSpecification(
skip: request.PageIndex.Value * request.PageSize.Value, skip: request.PageIndex.Value * request.PageSize.Value,
@@ -52,7 +51,7 @@ public class CatalogItemListPagedEndpoint : IEndpoint<IResult, ListPagedCatalogI
brandId: request.CatalogBrandId, brandId: request.CatalogBrandId,
typeId: request.CatalogTypeId); typeId: request.CatalogTypeId);
var items = await _itemRepository.ListAsync(pagedSpec); var items = await itemRepository.ListAsync(pagedSpec);
response.CatalogItems.AddRange(items.Select(_mapper.Map<CatalogItemDto>)); response.CatalogItems.AddRange(items.Select(_mapper.Map<CatalogItemDto>));
foreach (CatalogItemDto item in response.CatalogItems) foreach (CatalogItemDto item in response.CatalogItems)

View File

@@ -15,9 +15,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
/// <summary> /// <summary>
/// Creates a new Catalog Item /// Creates a new Catalog Item
/// </summary> /// </summary>
public class CreateCatalogItemEndpoint : IEndpoint<IResult, CreateCatalogItemRequest> public class CreateCatalogItemEndpoint : IEndpoint<IResult, CreateCatalogItemRequest, IRepository<CatalogItem>>
{ {
private IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer; private readonly IUriComposer _uriComposer;
public CreateCatalogItemEndpoint(IUriComposer uriComposer) public CreateCatalogItemEndpoint(IUriComposer uriComposer)
@@ -31,26 +30,25 @@ public class CreateCatalogItemEndpoint : IEndpoint<IResult, CreateCatalogItemReq
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async
(CreateCatalogItemRequest request, IRepository<CatalogItem> itemRepository) => (CreateCatalogItemRequest request, IRepository<CatalogItem> itemRepository) =>
{ {
_itemRepository = itemRepository; return await HandleAsync(request, itemRepository);
return await HandleAsync(request);
}) })
.Produces<CreateCatalogItemResponse>() .Produces<CreateCatalogItemResponse>()
.WithTags("CatalogItemEndpoints"); .WithTags("CatalogItemEndpoints");
} }
public async Task<IResult> HandleAsync(CreateCatalogItemRequest request) public async Task<IResult> HandleAsync(CreateCatalogItemRequest request, IRepository<CatalogItem> itemRepository)
{ {
var response = new CreateCatalogItemResponse(request.CorrelationId()); var response = new CreateCatalogItemResponse(request.CorrelationId());
var catalogItemNameSpecification = new CatalogItemNameSpecification(request.Name); var catalogItemNameSpecification = new CatalogItemNameSpecification(request.Name);
var existingCataloogItem = await _itemRepository.CountAsync(catalogItemNameSpecification); var existingCataloogItem = await itemRepository.CountAsync(catalogItemNameSpecification);
if (existingCataloogItem > 0) if (existingCataloogItem > 0)
{ {
throw new DuplicateException($"A catalogItem with name {request.Name} already exists"); throw new DuplicateException($"A catalogItem with name {request.Name} already exists");
} }
var newItem = new CatalogItem(request.CatalogTypeId, request.CatalogBrandId, request.Description, request.Name, request.Price, request.PictureUri); var newItem = new CatalogItem(request.CatalogTypeId, request.CatalogBrandId, request.Description, request.Name, request.Price, request.PictureUri);
newItem = await _itemRepository.AddAsync(newItem); newItem = await itemRepository.AddAsync(newItem);
if (newItem.Id != 0) if (newItem.Id != 0)
{ {
@@ -59,7 +57,7 @@ public class CreateCatalogItemEndpoint : IEndpoint<IResult, CreateCatalogItemReq
// In production, we recommend uploading to a blob storage and deliver the image via CDN after a verification process. // In production, we recommend uploading to a blob storage and deliver the image via CDN after a verification process.
newItem.UpdatePictureUri("eCatalog-item-default.png"); newItem.UpdatePictureUri("eCatalog-item-default.png");
await _itemRepository.UpdateAsync(newItem); await itemRepository.UpdateAsync(newItem);
} }
var dto = new CatalogItemDto var dto = new CatalogItemDto
@@ -73,6 +71,6 @@ public class CreateCatalogItemEndpoint : IEndpoint<IResult, CreateCatalogItemReq
Price = newItem.Price Price = newItem.Price
}; };
response.CatalogItem = dto; response.CatalogItem = dto;
return Results.Created($"api/catalog-items/{dto.Id}", response); return Results.Created($"api/catalog-items/{dto.Id}", response);
} }
} }

View File

@@ -13,32 +13,29 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
/// <summary> /// <summary>
/// Deletes a Catalog Item /// Deletes a Catalog Item
/// </summary> /// </summary>
public class DeleteCatalogItemEndpoint : IEndpoint<IResult, DeleteCatalogItemRequest> public class DeleteCatalogItemEndpoint : IEndpoint<IResult, DeleteCatalogItemRequest, IRepository<CatalogItem>>
{ {
private IRepository<CatalogItem> _itemRepository;
public void AddRoute(IEndpointRouteBuilder app) public void AddRoute(IEndpointRouteBuilder app)
{ {
app.MapDelete("api/catalog-items/{catalogItemId}", app.MapDelete("api/catalog-items/{catalogItemId}",
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async
(int catalogItemId, IRepository<CatalogItem> itemRepository) => (int catalogItemId, IRepository<CatalogItem> itemRepository) =>
{ {
_itemRepository = itemRepository; return await HandleAsync(new DeleteCatalogItemRequest(catalogItemId), itemRepository);
return await HandleAsync(new DeleteCatalogItemRequest(catalogItemId));
}) })
.Produces<DeleteCatalogItemResponse>() .Produces<DeleteCatalogItemResponse>()
.WithTags("CatalogItemEndpoints"); .WithTags("CatalogItemEndpoints");
} }
public async Task<IResult> HandleAsync(DeleteCatalogItemRequest request) public async Task<IResult> HandleAsync(DeleteCatalogItemRequest request, IRepository<CatalogItem> itemRepository)
{ {
var response = new DeleteCatalogItemResponse(request.CorrelationId()); var response = new DeleteCatalogItemResponse(request.CorrelationId());
var itemToDelete = await _itemRepository.GetByIdAsync(request.CatalogItemId); var itemToDelete = await itemRepository.GetByIdAsync(request.CatalogItemId);
if (itemToDelete is null) if (itemToDelete is null)
return Results.NotFound(); return Results.NotFound();
await _itemRepository.DeleteAsync(itemToDelete); await itemRepository.DeleteAsync(itemToDelete);
return Results.Ok(response); return Results.Ok(response);
} }

View File

@@ -13,9 +13,8 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
/// <summary> /// <summary>
/// Updates a Catalog Item /// Updates a Catalog Item
/// </summary> /// </summary>
public class UpdateCatalogItemEndpoint : IEndpoint<IResult, UpdateCatalogItemRequest> public class UpdateCatalogItemEndpoint : IEndpoint<IResult, UpdateCatalogItemRequest, IRepository<CatalogItem>>
{ {
private IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer; private readonly IUriComposer _uriComposer;
public UpdateCatalogItemEndpoint(IUriComposer uriComposer) public UpdateCatalogItemEndpoint(IUriComposer uriComposer)
@@ -29,25 +28,24 @@ public class UpdateCatalogItemEndpoint : IEndpoint<IResult, UpdateCatalogItemReq
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async
(UpdateCatalogItemRequest request, IRepository<CatalogItem> itemRepository) => (UpdateCatalogItemRequest request, IRepository<CatalogItem> itemRepository) =>
{ {
_itemRepository = itemRepository; return await HandleAsync(request, itemRepository);
return await HandleAsync(request);
}) })
.Produces<UpdateCatalogItemResponse>() .Produces<UpdateCatalogItemResponse>()
.WithTags("CatalogItemEndpoints"); .WithTags("CatalogItemEndpoints");
} }
public async Task<IResult> HandleAsync(UpdateCatalogItemRequest request) public async Task<IResult> HandleAsync(UpdateCatalogItemRequest request, IRepository<CatalogItem> itemRepository)
{ {
var response = new UpdateCatalogItemResponse(request.CorrelationId()); var response = new UpdateCatalogItemResponse(request.CorrelationId());
var existingItem = await _itemRepository.GetByIdAsync(request.Id); var existingItem = await itemRepository.GetByIdAsync(request.Id);
CatalogItem.CatalogItemDetails details = new(request.Name, request.Description, request.Price); CatalogItem.CatalogItemDetails details = new(request.Name, request.Description, request.Price);
existingItem.UpdateDetails(details); existingItem.UpdateDetails(details);
existingItem.UpdateBrand(request.CatalogBrandId); existingItem.UpdateBrand(request.CatalogBrandId);
existingItem.UpdateType(request.CatalogTypeId); existingItem.UpdateType(request.CatalogTypeId);
await _itemRepository.UpdateAsync(existingItem); await itemRepository.UpdateAsync(existingItem);
var dto = new CatalogItemDto var dto = new CatalogItemDto
{ {

View File

@@ -13,33 +13,31 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints;
/// <summary> /// <summary>
/// List Catalog Types /// List Catalog Types
/// </summary> /// </summary>
public class CatalogTypeListEndpoint : IEndpoint<IResult> public class CatalogTypeListEndpoint : IEndpoint<IResult, IRepository<CatalogType>>
{ {
private IRepository<CatalogType> _catalogTypeRepository;
private readonly IMapper _mapper; private readonly IMapper _mapper;
public CatalogTypeListEndpoint(IMapper mapper) public CatalogTypeListEndpoint(IMapper mapper)
{ {
_mapper = mapper; _mapper = mapper;
} }
public void AddRoute(IEndpointRouteBuilder app) public void AddRoute(IEndpointRouteBuilder app)
{ {
app.MapGet("api/catalog-types", app.MapGet("api/catalog-types",
async (IRepository<CatalogType> catalogTypeRepository) => async (IRepository<CatalogType> catalogTypeRepository) =>
{ {
_catalogTypeRepository = catalogTypeRepository; return await HandleAsync(catalogTypeRepository);
return await HandleAsync();
}) })
.Produces<ListCatalogTypesResponse>() .Produces<ListCatalogTypesResponse>()
.WithTags("CatalogTypeEndpoints"); .WithTags("CatalogTypeEndpoints");
} }
public async Task<IResult> HandleAsync() public async Task<IResult> HandleAsync(IRepository<CatalogType> catalogTypeRepository)
{ {
var response = new ListCatalogTypesResponse(); var response = new ListCatalogTypesResponse();
var items = await _catalogTypeRepository.ListAsync(); var items = await catalogTypeRepository.ListAsync();
response.CatalogTypes.AddRange(items.Select(_mapper.Map<CatalogTypeDto>)); response.CatalogTypes.AddRange(items.Select(_mapper.Map<CatalogTypeDto>));

View File

@@ -2,7 +2,10 @@
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.eShopWeb.Web.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace PublicApiIntegrationTests.CatalogItemEndpoints namespace PublicApiIntegrationTests.CatalogItemEndpoints
@@ -45,5 +48,26 @@ namespace PublicApiIntegrationTests.CatalogItemEndpoints
Assert.AreEqual(totalExpected, model2.CatalogItems.Count()); Assert.AreEqual(totalExpected, model2.CatalogItems.Count());
} }
[DataTestMethod]
[DataRow("catalog-items")]
[DataRow("catalog-brands")]
[DataRow("catalog-types")]
[DataRow("catalog-items/1")]
public async Task SuccessFullMutipleParallelCall(string endpointName)
{
var client = ProgramTest.NewClient;
var tasks = new List<Task<HttpResponseMessage>>();
for (int i = 0; i < 100; i++)
{
var task = client.GetAsync($"/api/{endpointName}");
tasks.Add(task);
}
await Task.WhenAll(tasks.ToList());
var totalKO = tasks.Count(t => t.Result.StatusCode != HttpStatusCode.OK);
Assert.AreEqual(0, totalKO);
}
} }
} }