Reacting to Routing repo's EndpointFinder changes

This commit is contained in:
Kiran Challa 2018-06-25 14:24:34 -07:00
parent 4dd4e5ef3e
commit 54c14b8782
6 changed files with 511 additions and 104 deletions

View File

@ -30,7 +30,8 @@ namespace Microsoft.AspNetCore.Builder
ParsedTemplate = TemplateParser.Parse(template); ParsedTemplate = TemplateParser.Parse(template);
Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints);
Defaults = GetDefaults(ParsedTemplate, defaults); Defaults = defaults;
MergedDefaults = GetDefaults(ParsedTemplate, defaults);
} }
catch (Exception exception) catch (Exception exception)
{ {
@ -41,7 +42,13 @@ namespace Microsoft.AspNetCore.Builder
public string Name { get; } public string Name { get; }
public string Template { get; } public string Template { get; }
// Non-inline defaults
public RouteValueDictionary Defaults { get; } public RouteValueDictionary Defaults { get; }
// Inline and non-inline defaults merged into one
public RouteValueDictionary MergedDefaults { get; }
public IDictionary<string, IRouteConstraint> Constraints { get; } public IDictionary<string, IRouteConstraint> Constraints { get; }
public RouteValueDictionary DataTokens { get; } public RouteValueDictionary DataTokens { get; }
internal RouteTemplate ParsedTemplate { get; private set; } internal RouteTemplate ParsedTemplate { get; private set; }

View File

@ -68,6 +68,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
if (action.AttributeRouteInfo == null) if (action.AttributeRouteInfo == null)
{ {
// In traditional conventional routing setup, the routes defined by a user have a static order
// defined by how they are added into the list. We would like to maintain the same order when building
// up the endpoints too.
//
// Start with an order of '1' for conventional routes as attribute routes have a default order of '0'.
// This is for scenarios dealing with migrating existing Routing based code to Dispatcher world.
var conventionalRouteOrder = 0;
// Check each of the conventional templates to see if the action would be reachable // Check each of the conventional templates to see if the action would be reachable
// If the action and template are compatible then create an endpoint with the // If the action and template are compatible then create an endpoint with the
// area/controller/action parameter parts replaced with literals // area/controller/action parameter parts replaced with literals
@ -100,7 +108,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
var subTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments.Take(i)); var subTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments.Take(i));
var subEndpoint = CreateEndpoint(action, subTemplate, 0, endpointInfo); var subEndpoint = CreateEndpoint(
action,
endpointInfo.Name,
subTemplate,
endpointInfo.Defaults,
++conventionalRouteOrder,
endpointInfo);
_endpoints.Add(subEndpoint); _endpoints.Add(subEndpoint);
} }
@ -119,14 +133,26 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var newTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments); var newTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments);
var endpoint = CreateEndpoint(action, newTemplate, 0, endpointInfo); var endpoint = CreateEndpoint(
action,
endpointInfo.Name,
newTemplate,
endpointInfo.Defaults,
++conventionalRouteOrder,
endpointInfo);
_endpoints.Add(endpoint); _endpoints.Add(endpoint);
} }
} }
} }
else else
{ {
var endpoint = CreateEndpoint(action, action.AttributeRouteInfo.Template, action.AttributeRouteInfo.Order, action.AttributeRouteInfo); var endpoint = CreateEndpoint(
action,
action.AttributeRouteInfo.Name,
action.AttributeRouteInfo.Template,
nonInlineDefaults: null,
action.AttributeRouteInfo.Order,
action.AttributeRouteInfo);
_endpoints.Add(endpoint); _endpoints.Add(endpoint);
} }
} }
@ -144,7 +170,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return false; return false;
} }
private bool UseDefaultValuePlusRemainingSegementsOptional(int segmentIndex, ActionDescriptor action, MvcEndpointInfo endpointInfo, RouteTemplate template) private bool UseDefaultValuePlusRemainingSegementsOptional(
int segmentIndex,
ActionDescriptor action,
MvcEndpointInfo endpointInfo,
RouteTemplate template)
{ {
// Check whether the remaining segments are all optional and one or more of them is // Check whether the remaining segments are all optional and one or more of them is
// for area/controller/action and has a default value // for area/controller/action and has a default value
@ -164,7 +194,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
if (IsMvcParameter(part.Name)) if (IsMvcParameter(part.Name))
{ {
if (endpointInfo.Defaults[part.Name] is string defaultValue if (endpointInfo.MergedDefaults[part.Name] is string defaultValue
&& action.RouteValues.TryGetValue(part.Name, out var routeValue) && action.RouteValues.TryGetValue(part.Name, out var routeValue)
&& string.Equals(defaultValue, routeValue, StringComparison.OrdinalIgnoreCase)) && string.Equals(defaultValue, routeValue, StringComparison.OrdinalIgnoreCase))
{ {
@ -196,7 +226,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
else else
{ {
if (endpointInfo.Defaults != null && string.Equals(actionValue, endpointInfo.Defaults[routeKey] as string, StringComparison.OrdinalIgnoreCase)) if (endpointInfo.MergedDefaults != null && string.Equals(actionValue, endpointInfo.MergedDefaults[routeKey] as string, StringComparison.OrdinalIgnoreCase))
{ {
return true; return true;
} }
@ -235,7 +265,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
} }
private MatcherEndpoint CreateEndpoint(ActionDescriptor action, string template, int order, object source) private MatcherEndpoint CreateEndpoint(
ActionDescriptor action,
string routeName,
string template,
object nonInlineDefaults,
int order,
object source)
{ {
RequestDelegate invokerDelegate = (context) => RequestDelegate invokerDelegate = (context) =>
{ {
@ -260,10 +296,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
metadata.Add(source); metadata.Add(source);
metadata.Add(action); metadata.Add(action);
if (!string.IsNullOrEmpty(routeName))
{
metadata.Add(new RouteNameMetadata(routeName));
}
// Add filter descriptors to endpoint metadata // Add filter descriptors to endpoint metadata
if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0) if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0)
{ {
metadata.AddRange(action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer).Select(f => f.Filter)); metadata.AddRange(action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer)
.Select(f => f.Filter));
} }
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
@ -281,14 +323,41 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var endpoint = new MatcherEndpoint( var endpoint = new MatcherEndpoint(
next => invokerDelegate, next => invokerDelegate,
template, template,
action.RouteValues, new RouteValueDictionary(nonInlineDefaults),
new RouteValueDictionary(action.RouteValues),
order, order,
metadataCollection, metadataCollection,
action.DisplayName, action.DisplayName);
address: null);
// Use defaults after the endpoint is created as it merges both the inline and
// non-inline defaults into one.
EnsureRequiredValuesInDefaults(endpoint.RequiredValues, endpoint.Defaults);
return endpoint; return endpoint;
} }
// Ensure required values are a subset of defaults
// Examples:
//
// Template: {controller}/{action}/{category}/{id?}
// Defaults(in-line or non in-line): category=products
// Required values: controller=foo, action=bar
// Final constructed template: foo/bar/{category}/{id?}
// Final defaults: controller=foo, action=bar, category=products
//
// Template: {controller=Home}/{action=Index}/{category=products}/{id?}
// Defaults: controller=Home, action=Index, category=products
// Required values: controller=foo, action=bar
// Final constructed template: foo/bar/{category}/{id?}
// Final defaults: controller=foo, action=bar, category=products
private void EnsureRequiredValuesInDefaults(RouteValueDictionary requiredValues, RouteValueDictionary defaults)
{
foreach (var kvp in requiredValues)
{
defaults[kvp.Key] = kvp.Value;
}
}
private IChangeToken GetCompositeChangeToken() private IChangeToken GetCompositeChangeToken()
{ {
if (_actionDescriptorChangeProviders.Length == 1) if (_actionDescriptorChangeProviders.Length == 1)
@ -321,5 +390,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public override IReadOnlyList<Endpoint> Endpoints => _endpoints; public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
public List<MvcEndpointInfo> ConventionalEndpointInfos { get; } public List<MvcEndpointInfo> ConventionalEndpointInfos { get; }
private class RouteNameMetadata : IRouteNameMetadata
{
public RouteNameMetadata(string routeName)
{
Name = routeName;
}
public string Name { get; }
}
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.EndpointFinders;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Routing namespace Microsoft.AspNetCore.Mvc.Routing
@ -15,16 +16,21 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{ {
private readonly ILogger<DispatcherUrlHelper> _logger; private readonly ILogger<DispatcherUrlHelper> _logger;
private readonly ILinkGenerator _linkGenerator; private readonly ILinkGenerator _linkGenerator;
private readonly IEndpointFinder<RouteValuesBasedEndpointFinderContext> _routeValuesBasedEndpointFinder;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DispatcherUrlHelper"/> class using the specified /// Initializes a new instance of the <see cref="DispatcherUrlHelper"/> class using the specified
/// <paramref name="actionContext"/>. /// <paramref name="actionContext"/>.
/// </summary> /// </summary>
/// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current request.</param> /// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current request.</param>
/// <param name="routeValuesBasedEndpointFinder">
/// The <see cref="IEndpointFinder{T}"/> which finds endpoints by required route values.
/// </param>
/// <param name="linkGenerator">The <see cref="ILinkGenerator"/> used to generate the link.</param> /// <param name="linkGenerator">The <see cref="ILinkGenerator"/> used to generate the link.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param> /// <param name="logger">The <see cref="ILogger"/>.</param>
public DispatcherUrlHelper( public DispatcherUrlHelper(
ActionContext actionContext, ActionContext actionContext,
IEndpointFinder<RouteValuesBasedEndpointFinderContext> routeValuesBasedEndpointFinder,
ILinkGenerator linkGenerator, ILinkGenerator linkGenerator,
ILogger<DispatcherUrlHelper> logger) ILogger<DispatcherUrlHelper> logger)
: base(actionContext) : base(actionContext)
@ -40,6 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_routeValuesBasedEndpointFinder = routeValuesBasedEndpointFinder;
_logger = logger; _logger = logger;
} }
@ -79,12 +86,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing
valuesDictionary["controller"] = urlActionContext.Controller; valuesDictionary["controller"] = urlActionContext.Controller;
} }
var successfullyGeneratedLink = _linkGenerator.TryGetLink( var endpoints = _routeValuesBasedEndpointFinder.FindEndpoints(
new LinkGeneratorContext() new RouteValuesBasedEndpointFinderContext()
{ {
SuppliedValues = valuesDictionary, ExplicitValues = valuesDictionary,
AmbientValues = AmbientValues AmbientValues = AmbientValues
}, });
var successfullyGeneratedLink = _linkGenerator.TryGetLink(
endpoints,
valuesDictionary,
AmbientValues,
out var link); out var link);
if (!successfullyGeneratedLink) if (!successfullyGeneratedLink)
@ -107,13 +119,18 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var valuesDictionary = routeContext.Values as RouteValueDictionary ?? GetValuesDictionary(routeContext.Values); var valuesDictionary = routeContext.Values as RouteValueDictionary ?? GetValuesDictionary(routeContext.Values);
var successfullyGeneratedLink = _linkGenerator.TryGetLink( var endpoints = _routeValuesBasedEndpointFinder.FindEndpoints(
new LinkGeneratorContext() new RouteValuesBasedEndpointFinderContext()
{ {
Address = new Address(routeContext.RouteName), RouteName = routeContext.RouteName,
SuppliedValues = valuesDictionary, ExplicitValues = valuesDictionary,
AmbientValues = AmbientValues AmbientValues = AmbientValues
}, });
var successfullyGeneratedLink = _linkGenerator.TryGetLink(
endpoints,
valuesDictionary,
AmbientValues,
out var link); out var link);
if (!successfullyGeneratedLink) if (!successfullyGeneratedLink)

View File

@ -5,6 +5,7 @@ using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.EndpointFinders;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -49,9 +50,16 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var endpointFeature = httpContext.Features.Get<IEndpointFeature>(); var endpointFeature = httpContext.Features.Get<IEndpointFeature>();
if (endpointFeature?.Endpoint != null) if (endpointFeature?.Endpoint != null)
{ {
var linkGenerator = httpContext.RequestServices.GetRequiredService<ILinkGenerator>(); var services = httpContext.RequestServices;
var logger = httpContext.RequestServices.GetRequiredService<ILogger<DispatcherUrlHelper>>(); var linkGenerator = services.GetRequiredService<ILinkGenerator>();
urlHelper = new DispatcherUrlHelper(context, linkGenerator, logger); var routeValuesBasedEndpointFinder = services.GetRequiredService<IEndpointFinder<RouteValuesBasedEndpointFinderContext>>();
var logger = services.GetRequiredService<ILogger<DispatcherUrlHelper>>();
urlHelper = new DispatcherUrlHelper(
context,
routeValuesBasedEndpointFinder,
linkGenerator,
logger);
} }
else else
{ {

View File

@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matchers; using Microsoft.AspNetCore.Routing.Matchers;
@ -20,7 +19,7 @@ using Microsoft.Extensions.Primitives;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal namespace Microsoft.AspNetCore.Mvc.Internal
{ {
public class MvcEndpointDataSourceTests public class MvcEndpointDataSourceTests
{ {
@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
{ {
// Arrange // Arrange
var routeValue = "Value"; var routeValue = "Value";
var routeValues = new Dictionary<string, string> var requiredValues = new Dictionary<string, string>
{ {
["Name"] = routeValue ["Name"] = routeValue
}; };
@ -43,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
{ {
new ActionDescriptor new ActionDescriptor
{ {
RouteValues = routeValues, RouteValues = requiredValues,
DisplayName = displayName, DisplayName = displayName,
AttributeRouteInfo = new AttributeRouteInfo AttributeRouteInfo = new AttributeRouteInfo
{ {
@ -66,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
var endpoint = Assert.Single(dataSource.Endpoints); var endpoint = Assert.Single(dataSource.Endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var endpointValue = matcherEndpoint.Values["Name"]; var endpointValue = matcherEndpoint.RequiredValues["Name"];
Assert.Equal(routeValue, endpointValue); Assert.Equal(routeValue, endpointValue);
Assert.Equal(displayName, matcherEndpoint.DisplayName); Assert.Equal(displayName, matcherEndpoint.DisplayName);
@ -187,13 +186,9 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
public void InitializeEndpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) public void InitializeEndpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointTemplates)
{ {
// Arrange // Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(); var actionDescriptorCollection = GetActionDescriptorCollection(
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor> new { controller = "TestController", action = "TestAction" });
{ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
CreateActionDescriptor("TestController", "TestAction")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute));
// Act // Act
@ -218,13 +213,9 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
public void InitializeEndpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) public void InitializeEndpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates)
{ {
// Arrange // Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(); var actionDescriptorCollection = GetActionDescriptorCollection(
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor> new { controller = "TestController", action = "TestAction", area = "TestArea" });
{ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
CreateActionDescriptor("TestController", "TestAction", "TestArea")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute));
// Act // Act
@ -243,13 +234,9 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
public void InitializeEndpoints_SingleAction_WithActionDefault() public void InitializeEndpoints_SingleAction_WithActionDefault()
{ {
// Arrange // Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(); var actionDescriptorCollection = GetActionDescriptorCollection(
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor> new { controller = "TestController", action = "TestAction" });
{ var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
CreateActionDescriptor("TestController", "TestAction")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
string.Empty, string.Empty,
"{controller}/{action}", "{controller}/{action}",
@ -268,15 +255,11 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
public void InitializeEndpoints_MultipleActions_WithActionConstraint() public void InitializeEndpoints_MultipleActions_WithActionConstraint()
{ {
// Arrange // Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(); var actionDescriptorCollection = GetActionDescriptorCollection(
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor> new { controller = "TestController", action = "TestAction" },
{ new { controller = "TestController", action = "TestAction1" },
CreateActionDescriptor("TestController", "TestAction"), new { controller = "TestController", action = "TestAction2" });
CreateActionDescriptor("TestController", "TestAction1"), var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
CreateActionDescriptor("TestController", "TestAction2")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
string.Empty, string.Empty,
"{controller}/{action}", "{controller}/{action}",
@ -297,16 +280,12 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
public void InitializeEndpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates) public void InitializeEndpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates)
{ {
// Arrange // Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(); var actionDescriptorCollection = GetActionDescriptorCollection(
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor> new { controller = "TestController1", action = "TestAction1" },
{ new { controller = "TestController1", action = "TestAction2" },
CreateActionDescriptor("TestController1", "TestAction1"), new { controller = "TestController1", action = "TestAction3" },
CreateActionDescriptor("TestController1", "TestAction2"), new { controller = "TestController2", action = "TestAction1" });
CreateActionDescriptor("TestController1", "TestAction3"), var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
CreateActionDescriptor("TestController2", "TestAction1")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
string.Empty, string.Empty,
endpointInfoRoute)); endpointInfoRoute));
@ -322,6 +301,276 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
Assert.Collection(dataSource.Endpoints, inspectors); Assert.Collection(dataSource.Endpoints, inspectors);
} }
[Fact]
public void ConventionalRoute_WithNoRouteName_DoesNotAddRouteNameMetadata()
{
// Arrange
var actionDescriptorCollection = GetActionDescriptorCollection(
new { controller = "Home", action = "Index" });
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(
CreateEndpointInfo(string.Empty, "named/{controller}/{action}/{id?}"));
// Act
dataSource.InitializeEndpoints();
// Assert
var endpoint = Assert.Single(dataSource.Endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata<IRouteNameMetadata>();
Assert.Null(routeNameMetadata);
}
[Fact]
public void CanCreateMultipleEndpoints_WithSameRouteName()
{
// Arrange
var actionDescriptorCollection = GetActionDescriptorCollection(
new { controller = "Home", action = "Index" },
new { controller = "Products", action = "Details" });
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(
CreateEndpointInfo("namedRoute", "named/{controller}/{action}/{id?}"));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Collection(
dataSource.Endpoints,
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata<IRouteNameMetadata>();
Assert.NotNull(routeNameMetadata);
Assert.Equal("namedRoute", routeNameMetadata.Name);
Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.Template);
},
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata<IRouteNameMetadata>();
Assert.NotNull(routeNameMetadata);
Assert.Equal("namedRoute", routeNameMetadata.Name);
Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.Template);
});
}
[Fact]
public void InitializeEndpoints_ConventionalRoutes_StaticallyDefinedOrder_IsMaintained()
{
// Arrange
var actionDescriptorCollection = GetActionDescriptorCollection(
new { controller = "Home", action = "Index" },
new { controller = "Products", action = "Details" });
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
name: string.Empty,
template: "{controller}/{action}/{id?}"));
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
name: "namedRoute",
"named/{controller}/{action}/{id?}"));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Collection(
dataSource.Endpoints,
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
Assert.Equal("Home/Index/{id?}", matcherEndpoint.Template);
Assert.Equal(1, matcherEndpoint.Order);
},
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.Template);
Assert.Equal(2, matcherEndpoint.Order);
},
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
Assert.Equal("Products/Details/{id?}", matcherEndpoint.Template);
Assert.Equal(1, matcherEndpoint.Order);
},
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.Template);
Assert.Equal(2, matcherEndpoint.Order);
});
}
[Fact]
public void RequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint()
{
// Arrange
var requiredValues = new RouteValueDictionary(new { area = "admin", controller = "home", action = "index" });
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}"));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Empty(dataSource.Endpoints);
}
// Since area, controller, action and page are special, check to see if the followin test succeeds for a
// custom required value too.
[Fact(Skip = "Needs review")]
public void NonReservedRequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint()
{
// Arrange
var requiredValues = new RouteValueDictionary(new { controller = "home", action = "index", foo = "bar" });
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}"));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Empty(dataSource.Endpoints);
}
[Fact]
public void TemplateParameter_WithNoDefaultOrRequiredValue_DoesNotProduceEndpoint()
{
// Arrange
var requiredValues = new RouteValueDictionary(new { controller = "home", action = "index" });
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}"));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Empty(dataSource.Endpoints);
}
[Fact]
public void TemplateParameter_WithDefaultValue_AndNullRequiredValue_DoesNotProduceEndpoint()
{
// Arrange
var requiredValues = new RouteValueDictionary(new { area = (string)null, controller = "home", action = "index" });
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area=admin}/{controller}/{action}"));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Empty(dataSource.Endpoints);
}
[Fact]
public void TemplateParameter_WithNullRequiredValue_DoesNotProduceEndpoint()
{
// Arrange
var requiredValues = new RouteValueDictionary(new { area = (string)null, controller = "home", action = "index" });
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}"));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Empty(dataSource.Endpoints);
}
[Fact]
public void NoDefaultValues_RequiredValues_UsedToCreateDefaultValues()
{
// Arrange
var expectedDefaults = new RouteValueDictionary(new { controller = "Foo", action = "Bar" });
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: expectedDefaults);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}"));
// Act
dataSource.InitializeEndpoints();
// Assert
var endpoint = Assert.Single(dataSource.Endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.Template);
AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults);
}
[Fact]
public void RequiredValues_NotPresent_InDefaultValues_IsAddedToDefaultValues()
{
// Arrange
var requiredValues = new RouteValueDictionary(
new { controller = "Foo", action = "Bar", subarea = "test" });
var expectedDefaults = requiredValues;
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(
CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}"));
// Act
dataSource.InitializeEndpoints();
// Assert
var endpoint = Assert.Single(dataSource.Endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.Template);
AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults);
}
[Fact]
public void RequiredValues_IsSubsetOf_DefaultValues()
{
// Arrange
var requiredValues = new RouteValueDictionary(
new { controller = "Foo", action = "Bar", subarea = "test" });
var expectedDefaults = new RouteValueDictionary(
new { controller = "Foo", action = "Bar", subarea = "test", subscription = "general" });
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(
CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}/{subscription=general}"));
// Act
dataSource.InitializeEndpoints();
// Assert
var endpoint = Assert.Single(dataSource.Endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.Template);
AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults);
}
[Fact]
public void RequiredValues_HavingNull_AndNotPresentInDefaultValues_IsAddedToDefaultValues()
{
// Arrange
var requiredValues = new RouteValueDictionary(
new { area = (string)null, controller = "Foo", action = "Bar", page = (string)null });
var expectedDefaults = requiredValues;
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(
CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}"));
// Act
dataSource.InitializeEndpoints();
// Assert
var endpoint = Assert.Single(dataSource.Endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.Template);
AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults);
}
private MvcEndpointDataSource CreateMvcEndpointDataSource( private MvcEndpointDataSource CreateMvcEndpointDataSource(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null,
MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null, MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null,
@ -362,18 +611,45 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, constraintResolver); return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, constraintResolver);
} }
private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params object[] requiredValues)
{
var actionDescriptors = new List<ActionDescriptor>();
foreach (var requiredValue in requiredValues)
{
actionDescriptors.Add(CreateActionDescriptor(requiredValue));
}
var actionDescriptorCollectionProvider = new Mock<IActionDescriptorCollectionProvider>();
actionDescriptorCollectionProvider
.Setup(m => m.ActionDescriptors)
.Returns(new ActionDescriptorCollection(actionDescriptors, version: 0));
return actionDescriptorCollectionProvider.Object;
}
private ActionDescriptor CreateActionDescriptor(string controller, string action, string area = null) private ActionDescriptor CreateActionDescriptor(string controller, string action, string area = null)
{ {
return new ActionDescriptor return CreateActionDescriptor(new { controller = controller, action = action, area = area });
}
private ActionDescriptor CreateActionDescriptor(object requiredValues)
{
var actionDescriptor = new ActionDescriptor();
var routeValues = new RouteValueDictionary(requiredValues);
foreach (var kvp in routeValues)
{ {
RouteValues = actionDescriptor.RouteValues[kvp.Key] = kvp.Value?.ToString();
{ }
["controller"] = controller, return actionDescriptor;
["action"] = action, }
["area"] = area
}, private void AssertIsSubset(RouteValueDictionary subset, RouteValueDictionary fullSet)
DisplayName = string.Empty, {
}; foreach (var subsetPair in subset)
{
var isPresent = fullSet.TryGetValue(subsetPair.Key, out var fullSetPairValue);
Assert.True(isPresent);
Assert.Equal(subsetPair.Value, fullSetPairValue);
}
} }
} }
} }

View File

@ -36,12 +36,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
endpoints.Add(new MatcherEndpoint( endpoints.Add(new MatcherEndpoint(
next => httpContext => Task.CompletedTask, next => httpContext => Task.CompletedTask,
template, template,
null, new RouteValueDictionary(),
new RouteValueDictionary(),
0, 0,
EndpointMetadataCollection.Empty, EndpointMetadataCollection.Empty,
null, null));
new Address(routeName)
));
return CreateUrlHelper(endpoints, appRoot, host, protocol); return CreateUrlHelper(endpoints, appRoot, host, protocol);
} }
@ -53,10 +52,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing
Endpoint = new MatcherEndpoint( Endpoint = new MatcherEndpoint(
next => cntxt => Task.CompletedTask, next => cntxt => Task.CompletedTask,
"/", "/",
new { }, new RouteValueDictionary(),
new RouteValueDictionary(),
0, 0,
EndpointMetadataCollection.Empty, EndpointMetadataCollection.Empty,
null,
null) null)
}); });
@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
string template, string template,
object defaults) object defaults)
{ {
var endpoint = GetEndpoint(routeName, template, defaults); var endpoint = GetEndpoint(routeName, template, new RouteValueDictionary(defaults));
var services = CreateServices(new[] { endpoint }); var services = CreateServices(new[] { endpoint });
var httpContext = CreateHttpContext(services, appRoot: "", host: null, protocol: null); var httpContext = CreateHttpContext(services, appRoot: "", host: null, protocol: null);
var actionContext = CreateActionContext(httpContext); var actionContext = CreateActionContext(httpContext);
@ -101,25 +100,36 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private List<MatcherEndpoint> GetDefaultEndpoints() private List<MatcherEndpoint> GetDefaultEndpoints()
{ {
var endpoints = new List<MatcherEndpoint>(); var endpoints = new List<MatcherEndpoint>();
endpoints.Add(new MatcherEndpoint( endpoints.Add(CreateEndpoint(null, "home/newaction/{id?}", new { id = "defaultid", controller = "home", action = "newaction" }, 1));
next => (httpContext) => Task.CompletedTask, endpoints.Add(CreateEndpoint(null, "home/contact/{id?}", new { id = "defaultid", controller = "home", action = "contact" }, 2));
"{controller}/{action}/{id}", endpoints.Add(CreateEndpoint(null, "home2/newaction/{id?}", new { id = "defaultid", controller = "home2", action = "newaction" }, 3));
new { id = "defaultid" }, endpoints.Add(CreateEndpoint(null, "home2/contact/{id?}", new { id = "defaultid", controller = "home2", action = "contact" }, 4));
0, endpoints.Add(CreateEndpoint(null, "home3/contact/{id?}", new { id = "defaultid", controller = "home3", action = "contact" }, 5));
EndpointMetadataCollection.Empty, endpoints.Add(CreateEndpoint("namedroute", "named/home/newaction/{id?}", new { id = "defaultid", controller = "home", action = "newaction" }, 6));
"RouteWithNoName", endpoints.Add(CreateEndpoint("namedroute", "named/home2/newaction/{id?}", new { id = "defaultid", controller = "home2", action = "newaction" }, 7));
address: null)); endpoints.Add(CreateEndpoint("namedroute", "named/home/contact/{id?}", new { id = "defaultid", controller = "home", action = "contact" }, 8));
endpoints.Add(new MatcherEndpoint( endpoints.Add(CreateEndpoint("MyRouteName", "any/url", new { }, 9));
next => (httpContext) => Task.CompletedTask,
"named/{controller}/{action}/{id}",
new { id = "defaultid" },
0,
EndpointMetadataCollection.Empty,
"RouteWithNoName",
new Address("namedroute")));
return endpoints; return endpoints;
} }
private MatcherEndpoint CreateEndpoint(string routeName, string template, object defaults, int order)
{
var metadata = EndpointMetadataCollection.Empty;
if (!string.IsNullOrEmpty(routeName))
{
metadata = new EndpointMetadataCollection(new[] { new RouteNameMetadata(routeName) });
}
return new MatcherEndpoint(
next => (httpContext) => Task.CompletedTask,
template,
new RouteValueDictionary(defaults),
new RouteValueDictionary(),
order,
metadata,
"DisplayName");
}
private IServiceProvider CreateServices(IEnumerable<Endpoint> endpoints) private IServiceProvider CreateServices(IEnumerable<Endpoint> endpoints)
{ {
if (endpoints == null) if (endpoints == null)
@ -135,16 +145,26 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }
private MatcherEndpoint GetEndpoint(string name, string template, object defaults) private MatcherEndpoint GetEndpoint(string name, string template, RouteValueDictionary defaults)
{ {
return new MatcherEndpoint( return new MatcherEndpoint(
next => c => Task.CompletedTask, next => c => Task.CompletedTask,
template, template,
defaults, defaults,
new RouteValueDictionary(),
0, 0,
EndpointMetadataCollection.Empty, EndpointMetadataCollection.Empty,
null, null);
new Address(name)); }
private class RouteNameMetadata : IRouteNameMetadata
{
public RouteNameMetadata(string routeName)
{
Name = routeName;
}
public string Name { get; }
} }
} }
} }