aspnetcore/src/Microsoft.AspNet.Mvc.Core/Routing/InnerAttributeRoute.cs

329 lines
13 KiB
C#

// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Internal.Routing;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.Internal;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Mvc.Routing
{
/// <summary>
/// An <see cref="IRouter"/> implementation for attribute routing.
/// </summary>
public class InnerAttributeRoute : IRouter
{
private readonly IRouter _next;
private readonly LinkGenerationDecisionTree _linkGenerationTree;
private readonly AttributeRouteMatchingEntry[] _matchingEntries;
private readonly IDictionary<string, AttributeRouteLinkGenerationEntry> _namedEntries;
private ILogger _logger;
private ILogger _constraintLogger;
/// <summary>
/// Creates a new <see cref="InnerAttributeRoute"/>.
/// </summary>
/// <param name="next">The next router. Invoked when a route entry matches.</param>
/// <param name="entries">The set of route entries.</param>
public InnerAttributeRoute(
[NotNull] IRouter next,
[NotNull] IEnumerable<AttributeRouteMatchingEntry> matchingEntries,
[NotNull] IEnumerable<AttributeRouteLinkGenerationEntry> linkGenerationEntries,
[NotNull] ILogger logger,
[NotNull] ILogger constraintLogger,
int version)
{
_next = next;
_logger = logger;
_constraintLogger = constraintLogger;
Version = version;
// Order all the entries by order, then precedence, and then finally by template in order to provide
// a stable routing and link generation order for templates with same order and precedence.
// We use ordinal comparison for the templates because we only care about them being exactly equal and
// we don't want to make any equivalence between templates based on the culture of the machine.
_matchingEntries = matchingEntries
.OrderBy(o => o.Order)
.ThenBy(e => e.Precedence)
.ThenBy(e => e.RouteTemplate, StringComparer.Ordinal)
.ToArray();
var namedEntries = new Dictionary<string, AttributeRouteLinkGenerationEntry>(
StringComparer.OrdinalIgnoreCase);
foreach (var entry in linkGenerationEntries)
{
// Skip unnamed entries
if (entry.Name == null)
{
continue;
}
// We only need to keep one AttributeRouteLinkGenerationEntry per route template
// so in case two entries have the same name and the same template we only keep
// the first entry.
AttributeRouteLinkGenerationEntry namedEntry = null;
if (namedEntries.TryGetValue(entry.Name, out namedEntry) &&
!namedEntry.TemplateText.Equals(entry.TemplateText, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name),
"linkGenerationEntries");
}
else if (namedEntry == null)
{
namedEntries.Add(entry.Name, entry);
}
}
_namedEntries = namedEntries;
// The decision tree will take care of ordering for these entries.
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
}
/// <summary>
/// Gets the version of this route. This corresponds to the value of
/// <see cref="ActionDescriptorsCollection.Version"/> when this route was created.
/// </summary>
public int Version { get; }
/// <inheritdoc />
public async Task RouteAsync([NotNull] RouteContext context)
{
foreach(var matchingEntry in _matchingEntries)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
requestPath = requestPath.Substring(1);
}
var values = matchingEntry.TemplateMatcher.Match(requestPath);
if (values == null)
{
// If we got back a null value set, that means the URI did not match
continue;
}
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(matchingEntry.Target);
MergeValues(newRouteData.Values, values);
if (!RouteConstraintMatcher.Match(
matchingEntry.Constraints,
newRouteData.Values,
context.HttpContext,
this,
RouteDirection.IncomingRequest,
_constraintLogger))
{
continue;
}
_logger.LogInformation(
"Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'.",
matchingEntry.RouteName,
matchingEntry.RouteTemplate);
try
{
context.RouteData = newRouteData;
await matchingEntry.Target.RouteAsync(context);
}
finally
{
// Restore the original values to prevent polluting the route data.
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
if (context.IsHandled)
{
break;
}
}
if (!context.IsHandled)
{
_logger.LogVerbose("Request did not match any attribute route.");
}
}
/// <inheritdoc />
public VirtualPathData GetVirtualPath([NotNull] VirtualPathContext context)
{
// If it's a named route we will try to generate a link directly and
// if we can't, we will not try to generate it using an unnamed route.
if (context.RouteName != null)
{
return GetVirtualPathForNamedRoute(context);
}
// The decision tree will give us back all entries that match the provided route data in the correct
// order. We just need to iterate them and use the first one that can generate a link.
var matches = _linkGenerationTree.GetMatches(context);
foreach (var match in matches)
{
var path = GenerateVirtualPath(context, match.Entry);
if (path != null)
{
context.IsBound = true;
return path;
}
}
return null;
}
private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
{
AttributeRouteLinkGenerationEntry entry;
if (_namedEntries.TryGetValue(context.RouteName, out entry))
{
var path = GenerateVirtualPath(context, entry);
if (path != null)
{
context.IsBound = true;
return path;
}
}
return null;
}
private VirtualPathData GenerateVirtualPath(VirtualPathContext context, AttributeRouteLinkGenerationEntry entry)
{
// In attribute the context includes the values that are used to select this entry - typically
// these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't
// want to pass these to the link generation code, or else they will end up as query parameters.
//
// So, we need to exclude from here any values that are 'required link values', but aren't
// parameters in the template.
//
// Ex:
// template: api/Products/{action}
// required values: { id = "5", action = "Buy", Controller = "CoolProducts" }
//
// result: { id = "5", action = "Buy" }
var inputValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in context.Values)
{
if (entry.RequiredLinkValues.ContainsKey(kvp.Key))
{
var parameter = entry.Template.Parameters
.FirstOrDefault(p => string.Equals(p.Name, kvp.Key, StringComparison.OrdinalIgnoreCase));
if (parameter == null)
{
continue;
}
}
inputValues.Add(kvp.Key, kvp.Value);
}
var bindingResult = entry.Binder.GetValues(context.AmbientValues, inputValues);
if (bindingResult == null)
{
// A required parameter in the template didn't get a value.
return null;
}
var matched = RouteConstraintMatcher.Match(
entry.Constraints,
bindingResult.CombinedValues,
context.Context,
this,
RouteDirection.UrlGeneration,
_constraintLogger);
if (!matched)
{
// A constraint rejected this link.
return null;
}
// These values are used to signal to the next route what we would produce if we round-tripped
// (generate a link and then parse). In MVC the 'next route' is typically the MvcRouteHandler.
var providedValues = new Dictionary<string, object>(
bindingResult.AcceptedValues,
StringComparer.OrdinalIgnoreCase);
providedValues.Add(AttributeRouting.RouteGroupKey, entry.RouteGroup);
var childContext = new VirtualPathContext(context.Context, context.AmbientValues, context.Values)
{
ProvidedValues = providedValues,
};
var pathData = _next.GetVirtualPath(childContext);
if (pathData != null)
{
// If path is non-null then the target router short-circuited, we don't expect this
// in typical MVC scenarios.
return pathData;
}
else if (!childContext.IsBound)
{
// The target router has rejected these values. We don't expect this in typical MVC scenarios.
return null;
}
var path = entry.Binder.BindValues(bindingResult.AcceptedValues);
if (path == null)
{
return null;
}
return new VirtualPathData(this, path);
}
private static void MergeValues(
IDictionary<string, object> destination,
IDictionary<string, object> values)
{
foreach (var kvp in values)
{
// This will replace the original value for the specified key.
// Values from the matched route will take preference over previous
// data in the route context.
destination[kvp.Key] = kvp.Value;
}
}
private bool ContextHasSameValue(VirtualPathContext context, string key, object value)
{
object providedValue;
if (!context.Values.TryGetValue(key, out providedValue))
{
// If the required value is an 'empty' route value, then ignore ambient values.
// This handles a case where we're generating a link to an action like:
// { area = "", controller = "Home", action = "Index" }
//
// and the ambient values has a value for area.
if (value != null)
{
context.AmbientValues.TryGetValue(key, out providedValue);
}
}
return TemplateBinder.RoutePartsEqual(providedValue, value);
}
}
}