Controlador único com vários métodos GET na API da Web do ASP.NET

167

Na Web API, eu tinha uma classe de estrutura semelhante:

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 conseguimos mapear métodos individuais, era muito simples obter a solicitação certa no lugar certo. Para uma classe semelhante que tinha apenas um GETmétodo, mas também um Objectparâmetro, usei com sucesso IActionValueBinder. No entanto, no caso descrito acima, recebo o seguinte erro:

Multiple actions were found that match the request: 

SomeValue GetItems(CustomParam parameter) on type SomeType

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

Estou tentando abordar esse problema, substituindo o ExecuteAsyncmétodo de ApiControllermas sem sorte até agora. Algum conselho sobre esse assunto?

Editar: esqueci de mencionar que agora estou tentando mover esse código na API da Web do ASP.NET, que tem uma abordagem diferente ao roteamento. A questão é: como faço para que o código funcione na API da Web do ASP.NET?

paulius_l
fonte
1
Você ainda tem o {parent} como RouteParameter.Optional?
Antony Scott
Sim eu fiz. Talvez eu esteja usando o IActionValueBinder da maneira errada, porque para tipos como int id (como na demonstração) ele funciona bem.
paulius_l
Desculpe, eu deveria ter sido mais claro. Eu pensaria que tê-lo como opcional significaria que ele corresponde à rota Item e à rota subitens, o que explicaria a mensagem de erro que você está vendo.
Antony Scott
No momento, estamos discutindo se as abordagens abaixo (com várias rotas) são contra as regras REST adequadas? Na minha opinião, isso está bem. Meu colega de trabalho acha que não é legal. Algum comentário sobre isso?
Remy
Eu geralmente era contra quando comecei a ler sobre o REST. Ainda não tenho certeza se essa é uma abordagem adequada, mas às vezes é mais conveniente ou fácil de usar, portanto, flexionar ligeiramente as regras pode não ser tão ruim. Contanto que ele funcione para resolver um problema específico. Já se passaram 6 meses desde que eu postei essa pergunta e não tivemos nenhum arrependimento por usar essa abordagem desde então.
paulius_l

Respostas:

249

Esta é a melhor maneira que encontrei para oferecer suporte a métodos GET extras e também aos métodos REST normais. Adicione as seguintes rotas ao seu 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)});

Eu verifiquei esta solução com a classe de teste abaixo. Consegui atingir com êxito cada método no meu controlador abaixo:

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

Eu verifiquei que ele suporta os seguintes pedidos:

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

Observe que, se suas ações GET extras não começarem com 'Get', convém adicionar um atributo HttpGet ao método.

sky-dev
fonte
4
Esta é uma ótima resposta e me ajudou muito com outra questão relacionada. Obrigado!!
Alfero Chingono 30/10/12
4
Tentei isso - não parece funcionar. As rotas são mapeadas aleatoriamente para o método GetBlah (long id). :(
BrainSlugs83
1
@ BrainSlugs83: Depende da ordem. E você desejará adicionar (aos métodos "withId"), um aconstraints: new{id=@"\d+"}
Eric Falsken
4
que tal adicionar mais um método - Get (int id, nome da string)? ... falha
Anil Purswani
1
Eu tive que adicionar uma rota extra como este routes.MapHttpRoute("DefaultApiPut", "Api/{controller}", new {action = "Put"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Put)});para o meu Putmétodo caso contrário ele estava me dando 404.
Syed Ali Taqi
57

Vá a partir disso:

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

Para isso:

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

Portanto, agora você pode especificar para qual ação (método) você deseja enviar sua solicitação HTTP.

postar em "http: // localhost: 8383 / api / Command / PostCreateUser" chama:

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

e postar em "http: // localhost: 8383 / api / Command / PostMakeBooking" chama:

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

Eu tentei isso em um aplicativo de serviço de API WEB auto-hospedado e funciona como um encanto :)

uggeh
fonte
8
Obrigado pela resposta útil. Gostaria de acrescentar que, se você iniciar os nomes dos métodos com Get, Post, etc., suas solicitações serão mapeadas para esses métodos com base no verbo HTTP usado. Mas você também pode nomear seu métodos nada, e depois decorá-los com o [HttpGet], [HttpPost], etc. atributos para mapear o verbo para o método.
indot_brad
gentilmente ver a minha pergunta
Moeez
@DikaArtaKarunia não tem problema, feliz que minha resposta ainda seja aplicável 6 anos depois: D
uggeh 21/02/19
31

Acho que os atributos são mais limpos de usar do que adicioná-los manualmente via código. Aqui está um exemplo simples.

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

}

Você também precisa disso no seu 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 }
);

Some Good Links http://www.asp.net/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api Este explica melhor o roteamento. http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api

Kalel Wade
fonte
3
Eu também precisava adicionar config.MapHttpAttributeRoutes();ao meu WebApiConfig.cse GlobalConfiguration.Configuration.EnsureInitialized();no final do meu WebApiApplication.Application_Start()método para obter atributos de rota para o trabalho.
Ergwun 23/02
@ Ergwun Este comentário me ajudou muito. Só para acrescentar a isto, config.MapHttpAttributeRoutes();precisa comparecer perante o mapeamento da rota (por exemplo, antes config.Routes.MappHttpRoute(....
Philip Stratford
11

Você precisa definir outras rotas no global.asax.cs como este:

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
fonte
5
Sim, é verdade, mas seria bom ver um exemplo dessas rotas. Isso tornaria essa resposta mais valiosa para a comunidade. (e você deseja obter um +1 de mim :)
Aran Mulholland
Você pode ler um exemplo aqui - stackoverflow.com/questions/11407267/…
Tom Kerkhove
2
Uma solução real teria sido melhor.
Tantos Goblins
6

Com o Web Api 2 mais novo, ficou mais fácil ter vários métodos de obtenção.

Se o parâmetro passado para os GETmétodos for diferente o suficiente para o sistema de roteamento de atributos diferenciar seus tipos, como é o caso de ints e Guids, você pode especificar o tipo esperado no campo[Route...] atributo

Por exemplo -

[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 mais detalhes sobre essa abordagem, clique aqui http://nodogmablog.bryanhogan.net/2017/02/web-api-2-controller-with-multiple-get-methods-part-2/

Outra opção é fornecer aos GETmétodos rotas diferentes.

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

Veja aqui para mais detalhes - http://nodogmablog.bryanhogan.net/2016/10/web-api-2-controller-with-multiple-get-methods/

Bryan
fonte
5

No ASP.NET Core 2.0, você pode adicionar o atributo Route ao controlador:

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

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

Eu estava tentando usar o roteamento de atributos da Web Api 2 para permitir vários métodos Get, e eu incorporara as sugestões úteis das respostas anteriores, mas no Controller eu havia decorado apenas o método "especial" (exemplo):

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

... sem também colocar um [RoutePrefix] na parte superior do controlador:

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

Eu estava recebendo erros informando que não foi encontrada nenhuma rota correspondente ao URI enviado. Depois que o [Route] decorava o método e o [RoutePrefix] decorava o Controller como um todo, funcionou.

StackOverflowUser
fonte
3

Não tenho certeza se você encontrou a resposta, mas fiz isso e 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";
}

Agora no 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
fonte
3

Você já tentou mudar para WebInvokeAttribute e definir o método como "GET"?

Acredito que tive um problema semelhante e mudei para informar explicitamente qual método (GET / PUT / POST / DELETE) é esperado na maioria dos métodos, se não 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) { ... }
}

O WebGet deve lidar com isso, mas já vi alguns problemas com o Get múltiplo e muito menos o Get do mesmo tipo de retorno.

[Editar: nada disso é válido com o pôr do sol do WCF WebAPI e a migração para o ASP.Net WebAPI na pilha MVC]

PMontgomery
fonte
1
Sinto muito, esqueci de mencionar que estou movendo o código para a API da Web do ASP.NET desde que a API da Web do WCF foi descontinuada. Eu editei a postagem. Obrigado.
21412 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
fonte
Bem-vindo ao Stack Overflow! Por favor edite sua resposta para incluir uma explicação para o seu código, bem como uma descrição de como ele é diferente dos outros catorze respostas aqui. Esta pergunta tem quase oito anos e já possui respostas aceitas e diversas e bem explicadas. Sem uma explicação sobre a sua , é provável que a votação seja reduzida ou removida. Ter essa explicação ajudará a justificar o lugar da sua resposta nessa questão.
Das_Geek 11/12/19
1
Pessoalmente (sei quais são as recomendações de SOs) para uma pergunta tão clara / básica, eu pessoalmente prefiro ter uma resposta de código pura . Não quero ler muitas explicações. Quero tornar rápido o software funcional útil . +1
MemeDeveloper
2

A alternativa de preguiça / pressa (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;}

Ligando para eles:

localhost: 5000 / api / nome_do_controlador / método1-42

"olá42"

localhost: 5000 / api / nome do controlador / método 2-99

"world99"

Arthur Zennig
fonte
0

Nenhum dos exemplos acima funcionou para minhas necessidades pessoais. A seguir, é o que acabei fazendo.

 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 o acima em sua rota, 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) });

O que acontece é o tipo de restrição de falhas no método, para que essa rota corresponda apenas aos métodos padrão GET, POST, PUT e DELETE. O "true" diz que queremos verificar se há uma correspondência dos itens na matriz. Se fosse falso, você estaria dizendo excluir aqueles no strVocê pode usar rotas acima deste método padrão, como:

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

No exemplo acima, ele está basicamente procurando o seguinte URL => http://www.domain.com/Account/Status/Activeou algo parecido.

Além do exposto, não tenho certeza se ficaria louco demais. No final do dia, deve ser por recurso. Mas vejo a necessidade de mapear URLs amigáveis ​​por vários motivos. Estou me sentindo bastante certo, à medida que a Web Api evolui, haverá algum tipo de provisão. Se tiver tempo vou construir uma solução mais permanente e postar.

origin1tech
fonte
Você pode usar em seu new System.Web.Http.Routing.HttpMethodConstraint(HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete) lugar.
abatishchev
0

Não foi possível fazer com que nenhuma das soluções de roteamento acima funcione - algumas das sintaxes parecem ter mudado e eu ainda sou novo no MVC - em uma pitada, apesar de eu ter reunido esse truque realmente terrível (e simples) que vai me deixar por enquanto - note, isso substitui o método "public MyObject GetMyObjects (long id)" - alteramos o tipo de "id" para uma string e alteramos o tipo de retorno para objeto.

// 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
fonte
0

Se você tiver várias ações no mesmo arquivo, passe o mesmo argumento, por exemplo, ID para todas as ações. Isso ocorre porque a ação pode apenas identificar o ID. Portanto, em vez de atribuir qualquer nome ao argumento, declare o ID dessa forma.


[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
fonte
Como seria o URL para exibir cada função no navegador?
precisa saber é
0

Alternativa Simples

Basta usar uma string de consulta.

Encaminhamento

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

solicitações de

GET /Test
GET /Test?objectId=1

Nota

Lembre-se de que o parâmetro da cadeia de caracteres de consulta não deve ser "id" ou qualquer que seja o parâmetro na rota configurada.

Seth Flowers
fonte
-1

Modifique o WebApiConfig e adicione no final outro Routes.MapHttpRoute como este:

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

Em seguida, crie um 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";
        }
}

Foi assim que eu resolvi. Espero que ajude alguém.

Eduardo Mercado
fonte