I have a super simple controller with 2 methods:
public IActionResult Users(long id)
{
return Json(new { name = "Example User" });
}
public IActionResult Users()
{
return Json(new { list = new List<User>() });
}
One to select all users and the other to return all users. In web api 2 I could user the following route and everything worked fine:
config.Routes.MapHttpRoute(
name: "Users",
routeTemplate: "v1/Users",
defaults: new { action = "Users", controller = "Users" },
constraints: null,
handler: new TokenValidationHandler() { InnerHandler = new HttpControllerDispatcher(config) }
);
I have the following routes setup in startup.cs:
app.UseMvc(routes =>
{
routes.MapRoute(name: "User_Default", template: "v1/{controller=Users}/{action=Users}/{id?}");
});
However this gives me a AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied
What am I doing wrong?
In your original webapi code, you were using Routes.MapHttpRoute which adds webapi specific routes. This is different from an MVC route which won´t take into account the parameters in the action, for instance you would have the same problem in MVC 5 if you were using Routes.MapRoute.
The same thing is happening in your MVC 6 code, since you are adding a standard MVC route using routes.MapRoute. In both cases the framework is finding 2 controller actions matching the same route with no additional constraints. It needs some help in order to select one of those 2 actions.
The easiest way to disambiguate the api actions would be using attribute routing instead of defining a route, as in this example:
[Route("v1/[controller]")]
public class UsersController : Controller
{
[HttpGet("{id:int}")]
public IActionResult Users(long id)
{
return Json(new { name = "Example User" });
}
public IActionResult Users()
{
return Json(new { list = new[] { "a", "b" } });
}
}
There are other options that would let you change the behaviour of the MVC routing in MVC 6. You could create your own IActionConstraint attribute to enforce having or not a given parameter. That way one of those actions requires an id parameter in the route while the other requires not to have an id parameter (Warning, untested code):
public class UsersController : Controller
{
[RouteParameterConstraint("id", ShouldAppear=true)]
public IActionResult Users(long id)
{
return Json(new { name = "Example User" });
}
[RouteParameterConstraint("id", ShouldNotAppear=true)]
public IActionResult Users()
{
return Json(new { list = new[] { "a", "b" } });
}
}
public class RouteParameterConstraintAttribute : Attribute, IActionConstraint
{
private routeParameterName;
public RouteParameterConstraintAttribute(string routeParameterName)
{
this.routerParamterName = routerParameterName;
}
public int Order => 0;
public bool ShouldAppear {get; set;}
public bool ShouldNotAppear {get; set;}
public bool Accept(ActionConstraintContext context)
{
if(ShouldAppear) return context.RouteContext.RouteData.Values["country"] != null;
if(ShouldNotAppear) return context.RouteContext.RouteData.Values["country"] == null;
return true;
}
}
A better option to deal with webapi 2 style controllers would be adding conventions into the MVC pipeline. This is exactly what the Microsoft.AspNet.Mvc.WebApiCompatShim is doing to help migrating webapi 2 controllers. You can see the conventions added here. Check this guide for a quick overview of this package.
Related
I am migrating project from Ruby on Rails to .NET Core, and I am lost on routing part, because path or slug part in url hit multiple controllers. I will explain:
LocationController(string path)
http://www.website.com/asia
http://www.website.com/north-america/usa/florida
Path is everything except website, so: asia, north-america/usa/florida
SchoolController(string slug)
http://www.website.com/st-martin-school
http://www.website.com/rene-claudius-school
Slug is everything except website, so: st-martin-school, rene-claudius-school
PageController(string slug)
http://www.website.com/privacy-policy
http://www.website.com/contact
Slug is everything except website, so: privacy-policy, contact
I know the solution isnt perfect, but this was decision because of SEO and now i can't change the urls. .NET core app now don't know what endpoint to hit. The routing on Ruby on Rails was different, it went over configured routes from top to down and what endpoint hit first, it was executed. How to solve this issue in .net core? I was reading about Wildcards, but still don't know how to use it.
Thanks for help.
Solution
Based on responses, It gave me idea of solution by custom constraint. Here it is.
Create a location constraint to catch all valid locations starting by continent name string:
public class LocationConstraint : IRouteConstraint
{
private static readonly string[] continents = { "africa", "asia", "australia", "europe", "north-america", "south-america" };
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (values[routeKey] is null)
{
return false;
}
string routeValue = values[routeKey].ToString();
if (continents.Contains(routeValue) || continents.Any(c => routeValue.StartsWith(c + "/", System.StringComparison.CurrentCultureIgnoreCase)))
{
return true;
}
return false;
}
}
Catch all valid location in LocationController:
[HttpGet("{**path:location}")]
public async Task<IActionResult> List(string path)
{
Catch all valid static routes in PageController:
[HttpGet("latest")]
...
[HttpGet("privacy-policy")]
...
Catch rest all schools in SchoolController:
[HttpGet("{slug}")]
public async Task<IActionResult> Detail(string slug)
{
You can achieve this by mapping appropriate routes in route table. In ASP.NET Core mapping routes is not allowed if the controller is decorated with [ApiController].
So if you are not using [ApiController] on your controller, following the solution I propose.
Following are the controllers and actions inside them, for example.
public class LocationController : ControllerBase
{
[HttpGet]
public ActionResult Get(string path, string country, string city)
{
return new JsonResult(new { Path = path, Country = country, City = city });
}
}
public class SchoolController : ControllerBase
{
[HttpGet]
public ActionResult Get(string slug)
{
return new JsonResult(new { Slug = slug });
}
}
public class PageController : ControllerBase
{
[HttpGet]
public ActionResult Get(string slug)
{
return new JsonResult(new { PageSlug = slug });
}
}
Following is the route mapping needed.
app.MapControllerRoute("schoolroute",
"/{slug=*-school}",
new { controller = "School", action = "Get" });
app.MapControllerRoute("privacypolicy",
"/{slug=privacy-policy}",
new { controller = "Page", action = "Get" });
app.MapControllerRoute("contact",
"/{slug=contact}",
new { controller = "Page", action = "Slug" });
app.MapControllerRoute("pathroute",
"/{path}/{country}/{city}",
new
{
controller = "Location",
action = "Get",
country = "",
city = ""
});
The controller action method signatures, in above solution, may not match with yours. My suggestion is to make appropriate changes in your code if possible. Else share relevant code in the original question and explain the issue you face.
namespace EmployeeApi.Controllers
{
public class EmployeeDetailsController : ApiController
{
// GET api/employeedetails
public IEnumerable<Employee> Get()
{
}
public IEnumerable<Details> Get(int id)
{
}
public IEnumerable<Team> GetTeamMember()
{
}
public IEnumerable<Details> GetTid(int id)
{
}
}
I would like to have my webApi something like this:
1) IEnumerable<Employee> Get() -> api/employeedetails
2) IEnumerable<Details> Get(int id) -> api/employeedetails/id
3) IEnumerable<Team> GetTeamMember() -> api/employeedetails/id/teammember
4) IEnumerable<Details> GetTid(int id) -> api/employeedetails/id/teammember/tid
I tried making changes to routing, but as I am new to it, could'nt understand much.So, please can some one help me understand and guide me on how this should be done.
Thanks in advance..:)
You could do this with Attribute Routing.
I prefere to use them as they give an easy overview on how the routing is configured when reading the controllers method.
namespace EmployeeApi.Controllers
{
public class EmployeeDetailsController : ApiController
{
// GET api/employeedetails
[Route("api/employeedetails")]
[HttpGet]
public IEnumerable<Employee> Get()
{
}
// GET api/employeedetails/1
[Route("api/employeedetails/{id}")]
[HttpGet]
public IEnumerable<Details> Get(int id)
{
}
// GET api/employeedetails/id/teammember
[Route("api/employeedetails/id/teammember")]
[HttpGet]
public IEnumerable<Team> GetTeamMember()
{
}
// GET api/employeedetails/id/teammember/1
[Route("api/employeedetails/id/teammember/{tid}")]
[HttpGet]
public IEnumerable<Details> GetTid(int tid)
{
}
}
You can also use RoutePrefix on top of the controller that specifies the prefix for the controller route, in your case the "api/employeedetails". You can find more details in the "Route Prefixes" section in the link
After the list of relevant comments has grown, I'll restructure my original answer now.
If you're not able to use attribute routing as suggested in Marcus' answer (see my update statement at the bottom), you need to configure your routes (probably in the App_Start/RouteConfig.cs file). You can try the following code there:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
name: "GetEmployeeDetails",
url: "api/employeedetails",
defaults: new { controller = "EmployeeDetails", action = "GetEmployees" }
);
routes.MapRoute(
name: "GetEmployeeDetailsById",
url: "api/employeedetails/{employeeId}",
defaults: new { controller = "EmployeeDetails", action = "GetDetails", employeeId = UrlParameter.Optional }
);
routes.MapRoute(
name: "GetTeamMember",
url: "api/employeedetails/{employeeId}/teammember",
defaults: new { controller = "EmployeeDetails", action = "GetTeams", employeeId = UrlParameter.Optional }
);
routes.MapRoute(
name: "GetTeamMemberById",
url: "api/employeedetails/{employeeId}/teammember/{teamId}",
defaults: new { controller = "EmployeeDetails", action = "GetDetailsForTeam", employeeId = UrlParameter.Optional, teamId = UrlParameter.Optional }
);
}
}
There'll probably be more routes (for example a generic default route) and also routes to be ignored, but this is out if scope for this question.
These routes correspond with the following action methods within your controller class:
public class EmployeeDetailsController : Controller
{
public IEnumerable<Employee> GetEmployees()
{
// Get your list of employees here
return ...;
}
public IEnumerable<Detail> GetDetails(int employeeId = 0)
{
// Get your list of details here
return ...;
}
public IEnumerable<Team> GetTeams(int employeeId = 0)
{
// Get your list of teams here
return ...;
}
public IEnumerable<Detail> GetDetailsForTeam(int employeeId = 0, int teamId = 0)
{
// Get your list of details here
return ...;
}
}
There is a chance that you do not need the employeeId parameter for the GetDetailsForTeam() method, since maybe the teamId is sufficient to get the desired information. If that is the case you can remove the parameter from the action method and the corresponding route.
These route configurations are pretty straightforward. Each route needs a unique name, otherwise you'll end up with a runtime error. The url - well - contains the url that route is supposed to handle. And after that you can specify the controller name, the action method to be called (these are your Get methods) and their respective parameters.
A word or two regarding naming conventions: In a controller named EmployeeDetailsController I would expect every "generically named" action method to return one or many EmployeeDetails objects (or their respective ActionResults). Therefore, a simple Get() method should return one or many EmployeeDetails objects.
In case you want to return objects of different types I would choose specific names (as suggested in my code above). In your case that would be a GetEmployees() method, a GetDetails(int employeeId = 0) method, a GetTeams(int employeeId = 0) method and a GetDetailsForTeam(int employeeId = 0, int teamId = 0) method. Note the optional parameters here.
If you have these methods in place I'd start with the routing. You need to make sure that each route can be connected to exactly one action method; that's why I asked for the complete URL in one of the comments. If you keep getting the "multiple actions were found" error, you're route URLs are not configured in such a way.
Also please note that route order does matter, though in your example I don't see any conflicting routes.
UPDATE: As an alternative you could use attribute routing, where you put the desired route directly into an attribute of your action method inside the controller. But for this to work with ASP.NET MVC 4 you'd need to install the AttributeRouting NuGet package.
I need to define template based routing to controller and then attribute based for actions in ASP.NET Core. Something like:
public class Foo : Controller
{
[HttpGet]
public object Get()
{
return new
{
ID = "A"
};
}
[HttpPost]
public object Create([FromBody]dynamic entity)
{
return new
{
ID = "B"
};
}
}
Route
app.UseMvc(routes =>
{
routes.MapRoute("Settings", "settings/api/foo",
new { controller = "Foo" }
);
});
And I expect this to work:
GET /settings/api/foo
POST /settings/api/foo
Unfortunately it is not a case. It looks like route attributes are ignored. What is the best way to achieve requirement?
The trick here is to route the URL to a specific controller and action. Then use action method overloading with an action method selector to switch between the GET and POST.
Change your route setup code to this:
app.UseMvc(routes =>
{
routes.MapRoute(
"Settings",
"settings/api/foo",
new {
controller = "Foo", // specific controller
action = "DoThing", // AND specific action
}
);
});
And change the controller to have both action methods (or however many you want - one for each HTTP verb) to have the same name, but using different action methods selectors:
public class FooController : Controller
{
[HttpGet] // different action method selector!
public object DoThing() // same name!
{
return new
{
ID = "A"
};
}
[HttpPost] // different action method selector!
public object DoThing([FromBody]dynamic entity) // same name!
{
return new
{
ID = "B"
};
}
}
This way MVC will route all the requests for that URL to an action called DoThing on controller Foo. Once it gets there it sees "oh my, oh my, there are two actions with the same name!" But then it sees the [HttpGet] and [HttpPost] action method selectors, and whichever one of them says it can handle the request will win.
I am building an angularJS application with a asp.net webapi backend. In my routeconfig file, i have this
routes.MapRoute(
name: "default",
url: "{*url}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
This works fine. Any Url that is called is returned the Home/Index view (the only view i have) to the application, and angularJS works out if there is a querystring and works out which state to show.
I have the basic Get, Put, Post and Delete methods in my WebApi, and i can call them fine. Examples are
public class CompanyController : ApiController
{
private CompanyService _service;
public CompanyController(CompanyService service)
{
_service = service;
}
public async Task<IHttpActionResult> Get()
{
...
return Ok(model);
}
public async Task<IHttpActionResult> Get(int id)
{
...
return Ok(model);
}
public async Task<IHttpActionResult> Post(CompanyModel model)
{
...
return Ok();
}
public async Task<IHttpActionResult> Put(Company model)
{
...
return Ok();
}
public async Task<IHttpActionResult> Delete(CompanyModel model)
{
...
return Ok();
}
}
Now i would like to add another method to my api, where the user can load companies, but also pass in a term to search for (a string), a pageSize (int) and a page number (int). Something like this
public async Task<IHttpActionResult> Get(string term, int page, int pageSize) {
...
return Ok(results);
}
Now i understand that i need to add another route, to make sure this method can be called. Fine, so i add this to my RouteConfig.
// search
routes.MapRoute(
name: "search",
url: "api/{controller}/{page}/{pageSize}/{term}",
defaults: new { page = #"\d+", pageSize = #"\d+", term = UrlParameter.Optional }
);
Why doesnt this work?? I got a resource cannot be found error, when trying to call it via postman using the url localhost/api/company/1/10/a, where 1 = page, 10 = pageSize and a = term
Its probably a simple answer, but new to MVC so still learning.
1- You are using Get method, which means you can pass your search option via Url, so you can create a search option object like :
public class SearchOptions
{
public string Term{get; set;}
public int Page {get; set;}
public int PageSize {get; set;}
}
then you can change your method to be like this
[HttpGet]
[Route("api/blabla/SearchSomething")]
public async Task<IHttpActionResult> Get([FromUri]SearchOptions searchOptions) {
...
return Ok(results);
}
Notice the Route attribute that I've decorated the method by, you can use different constraints for the method parameters, have a look at this.
Finally you can call the method from the client like this
api/blabla/SearchSomething?term=somevalue&page=1&pageSize=10
Hope that helps.
How can I get the url from web api in my view?
Example (from the msdn-blog):
[RoutePrefix("reviews")]
public class ReviewsController : ApiController
{
// eg.: /reviews
[Route]
public IHttpActionResult Get() { ... }
// eg.: /reviews/5
[Route("{reviewId}")]
public IHttpActionResult Show(int reviewId) { ... }
// eg.: /reviews/5/edit
[Route("{reviewId}/edit")]
public IHttpActionResult Edit(int reviewId) { ... }
}
Now I want to construct "/reviews/edit" in my view, how can I do this?
I've tried creating a little extension method, but it requires me to give every route an actual "RouteName". Is there a method I can use (like in MVC) where I can just pass the controller and action?
#Url.Action("Edit", "Reviews)
The method I'm using now (with RouteName) also doesn't allow me to use integers as parameters (unless I pass a default value). If I do need to name all my routes, how can I create a route url, but pass my parameters in the "data"-portion of my request?
Current method:
public static string ResolveWebApiRoute(this UrlHelper urlHelper, string routeName, object routeValues = null)
{
var newRouteValues = new RouteValueDictionary(routeValues);
newRouteValues.Add("httproute", true);
return urlHelper.RouteUrl(routeName, newRouteValues);
}
EDIT
When I used methods like Url.RouteUrl(new { controller = ..., action = ...}), It redirects directly to that action (e.g. new { controller = "Reviews", action = "Show"} --> /reviews/show, whilest I want it to redirect to /reviews/...
Generating links to Web API routes always require a RouteName, so you should have something like below:
[Route("{reviewId}/edit", Name="EditView")]
public IHttpActionResult Edit(int reviewId) { ... }
You can then generate a link like /reviews/1/editto Web API.
Url.RouteUrl(routeName: "EditView", routeValues: new { httpRoute = true, reviewId = 1 });
or
Url.HttpRouteUrl(routeName: "EditView", routeValues: , reviewId = 1)
Note that route names need to be specified explicitly and they are no longer generated automatically like what #Karhgath is suggesting. This was a change made from RC to RTM version.
When using route attributes I was able to get the route of a WebApi2 controller from an MVC view using something like this:
Url.HttpRouteUrl("RouteName", new { })
In WebApi2 when using AttributeRouting, route names are named by default Controller.Action, but you could specify a RouteName also:
[RoutePrefix("reviews")]
public class ReviewsController : Controller
{
// The route name is defaulted to "Reviews.Index"
[Route]
public ActionResult Index() { ... }
// The route name is "ShowReviewById"
[Route("{reviewId}"), RouteName("ShowReviewById")]
public ActionResult Show(int reviewId) { ... }
// The route name is by default "Reviews.Edit"
[Route("{reviewId}/edit")]
public ActionResult Edit(int reviewId) { ... }
Then to call it in the view you only need to set the route name and send the parameters as an anonymous object:
// Outputs: /reviews/123
#Url.Action("ShowReviewById", new { reviewId = 123 })
// Outputs: /reviews/123/edit
#Url.Action("Reviews.Edit", new { reviewId = 123 })