diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/MethodInfoExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/MethodInfoExtensions.cs index 2ee46822b8..2ae8f95e13 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/MethodInfoExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/MethodInfoExtensions.cs @@ -9,13 +9,69 @@ public static MethodInfo GetUnderlyingGenericTypeMethod(this MethodInfo construc { var constructedType = constructedTypeMethod.DeclaringType; var genericTypeDefinition = constructedType.GetGenericTypeDefinition(); + var genericArguments = constructedType.GenericTypeArguments; + + var constructedTypeParameters = constructedTypeMethod.GetParameters(); // Retrieve list of candidate methods that match name and parameter count var candidateMethods = genericTypeDefinition.GetMethods() .Where(m => { - return (m.Name == constructedTypeMethod.Name) - && (m.GetParameters().Length == constructedTypeMethod.GetParameters().Length); + var genericTypeDefinitionParameters = m.GetParameters(); + if (m.Name == constructedTypeMethod.Name && genericTypeDefinitionParameters.Length == constructedTypeParameters.Length) + { + for (var i = 0; i < genericTypeDefinitionParameters.Length; i++) + { + if (genericTypeDefinitionParameters[i].ParameterType.IsArray && constructedTypeParameters[i].ParameterType.IsArray) + { + var genericTypeDefinitionElement = genericTypeDefinitionParameters[i].ParameterType.GetElementType(); + var constructedTypeDefinitionElement = constructedTypeParameters[i].ParameterType.GetElementType(); + if (genericTypeDefinitionElement.IsGenericParameter && genericArguments.Any(p => p == constructedTypeDefinitionElement)) + { + continue; + } + else if (genericTypeDefinitionElement != constructedTypeDefinitionElement) + { + return false; + } + } + else if (genericTypeDefinitionParameters[i].ParameterType.IsConstructedGenericType && constructedTypeParameters[i].ParameterType.IsConstructedGenericType) + { + if (genericTypeDefinitionParameters[i].ParameterType.GetGenericTypeDefinition() != constructedTypeParameters[i].ParameterType.GetGenericTypeDefinition()) + { + return false; + } + var genericTypeDefinitionArguments = genericTypeDefinitionParameters[i].ParameterType.GetGenericArguments(); + var constructedDefinitionArguments = constructedTypeParameters[i].ParameterType.GetGenericArguments(); + if (genericTypeDefinitionArguments.Length != constructedDefinitionArguments.Length) + { + return false; + } + for (var j = 0; j < genericTypeDefinitionArguments.Length; j++) + { + if (genericTypeDefinitionArguments[j].IsGenericParameter && genericArguments.Any(p => p == constructedDefinitionArguments[j])) + { + continue; + } + else if (genericTypeDefinitionArguments[j] != constructedDefinitionArguments[j]) + { + return false; + } + } + continue; + } + else if (genericTypeDefinitionParameters[i].ParameterType.IsGenericParameter && genericArguments.Any(p => p == constructedTypeParameters[i].ParameterType)) + { + continue; + } + else if (genericTypeDefinitionParameters[i].ParameterType != constructedTypeParameters[i].ParameterType) + { + return false; + } + } + return true; + } + return false; }); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs index 8220746f76..6cc5395640 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs @@ -50,7 +50,11 @@ public static string GetMemberNameForFieldOrProperty(MemberInfo fieldOrPropertyI private static string QualifiedNameFor(Type type, bool expandGenericArgs = false) { if (type.IsArray) - return $"{QualifiedNameFor(type.GetElementType(), expandGenericArgs)}[]"; + { + var elementType = type.GetElementType(); + return elementType.IsGenericParameter ? $"`{elementType.GenericParameterPosition}[]" : $"{QualifiedNameFor(type.GetElementType(), expandGenericArgs)}[]"; + } + var builder = new StringBuilder(); diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=GenericControllers.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=GenericControllers.Startup_swaggerRequestUri=v1.verified.txt index eaa1e8eb16..825a647384 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=GenericControllers.Startup_swaggerRequestUri=v1.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=GenericControllers.Startup_swaggerRequestUri=v1.verified.txt @@ -5,7 +5,7 @@ "version": "1" }, "paths": { - "/{tennantId}/orders": { + "/{tenantId}/orders": { "post": { "tags": [ "Orders" @@ -13,7 +13,7 @@ "summary": "Creates a resource", "parameters": [ { - "name": "tennantId", + "name": "tenantId", "in": "path", "required": true, "schema": { @@ -57,9 +57,226 @@ } } } + }, + "delete": { + "tags": [ + "Orders" + ], + "summary": "Delete by Ids", + "parameters": [ + { + "name": "tenantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "deleting Ids", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deleted", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "404": { + "description": "Failed" + } + } + } + }, + "/{tenantId}/orders/DeleteById": { + "delete": { + "tags": [ + "Orders" + ], + "summary": "Delete by Id", + "parameters": [ + { + "name": "tenantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "deleting Id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deleted", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "404": { + "description": "Failed" + } + } + } + }, + "/{tenantId}/orders/Delete/List": { + "delete": { + "tags": [ + "Orders" + ], + "summary": "Delete by Id List", + "parameters": [ + { + "name": "tenantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "deleting Ids", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deleted", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "404": { + "description": "Failed" + } + } } }, - "/{tennantId}/products": { + "/{tenantId}/products": { "post": { "tags": [ "Products" @@ -67,7 +284,7 @@ "summary": "Creates a resource", "parameters": [ { - "name": "tennantId", + "name": "tenantId", "in": "path", "required": true, "schema": { @@ -111,6 +328,223 @@ } } } + }, + "delete": { + "tags": [ + "Products" + ], + "summary": "Delete by Ids", + "parameters": [ + { + "name": "tenantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "deleting Ids", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deleted", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "404": { + "description": "Failed" + } + } + } + }, + "/{tenantId}/products/DeleteById": { + "delete": { + "tags": [ + "Products" + ], + "summary": "Delete by Id", + "parameters": [ + { + "name": "tenantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "deleting Id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deleted", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "404": { + "description": "Failed" + } + } + } + }, + "/{tenantId}/products/Delete/List": { + "delete": { + "tags": [ + "Products" + ], + "summary": "Delete by Id List", + "parameters": [ + { + "name": "tenantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "deleting Ids", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deleted", + "content": { + "text/plain": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "404": { + "description": "Failed" + } + } } } }, diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeGenericResourceController.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeGenericResourceController.cs new file mode 100644 index 0000000000..9233a71fa1 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeGenericResourceController.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using Microsoft.AspNetCore.Mvc; + +namespace Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures; + +internal class FakeGenericResourceController + where T : class +{ + public byte DifferentMethodsSignatures([FromBody, Required] T t, CancellationToken cancellationToken) => default; + public sbyte DifferentMethodsSignatures([FromBody, Required] string key, T t) => default; + public short DifferentMethodsSignatures([FromBody, Required] T[] arrayOfTs) => default; + public ushort DifferentMethodsSignatures([FromBody, Required] string[] arrayOfStrings) => default; + public int DifferentMethodsSignatures([FromBody, Required] List listOfTs) => default; + public uint DifferentMethodsSignatures([FromBody, Required] List listOfStrings) => default; + public long DifferentMethodsSignatures([FromBody, Required] Dictionary dictionaryOfTs) => default; + public ulong DifferentMethodsSignatures([FromBody, Required] IEnumerable iEnumerableOfTs) => default; +} + +internal class NonGenericResourceController : FakeGenericResourceController; diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlMethodInfoExtensionsTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlMethodInfoExtensionsTests.cs new file mode 100644 index 0000000000..cea27fa58e --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlMethodInfoExtensionsTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures; +using Xunit; + +namespace Swashbuckle.AspNetCore.SwaggerGen.Test; + +public class XmlMethodInfoExtensionsTests +{ + [Theory] + [ClassData(typeof(DifferentMethodsSignaturesData))] + public void DifferentMethodsSignatures_ShouldBeExpected(MethodInfo methodInfo, string[] expectedParameterNames, Type expectedReturnType) + { + var underlyingGenericMethod = methodInfo.GetUnderlyingGenericTypeMethod(); + Assert.NotNull(underlyingGenericMethod); + var underlyingGenericMethodParameters = underlyingGenericMethod.GetParameters(); + Assert.NotNull(underlyingGenericMethodParameters); + Assert.NotEmpty(underlyingGenericMethodParameters); + Assert.Equal(expectedParameterNames, underlyingGenericMethodParameters.Select(s => s.Name)); + Assert.NotNull(underlyingGenericMethod.ReturnType); + Assert.Equal(expectedReturnType, underlyingGenericMethod.ReturnType); + } + + public class DifferentMethodsSignaturesData : TheoryData + { + public DifferentMethodsSignaturesData() + { + var methods = typeof(NonGenericResourceController) + .GetMethods() + .Where(s => s.Name == nameof(NonGenericResourceController.DifferentMethodsSignatures)); + + foreach (var method in methods) + { + Add(method, method.GetParameters().Select(p => p.Name).ToArray(), method.ReturnType); + } + } + } + +} diff --git a/test/WebSites/GenericControllers/Controllers/GenericResourceController.cs b/test/WebSites/GenericControllers/Controllers/GenericResourceController.cs index 49b4a8520d..cae99e3dd7 100644 --- a/test/WebSites/GenericControllers/Controllers/GenericResourceController.cs +++ b/test/WebSites/GenericControllers/Controllers/GenericResourceController.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Http; +using System.Threading; using Microsoft.AspNetCore.Mvc; namespace GenericControllers.Controllers @@ -11,11 +11,54 @@ namespace GenericControllers.Controllers /// Creates a resource /// /// The resource + /// /// [HttpPost] [ProducesResponseType(201)] [Consumes("application/json")] - public int Create([FromBody, Required]TResource resource) + public int Create([FromBody, Required] TResource resource, CancellationToken cancellationToken) + { + return 1; + } + + /// + /// Delete by Id + /// + /// deleting Id + /// + /// + /// Deleted + /// Failed + [HttpDelete($"{nameof(Delete)}ById")] + public virtual int Delete([Required, FromBody] TResource id, CancellationToken cancellationToken) + { + return 1; + } + + /// + /// Delete by Id List + /// + /// deleting Ids + /// + /// + /// Deleted + /// Failed + [HttpDelete($"{nameof(Delete)}/List")] + public virtual int Delete([Required, FromBody] List ids, CancellationToken cancellationToken) + { + return 1; + } + + /// + /// Delete by Ids + /// + /// deleting Ids + /// + /// + /// Deleted + /// Failed + [HttpDelete("")] + public virtual int Delete([Required, FromBody] TResource[] resources, CancellationToken cancellationToken) { return 1; } @@ -57,4 +100,4 @@ public int Create([FromBody, Required]TResource resource) //{ //} } -} \ No newline at end of file +} diff --git a/test/WebSites/GenericControllers/Controllers/OrdersController.cs b/test/WebSites/GenericControllers/Controllers/OrdersController.cs index 82b1dd4b02..05b8b9fc93 100644 --- a/test/WebSites/GenericControllers/Controllers/OrdersController.cs +++ b/test/WebSites/GenericControllers/Controllers/OrdersController.cs @@ -2,7 +2,7 @@ namespace GenericControllers.Controllers { - [Route("{tennantId}/orders")] + [Route("{tenantId}/orders")] public class OrdersController : GenericResourceController { } diff --git a/test/WebSites/GenericControllers/Controllers/ProductsController.cs b/test/WebSites/GenericControllers/Controllers/ProductsController.cs index 0ab10dd9c6..5c1c89104b 100644 --- a/test/WebSites/GenericControllers/Controllers/ProductsController.cs +++ b/test/WebSites/GenericControllers/Controllers/ProductsController.cs @@ -2,7 +2,7 @@ namespace GenericControllers.Controllers { - [Route("{tennantId}/products")] + [Route("{tenantId}/products")] public class ProductsController : GenericResourceController { }