From c0116429d78b93a3e53f379b9204d8f02520eb67 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 4 Oct 2021 19:07:05 +0000 Subject: [PATCH 1/3] Support EndpointGroupName metadata in MVC ApiExplorer --- .../src/DefaultApiDescriptionProvider.cs | 20 ++++++++++++- .../test/DefaultApiDescriptionProviderTest.cs | 15 ++++++++++ .../Mvc.FunctionalTests/ApiExplorerTest.cs | 28 +++++++++++++++++++ .../Controllers/ApiExplorerApiController.cs | 3 ++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index 5a5d82e1ff6c..c64a70796ecf 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -71,13 +71,17 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) continue; } + // ApiDescriptionActionData is only added to the ControllerActionDescriptor if + // the action is marked as `IsVisible` to the ApiExplorer. This null-check is + // effectively asserting if the endpoint should be generated into the final + // OpenAPI metadata. var extensionData = action.GetProperty(); if (extensionData != null) { var httpMethods = GetHttpMethods(action); foreach (var httpMethod in httpMethods) { - context.Results.Add(CreateApiDescription(action, httpMethod, extensionData.GroupName)); + context.Results.Add(CreateApiDescription(action, httpMethod, GetGroupName(action))); } } } @@ -463,6 +467,20 @@ internal static MediaTypeCollection GetDeclaredContentTypes(IReadOnlyList(); + var endpointGroupName = action.EndpointMetadata.LastOrDefault(em => em is IEndpointGroupNameMetadata) as IEndpointGroupNameMetadata; + return endpointGroupName?.EndpointGroupName ?? extensionData?.GroupName; + } + private class ApiParameterDescriptionContext { public ModelMetadata ModelMetadata { get; } diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index c3492411df05..79648eea7fb6 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -88,6 +88,21 @@ public void GetApiDescription_PopulatesGroupName() Assert.Equal("Customers", description.GroupName); } + [Fact] + public void GetApiDescription_PopulatesGroupName_FromMetadata() + { + // Arrange + var action = CreateActionDescriptor(); + action.EndpointMetadata = new List() { new EndpointGroupNameAttribute("Customers") }; + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + Assert.Equal("Customers", description.GroupName); + } + [Fact] public void GetApiDescription_HttpMethodIsNullWithoutConstraint() { diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs index d6305c36acfe..33d102f4b842 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs @@ -120,6 +120,34 @@ public async Task ApiExplorer_GroupName_SetByAttributeOnAction() Assert.Equal("SetOnAction", description.GroupName); } + [Fact] + public async Task ApiExplorer_GroupName_SetByEndpointMetadataOnController() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ApiExplorerApiController/ActionWithIdParameter"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal("GroupNameOnController", description.GroupName); + } + + [Fact] + public async Task ApiExplorer_GroupName_SetByEndpointMetadataOnAction() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ApiExplorerApiController/ActionWithSomeParameters"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Equal("GroupNameOnAction", description.GroupName); + } + [Fact] public async Task ApiExplorer_RouteTemplate_DisplaysFixedRoute() { diff --git a/src/Mvc/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerApiController.cs b/src/Mvc/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerApiController.cs index a7bc423c4689..6c999d1a271d 100644 --- a/src/Mvc/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerApiController.cs +++ b/src/Mvc/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerApiController.cs @@ -4,15 +4,18 @@ using System.IO; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; namespace ApiExplorerWebSite { [Route("ApiExplorerApiController/[action]")] [ApiController] + [EndpointGroupName("GroupNameOnController")] public class ApiExplorerApiController : Controller { public IActionResult ActionWithoutParameters() => Ok(); + [EndpointGroupName("GroupNameOnAction")] public void ActionWithSomeParameters(object input) { } From 91b723fb27c9a67cb51ed939ebc0eae3b4c10031 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 4 Oct 2021 22:16:16 +0000 Subject: [PATCH 2/3] Address feedback from peer review --- .../src/DefaultApiDescriptionProvider.cs | 7 +++---- .../test/DefaultApiDescriptionProviderTest.cs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index c64a70796ecf..18fb6ac7d32c 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -81,7 +81,7 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) var httpMethods = GetHttpMethods(action); foreach (var httpMethod in httpMethods) { - context.Results.Add(CreateApiDescription(action, httpMethod, GetGroupName(action))); + context.Results.Add(CreateApiDescription(action, httpMethod, GetGroupName(action, extensionData))); } } } @@ -467,7 +467,7 @@ internal static MediaTypeCollection GetDeclaredContentTypes(IReadOnlyList(); - var endpointGroupName = action.EndpointMetadata.LastOrDefault(em => em is IEndpointGroupNameMetadata) as IEndpointGroupNameMetadata; + var endpointGroupName = action.EndpointMetadata.OfType().LastOrDefault(); return endpointGroupName?.EndpointGroupName ?? extensionData?.GroupName; } diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index 79648eea7fb6..6c8201e2bc81 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -103,6 +103,22 @@ public void GetApiDescription_PopulatesGroupName_FromMetadata() Assert.Equal("Customers", description.GroupName); } + [Fact] + public void GetApiDescription_PopulatesGroupName_FromMetadataOrExtensionData() + { + // Arrange + var action = CreateActionDescriptor(); + action.EndpointMetadata = new List() { new EndpointGroupNameAttribute("Customers") }; + action.GetProperty().GroupName = "NotUsedCustomers"; + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + Assert.Equal("Customers", description.GroupName); + } + [Fact] public void GetApiDescription_HttpMethodIsNullWithoutConstraint() { From 3aff9eb4a649de02cc442bbb8d71549c8082349b Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 4 Oct 2021 20:30:24 -0700 Subject: [PATCH 3/3] Update src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs Co-authored-by: Pranav K --- src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index 18fb6ac7d32c..0c61b043cad5 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -477,7 +477,7 @@ internal static MediaTypeCollection GetDeclaredContentTypes(IReadOnlyList().LastOrDefault(); - return endpointGroupName?.EndpointGroupName ?? extensionData?.GroupName; + return endpointGroupName?.EndpointGroupName ?? extensionData.GroupName; } private class ApiParameterDescriptionContext