Controlador único con múltiples métodos GET en ASP.NET Web API

167

En Web API tuve una clase de estructura similar:

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

Como pudimos mapear métodos individuales, fue muy simple obtener la solicitud correcta en el lugar correcto. Para una clase similar que solo tenía un GETmétodo único pero también tenía un Objectparámetro, utilicé con éxito IActionValueBinder. Sin embargo, en el caso descrito anteriormente me sale el siguiente error:

Multiple actions were found that match the request: 

SomeValue GetItems(CustomParam parameter) on type SomeType

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

Estoy tratando de abordar este problema anulando el ExecuteAsyncmétodo ApiControllerpero hasta ahora sin suerte. ¿Algún consejo sobre este tema?

Editar: Olvidé mencionar que ahora estoy tratando de mover este código en la API web ASP.NET que tiene un enfoque diferente para el enrutamiento. La pregunta es, ¿cómo hago que el código funcione en la API web ASP.NET?

paulius_l
fuente
1
¿Aún tiene el {parent} como RouteParameter.Optional?
Antony Scott
Sí, lo hice. Tal vez estoy usando IActionValueBinder de manera incorrecta porque para tipos como int id (como en la demostración) funciona bien.
paulius_l
Lo siento, debería haber sido más claro. Pensé que tenerlo como opcional significaría que coincide con la ruta del artículo y la ruta de los subelementos, lo que explicaría el mensaje de error que está viendo.
Antony Scott
Actualmente estamos teniendo la discusión, si los enfoques a continuación (con múltiples rutas) están en contra de las reglas REST adecuadas? En mi opinión esto está bien. Mi compañero de trabajo piensa que no es agradable. ¿Algún comentario sobre esto?
Remy
Generalmente estaba en contra cuando comencé a leer sobre REST. Todavía no estoy seguro de si ese es un enfoque adecuado, pero a veces es más conveniente o fácil de usar, por lo que flexionar ligeramente las reglas puede no ser tan malo. Mientras funcione para resolver un problema específico. Ya han pasado 6 meses desde que publiqué esta pregunta y desde entonces no hemos tenido ningún remordimiento por usar este enfoque.
paulius_l

Respuestas:

249

Esta es la mejor manera que he encontrado para admitir métodos GET adicionales y también para los métodos REST normales. Agregue las siguientes rutas a su 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)});

Verifiqué esta solución con la clase de prueba a continuación. Pude acertar con éxito cada método en mi controlador a continuación:

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

Verifiqué que admite las siguientes solicitudes:

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

Tenga en cuenta que si sus acciones GET adicionales no comienzan con 'Obtener', es posible que desee agregar un atributo HttpGet al método.

sky-dev
fuente
44
Esta es una gran respuesta y me ayudó mucho con otra pregunta relacionada. ¡¡Gracias!!
Alfero Chingono
44
Intenté esto, no parece funcionar. Todas las rutas se asignan aleatoriamente al método GetBlah (identificación larga). :(
BrainSlugs83
1
@ BrainSlugs83: Depende del orden. Y querrá agregar (a los métodos "withId"), aconstraints: new{id=@"\d+"}
Eric Falsken
44
¿qué tal agregar un método más: Get (int id, string name)? ... falla
Anil Purswani
1
Tuve que agregar una ruta adicional como esta routes.MapHttpRoute("DefaultApiPut", "Api/{controller}", new {action = "Put"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Put)});para mi Putmétodo, de lo contrario me estaba dando 404.
Syed Ali Taqi
57

Ir de esto:

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

A esto:

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

Por lo tanto, ahora puede especificar a qué acción (método) desea enviar su solicitud HTTP.

la publicación en "http: // localhost: 8383 / api / Command / PostCreateUser" invoca:

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

y publicando en "http: // localhost: 8383 / api / Command / PostMakeBooking" invoca:

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

Probé esto en una aplicación de servicio de API WEB alojada y funciona de maravilla :)

uggeh
fuente
8
Gracias por la útil respuesta. Me gustaría agregar que si comienza los nombres de sus métodos con Get, Post, etc., sus solicitudes se asignarán a esos métodos en función del verbo HTTP utilizado. Pero también se puede nombrar a su métodos nada, y luego decorar con el [HttpGet], [HttpPost], etc. atributos para mapear el verbo al método.
indot_brad
amablemente vea mi pregunta
Moeez
@DikaArtaKarunia no hay problema, me alegra que mi respuesta todavía sea aplicable 6 años después: D
uggeh
31

Creo que los atributos son más limpios de usar que agregarlos manualmente a través del código. Aquí hay un ejemplo 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);
    }

}

También necesita esto en su 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 }
);

Algunos buenos enlaces http://www.asp.net/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api Este explica la ruta mejor. http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api

Kalel Wade
fuente
3
También necesitaba agregar config.MapHttpAttributeRoutes();a mi WebApiConfig.cs, y GlobalConfiguration.Configuration.EnsureInitialized();al final de mi WebApiApplication.Application_Start()método para que los atributos de ruta funcionen.
Ergwun
@Ergwun Este comentario me ayudó mucho. Solo para agregarlo, config.MapHttpAttributeRoutes();debe aparecer antes del mapeo de la ruta (por ejemplo, antes config.Routes.MappHttpRoute(....
Philip Stratford
11

Debe definir más rutas en global.asax.cs como esta:

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
fuente
55
Sí, eso es cierto, pero sería bueno ver un ejemplo de esas rutas. Haría esta respuesta más valiosa para la comunidad. (y obtendrías un +1 de mí :)
Aran Mulholland
Puede leer un ejemplo aquí: stackoverflow.com/questions/11407267/…
Tom Kerkhove
2
Una solución real hubiera sido mejor.
Tantos Goblins
6

Con la nueva Web Api 2 se ha vuelto más fácil tener múltiples métodos get.

Si el parámetro pasado a los GETmétodos es lo suficientemente diferente para que el sistema de enrutamiento de atributos distinga sus tipos, como es el caso de intsy Guids, puede especificar el tipo esperado en el[Route...] atributo

Por ejemplo -

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

Para obtener más detalles sobre este enfoque, consulte aquí http://nodogmablog.bryanhogan.net/2017/02/web-api-2-controller-with-multiple-get-methods-part-2/

Otra opción es dar a los GETmétodos diferentes rutas.

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

Ver aquí para más detalles: http://nodogmablog.bryanhogan.net/2016/10/web-api-2-controller-with-multiple-get-methods/

Bryan
fuente
5

En ASP.NET Core 2.0 puede agregar el atributo Ruta al controlador:

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

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

Intenté utilizar el enrutamiento de atributos de Web Api 2 para permitir múltiples métodos Get, e incorporé las sugerencias útiles de respuestas anteriores, pero en el controlador solo había decorado el método "especial" (ejemplo):

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

... sin también colocar un [RoutePrefix] en la parte superior del Controlador:

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

Recibía errores que indicaban que no se encontró ninguna ruta que coincida con el URI enviado. Una vez que tuve tanto la [Ruta] que decoraba el método como la [RutaPrefijo] que decoraba el Controlador en su conjunto, funcionó.

StackOverflowUser
fuente
3

No estoy seguro de haber encontrado la respuesta, pero hice esto y funciona

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

Ahora en 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
fuente
3

¿Has intentado cambiar a WebInvokeAttribute y configurar el Método en "GET"?

Creo que tuve un problema similar y cambié a decir explícitamente qué método (GET / PUT / POST / DELETE) se espera en la mayoría de mis métodos, si no en todos.

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

El WebGet debería manejarlo, pero he visto que tiene algunos problemas con múltiples Obtener mucho menos múltiples Obtener del mismo tipo de retorno.

[Editar: nada de esto es válido con la puesta de sol de WCF WebAPI y la migración a ASP.Net WebAPI en la pila MVC]

PMontgomery
fuente
1
Lo siento, olvidé mencionar que estoy moviendo el código a la API web de ASP.NET desde que se suspendió la API web de WCF. Edité la publicación. Gracias.
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
fuente
¡Bienvenido a Stack Overflow! Por favor, editar su respuesta para incluir una explicación de su código, así como una descripción de cómo es diferente de los otros catorce respuestas aquí. Esta pregunta tiene casi ocho años y ya tiene respuestas aceptadas y varias bien explicadas. Sin una explicación suya , es probable que sea rechazado o eliminado. Tener esa explicación ayudará a justificar el lugar de su respuesta en esta pregunta.
Das_Geek
1
Personalmente (sé cuáles son las recomendaciones de SO) para una pregunta tan clara / básica , personalmente preferiría tener una respuesta de código puro . No quiero leer muchas explicaciones. Quiero hacer que un software funcional útil sea rápido . +1
MemeDeveloper
2

La alternativa perezosa / apresurada (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;}

Llamándolos:

localhost: 5000 / api / controllername / method1-42

"hola42"

localhost: 5000 / api / controllername / method2-99

"world99"

Arthur Zennig
fuente
0

Ninguno de los ejemplos anteriores funcionó para mis necesidades personales. Lo siguiente es lo que terminé haciendo.

 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;             

    }

Para usar lo anterior en su ruta use:

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

Lo que sucede es el tipo de restricción de falsificaciones en el método para que esta ruta solo coincida con los métodos predeterminados GET, POST, PUT y DELETE. El "verdadero" dice que queremos verificar si hay una coincidencia de los elementos en la matriz. Si fuera falso, estaría diciendo que excluya a los que están en la cadena.Puede usar rutas por encima de este método predeterminado como:

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

En lo anterior, esencialmente está buscando la siguiente URL => http://www.domain.com/Account/Status/Activeo algo así.

Más allá de lo anterior, no estoy seguro de volverme loco. Al final del día, debe ser por recurso. Pero sí veo la necesidad de mapear las URL amigables por varias razones. Me siento bastante seguro a medida que Web Api evoluciona, habrá algún tipo de provisión. Si es tiempo, construiré una solución más permanente y publicaré.

origin1tech
fuente
Puedes usar new System.Web.Http.Routing.HttpMethodConstraint(HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete) en su lugar.
abatishchev
0

No se pudo hacer que ninguna de las soluciones de enrutamiento anteriores funcionara; parte de la sintaxis parece haber cambiado y todavía soy nuevo en MVC, en un apuro aunque armé este truco realmente horrible (y simple) que me atrapará por ahora, tenga en cuenta que esto reemplaza el método "public MyObject GetMyObjects (long id)": cambiamos el tipo de "id" a una cadena y cambiamos el tipo de retorno a 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
fuente
0

Si tiene varias acciones dentro del mismo archivo, pase el mismo argumento, por ejemplo, Id, a todas las acciones. Esto se debe a que la acción solo puede identificar Id, por lo tanto, en lugar de dar un nombre al argumento, solo declare Id de esta manera.


[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
fuente
¿Cómo sería la URL para ver cada función en el navegador?
Si8
0

Alternativa simple

Solo usa una cadena de consulta.

Enrutamiento

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

Controlador

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

    public SomeViewModel GetById(int objectId)
    {
    }
}

Peticiones

GET /Test
GET /Test?objectId=1

Nota

Tenga en cuenta que el parámetro de cadena de consulta no debe ser "id" o cualquiera que sea el parámetro en la ruta configurada.

Seth Flowers
fuente
-1

Modifique el WebApiConfig y agregue al final otras Rutas. MapHttpRoute así:

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

Luego cree un controlador como este:

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

Así es como lo resolví. Espero que ayude a alguien.

Eduardo Mercado
fuente