Why are these two Actions considered ambiguous despite the parameters being different? - asp.net-mvc

The current request for action 'Index' on controller type 'UsersController' is ambiguous between the following action methods:
System.Web.Mvc.ActionResult PostUser(Models.SimpleUser) on type Controllers.UsersController
System.Web.Mvc.ActionResult PostUser(Int32, Models.SimpleUser) on type Controllers.UsersController
Is happening when I try to POST website.com/users/ with form values.
When there is NO ID (website.com/users/) I want it to create a new user, when there IS an ID (/users/51 for example) I want it to update that user, so how can I make it tell the difference between these two Actions?
Here are the two actions:
[HttpPost]
[ActionName("Index")]
public ActionResult PostUser(SimpleUser u)
{
return Content("User to be created...");
}
[HttpPost]
[ActionName("Index")]
public ActionResult PostUser(int id, SimpleUser u)
{
return Content("User to be updated...");
}
Here is the MapRoute:
routes.MapRoute(
"Users",
"users/{id}",
new { controller = "Users", action = "Index" }
);

The main problem is there is some ambiguity between your two action methods, as model binding doesn't help decern the routing configuration. Your current routing simply points to index method.
You can still have the same URL, but it would be helpful to name your actions differently and then apply some new routes.
[HttpPost]
public ActionResult Create(SimpleUser u)
{
return Content("User to be created...");
}
[HttpPost]
public ActionResult Edit(int id, SimpleUser u)
{
return Content("User to be updated...");
}
And then in your routing try
routes.MapRoute(
"UserEdit",
"users/{id}",
new { controller = "Users", action = "Edit",
httpMethod = new HttpMethodConstraint("POST") });
routes.MapRoute(
"UserCreate",
"users",
new { controller = "Users", action = "Create",
httpMethod = new HttpMethodConstraint("POST") });
The routes will be constrained to POST events only, which means you can still add some routing for your GET methods on the same route. Such as for a list or something.

You must disambiguate your methods by having two different action names.
What you can do to have a RESTful API is to create your own RouteHandler class which will change the routing, dependent on whether or not the {id} value is present. The RouteHandler class is simple:
using System.Web.Mvc;
using System.Web.Routing;
public class MyRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var routeData = requestContext.RouteData;
if (routeData.Values("id").ToString() == "0" /* our default invalid value */ )
{
var action = routeData.Values("action");
routeData.Values("action") = action + "Add";
}
var handler = new MvcHandler(requestContext);
return handler;
}
}
Next, modify RegisterRoutes() in Global.asax.cs as follows:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.Add("Default", new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home",
action = "Index",
id = "0" /* our default invalid value */ }),
new MyRouteHandler()));
}
Finally, change the ActionName attribute for your Add method to "IndexAdd". When the request comes in without an id, the default invalid value will be supplied, which will alert your RouteHandler to change the method to your "Add" method.
counsellorben

It would be better if you consider using Create and Edit actions to achieve what you want. Generally index can be used to list all the users(in your case).
About the problem you are facing, it is arising because there is no route which does not take any id as parameter.

Related

MVC - Passing path to action in controller

Fairly new to MVC, I would like the URLs of my article pages to be like this:-
http://www.example.com/article1
http://www.example.com/article2
http://www.example.com/article3
How can I set up the routing such that whenever someone types in the above it calls an action called article in the controller and passes the path to it?
I tried something like this but to no avail: -
routes.MapRoute(
name: "article",
url: "{article}",
defaults: new { controller = "Home", action = "article" }
);
One solution is to add multiple routes.
routes.MapRoute(
name: "article1",
url: "article1",
defaults: new { controller = "<YourControllerName>", action = "article1" }
);
routes.MapRoute(
name: "article2",
url: "article2",
defaults: new { controller = "<YourControllerName>", action = "article2" }
);
Edit:
From OP's comment, it is understood that there would be 'n' number of articles(urls). To deal with that, we can create a custom route handler.
Step 1: Create a new custom route handler inheriting from MvcRouteHandler
public class CustomRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var controller = requestContext.RouteData.Values["controller"].ToString();
requestContext.RouteData.Values["controller"] = "Home";
requestContext.RouteData.Values["action"] = "Index";
requestContext.RouteData.Values["articleId"] = controller;
return base.GetHttpHandler(requestContext);
}
}
Step 2: Register the new route. Make sure to add this route before default route.
routes.Add("Article", new Route("{controller}", new CustomRouteHandler()));
In the given CustomRouteHandler class, the Controller and Action is hard coded with "Home" and "Index" respectively. You can change that to your own controller and action name. Also you would see a "articleId" setting to RouteData.Values. With that setting, you would get the articleId as a parameter in your Action method.
public ActionResult Index(string articleId)
{
return View();
}
After all the changes, for the url http://www.example.com/article1, the Index() method of HomeController gets invoked with articleId set to 'article1'.
Similarly for http://www.example.com/article2, the Index() method gets invoked with parameter articleId set to 'article2'.

How do I use routes for multi-tenancy in all but one controller?

Our app has multiple tenants. Every tenant has a short code assigned to them that users know them by. I want to use that code in my URLs as a route parameter, and have Ninject inject a DbContext with the tenant's database connection string into the tenant-specific controllers.
So for examine I have a CarController, and every tenant has their own products. The URLs would look like {tenantcode}/{controller}/{action}. I understand how to do this part.
However, I have several controllers that should NOT be instanced by tenant. Specifically, the home controller, and account controller for login/registration. These don't matter.
So example URLs I need:
myapp.com/ - HomeController
myapp.com/Account/Login - AccountController
myapp.com/GM/Car/Add - CarController that has GM's DbContext injected
myapp.com/Ford/Car/Add - CarController that has Ford's DbContext injected
How can I exclude certain controllers from routes? Running ASP.NET MVC 5.
Many thanks to Darko Z for starting me in the right direction. I ended up using a hybrid of traditional routes, and the new attribute based routing in MVC 5.
First, the "excluded" routes got decorated with the new RouteAttribute class
public class HomeController : Controller
{
private readonly TenantContext context;
public HomeController(TenantContext Context)
{
this.context = Context;
}
//
// GET: http://myapp.com/
// By decorating just this action with an empty RouteAttribute, we make it the "start page"
[Route]
public ActionResult Index(bool Error = false)
{
// Look up and make a nice list of the tenants this user can access
var tenantQuery =
from u in context.Users
where u.UserId == userId
from t in u.Tenants
select new
{
t.Id,
t.Name,
};
return View(tenantQuery);
}
}
// By decorating this whole controller with RouteAttribute, all /Account URLs wind up here
[Route("Account/{action}")]
public class AccountController : Controller
{
//
// GET: /Account/LogOn
public ActionResult LogOn()
{
return View();
}
//
// POST: /Account/LogOn
[HttpPost]
public ActionResult LogOn(LogOnViewModel model, string ReturnUrl)
{
// Log on logic here
}
}
Next, I register the tenant generic route that Darko Z suggested. It's important to call MapMvcAttributeRoutes() before making other routes. This is because my attribute based routes are the "exceptions", and like he said, those exceptions have to be at the top to make sure they are picked up first.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// exceptions are the attribute-based routes
routes.MapMvcAttributeRoutes();
// tenant code is the default route
routes.MapRoute(
name: "Tenant",
url: "{tenantcode}/{controller}/{action}/{id}",
defaults: new { controller = "TenantHome", action = "Index", id = UrlParameter.Optional }
);
}
}
So as I'm sure you know you specify routes in MVC in the order from most specific to most generic. So in your case I would do something like this:
//exclusions - basically hardcoded, pacing this at the top will
//ensure that these will be picked up first. Of course this means
//you must make sure that tenant codes cannot be the same as any
//controller name here
routes.MapRoute(
"Home",
"Home/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
routes.MapRoute(
"Account",
"Account/{action}/{id}",
new { controller = "Account", action = "Index", id = "" }
);
//tenant generic route
routes.MapRoute(
"Default",
"{tenantcode}/{controller}/{action}",
new { tenantcode = "Default", controller = "Tenant", action = "Index" }
);
//default route
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
This is obviously only good if there are less excluded controllers than controllers that need the tenant code. If not then you can take the opposite approach and reverse the above. Main takeaway here is that (happy to be proven wrong) there is no way to have a generic ignore within an AddRoute call. While there is an IgnoreRoute, that just completely doesn't apply any routing rules and is used for static resources. Hope that helps.

Asp.Net Custom Routing and custom routing and add category before controller

I'm just learning MVC and want to add some custom routing to my site.
My site is split into brands so before accessing other parts of the site the user will select a brand. Rather than storing the chosen brand somewhere or passing it as a parameter I would like to make it part of the URL so to access the NewsControllers index action for example rather than "mysite.com/news" I would like to use "mysite.com/brand/news/".
I just really want to add a route which says if a URL has a brand, go to the controller/action as normal and pass through the brand...is this possible?
Thanks
C
Yes, this is possible. First, you must create a RouteConstraint to insure that a brand has been chosen. If a brand has not been chosen, this route should fail, and a route to an action to redirect to the brand selector should follow. The RouteConstraint should look like this:
using System;
using System.Web;
using System.Web.Routing;
namespace Examples.Extensions
{
public class MustBeBrand : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if this is a valid brand
var _db = new BrandDbContext();
return _db.Brands.FirstOrDefault(x => x.BrandName.ToLowerInvariant() ==
values[parameterName].ToString().ToLowerInvariant()) != null;
}
}
}
Then, define your Routes as follows (assuming that your brand selector is the home page):
routes.MapRoute(
"BrandRoute",
"{controller}/{brand}/{action}/{id}",
new { controller = "News", action = "Index", id = UrlParameter.Optional },
new { brand = new MustBeBrand() }
);
routes.MapRoute(
"Default",
"",
new { controller = "Selector", action = "Index" }
);
routes.MapRoute(
"NotBrandRoute",
"{*ignoreThis}",
new { controller = "Selector", action = "Redirect" }
);
Then, in your SelectorController:
public ActionResult Redirect()
{
return RedirectToAction("Index");
}
public ActionResult Index()
{
// brand selector action
}
If your home page is not the brand selector, or there is other non-brand content on the site, then this routing is not correct. You will need additional routes between BrandRoute and Default which match routes to your other content.

ASP.NET MVC Routing with Default Controller

For a scenario, I have a ASP.NET MVC application with URLs that look like the following:
http://example.com/Customer/List
http://example.com/Customer/List/Page/2
http://example.com/Customer/List
http://example.com/Customer/View/8372
http://example.com/Customer/Search/foo/Page/5
These URLs are achieved with following routes in Global.asax.cs
routes.MapRoute(
"CustomerSearch"
, "Customer/Search/{query}/Page/{page}"
, new { controller = "Customer", action = "Search" }
);
routes.MapRoute(
"CustomerGeneric"
, "Customer/{action}/{id}/Page/{page}"
, new { controller = "Customer" }
);
//-- Default Route
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Customer", action = "Index", id = "" }
);
These all have gone well until a new requirement arrived and wants to drop keyword 'Customer' off the URL, to make the URLs look like:
http://example.com/List
http://example.com/List/Page/2
http://example.com/List
http://example.com/View/8372
http://example.com/Search/foo/Page/5
Edit: corrected example links, thanks to #haacked.
I tried to add new MapRoutes to take {action} only and have default controller set to Customer. eg/
routes.MapRoute(
"CustomerFoo"
, "{action}"
, new { controller = "Customer", action = "Index" }
);
This seems to work, however now all links generated by Html.ActionLink() are weird and no longer URL friendly.
So, is this achievable? Am I approaching in the right direction?
don't mix a rule like: "{action}/{id}" with one that's "{controller}/{action}/{id}" ... specially when id in the later has a default value i.e. is optional.
In that case you have nothing that allows routing to know which one is the right one to use.
A workaround, if that's what you need, would be to add a constrain (see this) to the action in the earlier to a set of values i.e. List, View. Of course that with these types of rules, you can't have a controller with the same name of an action.
Also remember that if you specify a default action & id in the "{action}/{id}" rule, that will be used when you hit the route of your site.
Why does the first URL in the new list still have "Customer". I assume that's a typo and you meant:
http://example.com/List
http://example.com/List/Page/2
http://example.com/List
http://example.com/View/8372
http://example.com/Search/foo/Page/5
The following routes work for me:
routes.MapRoute(
"CustomerSearch"
, "Search/{query}/Page/{page}"
, new { controller = "Customer", action = "Search" }
);
routes.MapRoute(
"CustomerGeneric"
, "{action}/{id}/Page/{page}"
, new { controller = "Customer" }
);
//-- Default Route
routes.MapRoute(
"Default",
"{action}/{id}",
new { controller = "Customer", action = "Index", id = "" }
);
How are you generating your links. Since the Controller is no longer in the URL of your route (aka, you don't have "{controller}" in the route URL), but it's a default value, you need to make sure to specify the controller when generating routes.
Thus instead of
Html.ActionLink("LinkText", "ActionName")
do
Html.ActionLink("LinkText", "ActionName", "Customer")
Why? Suppose you had the following routes.
routes.MapRoute(
"Default",
"foo/{action}",
new { controller = "Cool" }
);
routes.MapRoute(
"Default",
"bar/{action}",
new { controller = "Neat" }
);
Which route did you mean when you call this?
<%= Html.ActionLink("LinkText", "ActionName") %>
You can differentiate by specifying the controller and we'll pick the one that has a default value that matches the specified one.
You can create a route that is constrained to only match actions in your Customer controller.
public static class RoutingExtensions {
///<summary>Creates a route that maps URLs without a controller to action methods in the specified controller</summary>
///<typeparam name="TController">The controller type to map the URLs to.</typeparam>
public static void MapDefaultController<TController>(this RouteCollection routes) where TController : ControllerBase {
routes.MapControllerActions<TController>(typeof(TController).Name, "{action}/{id}", new { action = "Index", id = UrlParameter.Optional });
}
///<summary>Creates a route that only matches actions from the given controller.</summary>
///<typeparam name="TController">The controller type to map the URLs to.</typeparam>
public static void MapControllerActions<TController>(this RouteCollection routes, string name, string url, object defaults) where TController : ControllerBase {
var methods = typeof(TController).GetMethods()
.Where(m => !m.ContainsGenericParameters)
.Where(m => !m.IsDefined(typeof(ChildActionOnlyAttribute), true))
.Where(m => !m.IsDefined(typeof(NonActionAttribute), true))
.Where(m => !m.GetParameters().Any(p => p.IsOut || p.ParameterType.IsByRef))
.Select(m => m.GetActionName());
routes.Add(name, new Route(url, new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(defaults) { { "controller", typeof(TController).Name.Replace("Controller", "") } },
Constraints = new RouteValueDictionary { { "action", new StringListConstraint(methods) } }
});
}
private static string GetActionName(this MethodInfo method) {
var attr = method.GetCustomAttribute<ActionNameAttribute>();
if (attr != null)
return attr.Name;
return method.Name;
}
class StringListConstraint : IRouteConstraint {
readonly HashSet<string> validValues;
public StringListConstraint(IEnumerable<string> values) { validValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase); }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
return validValues.Contains(values[parameterName]);
}
}
#region GetCustomAttributes
///<summary>Gets a custom attribute defined on a member.</summary>
///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
///<param name="provider">The object to get the attribute for.</param>
///<returns>The first attribute of the type defined on the member, or null if there aren't any</returns>
public static TAttribute GetCustomAttribute<TAttribute>(this ICustomAttributeProvider provider) where TAttribute : Attribute {
return provider.GetCustomAttribute<TAttribute>(false);
}
///<summary>Gets the first custom attribute defined on a member, or null if there aren't any.</summary>
///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
///<param name="provider">The object to get the attribute for.</param>
///<param name="inherit">Whether to look up the hierarchy chain for attributes.</param>
///<returns>The first attribute of the type defined on the member, or null if there aren't any</returns>
public static TAttribute GetCustomAttribute<TAttribute>(this ICustomAttributeProvider provider, bool inherit) where TAttribute : Attribute {
return provider.GetCustomAttributes<TAttribute>(inherit).FirstOrDefault();
}
///<summary>Gets the custom attributes defined on a member.</summary>
///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
///<param name="provider">The object to get the attribute for.</param>
public static TAttribute[] GetCustomAttributes<TAttribute>(this ICustomAttributeProvider provider) where TAttribute : Attribute {
return provider.GetCustomAttributes<TAttribute>(false);
}
///<summary>Gets the custom attributes defined on a member.</summary>
///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
///<param name="provider">The object to get the attribute for.</param>
///<param name="inherit">Whether to look up the hierarchy chain for attributes.</param>
public static TAttribute[] GetCustomAttributes<TAttribute>(this ICustomAttributeProvider provider, bool inherit) where TAttribute : Attribute {
if (provider == null) throw new ArgumentNullException("provider");
return (TAttribute[])provider.GetCustomAttributes(typeof(TAttribute), inherit);
}
#endregion
}

ASP.NET MVC Route with dash

I've got ASP.NET MVC routing question.
I prepared following routing table to map such url
mywebsite/mycontroller/myaction/14-longandprettyseoname
to parameters:
14 => id (integer)
longandprettyseoname -> seo_name (string)
routes.MapRoute(
"myname",
"mycontroller/myaction/{id}-{seo_name}",
new { controller = "mycontroller", action = "myaction", id = 0, seo_name = (string)null });
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" });
It works for URL above but it has problems for following type of urls
mywebsite/mycontroller/myaction/14-long-and-pretty-seo-name
Is that possible to make it working?
EDIT:
"mycontroller/myaction/{seo_name}-{id}"
seems to be working
The most obvious way to do this is to use constraints.
Since that your id is an integer, you can add a constraint which will look for an integer value:
new { id = #"\d+" }
and here is the whole route:
routes.MapRoute("myname","mycontroller/myaction/{id}-{seo_name}",
new { controller = "mycontroller", action = "myaction" },
new { id = #"\d+"});
My solution is define route as:
routes.MapRoute("myname","mycontroller/myaction/{id}",
new { controller = "mycontroller", action = "myaction"});
and parse id and seoname manualy using Regex in HTTP handler:
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(context));
var match = System.Text.RegularExpressions.Regex.Match((string)routeData.Values["id"], #"^(?<id>\d+)-(?<seoname>[\S\s]*)$");
if (!match.Success)
{
context.Response.StatusCode = 400;
context.Response.StatusDescription = "Bad Request";
return;
}
int id = Int32.Parse(match.Groups["id"].Value);
string seoname = match.Groups["seoname"].Value;
I don't think the route will be distinguishable as it will not be able to figure which "-" to split at to specify the {id} and the {seo-name}.
How about using underscores for your SEO name? Or you could just use the SEO name as the actual {id}. If the SEO name is something that is going to be unique, this is a very viable option you can use as a pseudo primary key to that entry in your db (assuming it's pulling something from a DB)
Also, utilize Phil Haack's route debugger to see what works and doesn't work.
Define a specific route such as:
routes.MapRoute(
"TandC", // Route controllerName
"CommonPath/{controller}/Terms-and-Conditions", // URL with parameters
new { controller = "Home", action = "Terms_and_Conditions" } // Parameter defaults
);
But this route has to be registered BEFORE your default route.
What you could do is create a custom controller factory. That way you can have custom code to decide which controller needs to be called when.
public class CustomControllerFactory : IControllerFactory
{
#region IControllerFactory Members
public IController CreateController(RequestContext requestContext, string controllerName)
{
if (string.IsNullOrEmpty(controllerName))
throw new ArgumentNullException("controllerName");
//string language = requestContext.HttpContext.Request.Headers["Accept-Language"];
//can be used to translate controller name and get correct controller even when url is in foreign language
//format controller name
controllerName = String.Format("MyNamespace.Controllers.{0}Controller",controllerName.Replace("-","_"));
IController controller = Activator.CreateInstance(Type.GetType(controllerName)) as IController;
controller.ActionInvoker = new CustomInvoker(); //only when using custominvoker for actionname rewriting
return controller;
}
public void ReleaseController(IController controller)
{
if (controller is IDisposable)
(controller as IDisposable).Dispose();
else
controller = null;
}
#endregion
}
To use this custom controllerfactory, you should add this in your global.asax
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));
}
Note that this only works for the controller, not for the actions... To hook up custom rewriting on actions before they get executed, use this code:
public class CustomInvoker : ControllerActionInvoker
{
#region IActionInvoker Members
public override bool InvokeAction(ControllerContext controllerContext, string actionName)
{
return base.InvokeAction(controllerContext, actionName.Replace("-", "_"));
}
#endregion
}
I got most of this code from this blog and adjusted it to my needs. In my case, I want dashes to separate words in my controller name but you can't create an action with a dash in the name.
Hope this helps!

Resources