From 8fa95d66d4fa5bc76ac054434bd4f8e6d4e546f0 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 13 Apr 2017 18:46:57 -0700 Subject: [PATCH] Add support for suppressing inbound and outbound routing --- .../Routing/AttributeRouteInfo.cs | 10 + .../DefaultApiDescriptionProvider.cs | 5 + .../ApplicationModels/AttributeRouteModel.cs | 16 +- .../Internal/AttributeRoute.cs | 22 +- .../Internal/AttributeRouteEntries.cs | 15 -- .../ControllerActionDescriptorBuilder.cs | 8 +- .../AttributeRouteModelTests.cs | 86 ++++++- .../Internal/AttributeRouteTest.cs | 225 ++++++++++++++++++ .../ApiExplorerTest.cs | 13 + .../ApplicationModelTest.cs | 40 ++++ .../ApiExplorerInboundOutboundConvention.cs | 44 ++++ .../ApiExplorerInboundOutboundController.cs | 20 ++ test/WebSites/ApiExplorerWebSite/Startup.cs | 3 + .../Controllers/HomeController.cs | 38 +++ 14 files changed, 515 insertions(+), 30 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRouteEntries.cs create mode 100644 test/WebSites/ApiExplorerWebSite/ApiExplorerInboundOutboundConvention.cs create mode 100644 test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerInboundOutboundController.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs index 312af9feee..5531b44ced 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs @@ -26,5 +26,15 @@ namespace Microsoft.AspNetCore.Mvc.Routing /// route by provided route data. /// public string Name { get; set; } + + /// + /// Gets or sets a value that determines if the route entry associated with this model participates in link generation. + /// + public bool SuppressLinkGeneration { get; set; } + + /// + /// Gets or sets a value that determines if the route entry associated with this model participates in path matching (inbound routing). + /// + public bool SuppressPathMatching { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs index 6997bd16ae..8a20803882 100644 --- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs @@ -66,6 +66,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer foreach (var action in context.Actions.OfType()) { + if (action.AttributeRouteInfo != null && action.AttributeRouteInfo.SuppressPathMatching) + { + continue; + } + var extensionData = action.GetProperty(); if (extensionData != null) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs index 32ca4d4142..6d3c9daaf8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs @@ -42,9 +42,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Name = other.Name; Order = other.Order; Template = other.Template; + SuppressLinkGeneration = other.SuppressLinkGeneration; + SuppressPathMatching = other.SuppressPathMatching; } - public IRouteTemplateProvider Attribute { get; private set; } + public IRouteTemplateProvider Attribute { get;} public string Template { get; set; } @@ -52,6 +54,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public string Name { get; set; } + /// + /// Gets or sets a value that determines if this model participates in link generation. + /// + public bool SuppressLinkGeneration { get; set; } + + /// + /// Gets or sets a value that determines if this model participates in path matching (inbound routing). + /// + public bool SuppressPathMatching { get; set; } + public bool IsAbsoluteTemplate { get @@ -96,6 +108,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Template = combinedTemplate, Order = right.Order ?? left.Order, Name = ChooseName(left, right), + SuppressLinkGeneration = left.SuppressLinkGeneration || right.SuppressLinkGeneration, + SuppressPathMatching = left.SuppressPathMatching || right.SuppressPathMatching, }; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRoute.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRoute.cs index 158fd66c49..2571c98fc0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRoute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRoute.cs @@ -88,6 +88,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal // action by expected route values, and then use the TemplateBinder to generate the link. foreach (var routeInfo in routeInfos) { + if (routeInfo.SuppressLinkGeneration) + { + continue; + } + var defaults = new RouteValueDictionary(); foreach (var kvp in routeInfo.ActionDescriptor.RouteValues) { @@ -117,7 +122,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // We're creating one AttributeRouteMatchingEntry per group, so we need to identify the distinct set of // groups. It's guaranteed that all members of the group have the same template and precedence, // so we only need to hang on to a single instance of the RouteInfo for each group. - var groups = GroupRouteInfos(routeInfos); + var groups = GetInboundRouteGroups(routeInfos); foreach (var group in groups) { var handler = _handlerFactory(group.ToArray()); @@ -135,9 +140,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal } } - private static IEnumerable> GroupRouteInfos(List routeInfos) + private static IEnumerable> GetInboundRouteGroups(List routeInfos) { - return routeInfos.GroupBy(r => r, r => r.ActionDescriptor, RouteInfoEqualityComparer.Instance); + return routeInfos + .Where(routeInfo => !routeInfo.SuppressPathMatching) + .GroupBy(r => r, r => r.ActionDescriptor, RouteInfoEqualityComparer.Instance); } private static List GetRouteInfos(IReadOnlyList actions) @@ -194,8 +201,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal try { - RouteTemplate parsedTemplate; - if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out parsedTemplate)) + if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out var parsedTemplate)) { // Parsing with throw if the template is invalid. parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template); @@ -203,6 +209,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal } routeInfo.RouteTemplate = parsedTemplate; + routeInfo.SuppressPathMatching = action.AttributeRouteInfo.SuppressPathMatching; + routeInfo.SuppressLinkGeneration = action.AttributeRouteInfo.SuppressLinkGeneration; } catch (Exception ex) { @@ -243,6 +251,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal public string RouteName { get; set; } public RouteTemplate RouteTemplate { get; set; } + + public bool SuppressPathMatching { get; set; } + + public bool SuppressLinkGeneration { get; set; } } private class RouteInfoEqualityComparer : IEqualityComparer diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRouteEntries.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRouteEntries.cs deleted file mode 100644 index 735e7bfcf8..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/AttributeRouteEntries.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.AspNetCore.Routing.Tree; - -namespace Microsoft.AspNetCore.Mvc.Internal -{ - public class AttributeRouteEntries - { - public List InboundEntries { get; } = new List(); - - public List OutboundEntries { get; } = new List(); - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs index 13ff927b6b..4b0a25f09c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs @@ -365,9 +365,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal AttributeRouteModel action, AttributeRouteModel controller) { - var combinedRoute = AttributeRouteModel.CombineAttributeRouteModel( - controller, - action); + var combinedRoute = AttributeRouteModel.CombineAttributeRouteModel(controller, action); if (combinedRoute == null) { @@ -375,11 +373,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal } else { - return new AttributeRouteInfo() + return new AttributeRouteInfo { Template = combinedRoute.Template, Order = combinedRoute.Order ?? DefaultAttributeRouteOrder, Name = combinedRoute.Name, + SuppressLinkGeneration = combinedRoute.SuppressLinkGeneration, + SuppressPathMatching = combinedRoute.SuppressPathMatching, }; } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs index e3a54e159e..c7a578850b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using Microsoft.AspNetCore.Routing; using Xunit; namespace Microsoft.AspNetCore.Mvc.ApplicationModels @@ -15,10 +14,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public void CopyConstructor_CopiesAllProperties() { // Arrange - var route = new AttributeRouteModel(new HttpGetAttribute("/api/Products")); - - route.Name = "products"; - route.Order = 5; + var route = new AttributeRouteModel(new HttpGetAttribute("/api/Products")) + { + Name = "products", + Order = 5, + SuppressLinkGeneration = true, + SuppressPathMatching = true, + }; // Act var route2 = new AttributeRouteModel(route); @@ -277,6 +279,80 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Assert.Equal(expectedName, combined.Name); } + [Fact] + public void Combine_SetsSuppressLinkGenerationToFalse_IfNeitherIsTrue() + { + // Arrange + var left = new AttributeRouteModel + { + Template = "Template" + }; + var right = new AttributeRouteModel(); + var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right); + + // Assert + Assert.False(combined.SuppressLinkGeneration); + } + + [Theory] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Combine_SetsSuppressLinkGenerationToTrue_IfEitherIsTrue(bool leftSuppress, bool rightSuppress) + { + // Arrange + var left = new AttributeRouteModel + { + Template = "Template", + SuppressLinkGeneration = leftSuppress, + }; + var right = new AttributeRouteModel + { + SuppressLinkGeneration = rightSuppress, + }; + var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right); + + // Assert + Assert.True(combined.SuppressLinkGeneration); + } + + [Fact] + public void Combine_SetsSuppressPathGenerationToFalse_IfNeitherIsTrue() + { + // Arrange + var left = new AttributeRouteModel + { + Template = "Template", + }; + var right = new AttributeRouteModel(); + var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right); + + // Assert + Assert.False(combined.SuppressPathMatching); + } + + [Theory] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Combine_SetsSuppressPathGenerationToTrue_IfEitherIsTrue(bool leftSuppress, bool rightSuppress) + { + // Arrange + var left = new AttributeRouteModel + { + Template = "Template", + SuppressPathMatching = leftSuppress, + }; + var right = new AttributeRouteModel + { + SuppressPathMatching = rightSuppress, + }; + var combined = AttributeRouteModel.CombineAttributeRouteModel(left, right); + + // Assert + Assert.True(combined.SuppressPathMatching); + } + public static IEnumerable CombineNamesTestData { get diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs index acd52a2c83..cecd129c21 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AttributeRouteTest.cs @@ -545,6 +545,231 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.IsType(exception.InnerException); } + [Fact] + public void GetEntries_DoesNotCreateOutboundEntriesForAttributesWithSuppressForLinkGenerationSetToTrue() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/get/{id}", + Name = "BLOG_LINK1", + SuppressLinkGeneration = true, + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/{snake-cased-name}", + Name = "BLOG_INDEX2", + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/", + Name = "BLOG_HOME", + SuppressPathMatching = true, + }, + }, + }; + + var builder = CreateBuilder(); + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object); + + // Act + route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + builder.OutboundEntries, + e => + { + Assert.Equal("BLOG_INDEX2", e.RouteName); + Assert.Equal("blog/{snake-cased-name}", e.RouteTemplate.TemplateText); + }, + e => + { + Assert.Equal("BLOG_HOME", e.RouteName); + Assert.Equal("blog/", e.RouteTemplate.TemplateText); + }); + } + + [Fact] + public void GetEntries_DoesNotCreateOutboundEntriesForAttributesWithSuppressForLinkGenerationSetToTrue_WhenMultipleAttributesHaveTheSameTemplate() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/get/{id}", + Name = "BLOG_LINK1", + SuppressLinkGeneration = true, + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/get/{id}", + Name = "BLOG_LINK2", + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/", + Name = "BLOG_HOME", + SuppressPathMatching = true, + }, + }, + }; + + var builder = CreateBuilder(); + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object); + + // Act + route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + builder.OutboundEntries, + e => + { + Assert.Equal("BLOG_LINK2", e.RouteName); + Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText); + }, + e => + { + Assert.Equal("BLOG_HOME", e.RouteName); + Assert.Equal("blog/", e.RouteTemplate.TemplateText); + }); + } + + + [Fact] + public void GetEntries_DoesNotCreateInboundEntriesForAttributesWithSuppressForPathMatchingSetToTrue() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/get/{id}", + Name = "BLOG_LINK1", + SuppressLinkGeneration = true, + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/{snake-cased-name}", + Name = "BLOG_LINK2", + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/", + Name = "BLOG_HOME", + SuppressPathMatching = true, + }, + }, + }; + + var builder = CreateBuilder(); + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object); + + // Act + route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + builder.InboundEntries, + e => + { + Assert.Equal("BLOG_LINK1", e.RouteName); + Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText); + }, + e => + { + Assert.Equal("BLOG_LINK2", e.RouteName); + Assert.Equal("blog/{snake-cased-name}", e.RouteTemplate.TemplateText); + }); + } + + [Fact] + public void GetEntries_DoesNotCreateInboundEntriesForAttributesWithSuppressForPathMatchingSetToTrue_WhenMultipleAttributesHaveTheSameTemplate() + { + // Arrange + var actions = new List() + { + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/get/{id}", + Name = "BLOG_LINK1", + SuppressPathMatching = true, + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/get/{id}", + Name = "BLOG_LINK2", + }, + }, + new ActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo + { + Template = "blog/", + Name = "BLOG_HOME", + SuppressLinkGeneration = true, + }, + }, + }; + + var builder = CreateBuilder(); + var actionDescriptorProvider = CreateActionDescriptorProvider(actions); + var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object); + + // Act + route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors); + + // Assert + Assert.Collection( + builder.InboundEntries, + e => + { + Assert.Equal("BLOG_LINK2", e.RouteName); + Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText); + }, + e => + { + Assert.Equal("BLOG_HOME", e.RouteName); + Assert.Equal("blog/", e.RouteTemplate.TemplateText); + }); + } + private static TreeRouteBuilder CreateBuilder() { var services = new ServiceCollection() diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs index c83ac821dd..b9d9d4f309 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs @@ -1052,6 +1052,19 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("ApiExplorerReload/NewIndex", description.RelativePath); } + [Fact] + public async Task ApiExplorer_DoesNotListActionsSuppressedForPathMatching() + { + // Act + var body = await Client.GetStringAsync("ApiExplorerInboundOutbound/SuppressedForLinkGeneration"); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + Assert.Empty(description.ParameterDescriptions); + Assert.Equal("ApiExplorerInboundOutbound/SuppressedForLinkGeneration", description.RelativePath); + } + private IEnumerable GetSortedMediaTypes(ApiExplorerResponseType apiResponseType) { return apiResponseType.ResponseFormats diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApplicationModelTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApplicationModelTest.cs index bd9155b780..3d2419419c 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApplicationModelTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApplicationModelTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -126,5 +127,44 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var body = await response.Content.ReadAsStringAsync(); Assert.Equal("From Header - HelloWorld", body); } + + [Fact] + public async Task ActionModelSuppressedForPathMatching_CannotBeRouted() + { + // Arrange & Act + var response = await Client.GetAsync("Home/CannotBeRouted"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task ActionModelNotSuppressedForPathMatching_CanBeRouted() + { + // Arrange & Act + var response = await Client.GetStringAsync("Home/CanBeRouted"); + + // Assert + Assert.Equal("Hello world", response); + } + + [Fact] + public async Task ActionModelSuppressedForLinkGeneration_CannotBeLinked() + { + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => Client.GetStringAsync("Home/RouteToSuppressLinkGeneration")); + Assert.Equal("No route matches the supplied values.", ex.Message); + } + + [Fact] + public async Task ActionModelSuppressedForPathMatching_CanBeLinked() + { + // Arrange & Act + var response = await Client.GetAsync("Home/RouteToSuppressPathMatching"); + + // Assert + Assert.Equal("/Home/CannotBeRouted", response.Headers.Location.ToString()); + } } } \ No newline at end of file diff --git a/test/WebSites/ApiExplorerWebSite/ApiExplorerInboundOutboundConvention.cs b/test/WebSites/ApiExplorerWebSite/ApiExplorerInboundOutboundConvention.cs new file mode 100644 index 0000000000..fa826ebf6d --- /dev/null +++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerInboundOutboundConvention.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using ApiExplorerWebSite.Controllers; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace ApiExplorerWebSite +{ + // Disables ApiExplorer for a specific controller type. + // This is part of the test that validates that ApiExplorer can be configured via + // convention + public class ApiExplorerInboundOutboundConvention : IApplicationModelConvention + { + private readonly TypeInfo _type; + + public ApiExplorerInboundOutboundConvention(Type type) + { + _type = type.GetTypeInfo(); + } + + public void Apply(ApplicationModel application) + { + foreach (var controller in application.Controllers) + { + if (controller.ControllerType == _type) + { + foreach (var action in controller.Actions) + { + if (action.ActionName == nameof(ApiExplorerInboundOutBoundController.SuppressedForPathMatching)) + { + action.Selectors[0].AttributeRouteModel.SuppressPathMatching = true; + } + else if (action.ActionName == nameof(ApiExplorerInboundOutBoundController.SuppressedForLinkGeneration)) + { + action.Selectors[0].AttributeRouteModel.SuppressLinkGeneration = true; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerInboundOutboundController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerInboundOutboundController.cs new file mode 100644 index 0000000000..55b4787a03 --- /dev/null +++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerInboundOutboundController.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; + +namespace ApiExplorerWebSite.Controllers +{ + public class ApiExplorerInboundOutBoundController : Controller + { + [HttpGet("ApiExplorerInboundOutbound/SuppressedForLinkGeneration")] + public void SuppressedForLinkGeneration() + { + } + + [HttpGet("ApiExplorerInboundOutbound/SuppressedForPathMatching")] + public void SuppressedForPathMatching() + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/ApiExplorerWebSite/Startup.cs b/test/WebSites/ApiExplorerWebSite/Startup.cs index f994f315a8..5d53827bcb 100644 --- a/test/WebSites/ApiExplorerWebSite/Startup.cs +++ b/test/WebSites/ApiExplorerWebSite/Startup.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; +using ApiExplorerWebSite.Controllers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Formatters; @@ -25,6 +26,8 @@ namespace ApiExplorerWebSite options.Conventions.Add(new ApiExplorerVisibilityEnabledConvention()); options.Conventions.Add(new ApiExplorerVisibilityDisabledConvention( typeof(ApiExplorerVisbilityDisabledByConventionController))); + options.Conventions.Add(new ApiExplorerInboundOutboundConvention( + typeof(ApiExplorerInboundOutBoundController))); var jsonOutputFormatter = options.OutputFormatters.OfType().First(); diff --git a/test/WebSites/ApplicationModelWebSite/Controllers/HomeController.cs b/test/WebSites/ApplicationModelWebSite/Controllers/HomeController.cs index 2941082236..e5c526bb56 100644 --- a/test/WebSites/ApplicationModelWebSite/Controllers/HomeController.cs +++ b/test/WebSites/ApplicationModelWebSite/Controllers/HomeController.cs @@ -1,7 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Linq; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; namespace ApplicationModelWebSite { @@ -17,5 +20,40 @@ namespace ApplicationModelWebSite { return ControllerContext.ActionDescriptor.Properties["source"].ToString() + " - " + helloWorld; } + + [HttpGet("Home/CannotBeRouted", Name = nameof(SuppressPathMatching))] + [HttpGet("Home/CanBeRouted")] + [SuppressPatchMatchingConvention] + public object SuppressPathMatching() + { + return "Hello world"; + } + + [HttpGet("Home/SuppressLinkGeneration", Name = nameof(SuppressLinkGeneration))] + [SuppressLinkGenerationConvention] + public object SuppressLinkGeneration() => "Hello world"; + + [HttpGet("Home/RouteToSuppressLinkGeneration")] + public IActionResult RouteToSuppressLinkGeneration() => RedirectToRoute(nameof(SuppressLinkGeneration)); + + [HttpGet("Home/RouteToSuppressPathMatching")] + public IActionResult RouteToSuppressPathMatching() => RedirectToRoute(nameof(SuppressPathMatching)); + + private class SuppressPatchMatchingConvention : Attribute, IActionModelConvention + { + public void Apply(ActionModel model) + { + var selector = model.Selectors.First(f => f.AttributeRouteModel.Template == "Home/CannotBeRouted"); + selector.AttributeRouteModel.SuppressPathMatching = true; + } + } + + private class SuppressLinkGenerationConvention : Attribute, IActionModelConvention + { + public void Apply(ActionModel model) + { + model.Selectors[0].AttributeRouteModel.SuppressLinkGeneration = true; + } + } } } \ No newline at end of file