From fa946a82e9b13d79075d089e77c34f1201dd8029 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 4 Oct 2021 21:59:01 -0700 Subject: [PATCH] Support EndpointGroupName metadata in MVC ApiExplorer (#37264) * Support EndpointGroupName metadata in MVC ApiExplorer * Address feedback from peer review * Update src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs Co-authored-by: Pranav K Co-authored-by: Pranav K --- .../src/DefaultApiDescriptionProvider.cs | 19 +++++++++++- .../test/DefaultApiDescriptionProviderTest.cs | 31 +++++++++++++++++++ .../Mvc.FunctionalTests/ApiExplorerTest.cs | 28 +++++++++++++++++ .../Controllers/ApiExplorerApiController.cs | 3 ++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index 5a5d82e1ff6..0c61b043cad 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -71,13 +71,17 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer 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, extensionData))); } } } @@ -463,6 +467,19 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer .ToArray(); } + private static string? GetGroupName(ControllerActionDescriptor action, ApiDescriptionActionData extensionData) + { + // The `GroupName` set in the `ApiDescriptionActionData` is either the + // group name set via [ApiExplorerSettings(GroupName = "foo")] on the + // action or controller. So, this lookup favors the following sequence: + // - EndpointGroupName on the action, if it is set + // - EndpointGroupName on the controller, if it is set + // - ApiExplorerSettings.GroupName on the action, if it is set + // - ApiExplorerSettings.GroupName on the controller, if it is set + var endpointGroupName = action.EndpointMetadata.OfType().LastOrDefault(); + 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 c3492411df0..6c8201e2bc8 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -88,6 +88,37 @@ namespace Microsoft.AspNetCore.Mvc.Description 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_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() { diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs index d6305c36acf..33d102f4b84 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs @@ -120,6 +120,34 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests 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 a7bc423c468..6c999d1a271 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) { }