// 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.Contracts;
using System.Linq;
using System.Reflection;
using System.Web.Mvc.Properties;
using System.Web.Routing;
namespace System.Web.Mvc.Routing
{
public class DefaultDirectRouteProvider : IDirectRouteProvider
{
///
/// Gets direct routes for the given controller descriptor and action descriptors based on
/// attributes.
///
/// The controller descriptor.
/// The action descriptors for all actions.
/// The constraint resolver.
/// A set of route entries.
///
/// The implementation returns route entries for the given controller and actions.
///
/// Any actions that have associated instances will produce route
/// entries that route direct to those actions.
///
/// Any actions that do not have an associated instances will be
/// associated with the controller. If the controller has any associated
/// instances, then route entries will be created for the controller and associated actions.
///
public virtual IReadOnlyList GetDirectRoutes(
ControllerDescriptor controllerDescriptor,
IReadOnlyList actionDescriptors,
IInlineConstraintResolver constraintResolver)
{
List entries = new List();
List actionsWithoutRoutes = new List();
foreach (ActionDescriptor action in actionDescriptors)
{
IReadOnlyList factories = GetActionRouteFactories(action);
if (factories != null && factories.Count > 0)
{
IReadOnlyCollection actionEntries = GetActionDirectRoutes(action, factories, constraintResolver);
if (actionEntries != null)
{
entries.AddRange(actionEntries);
}
}
else
{
// IF there are no routes on the specific action, attach it to the controller routes (if any).
actionsWithoutRoutes.Add(action);
}
}
if (actionsWithoutRoutes.Count > 0)
{
IReadOnlyList controllerFactories = GetControllerRouteFactories(controllerDescriptor);
if (controllerFactories != null && controllerFactories.Count > 0)
{
IReadOnlyCollection controllerEntries = GetControllerDirectRoutes(
controllerDescriptor,
actionsWithoutRoutes,
controllerFactories,
constraintResolver);
if (controllerEntries != null)
{
entries.AddRange(controllerEntries);
}
}
}
return entries;
}
///
/// Gets route factories for the given controller descriptor.
///
/// The controller descriptor.
/// A set of route factories.
///
/// The implementation returns instances based on attributes on the controller.
///
protected virtual IReadOnlyList GetControllerRouteFactories(ControllerDescriptor controllerDescriptor)
{
object[] attributes = controllerDescriptor.GetCustomAttributes(inherit: false);
IEnumerable newFactories = attributes.OfType();
IEnumerable oldProviders = attributes.OfType();
List combined = new List();
combined.AddRange(newFactories);
foreach (IRouteInfoProvider oldProvider in oldProviders)
{
if (oldProvider is IDirectRouteFactory)
{
continue;
}
combined.Add(new RouteInfoDirectRouteFactory(oldProvider));
}
return combined;
}
///
/// Gets a set of route factories for the given action descriptor.
///
/// The action descriptor.
/// A set of route factories.
///
/// The implementation returns instances based on attributes on the action. Returns
/// null if the action was defined on a base class of this controller.
///
protected virtual IReadOnlyList GetActionRouteFactories(ActionDescriptor actionDescriptor)
{
// Skip Route attributes on inherited actions.
IMethodInfoActionDescriptor methodInfoActionDescriptor = actionDescriptor as IMethodInfoActionDescriptor;
if (methodInfoActionDescriptor != null &&
methodInfoActionDescriptor.MethodInfo != null &&
actionDescriptor.ControllerDescriptor != null &&
methodInfoActionDescriptor.MethodInfo.DeclaringType != actionDescriptor.ControllerDescriptor.ControllerType)
{
return null;
}
object[] attributes = actionDescriptor.GetCustomAttributes(inherit: false);
IEnumerable newFactories = attributes.OfType();
IEnumerable oldProviders = attributes.OfType();
List combined = new List();
combined.AddRange(newFactories);
foreach (IRouteInfoProvider oldProvider in oldProviders)
{
if (oldProvider is IDirectRouteFactory)
{
continue;
}
combined.Add(new RouteInfoDirectRouteFactory(oldProvider));
}
return combined;
}
///
/// Creates instances based on the provided factories, controller and actions. The route
/// entries provided direct routing to the provided controller and can reach the set of provided actions.
///
/// The controller descriptor.
/// The action descriptors.
/// The direct route factories.
/// The constraint resolver.
/// A set of route entries.
protected virtual IReadOnlyList GetControllerDirectRoutes(
ControllerDescriptor controllerDescriptor,
IReadOnlyList actionDescriptors,
IReadOnlyList factories,
IInlineConstraintResolver constraintResolver)
{
return CreateRouteEntries(
GetAreaPrefix(controllerDescriptor),
GetRoutePrefix(controllerDescriptor),
factories,
actionDescriptors,
constraintResolver,
targetIsAction: false);
}
///
/// Creates instances based on the provided factories and action. The route entries
/// provide direct routing to the provided action.
///
/// The action descriptor.
/// The direct route factories.
/// The constraint resolver.
/// A set of route entries.
protected virtual IReadOnlyList GetActionDirectRoutes(
ActionDescriptor actionDescriptor,
IReadOnlyList factories,
IInlineConstraintResolver constraintResolver)
{
return CreateRouteEntries(
GetAreaPrefix(actionDescriptor.ControllerDescriptor),
GetRoutePrefix(actionDescriptor.ControllerDescriptor),
factories,
new ActionDescriptor[] { actionDescriptor },
constraintResolver,
targetIsAction: true);
}
///
/// Gets the route prefix from the provided controller.
///
/// The controller descriptor.
/// The route prefix or null.
protected virtual string GetRoutePrefix(ControllerDescriptor controllerDescriptor)
{
IRoutePrefix[] attributes = controllerDescriptor.GetCustomAttributes(inherit: false).OfType().ToArray();
if (attributes == null)
{
return null;
}
if (attributes.Length > 1)
{
string errorMessage = Error.Format(
MvcResources.RoutePrefix_CannotSupportMultiRoutePrefix,
controllerDescriptor.ControllerType.FullName);
throw new InvalidOperationException(errorMessage);
}
if (attributes.Length == 1)
{
IRoutePrefix attribute = attributes[0];
if (attribute != null)
{
string prefix = attribute.Prefix;
if (prefix == null)
{
string errorMessage = Error.Format(
MvcResources.RoutePrefix_PrefixCannotBeNull,
controllerDescriptor.ControllerType.FullName);
throw new InvalidOperationException(errorMessage);
}
if (prefix.StartsWith("/", StringComparison.Ordinal)
|| prefix.EndsWith("/", StringComparison.Ordinal))
{
string errorMessage = Error.Format(
MvcResources.RoutePrefix_CannotStartOrEnd_WithForwardSlash, prefix,
controllerDescriptor.ControllerName);
throw new InvalidOperationException(errorMessage);
}
return prefix;
}
}
return null;
}
///
/// Gets the area prefix from the provided controller.
///
/// The controller descriptor.
/// The area prefix or null.
protected virtual string GetAreaPrefix(ControllerDescriptor controllerDescriptor)
{
RouteAreaAttribute area = controllerDescriptor.GetAreaFrom();
string areaName = controllerDescriptor.GetAreaName(area);
string areaPrefix = area != null ? area.AreaPrefix ?? area.AreaName : null;
ValidateAreaPrefixTemplate(areaPrefix, areaName, controllerDescriptor);
return areaPrefix;
}
private static IReadOnlyList CreateRouteEntries(
string areaPrefix,
string controllerPrefix,
IReadOnlyCollection factories,
IReadOnlyCollection actions,
IInlineConstraintResolver constraintResolver,
bool targetIsAction)
{
List entries = new List();
foreach (IDirectRouteFactory factory in factories)
{
RouteEntry entry = CreateRouteEntry(areaPrefix, controllerPrefix, factory, actions, constraintResolver, targetIsAction);
entries.Add(entry);
}
return entries;
}
// Internal for testing
internal static RouteEntry CreateRouteEntry(
string areaPrefix,
string controllerPrefix,
IDirectRouteFactory factory,
IReadOnlyCollection actions,
IInlineConstraintResolver constraintResolver,
bool targetIsAction)
{
Contract.Assert(factory != null);
DirectRouteFactoryContext context = new DirectRouteFactoryContext(
areaPrefix,
controllerPrefix,
actions,
constraintResolver,
targetIsAction);
RouteEntry entry = factory.CreateRoute(context);
if (entry == null)
{
throw Error.InvalidOperation(
MvcResources.TypeMethodMustNotReturnNull,
typeof(IDirectRouteFactory).Name,
"CreateRoute");
}
DirectRouteBuilder.ValidateRouteEntry(entry);
return entry;
}
private static void ValidateAreaPrefixTemplate(string areaPrefix, string areaName, ControllerDescriptor controllerDescriptor)
{
if (areaPrefix != null && areaPrefix.EndsWith("/", StringComparison.Ordinal))
{
string errorMessage = Error.Format(MvcResources.RouteAreaPrefix_CannotEnd_WithForwardSlash,
areaPrefix, areaName, controllerDescriptor.ControllerName);
throw new InvalidOperationException(errorMessage);
}
}
}
}