Один контроллер с несколькими методами GET в ASP.NET Web API

167

В Web API у меня был класс схожей структуры:

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

Поскольку мы могли отображать отдельные методы, было очень просто получить правильный запрос в нужном месте. Для аналогичного класса, который имел только один GETметод, но также имел Objectпараметр, я успешно использовал IActionValueBinder. Однако в описанном выше случае я получаю следующую ошибку:

Multiple actions were found that match the request: 

SomeValue GetItems(CustomParam parameter) on type SomeType

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

Я пытаюсь подойти к этой проблеме, переопределив ExecuteAsyncметод, ApiControllerно пока безуспешно. Любой совет по этому вопросу?

Изменить: я забыл упомянуть, что сейчас я пытаюсь переместить этот код в ASP.NET Web API, который имеет другой подход к маршрутизации. Вопрос в том, как заставить код работать на ASP.NET Web API?

paulius_l
источник
1
Вы все еще получили {parent} как RouteParameter.Optional?
Энтони Скотт
Да, я сделал. Возможно, я использую IActionValueBinder неправильным способом, потому что для таких типов, как int id (как в демоверсии), он работает нормально.
paulius_l
Извините, мне следовало быть понятнее. Я бы подумал, что если он будет необязательным, это будет означать, что он соответствует маршруту Предмета, а также маршруту подэлементов, что объясняет сообщение об ошибке, которое вы видите.
Энтони Скотт
В настоящее время мы обсуждаем, если подходы ниже (с несколькими маршрутами) противоречат надлежащим правилам REST? По моему это нормально. Мой коллега думает, что это нехорошо. Есть комментарии по этому поводу?
Реми
Я вообще был против, когда начал читать про REST. Я до сих пор не уверен, что это правильный подход, но иногда он более удобен или удобен для пользователя, поэтому незначительное изменение правил может быть не таким уж плохим. Пока это работает, чтобы решить конкретную проблему. Прошло уже 6 месяцев с тех пор, как я разместил этот вопрос, и с тех пор у нас не было никаких сожалений по поводу использования этого подхода.
paulius_l

Ответы:

249

Это лучший способ, который я нашел для поддержки дополнительных методов GET и поддержки обычных методов REST. Добавьте следующие маршруты в ваш 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)});

Я проверил это решение с тестовым классом ниже. Мне удалось ударить каждый метод в моем контроллере ниже:

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

Я проверил, что он поддерживает следующие запросы:

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

Обратите внимание: если ваши дополнительные действия GET не начинаются с «Get», вы можете добавить в метод атрибут HttpGet.

небесно-DEV
источник
4
Это отличный ответ, и он мне очень помог с другим связанным вопросом. Спасибо!!
Альферо Чингоно
4
Пробовал это - не похоже на работу. Все маршруты случайным образом отображаются в метод GetBlah (длинный идентификатор). :(
BrainSlugs83
1
@ BrainSlugs83: Это зависит от порядка. И вы захотите добавить (к методам withId):constraints: new{id=@"\d+"}
Эрик Фальскен
4
как насчет добавления еще одного метода - Get (int id, string name)? ... это не удается
Анил Пурсвани
1
Мне пришлось добавить дополнительный маршрут, как этот, routes.MapHttpRoute("DefaultApiPut", "Api/{controller}", new {action = "Put"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Put)});для моего Putметода, иначе он давал мне 404.
Сайед Али Таки
57

Перейти от этого:

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

К этому:

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

Следовательно, теперь вы можете указать, какому действию (методу) вы хотите отправить свой HTTP-запрос.

публикация в "http: // localhost: 8383 / api / Command / PostCreateUser" вызывает:

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

и публикация в "http: // localhost: 8383 / api / Command / PostMakeBooking" вызывает:

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

Я попробовал это в приложении-сервисе WEB API, работающем самостоятельно, и оно работает как шарм :)

uggeh
источник
8
Спасибо за полезный ответ. Я хотел бы добавить, что если вы начнете имена своих методов с Get, Post и т. Д., Ваши запросы будут сопоставляться с этими методами на основе используемого глагола HTTP. Но вы также можете назвать ваши методы ничего, а затем украсить их с [HttpGet], [HttpPost]и т.д. атрибуты для отображения глагола метода.
indot_brad
пожалуйста, смотрите мой вопрос
Moeez
@DikaArtaKarunia без проблем, рад, что мой ответ все еще применим 6 лет спустя: D
uggeh
31

Я считаю, что атрибуты удобнее использовать, чем добавлять их вручную с помощью кода. Вот простой пример.

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

}

Вам также нужно это в вашем 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 }
);

Некоторые полезные ссылки http://www.asp.net/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api Этот лучше объясняет маршрутизацию. http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api

Калель Уэйд
источник
3
Мне нужно было также добавить config.MapHttpAttributeRoutes();к моему WebApiConfig.cs, и GlobalConfiguration.Configuration.EnsureInitialized();в конце моего WebApiApplication.Application_Start()метода, чтобы заставить работать атрибуты маршрута.
Эргвун
@Ergwun Этот комментарий мне очень помог. Просто чтобы добавить к нему, config.MapHttpAttributeRoutes();должен появиться до отображения маршрута (например, до config.Routes.MappHttpRoute(....
Филип Стратфорд
11

Вам нужно определить дальнейшие маршруты в global.asax.cs следующим образом:

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
Да, это правда, но было бы неплохо увидеть пример этих маршрутов. Это сделало бы этот ответ более ценным для сообщества. (и вы получите +1 от меня :)
Аран Малхолланд
Вы можете прочитать здесь пример - stackoverflow.com/questions/11407267/...
Том Kerkhove
2
Фактическое решение было бы лучше.
Так много гоблинов
6

С более новым Web Api 2 стало проще иметь несколько методов get.

Если параметр, передаваемый в GETметоды, достаточно отличается для системы маршрутизации атрибута, чтобы различать их типы, как в случае с ints и Guids, вы можете указать ожидаемый тип в [Route...]атрибуте

Например -

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

Подробнее об этом подходе см. Здесь http://nodogmablog.bryanhogan.net/2017/02/web-api-2-controller-with-multiple-get-methods-part-2/.

Другой вариант - дать GETметодам разные маршруты.

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

Смотрите здесь для получения более подробной информации - http://nodogmablog.bryanhogan.net/2016/10/web-api-2-controller-with-multiple-get-methods/

Bryan
источник
5

В ASP.NET Core 2.0 вы можете добавить атрибут Route к контроллеру:

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

    public SomeValue GetChildItems(CustomParam parameter, SomeObject parent) { ... }
}
maskalek
источник
4

Я пытался использовать маршрутизацию атрибутов Web Api 2, чтобы учесть несколько методов Get, и я включил полезные предложения из предыдущих ответов, но в Controller я только украсил «специальный» метод (пример):

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

... также не помещая [RoutePrefix] в верхней части контроллера:

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

Я получаю ошибки, утверждающие, что не найдено ни одного маршрута, соответствующего представленному URI. Как только у меня было [Route], украшающее метод, так и [RoutePrefix], украшающее Контроллер в целом, это работало.

StackOverflowUser
источник
3

Я не уверен, что вы нашли ответ, но я сделал это, и это работает

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

Сейчас в 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

Вы пытались переключиться на WebInvokeAttribute и установить метод для «GET»?

Я полагаю, что у меня была похожая проблема, и я переключился на явное указание, какой метод (GET / PUT / POST / DELETE) ожидается для большинства, если не для всех, моих методов.

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

WebGet должен справиться с этим, но я видел, что у него есть некоторые проблемы с множеством Get, намного меньше множественных Get с тем же типом возврата.

[Редактировать: все это недопустимо в случае заката WCF WebAPI и перехода на ASP.Net WebAPI в стеке MVC]

PMontgomery
источник
1
Извините, я забыл упомянуть, что я перемещаю код в ASP.NET Web API, так как WCF Web API был прекращен. Я редактировал пост. Спасибо.
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
источник
Добро пожаловать в стек переполнения! Пожалуйста, отредактируйте ваш ответ, включив в него объяснение вашего кода, а также описание того, как он отличается от четырнадцати других ответов здесь. Этому вопросу почти восемь лет , и у него уже есть принятые и несколько хорошо объясненных ответов. Без вашего объяснения он, скорее всего, будет опущен или удален. Наличие такого объяснения поможет обосновать место вашего ответа на этот вопрос.
Das_Geek
1
Лично (я знаю, каковы рекомендации SO) для вопроса, такого ясного / базового, я лично предпочел бы иметь чистый кодовый ответ. Я не хочу читать много объяснений, я хочу сделать полезное функциональное программное обеспечение быстрым . +1
MemeDeveloper
2

Альтернатива ленивых / спешащих (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;}

Называя их :

локальный: 5000 / API / controllername / method1-42

"Hello42"

локальный: 5000 / API / controllername / method2-99

"World99"

Артур Зенниг
источник
0

Ни один из приведенных выше примеров не помог мне. Ниже то, что я в итоге сделал.

 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;             

    }

Чтобы использовать вышеупомянутое в вашем маршруте, используйте:

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

То, что происходит, является методом подделок в методе, так что этот маршрут будет соответствовать только методам GET, POST, PUT и DELETE по умолчанию. Значение true говорит о том, что мы хотим проверить соответствие элементов в массиве. Если бы это было ложно, вы бы сказали исключить тех из str. Затем вы можете использовать маршруты выше этого метода по умолчанию, например:

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

В приведенном выше тексте он ищет следующий URL => http://www.domain.com/Account/Status/Activeили что-то в этом роде.

Помимо вышесказанного, я не уверен, что стану слишком сумасшедшим. В конце дня это должно быть за ресурс. Но я вижу необходимость отображать дружественные ссылки по разным причинам. Я чувствую себя довольно уверенно, так как Web Api развивается, будет какое-то положение. Если будет время, я построю более постоянное решение и выложу.

origin1tech
источник
Вы можете использовать new System.Web.Http.Routing.HttpMethodConstraint(HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete) вместо этого.
Абатищев
0

Не удалось заставить работать ни одно из вышеперечисленных решений по маршрутизации - кажется, что некоторые синтаксис изменился, и я все еще новичок в MVC - в крайнем случае, хотя я собрал этот действительно ужасный (и простой) хак, который заставит меня на данный момент - обратите внимание, это заменяет метод "public MyObject GetMyObjects (long id)" - мы меняем тип "id" на строку и меняем тип возвращаемого значения на 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
источник
0

Если у вас есть несколько действий в одном файле, передайте один и тот же аргумент, например, Id для всех действий. Это потому, что действие только может идентифицировать Id, поэтому вместо присвоения имени аргументу только объявите Id следующим образом.


[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 }
);
Уттам Кумар
источник
Как будет выглядеть URL для просмотра каждой функции в браузере?
Si8
0

Простая альтернатива

Просто используйте строку запроса.

Маршрутизация

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

контроллер

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

    public SomeViewModel GetById(int objectId)
    {
    }
}

Запросы

GET /Test
GET /Test?objectId=1

Заметка

Имейте в виду, что параметр строки запроса не должен быть «id» или каким-либо другим параметром в настроенном маршруте.

Сет Цветы
источник
-1

Измените WebApiConfig и добавьте в конце еще один Routes.MapHttpRoute следующим образом:

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

Затем создайте контроллер следующим образом:

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

Вот как я это решил. Надеюсь, это кому-нибудь поможет.

Эдуардо Меркадо
источник