Skip to content

Commit b3b115b

Browse files
authored
Refactor Catalog API in new API version (#680)
* 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
1 parent cd3f752 commit b3b115b

File tree

15 files changed

+1420
-478
lines changed

15 files changed

+1420
-478
lines changed

src/Catalog.API/Apis/CatalogApi.cs

+89-65
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel;
2+
using System.ComponentModel.DataAnnotations;
23
using Microsoft.AspNetCore.Http.HttpResults;
34
using Microsoft.AspNetCore.Mvc;
45
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -8,16 +9,25 @@ namespace eShop.Catalog.API;
89

910
public static class CatalogApi
1011
{
11-
public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder app)
12+
public static IEndpointRouteBuilder MapCatalogApi(this IEndpointRouteBuilder app)
1213
{
13-
var api = app.MapGroup("api/catalog").HasApiVersion(1.0);
14+
// RouteGroupBuilder for catalog endpoints
15+
var vApi = app.NewVersionedApi("Catalog");
16+
var api = vApi.MapGroup("api/catalog").HasApiVersion(1, 0).HasApiVersion(2, 0);
17+
var v1 = vApi.MapGroup("api/catalog").HasApiVersion(1, 0);
18+
var v2 = vApi.MapGroup("api/catalog").HasApiVersion(2, 0);
1419

1520
// Routes for querying catalog items.
16-
api.MapGet("/items", GetAllItems)
21+
v1.MapGet("/items", GetAllItemsV1)
1722
.WithName("ListItems")
1823
.WithSummary("List catalog items")
1924
.WithDescription("Get a paginated list of items in the catalog.")
2025
.WithTags("Items");
26+
v2.MapGet("/items", GetAllItems)
27+
.WithName("ListItems-V2")
28+
.WithSummary("List catalog items")
29+
.WithDescription("Get a paginated list of items in the catalog.")
30+
.WithTags("Items");
2131
api.MapGet("/items/by", GetItemsByIds)
2232
.WithName("BatchGetItems")
2333
.WithSummary("Batch get catalog items")
@@ -28,7 +38,7 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
2838
.WithSummary("Get catalog item")
2939
.WithDescription("Get an item from the catalog")
3040
.WithTags("Items");
31-
api.MapGet("/items/by/{name:minlength(1)}", GetItemsByName)
41+
v1.MapGet("/items/by/{name:minlength(1)}", GetItemsByName)
3242
.WithName("GetItemsByName")
3343
.WithSummary("Get catalog items by name")
3444
.WithDescription("Get a paginated list of catalog items with the specified name.")
@@ -40,19 +50,26 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
4050
.WithTags("Items");
4151

4252
// Routes for resolving catalog items using AI.
43-
api.MapGet("/items/withsemanticrelevance/{text:minlength(1)}", GetItemsBySemanticRelevance)
53+
v1.MapGet("/items/withsemanticrelevance/{text:minlength(1)}", GetItemsBySemanticRelevanceV1)
4454
.WithName("GetRelevantItems")
4555
.WithSummary("Search catalog for relevant items")
4656
.WithDescription("Search the catalog for items related to the specified text")
4757
.WithTags("Search");
4858

59+
// Routes for resolving catalog items using AI.
60+
v2.MapGet("/items/withsemanticrelevance", GetItemsBySemanticRelevance)
61+
.WithName("GetRelevantItems-V2")
62+
.WithSummary("Search catalog for relevant items")
63+
.WithDescription("Search the catalog for items related to the specified text")
64+
.WithTags("Search");
65+
4966
// Routes for resolving catalog items by type and brand.
50-
api.MapGet("/items/type/{typeId}/brand/{brandId?}", GetItemsByBrandAndTypeId)
67+
v1.MapGet("/items/type/{typeId}/brand/{brandId?}", GetItemsByBrandAndTypeId)
5168
.WithName("GetItemsByTypeAndBrand")
5269
.WithSummary("Get catalog items by type and brand")
5370
.WithDescription("Get catalog items of the specified type and brand")
5471
.WithTags("Types");
55-
api.MapGet("/items/type/all/brand/{brandId:int?}", GetItemsByBrandId)
72+
v1.MapGet("/items/type/all/brand/{brandId:int?}", GetItemsByBrandId)
5673
.WithName("GetItemsByBrand")
5774
.WithSummary("List catalog items by brand")
5875
.WithDescription("Get a list of catalog items for the specified brand")
@@ -73,11 +90,16 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
7390
.WithTags("Brands");
7491

7592
// Routes for modifying catalog items.
76-
api.MapPut("/items", UpdateItem)
93+
v1.MapPut("/items", UpdateItemV1)
7794
.WithName("UpdateItem")
7895
.WithSummary("Create or replace a catalog item")
7996
.WithDescription("Create or replace a catalog item")
8097
.WithTags("Items");
98+
v2.MapPut("/items/{id:int}", UpdateItem)
99+
.WithName("UpdateItem-V2")
100+
.WithSummary("Create or replace a catalog item")
101+
.WithDescription("Create or replace a catalog item")
102+
.WithTags("Items");
81103
api.MapPost("/items", CreateItem)
82104
.WithName("CreateItem")
83105
.WithSummary("Create a catalog item")
@@ -91,17 +113,43 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a
91113
}
92114

93115
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
94-
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItems(
116+
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItemsV1(
95117
[AsParameters] PaginationRequest paginationRequest,
96118
[AsParameters] CatalogServices services)
119+
{
120+
return await GetAllItems(paginationRequest, services, null, null, null);
121+
}
122+
123+
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
124+
public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItems(
125+
[AsParameters] PaginationRequest paginationRequest,
126+
[AsParameters] CatalogServices services,
127+
[Description("The name of the item to return")] string name,
128+
[Description("The type of items to return")] int? type,
129+
[Description("The brand of items to return")] int? brand)
97130
{
98131
var pageSize = paginationRequest.PageSize;
99132
var pageIndex = paginationRequest.PageIndex;
100133

101-
var totalItems = await services.Context.CatalogItems
134+
var root = (IQueryable<CatalogItem>)services.Context.CatalogItems;
135+
136+
if (name is not null)
137+
{
138+
root = root.Where(c => c.Name.StartsWith(name));
139+
}
140+
if (type is not null)
141+
{
142+
root = root.Where(c => c.CatalogTypeId == type);
143+
}
144+
if (brand is not null)
145+
{
146+
root = root.Where(c => c.CatalogBrandId == brand);
147+
}
148+
149+
var totalItems = await root
102150
.LongCountAsync();
103151

104-
var itemsOnPage = await services.Context.CatalogItems
152+
var itemsOnPage = await root
105153
.OrderBy(c => c.Name)
106154
.Skip(pageSize * pageIndex)
107155
.Take(pageSize)
@@ -148,20 +196,7 @@ public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByName(
148196
[AsParameters] CatalogServices services,
149197
[Description("The name of the item to return")] string name)
150198
{
151-
var pageSize = paginationRequest.PageSize;
152-
var pageIndex = paginationRequest.PageIndex;
153-
154-
var totalItems = await services.Context.CatalogItems
155-
.Where(c => c.Name.StartsWith(name))
156-
.LongCountAsync();
157-
158-
var itemsOnPage = await services.Context.CatalogItems
159-
.Where(c => c.Name.StartsWith(name))
160-
.Skip(pageSize * pageIndex)
161-
.Take(pageSize)
162-
.ToListAsync();
163-
164-
return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
199+
return await GetAllItems(paginationRequest, services, name, null, null);
165200
}
166201

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

191226
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
192-
public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevance(
227+
public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevanceV1(
193228
[AsParameters] PaginationRequest paginationRequest,
194229
[AsParameters] CatalogServices services,
195230
[Description("The text string to use when search for related items in the catalog")] string text)
231+
232+
{
233+
return await GetItemsBySemanticRelevance(paginationRequest, services, text);
234+
}
235+
236+
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
237+
public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevance(
238+
[AsParameters] PaginationRequest paginationRequest,
239+
[AsParameters] CatalogServices services,
240+
[Description("The text string to use when search for related items in the catalog"), Required, MinLength(1)] string text)
196241
{
197242
var pageSize = paginationRequest.PageSize;
198243
var pageIndex = paginationRequest.PageIndex;
@@ -243,25 +288,7 @@ public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandAndType
243288
[Description("The type of items to return")] int typeId,
244289
[Description("The brand of items to return")] int? brandId)
245290
{
246-
var pageSize = paginationRequest.PageSize;
247-
var pageIndex = paginationRequest.PageIndex;
248-
249-
var root = (IQueryable<CatalogItem>)services.Context.CatalogItems;
250-
root = root.Where(c => c.CatalogTypeId == typeId);
251-
if (brandId is not null)
252-
{
253-
root = root.Where(c => c.CatalogBrandId == brandId);
254-
}
255-
256-
var totalItems = await root
257-
.LongCountAsync();
258-
259-
var itemsOnPage = await root
260-
.Skip(pageSize * pageIndex)
261-
.Take(pageSize)
262-
.ToListAsync();
263-
264-
return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
291+
return await GetAllItems(paginationRequest, services, null, typeId, brandId);
265292
}
266293

267294
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
@@ -270,38 +297,35 @@ public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandId(
270297
[AsParameters] CatalogServices services,
271298
[Description("The brand of items to return")] int? brandId)
272299
{
273-
var pageSize = paginationRequest.PageSize;
274-
var pageIndex = paginationRequest.PageIndex;
275-
276-
var root = (IQueryable<CatalogItem>)services.Context.CatalogItems;
300+
return await GetAllItems(paginationRequest, services, null, null, brandId);
301+
}
277302

278-
if (brandId is not null)
303+
public static async Task<Results<Created, BadRequest<ProblemDetails>, NotFound<ProblemDetails>>> UpdateItemV1(
304+
HttpContext httpContext,
305+
[AsParameters] CatalogServices services,
306+
CatalogItem productToUpdate)
307+
{
308+
if (productToUpdate?.Id == null)
279309
{
280-
root = root.Where(ci => ci.CatalogBrandId == brandId);
310+
return TypedResults.BadRequest<ProblemDetails>(new (){
311+
Detail = "Item id must be provided in the request body."
312+
});
281313
}
282-
283-
var totalItems = await root
284-
.LongCountAsync();
285-
286-
var itemsOnPage = await root
287-
.Skip(pageSize * pageIndex)
288-
.Take(pageSize)
289-
.ToListAsync();
290-
291-
return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
314+
return await UpdateItem(httpContext, productToUpdate.Id, services, productToUpdate);
292315
}
293316

294-
public static async Task<Results<Created, NotFound<ProblemDetails>>> UpdateItem(
317+
public static async Task<Results<Created, BadRequest<ProblemDetails>, NotFound<ProblemDetails>>> UpdateItem(
295318
HttpContext httpContext,
319+
[Description("The id of the catalog item to delete")] int id,
296320
[AsParameters] CatalogServices services,
297321
CatalogItem productToUpdate)
298322
{
299-
var catalogItem = await services.Context.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id);
323+
var catalogItem = await services.Context.CatalogItems.SingleOrDefaultAsync(i => i.Id == id);
300324

301325
if (catalogItem == null)
302326
{
303327
return TypedResults.NotFound<ProblemDetails>(new (){
304-
Detail = $"Item with id {productToUpdate.Id} not found."
328+
Detail = $"Item with id {id} not found."
305329
});
306330
}
307331

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

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

0 commit comments

Comments
 (0)