Comment traiter un POST différemment sur une même ressource (adresse) d’un unique objet ou d’un tableau

Nous allons voir dans cet article comment fonctionne la possibilité avec WEB API 2 de sélectionner la bonne action à exécuter en fonction du type de ses arguments.

Prenons un cas concret. Vous souhaitez faire un POST sur une même adresse (api/values) et envoyer dans le corps de ce POST, un objet ou encore une liste de ce même objet. Si vous faite ce genre de code  :

// POST api/values
public void Post([FromBody]string value)
{
}

// POST api/values
public void PostMultipleValues([FromBody]List<string> value)
{
}

Vous allez gentiment soulever une exception indiquant qu'il ne sait pas quelle méthode choisir car il existe plusieurs prétendantes pour cette requête :

{
   "Message":"An error has occurred.",
   "ExceptionMessage":"Multiple actions were found that match the request: \r\nPost on type WebApplication2.Controllers.ValuesController\r\nPost on type WebApplication2.Controllers.ValuesController",
   "ExceptionType":"System.InvalidOperationException",
   "StackTrace":"   at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)\r\n   at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext)\r\n   at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)\r\n   at System.Web.Http.Dispatcher.HttpControllerDispatcher.d__1.MoveNext()"
}

Ainsi nous pouvons confirmer que la surcharge des méthodes ne fonctionne malheureusement pas dans ce cadre-ci.

Pour résoudre notre problème, une première indication est affichée dans notre stacktrace car soulève une erreur au niveau de la class ApiControllerActionSelector et c'est bien elle qui va nous intéresser ici. En effet, nous allons devoir l'étendre afin de la modifier et de bien choisir la méthode adéquate en fonction du contenu de notre POST.

Pour ce faire, rien de plus simple, vous allez créer une nouvelle classe qui sera nommée ApiActionSelector (mais que vous pouvez nommer comme bon vous semble) et qui va héritée de la classe ApiControllerActionSelector. A l’intérieur de cette même classe, vous aller "overrider" la méthode SelectAction afin d'avoir accès au contenu de l'objet HttpControllerContext (et donc au contenu du POST) et par la suite de retourner l'action voulue.

Voici un exemple de code :

public class ApiActionSelector : ApiControllerActionSelector
{
    public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
    {
        // Ici on récupère le contenu brute de notre POST
        var postContent = controllerContext.Request.Content.ReadAsStringAsync().Result;

        // On vérifie que le contenu du POST peut-être un Json Array et donc une collection
        var isArray = JsonConvert.DeserializeObject(postContent).GetType().Name == "JArray";

        // Pour cette condition, elle est à bien vérifier selon la configuration de vos routes, etc...
        var controller = controllerContext.RouteData.Values["controller"];
        if (controller != null && controller.ToString() == "values"
                                && controllerContext.Request.Method == HttpMethod.Post)
        {
            // Si c'est bien une collection, on redirige sur l'action voulue
            controllerContext.RouteData.Values["action"] = isArray ? "PostMultipleValues" : "Post";
        }
        
        return base.SelectAction(controllerContext);
    }
}

J'espère que mes commentaires vous permettent de bien comprendre le principe. "L'originalité" est la conversion avec JsonConverter du POST afin de savoir s'il s'agit ou non d'une collection.

Surtout ne pas oublié d'ajouter cette classe comme remplacement de l'ApiControllerActionSelector dans les services ! Dans le template de base de Visual Studio, il suffit d'aller dans le dossier App_Start, d'éditer le fichier WebApiConfig et d'ajouter dans la méthode Register :

config.Services.Replace(typeof(IHttpActionSelector), new ApiActionSelector());