Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Catalog API #659

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 48 additions & 107 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 Down Expand Up @@ -28,35 +29,19 @@ 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)
.WithName("GetItemsByName")
.WithSummary("Get catalog items by name")
.WithDescription("Get a paginated list of catalog items with the specified name.")
.WithTags("Items");
api.MapGet("/items/{id:int}/pic", GetItemPictureById)
.WithName("GetItemPicture")
.WithSummary("Get catalog item picture")
.WithDescription("Get the picture for a catalog item")
.WithTags("Items");

// Routes for resolving catalog items using AI.
api.MapGet("/items/withsemanticrelevance/{text:minlength(1)}", GetItemsBySemanticRelevance)
api.MapGet("/items/withsemanticrelevance", GetItemsBySemanticRelevance)
.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 by type and brand.
api.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)
.WithName("GetItemsByBrand")
.WithSummary("List catalog items by brand")
.WithDescription("Get a list of catalog items for the specified brand")
.WithTags("Brands");
api.MapGet("/catalogtypes",
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
async (CatalogContext context) => await context.CatalogTypes.OrderBy(x => x.Type).ToListAsync())
Expand All @@ -73,15 +58,14 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
.WithTags("Brands");

// Routes for modifying catalog items.
api.MapPut("/items", UpdateItem)
.WithName("UpdateItem")
.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")
.WithDescription("Create a new item in the catalog");
api.MapPut("/items/{id:int}", UpdateItem)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to retain the Items tag on this and the other items-related endpoints?

.WithName("UpdateItem")
.WithSummary("Replace a catalog item")
.WithDescription("Replace a catalog item");
api.MapDelete("/items/{id:int}", DeleteItemById)
.WithName("DeleteItem")
.WithSummary("Delete catalog item")
Expand All @@ -93,16 +77,33 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItems(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services)
[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;
mikekistler marked this conversation as resolved.
Show resolved Hide resolved

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
.OrderBy(c => c.Name)
var itemsOnPage = await root
mikekistler marked this conversation as resolved.
Show resolved Hide resolved
.Skip(pageSize * pageIndex)
.Take(pageSize)
.ToListAsync();
Expand Down Expand Up @@ -142,28 +143,6 @@ public static async Task<Results<Ok<CatalogItem>, NotFound, BadRequest<ProblemDe
return TypedResults.Ok(item);
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByName(
[AsParameters] PaginationRequest paginationRequest,
[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));
}

[ProducesResponseType<byte[]>(StatusCodes.Status200OK, "application/octet-stream",
[ "image/png", "image/gif", "image/jpeg", "image/bmp", "image/tiff",
"image/wmf", "image/jp2", "image/svg+xml", "image/webp" ])]
Expand All @@ -188,18 +167,25 @@ public static async Task<Results<PhysicalFileHttpResult,NotFound>> GetItemPictur
return TypedResults.PhysicalFile(path, mimetype, lastModified: lastModified);
}

[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>>, BadRequest<ProblemDetails>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevance(
HttpContext httpContext,
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
[Description("The text string to use when search for related items in the catalog")] string text)
[Description("The text string to use when search for related items in the catalog"), Required, MinLength(1)] string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return TypedResults.BadRequest<ProblemDetails>(new (){
Detail = "The text parameter is required.",
});
}

var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

if (!services.CatalogAI.IsEnabled)
{
return await GetItemsByName(paginationRequest, services, text);
return await GetAllItems(paginationRequest, services, text, null, null);
}

// Create an embedding for the input search
Expand Down Expand Up @@ -236,66 +222,21 @@ public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRout
return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandAndTypeId(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
[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));
}

[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandId(
[AsParameters] PaginationRequest paginationRequest,
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound, "application/problem+json")]
public static async Task<Results<Created, BadRequest<ProblemDetails>, NotFound<ProblemDetails>>> UpdateItem(
[Description("The id of the catalog item to delete")] int id,
HttpContext httpContext,
[AsParameters] CatalogServices services,
[Description("The brand of items to return")] int? brandId)
CatalogItem productToUpdate)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

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

if (brandId is not null)
if (id != productToUpdate.Id)
{
root = root.Where(ci => ci.CatalogBrandId == brandId);
return TypedResults.BadRequest<ProblemDetails>(new ()
{
Detail = "The id in the URL does not match the id 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));
}

public static async Task<Results<Created, NotFound<ProblemDetails>>> UpdateItem(
HttpContext httpContext,
[AsParameters] CatalogServices services,
CatalogItem productToUpdate)
{
var catalogItem = await services.Context.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id);

if (catalogItem == null)
Expand Down
14 changes: 7 additions & 7 deletions src/Catalog.API/Catalog.API.http
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
@Catalog.API_HostAddress = http://localhost:5222
@CatalogAPI_HostAddress = http://localhost:5222
@ApiVersion = 1.0

GET {{Catalog.API_HostAddress}}/openapi/v1.json
GET {{CatalogAPI_HostAddress}}/openapi/v1.json

###

GET {{Catalog.API_HostAddress}}/api/catalog/items?api-version={{ApiVersion}}
GET {{CatalogAPI_HostAddress}}/api/catalog/items?api-version={{ApiVersion}}

###

GET {{Catalog.API_HostAddress}}/api/catalog/items/type/1/brand/2?api-version={{ApiVersion}}
GET {{CatalogAPI_HostAddress}}/api/catalog/items?type=3&brand=3&api-version={{ApiVersion}}

###

# A request with an unknown API version returns a 400 ProblemDetails response

GET {{Catalog.API_HostAddress}}/api/catalog/items/463/pic?api-version=99
GET {{CatalogAPI_HostAddress}}/api/catalog/items/463/pic?api-version=99

###

# A request with an unknown item id returns a 404 NotFound with empty response body

GET {{Catalog.API_HostAddress}}/api/catalog/items/463/pic?api-version={{ApiVersion}}
GET {{CatalogAPI_HostAddress}}/api/catalog/items/463/pic?api-version={{ApiVersion}}

###

PUT {{Catalog.API_HostAddress}}/api/catalog/items?api-version={{ApiVersion}}
PUT {{CatalogAPI_HostAddress}}/api/catalog/items?api-version={{ApiVersion}}
content-type: application/json

{
Expand Down
Loading
Loading