diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 5af8b11..e0b5a21 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/src/Web/API/BaseRequest.cs b/src/Web/API/BaseRequest.cs new file mode 100644 index 0000000..06eb1c6 --- /dev/null +++ b/src/Web/API/BaseRequest.cs @@ -0,0 +1,20 @@ +using System; + +namespace Microsoft.eShopWeb.Web.API +{ + /// + /// Base class used by API requests + /// + public abstract class BaseMessage + { + /// + /// Unique Identifier used by logging + /// + protected Guid _correlationId = Guid.NewGuid(); + public Guid CorrelationId() => _correlationId; + } + + public abstract class BaseRequest : BaseMessage + { + } +} diff --git a/src/Web/API/BaseResponse.cs b/src/Web/API/BaseResponse.cs new file mode 100644 index 0000000..d2c8535 --- /dev/null +++ b/src/Web/API/BaseResponse.cs @@ -0,0 +1,19 @@ +using System; + +namespace Microsoft.eShopWeb.Web.API +{ + /// + /// Base class used by API responses + /// + public abstract class BaseResponse : BaseMessage + { + public BaseResponse(Guid correlationId) : base() + { + base._correlationId = correlationId; + } + + public BaseResponse() + { + } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/CatalogItemDto.cs b/src/Web/API/CatalogItemEndpoints/CatalogItemDto.cs new file mode 100644 index 0000000..9a3a197 --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/CatalogItemDto.cs @@ -0,0 +1,13 @@ +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class CatalogItemDto + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; set; } + public string PictureUri { get; set; } + public int CatalogTypeId { get; set; } + public int CatalogBrandId { get; set; } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs b/src/Web/API/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs new file mode 100644 index 0000000..f5e168c --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs @@ -0,0 +1,12 @@ +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class CreateCatalogItemRequest : BaseRequest + { + public int CatalogBrandId { get; set; } + public int CatalogTypeId { get; set; } + public string Description { get; set; } + public string Name { get; set; } + public string PictureUri { get; set; } + public decimal Price { get; set; } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/Create.CreateCatalogItemResponse.cs b/src/Web/API/CatalogItemEndpoints/Create.CreateCatalogItemResponse.cs new file mode 100644 index 0000000..9b22c63 --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/Create.CreateCatalogItemResponse.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class CreateCatalogItemResponse : BaseResponse + { + public CreateCatalogItemResponse(Guid correlationId) : base(correlationId) + { + } + + public CatalogItemDto CatalogItem { get; set; } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/Create.cs b/src/Web/API/CatalogItemEndpoints/Create.cs new file mode 100644 index 0000000..133690b --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/Create.cs @@ -0,0 +1,48 @@ +using Ardalis.ApiEndpoints; +using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Swashbuckle.AspNetCore.Annotations; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class Create : BaseAsyncEndpoint + { + private readonly IAsyncRepository _itemRepository; + + public Create(IAsyncRepository itemRepository) + { + _itemRepository = itemRepository; + } + + [HttpPost("api/catalog-items")] + [SwaggerOperation( + Summary = "Creates a new Catalog Item", + Description = "Creates a new Catalog Item", + OperationId = "catalog-items.create", + Tags = new[] { "CatalogItemEndpoints" }) + ] + public override async Task> HandleAsync(CreateCatalogItemRequest request) + { + var response = new CreateCatalogItemResponse(request.CorrelationId()); + + CatalogItem newItem = new CatalogItem(request.CatalogTypeId, request.CatalogBrandId, request.Description, request.Name, request.Price, request.PictureUri); + + newItem = await _itemRepository.AddAsync(newItem); + + var dto = new CatalogItemDto + { + Id = newItem.Id, + CatalogBrandId = newItem.CatalogBrandId, + CatalogTypeId = newItem.CatalogTypeId, + Description = newItem.Description, + Name = newItem.Name, + PictureUri = newItem.PictureUri, + Price = newItem.Price + }; + response.CatalogItem = dto; + return response; + } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/Delete.DeleteCatalogItemRequest.cs b/src/Web/API/CatalogItemEndpoints/Delete.DeleteCatalogItemRequest.cs new file mode 100644 index 0000000..7e59de7 --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/Delete.DeleteCatalogItemRequest.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class DeleteCatalogItemRequest : BaseRequest + { + //[FromRoute] + public int CatalogItemId { get; set; } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/Delete.DeleteCatalogItemResponse.cs b/src/Web/API/CatalogItemEndpoints/Delete.DeleteCatalogItemResponse.cs new file mode 100644 index 0000000..40bed9a --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/Delete.DeleteCatalogItemResponse.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class DeleteCatalogItemResponse : BaseResponse + { + public DeleteCatalogItemResponse(Guid correlationId) : base(correlationId) + { + } + + public string Status { get; set; } = "Deleted"; + } +} diff --git a/src/Web/API/CatalogItemEndpoints/Delete.cs b/src/Web/API/CatalogItemEndpoints/Delete.cs new file mode 100644 index 0000000..323bea5 --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/Delete.cs @@ -0,0 +1,38 @@ +using Ardalis.ApiEndpoints; +using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Swashbuckle.AspNetCore.Annotations; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class Delete : BaseAsyncEndpoint + { + private readonly IAsyncRepository _itemRepository; + + public Delete(IAsyncRepository itemRepository) + { + _itemRepository = itemRepository; + } + + [HttpDelete("api/catalog-items/{CatalogItemId}")] + [SwaggerOperation( + Summary = "Deletes a Catalog Item", + Description = "Deletes a Catalog Item", + OperationId = "catalog-items.Delete", + Tags = new[] { "CatalogItemEndpoints" }) + ] + public override async Task> HandleAsync([FromRoute]DeleteCatalogItemRequest request) + { + var response = new DeleteCatalogItemResponse(request.CorrelationId()); + + var itemToDelete = await _itemRepository.GetByIdAsync(request.CatalogItemId); + if (itemToDelete is null) return NotFound(); + + await _itemRepository.DeleteAsync(itemToDelete); + + return Ok(response); + } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/GetById.GetByIdCatalogItemRequest.cs b/src/Web/API/CatalogItemEndpoints/GetById.GetByIdCatalogItemRequest.cs new file mode 100644 index 0000000..b935c97 --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/GetById.GetByIdCatalogItemRequest.cs @@ -0,0 +1,7 @@ +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class GetByIdCatalogItemRequest : BaseRequest + { + public int CatalogItemId { get; set; } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/GetById.GetByIdCatalogItemResponse.cs b/src/Web/API/CatalogItemEndpoints/GetById.GetByIdCatalogItemResponse.cs new file mode 100644 index 0000000..ebdd3e0 --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/GetById.GetByIdCatalogItemResponse.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class GetByIdCatalogItemResponse : BaseResponse + { + public GetByIdCatalogItemResponse(Guid correlationId) : base(correlationId) + { + } + + public CatalogItemDto CatalogItem { get; set; } + } +} diff --git a/src/Web/API/CatalogItemEndpoints/GetById.cs b/src/Web/API/CatalogItemEndpoints/GetById.cs new file mode 100644 index 0000000..ff28eaf --- /dev/null +++ b/src/Web/API/CatalogItemEndpoints/GetById.cs @@ -0,0 +1,46 @@ +using Ardalis.ApiEndpoints; +using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Swashbuckle.AspNetCore.Annotations; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints +{ + public class GetById : BaseAsyncEndpoint + { + private readonly IAsyncRepository _itemRepository; + + public GetById(IAsyncRepository itemRepository) + { + _itemRepository = itemRepository; + } + + [HttpGet("api/catalog-items/{CatalogItemId}")] + [SwaggerOperation( + Summary = "Get a Catalog Item by Id", + Description = "Gets a Catalog Item by Id", + OperationId = "catalog-items.GetById", + Tags = new[] { "CatalogItemEndpoints" }) + ] + public override async Task> HandleAsync([FromRoute]GetByIdCatalogItemRequest request) + { + var response = new GetByIdCatalogItemResponse(request.CorrelationId()); + + var item = await _itemRepository.GetByIdAsync(request.CatalogItemId); + if (item is null) return NotFound(); + + response.CatalogItem = new CatalogItemDto + { + Id = item.Id, + CatalogBrandId = item.CatalogBrandId, + CatalogTypeId = item.CatalogTypeId, + Description = item.Description, + Name = item.Name, + PictureUri = item.PictureUri, + Price = item.Price + }; + return Ok(response); + } + } +} diff --git a/src/Web/API/CustomSchemaFilters.cs b/src/Web/API/CustomSchemaFilters.cs new file mode 100644 index 0000000..490bb6c --- /dev/null +++ b/src/Web/API/CustomSchemaFilters.cs @@ -0,0 +1,17 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Microsoft.eShopWeb.Web.API +{ + public class CustomSchemaFilters : ISchemaFilter + { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var excludeProperties = new[] { "CorrelationId" }; + + foreach (var prop in excludeProperties) + if (schema.Properties.ContainsKey(prop)) + schema.Properties.Remove(prop); + } + } +} diff --git a/src/Web/API/README.md b/src/Web/API/README.md new file mode 100644 index 0000000..91d3f77 --- /dev/null +++ b/src/Web/API/README.md @@ -0,0 +1,4 @@ +# API Endpoints + +This folder demonstrates how to configure API endpoints as individual classes. You can compare it to the traditional controller-based approach found in /Controllers/Api. + diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index e7591c5..c6a78fd 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -13,6 +13,7 @@ using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Infrastructure.Logging; using Microsoft.eShopWeb.Infrastructure.Services; +using Microsoft.eShopWeb.Web.API; using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.Services; using Microsoft.Extensions.Configuration; @@ -24,6 +25,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Mime; +using static Microsoft.eShopWeb.Web.API.BaseRequest; namespace Microsoft.eShopWeb.Web { @@ -129,7 +131,12 @@ namespace Microsoft.eShopWeb.Web services.AddHttpContextAccessor(); - services.AddSwaggerGen(c => c.SwaggerDoc("v1", new OpenApi.Models.OpenApiInfo { Title = "My API", Version = "v1" })); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApi.Models.OpenApiInfo { Title = "My API", Version = "v1" }); + c.EnableAnnotations(); + c.SchemaFilter(); + }); services.AddHealthChecks(); diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index f3ca000..7aa917f 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -13,23 +13,24 @@ + - - + + - - - - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/FunctionalTests/FunctionalTests.csproj b/tests/FunctionalTests/FunctionalTests.csproj index 3253ce9..ef7ae1f 100644 --- a/tests/FunctionalTests/FunctionalTests.csproj +++ b/tests/FunctionalTests/FunctionalTests.csproj @@ -13,14 +13,14 @@ - + - + all runtime; build; native; contentfiles; analyzers - + @@ -33,4 +33,8 @@ + + + + diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj index dd0510c..4317f2f 100644 --- a/tests/IntegrationTests/IntegrationTests.csproj +++ b/tests/IntegrationTests/IntegrationTests.csproj @@ -7,11 +7,11 @@ - + - + - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index f0b97b8..2ea972b 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -9,9 +9,9 @@ - + - + all runtime; build; native; contentfiles; analyzers