diff --git a/src/Catalog.API/Apis/CatalogApi.cs b/src/Catalog.API/Apis/CatalogApi.cs index 224dda648..69a1a5220 100644 --- a/src/Catalog.API/Apis/CatalogApi.cs +++ b/src/Catalog.API/Apis/CatalogApi.cs @@ -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; @@ -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") @@ -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.") @@ -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") @@ -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") @@ -91,17 +113,43 @@ public static IEndpointRouteBuilder MapCatalogApiV1(this IEndpointRouteBuilder a } [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] - public static async Task>> GetAllItems( + public static async Task>> GetAllItemsV1( [AsParameters] PaginationRequest paginationRequest, [AsParameters] CatalogServices services) + { + return await GetAllItems(paginationRequest, services, null, null, null); + } + + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public static async Task>> 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)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) @@ -148,20 +196,7 @@ public static async Task>> 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(pageIndex, pageSize, totalItems, itemsOnPage)); + return await GetAllItems(paginationRequest, services, name, null, null); } [ProducesResponseType(StatusCodes.Status200OK, "application/octet-stream", @@ -189,10 +224,20 @@ public static async Task> GetItemPictur } [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] - public static async Task>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevance( + public static async Task>, 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(StatusCodes.Status400BadRequest, "application/problem+json")] + public static async Task>, 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; @@ -243,25 +288,7 @@ public static async Task>> 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)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(pageIndex, pageSize, totalItems, itemsOnPage)); + return await GetAllItems(paginationRequest, services, null, typeId, brandId); } [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] @@ -270,38 +297,35 @@ public static async Task>> GetItemsByBrandId( [AsParameters] CatalogServices services, [Description("The brand of items to return")] int? brandId) { - var pageSize = paginationRequest.PageSize; - var pageIndex = paginationRequest.PageIndex; - - var root = (IQueryable)services.Context.CatalogItems; + return await GetAllItems(paginationRequest, services, null, null, brandId); + } - if (brandId is not null) + public static async Task, NotFound>> UpdateItemV1( + HttpContext httpContext, + [AsParameters] CatalogServices services, + CatalogItem productToUpdate) + { + if (productToUpdate?.Id == null) { - root = root.Where(ci => ci.CatalogBrandId == brandId); + return TypedResults.BadRequest(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(pageIndex, pageSize, totalItems, itemsOnPage)); + return await UpdateItem(httpContext, productToUpdate.Id, services, productToUpdate); } - public static async Task>> UpdateItem( + public static async Task, NotFound>> 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(new (){ - Detail = $"Item with id {productToUpdate.Id} not found." + Detail = $"Item with id {id} not found." }); } @@ -328,7 +352,7 @@ public static async Task>> UpdateItem( { await services.Context.SaveChangesAsync(); } - return TypedResults.Created($"/api/catalog/items/{productToUpdate.Id}"); + return TypedResults.Created($"/api/catalog/items/{id}"); } [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] diff --git a/src/Catalog.API/Catalog.API.json b/src/Catalog.API/Catalog.API.json index 9b59027a7..bce1b2a62 100644 --- a/src/Catalog.API/Catalog.API.json +++ b/src/Catalog.API/Catalog.API.json @@ -6,33 +6,25 @@ "version": "1.0" }, "paths": { - "/api/catalog/items": { + "/api/catalog/items/by": { "get": { "tags": [ "Items" ], - "summary": "List catalog items", - "description": "Get a paginated list of items in the catalog.", - "operationId": "ListItems", + "summary": "Batch get catalog items", + "description": "Get multiple items from the catalog", + "operationId": "BatchGetItems", "parameters": [ { - "name": "PageSize", - "in": "query", - "description": "Number of items to return in a single page of results", - "schema": { - "type": "integer", - "format": "int32", - "default": 10 - } - }, - { - "name": "PageIndex", + "name": "ids", "in": "query", - "description": "The index of the page of results to return", + "description": "List of ids for catalog items to return", "schema": { - "type": "integer", - "format": "int32", - "default": 0 + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } } }, { @@ -52,7 +44,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedItemsOfCatalogItem" + "type": "array", + "items": { + "$ref": "#/components/schemas/CatalogItem" + } } } } @@ -68,15 +63,27 @@ } } } - }, - "put": { + } + }, + "/api/catalog/items/{id}": { + "get": { "tags": [ "Items" ], - "summary": "Create or replace a catalog item", - "description": "Create or replace a catalog item", - "operationId": "UpdateItem", + "summary": "Get catalog item", + "description": "Get an item from the catalog", + "operationId": "GetItem", "parameters": [ + { + "name": "id", + "in": "path", + "description": "The catalog item id", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "api-version", "in": "query", @@ -88,21 +95,22 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CatalogItem" + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogItem" + } } } - } - }, - "responses": { - "201": { - "description": "Created" }, "404": { - "description": "Not Found", + "description": "Not Found" + }, + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -113,14 +121,24 @@ } } }, - "post": { + "delete": { "tags": [ "Catalog" ], - "summary": "Create a catalog item", - "description": "Create a new item in the catalog", - "operationId": "CreateItem", + "summary": "Delete catalog item", + "description": "Delete the specified catalog item", + "operationId": "DeleteItem", "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the catalog item to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "api-version", "in": "query", @@ -132,25 +150,111 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CatalogItem" - } + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/catalog/items/{id}/pic": { + "get": { + "tags": [ + "Items" + ], + "summary": "Get catalog item picture", + "description": "Get the picture for a catalog item", + "operationId": "GetItemPicture", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The catalog item id", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "1.0" } } - }, + ], "responses": { - "201": { - "description": "Created" + "404": { + "description": "Not Found" }, - "400": { - "description": "Bad Request", + "200": { + "description": "OK", "content": { - "application/problem+json": { + "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "type": "string", + "format": "byte" + } + }, + "image/png": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/gif": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/jpeg": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/bmp": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/tiff": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/wmf": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/jp2": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/svg+xml": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/webp": { + "schema": { + "type": "string", + "format": "byte" } } } @@ -158,27 +262,62 @@ } } }, - "/api/catalog/items/by": { + "/api/catalog/catalogtypes": { "get": { "tags": [ - "Items" + "Types" ], - "summary": "Batch get catalog items", - "description": "Get multiple items from the catalog", - "operationId": "BatchGetItems", + "summary": "List catalog item types", + "description": "Get a list of the types of catalog items", + "operationId": "ListItemTypes", "parameters": [ { - "name": "ids", + "name": "api-version", "in": "query", - "description": "List of ids for catalog items to return", + "description": "The API version, in the format 'major.minor'.", + "required": true, "schema": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" + "type": "string", + "example": "1.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CatalogType" + } + } } } }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/catalog/catalogbrands": { + "get": { + "tags": [ + "Brands" + ], + "summary": "List catalog item brands", + "description": "Get a list of the brands of catalog items", + "operationId": "ListItemBrands", + "parameters": [ { "name": "api-version", "in": "query", @@ -198,7 +337,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/CatalogItem" + "$ref": "#/components/schemas/CatalogBrand" } } } @@ -217,23 +356,77 @@ } } }, - "/api/catalog/items/{id}": { + "/api/catalog/items": { + "post": { + "tags": [ + "Catalog" + ], + "summary": "Create a catalog item", + "description": "Create a new item in the catalog", + "operationId": "CreateItem", + "parameters": [ + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "1.0" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogItem" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, "get": { "tags": [ "Items" ], - "summary": "Get catalog item", - "description": "Get an item from the catalog", - "operationId": "GetItem", + "summary": "List catalog items", + "description": "Get a paginated list of items in the catalog.", + "operationId": "ListItems", "parameters": [ { - "name": "id", - "in": "path", - "description": "The catalog item id", - "required": true, + "name": "PageSize", + "in": "query", + "description": "Number of items to return in a single page of results", "schema": { "type": "integer", - "format": "int32" + "format": "int32", + "default": 10 + } + }, + { + "name": "PageIndex", + "in": "query", + "description": "The index of the page of results to return", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 } }, { @@ -253,18 +446,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CatalogItem" + "$ref": "#/components/schemas/PaginatedItemsOfCatalogItem" } } } }, - "404": { - "description": "Not Found" - }, "400": { "description": "Bad Request", "content": { - "application/json": { + "application/problem+json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } @@ -273,24 +463,14 @@ } } }, - "delete": { + "put": { "tags": [ - "Catalog" + "Items" ], - "summary": "Delete catalog item", - "description": "Delete the specified catalog item", - "operationId": "DeleteItem", + "summary": "Create or replace a catalog item", + "description": "Create or replace a catalog item", + "operationId": "UpdateItem", "parameters": [ - { - "name": "id", - "in": "path", - "description": "The id of the catalog item to delete", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, { "name": "api-version", "in": "query", @@ -302,12 +482,38 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogItem" + } + } + } + }, "responses": { - "204": { - "description": "No Content" + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } }, "404": { - "description": "Not Found" + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } } } } @@ -386,108 +592,6 @@ } } }, - "/api/catalog/items/{id}/pic": { - "get": { - "tags": [ - "Items" - ], - "summary": "Get catalog item picture", - "description": "Get the picture for a catalog item", - "operationId": "GetItemPicture", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The catalog item id", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "api-version", - "in": "query", - "description": "The API version, in the format 'major.minor'.", - "required": true, - "schema": { - "type": "string", - "example": "1.0" - } - } - ], - "responses": { - "404": { - "description": "Not Found" - }, - "200": { - "description": "OK", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/png": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/gif": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/jpeg": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/bmp": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/tiff": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/wmf": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/jp2": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/svg+xml": { - "schema": { - "type": "string", - "format": "byte" - } - }, - "image/webp": { - "schema": { - "type": "string", - "format": "byte" - } - } - } - } - } - } - }, "/api/catalog/items/withsemanticrelevance/{text}": { "get": { "tags": [ @@ -719,100 +823,6 @@ } } } - }, - "/api/catalog/catalogtypes": { - "get": { - "tags": [ - "Types" - ], - "summary": "List catalog item types", - "description": "Get a list of the types of catalog items", - "operationId": "ListItemTypes", - "parameters": [ - { - "name": "api-version", - "in": "query", - "description": "The API version, in the format 'major.minor'.", - "required": true, - "schema": { - "type": "string", - "example": "1.0" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CatalogType" - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } - }, - "/api/catalog/catalogbrands": { - "get": { - "tags": [ - "Brands" - ], - "summary": "List catalog item brands", - "description": "Get a list of the brands of catalog items", - "operationId": "ListItemBrands", - "parameters": [ - { - "name": "api-version", - "in": "query", - "description": "The API version, in the format 'major.minor'.", - "required": true, - "schema": { - "type": "string", - "example": "1.0" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CatalogBrand" - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - } - } } }, "components": { @@ -964,14 +974,14 @@ { "name": "Catalog" }, - { - "name": "Search" - }, { "name": "Types" }, { "name": "Brands" + }, + { + "name": "Search" } ] } \ No newline at end of file diff --git a/src/Catalog.API/Catalog.API_v2.json b/src/Catalog.API/Catalog.API_v2.json new file mode 100644 index 000000000..e8fc6d8eb --- /dev/null +++ b/src/Catalog.API/Catalog.API_v2.json @@ -0,0 +1,791 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "eShop - Catalog HTTP API", + "description": "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample", + "version": "2.0" + }, + "paths": { + "/api/catalog/items/by": { + "get": { + "tags": [ + "Items" + ], + "summary": "Batch get catalog items", + "description": "Get multiple items from the catalog", + "operationId": "BatchGetItems", + "parameters": [ + { + "name": "ids", + "in": "query", + "description": "List of ids for catalog items to return", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CatalogItem" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/catalog/items/{id}": { + "get": { + "tags": [ + "Items" + ], + "summary": "Get catalog item", + "description": "Get an item from the catalog", + "operationId": "GetItem", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The catalog item id", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogItem" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Catalog" + ], + "summary": "Delete catalog item", + "description": "Delete the specified catalog item", + "operationId": "DeleteItem", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the catalog item to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found" + } + } + }, + "put": { + "tags": [ + "Items" + ], + "summary": "Create or replace a catalog item", + "description": "Create or replace a catalog item", + "operationId": "UpdateItem-V2", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the catalog item to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogItem" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/catalog/items/{id}/pic": { + "get": { + "tags": [ + "Items" + ], + "summary": "Get catalog item picture", + "description": "Get the picture for a catalog item", + "operationId": "GetItemPicture", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The catalog item id", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "responses": { + "404": { + "description": "Not Found" + }, + "200": { + "description": "OK", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/png": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/gif": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/jpeg": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/bmp": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/tiff": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/wmf": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/jp2": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/svg+xml": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "image/webp": { + "schema": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + }, + "/api/catalog/catalogtypes": { + "get": { + "tags": [ + "Types" + ], + "summary": "List catalog item types", + "description": "Get a list of the types of catalog items", + "operationId": "ListItemTypes", + "parameters": [ + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CatalogType" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/catalog/catalogbrands": { + "get": { + "tags": [ + "Brands" + ], + "summary": "List catalog item brands", + "description": "Get a list of the brands of catalog items", + "operationId": "ListItemBrands", + "parameters": [ + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CatalogBrand" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/catalog/items": { + "post": { + "tags": [ + "Catalog" + ], + "summary": "Create a catalog item", + "description": "Create a new item in the catalog", + "operationId": "CreateItem", + "parameters": [ + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogItem" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "get": { + "tags": [ + "Items" + ], + "summary": "List catalog items", + "description": "Get a paginated list of items in the catalog.", + "operationId": "ListItems-V2", + "parameters": [ + { + "name": "PageSize", + "in": "query", + "description": "Number of items to return in a single page of results", + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "PageIndex", + "in": "query", + "description": "The index of the page of results to return", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "name", + "in": "query", + "description": "The name of the item to return", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "description": "The type of items to return", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "brand", + "in": "query", + "description": "The brand of items to return", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedItemsOfCatalogItem" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/catalog/items/withsemanticrelevance": { + "get": { + "tags": [ + "Search" + ], + "summary": "Search catalog for relevant items", + "description": "Search the catalog for items related to the specified text", + "operationId": "GetRelevantItems-V2", + "parameters": [ + { + "name": "PageSize", + "in": "query", + "description": "Number of items to return in a single page of results", + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "PageIndex", + "in": "query", + "description": "The index of the page of results to return", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "text", + "in": "query", + "description": "The text string to use when search for related items in the catalog", + "required": true, + "schema": { + "minLength": 1, + "type": "string" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The API version, in the format 'major.minor'.", + "required": true, + "schema": { + "type": "string", + "example": "2.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedItemsOfCatalogItem" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "CatalogBrand": { + "required": [ + "brand" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "brand": { + "type": "string", + "nullable": true + } + } + }, + "CatalogItem": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "pictureFileName": { + "type": "string" + }, + "catalogTypeId": { + "type": "integer", + "format": "int32" + }, + "catalogType": { + "$ref": "#/components/schemas/CatalogType" + }, + "catalogBrandId": { + "type": "integer", + "format": "int32" + }, + "catalogBrand": { + "$ref": "#/components/schemas/CatalogBrand" + }, + "availableStock": { + "type": "integer", + "format": "int32" + }, + "restockThreshold": { + "type": "integer", + "format": "int32" + }, + "maxStockThreshold": { + "type": "integer", + "format": "int32" + }, + "onReorder": { + "type": "boolean" + } + } + }, + "CatalogType": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "nullable": true + } + } + }, + "PaginatedItemsOfCatalogItem": { + "required": [ + "pageIndex", + "pageSize", + "count", + "data" + ], + "type": "object", + "properties": { + "pageIndex": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "count": { + "type": "integer", + "format": "int64" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CatalogItem" + }, + "nullable": true + } + } + }, + "ProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "status": { + "type": "integer", + "format": "int32" + }, + "detail": { + "type": "string" + }, + "instance": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "Items" + }, + { + "name": "Catalog" + }, + { + "name": "Types" + }, + { + "name": "Brands" + }, + { + "name": "Search" + } + ] +} \ No newline at end of file diff --git a/src/Catalog.API/Program.cs b/src/Catalog.API/Program.cs index 5ab2e2e33..179de5bfa 100644 --- a/src/Catalog.API/Program.cs +++ b/src/Catalog.API/Program.cs @@ -17,8 +17,7 @@ app.UseStatusCodePages(); -app.NewVersionedApi("Catalog") - .MapCatalogApiV1(); +app.MapCatalogApi(); app.UseDefaultOpenApi(); app.Run(); diff --git a/src/ClientApp/Services/Catalog/CatalogService.cs b/src/ClientApp/Services/Catalog/CatalogService.cs index ca81f1734..174690457 100644 --- a/src/ClientApp/Services/Catalog/CatalogService.cs +++ b/src/ClientApp/Services/Catalog/CatalogService.cs @@ -9,8 +9,8 @@ namespace eShop.ClientApp.Services.Catalog; public class CatalogService : ICatalogService { private const string ApiUrlBase = "api/catalog"; - private const string ApiVersion = "api-version=1.0"; - + private const string ApiVersion = "api-version=2.0"; + private readonly IFixUriService _fixUriService; private readonly IRequestProvider _requestProvider; private readonly ISettingsService _settingsService; @@ -26,7 +26,7 @@ public CatalogService(ISettingsService settingsService, IRequestProvider request public async Task> FilterAsync(int catalogBrandId, int catalogTypeId) { var uri = UriHelper.CombineUri(_settingsService.GatewayCatalogEndpointBase, - $"{ApiUrlBase}/items/type/{catalogTypeId}/brand/{catalogBrandId}?PageSize=100&PageIndex=0&{ApiVersion}"); + $"{ApiUrlBase}//items?type={catalogTypeId}&brand={catalogBrandId}&PageSize=100&PageIndex=0&{ApiVersion}"); var catalog = await _requestProvider.GetAsync(uri).ConfigureAwait(false); diff --git a/src/ClientApp/Services/FixUri/FixUriService.cs b/src/ClientApp/Services/FixUri/FixUriService.cs index 4e79159ed..d518b7688 100644 --- a/src/ClientApp/Services/FixUri/FixUriService.cs +++ b/src/ClientApp/Services/FixUri/FixUriService.cs @@ -9,8 +9,8 @@ namespace eShop.ClientApp.Services.FixUri; public class FixUriService : IFixUriService { - private const string ApiVersion = "api-version=1.0"; - + private const string ApiVersion = "api-version=2.0"; + private readonly ISettingsService _settingsService; private readonly Regex IpRegex = new(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"); diff --git a/src/HybridApp/Services/CatalogService.cs b/src/HybridApp/Services/CatalogService.cs index bd8e204ba..ed7d96c42 100644 --- a/src/HybridApp/Services/CatalogService.cs +++ b/src/HybridApp/Services/CatalogService.cs @@ -12,65 +12,58 @@ public class CatalogService(HttpClient httpClient) : ICatalogService public Task GetCatalogItem(int id) { - var uri = $"{remoteServiceBaseUrl}items/{id}?api-version=1.0"; + var uri = $"{remoteServiceBaseUrl}items/{id}?api-version=2.0"; return httpClient.GetFromJsonAsync(uri); } public async Task GetCatalogItems(int pageIndex, int pageSize, int? brand, int? type) { var uri = GetAllCatalogItemsUri(remoteServiceBaseUrl, pageIndex, pageSize, brand, type); - var result = await httpClient.GetFromJsonAsync($"{uri}&api-version=1.0"); + var result = await httpClient.GetFromJsonAsync($"{uri}&api-version=2.0"); return result!; } public async Task> GetCatalogItems(IEnumerable ids) { - var uri = $"{remoteServiceBaseUrl}items/by?ids={string.Join("&ids=", ids)}&api-version=1.0"; + var uri = $"{remoteServiceBaseUrl}items/by?ids={string.Join("&ids=", ids)}&api-version=2.0"; var result = await httpClient.GetFromJsonAsync>(uri); return result!; } public Task GetCatalogItemsWithSemanticRelevance(int page, int take, string text) { - var url = $"{remoteServiceBaseUrl}items/withsemanticrelevance/{HttpUtility.UrlEncode(text)}?pageIndex={page}&pageSize={take}&api-version=1.0"; + var url = $"{remoteServiceBaseUrl}items/withsemanticrelevance?text={HttpUtility.UrlEncode(text)}&pageIndex={page}&pageSize={take}&api-version=2.0"; var result = httpClient.GetFromJsonAsync(url); return result!; } public async Task> GetBrands() { - var uri = $"{remoteServiceBaseUrl}catalogBrands?api-version=1.0"; + var uri = $"{remoteServiceBaseUrl}catalogBrands?api-version=2.0"; var result = await httpClient.GetFromJsonAsync(uri); return result!; } public async Task> GetTypes() { - var uri = $"{remoteServiceBaseUrl}catalogTypes?api-version=1.0"; + var uri = $"{remoteServiceBaseUrl}catalogTypes?api-version=2.0"; var result = await httpClient.GetFromJsonAsync(uri); return result!; } private static string GetAllCatalogItemsUri(string baseUri, int pageIndex, int pageSize, int? brand, int? type) { - string filterQs; + string filterQs = string.Empty; if (type.HasValue) { - var brandQs = brand.HasValue ? brand.Value.ToString() : string.Empty; - filterQs = $"/type/{type.Value}/brand/{brandQs}"; - - } - else if (brand.HasValue) - { - var brandQs = brand.HasValue ? brand.Value.ToString() : string.Empty; - filterQs = $"/type/all/brand/{brandQs}"; + filterQs += $"type={type.Value}&"; } - else + if (brand.HasValue) { - filterQs = string.Empty; + filterQs += $"brand={brand.Value}&"; } - return $"{baseUri}items{filterQs}?pageIndex={pageIndex}&pageSize={pageSize}&api-version=1.0"; + return $"{baseUri}items?{filterQs}pageIndex={pageIndex}&pageSize={pageSize}&api-version=2.0"; } } diff --git a/src/HybridApp/Services/ProductImageUrlProvider.cs b/src/HybridApp/Services/ProductImageUrlProvider.cs index ea9b483e7..ab07365b8 100644 --- a/src/HybridApp/Services/ProductImageUrlProvider.cs +++ b/src/HybridApp/Services/ProductImageUrlProvider.cs @@ -5,5 +5,5 @@ namespace eShop.HybridApp.Services; public class ProductImageUrlProvider : IProductImageUrlProvider { public string GetProductImageUrl(int productId) - => $"{MauiProgram.MobileBffHost}api/catalog/items/{productId}/pic?api-version=1.0"; + => $"{MauiProgram.MobileBffHost}api/catalog/items/{productId}/pic?api-version=2.0"; } diff --git a/src/Mobile.Bff.Shopping/appsettings.json b/src/Mobile.Bff.Shopping/appsettings.json index 653633129..1e0667aeb 100644 --- a/src/Mobile.Bff.Shopping/appsettings.json +++ b/src/Mobile.Bff.Shopping/appsettings.json @@ -17,7 +17,7 @@ "QueryParameters": [ { "Name": "api-version", - "Values": [ "1.0", "1" ], + "Values": [ "1.0", "1", "2.0" ], "Mode": "Exact" } ] @@ -35,7 +35,7 @@ "QueryParameters": [ { "Name": "api-version", - "Values": [ "1.0", "1" ], + "Values": [ "1.0", "1", "2.0" ], "Mode": "Exact" } ] @@ -53,7 +53,7 @@ "QueryParameters": [ { "Name": "api-version", - "Values": [ "1.0", "1" ], + "Values": [ "1.0", "1", "2.0" ], "Mode": "Exact" } ] @@ -82,7 +82,7 @@ } ] }, - "route5": { + "route5v1": { "ClusterId": "catalog", "Match": { "Path": "/catalog-api/api/catalog/items/withsemanticrelevance/{text}", @@ -100,6 +100,24 @@ } ] }, + "route5": { + "ClusterId": "catalog", + "Match": { + "Path": "/catalog-api/api/catalog/items/withsemanticrelevance", + "QueryParameters": [ + { + "Name": "api-version", + "Values": [ "2.0" ], + "Mode": "Exact" + } + ] + }, + "Transforms": [ + { + "PathRemovePrefix": "/catalog-api" + } + ] + }, "route6": { "ClusterId": "catalog", "Match": { @@ -143,7 +161,7 @@ "QueryParameters": [ { "Name": "api-version", - "Values": [ "1.0", "1" ], + "Values": [ "1.0", "1", "2.0" ], "Mode": "Exact" } ] @@ -161,7 +179,7 @@ "QueryParameters": [ { "Name": "api-version", - "Values": [ "1.0", "1" ], + "Values": [ "1.0", "1", "2.0" ], "Mode": "Exact" } ] @@ -179,7 +197,7 @@ "QueryParameters": [ { "Name": "api-version", - "Values": [ "1.0", "1" ], + "Values": [ "1.0", "1", "2.0" ], "Mode": "Exact" } ] @@ -197,7 +215,7 @@ "QueryParameters": [ { "Name": "api-version", - "Values": [ "1.0", "1" ], + "Values": [ "1.0", "1", "2.0" ], "Mode": "Exact" } ] diff --git a/src/WebApp/Extensions/Extensions.cs b/src/WebApp/Extensions/Extensions.cs index d33dd7e44..14802f8e1 100644 --- a/src/WebApp/Extensions/Extensions.cs +++ b/src/WebApp/Extensions/Extensions.cs @@ -37,7 +37,7 @@ public static void AddApplicationServices(this IHostApplicationBuilder builder) .AddAuthToken(); builder.Services.AddHttpClient(o => o.BaseAddress = new("http://catalog-api")) - .AddApiVersion(1.0) + .AddApiVersion(2.0) .AddAuthToken(); builder.Services.AddHttpClient(o => o.BaseAddress = new("http://ordering-api")) diff --git a/src/WebApp/Services/ProductImageUrlProvider.cs b/src/WebApp/Services/ProductImageUrlProvider.cs index f40c4f2aa..3b898d355 100644 --- a/src/WebApp/Services/ProductImageUrlProvider.cs +++ b/src/WebApp/Services/ProductImageUrlProvider.cs @@ -5,5 +5,5 @@ namespace eShop.WebApp.Services; public class ProductImageUrlProvider : IProductImageUrlProvider { public string GetProductImageUrl(int productId) - => $"product-images/{productId}?api-version=1.0"; + => $"product-images/{productId}?api-version=2.0"; } diff --git a/src/WebAppComponents/Services/CatalogService.cs b/src/WebAppComponents/Services/CatalogService.cs index 2bc89a34f..1a1945483 100644 --- a/src/WebAppComponents/Services/CatalogService.cs +++ b/src/WebAppComponents/Services/CatalogService.cs @@ -30,7 +30,7 @@ public async Task> GetCatalogItems(IEnumerable ids) public Task GetCatalogItemsWithSemanticRelevance(int page, int take, string text) { - var url = $"{remoteServiceBaseUrl}items/withsemanticrelevance/{HttpUtility.UrlEncode(text)}?pageIndex={page}&pageSize={take}"; + var url = $"{remoteServiceBaseUrl}items/withsemanticrelevance?text={HttpUtility.UrlEncode(text)}&pageIndex={page}&pageSize={take}"; var result = httpClient.GetFromJsonAsync(url); return result!; } @@ -51,24 +51,17 @@ public async Task> GetTypes() private static string GetAllCatalogItemsUri(string baseUri, int pageIndex, int pageSize, int? brand, int? type) { - string filterQs; + string filterQs = string.Empty; if (type.HasValue) { - var brandQs = brand.HasValue ? brand.Value.ToString() : string.Empty; - filterQs = $"/type/{type.Value}/brand/{brandQs}"; - - } - else if (brand.HasValue) - { - var brandQs = brand.HasValue ? brand.Value.ToString() : string.Empty; - filterQs = $"/type/all/brand/{brandQs}"; + filterQs += $"type={type.Value}&"; } - else + if (brand.HasValue) { - filterQs = string.Empty; + filterQs += $"brand={brand.Value}&"; } - return $"{baseUri}items{filterQs}?pageIndex={pageIndex}&pageSize={pageSize}"; + return $"{baseUri}items?{filterQs}pageIndex={pageIndex}&pageSize={pageSize}"; } } diff --git a/src/eShop.ServiceDefaults/OpenApi.Extensions.cs b/src/eShop.ServiceDefaults/OpenApi.Extensions.cs index a25650264..7a60753b8 100644 --- a/src/eShop.ServiceDefaults/OpenApi.Extensions.cs +++ b/src/eShop.ServiceDefaults/OpenApi.Extensions.cs @@ -58,7 +58,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi( // the default format will just be ApiVersion.ToString(); for example, 1.0. // this will format the version as "'v'major[.minor][-status]" var versioned = apiVersioning.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV"); - string[] versions = ["v1"]; + string[] versions = ["v1", "v2"]; foreach (var description in versions) { builder.Services.AddOpenApi(description, options => diff --git a/src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs b/src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs index fa44731ec..71adbad22 100644 --- a/src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs +++ b/src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -158,7 +159,14 @@ public static OpenApiOptions ApplyApiVersionDescription(this OpenApiOptions opti if (apiVersionParameter is not null) { apiVersionParameter.Description = "The API version, in the format 'major.minor'."; - apiVersionParameter.Schema.Example = new OpenApiString("1.0"); + switch (context.DocumentName) { + case "v1": + apiVersionParameter.Schema.Example = new OpenApiString("1.0"); + break; + case "v2": + apiVersionParameter.Schema.Example = new OpenApiString("2.0"); + break; + } } return Task.CompletedTask; }); diff --git a/tests/Catalog.FunctionalTests/CatalogApiTests.cs b/tests/Catalog.FunctionalTests/CatalogApiTests.cs index d526173fb..f4af46f20 100644 --- a/tests/Catalog.FunctionalTests/CatalogApiTests.cs +++ b/tests/Catalog.FunctionalTests/CatalogApiTests.cs @@ -10,20 +10,26 @@ namespace eShop.Catalog.FunctionalTests; public sealed class CatalogApiTests : IClassFixture { private readonly WebApplicationFactory _webApplicationFactory; - private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); public CatalogApiTests(CatalogApiFixture fixture) { - var handler = new ApiVersionHandler(new QueryStringApiVersionWriter(), new ApiVersion(1.0)); - _webApplicationFactory = fixture; - _httpClient = _webApplicationFactory.CreateDefaultClient(handler); } - [Fact] - public async Task GetCatalogItemsRespectsPageSize() + private HttpClient CreateHttpClient(ApiVersion apiVersion) + { + var handler = new ApiVersionHandler(new QueryStringApiVersionWriter(), apiVersion); + return _webApplicationFactory.CreateDefaultClient(handler); + } + + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetCatalogItemsRespectsPageSize(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act var response = await _httpClient.GetAsync("/api/catalog/items?pageIndex=0&pageSize=5"); @@ -38,9 +44,13 @@ public async Task GetCatalogItemsRespectsPageSize() Assert.Equal(5, result.PageSize); } - [Fact] - public async Task UpdateCatalogItemWorksWithoutPriceUpdate() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task UpdateCatalogItemWorksWithoutPriceUpdate(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act - 1 var response = await _httpClient.GetAsync("/api/catalog/items/1"); response.EnsureSuccessStatusCode(); @@ -50,7 +60,12 @@ public async Task UpdateCatalogItemWorksWithoutPriceUpdate() // Act - 2 var priorAvailableStock = itemToUpdate.AvailableStock; itemToUpdate.AvailableStock -= 1; - response = await _httpClient.PutAsJsonAsync("/api/catalog/items", itemToUpdate); + response = version switch + { + 1.0 => await _httpClient.PutAsJsonAsync("/api/catalog/items", itemToUpdate), + 2.0 => await _httpClient.PutAsJsonAsync($"/api/catalog/items/{itemToUpdate.Id}", itemToUpdate), + _ => throw new ArgumentOutOfRangeException(nameof(version), version, null) + }; response.EnsureSuccessStatusCode(); // Act - 3 @@ -64,9 +79,13 @@ public async Task UpdateCatalogItemWorksWithoutPriceUpdate() Assert.NotEqual(priorAvailableStock, updatedItem.AvailableStock); } - [Fact] - public async Task UpdateCatalogItemWorksWithPriceUpdate() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task UpdateCatalogItemWorksWithPriceUpdate(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act - 1 var response = await _httpClient.GetAsync("/api/catalog/items/1"); response.EnsureSuccessStatusCode(); @@ -77,7 +96,12 @@ public async Task UpdateCatalogItemWorksWithPriceUpdate() var priorAvailableStock = itemToUpdate.AvailableStock; itemToUpdate.AvailableStock -= 1; itemToUpdate.Price = 1.99m; - response = await _httpClient.PutAsJsonAsync("/api/catalog/items", itemToUpdate); + response = version switch + { + 1.0 => await _httpClient.PutAsJsonAsync("/api/catalog/items", itemToUpdate), + 2.0 => await _httpClient.PutAsJsonAsync($"/api/catalog/items/{itemToUpdate.Id}", itemToUpdate), + _ => throw new ArgumentOutOfRangeException(nameof(version), version, null) + }; response.EnsureSuccessStatusCode(); // Act - 3 @@ -92,44 +116,61 @@ public async Task UpdateCatalogItemWorksWithPriceUpdate() Assert.NotEqual(priorAvailableStock, updatedItem.AvailableStock); } - [Fact] - public async Task GetCatalogItemsbyIds() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetCatalogItemsbyIds(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act var response = await _httpClient.GetAsync("/api/catalog/items/by?ids=1&ids=2&ids=3"); - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions); - // Assert 3 items + // Assert 3 items Assert.Equal(3, result.Count); } - [Fact] - public async Task GetCatalogItemWithId() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetCatalogItemWithId(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act var response = await _httpClient.GetAsync("/api/catalog/items/2"); - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize(body, _jsonSerializerOptions); - // Assert + // Assert Assert.Equal(2, result.Id); Assert.NotNull(result); } - [Fact] - public async Task GetCatalogItemWithExactName() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetCatalogItemWithExactName(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act - var response = await _httpClient.GetAsync("api/catalog/items/by/Wanderer%20Black%20Hiking%20Boots?PageSize=5&PageIndex=0"); + var response = version switch + { + 1.0 => await _httpClient.GetAsync("api/catalog/items/by/Wanderer%20Black%20Hiking%20Boots?PageSize=5&PageIndex=0"), + 2.0 => await _httpClient.GetAsync("api/catalog/items?name=Wanderer%20Black%20Hiking%20Boots&PageSize=5&PageIndex=0"), + _ => throw new ArgumentOutOfRangeException(nameof(version), version, null) + }; - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions); @@ -142,14 +183,22 @@ public async Task GetCatalogItemWithExactName() Assert.Equal("Wanderer Black Hiking Boots", result.Data.ToList().FirstOrDefault().Name); } - // searching partial name Alpine - [Fact] - public async Task GetCatalogItemWithPartialName() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetCatalogItemWithPartialName(double version) { - // Act - var response = await _httpClient.GetAsync("api/catalog/items/by/Alpine?PageSize=5&PageIndex=0"); + var _httpClient = CreateHttpClient(new ApiVersion(version)); + + // Act + var response = version switch + { + 1.0 => await _httpClient.GetAsync("api/catalog/items/by/Alpine?PageSize=5&PageIndex=0"), + 2.0 => await _httpClient.GetAsync("api/catalog/items?name=Alpine&PageSize=5&PageIndex=0"), + _ => throw new ArgumentOutOfRangeException(nameof(version), version, null) + }; - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions); @@ -162,52 +211,72 @@ public async Task GetCatalogItemWithPartialName() Assert.Contains("Alpine", result.Data.ToList().FirstOrDefault().Name); } - - [Fact] - public async Task GetCatalogItemPicWithId() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetCatalogItemPicWithId(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act var response = await _httpClient.GetAsync("api/catalog/items/1/pic"); - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var result = response.Content.Headers.ContentType.MediaType; - // Assert + // Assert Assert.Equal("image/webp", result); } - - [Fact] - public async Task GetCatalogItemWithsemanticrelevance() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetCatalogItemWithsemanticrelevance(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act - var response = await _httpClient.GetAsync("api/catalog/items/withsemanticrelevance/Wanderer?PageSize=5&PageIndex=0"); + var response = version switch + { + 1.0 => await _httpClient.GetAsync("api/catalog/items/withsemanticrelevance/Wanderer?PageSize=5&PageIndex=0"), + 2.0 => await _httpClient.GetAsync("api/catalog/items/withsemanticrelevance?text=Wanderer&PageSize=5&PageIndex=0"), + _ => throw new ArgumentOutOfRangeException(nameof(version), version, null) + }; - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions); - // Assert + // Assert Assert.Equal(1, result.Count); Assert.NotNull(result.Data); Assert.Equal(0, result.PageIndex); Assert.Equal(5, result.PageSize); } - [Fact] - public async Task GetCatalogItemWithTypeIdBrandId() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetCatalogItemWithTypeIdBrandId(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act - var response = await _httpClient.GetAsync("api/catalog/items/type/3/brand/3?PageSize=5&PageIndex=0"); + var response = version switch + { + 1.0 => await _httpClient.GetAsync("api/catalog/items/type/3/brand/3?PageSize=5&PageIndex=0"), + 2.0 => await _httpClient.GetAsync("api/catalog/items?type=3&brand=3&PageSize=5&PageIndex=0"), + _ => throw new ArgumentOutOfRangeException(nameof(version), version, null) + }; - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions); - // Assert + // Assert Assert.NotNull(result.Data); Assert.Equal(4, result.Count); Assert.Equal(0, result.PageIndex); @@ -216,18 +285,27 @@ public async Task GetCatalogItemWithTypeIdBrandId() Assert.Equal(3, result.Data.ToList().FirstOrDefault().CatalogBrandId); } - [Fact] - public async Task GetAllCatalogTypeItemWithBrandId() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetAllCatalogTypeItemWithBrandId(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act - var response = await _httpClient.GetAsync("api/catalog/items/type/all/brand/3?PageSize=5&PageIndex=0"); + var response = version switch + { + 1.0 => await _httpClient.GetAsync("api/catalog/items/type/all/brand/3?PageSize=5&PageIndex=0"), + 2.0 => await _httpClient.GetAsync("api/catalog/items?brand=3&PageSize=5&PageIndex=0"), + _ => throw new ArgumentOutOfRangeException(nameof(version), version, null) + }; // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions); - // Assert + // Assert Assert.NotNull(result.Data); Assert.Equal(11, result.Count); Assert.Equal(0, result.PageIndex); @@ -235,44 +313,62 @@ public async Task GetAllCatalogTypeItemWithBrandId() Assert.Equal(3, result.Data.ToList().FirstOrDefault().CatalogBrandId); } - [Fact] - public async Task GetAllCatalogTypes() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetAllCatalogTypes(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act var response = await _httpClient.GetAsync("api/catalog/catalogtypes"); - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions); - // Assert + // Assert Assert.Equal(8, result.Count); Assert.NotNull(result); } - [Fact] - public async Task GetAllCatalogBrands() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task GetAllCatalogBrands(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + // Act var response = await _httpClient.GetAsync("api/catalog/catalogbrands"); - // Arrange + // Arrange response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions); - // Assert + // Assert Assert.Equal(13, result.Count); Assert.NotNull(result); } - [Fact] - public async Task AddCatalogItem() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task AddCatalogItem(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + + var id = version switch { + 1.0 => 10015, + 2.0 => 10016, + _ => 0 + }; + // Act - 1 var bodyContent = new CatalogItem { - Id = 10015, + Id = id, Name = "TestCatalog1", Description = "Test catalog description 1", Price = 11000.08m, @@ -290,7 +386,7 @@ public async Task AddCatalogItem() response.EnsureSuccessStatusCode(); // Act - 2 - response = await _httpClient.GetAsync("/api/catalog/items/10015"); + response = await _httpClient.GetAsync($"/api/catalog/items/{id}"); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); var addedItem = JsonSerializer.Deserialize(body, _jsonSerializerOptions); @@ -300,15 +396,25 @@ public async Task AddCatalogItem() } - [Fact] - public async Task DeleteCatalogItem() + [Theory] + [InlineData(1.0)] + [InlineData(2.0)] + public async Task DeleteCatalogItem(double version) { + var _httpClient = CreateHttpClient(new ApiVersion(version)); + + var id = version switch { + 1.0 => 5, + 2.0 => 6, + _ => 0 + }; + //Act - 1 - var response = await _httpClient.DeleteAsync("/api/catalog/items/5"); + var response = await _httpClient.DeleteAsync($"/api/catalog/items/{id}"); response.EnsureSuccessStatusCode(); // Act - 2 - var response1 = await _httpClient.GetAsync("/api/catalog/items/5"); + var response1 = await _httpClient.GetAsync($"/api/catalog/items/{id}"); var responseStatus = response1.StatusCode; // Assert - 1