// 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); } } } }