296 lines
11 KiB
C#
296 lines
11 KiB
C#
// 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.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
|
using Microsoft.AspNetCore.Mvc.Core;
|
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.AspNetCore.Routing.Template;
|
|
using Microsoft.AspNetCore.Routing.Tree;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Internal;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
|
{
|
|
public class AttributeRoute : IRouter
|
|
{
|
|
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
|
|
private readonly IServiceProvider _services;
|
|
private readonly Func<ActionDescriptor[], IRouter> _handlerFactory;
|
|
|
|
private TreeRouter _router;
|
|
|
|
public AttributeRoute(
|
|
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
|
|
IServiceProvider services,
|
|
Func<ActionDescriptor[], IRouter> handlerFactory)
|
|
{
|
|
if (actionDescriptorCollectionProvider == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(actionDescriptorCollectionProvider));
|
|
}
|
|
|
|
if (services == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(services));
|
|
}
|
|
|
|
if (handlerFactory == null)
|
|
{
|
|
_handlerFactory = handlerFactory;
|
|
}
|
|
|
|
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
|
|
_services = services;
|
|
_handlerFactory = handlerFactory;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
|
{
|
|
var router = GetTreeRouter();
|
|
return router.GetVirtualPath(context);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task RouteAsync(RouteContext context)
|
|
{
|
|
var router = GetTreeRouter();
|
|
return router.RouteAsync(context);
|
|
}
|
|
|
|
private TreeRouter GetTreeRouter()
|
|
{
|
|
var actions = _actionDescriptorCollectionProvider.ActionDescriptors;
|
|
|
|
// This is a safe-race. We'll never set router back to null after initializing
|
|
// it on startup.
|
|
if (_router == null || _router.Version != actions.Version)
|
|
{
|
|
var builder = _services.GetRequiredService<TreeRouteBuilder>();
|
|
AddEntries(builder, actions);
|
|
_router = builder.Build(actions.Version);
|
|
}
|
|
|
|
return _router;
|
|
}
|
|
|
|
// internal for testing
|
|
internal void AddEntries(TreeRouteBuilder builder, ActionDescriptorCollection actions)
|
|
{
|
|
var routeInfos = GetRouteInfos(actions.Items);
|
|
|
|
// 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.
|
|
foreach (var routeInfo in routeInfos)
|
|
{
|
|
var defaults = new RouteValueDictionary();
|
|
foreach (var kvp in routeInfo.ActionDescriptor.RouteValues)
|
|
{
|
|
defaults.Add(kvp.Key, kvp.Value);
|
|
}
|
|
|
|
// We use the `NullRouter` as the route handler because we don't need to do anything for link
|
|
// generations. The TreeRouter does it all for us.
|
|
builder.MapOutbound(
|
|
NullRouter.Instance,
|
|
routeInfo.RouteTemplate,
|
|
defaults,
|
|
routeInfo.RouteName,
|
|
routeInfo.Order);
|
|
}
|
|
|
|
// We're creating one AttributeRouteMatchingEntry per group, so we need to identify the distinct set of
|
|
// groups. It's guaranteed that all members of the group have the same template and precedence,
|
|
// so we only need to hang on to a single instance of the RouteInfo for each group.
|
|
var groups = GroupRouteInfos(routeInfos);
|
|
foreach (var group in groups)
|
|
{
|
|
var handler = _handlerFactory(group.ToArray());
|
|
|
|
// 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.
|
|
builder.MapInbound(
|
|
handler,
|
|
group.Key.RouteTemplate,
|
|
group.Key.RouteName,
|
|
group.Key.Order);
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<IGrouping<RouteInfo, ActionDescriptor>> GroupRouteInfos(List<RouteInfo> routeInfos)
|
|
{
|
|
return routeInfos.GroupBy(r => r, r => r.ActionDescriptor, RouteInfoEqualityComparer.Instance);
|
|
}
|
|
|
|
private static List<RouteInfo> GetRouteInfos(IReadOnlyList<ActionDescriptor> actions)
|
|
{
|
|
var routeInfos = new List<RouteInfo>();
|
|
var errors = new List<RouteInfo>();
|
|
|
|
// This keeps a cache of 'Template' objects. It's a fairly common case that multiple actions
|
|
// will use the same route template string; thus, the `Template` object can be shared.
|
|
//
|
|
// For a relatively simple route template, the `Template` object will hold about 500 bytes
|
|
// of memory, so sharing is worthwhile.
|
|
var templateCache = new Dictionary<string, RouteTemplate>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var attributeRoutedActions = actions.Where(a => a.AttributeRouteInfo?.Template != null);
|
|
foreach (var action in attributeRoutedActions)
|
|
{
|
|
var routeInfo = GetRouteInfo(templateCache, action);
|
|
if (routeInfo.ErrorMessage == null)
|
|
{
|
|
routeInfos.Add(routeInfo);
|
|
}
|
|
else
|
|
{
|
|
errors.Add(routeInfo);
|
|
}
|
|
}
|
|
|
|
if (errors.Count > 0)
|
|
{
|
|
var allErrors = string.Join(
|
|
Environment.NewLine + Environment.NewLine,
|
|
errors.Select(
|
|
e => Resources.FormatAttributeRoute_IndividualErrorMessage(
|
|
e.ActionDescriptor.DisplayName,
|
|
Environment.NewLine,
|
|
e.ErrorMessage)));
|
|
|
|
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(Environment.NewLine, allErrors);
|
|
throw new InvalidOperationException(message);
|
|
}
|
|
|
|
return routeInfos;
|
|
}
|
|
|
|
private static RouteInfo GetRouteInfo(
|
|
Dictionary<string, RouteTemplate> templateCache,
|
|
ActionDescriptor action)
|
|
{
|
|
var routeInfo = new RouteInfo()
|
|
{
|
|
ActionDescriptor = action,
|
|
};
|
|
|
|
try
|
|
{
|
|
RouteTemplate parsedTemplate;
|
|
if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out parsedTemplate))
|
|
{
|
|
// Parsing with throw if the template is invalid.
|
|
parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template);
|
|
templateCache.Add(action.AttributeRouteInfo.Template, parsedTemplate);
|
|
}
|
|
|
|
routeInfo.RouteTemplate = parsedTemplate;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
routeInfo.ErrorMessage = ex.Message;
|
|
return routeInfo;
|
|
}
|
|
|
|
foreach (var kvp in action.RouteValues)
|
|
{
|
|
foreach (var parameter in routeInfo.RouteTemplate.Parameters)
|
|
{
|
|
if (string.Equals(kvp.Key, parameter.Name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
routeInfo.ErrorMessage = Resources.FormatAttributeRoute_CannotContainParameter(
|
|
routeInfo.RouteTemplate.TemplateText,
|
|
kvp.Key,
|
|
kvp.Value);
|
|
|
|
return routeInfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
routeInfo.Order = action.AttributeRouteInfo.Order;
|
|
routeInfo.RouteName = action.AttributeRouteInfo.Name;
|
|
|
|
return routeInfo;
|
|
}
|
|
|
|
private class RouteInfo
|
|
{
|
|
public ActionDescriptor ActionDescriptor { get; set; }
|
|
|
|
public string ErrorMessage { get; set; }
|
|
|
|
public int Order { get; set; }
|
|
|
|
public string RouteName { get; set; }
|
|
|
|
public RouteTemplate RouteTemplate { get; set; }
|
|
}
|
|
|
|
private class RouteInfoEqualityComparer : IEqualityComparer<RouteInfo>
|
|
{
|
|
public static readonly RouteInfoEqualityComparer Instance = new RouteInfoEqualityComparer();
|
|
|
|
public bool Equals(RouteInfo x, RouteInfo y)
|
|
{
|
|
if (x == null && y == null)
|
|
{
|
|
return true;
|
|
}
|
|
else if (x == null ^ y == null)
|
|
{
|
|
return false;
|
|
}
|
|
else if (x.Order != y.Order)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return string.Equals(
|
|
x.RouteTemplate.TemplateText,
|
|
y.RouteTemplate.TemplateText,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
public int GetHashCode(RouteInfo obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var hash = new HashCodeCombiner();
|
|
hash.Add(obj.Order);
|
|
hash.Add(obj.RouteTemplate.TemplateText, StringComparer.OrdinalIgnoreCase);
|
|
return hash;
|
|
}
|
|
}
|
|
|
|
// Used only to hook up link generation, and it doesn't need to do anything.
|
|
private class NullRouter : IRouter
|
|
{
|
|
public static readonly NullRouter Instance = new NullRouter();
|
|
|
|
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public Task RouteAsync(RouteContext context)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
}
|
|
}
|