// 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.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using System.Web.Http.Properties; namespace System.Web.Http.Routing { /// /// Corresponds to the MVC implementation of attribute routing in System.Web.Mvc.Routing.AttributeRoutingMapper. /// internal static class AttributeRoutingMapper { // Attribute routing will inject a single top-level route into the route table. private const string AttributeRouteName = "MS_attributerouteWebApi"; public static void MapAttributeRoutes( HttpConfiguration configuration, IInlineConstraintResolver constraintResolver, IDirectRouteProvider directRouteProvider) { if (configuration == null) { throw new ArgumentNullException("configuration"); } if (constraintResolver == null) { throw new ArgumentNullException("constraintResolver"); } if (directRouteProvider == null) { throw new ArgumentNullException("directRouteProvider"); } RouteCollectionRoute aggregateRoute = new RouteCollectionRoute(); configuration.Routes.Add(AttributeRouteName, aggregateRoute); Action previousInitializer = configuration.Initializer; configuration.Initializer = config => { // Chain to the previous initializer hook. Do this before we access the config since // initialization may make last minute changes to the configuration. previousInitializer(config); SubRouteCollection subRoutes = null; // Add a single placeholder route that handles all of attribute routing. // Add an initialize hook that initializes these routes after the config has been initialized. Func initializer = () => { subRoutes = new SubRouteCollection(); AddRouteEntries(subRoutes, configuration, constraintResolver, directRouteProvider); return subRoutes; }; // This won't change config. It wants to pick up the finalized config. aggregateRoute.EnsureInitialized(initializer); if (subRoutes != null) { AddGenerationHooksForSubRoutes(config.Routes, subRoutes.Entries); } }; } // Add generation hooks for the Attribute-routing subroutes. // This lets us generate urls for routes supplied by attr-based routing. private static void AddGenerationHooksForSubRoutes(HttpRouteCollection routeTable, IEnumerable entries) { Contract.Assert(entries != null); foreach (RouteEntry entry in entries) { Contract.Assert(entry != null); string name = entry.Name; if (name != null) { IHttpRoute route = entry.Route; Contract.Assert(route != null); IHttpRoute linkGenerationRoute = new LinkGenerationRoute(route); routeTable.Add(name, linkGenerationRoute); } } } [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "This method is a coordinator, so this coupling is expected. Also fine unless building Debug configuration.")] private static void AddRouteEntries( SubRouteCollection collector, HttpConfiguration configuration, IInlineConstraintResolver constraintResolver, IDirectRouteProvider directRouteProvider) { Contract.Assert(configuration != null); Contract.Assert(directRouteProvider != null); IHttpControllerSelector controllerSelector = configuration.Services.GetHttpControllerSelector(); IDictionary controllerMap = controllerSelector.GetControllerMapping(); if (controllerMap != null) { foreach (HttpControllerDescriptor controllerDescriptor in controllerMap.Values) { IHttpActionSelector actionSelector = controllerDescriptor.Configuration.Services.GetActionSelector(); ILookup actionsByName = actionSelector.GetActionMapping(controllerDescriptor); if (actionsByName == null) { continue; } List actions = actionsByName.SelectMany(g => g).ToList(); IReadOnlyCollection newEntries = directRouteProvider.GetDirectRoutes(controllerDescriptor, actions, constraintResolver); if (newEntries == null) { throw Error.InvalidOperation( SRResources.TypeMethodMustNotReturnNull, typeof(IDirectRouteProvider).Name, "GetDirectRoutes"); } foreach (RouteEntry entry in newEntries) { if (entry == null) { throw Error.InvalidOperation( SRResources.TypeMethodMustNotReturnNull, typeof(IDirectRouteProvider).Name, "GetDirectRoutes"); } DirectRouteBuilder.ValidateRouteEntry(entry); // We need to mark each action as only reachable by direct routes so that traditional routes // don't accidentally hit them. HttpControllerDescriptor routeControllerDescriptor = entry.Route.GetTargetControllerDescriptor(); if (routeControllerDescriptor == null) { HttpActionDescriptor[] actionDescriptors = entry.Route.GetTargetActionDescriptors(); foreach (var actionDescriptor in actionDescriptors) { actionDescriptor.SetIsAttributeRouted(true); } } else { routeControllerDescriptor.SetIsAttributeRouted(true); } } collector.AddRange(newEntries); } } } } }