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.
Related
I notice that there are a bunch of similar questions out there about this topic.
I'm getting this error when calling any of the methods below.
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
I can't however sort out what is best practice in resolving the issue.
So far I haven't set up any specific routing middleware.
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
What you're trying to do is impossible because the actions are dynamically activated. The request data (such as a query string) cannot be bound until the framework knows the action signature. It can't know the action signature until it follows the route. Therefore, you can't make routing dependent on things the framework doesn't even know yet.
Long and short, you need to differentiate the routes in some way: either some other static path or making the userId a route param. However, you don't actually need separate actions here. All action params are optional by default. Therefore, you can just have:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenu(int menuId, int userId)
And then you can branch on whether userId == 0 (the default). That should be fine here, because there will never be a user with an id of 0, but you may also consider making the param nullable and then branching on userId.HasValue instead, which is a bit more explicit.
You can also continue to keep the logic separate, if you prefer, by utilizing private methods. For example:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItems(int menuId, int userId) =>
userId == 0 ? GetMenuItemsByMenuId(menuId) : GetMenuItemsByUserId(menuId, userId);
private IActionResult GetMenuItemsByMenuId(int menuId)
{
...
}
private IActionResult GetMenuItemsByUserId(int menuId, int userId)
{
...
}
Action routes need to be unique to avoid route conflicts.
If willing to change the URL consider including the userId in the route
// api/menus/{menuId}/menuitems
[HttpGet("{menuId:int}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
//....
}
// api/menus/{menuId}/menuitems/{userId}
[HttpGet("{menuId:int}/menuitems/{userId:int}")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId) {
//...
}
##Reference Routing to controller actions in ASP.NET Core
##Reference Routing in ASP.NET Core
You have the same route in your HttpGet attribute
Change to something like this :
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/getAllMenusItems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/getMenuItemsFiltered")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
This is another solution that you can use for this kind of scenario:
Solution 1 and more complex, using IActionConstrain, and ModelBinders(this gives you the flexibility to bind your input to a specific DTO):
The problem you have is that your controller has the same routing for 2 different methods receiving different parameters.
Let me illustrate it with a similar example, you can have the 2 methods like this:
Get(string entityName, long id)
Get(string entityname, string timestamp)
So far this is valid, at least C# is not giving you an error because it is an overload of parameters. But with the controller, you have a problem, when aspnet receives the extra parameter it doesn't know where to redirect your request.
You can change the routing which is one solution.
Normally I prefer to keep the same names and wrap the parameters on a DtoClass, IntDto and StringDto for example
public class IntDto
{
public int i { get; set; }
}
public class StringDto
{
public string i { get; set; }
}
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
but still, you have the error. In order to bind your input to the specific type on your methods, I create a ModelBinder, for this scenario, it is below(see that I am trying to parse the parameter from the query string but I am using a discriminator header which is used normally for content negotiation between the client and the server(Content negotiation):
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
dynamic model = null;
string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.Accept).Value;
var val = bindingContext.HttpContext.Request.QueryString.Value.Trim('?').Split('=')[1];
if (contentType == "application/myContentType.json")
{
model = new StringDto{i = val};
}
else model = new IntDto{ i = int.Parse(val)};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Then you need to create a ModelBinderProvider (see that if I am receiving trying to bind one of these types, then I use MyModelBinder)
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(IntDto) || context.Metadata.ModelType == typeof(StringDto))
return new MyModelBinder();
return null;
}
and register it into the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
});
}
So far you didn't resolve the issue you have but we are close. In order to hit the controller actions now, you need to pass a header type on the request: application/json or application/myContentType.json. But in order to support conditional logic to determine whether or not an associated action method is valid or not to be selected for a given request, you can create your own ActionConstraint. Basically the idea here is to decorate your ActionMethod with this attribute to restrict the user to hit that action if he doesn't pass the correct media type. See below the code and how to use it
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
{
private readonly string[] _mediaTypes;
private readonly string _requestHeaderToMatch;
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
}
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes, int order)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
Order = order;
}
public int Order { get; set; }
public bool Accept(ActionConstraintContext context)
{
var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
{
return false;
}
// if one of the media types matches, return true
foreach (var mediaType in _mediaTypes)
{
var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
mediaType, StringComparison.OrdinalIgnoreCase);
if (mediaTypeMatches)
{
return true;
}
}
return false;
}
}
Here is your final change:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/json" })]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/myContentType.json" })]
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
Now the error is gone if you run your app. But how you pass the parameters?:
This one is going to hit this method:
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
And this one the other one:
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
Solution 2: Routes constrains
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet("{i:int}")]
public IActionResult Get(int i)
{
return new JsonResult(i);
}
[HttpGet("{i}")]
public IActionResult Get(string i)
{
return new JsonResult(i);
}
}
This is a kind of test because I am using the default routing:
https://localhost:44374/weatherforecast/"test" should go to the one that receives the string parameter
https://localhost:44374/weatherforecast/1 should go to the one that receives an int parameter
In my case [HttpPost("[action]")] was written twice.
I got this error, and just needed to restart the service to get it working again. Probably because I was modifying the code, and it re-registered the same controller method somehow.
You can have a dispatcher endpoint that will get the calls from both endpoints and will call the right based on parameters.
(It will works fine if their are in same controller).
Example:
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId, int? userId)
{
if(userId.HasValue)
return GetMenuItemsByMenuAndUser(menuId, userId)
.... original logic
}
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
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.
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 })
As a result of a previous question of mine, I have discovered two ways of handling REST routes in MVC3.
This is a followup question where I am trying to learn factual differences/subtleties between these two approaches. I am looking authoritative answer if possible.
Method 1: Single Route, with Action Name + Http Verb Attributes on Controller Actions
Register a single route in Global.asax using a specified action parameter.
public override void RegisterArea(AreaRegistrationContext context)
{
// actions should handle: GET, POST, PUT, DELETE
context.MapRoute("Api-SinglePost", "api/posts/{id}",
new { controller = "Posts", action = "SinglePost" });
}
Apply both ActionName and HttpVerb attributes to controller actions
[HttpGet]
[ActionName("SinglePost")]
public JsonResult Get(string id)
{
return Json(_service.Get(id));
}
[HttpDelete]
[ActionName("SinglePost")]
public JsonResult Delete(string id)
{
return Json(_service.Delete(id));
}
[HttpPost]
[ActionName("SinglePost")]
public JsonResult Create(Post post)
{
return Json(_service.Save(post));
}
[HttpPut]
[ActionName("SinglePost")]
public JsonResult Update(Post post)
{
return Json(_service.Update(post););
}
Method 2: Unique Routes + Verb Constraints, with Http Verb Attribute on Controller Actions
Register unique routes in Global.asax with HttpMethodContraint
var postsUrl = "api/posts";
routes.MapRoute("posts-get", postsUrl + "/{id}",
new { controller = "Posts", action = "Get",
new { httpMethod = new HttpMethodConstraint("GET") });
routes.MapRoute("posts-create", postsUrl,
new { controller = "Posts", action = "Create",
new { httpMethod = new HttpMethodConstraint("POST") });
routes.MapRoute("posts-update", postsUrl,
new { controller = "Posts", action = "Update",
new { httpMethod = new HttpMethodConstraint("PUT") });
routes.MapRoute("posts-delete", postsUrl + "/{id}",
new { controller = "Posts", action = "Delete",
new { httpMethod = new HttpMethodConstraint("DELETE") });
Use only an Http Verb Attribute on the Controller Actions
[HttpGet]
public JsonResult Get(string id)
{
return Json(_service.Get(id));
}
[HttpDelete]
public JsonResult Delete(string id)
{
return Json(_service.Delete(id));
}
[HttpPost]
public JsonResult Create(Post post)
{
return Json(_service.Save(post));
}
[HttpPut]
public JsonResult Update(Post post)
{
return Json(_service.Update(post););
}
Both of these methods let me have uniquely named Controller Action Methods, and allow for RESTful routes tied to the verbs... but what is inherently different about restricting the route vs. using a proxy action name?
You won't get an authoritative answer by here are my 2 cents:
I prefer method 2 because then you have all your routing in one place. You can encapsulate your routing into a method e.g. MapResourceRoutes(string controller, string uri) and have it used muptiple times throughout your API.
Also method 2 gives you clearly named routes that you can use for linking and reverse routing.
I don't know that you will ever find an authoritative answer, but I will offer my opinion and as you can tell by my points, my opinion matters ;-). My purist self thinks that the first option is more pure, however my experience has been that helper methods like Url.Action() can sometimes have trouble resolving the correct route with this approach and I have taken to the second method as it really only has implications internally as the api looks identical to the consumer.
At this point, the correct answer is use Web API.