Skip to content

Commit

Permalink
Refactor Catalog API in new API version (#680)
Browse files Browse the repository at this point in the history
* Add Catalog API v2 with all the same APIs
* Split GetAllItems into v1 and v2
* Collapse GetItemsByName, Type, or Brand into GetAllItems
* Add id path parameter to UpdateItem
* Move text parameter from path to query
* Update Catalog clients to V2 API version
* Refactor v1 APIs to use V2 implementation
* Update ApiVersion of WebAppCatalogService (client) to 2.0
* Address PR review comments
  • Loading branch information
mikekistler authored Jan 10, 2025
1 parent cd3f752 commit b3b115b
Show file tree
Hide file tree
Showing 15 changed files with 1,420 additions and 478 deletions.
154 changes: 89 additions & 65 deletions src/Catalog.API/Apis/CatalogApi.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
Expand All @@ -8,16 +9,25 @@ namespace eShop.Catalog.API;

public static class CatalogApi
{
public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder app)
public static IEndpointRouteBuilder MapCatalogApi(this IEndpointRouteBuilder app)
{
var api = app.MapGroup("api/catalog").HasApiVersion(1.0);
// RouteGroupBuilder for catalog endpoints
var vApi = app.NewVersionedApi("Catalog");
var api = vApi.MapGroup("api/catalog").HasApiVersion(1, 0).HasApiVersion(2, 0);
var v1 = vApi.MapGroup("api/catalog").HasApiVersion(1, 0);
var v2 = vApi.MapGroup("api/catalog").HasApiVersion(2, 0);

// Routes for querying catalog items.
api.MapGet("/items", GetAllItems)
v1.MapGet("/items", GetAllItemsV1)
.WithName("ListItems")
.WithSummary("List catalog items")
.WithDescription("Get a paginated list of items in the catalog.")
.WithTags("Items");
v2.MapGet("/items", GetAllItems)
.WithName("ListItems-V2")
.WithSummary("List catalog items")
.WithDescription("Get a paginated list of items in the catalog.")
.WithTags("Items");
api.MapGet("/items/by", GetItemsByIds)
.WithName("BatchGetItems")
.WithSummary("Batch get catalog items")
Expand All @@ -28,7 +38,7 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
.WithSummary("Get catalog item")
.WithDescription("Get an item from the catalog")
.WithTags("Items");
api.MapGet("/items/by/{name:minlength(1)}", GetItemsByName)
v1.MapGet("/items/by/{name:minlength(1)}", GetItemsByName)
.WithName("GetItemsByName")
.WithSummary("Get catalog items by name")
.WithDescription("Get a paginated list of catalog items with the specified name.")
Expand All @@ -40,19 +50,26 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
.WithTags("Items");

// Routes for resolving catalog items using AI.
api.MapGet("/items/withsemanticrelevance/{text:minlength(1)}", GetItemsBySemanticRelevance)
v1.MapGet("/items/withsemanticrelevance/{text:minlength(1)}", GetItemsBySemanticRelevanceV1)
.WithName("GetRelevantItems")
.WithSummary("Search catalog for relevant items")
.WithDescription("Search the catalog for items related to the specified text")
.WithTags("Search");

// Routes for resolving catalog items using AI.
v2.MapGet("/items/withsemanticrelevance", GetItemsBySemanticRelevance)
.WithName("GetRelevantItems-V2")
.WithSummary("Search catalog for relevant items")
.WithDescription("Search the catalog for items related to the specified text")
.WithTags("Search");

// Routes for resolving catalog items by type and brand.
api.MapGet("/items/type/{typeId}/brand/{brandId?}", GetItemsByBrandAndTypeId)
v1.MapGet("/items/type/{typeId}/brand/{brandId?}", GetItemsByBrandAndTypeId)
.WithName("GetItemsByTypeAndBrand")
.WithSummary("Get catalog items by type and brand")
.WithDescription("Get catalog items of the specified type and brand")
.WithTags("Types");
api.MapGet("/items/type/all/brand/{brandId:int?}", GetItemsByBrandId)
v1.MapGet("/items/type/all/brand/{brandId:int?}", GetItemsByBrandId)
.WithName("GetItemsByBrand")
.WithSummary("List catalog items by brand")
.WithDescription("Get a list of catalog items for the specified brand")
Expand All @@ -73,11 +90,16 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
.WithTags("Brands");

// Routes for modifying catalog items.
api.MapPut("/items", UpdateItem)
v1.MapPut("/items", UpdateItemV1)
.WithName("UpdateItem")
.WithSummary("Create or replace a catalog item")
.WithDescription("Create or replace a catalog item")
.WithTags("Items");
v2.MapPut("/items/{id:int}", UpdateItem)
.WithName("UpdateItem-V2")
.WithSummary("Create or replace a catalog item")
.WithDescription("Create or replace a catalog item")
.WithTags("Items");
api.MapPost("/items", CreateItem)
.WithName("CreateItem")
.WithSummary("Create a catalog item")
Expand All @@ -91,17 +113,43 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItems(
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItemsV1(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services)
{
return await GetAllItems(paginationRequest, services, null, null, null);
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItems(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
[Description("The name of the item to return")] string name,
[Description("The type of items to return")] int? type,
[Description("The brand of items to return")] int? brand)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

var totalItems = await services.Context.CatalogItems
var root = (IQueryable<CatalogItem>)services.Context.CatalogItems;

if (name is not null)
{
root = root.Where(c => c.Name.StartsWith(name));
}
if (type is not null)
{
root = root.Where(c => c.CatalogTypeId == type);
}
if (brand is not null)
{
root = root.Where(c => c.CatalogBrandId == brand);
}

var totalItems = await root
.LongCountAsync();

var itemsOnPage = await services.Context.CatalogItems
var itemsOnPage = await root
.OrderBy(c => c.Name)
.Skip(pageSize * pageIndex)
.Take(pageSize)
Expand Down Expand Up @@ -148,20 +196,7 @@ public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByName(
[AsParameters] CatalogServices services,
[Description("The name of the item to return")] string name)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

var totalItems = await services.Context.CatalogItems
.Where(c => c.Name.StartsWith(name))
.LongCountAsync();

var itemsOnPage = await services.Context.CatalogItems
.Where(c => c.Name.StartsWith(name))
.Skip(pageSize * pageIndex)
.Take(pageSize)
.ToListAsync();

return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
return await GetAllItems(paginationRequest, services, name, null, null);
}

[ProducesResponseType<byte[]>(StatusCodes.Status200OK, "application/octet-stream",
Expand Down Expand Up @@ -189,10 +224,20 @@ public static async Task<Results<PhysicalFileHttpResult,NotFound>> GetItemPictur
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevance(
public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevanceV1(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
[Description("The text string to use when search for related items in the catalog")] string text)

{
return await GetItemsBySemanticRelevance(paginationRequest, services, text);
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevance(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
[Description("The text string to use when search for related items in the catalog"), Required, MinLength(1)] string text)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;
Expand Down Expand Up @@ -243,25 +288,7 @@ public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandAndType
[Description("The type of items to return")] int typeId,
[Description("The brand of items to return")] int? brandId)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

var root = (IQueryable<CatalogItem>)services.Context.CatalogItems;
root = root.Where(c => c.CatalogTypeId == typeId);
if (brandId is not null)
{
root = root.Where(c => c.CatalogBrandId == brandId);
}

var totalItems = await root
.LongCountAsync();

var itemsOnPage = await root
.Skip(pageSize * pageIndex)
.Take(pageSize)
.ToListAsync();

return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
return await GetAllItems(paginationRequest, services, null, typeId, brandId);
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
Expand All @@ -270,38 +297,35 @@ public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandId(
[AsParameters] CatalogServices services,
[Description("The brand of items to return")] int? brandId)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

var root = (IQueryable<CatalogItem>)services.Context.CatalogItems;
return await GetAllItems(paginationRequest, services, null, null, brandId);
}

if (brandId is not null)
public static async Task<Results<Created, BadRequest<ProblemDetails>, NotFound<ProblemDetails>>> UpdateItemV1(
HttpContext httpContext,
[AsParameters] CatalogServices services,
CatalogItem productToUpdate)
{
if (productToUpdate?.Id == null)
{
root = root.Where(ci => ci.CatalogBrandId == brandId);
return TypedResults.BadRequest<ProblemDetails>(new (){
Detail = "Item id must be provided in the request body."
});
}

var totalItems = await root
.LongCountAsync();

var itemsOnPage = await root
.Skip(pageSize * pageIndex)
.Take(pageSize)
.ToListAsync();

return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
return await UpdateItem(httpContext, productToUpdate.Id, services, productToUpdate);
}

public static async Task<Results<Created, NotFound<ProblemDetails>>> UpdateItem(
public static async Task<Results<Created, BadRequest<ProblemDetails>, NotFound<ProblemDetails>>> UpdateItem(
HttpContext httpContext,
[Description("The id of the catalog item to delete")] int id,
[AsParameters] CatalogServices services,
CatalogItem productToUpdate)
{
var catalogItem = await services.Context.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id);
var catalogItem = await services.Context.CatalogItems.SingleOrDefaultAsync(i => i.Id == id);

if (catalogItem == null)
{
return TypedResults.NotFound<ProblemDetails>(new (){
Detail = $"Item with id {productToUpdate.Id} not found."
Detail = $"Item with id {id} not found."
});
}

Expand All @@ -328,7 +352,7 @@ public static async Task<Results<Created, NotFound<ProblemDetails>>> UpdateItem(
{
await services.Context.SaveChangesAsync();
}
return TypedResults.Created($"/api/catalog/items/{productToUpdate.Id}");
return TypedResults.Created($"/api/catalog/items/{id}");
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
Expand Down
Loading

0 comments on commit b3b115b

Please sign in to comment.