Pojedynczy kontroler z wieloma metodami GET w ASP.NET Web API

167

W Web API miałem klasę o podobnej budowie:

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

Ponieważ mogliśmy mapować poszczególne metody, bardzo łatwo było uzyskać właściwe żądanie we właściwym miejscu. Dla podobnej klasy, która miała tylko jedną GETmetodę, ale miała również Objectparametr, z powodzeniem zastosowałem IActionValueBinder. Jednak w przypadku opisanym powyżej pojawia się następujący błąd:

Multiple actions were found that match the request: 

SomeValue GetItems(CustomParam parameter) on type SomeType

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

Próbuję podejść do tego problemu, zastępując ExecuteAsyncmetodę, ApiControllerale jak dotąd bez powodzenia. Jakieś rady w tej sprawie?

Edycja: zapomniałem wspomnieć, że teraz próbuję przenieść ten kod na ASP.NET Web API, który ma inne podejście do routingu. Pytanie brzmi, jak sprawić, by kod działał w interfejsie API sieci Web ASP.NET?

paulius_l
źródło
1
Czy nadal masz {parent} jako RouteParameter.Optional?
Antony Scott
Tak. Może używam IActionValueBinder w niewłaściwy sposób, ponieważ w przypadku typów takich jak int id (jak w wersji demonstracyjnej) działa dobrze.
paulius_l
Przepraszam, powinienem był wyjaśnić. Pomyślałbym, że posiadanie go jako opcjonalnego oznaczałoby, że pasuje do trasy elementu, a także trasy elementów podrzędnych, co wyjaśniłoby komunikat o błędzie, który widzisz.
Antony Scott
Obecnie prowadzimy dyskusję, czy poniższe podejścia (z wieloma trasami) są sprzeczne z odpowiednimi regułami REST? Moim zdaniem to jest w porządku. Mój współpracownik uważa, że ​​to nie jest miłe. Jakieś komentarze na ten temat?
Remy
Generalnie byłem temu przeciwny, kiedy zacząłem czytać o REST. Nadal nie jestem pewien, czy jest to właściwe podejście, ale czasami jest to wygodniejsze lub bardziej przyjazne dla użytkownika, więc lekkie naginanie reguł może nie być takie złe. O ile działa, aby rozwiązać określony problem. Minęło już 6 miesięcy, odkąd opublikowałem to pytanie i od tego czasu nie żałujemy, że skorzystaliśmy z tego podejścia.
paulius_l

Odpowiedzi:

249

Jest to najlepszy sposób, w jaki znalazłem obsługę dodatkowych metod GET, a także normalnych metod REST. Dodaj następujące trasy do swojej konfiguracji 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)});

Sprawdziłem to rozwiązanie z poniższą klasą testową. Udało mi się pomyślnie trafić każdą metodę w moim kontrolerze poniżej:

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

Sprawdziłem, że obsługuje następujące żądania:

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

Zauważ, że jeśli twoje dodatkowe akcje GET nie zaczynają się od „Get”, możesz chcieć dodać atrybut HttpGet do metody.

sky-dev
źródło
4
To świetna odpowiedź i bardzo mi pomogła z innym powiązanym pytaniem. Dzięki!!
Alfero Chingono
4
Próbowałem tego - nie wydaje się działać. Wszystkie trasy są losowo mapowane na metodę GetBlah (long id). :(
BrainSlugs83
1
@ BrainSlugs83: To zależy od zamówienia. Będziesz chciał dodać (do metod „withId”) aconstraints: new{id=@"\d+"}
Eric Falsken,
4
co powiesz na dodanie jeszcze jednej metody - Get (int id, nazwa ciągu)? ... zawodzi
Anil Purswani,
1
Musiałem dodać dodatkową trasę, taką jak ta routes.MapHttpRoute("DefaultApiPut", "Api/{controller}", new {action = "Put"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Put)});dla mojej Putmetody, w przeciwnym razie dała mi 404.
Syed Ali Taqi
57

Idź z tego:

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

Do tego:

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

W związku z tym możesz teraz określić, do której akcji (metody) chcesz wysłać żądanie HTTP.

wysyłanie do „http: // localhost: 8383 / api / Command / PostCreateUser” wywołuje:

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

a wysłanie do „http: // localhost: 8383 / api / Command / PostMakeBooking” wywołuje:

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

Wypróbowałem to w aplikacji usługi WEB API z własnym hostingiem i działa jak urok :)

uggeh
źródło
8
Dzięki za pomocną odpowiedź. Chciałbym dodać, że jeśli zaczniesz nazwy metod od Get, Post itp., Twoje żądania będą mapowane na te metody na podstawie użytego czasownika HTTP. Ale można też nazwać niczego metodami, a następnie ozdobić je z [HttpGet], [HttpPost]itp atrybuty map czasownika metody.
indot_brad
uprzejmie zobacz moje pytanie
Moeez
@DikaArtaKarunia nie ma problemu, cieszę się, że moja odpowiedź jest nadal
aktualna
31

Uważam, że atrybuty są czystsze w użyciu niż ręczne dodawanie ich za pomocą kodu. Oto prosty przykład.

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

}

Potrzebujesz tego również w swoim pliku 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 }
);

Kilka dobrych linków http://www.asp.net/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api To wyjaśnia lepiej routing. http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api

Kalel Wade
źródło
3
Musiałem również dodać config.MapHttpAttributeRoutes();do mojej metody WebApiConfig.cs, a GlobalConfiguration.Configuration.EnsureInitialized();na końcu mojej WebApiApplication.Application_Start()metody, aby atrybuty trasy działały.
Ergwun
@Ergwun Ten komentarz bardzo mi pomógł. Wystarczy do tego dodać, config.MapHttpAttributeRoutes();pojawi się przed wytyczeniem trasy (np config.Routes.MappHttpRoute(.... Przed
Philip Stratford
11

Musisz zdefiniować dalsze trasy w global.asax.cs w ten sposób:

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
źródło
5
Tak, to prawda, ale fajnie byłoby faktycznie zobaczyć przykład takich tras. Dzięki temu ta odpowiedź byłaby bardziej wartościowa dla społeczności. (i dostaniesz ode mnie +1 :)
Aran Mulholland
Możesz przeczytać przykład tutaj - stackoverflow.com/questions/11407267/…
Tom Kerkhove
2
Rzeczywiste rozwiązanie byłoby lepsze.
So Many Goblins
6

Dzięki nowszemu interfejsowi Web Api 2 korzystanie z wielu metod pobierania stało się łatwiejsze.

Jeśli parametry przekazane do GETmetod są na tyle różne, że system routingu atrybutów rozróżnia ich typy, tak jak ma to miejsce w przypadku ints i Guids, można określić oczekiwany typ w [Route...]atrybucie

Na przykład -

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

Więcej informacji na temat tego podejścia można znaleźć tutaj: http://nodogmablog.bryanhogan.net/2017/02/web-api-2-controller-with-multiple-get-methods-part-2/

Inną opcją jest podanie GETmetodom różnych tras.

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

Zobacz tutaj, aby uzyskać więcej informacji - http://nodogmablog.bryanhogan.net/2016/10/web-api-2-controller-with-multiple-get-methods/

Bryan
źródło
5

W ASP.NET Core 2.0 można dodać atrybut Route do kontrolera: In ASP.NET Core 2.0 you can add Route attribute to the controller:

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

    public SomeValue GetChildItems(CustomParam parameter, SomeObject parent) { ... }
}
maskalek
źródło
4

Próbowałem użyć routingu atrybutów Web Api 2, aby umożliwić wiele metod Get i włączyłem pomocne sugestie z poprzednich odpowiedzi, ale w kontrolerze udekorowałem tylko metodę „specjalną” (przykład):

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

... bez również umieszczania [RoutePrefix] na górze kontrolera:

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

Otrzymywałem błędy informujące, że nie znaleziono trasy pasującej do przesłanego identyfikatora URI. Kiedy miałem zarówno [Route] dekorującą metodę, jak i [RoutePrefix] dekorującą kontroler jako całość, zadziałało.

StackOverflowUser
źródło
3

Nie jestem pewien, czy znalazłeś odpowiedź, ale zrobiłem to i działa

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

Teraz w 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
źródło
3

Czy próbowałeś przełączyć się na WebInvokeAttribute i ustawić metodę na „GET”?

Wydaje mi się, że miałem podobny problem i przełączyłem się na jawne wskazanie, której metody (GET / PUT / POST / DELETE) oczekuje się w przypadku większości moich metod, jeśli nie wszystkich.

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 powinien sobie z tym poradzić, ale widziałem, że ma pewne problemy z wieloma Get znacznie mniej wielokrotnymi Get tego samego typu zwrotu.

[Edycja: nic z tego nie jest poprawne po wygaśnięciu WCF WebAPI i migracji do ASP.Net WebAPI na stosie MVC]

PMontgomery
źródło
1
Przykro mi, zapomniałem wspomnieć, że przenoszę kod do ASP.NET Web API, ponieważ WCF Web API zostało wycofane. Edytowałem post. Dziękuję Ci.
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
źródło
Witamy w Stack Overflow! Proszę edytować swoją odpowiedź na to wyjaśnienie kodu, jak również opis jak to różni się od czternastu innych odpowiedzi tutaj. To pytanie ma prawie osiem lat i ma już zaakceptowane i kilka dobrze wyjaśnionych odpowiedzi. Bez twojego wyjaśnienia prawdopodobnie zostanie odrzucony lub usunięty. Takie wyjaśnienie pomoże uzasadnić miejsce Twojej odpowiedzi na to pytanie.
Das_Geek,
1
Osobiście (wiem, jakie są zalecenia SO) na tak jasne / podstawowe pytanie , osobiście wolałbym mieć czystą odpowiedź kodową . Nie chcę czytać wielu wyjaśnień. Chcę szybko tworzyć przydatne, funkcjonalne oprogramowanie . +1
MemeDeveloper
2

Alternatywa dla leniwych / pośpiesznych (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;}

Wzywając ich:

localhost: 5000 / api / nazwa kontrolera / metoda1-42

„cześć42”

localhost: 5000 / api / nazwa kontrolera / metoda2-99

„world99”

Arthur Zennig
źródło
0

Żaden z powyższych przykładów nie spełnił moich osobistych potrzeb. Poniżej znajduje się to, co ostatecznie zrobiłem.

 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;             

    }

Aby wykorzystać powyższe na swojej trasie, użyj:

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

To, co się dzieje, to rodzaj fałszerstw w metodzie, tak że ta trasa będzie pasować tylko do domyślnych metod GET, POST, PUT i DELETE. „Prawda” mówi, że chcemy sprawdzić zgodność elementów w tablicy. Gdyby to było fałszywe, powiedziałbyś, wykluczaj te w str Możesz wtedy użyć tras powyżej tej domyślnej metody, takich jak:

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

W powyższym zasadniczo szuka następującego adresu URL => http://www.domain.com/Account/Status/Activelub czegoś podobnego.

Poza tym nie jestem pewien, czy byłbym zbyt szalony. Pod koniec dnia powinno to być na zasób. Ale widzę potrzebę mapowania przyjaznych adresów URL z różnych powodów. Jestem pewien, że w miarę ewolucji Web Api pojawi się jakiś rodzaj zabezpieczenia. Jeśli czas, zbuduję trwalsze rozwiązanie i opublikuję.

origin1tech
źródło
Możesz użyć new System.Web.Http.Routing.HttpMethodConstraint(HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete) zamiast tego.
abatishchev
0

Nie mogłem sprawić, by żadne z powyższych rozwiązań routingu działało - wydaje się, że część składni uległa zmianie i nadal jestem nowy w MVC - w mgnieniu oka, chociaż stworzyłem ten naprawdę okropny (i prosty) hack, który mnie dostanie na razie - uwaga, to zastępuje metodę "public MyObject GetMyObjects (long id)" - zmieniamy typ "id" na string, a zwracany typ na 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
źródło
0

Jeśli masz wiele akcji w tym samym pliku, przekaż ten sam argument, np. Id do wszystkich akcji. Dzieje się tak, ponieważ akcja może tylko identyfikować Id, więc zamiast nadawać argumentowi jakąkolwiek nazwę, zadeklaruj tylko Id w ten sposób.


[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
źródło
Jak wyglądałby adres URL przy wyświetlaniu każdej funkcji w przeglądarce?
Si8
0

Prosta alternatywa

Po prostu użyj ciągu zapytania.

Wytyczanie

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

Kontroler

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

    public SomeViewModel GetById(int objectId)
    {
    }
}

Upraszanie

GET /Test
GET /Test?objectId=1

Uwaga

Pamiętaj, że parametr ciągu zapytania nie powinien mieć wartości „id” ani żadnego innego parametru skonfigurowanej trasy.

Seth Flowers
źródło
-1

Zmodyfikuj WebApiConfig i dodaj na końcu kolejne Routes.MapHttpRoute w następujący sposób:

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

Następnie utwórz taki kontroler:

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

Tak to rozwiązałem. Mam nadzieję, że komuś to pomoże.

Eduardo Mercado
źródło