Files
Umbraco-CMS/src/Umbraco.Web/WebApi/ParameterSwapControllerActionSelector.cs

159 lines
6.4 KiB
C#

using System;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Http.Controllers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
namespace Umbraco.Web.WebApi
{
/// <summary>
/// This is used to auto-select specific actions on controllers that would otherwise be ambiguous based on a single parameter type
/// </summary>
/// <remarks>
/// As an example, lets say we have 2 methods: GetChildren(int id) and GetChildren(Guid id), by default Web Api won't allow this since
/// it won't know what to select, but if this Tuple is passed in new Tuple{string, string}("GetChildren", "id")
///
/// This supports POST values too however only for JSON values
/// </remarks>
internal class ParameterSwapControllerActionSelector : ApiControllerActionSelector
{
private readonly ParameterSwapInfo[] _actions;
/// <summary>
/// Constructor accepting a list of action name + parameter name
/// </summary>
/// <param name="actions"></param>
public ParameterSwapControllerActionSelector(params ParameterSwapInfo[] actions)
{
_actions = actions;
}
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
var found = _actions.FirstOrDefault(x => controllerContext.Request.RequestUri.GetLeftPart(UriPartial.Path).InvariantEndsWith(x.ActionName));
if (found != null)
{
HttpActionDescriptor method;
if (TryBindFromUri(controllerContext, found, out method))
{
return method;
}
//if it's a post we can try to read from the body and bind from the json value
if (controllerContext.Request.Method == HttpMethod.Post)
{
var requestContent = new HttpMessageContent(controllerContext.Request);
var strJson = requestContent.HttpRequestMessage.Content.ReadAsStringAsync().Result;
var json = JsonConvert.DeserializeObject<JObject>(strJson);
if (json == null)
{
return base.SelectAction(controllerContext);
}
var requestParam = json[found.ParamName];
if (requestParam != null)
{
var paramTypes = found.SupportedTypes;
foreach (var paramType in paramTypes)
{
try
{
var converted = requestParam.ToObject(paramType);
if (converted != null)
{
method = MatchByType(paramType, controllerContext, found);
if (method != null)
return method;
}
}
catch (JsonReaderException)
{
//can't convert
}
catch (JsonSerializationException)
{
//can't convert
}
}
}
}
}
return base.SelectAction(controllerContext);
}
private bool TryBindFromUri(HttpControllerContext controllerContext, ParameterSwapInfo found, out HttpActionDescriptor method)
{
var requestParam = HttpUtility.ParseQueryString(controllerContext.Request.RequestUri.Query).Get(found.ParamName);
requestParam = (requestParam == null) ? null : requestParam.Trim();
var paramTypes = found.SupportedTypes;
if (requestParam == string.Empty && paramTypes.Length > 0)
{
//if it's empty then in theory we can select any of the actions since they'll all need to deal with empty or null parameters
//so we'll try to use the first one available
method = MatchByType(paramTypes[0], controllerContext, found);
if (method != null)
return true;
}
if (requestParam != null)
{
foreach (var paramType in paramTypes)
{
//check if this is IEnumerable and if so this will get it's type
//we need to know this since the requestParam will always just be a string
var enumType = paramType.GetEnumeratedType();
var converted = requestParam.TryConvertTo(enumType ?? paramType);
if (converted)
{
method = MatchByType(paramType, controllerContext, found);
if (method != null)
return true;
}
}
}
method = null;
return false;
}
private static ReflectedHttpActionDescriptor MatchByType(Type idType, HttpControllerContext controllerContext, ParameterSwapInfo found)
{
var controllerType = controllerContext.Controller.GetType();
var methods = controllerType.GetMethods().Where(info => info.Name == found.ActionName).ToArray();
if (methods.Length > 1)
{
//choose the one that has the parameter with the T type
var method = methods.FirstOrDefault(x => x.GetParameters().FirstOrDefault(p => p.Name == found.ParamName && p.ParameterType == idType) != null);
return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, method);
}
return null;
}
internal class ParameterSwapInfo
{
public string ActionName { get; private set; }
public string ParamName { get; private set; }
public Type[] SupportedTypes { get; private set; }
public ParameterSwapInfo(string actionName, string paramName, params Type[] supportedTypes)
{
ActionName = actionName;
ParamName = paramName;
SupportedTypes = supportedTypes;
}
}
}
}