// 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 System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web.Mvc.Async;
using System.Web.Mvc.Properties;
using System.Web.Routing;
namespace System.Web.Mvc.Routing
{
///
/// Represents a candidate action found via a direct route.
///
///
/// Members are get/set for testability of the 'best-match' algorithm.
///
internal class DirectRouteCandidate
{
public ActionDescriptor ActionDescriptor
{
get;
set;
}
public IEnumerable ActionNameSelectors
{
get;
set;
}
public IEnumerable ActionSelectors
{
get;
set;
}
public ControllerDescriptor ControllerDescriptor
{
get;
set;
}
public bool HasActionNameSelectors
{
get
{
return ActionNameSelectors != null && ActionNameSelectors.Any();
}
}
public bool HasActionSelectors
{
get
{
return ActionSelectors != null && ActionSelectors.Any();
}
}
public int Order
{
get;
set;
}
public decimal Precedence
{
get;
set;
}
public RouteData RouteData
{
get;
set;
}
public static DirectRouteCandidate SelectBestCandidate(List candidates, ControllerContext controllerContext)
{
Debug.Assert(controllerContext != null);
Debug.Assert(candidates != null);
// These filters will allow actions to opt-out of execution via the provided public extensibility points.
List filteredByActionName = ApplyActionNameFilters(candidates, controllerContext);
List applicableCandidates = ApplyActionSelectors(filteredByActionName, controllerContext);
// At this point all of the remaining actions are applicable - now we're just trying to find the
// most specific match.
//
// Order is first, because it's the 'override' to our algorithm
List filteredByOrder = FilterByOrder(applicableCandidates);
List filteredByPrecedence = FilterByPrecedence(filteredByOrder);
if (filteredByPrecedence.Count == 0)
{
return null;
}
else if (filteredByPrecedence.Count == 1)
{
return filteredByPrecedence[0];
}
else
{
throw CreateAmbiguiousMatchException(candidates);
}
}
private static AmbiguousMatchException CreateAmbiguiousMatchException(List candidates)
{
string ambiguityList = CreateAmbiguousMatchList(candidates);
string message = String.Format(
CultureInfo.CurrentCulture,
MvcResources.DirectRoute_AmbiguousMatch,
ambiguityList);
return new AmbiguousMatchException(message);
}
protected static string CreateAmbiguousMatchList(IEnumerable candidates)
{
StringBuilder exceptionMessageBuilder = new StringBuilder();
foreach (DirectRouteCandidate candidate in candidates)
{
MethodInfo method = null;
ReflectedActionDescriptor reflectedActionDescriptor = candidate.ActionDescriptor as ReflectedActionDescriptor;
if (reflectedActionDescriptor == null)
{
ReflectedAsyncActionDescriptor reflectedAsyncActionDescriptor = candidate.ActionDescriptor as ReflectedAsyncActionDescriptor;
if (reflectedAsyncActionDescriptor != null)
{
method = reflectedAsyncActionDescriptor.AsyncMethodInfo;
}
}
else
{
method = reflectedActionDescriptor.MethodInfo;
}
string controllerAction = method == null ? candidate.ActionDescriptor.ActionName : Convert.ToString(method, CultureInfo.CurrentCulture);
string controllerType = method.DeclaringType.FullName;
exceptionMessageBuilder.AppendLine();
exceptionMessageBuilder.AppendFormat(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatchType, controllerAction, controllerType);
}
return exceptionMessageBuilder.ToString();
}
private static List ApplyActionNameFilters(List candidates, ControllerContext controllerContext)
{
List filtered = new List();
foreach (DirectRouteCandidate candidate in candidates)
{
string actionName;
candidate.RouteData.Values.TryGetValue("action", out actionName);
if (candidate.HasActionNameSelectors)
{
// For the sake of consistency - we still want to call the action name selectors even if
// this route was matched without providing an action name.
actionName = actionName ?? candidate.ActionDescriptor.ActionName;
if (candidate.ActionNameSelectors.All(selector => selector(controllerContext, actionName)))
{
filtered.Add(candidate);
}
}
else if (actionName != null)
{
if (String.Equals(actionName, candidate.ActionDescriptor.ActionName, StringComparison.OrdinalIgnoreCase))
{
filtered.Add(candidate);
}
}
else
{
// No name-based filtering applies for this route.
filtered.Add(candidate);
}
}
return filtered;
}
private static List ApplyActionSelectors(List candidates, ControllerContext controllerContext)
{
List matchesWithActionSelectors = new List();
List matchesWithoutActionSelectors = new List();
foreach (DirectRouteCandidate candidate in candidates)
{
if (candidate.HasActionSelectors)
{
if (candidate.ActionSelectors.All(selector => selector(controllerContext)))
{
matchesWithActionSelectors.Add(candidate);
}
}
else
{
matchesWithoutActionSelectors.Add(candidate);
}
}
return matchesWithActionSelectors.Any() ? matchesWithActionSelectors : matchesWithoutActionSelectors;
}
private static List FilterByOrder(List candidates)
{
if (!candidates.Any())
{
return candidates;
}
int minimum = candidates.Min(c => c.Order);
return candidates.Where(c => c.Order == minimum).AsList();
}
private static List FilterByPrecedence(List candidates)
{
if (!candidates.Any())
{
return candidates;
}
decimal minimum = candidates.Min(c => c.Precedence);
return candidates.Where(c => c.Precedence == minimum).AsList();
}
}
}