Singolo controller con più metodi GET nell'API Web ASP.NET


167

Nell'API Web avevo una classe di struttura simile:

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) { ... }
}

Dato che potevamo mappare i singoli metodi, è stato molto semplice ottenere la richiesta giusta nel posto giusto. Per una classe simile che aveva un solo GETmetodo ma aveva anche un Objectparametro, l'ho usato con successo IActionValueBinder. Tuttavia, nel caso sopra descritto, viene visualizzato il seguente errore:

Multiple actions were found that match the request: 

SomeValue GetItems(CustomParam parameter) on type SomeType

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

Sto cercando di affrontare questo problema ignorando il ExecuteAsyncmetodo ApiControllerma senza fortuna finora. Qualche consiglio su questo problema?

Modifica: ho dimenticato di menzionare che ora sto provando a spostare questo codice sull'API Web ASP.NET che ha un approccio diverso al routing. La domanda è: come faccio a far funzionare il codice sull'API Web ASP.NET?


1
Hai ancora {parent} come RouteParameter.Optional?
Antony Scott,

Si l'ho fatto. Forse sto usando IActionValueBinder nel modo sbagliato perché per tipi come int id (come nella demo) funziona benissimo.
paulius_l

Scusa, avrei dovuto essere più chiaro. Avrei pensato che averlo come opzionale significherebbe che corrispondeva al percorso dell'articolo e al percorso degli elementi secondari, il che spiegherebbe il messaggio di errore che stai vedendo.
Antony Scott,

Stiamo attualmente riscontrando la disgregazione, se gli approcci seguenti (con percorsi multipli) sono contrari alle regole REST appropriate? Secondo me va bene. Il mio collega pensa che non sia carino. Qualche commento su questo?
Remy,

In genere ero contrario quando ho iniziato a leggere su REST. Non sono ancora sicuro che si tratti di un approccio adeguato, ma a volte è più conveniente o facile da usare, quindi piegare leggermente le regole potrebbe non essere così male. Finché funziona per risolvere un problema specifico. Sono già passati 6 mesi da quando ho pubblicato questa domanda e da allora non abbiamo più avuto rimpianti per l'utilizzo di questo approccio.
paulius_l

Risposte:


249

Questo è il modo migliore che ho trovato per supportare metodi GET extra e supportare anche i normali metodi REST. Aggiungi i seguenti percorsi al tuo 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)});

Ho verificato questa soluzione con la classe di test di seguito. Sono stato in grado di colpire con successo ogni metodo nel mio controller di seguito:

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)
    {
    }
}

Ho verificato che supporta le seguenti richieste:

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

Nota che se le tue azioni GET extra non iniziano con "Get", potresti voler aggiungere un attributo HttpGet al metodo.


4
Questa è un'ottima risposta e mi ha aiutato molto con un'altra domanda correlata. Grazie!!
Alfero Chingono,

4
Ho provato questo - non sembra funzionare. I percorsi sono tutti mappati casualmente sul metodo GetBlah (long id). :(
BrainSlugs83,

1
@ BrainSlugs83: dipende dall'ordine. E vorrai aggiungere (ai metodi "withId"), aconstraints: new{id=@"\d+"}
Eric Falsken il

4
che ne dici di aggiungere un altro metodo - Get (int id, nome stringa)? ... fallisce
Anil Purswani il

1
Ho dovuto aggiungere un percorso aggiuntivo come questo routes.MapHttpRoute("DefaultApiPut", "Api/{controller}", new {action = "Put"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Put)});per il mio Putmetodo, altrimenti mi stava dando 404.
Syed Ali Taqi,

57

Vai da questo:

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

A questa:

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

Quindi, ora puoi specificare a quale azione (metodo) vuoi inviare la tua richiesta HTTP.

la pubblicazione su "http: // localhost: 8383 / api / Command / PostCreateUser" invoca:

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

e la pubblicazione su "http: // localhost: 8383 / api / Command / PostMakeBooking" invoca:

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

Ho provato questo in un'applicazione di servizio API WEB self-hosted e funziona come un fascino :)


8
Grazie per la risposta utile Vorrei aggiungere che se inizi i nomi dei tuoi metodi con Get, Post, ecc., Le tue richieste verranno associate a tali metodi in base al verbo HTTP utilizzato. Ma si può anche chiamare il vostro metodi di nulla, e poi decorare con la [HttpGet], [HttpPost]ecc attributi per mappare il verbo al metodo.
indot_brad,

gentilmente vedi la mia domanda
Moeez,

@DikaArtaKarunia nessun problema, felice che la mia risposta sia ancora applicabile 6 anni dopo: D
Uggeh

31

Trovo che gli attributi siano più puliti da usare rispetto all'aggiunta manuale tramite codice. Qui c'è un semplice esempio.

[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);
    }

}

Ne hai bisogno anche nel tuo 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 }
);

Alcuni buoni collegamenti http://www.asp.net/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api Questo spiega meglio il routing. http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api


3
Avevo bisogno di aggiungere anche config.MapHttpAttributeRoutes();al mio WebApiConfig.cse GlobalConfiguration.Configuration.EnsureInitialized();alla fine del mio WebApiApplication.Application_Start()metodo per far funzionare gli attributi del percorso.
Ergwun,

@Ergwun Questo commento mi ha aiutato molto. Solo per aggiungerlo, config.MapHttpAttributeRoutes();deve apparire prima della mappatura del percorso (ad es config.Routes.MappHttpRoute(.... Prima .
Philip Stratford

11

È necessario definire ulteriori percorsi in global.asax.cs in questo modo:

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 }
);

5
Sì, è vero, ma sarebbe bello vedere un esempio di quei percorsi. Renderebbe questa risposta più preziosa per la comunità. (e otterresti un +1 da me :)
Aran Mulholland,


2
Una soluzione reale sarebbe stata più bella.
So Many Goblins il

6

Con la più recente Web Api 2 è diventato più facile avere più metodi get.

Se il parametro passato ai GETmetodi è abbastanza diverso da consentire al sistema di routing degli attributi di distinguere i loro tipi come nel caso di ints e Guids, è possibile specificare il tipo previsto [Route...]nell'attributo

Per esempio -

[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}";
    }
} 

Per maggiori dettagli su questo approccio, consultare qui http://nodogmablog.bryanhogan.net/2017/02//web-api-2-controller-with-multiple-get-methods-part-2/

Un'altra opzione è quella di dare ai GETmetodi percorsi diversi.

    [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";
        }
   }

Vedi qui per maggiori dettagli - http://nodogmablog.bryanhogan.net/2016/10/10/web-api-2-controller-with-multiple-get-methods/


5

In ASP.NET Core 2.0 è possibile aggiungere l' attributo Route al controller:

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

    public SomeValue GetChildItems(CustomParam parameter, SomeObject parent) { ... }
}

4

Stavo cercando di utilizzare il routing degli attributi Web Api 2 per consentire più metodi Get e avevo incorporato i suggerimenti utili dalle risposte precedenti, ma nel controller avevo decorato solo il metodo "speciale" (esempio):

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

... senza anche posizionare un [RoutePrefix] nella parte superiore del controller:

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

Stavo ricevendo errori indicando che non è stata trovata alcuna route corrispondente all'URI inviato. Una volta ho avuto sia il [Route] che decorava il metodo sia [RoutePrefix] che decoravano il Controller nel suo insieme, ha funzionato.


3

Non sono sicuro di aver trovato la risposta, ma l'ho fatto e funziona

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";
}

Ora in 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 }
);

3

Hai provato a passare a WebInvokeAttribute e impostare il metodo su "GET"?

Credo di avere avuto un problema simile e sono passato a dire esplicitamente quale metodo (GET / PUT / POST / DELETE) è atteso per la maggior parte, se non per tutti, i miei metodi.

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) { ... }
}

Il WebGet dovrebbe gestirlo, ma ho visto che ha alcuni problemi con più Get molto meno multiplo Get dello stesso tipo restituito.

[Modifica: nulla di tutto ciò è valido con il tramonto di WCF WebAPI e la migrazione a ASP.Net WebAPI sullo stack MVC]


1
Mi dispiace, ho dimenticato di dire che sto spostando il codice nell'API Web ASP.NET da quando l'API Web WCF è stata interrotta. Ho modificato il post. Grazie.
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) { ... }
    }

Benvenuto in Stack Overflow! Si prega di modificare la risposta per includere una spiegazione per il codice, così come una descrizione di come sia diverso dagli altri quattordici risposte qui. Questa domanda ha quasi otto anni e ha già una risposta accettata e ben spiegata. Senza una spiegazione sulla tua , probabilmente verrà ridimensionato o rimosso. Avere quella spiegazione ti aiuterà a giustificare la tua risposta su questa domanda.
Das_Geek,

1
Personalmente (so quali sono le raccomandazioni degli SO) per una domanda così chiara / semplice Personalmente preferirei avere una risposta in codice puro . Non voglio leggere molte spiegazioni, voglio rendere velocemente utile un software funzionale utile . +1
MemeDeveloper

2

L'alternativa pigra / frettolosa (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;}

Chiamandoli:

localhost: 5000 / api / NomeControllore / method1-42

"Hello42"

localhost: 5000 / api / NomeControllore / method2-99

"World99"


0

Nessuno degli esempi sopra ha funzionato per le mie esigenze personali. Di seguito è quello che ho finito per fare.

 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;             

    }

Per utilizzare quanto sopra nel percorso utilizzare:

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) });

Ciò che accade è il tipo di vincolo di falsi nel metodo in modo che questa route corrisponda solo ai metodi GET, POST, PUT e DELETE predefiniti. Il "vero" dice che vogliamo verificare la corrispondenza degli elementi nella matrice. Se fosse falso, diresti di escludere quelli nello str. Puoi quindi usare route sopra questo metodo predefinito come:

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

Quanto sopra è essenzialmente alla ricerca del seguente URL => http://www.domain.com/Account/Status/Activeo qualcosa del genere.

Al di là di quanto sopra non sono sicuro che diventerei troppo pazzo. Alla fine della giornata dovrebbe essere per risorsa. Ma vedo la necessità di mappare gli URL amichevoli per vari motivi. Sono abbastanza sicuro mentre si evolve Web Api ci sarà una sorta di disposizione. Se il tempo costruirò una soluzione più permanente e pubblicherò.


Puoi usare new System.Web.Http.Routing.HttpMethodConstraint(HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete) invece.
abatishchev,

0

Non è stato possibile far funzionare nessuna delle soluzioni di routing di cui sopra - parte della sintassi sembra essere cambiata e sono ancora nuovo in MVC - in un pizzico, anche se ho messo insieme questo hack davvero terribile (e semplice) che mi porterà per ora - nota, questo sostituisce il metodo "public MyObject GetMyObjects (long id)" - cambiamo il tipo di "id" in una stringa e cambiamo il tipo restituito in oggetto.

// 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;
}

0

Se hai più azioni all'interno dello stesso file, passa lo stesso argomento, ad esempio Id a tutte le azioni. Questo perché solo l'azione può identificare Id, quindi invece di dare un nome all'argomento dichiarare solo Id in questo modo.


[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 }
);

Come sarebbe l'Url come visualizzare ogni funzione nel browser?
Si8,

0

Alternativa semplice

Usa solo una stringa di query.

Routing

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

controllore

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

    public SomeViewModel GetById(int objectId)
    {
    }
}

richieste

GET /Test
GET /Test?objectId=1

Nota

Tenere presente che il parametro della stringa di query non deve essere "id" o qualunque sia il parametro nella route configurata.


-1

Modifica WebApiConfig e aggiungi alla fine un altro Routes.MapHttpRoute in questo modo:

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

Quindi crea un controller come questo:

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";
        }
}

Ecco come l'ho risolto. Spero che possa aiutare qualcuno.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.