asp.Netcore mvc 3.1ソースコード分析(一)
92661 ワード
まずIApplicationBuilderインタフェースの拡張方法UseMvcを見てみましょう
まず、Mvc関連サービスが登録されているかどうかを判断します.
さらにMvcOptionsの属性を判断するEnableEndpointRoutingは、UseMvcを使用する場合はFalseに設定する必要があります
次に、DefaultHandlerプロパティがMvcRouteHandlerであるRouteBuilderオブジェクトを作成します.MvcRouteHandlerはmvcのプロセスに入ります.
RouteBuilderのRoutersプロパティに、MvcAttributeRouteHandlerに対応するプロパティルーティングを処理するためにAttributeRouteを挿入します.
ルーティングを登録すると、デフォルトではMvcRouteHandlerとMvcAttributeRouteHandlerの2つのHanderによってmvcロジックが出力されます.
普段どのようにルートを登録しているか見てみましょう
私たちが登録したrouteのデフォルトはIrouteBuilderのDefaultHandlerによって処理されます.
MvcRouteHandler
MvcRouteHandler IActionSelectorにより適切なActionDescriptor、すなわち対応するControllerのActionメソッドを見つける
IActionInvokerFactoryのCreateInvokerを呼び出してIActionInvokerオブジェクトを構築し、
最後にIActionInvokerオブジェクトのInvokeAsync実行アクションを呼び出す
MvcAttributeRouteHandler
MvcAttributeRouteHandlerとMvcRouteHandlerは論理差が少ない
主にAttributeRouteクラスが特性ルーティングをどのように処理するかを見てみましょう
まずTreeRouteBuilderオブジェクトを作成し、そのBuildメソッドを呼び出してTreeRouterを作成します.
public static IApplicationBuilder UseMvc(
this IApplicationBuilder app,
Action configureRoutes)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (configureRoutes == null)
{
throw new ArgumentNullException(nameof(configureRoutes));
}
VerifyMvcIsRegistered(app);
var options = app.ApplicationServices.GetRequiredService>();
if (options.Value.EnableEndpointRouting)
{
var message =
"Endpoint Routing does not support 'IApplicationBuilder.UseMvc(...)'. To use " +
"'IApplicationBuilder.UseMvc' set 'MvcOptions.EnableEndpointRouting = false' inside " +
"'ConfigureServices(...).";
throw new InvalidOperationException(message);
}
var routes = new RouteBuilder(app)
{
DefaultHandler = app.ApplicationServices.GetRequiredService(),
};
configureRoutes(routes);
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));
return app.UseRouter(routes.Build());
}
まず、Mvc関連サービスが登録されているかどうかを判断します.
さらにMvcOptionsの属性を判断するEnableEndpointRoutingは、UseMvcを使用する場合はFalseに設定する必要があります
次に、DefaultHandlerプロパティがMvcRouteHandlerであるRouteBuilderオブジェクトを作成します.MvcRouteHandlerはmvcのプロセスに入ります.
RouteBuilderのRoutersプロパティに、MvcAttributeRouteHandlerに対応するプロパティルーティングを処理するためにAttributeRouteを挿入します.
ルーティングを登録すると、デフォルトではMvcRouteHandlerとMvcAttributeRouteHandlerの2つのHanderによってmvcロジックが出力されます.
普段どのようにルートを登録しているか見てみましょう
public static IRouteBuilder MapRoute(
this IRouteBuilder routeBuilder,
string name,
string template,
object defaults,
object constraints,
object dataTokens)
{
if (routeBuilder.DefaultHandler == null)
{
throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder)));
}
routeBuilder.Routes.Add(new Route(
routeBuilder.DefaultHandler,
name,
template,
new RouteValueDictionary(defaults),
new RouteValueDictionary(constraints),
new RouteValueDictionary(dataTokens),
CreateInlineConstraintResolver(routeBuilder.ServiceProvider)));
return routeBuilder;
}
私たちが登録したrouteのデフォルトはIrouteBuilderのDefaultHandlerによって処理されます.
MvcRouteHandler
internal class MvcRouteHandler : IRouter
{
private readonly IActionInvokerFactory _actionInvokerFactory;
private readonly IActionSelector _actionSelector;
private readonly ILogger _logger;
private readonly DiagnosticListener _diagnosticListener;
public MvcRouteHandler(
IActionInvokerFactory actionInvokerFactory,
IActionSelector actionSelector,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory)
{
_actionInvokerFactory = actionInvokerFactory;
_actionSelector = actionSelector;
_diagnosticListener = diagnosticListener;
_logger = loggerFactory.CreateLogger();
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// We return null here because we're not responsible for generating the url, the route is.
return null;
}
public Task RouteAsync(RouteContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var candidates = _actionSelector.SelectCandidates(context);
if (candidates == null || candidates.Count == 0)
{
_logger.NoActionsMatched(context.RouteData.Values);
return Task.CompletedTask;
}
var actionDescriptor = _actionSelector.SelectBestCandidate(context, candidates);
if (actionDescriptor == null)
{
_logger.NoActionsMatched(context.RouteData.Values);
return Task.CompletedTask;
}
context.Handler = (c) =>
{
var routeData = c.GetRouteData();
var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor);
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatActionInvokerFactory_CouldNotCreateInvoker(
actionDescriptor.DisplayName));
}
return invoker.InvokeAsync();
};
return Task.CompletedTask;
}
}
MvcRouteHandler IActionSelectorにより適切なActionDescriptor、すなわち対応するControllerのActionメソッドを見つける
IActionInvokerFactoryのCreateInvokerを呼び出してIActionInvokerオブジェクトを構築し、
最後にIActionInvokerオブジェクトのInvokeAsync実行アクションを呼び出す
MvcAttributeRouteHandler
internal class MvcAttributeRouteHandler : IRouter
{
private readonly IActionInvokerFactory _actionInvokerFactory;
private readonly IActionSelector _actionSelector;
private readonly ILogger _logger;
private readonly DiagnosticListener _diagnosticListener;
public MvcAttributeRouteHandler(
IActionInvokerFactory actionInvokerFactory,
IActionSelector actionSelector,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory)
{
_actionInvokerFactory = actionInvokerFactory;
_actionSelector = actionSelector;
_diagnosticListener = diagnosticListener;
_logger = loggerFactory.CreateLogger();
}
public ActionDescriptor[] Actions { get; set; }
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// We return null here because we're not responsible for generating the url, the route is.
return null;
}
public Task RouteAsync(RouteContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (Actions == null)
{
var message = Resources.FormatPropertyOfTypeCannotBeNull(
nameof(Actions),
nameof(MvcAttributeRouteHandler));
throw new InvalidOperationException(message);
}
var actionDescriptor = _actionSelector.SelectBestCandidate(context, Actions);
if (actionDescriptor == null)
{
_logger.NoActionsMatched(context.RouteData.Values);
return Task.CompletedTask;
}
foreach (var kvp in actionDescriptor.RouteValues)
{
if (!string.IsNullOrEmpty(kvp.Value))
{
context.RouteData.Values[kvp.Key] = kvp.Value;
}
}
context.Handler = (c) =>
{
var routeData = c.GetRouteData();
var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor);
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatActionInvokerFactory_CouldNotCreateInvoker(
actionDescriptor.DisplayName));
}
return invoker.InvokeAsync();
};
return Task.CompletedTask;
}
}
MvcAttributeRouteHandlerとMvcRouteHandlerは論理差が少ない
主にAttributeRouteクラスが特性ルーティングをどのように処理するかを見てみましょう
internal class AttributeRoute : IRouter
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
private readonly IServiceProvider _services;
private readonly Func _handlerFactory;
private TreeRouter _router;
public AttributeRoute(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
IServiceProvider services,
Func handlerFactory)
{
if (actionDescriptorCollectionProvider == null)
{
throw new ArgumentNullException(nameof(actionDescriptorCollectionProvider));
}
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (handlerFactory == null)
{
throw new ArgumentNullException(nameof(handlerFactory));
}
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
_services = services;
_handlerFactory = handlerFactory;
}
///
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
var router = GetTreeRouter();
return router.GetVirtualPath(context);
}
///
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();
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)
{
if (routeInfo.SuppressLinkGeneration)
{
continue;
}
var defaults = new RouteValueDictionary();
foreach (var kvp in routeInfo.ActionDescriptor.RouteValues)
{
defaults.Add(kvp.Key, kvp.Value);
}
try
{
// 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);
}
catch (RouteCreationException routeCreationException)
{
throw new RouteCreationException(
"An error occurred while adding a route to the route builder. " +
$"Route name '{routeInfo.RouteName}' and template '{routeInfo.RouteTemplate.TemplateText}'.",
routeCreationException);
}
}
// 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 = GetInboundRouteGroups(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> GetInboundRouteGroups(List routeInfos)
{
return routeInfos
.Where(routeInfo => !routeInfo.SuppressPathMatching)
.GroupBy(r => r, r => r.ActionDescriptor, RouteInfoEqualityComparer.Instance);
}
private static List GetRouteInfos(IReadOnlyList actions)
{
var routeInfos = new List();
var errors = new List();
// 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 RouteCreationException(message);
}
return routeInfos;
}
private static RouteInfo GetRouteInfo(
Dictionary<string, RouteTemplate> templateCache,
ActionDescriptor action)
{
var routeInfo = new RouteInfo()
{
ActionDescriptor = action,
};
try
{
if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out var parsedTemplate))
{
// Parsing with throw if the template is invalid.
parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template);
templateCache.Add(action.AttributeRouteInfo.Template, parsedTemplate);
}
routeInfo.RouteTemplate = parsedTemplate;
routeInfo.SuppressPathMatching = action.AttributeRouteInfo.SuppressPathMatching;
routeInfo.SuppressLinkGeneration = action.AttributeRouteInfo.SuppressLinkGeneration;
}
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; }
public bool SuppressPathMatching { get; set; }
public bool SuppressLinkGeneration { get; set; }
}
private class RouteInfoEqualityComparer : IEqualityComparer
{
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();
}
}
}
まずTreeRouteBuilderオブジェクトを作成し、そのBuildメソッドを呼び出してTreeRouterを作成します.
public class TreeRouteBuilder
{
private readonly ILogger _logger;
private readonly ILogger _constraintLogger;
private readonly UrlEncoder _urlEncoder;
private readonly ObjectPool _objectPool;
private readonly IInlineConstraintResolver _constraintResolver;
///
/// Initializes a new instance of .
///
/// The .
/// The .
/// The .
internal TreeRouteBuilder(
ILoggerFactory loggerFactory,
ObjectPool objectPool,
IInlineConstraintResolver constraintResolver)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
if (objectPool == null)
{
throw new ArgumentNullException(nameof(objectPool));
}
if (constraintResolver == null)
{
throw new ArgumentNullException(nameof(constraintResolver));
}
_urlEncoder = UrlEncoder.Default;
_objectPool = objectPool;
_constraintResolver = constraintResolver;
_logger = loggerFactory.CreateLogger();
_constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName);
}
///
/// Adds a new inbound route to the .
///
/// The for handling the route.
/// The of the route.
/// The route name.
/// The route order.
/// The .
public InboundRouteEntry MapInbound(
IRouter handler,
RouteTemplate routeTemplate,
string routeName,
int order)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
if (routeTemplate == null)
{
throw new ArgumentNullException(nameof(routeTemplate));
}
var entry = new InboundRouteEntry()
{
Handler = handler,
Order = order,
Precedence = RoutePrecedence.ComputeInbound(routeTemplate),
RouteName = routeName,
RouteTemplate = routeTemplate,
};
var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText);
foreach (var parameter in routeTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var constraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
}
}
}
entry.Constraints = constraintBuilder.Build();
entry.Defaults = new RouteValueDictionary();
foreach (var parameter in entry.RouteTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
}
}
InboundEntries.Add(entry);
return entry;
}
///
/// Adds a new outbound route to the .
///
/// The for handling the link generation.
/// The of the route.
/// The containing the route values.
/// The route name.
/// The route order.
/// The .
public OutboundRouteEntry MapOutbound(
IRouter handler,
RouteTemplate routeTemplate,
RouteValueDictionary requiredLinkValues,
string routeName,
int order)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
if (routeTemplate == null)
{
throw new ArgumentNullException(nameof(routeTemplate));
}
if (requiredLinkValues == null)
{
throw new ArgumentNullException(nameof(requiredLinkValues));
}
var entry = new OutboundRouteEntry()
{
Handler = handler,
Order = order,
Precedence = RoutePrecedence.ComputeOutbound(routeTemplate),
RequiredLinkValues = requiredLinkValues,
RouteName = routeName,
RouteTemplate = routeTemplate,
};
var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText);
foreach (var parameter in routeTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var constraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
}
}
}
entry.Constraints = constraintBuilder.Build();
entry.Defaults = new RouteValueDictionary();
foreach (var parameter in entry.RouteTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
}
}
OutboundEntries.Add(entry);
return entry;
}
///
/// Gets the list of .
///
public IList InboundEntries { get; } = new List();
///
/// Gets the list of .
///
public IList OutboundEntries { get; } = new List();
///
/// Builds a with the
/// and defined in this .
///
/// The .
public TreeRouter Build()
{
return Build(version: 0);
}
///
/// Builds a with the
/// and defined in this .
///
/// The version of the .
/// The .
public TreeRouter Build(int version)
{
// Tree route builder builds a tree for each of the different route orders defined by
// the user. When a route needs to be matched, the matching algorithm in tree router
// just iterates over the trees in ascending order when it tries to match the route.
var trees = new Dictionary<int, UrlMatchingTree>();
foreach (var entry in InboundEntries)
{
if (!trees.TryGetValue(entry.Order, out var tree))
{
tree = new UrlMatchingTree(entry.Order);
trees.Add(entry.Order, tree);
}
tree.AddEntry(entry);
}
return new TreeRouter(
trees.Values.OrderBy(tree => tree.Order).ToArray(),
OutboundEntries,
_urlEncoder,
_objectPool,
_logger,
_constraintLogger,
version);
}
///
/// Removes all and from this
/// .
///
public void Clear()
{
InboundEntries.Clear();
OutboundEntries.Clear();
}
}
TreeRouter
///
/// An implementation for attribute routing.
///
public class TreeRouter : IRouter
{
// Key used by routing and action selection to match an attribute route entry to a
// group of action descriptors.
public static readonly string RouteGroupKey = "!__route_group";
private readonly LinkGenerationDecisionTree _linkGenerationTree;
private readonly UrlMatchingTree[] _trees;
private readonly IDictionary<string, OutboundMatch> _namedEntries;
private readonly ILogger _logger;
private readonly ILogger _constraintLogger;
///
/// Creates a new instance of .
///
/// The list of that contains the route entries.
/// The set of .
/// The .
/// The .
/// The instance.
/// The instance used
/// in .
/// The version of this route.
internal TreeRouter(
UrlMatchingTree[] trees,
IEnumerable linkGenerationEntries,
UrlEncoder urlEncoder,
ObjectPool objectPool,
ILogger routeLogger,
ILogger constraintLogger,
int version)
{
if (trees == null)
{
throw new ArgumentNullException(nameof(trees));
}
if (linkGenerationEntries == null)
{
throw new ArgumentNullException(nameof(linkGenerationEntries));
}
if (urlEncoder == null)
{
throw new ArgumentNullException(nameof(urlEncoder));
}
if (objectPool == null)
{
throw new ArgumentNullException(nameof(objectPool));
}
if (routeLogger == null)
{
throw new ArgumentNullException(nameof(routeLogger));
}
if (constraintLogger == null)
{
throw new ArgumentNullException(nameof(constraintLogger));
}
_trees = trees;
_logger = routeLogger;
_constraintLogger = constraintLogger;
_namedEntries = new Dictionary<string, OutboundMatch>(StringComparer.OrdinalIgnoreCase);
var outboundMatches = new List();
foreach (var entry in linkGenerationEntries)
{
var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults);
var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder };
outboundMatches.Add(outboundMatch);
// Skip unnamed entries
if (entry.RouteName == null)
{
continue;
}
// We only need to keep one OutboundMatch per route template
// so in case two entries have the same name and the same template we only keep
// the first entry.
if (_namedEntries.TryGetValue(entry.RouteName, out var namedMatch) &&
!string.Equals(
namedMatch.Entry.RouteTemplate.TemplateText,
entry.RouteTemplate.TemplateText,
StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName),
nameof(linkGenerationEntries));
}
else if (namedMatch == null)
{
_namedEntries.Add(entry.RouteName, outboundMatch);
}
}
// The decision tree will take care of ordering for these entries.
_linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray());
Version = version;
}
///
/// Gets the version of this route.
///
public int Version { get; }
internal IEnumerable MatchingTrees => _trees;
///
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(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.Values, context.AmbientValues);
if (matches == null)
{
return null;
}
for (var i = 0; i < matches.Count; i++)
{
var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder);
if (path != null)
{
return path;
}
}
return null;
}
///
public async Task RouteAsync(RouteContext context)
{
foreach (var tree in _trees)
{
var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
var root = tree.Root;
var treeEnumerator = new TreeEnumerator(root, tokenizer);
// Create a snapshot before processing the route. We'll restore this snapshot before running each
// to restore the state. This is likely an "empty" snapshot, which doesn't allocate.
var snapshot = context.RouteData.PushState(router: null, values: null, dataTokens: null);
while (treeEnumerator.MoveNext())
{
var node = treeEnumerator.Current;
foreach (var item in node.Matches)
{
var entry = item.Entry;
var matcher = item.TemplateMatcher;
try
{
if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values))
{
continue;
}
if (!RouteConstraintMatcher.Match(
entry.Constraints,
context.RouteData.Values,
context.HttpContext,
this,
RouteDirection.IncomingRequest,
_constraintLogger))
{
continue;
}
_logger.RequestMatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText);
context.RouteData.Routers.Add(entry.Handler);
await entry.Handler.RouteAsync(context);
if (context.Handler != null)
{
return;
}
}
finally
{
if (context.Handler == null)
{
// Restore the original values to prevent polluting the route data.
snapshot.Restore();
}
}
}
}
}
}
private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
{
if (_namedEntries.TryGetValue(context.RouteName, out var match))
{
var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder);
if (path != null)
{
return path;
}
}
return null;
}
private VirtualPathData GenerateVirtualPath(
VirtualPathContext context,
OutboundRouteEntry entry,
TemplateBinder binder)
{
// 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 RouteValueDictionary();
foreach (var kvp in context.Values)
{
if (entry.RequiredLinkValues.ContainsKey(kvp.Key))
{
var parameter = entry.RouteTemplate.GetParameter(kvp.Key);
if (parameter == null)
{
continue;
}
}
inputValues.Add(kvp.Key, kvp.Value);
}
var bindingResult = 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.HttpContext,
this,
RouteDirection.UrlGeneration,
_constraintLogger);
if (!matched)
{
// A constraint rejected this link.
return null;
}
var pathData = entry.Handler.GetVirtualPath(context);
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;
}
var path = binder.BindValues(bindingResult.AcceptedValues);
if (path == null)
{
return null;
}
return new VirtualPathData(this, path);
}
}