Contrôleur unique avec plusieurs méthodes GET dans l'API Web ASP.NET

167

Dans l'API Web, j'avais une classe de structure similaire:

public class SomeController : ApiController
{
    [WebGet(UriTemplate = "{itemSource}/Items")]
    public SomeValue GetItems(CustomParam parameter) { ... }

    [WebGet(UriTemplate = "{itemSource}/Items/{parent}")]
    public SomeValue GetChildItems(CustomParam parameter, SomeObject parent) { ... }
}

Comme nous pouvions mapper des méthodes individuelles, il était très simple d'obtenir la bonne demande au bon endroit. Pour une classe similaire qui n'avait qu'une seule GETméthode mais aussi un Objectparamètre, j'ai utilisé avec succès IActionValueBinder. Cependant, dans le cas décrit ci-dessus, j'obtiens l'erreur suivante:

Multiple actions were found that match the request: 

SomeValue GetItems(CustomParam parameter) on type SomeType

SomeValue GetChildItems(CustomParam parameter, SomeObject parent) on type SomeType

J'essaie d'aborder ce problème en annulant la ExecuteAsyncméthode de ApiControllermais sans succès jusqu'à présent. Un conseil à ce sujet?

Edit: J'ai oublié de mentionner que j'essaie maintenant de déplacer ce code sur l'API Web ASP.NET qui a une approche différente du routage. La question est: comment faire fonctionner le code sur l'API Web ASP.NET?

paulius_l
la source
1
Avez-vous toujours le {parent} comme RouteParameter.Optional?
Antony Scott le
Oui je l'ai fait. Peut-être que j'utilise IActionValueBinder de la mauvaise manière car pour des types tels que int id (comme dans la démo), cela fonctionne bien.
paulius_l
Désolé, j'aurais dû être plus clair. J'aurais pensé que l'avoir comme facultatif signifierait qu'il correspond à la route des éléments ainsi qu'à la route des sous-éléments, ce qui expliquerait le message d'erreur que vous voyez.
Antony Scott
Nous avons actuellement la discussion, si les approches ci-dessous (avec plusieurs itinéraires) sont contre les règles REST appropriées? À mon avis, c'est bien. Mon collègue pense que ce n'est pas gentil. Des commentaires à ce sujet?
Remy
J'étais généralement contre quand j'ai commencé à lire sur REST. Je ne sais toujours pas si c'est une approche appropriée, mais parfois elle est plus pratique ou plus conviviale, donc contourner légèrement les règles n'est peut-être pas si grave. Tant que cela fonctionne pour résoudre un problème spécifique. 6 mois se sont déjà écoulés depuis que j'ai posté cette question et nous n'avons eu aucun regret d'utiliser cette approche depuis.
paulius_l

Réponses:

249

C'est le meilleur moyen que j'ai trouvé pour prendre en charge des méthodes GET supplémentaires et prendre en charge les méthodes REST normales. Ajoutez les routes suivantes à votre WebApiConfig:

routes.MapHttpRoute("DefaultApiWithId", "Api/{controller}/{id}", new { id = RouteParameter.Optional }, new { id = @"\d+" });
routes.MapHttpRoute("DefaultApiWithAction", "Api/{controller}/{action}");
routes.MapHttpRoute("DefaultApiGet", "Api/{controller}", new { action = "Get" }, new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) });
routes.MapHttpRoute("DefaultApiPost", "Api/{controller}", new {action = "Post"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Post)});

J'ai vérifié cette solution avec la classe de test ci-dessous. J'ai pu réussir chaque méthode de mon contrôleur ci-dessous:

public class TestController : ApiController
{
    public string Get()
    {
        return string.Empty;
    }

    public string Get(int id)
    {
        return string.Empty;
    }

    public string GetAll()
    {
        return string.Empty;
    }

    public void Post([FromBody]string value)
    {
    }

    public void Put(int id, [FromBody]string value)
    {
    }

    public void Delete(int id)
    {
    }
}

J'ai vérifié qu'il prend en charge les demandes suivantes:

GET /Test
GET /Test/1
GET /Test/GetAll
POST /Test
PUT /Test/1
DELETE /Test/1

Notez que si vos actions GET supplémentaires ne commencent pas par «Get», vous souhaiterez peut-être ajouter un attribut HttpGet à la méthode.

sky-dev
la source
4
C'est une excellente réponse et cela m'a beaucoup aidé avec une autre question connexe. Merci!!
Alfero Chingono
4
J'ai essayé cela - ne semble pas fonctionner. Les itinéraires sont tous mappés de manière aléatoire à la méthode GetBlah (long id). :(
BrainSlugs83
1
@ BrainSlugs83: Cela dépend de l'ordre. Et vous voudrez ajouter (aux méthodes "withId"), aconstraints: new{id=@"\d+"}
Eric Falsken
4
que diriez-vous d'ajouter une autre méthode - Get (int id, nom de chaîne)? ... ça échoue
Anil Purswani
1
J'ai dû ajouter un itinéraire supplémentaire comme celui-ci routes.MapHttpRoute("DefaultApiPut", "Api/{controller}", new {action = "Put"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Put)});pour ma Putméthode, sinon cela me donnait 404.
Syed Ali Taqi
57

Allez de ceci:

config.Routes.MapHttpRoute("API Default", "api/{controller}/{id}",
            new { id = RouteParameter.Optional });

Pour ça:

config.Routes.MapHttpRoute("API Default", "api/{controller}/{action}/{id}",
            new { id = RouteParameter.Optional });

Par conséquent, vous pouvez maintenant spécifier à quelle action (méthode) vous souhaitez envoyer votre requête HTTP.

la publication sur "http: // localhost: 8383 / api / Command / PostCreateUser" appelle:

public bool PostCreateUser(CreateUserCommand command)
{
    //* ... *//
    return true;
}

et la publication sur "http: // localhost: 8383 / api / Command / PostMakeBooking" appelle:

public bool PostMakeBooking(MakeBookingCommand command)
{
    //* ... *//
    return true;
}

J'ai essayé cela dans une application de service API WEB auto-hébergée et cela fonctionne comme un charme :)

uggeh
la source
8
Merci pour la réponse utile. Je voudrais ajouter que si vous démarrez vos noms de méthodes avec Get, Post, etc., vos demandes seront mappées à ces méthodes en fonction du verbe HTTP utilisé. Mais vous pouvez aussi le nom de votre méthodes quoi que ce soit, puis les décorer avec les [HttpGet], [HttpPost]etc. attributs pour cartographier le verbe à la méthode.
indot_brad
veuillez voir ma question
Moeez
@DikaArtaKarunia pas de problème, heureux que ma réponse soit toujours applicable 6 ans plus tard: D
uggeh
31

Je trouve que les attributs sont plus propres à utiliser que de les ajouter manuellement via le code. Voici un exemple simple.

[RoutePrefix("api/example")]
public class ExampleController : ApiController
{
    [HttpGet]
    [Route("get1/{param1}")] //   /api/example/get1/1?param2=4
    public IHttpActionResult Get(int param1, int param2)
    {
        Object example = null;
        return Ok(example);
    }

}

Vous en avez également besoin dans votre webapiconfig

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

config.Routes.MapHttpRoute(
    name: "ActionApi",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Quelques bons liens http://www.asp.net/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api Celui-ci explique mieux le routage. http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api

Kalel Wade
la source
3
J'avais besoin d'ajouter également config.MapHttpAttributeRoutes();à mon WebApiConfig.cs, et GlobalConfiguration.Configuration.EnsureInitialized();à la fin de ma WebApiApplication.Application_Start()méthode pour que les attributs de route fonctionnent.
Ergwun
@Ergwun Ce commentaire m'a beaucoup aidé. Juste pour ajouter à cela, config.MapHttpAttributeRoutes();doit apparaître avant la cartographie de l'itinéraire (par exemple avant config.Routes.MappHttpRoute(....
Philip Stratford
11

Vous devez définir d'autres routes dans global.asax.cs comme ceci:

routes.MapHttpRoute(
    name: "Api with action",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
Alexander Zeitler
la source
5
Oui, c'est vrai, mais ce serait bien de voir un exemple de ces itinéraires. Cela rendrait cette réponse plus précieuse pour la communauté. (et vous obtiendrez un +1 de ma part :)
Aran Mulholland
Vous pouvez lire un exemple ici - stackoverflow.com/questions/11407267/…
Tom Kerkhove
2
Une solution réelle aurait été plus agréable.
So Many Goblins
6

Avec le nouveau Web Api 2, il est devenu plus facile d'avoir plusieurs méthodes d'obtention.

Si les paramètres passés aux GETméthodes sont suffisamment différents pour que le système de routage d'attributs distingue leurs types comme c'est le cas avec ints et Guids, vous pouvez spécifier le type attendu dans l' [Route...]attribut

Par exemple -

[RoutePrefix("api/values")]
public class ValuesController : ApiController
{

    // GET api/values/7
    [Route("{id:int}")]
    public string Get(int id)
    {
       return $"You entered an int - {id}";
    }

    // GET api/values/AAC1FB7B-978B-4C39-A90D-271A031BFE5D
    [Route("{id:Guid}")]
    public string Get(Guid id)
    {
       return $"You entered a GUID - {id}";
    }
} 

Pour plus de détails sur cette approche, voir ici http://nodogmablog.bryanhogan.net/2017/02/web-api-2-controller-with-multiple-get-methods-part-2/

Une autre option consiste à donner aux GETméthodes des itinéraires différents.

    [RoutePrefix("api/values")]
    public class ValuesController : ApiController
    {
        public string Get()
        {
            return "simple get";
        }

        [Route("geta")]
        public string GetA()
        {
            return "A";
        }

        [Route("getb")]
        public string GetB()
        {
            return "B";
        }
   }

Voir ici pour plus de détails - http://nodogmablog.bryanhogan.net/2016/10/web-api-2-controller-with-multiple-get-methods/

Bryan
la source
5

Dans ASP.NET Core 2.0, vous pouvez ajouter l' attribut Route au contrôleur:

[Route("api/[controller]/[action]")]
public class SomeController : Controller
{
    public SomeValue GetItems(CustomParam parameter) { ... }

    public SomeValue GetChildItems(CustomParam parameter, SomeObject parent) { ... }
}
Maskalek
la source
4

J'essayais d'utiliser le routage d'attributs Web Api 2 pour autoriser plusieurs méthodes Get, et j'avais intégré les suggestions utiles des réponses précédentes, mais dans le contrôleur, j'avais seulement décoré la méthode "spéciale" (exemple):

[Route( "special/{id}" )]
public IHttpActionResult GetSomethingSpecial( string id ) {

... sans également placer un [RoutePrefix] en haut du Controller:

[RoutePrefix("api/values")]
public class ValuesController : ApiController

J'obtenais des erreurs indiquant qu'aucune route ne correspondait à l'URI soumis. Une fois que j'ai eu à la fois la [Route] décorant la méthode ainsi que [RoutePrefix] décorant le contrôleur dans son ensemble, cela a fonctionné.

StackOverflowUser
la source
3

Je ne sais pas si vous avez trouvé la réponse, mais je l'ai fait et ça marche

public IEnumerable<string> Get()
{
    return new string[] { "value1", "value2" };
}

// GET /api/values/5
public string Get(int id)
{
    return "value";
}

// GET /api/values/5
[HttpGet]
public string GetByFamily()
{
    return "Family value";
}

Maintenant dans global.asx

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapHttpRoute(
    name: "DefaultApi2",
    routeTemplate: "api/{controller}/{action}",
    defaults: new { id = RouteParameter.Optional }
);

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Pavan Josyula
la source
3

Avez-vous essayé de basculer vers WebInvokeAttribute et de définir la méthode sur "GET"?

Je crois que j'ai eu un problème similaire et je suis passé à dire explicitement quelle méthode (GET / PUT / POST / DELETE) est attendue sur la plupart, sinon la totalité, de mes méthodes.

public class SomeController : ApiController
{
    [WebInvoke(UriTemplate = "{itemSource}/Items"), Method="GET"]
    public SomeValue GetItems(CustomParam parameter) { ... }

    [WebInvoke(UriTemplate = "{itemSource}/Items/{parent}", Method = "GET")]
    public SomeValue GetChildItems(CustomParam parameter, SomeObject parent) { ... }
}

Le WebGet devrait le gérer, mais j'ai vu qu'il avait des problèmes avec plusieurs Get beaucoup moins multiples Get du même type de retour.

[Modifier: rien de tout cela n'est valide avec le coucher du soleil de WCF WebAPI et la migration vers ASP.Net WebAPI sur la pile MVC]

PMontgomery
la source
1
Je suis désolé, j'ai oublié de mentionner que je déplace le code vers l'API Web ASP.NET car l'API Web WCF a été interrompue. J'ai édité le message. Je vous remercie.
paulius_l
2
**Add Route function to direct the routine what you want**
    public class SomeController : ApiController
    {
        [HttpGet()]
        [Route("GetItems")]
        public SomeValue GetItems(CustomParam parameter) { ... }

        [HttpGet()]
        [Route("GetChildItems")]
        public SomeValue GetChildItems(CustomParam parameter, SomeObject parent) { ... }
    }
JackyShen
la source
Bienvenue dans Stack Overflow! Veuillez modifier votre réponse pour inclure une explication de votre code, ainsi qu'une description de la façon dont il est différent des quatorze autres réponses ici. Cette question a presque huit ans et a déjà des réponses acceptées et bien expliquées. Sans une explication sur la vôtre , il sera probablement rejeté ou supprimé. Avoir cette explication aidera à justifier la place de votre réponse sur cette question.
Das_Geek
1
Personnellement (je sais quelles sont les recommandations des SO) pour une question aussi claire / basique, je préférerais personnellement une réponse purement code . Je ne veux pas lire beaucoup d'explications Je veux rendre rapidement des logiciels fonctionnels utiles . +1
MemeDeveloper
2

L'alternative paresseuse / pressée (Dotnet Core 2.2):

[HttpGet("method1-{item}")]
public string Method1(var item) { 
return "hello" + item;}

[HttpGet("method2-{item}")]
public string Method2(var item) { 
return "world" + item;}

Les appeler:

localhost: 5000 / api / controllerername / method1-42

"bonjour42"

localhost: 5000 / api / controllerername / method2-99

"monde99"

Arthur Zennig
la source
0

Aucun des exemples ci-dessus n'a fonctionné pour mes besoins personnels. Voici ce que j'ai fini par faire.

 public class ContainsConstraint : IHttpRouteConstraint
{       
    public string[] array { get; set; }
    public bool match { get; set; }

    /// <summary>
    /// Check if param contains any of values listed in array.
    /// </summary>
    /// <param name="param">The param to test.</param>
    /// <param name="array">The items to compare against.</param>
    /// <param name="match">Whether we are matching or NOT matching.</param>
    public ContainsConstraint(string[] array, bool match)
    {

        this.array = array;
        this.match = match;
    }

    public bool Match(System.Net.Http.HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        if (values == null) // shouldn't ever hit this.                   
            return true;

        if (!values.ContainsKey(parameterName)) // make sure the parameter is there.
            return true;

        if (string.IsNullOrEmpty(values[parameterName].ToString())) // if the param key is empty in this case "action" add the method so it doesn't hit other methods like "GetStatus"
            values[parameterName] = request.Method.ToString();

        bool contains = array.Contains(values[parameterName]); // this is an extension but all we are doing here is check if string array contains value you can create exten like this or use LINQ or whatever u like.

        if (contains == match) // checking if we want it to match or we don't want it to match
            return true;
        return false;             

    }

Pour utiliser ce qui précède dans votre itinéraire, utilisez:

config.Routes.MapHttpRoute("Default", "{controller}/{action}/{id}", new { action = RouteParameter.Optional, id = RouteParameter.Optional}, new { action = new ContainsConstraint( new string[] { "GET", "PUT", "DELETE", "POST" }, true) });

Ce qui se passe, c'est le genre de contrainte de faux dans la méthode afin que cette route ne corresponde qu'aux méthodes par défaut GET, POST, PUT et DELETE. Le "vrai" indique que nous voulons vérifier la correspondance des éléments du tableau. Si c'était faux, vous diriez exclure ceux dans la str Vous pouvez alors utiliser des routes au-dessus de cette méthode par défaut comme:

config.Routes.MapHttpRoute("GetStatus", "{controller}/status/{status}", new { action = "GetStatus" });

Dans ce qui précède, il recherche essentiellement l'URL suivante => http://www.domain.com/Account/Status/Activeou quelque chose comme ça.

Au-delà de ce qui précède, je ne suis pas sûr de devenir trop fou. En fin de compte, cela devrait être par ressource. Mais je vois le besoin de mapper des URL conviviales pour diverses raisons. Je suis convaincu qu'au fur et à mesure de l'évolution de l'API Web, il y aura une sorte de disposition. Si le temps le permet, je construirai une solution plus permanente et publierai.

origin1tech
la source
Vous pouvez utiliser à la new System.Web.Http.Routing.HttpMethodConstraint(HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete) place.
abatishchev
0

Impossible de faire fonctionner l'une des solutions de routage ci-dessus - une partie de la syntaxe semble avoir changé et je suis encore nouveau dans MVC - à la rigueur bien que j'ai mis en place ce hack vraiment horrible (et simple) qui me fera par pour l'instant - notez que cela remplace la méthode "public MyObject GetMyObjects (long id)" - nous changeons le type de "id" en une chaîne, et changeons le type de retour en object.

// GET api/MyObjects/5
// GET api/MyObjects/function
public object GetMyObjects(string id)
{
    id = (id ?? "").Trim();

    // Check to see if "id" is equal to a "command" we support
    // and return alternate data.

    if (string.Equals(id, "count", StringComparison.OrdinalIgnoreCase))
    {
        return db.MyObjects.LongCount();
    }

    // We now return you back to your regularly scheduled
    // web service handler (more or less)

    var myObject = db.MyObjects.Find(long.Parse(id));
    if (myObject == null)
    {
        throw new HttpResponseException
        (
            Request.CreateResponse(HttpStatusCode.NotFound)
        );
    }

    return myObject;
}
BrainSlugs83
la source
0

Si vous avez plusieurs actions dans le même fichier, passez le même argument, par exemple Id à toutes les actions. C'est parce que l'action ne peut identifier que l'Id, donc au lieu de donner un nom à l'argument, déclarez uniquement l'Id comme ceci.


[httpget]
[ActionName("firstAction")] firstAction(string Id)
{.....
.....
}
[httpget]
[ActionName("secondAction")] secondAction(Int Id)
{.....
.....
}
//Now go to webroute.config file under App-start folder and add following
routes.MapHttpRoute(
name: "firstAction",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);

routes.MapHttpRoute(
name: "secondAction",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Uttam Kumar
la source
À quoi ressemblerait l'URL pour afficher chaque fonction dans le navigateur?
Si8
0

Alternative simple

Utilisez simplement une chaîne de requête.

Routage

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Manette

public class TestController : ApiController
{
    public IEnumerable<SomeViewModel> Get()
    {
    }

    public SomeViewModel GetById(int objectId)
    {
    }
}

Demandes

GET /Test
GET /Test?objectId=1

Remarque

Gardez à l'esprit que le paramètre de la chaîne de requête ne doit pas être "id" ou quel que soit le paramètre dans la route configurée.

Fleurs de Seth
la source
-1

Modifiez le WebApiConfig et ajoutez à la fin un autre Routes.MapHttpRoute comme ceci:

config.Routes.MapHttpRoute(
                name: "ServiceApi",
                routeTemplate: "api/Service/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

Ensuite, créez un contrôleur comme celui-ci:

public class ServiceController : ApiController
{
        [HttpGet]
        public string Get(int id)
        {
            return "object of id id";
        }
        [HttpGet]
        public IQueryable<DropDownModel> DropDowEmpresa()
        {
            return db.Empresa.Where(x => x.Activo == true).Select(y =>
                  new DropDownModel
                  {
                      Id = y.Id,
                      Value = y.Nombre,
                  });
        }

        [HttpGet]
        public IQueryable<DropDownModel> DropDowTipoContacto()
        {
            return db.TipoContacto.Select(y =>
                  new DropDownModel
                  {
                      Id = y.Id,
                      Value = y.Nombre,
                  });
        }

        [HttpGet]
        public string FindProductsByName()
        {
            return "FindProductsByName";
        }
}

Voilà comment je l'ai résolu. J'espère que cela aidera quelqu'un.

Eduardo Mercado
la source