// 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.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Web.Http.Controllers; using System.Web.Http.Properties; using System.Web.Http.Routing; namespace System.Web.Http.Dispatcher { /// /// Default instance for choosing a given a /// A different implementation can be registered via the . /// public class DefaultHttpControllerSelector : IHttpControllerSelector { public static readonly string ControllerSuffix = "Controller"; private const string ControllerKey = "controller"; private readonly HttpConfiguration _configuration; private readonly HttpControllerTypeCache _controllerTypeCache; private readonly Lazy> _controllerInfoCache; /// /// Initializes a new instance of the class. /// /// The configuration. public DefaultHttpControllerSelector(HttpConfiguration configuration) { if (configuration == null) { throw Error.ArgumentNull("configuration"); } _controllerInfoCache = new Lazy>(InitializeControllerInfoCache); _configuration = configuration; _controllerTypeCache = new HttpControllerTypeCache(_configuration); } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for disposing of response instance.")] public virtual HttpControllerDescriptor SelectController(HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } IHttpRouteData routeData = request.GetRouteData(); HttpControllerDescriptor controllerDescriptor; if (routeData != null) { controllerDescriptor = GetDirectRouteController(routeData); if (controllerDescriptor != null) { return controllerDescriptor; } } string controllerName = GetControllerName(request); if (String.IsNullOrEmpty(controllerName)) { throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.NotFound, Error.Format(SRResources.ResourceNotFound, request.RequestUri), Error.Format(SRResources.ControllerNameNotFound, request.RequestUri))); } if (_controllerInfoCache.Value.TryGetValue(controllerName, out controllerDescriptor)) { return controllerDescriptor; } ICollection matchingTypes = _controllerTypeCache.GetControllerTypes(controllerName); // ControllerInfoCache is already initialized. Contract.Assert(matchingTypes.Count != 1); if (matchingTypes.Count == 0) { // no matching types throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.NotFound, Error.Format(SRResources.ResourceNotFound, request.RequestUri), Error.Format(SRResources.DefaultControllerFactory_ControllerNameNotFound, controllerName))); } else { // multiple matching types throw CreateAmbiguousControllerException(request.GetRouteData().Route, controllerName, matchingTypes); } } public virtual IDictionary GetControllerMapping() { return _controllerInfoCache.Value.ToDictionary(c => c.Key, c => c.Value, StringComparer.OrdinalIgnoreCase); } public virtual string GetControllerName(HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } IHttpRouteData routeData = request.GetRouteData(); if (routeData == null) { return null; } // Look up controller in route data string controllerName = null; routeData.Values.TryGetValue(ControllerKey, out controllerName); return controllerName; } // If routeData is from an attribute route, get the controller that can handle it. // Else return null. Throws an exception if multiple controllers match private static HttpControllerDescriptor GetDirectRouteController(IHttpRouteData routeData) { CandidateAction[] candidates = routeData.GetDirectRouteCandidates(); if (candidates != null) { // Set the controller descriptor for the first action descriptor Contract.Assert(candidates.Length > 0); Contract.Assert(candidates[0].ActionDescriptor != null); HttpControllerDescriptor controllerDescriptor = candidates[0].ActionDescriptor.ControllerDescriptor; // Check that all other candidate action descriptors share the same controller descriptor for (int i = 1; i < candidates.Length; i++) { CandidateAction candidate = candidates[i]; if (candidate.ActionDescriptor.ControllerDescriptor != controllerDescriptor) { // We've found an ambiguity (multiple controllers matched) throw CreateDirectRouteAmbiguousControllerException(candidates); } } return controllerDescriptor; } return null; } private static Exception CreateDirectRouteAmbiguousControllerException(CandidateAction[] candidates) { Contract.Assert(candidates != null); Contract.Assert(candidates.Length > 1); HashSet matchingTypes = new HashSet(); for (int i = 0; i < candidates.Length; i++) { matchingTypes.Add(candidates[i].ActionDescriptor.ControllerDescriptor.ControllerType); } // we need to generate an exception containing all the controller types StringBuilder typeList = new StringBuilder(); foreach (Type matchedType in matchingTypes) { typeList.AppendLine(); typeList.Append(matchedType.FullName); } return Error.InvalidOperation(SRResources.DirectRoute_AmbiguousController, typeList, Environment.NewLine); } private static Exception CreateAmbiguousControllerException(IHttpRoute route, string controllerName, ICollection matchingTypes) { Contract.Assert(route != null); Contract.Assert(controllerName != null); Contract.Assert(matchingTypes != null); // Generate an exception containing all the controller types StringBuilder typeList = new StringBuilder(); foreach (Type matchedType in matchingTypes) { typeList.AppendLine(); typeList.Append(matchedType.FullName); } string errorMessage = Error.Format(SRResources.DefaultControllerFactory_ControllerNameAmbiguous_WithRouteTemplate, controllerName, route.RouteTemplate, typeList, Environment.NewLine); return new InvalidOperationException(errorMessage); } private ConcurrentDictionary InitializeControllerInfoCache() { var result = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); var duplicateControllers = new HashSet(); Dictionary> controllerTypeGroups = _controllerTypeCache.Cache; foreach (KeyValuePair> controllerTypeGroup in controllerTypeGroups) { string controllerName = controllerTypeGroup.Key; foreach (IGrouping controllerTypesGroupedByNs in controllerTypeGroup.Value) { foreach (Type controllerType in controllerTypesGroupedByNs) { if (result.Keys.Contains(controllerName)) { duplicateControllers.Add(controllerName); break; } else { result.TryAdd(controllerName, new HttpControllerDescriptor(_configuration, controllerName, controllerType)); } } } } foreach (string duplicateController in duplicateControllers) { HttpControllerDescriptor descriptor; result.TryRemove(duplicateController, out descriptor); } return result; } } }