Fix #4447 - Teach MVC to understand defaults
The wireup for defaults in attribute routes was missing
This commit is contained in:
parent
e0c0617185
commit
3ec60a0181
|
|
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
|
||||||
|
|
||||||
public AttributeRouteInfo AttributeRouteInfo { get; set; }
|
public AttributeRouteInfo AttributeRouteInfo { get; set; }
|
||||||
|
|
||||||
public IDictionary<string, object> RouteValueDefaults { get; }
|
public IDictionary<string, object> RouteValueDefaults { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The set of constraints for this action. Must all be satisfied for the action to be selected.
|
/// The set of constraints for this action. Must all be satisfied for the action to be selected.
|
||||||
|
|
@ -53,6 +53,6 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores arbitrary metadata properties associated with the <see cref="ActionDescriptor"/>.
|
/// Stores arbitrary metadata properties associated with the <see cref="ActionDescriptor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDictionary<object, object> Properties { get; }
|
public IDictionary<object, object> Properties { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,23 +98,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// it on startup.
|
// it on startup.
|
||||||
if (_router == null || _router.Version != actions.Version)
|
if (_router == null || _router.Version != actions.Version)
|
||||||
{
|
{
|
||||||
_router = BuildRoute(actions);
|
var entries = GetEntries(actions);
|
||||||
|
_router = BuildRoute(entries, actions.Version);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _router;
|
return _router;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TreeRouter BuildRoute(ActionDescriptorCollection actions)
|
// internal for testing
|
||||||
|
internal AttributeRouteEntries GetEntries(ActionDescriptorCollection actions)
|
||||||
{
|
{
|
||||||
var routeBuilder = new TreeRouteBuilder(_target, _loggerFactory);
|
var entries = new AttributeRouteEntries();
|
||||||
|
|
||||||
var routeInfos = GetRouteInfos(_constraintResolver, actions.Items);
|
var routeInfos = GetRouteInfos(_constraintResolver, actions.Items);
|
||||||
|
|
||||||
// We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended
|
// We're creating one TreeRouteLinkGenerationEntry per action. This allows us to match the intended
|
||||||
// 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)
|
||||||
{
|
{
|
||||||
routeBuilder.Add(new TreeRouteLinkGenerationEntry()
|
entries.LinkGenerationEntries.Add(new TreeRouteLinkGenerationEntry()
|
||||||
{
|
{
|
||||||
|
// Using routeInfo.Defaults here WITHOUT adding the RouteGroupKey. We don't want to impact the
|
||||||
|
// behavior of link generation.
|
||||||
Binder = new TemplateBinder(_urlEncoder, _contextPool, routeInfo.ParsedTemplate, routeInfo.Defaults),
|
Binder = new TemplateBinder(_urlEncoder, _contextPool, routeInfo.ParsedTemplate, routeInfo.Defaults),
|
||||||
Defaults = routeInfo.Defaults,
|
Defaults = routeInfo.Defaults,
|
||||||
Constraints = routeInfo.Constraints,
|
Constraints = routeInfo.Constraints,
|
||||||
|
|
@ -133,24 +138,44 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos);
|
var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos);
|
||||||
foreach (var routeInfo in distinctRouteInfosByGroup)
|
foreach (var routeInfo in distinctRouteInfosByGroup)
|
||||||
{
|
{
|
||||||
routeBuilder.Add(new TreeRouteMatchingEntry()
|
// Note that because we only support 'inline' defaults, each routeInfo group also has the same
|
||||||
|
// set of defaults.
|
||||||
|
//
|
||||||
|
// We then inject the route group as a default for the matcher so it gets passed back to MVC
|
||||||
|
// for use in action selection.
|
||||||
|
var defaults = new RouteValueDictionary(routeInfo.Defaults);
|
||||||
|
defaults[TreeRouter.RouteGroupKey] = routeInfo.RouteGroup;
|
||||||
|
|
||||||
|
entries.MatchingEntries.Add(new TreeRouteMatchingEntry()
|
||||||
{
|
{
|
||||||
Order = routeInfo.Order,
|
Order = routeInfo.Order,
|
||||||
Precedence = routeInfo.MatchPrecedence,
|
Precedence = routeInfo.MatchPrecedence,
|
||||||
Target = _target,
|
Target = _target,
|
||||||
RouteName = routeInfo.Name,
|
RouteName = routeInfo.Name,
|
||||||
RouteTemplate = TemplateParser.Parse(routeInfo.RouteTemplate),
|
RouteTemplate = routeInfo.ParsedTemplate,
|
||||||
TemplateMatcher = new TemplateMatcher(
|
TemplateMatcher = new TemplateMatcher(routeInfo.ParsedTemplate, defaults),
|
||||||
routeInfo.ParsedTemplate,
|
Constraints = routeInfo.Constraints,
|
||||||
new RouteValueDictionary(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
{ TreeRouter.RouteGroupKey, routeInfo.RouteGroup }
|
|
||||||
}),
|
|
||||||
Constraints = routeInfo.Constraints
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return routeBuilder.Build(actions.Version);
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TreeRouter BuildRoute(AttributeRouteEntries entries, int version)
|
||||||
|
{
|
||||||
|
var routeBuilder = new TreeRouteBuilder(_target, _loggerFactory);
|
||||||
|
|
||||||
|
foreach (var entry in entries.LinkGenerationEntries)
|
||||||
|
{
|
||||||
|
routeBuilder.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in entries.MatchingEntries)
|
||||||
|
{
|
||||||
|
routeBuilder.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return routeBuilder.Build(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<RouteInfo> GroupRouteInfosByGroupId(List<RouteInfo> routeInfos)
|
private static IEnumerable<RouteInfo> GroupRouteInfosByGroupId(List<RouteInfo> routeInfos)
|
||||||
|
|
@ -182,8 +207,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// of memory, so sharing is worthwhile.
|
// of memory, so sharing is worthwhile.
|
||||||
var templateCache = new Dictionary<string, RouteTemplate>(StringComparer.OrdinalIgnoreCase);
|
var templateCache = new Dictionary<string, RouteTemplate>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var attributeRoutedActions = actions.Where(a => a.AttributeRouteInfo != null &&
|
var attributeRoutedActions = actions.Where(a => a.AttributeRouteInfo?.Template != null);
|
||||||
a.AttributeRouteInfo.Template != null);
|
|
||||||
foreach (var action in attributeRoutedActions)
|
foreach (var action in attributeRoutedActions)
|
||||||
{
|
{
|
||||||
var routeInfo = GetRouteInfo(constraintResolver, templateCache, action);
|
var routeInfo = GetRouteInfo(constraintResolver, templateCache, action);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// 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<TreeRouteLinkGenerationEntry> LinkGenerationEntries { get; } = new List<TreeRouteLinkGenerationEntry>();
|
||||||
|
|
||||||
|
public List<TreeRouteMatchingEntry> MatchingEntries { get; } = new List<TreeRouteMatchingEntry>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,13 +3,16 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
using Microsoft.AspNetCore.Routing.Tree;
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
|
@ -31,14 +34,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
public async Task AttributeRoute_UsesUpdatedActionDescriptors()
|
public async Task AttributeRoute_UsesUpdatedActionDescriptors()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var handler = new Mock<IRouter>(MockBehavior.Strict);
|
var handler = CreateHandler();
|
||||||
handler
|
|
||||||
.Setup(h => h.RouteAsync(It.IsAny<RouteContext>()))
|
|
||||||
.Callback<RouteContext>(c => c.Handler = NullHandler)
|
|
||||||
.Returns(Task.FromResult(true))
|
|
||||||
.Verifiable();
|
|
||||||
|
|
||||||
var actionDescriptors = new List<ActionDescriptor>()
|
var actions = new List<ActionDescriptor>()
|
||||||
{
|
{
|
||||||
new ActionDescriptor()
|
new ActionDescriptor()
|
||||||
{
|
{
|
||||||
|
|
@ -64,21 +62,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(MockBehavior.Strict);
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
actionDescriptorProvider
|
var route = CreateRoute(handler.Object, actionDescriptorProvider.Object);
|
||||||
.SetupGet(ad => ad.ActionDescriptors)
|
|
||||||
.Returns(new ActionDescriptorCollection(actionDescriptors, version: 1));
|
|
||||||
|
|
||||||
var policy = new UriBuilderContextPooledObjectPolicy(new UrlTestEncoder());
|
|
||||||
var pool = new DefaultObjectPool<UriBuildingContext>(policy);
|
|
||||||
|
|
||||||
var route = new AttributeRoute(
|
|
||||||
handler.Object,
|
|
||||||
actionDescriptorProvider.Object,
|
|
||||||
Mock.Of<IInlineConstraintResolver>(),
|
|
||||||
pool,
|
|
||||||
new UrlTestEncoder(),
|
|
||||||
NullLoggerFactory.Instance);
|
|
||||||
|
|
||||||
var requestServices = new Mock<IServiceProvider>(MockBehavior.Strict);
|
var requestServices = new Mock<IServiceProvider>(MockBehavior.Strict);
|
||||||
requestServices
|
requestServices
|
||||||
|
|
@ -102,10 +87,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());
|
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());
|
||||||
|
|
||||||
// Arrange 2 - remove the action and update the collection
|
// Arrange 2 - remove the action and update the collection
|
||||||
actionDescriptors.RemoveAt(1);
|
actions.RemoveAt(1);
|
||||||
actionDescriptorProvider
|
actionDescriptorProvider
|
||||||
.SetupGet(ad => ad.ActionDescriptors)
|
.SetupGet(ad => ad.ActionDescriptors)
|
||||||
.Returns(new ActionDescriptorCollection(actionDescriptors, version: 2));
|
.Returns(new ActionDescriptorCollection(actions, version: 2));
|
||||||
|
|
||||||
context = new RouteContext(httpContext);
|
context = new RouteContext(httpContext);
|
||||||
|
|
||||||
|
|
@ -118,5 +103,513 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());
|
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GetEntries_CreatesLinkGenerationEntry()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{id}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
|
|
||||||
|
var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
entries.LinkGenerationEntries,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(e.Binder);
|
||||||
|
Assert.Empty(e.Constraints);
|
||||||
|
Assert.Empty(e.Defaults);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.Name);
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(actions[0].RouteValueDefaults, e.RequiredLinkValues);
|
||||||
|
Assert.Equal("1", e.RouteGroup);
|
||||||
|
Assert.Equal("api/Blog/{id}", e.Template.TemplateText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GetEntries_CreatesLinkGenerationEntry_WithConstraint()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{id:int}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
|
|
||||||
|
var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
entries.LinkGenerationEntries,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(e.Binder);
|
||||||
|
Assert.Single(e.Constraints, kvp => kvp.Key == "id");
|
||||||
|
Assert.Empty(e.Defaults);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.Name);
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(actions[0].RouteValueDefaults, e.RequiredLinkValues);
|
||||||
|
Assert.Equal("1", e.RouteGroup);
|
||||||
|
Assert.Equal("api/Blog/{id:int}", e.Template.TemplateText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GetEntries_CreatesLinkGenerationEntry_WithDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{*slug=hello}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
|
|
||||||
|
var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
entries.LinkGenerationEntries,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(e.Binder);
|
||||||
|
Assert.Empty(e.Constraints);
|
||||||
|
Assert.Equal(new RouteValueDictionary(new { slug = "hello" }), e.Defaults);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.Name);
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(actions[0].RouteValueDefaults, e.RequiredLinkValues);
|
||||||
|
Assert.Equal("1", e.RouteGroup);
|
||||||
|
Assert.Equal("api/Blog/{*slug=hello}", e.Template.TemplateText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// These actions seem like duplicates, but this is a real case that can happen where two different
|
||||||
|
// actions define the same route info. Link generation happens based on the action name + controller
|
||||||
|
// name.
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GetEntries_CreatesLinkGenerationEntry_ForEachAction()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{id}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{id}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index2" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
|
|
||||||
|
var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
entries.LinkGenerationEntries,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(e.Binder);
|
||||||
|
Assert.Empty(e.Constraints);
|
||||||
|
Assert.Empty(e.Defaults);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.Name);
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(actions[0].RouteValueDefaults, e.RequiredLinkValues);
|
||||||
|
Assert.Equal("1", e.RouteGroup);
|
||||||
|
Assert.Equal("api/Blog/{id}", e.Template.TemplateText);
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.NotNull(e.Binder);
|
||||||
|
Assert.Empty(e.Constraints);
|
||||||
|
Assert.Empty(e.Defaults);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeGenerated(e.Template), e.GenerationPrecedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.Name);
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(actions[1].RouteValueDefaults, e.RequiredLinkValues);
|
||||||
|
Assert.Equal("1", e.RouteGroup);
|
||||||
|
Assert.Equal("api/Blog/{id}", e.Template.TemplateText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GetEntries_CreatesMatchingEntry()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{id}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
|
|
||||||
|
var handler = CreateHandler().Object;
|
||||||
|
var route = CreateRoute(handler, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
entries.MatchingEntries,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Empty(e.Constraints);
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeMatched(e.RouteTemplate), e.Precedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
|
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
||||||
|
Assert.Same(handler, e.Target);
|
||||||
|
Assert.Collection(
|
||||||
|
e.TemplateMatcher.Defaults.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp => Assert.Equal(new KeyValuePair<string, object>(TreeRouter.RouteGroupKey, "1"), kvp));
|
||||||
|
Assert.Same(e.RouteTemplate, e.TemplateMatcher.Template);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GetEntries_CreatesMatchingEntry_WithConstraint()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{id:int}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
|
|
||||||
|
var handler = CreateHandler().Object;
|
||||||
|
var route = CreateRoute(handler, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
entries.MatchingEntries,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Single(e.Constraints, kvp => kvp.Key == "id");
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeMatched(e.RouteTemplate), e.Precedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
|
Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
|
||||||
|
Assert.Same(handler, e.Target);
|
||||||
|
Assert.Collection(
|
||||||
|
e.TemplateMatcher.Defaults.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp => Assert.Equal(new KeyValuePair<string, object>(TreeRouter.RouteGroupKey, "1"), kvp));
|
||||||
|
Assert.Same(e.RouteTemplate, e.TemplateMatcher.Template);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GetEntries_CreatesMatchingEntry_WithDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{*slug=hello}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
|
|
||||||
|
var handler = CreateHandler().Object;
|
||||||
|
var route = CreateRoute(handler, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
entries.MatchingEntries,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Empty(e.Constraints);
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeMatched(e.RouteTemplate), e.Precedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
|
Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
|
||||||
|
Assert.Same(handler, e.Target);
|
||||||
|
Assert.Collection(
|
||||||
|
e.TemplateMatcher.Defaults.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp => Assert.Equal(new KeyValuePair<string, object>(TreeRouter.RouteGroupKey, "1"), kvp),
|
||||||
|
kvp => Assert.Equal(new KeyValuePair<string, object>("slug", "hello"), kvp));
|
||||||
|
Assert.Same(e.RouteTemplate, e.TemplateMatcher.Template);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// These actions seem like duplicates, but this is a real case that can happen where two different
|
||||||
|
// actions define the same route info. Link generation happens based on the action name + controller
|
||||||
|
// name.
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GetEntries_CreatesMatchingEntry_CombinesLikeActions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = new List<ActionDescriptor>()
|
||||||
|
{
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{id}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new ActionDescriptor()
|
||||||
|
{
|
||||||
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
|
{
|
||||||
|
Template = "api/Blog/{id}",
|
||||||
|
Name = "BLOG_INDEX",
|
||||||
|
Order = 17,
|
||||||
|
},
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>()
|
||||||
|
{
|
||||||
|
new RouteDataActionConstraint(TreeRouter.RouteGroupKey, "1"),
|
||||||
|
},
|
||||||
|
RouteValueDefaults = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{ "controller", "Blog" },
|
||||||
|
{ "action", "Index2" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
|
||||||
|
|
||||||
|
var handler = CreateHandler().Object;
|
||||||
|
var route = CreateRoute(handler, actionDescriptorProvider.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var entries = route.GetEntries(actionDescriptorProvider.Object.ActionDescriptors);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
entries.MatchingEntries,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Empty(e.Constraints);
|
||||||
|
Assert.Equal(17, e.Order);
|
||||||
|
Assert.Equal(RoutePrecedence.ComputeMatched(e.RouteTemplate), e.Precedence);
|
||||||
|
Assert.Equal("BLOG_INDEX", e.RouteName);
|
||||||
|
Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
|
||||||
|
Assert.Same(handler, e.Target);
|
||||||
|
Assert.Collection(
|
||||||
|
e.TemplateMatcher.Defaults.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp => Assert.Equal(new KeyValuePair<string, object>(TreeRouter.RouteGroupKey, "1"), kvp));
|
||||||
|
Assert.Same(e.RouteTemplate, e.TemplateMatcher.Template);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mock<IRouter> CreateHandler()
|
||||||
|
{
|
||||||
|
var handler = new Mock<IRouter>(MockBehavior.Strict);
|
||||||
|
handler
|
||||||
|
.Setup(h => h.RouteAsync(It.IsAny<RouteContext>()))
|
||||||
|
.Callback<RouteContext>(c => c.Handler = NullHandler)
|
||||||
|
.Returns(TaskCache.CompletedTask)
|
||||||
|
.Verifiable();
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mock<IActionDescriptorCollectionProvider> CreateActionDescriptorProvider(
|
||||||
|
IReadOnlyList<ActionDescriptor> actions)
|
||||||
|
{
|
||||||
|
var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(MockBehavior.Strict);
|
||||||
|
actionDescriptorProvider
|
||||||
|
.SetupGet(ad => ad.ActionDescriptors)
|
||||||
|
.Returns(new ActionDescriptorCollection(actions, version: 1));
|
||||||
|
|
||||||
|
return actionDescriptorProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttributeRoute CreateRoute(
|
||||||
|
IRouter handler,
|
||||||
|
IActionDescriptorCollectionProvider actionDescriptorProvider)
|
||||||
|
{
|
||||||
|
var constraintResolver = new Mock<IInlineConstraintResolver>();
|
||||||
|
constraintResolver
|
||||||
|
.Setup(c => c.ResolveConstraint("int"))
|
||||||
|
.Returns(new IntRouteConstraint());
|
||||||
|
|
||||||
|
var policy = new UriBuilderContextPooledObjectPolicy(new UrlTestEncoder());
|
||||||
|
var pool = new DefaultObjectPool<UriBuildingContext>(policy);
|
||||||
|
|
||||||
|
var route = new AttributeRoute(
|
||||||
|
handler,
|
||||||
|
actionDescriptorProvider,
|
||||||
|
constraintResolver.Object,
|
||||||
|
pool,
|
||||||
|
new UrlTestEncoder(),
|
||||||
|
NullLoggerFactory.Instance);
|
||||||
|
|
||||||
|
return route;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ namespace RoutingWebSite
|
||||||
return Content(Url.Action(), "text/plain");
|
return Content(Url.Action(), "text/plain");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("/TeamName/{*Name}/")]
|
[HttpGet("/TeamName/{*Name=DefaultName}/")]
|
||||||
public ActionResult GetTeam(string name = "DefaultName")
|
public ActionResult GetTeam(string name)
|
||||||
{
|
{
|
||||||
return _generator.Generate("/TeamName/" + name);
|
return _generator.Generate("/TeamName/" + name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue