Add support for suppressing inbound and outbound routing
This commit is contained in:
parent
c69e48f06b
commit
8fa95d66d4
|
|
@ -26,5 +26,15 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
/// route by provided route data.
|
/// route by provided route data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that determines if the route entry associated with this model participates in link generation.
|
||||||
|
/// </summary>
|
||||||
|
public bool SuppressLinkGeneration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that determines if the route entry associated with this model participates in path matching (inbound routing).
|
||||||
|
/// </summary>
|
||||||
|
public bool SuppressPathMatching { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +66,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
|
|
||||||
foreach (var action in context.Actions.OfType<ControllerActionDescriptor>())
|
foreach (var action in context.Actions.OfType<ControllerActionDescriptor>())
|
||||||
{
|
{
|
||||||
|
if (action.AttributeRouteInfo != null && action.AttributeRouteInfo.SuppressPathMatching)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var extensionData = action.GetProperty<ApiDescriptionActionData>();
|
var extensionData = action.GetProperty<ApiDescriptionActionData>();
|
||||||
if (extensionData != null)
|
if (extensionData != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
Name = other.Name;
|
Name = other.Name;
|
||||||
Order = other.Order;
|
Order = other.Order;
|
||||||
Template = other.Template;
|
Template = other.Template;
|
||||||
|
SuppressLinkGeneration = other.SuppressLinkGeneration;
|
||||||
|
SuppressPathMatching = other.SuppressPathMatching;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IRouteTemplateProvider Attribute { get; private set; }
|
public IRouteTemplateProvider Attribute { get;}
|
||||||
|
|
||||||
public string Template { get; set; }
|
public string Template { get; set; }
|
||||||
|
|
||||||
|
|
@ -52,6 +54,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that determines if this model participates in link generation.
|
||||||
|
/// </summary>
|
||||||
|
public bool SuppressLinkGeneration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that determines if this model participates in path matching (inbound routing).
|
||||||
|
/// </summary>
|
||||||
|
public bool SuppressPathMatching { get; set; }
|
||||||
|
|
||||||
public bool IsAbsoluteTemplate
|
public bool IsAbsoluteTemplate
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -96,6 +108,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
Template = combinedTemplate,
|
Template = combinedTemplate,
|
||||||
Order = right.Order ?? left.Order,
|
Order = right.Order ?? left.Order,
|
||||||
Name = ChooseName(left, right),
|
Name = ChooseName(left, right),
|
||||||
|
SuppressLinkGeneration = left.SuppressLinkGeneration || right.SuppressLinkGeneration,
|
||||||
|
SuppressPathMatching = left.SuppressPathMatching || right.SuppressPathMatching,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// action by expected route values, and then use the TemplateBinder to generate the link.
|
// action by expected route values, and then use the TemplateBinder to generate the link.
|
||||||
foreach (var routeInfo in routeInfos)
|
foreach (var routeInfo in routeInfos)
|
||||||
{
|
{
|
||||||
|
if (routeInfo.SuppressLinkGeneration)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var defaults = new RouteValueDictionary();
|
var defaults = new RouteValueDictionary();
|
||||||
foreach (var kvp in routeInfo.ActionDescriptor.RouteValues)
|
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
|
// 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,
|
// 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.
|
// 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)
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
var handler = _handlerFactory(group.ToArray());
|
var handler = _handlerFactory(group.ToArray());
|
||||||
|
|
@ -135,9 +140,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<IGrouping<RouteInfo, ActionDescriptor>> GroupRouteInfos(List<RouteInfo> routeInfos)
|
private static IEnumerable<IGrouping<RouteInfo, ActionDescriptor>> GetInboundRouteGroups(List<RouteInfo> 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<RouteInfo> GetRouteInfos(IReadOnlyList<ActionDescriptor> actions)
|
private static List<RouteInfo> GetRouteInfos(IReadOnlyList<ActionDescriptor> actions)
|
||||||
|
|
@ -194,8 +201,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
RouteTemplate parsedTemplate;
|
if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out var parsedTemplate))
|
||||||
if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out parsedTemplate))
|
|
||||||
{
|
{
|
||||||
// Parsing with throw if the template is invalid.
|
// Parsing with throw if the template is invalid.
|
||||||
parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template);
|
parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template);
|
||||||
|
|
@ -203,6 +209,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
routeInfo.RouteTemplate = parsedTemplate;
|
routeInfo.RouteTemplate = parsedTemplate;
|
||||||
|
routeInfo.SuppressPathMatching = action.AttributeRouteInfo.SuppressPathMatching;
|
||||||
|
routeInfo.SuppressLinkGeneration = action.AttributeRouteInfo.SuppressLinkGeneration;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -243,6 +251,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
public string RouteName { get; set; }
|
public string RouteName { get; set; }
|
||||||
|
|
||||||
public RouteTemplate RouteTemplate { get; set; }
|
public RouteTemplate RouteTemplate { get; set; }
|
||||||
|
|
||||||
|
public bool SuppressPathMatching { get; set; }
|
||||||
|
|
||||||
|
public bool SuppressLinkGeneration { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RouteInfoEqualityComparer : IEqualityComparer<RouteInfo>
|
private class RouteInfoEqualityComparer : IEqualityComparer<RouteInfo>
|
||||||
|
|
|
||||||
|
|
@ -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<InboundRouteEntry> InboundEntries { get; } = new List<InboundRouteEntry>();
|
|
||||||
|
|
||||||
public List<OutboundRouteEntry> OutboundEntries { get; } = new List<OutboundRouteEntry>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -365,9 +365,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
AttributeRouteModel action,
|
AttributeRouteModel action,
|
||||||
AttributeRouteModel controller)
|
AttributeRouteModel controller)
|
||||||
{
|
{
|
||||||
var combinedRoute = AttributeRouteModel.CombineAttributeRouteModel(
|
var combinedRoute = AttributeRouteModel.CombineAttributeRouteModel(controller, action);
|
||||||
controller,
|
|
||||||
action);
|
|
||||||
|
|
||||||
if (combinedRoute == null)
|
if (combinedRoute == null)
|
||||||
{
|
{
|
||||||
|
|
@ -375,11 +373,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return new AttributeRouteInfo()
|
return new AttributeRouteInfo
|
||||||
{
|
{
|
||||||
Template = combinedRoute.Template,
|
Template = combinedRoute.Template,
|
||||||
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
|
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
|
||||||
Name = combinedRoute.Name,
|
Name = combinedRoute.Name,
|
||||||
|
SuppressLinkGeneration = combinedRoute.SuppressLinkGeneration,
|
||||||
|
SuppressPathMatching = combinedRoute.SuppressPathMatching,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Routing;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
|
|
@ -15,10 +14,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
public void CopyConstructor_CopiesAllProperties()
|
public void CopyConstructor_CopiesAllProperties()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var route = new AttributeRouteModel(new HttpGetAttribute("/api/Products"));
|
var route = new AttributeRouteModel(new HttpGetAttribute("/api/Products"))
|
||||||
|
{
|
||||||
route.Name = "products";
|
Name = "products",
|
||||||
route.Order = 5;
|
Order = 5,
|
||||||
|
SuppressLinkGeneration = true,
|
||||||
|
SuppressPathMatching = true,
|
||||||
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var route2 = new AttributeRouteModel(route);
|
var route2 = new AttributeRouteModel(route);
|
||||||
|
|
@ -277,6 +279,80 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
Assert.Equal(expectedName, combined.Name);
|
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<object[]> CombineNamesTestData
|
public static IEnumerable<object[]> CombineNamesTestData
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -545,6 +545,231 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.IsType<RouteCreationException>(exception.InnerException);
|
Assert.IsType<RouteCreationException>(exception.InnerException);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetEntries_DoesNotCreateOutboundEntriesForAttributesWithSuppressForLinkGenerationSetToTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
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<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
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<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
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<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
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()
|
private static TreeRouteBuilder CreateBuilder()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection()
|
var services = new ServiceCollection()
|
||||||
|
|
|
||||||
|
|
@ -1052,6 +1052,19 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("ApiExplorerReload/NewIndex", description.RelativePath);
|
Assert.Equal("ApiExplorerReload/NewIndex", description.RelativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_DoesNotListActionsSuppressedForPathMatching()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var body = await Client.GetStringAsync("ApiExplorerInboundOutbound/SuppressedForLinkGeneration");
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Empty(description.ParameterDescriptions);
|
||||||
|
Assert.Equal("ApiExplorerInboundOutbound/SuppressedForLinkGeneration", description.RelativePath);
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
|
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
|
||||||
{
|
{
|
||||||
return apiResponseType.ResponseFormats
|
return apiResponseType.ResponseFormats
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -126,5 +127,44 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
Assert.Equal("From Header - HelloWorld", body);
|
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<InvalidOperationException>(
|
||||||
|
() => 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using ApiExplorerWebSite.Controllers;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
|
@ -25,6 +26,8 @@ namespace ApiExplorerWebSite
|
||||||
options.Conventions.Add(new ApiExplorerVisibilityEnabledConvention());
|
options.Conventions.Add(new ApiExplorerVisibilityEnabledConvention());
|
||||||
options.Conventions.Add(new ApiExplorerVisibilityDisabledConvention(
|
options.Conventions.Add(new ApiExplorerVisibilityDisabledConvention(
|
||||||
typeof(ApiExplorerVisbilityDisabledByConventionController)));
|
typeof(ApiExplorerVisbilityDisabledByConventionController)));
|
||||||
|
options.Conventions.Add(new ApiExplorerInboundOutboundConvention(
|
||||||
|
typeof(ApiExplorerInboundOutBoundController)));
|
||||||
|
|
||||||
var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().First();
|
var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().First();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
|
|
||||||
namespace ApplicationModelWebSite
|
namespace ApplicationModelWebSite
|
||||||
{
|
{
|
||||||
|
|
@ -17,5 +20,40 @@ namespace ApplicationModelWebSite
|
||||||
{
|
{
|
||||||
return ControllerContext.ActionDescriptor.Properties["source"].ToString() + " - " + helloWorld;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue