// 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; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Routing; namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim { public class WebApiActionConventionsApplicationModelConvention : IControllerModelConvention { private static readonly string[] SupportedHttpMethodConventions = new string[] { "GET", "PUT", "POST", "DELETE", "PATCH", "HEAD", "OPTIONS", }; public void Apply(ControllerModel controller) { if (controller == null) { throw new ArgumentNullException(nameof(controller)); } if (IsConventionApplicable(controller)) { var newActions = new List(); foreach (var action in controller.Actions) { SetHttpMethodFromConvention(action); // Action Name doesn't really come into play with attribute routed actions. However for a // non-attribute-routed action we need to create a 'named' version and an 'unnamed' version. if (!IsActionAttributeRouted(action)) { var namedAction = action; var unnamedAction = new ActionModel(namedAction); unnamedAction.RouteValues.Add("action", null); newActions.Add(unnamedAction); } } foreach (var action in newActions) { controller.Actions.Add(action); } } } private bool IsConventionApplicable(ControllerModel controller) { return controller.Attributes.OfType().Any(); } private bool IsActionAttributeRouted(ActionModel action) { foreach (var controllerSelectorModel in action.Controller.Selectors) { if (controllerSelectorModel.AttributeRouteModel?.Template != null) { return true; } } foreach (var actionSelectorModel in action.Selectors) { if (actionSelectorModel.AttributeRouteModel?.Template != null) { return true; } } return false; } private void SetHttpMethodFromConvention(ActionModel action) { foreach (var selector in action.Selectors) { if (selector.ActionConstraints.OfType().Count() > 0) { // If the HttpMethods are set from attributes, don't override it with the convention return; } } // The Method name is used to infer verb constraints. Changing the action name has no impact. foreach (var verb in SupportedHttpMethodConventions) { if (action.ActionMethod.Name.StartsWith(verb, StringComparison.OrdinalIgnoreCase)) { foreach (var selector in action.Selectors) { selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { verb })); } return; } } // If no convention matches, then assume POST foreach (var actionSelectorModel in action.Selectors) { actionSelectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { "POST" })); } } } }