Fallback Route in ASP MVC if action doesn't exist - asp.net

The default route in MVC {controller}/{action}/{id} is for the most part quite helpful as is being able to set a default if the incoming url doesn't include a parameter but is there also a way to specify a default action for when an action doesn't exist on a controller?
What I want to achieve is being able to have controllers with several specific actions and then its own catchall which uses the url to grab content from a basic CMS.
For example a products controller would be something like:
public class ProductsController: Controller{
public ActionResult ProductInfo(int id){...}
public ActionResult AddProduct(){...}
public ActionResult ContentFromCms(string url){...}
}
Where the default route would handle /Products/ProductInfo/54 etc but a request url of /Products/Suppliers/Acme would return ContentFromCms("Suppliers/Acme"); (sending the url as a parameter would be nicer but not needed and a parameterless method where I get it from Request would be fine).
Currently I can think of two possible ways to achieve this, either:
Create a new constraint which reflects over a controller to see if it does have an action of a given name and use this in the {controller}/{action}/{id} route thus allowing me to have a more general catchall like {controller}/{*url}.
Override HandleUnknownAction on the controller.
The first approach seems like it would be quite a roundabout way of checking this while for the second I don't know the internals of MVC and Routing well enough to know how to proceed.
Update
There's not been any replies but I thought I'd leave my solution incase anyone finds this in future or for people to suggest improvements/better ways
For the controllers I that wanted to have their own catchall I gave them an interface
interface IHasDefaultController
{
public string DefaultRouteName { get; }
System.Web.Mvc.ActionResult DefaultAction();
}
I then derived from the ControllerActionInvoker and overrode FindAction. This calls the base FindAction then, if the base returns null and the controller impliments the interface I call FindAction again with the default actionname.
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
ActionDescriptor foundAction = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (foundAction == null && controllerDescriptor.ControllerType.GetInterface("Kingsweb.Controllers.IWikiController") != null)
{
foundAction = base.FindAction(controllerContext, controllerDescriptor, "WikiPage");
}
return foundAction;
}
As I also want parameters from the routing I also replace the RouteData at the start of the default Actionresult on the controller
ControllerContext.RouteData = Url.RouteCollection[DefaultRouteName].GetRouteData(HttpContext);

You approach is quite fine. As a side-note:
replace
controllerDescriptor.ControllerType.GetInterface("Kingsweb.Controllers.IWikiController") != null
with
typeof(Kingsweb.Controllers.IWikiController).IsAssignableFrom(controllerDescriptor.ControllerType)
this is more strongly-typed way then passing in the name of the interface via string: what if you change the namespace tomorrow?..

Related

Override MVC routes where applicable without inheriting controller

Say I have a controller FooController which has a bunch of action methods. Some of these methods would I like to override, but none of the methods are marked as virtual and I do not want to change the code of FooController.
So I implement a CustomFooController (not inheriting from FooController) and write new versions of the methods that I want.
Now I want to route first to CustomFooController and if the action is not available there I want to default to FooController. I have set up this route config to override the route:
routes.MapRoute(
"customFoo",
"foo/{action}",
new { controller = "CustomFoo", action = UrlParameter.Optional }
);
Here is some example definitions:
public class FooController : Controller
{
public ActionResult Bar { ... }
public ActionResult Baz { ... }
}
public class CustomFooController : Controller
{
public ActionResult Bar { ... }
}
So when accessing /Foo/Bar we should hit CustomFooController.Bar() and when accessing /Foo/Baz we should hit FooController.Baz() since Baz() is not implemented in CustomFooController.
But I get "The resource cannot be found", I understand why, but can I somehow work around it without modifying FooController?
You might want to have a look at HandleUnknownAction() (see msdn reference page).
It is invoked when a request matches a controller, but no method with the specified action name is found in that controller.
So you could override that method in your CustomFooController to redirect to the appropriate action in FooController (or even other controllers based on your custom inspection of request related data).
Redirection can be done with RedirectToAction() (see msdn reference page)
I think I found a neat solution myself. Since I don't want to modify FooController and a bunch of other controllers from another application I'm extending (and relying on updates from) I decided to use extension method MapMvcAttributeRoutes() from System.Web.Mvc.RouteCollectionAttributeRoutingExtensions in my startup RouteConfig in order to first match with route attributes on e.g. my CustomFooController which takes precedence over the conventional routing.
Just to be on the safe side: HandleUnknownAction() is implemented only in CustomFooController. You don't have to modify FooController (or other controllers).

How can I add multiple Get actions with different input params when working RESTFUL?

I'm trying to figure out whats the best way to have multiple Get actions in a REST controller.
I would like to do something like this:
Get By Id:
public ResponseType Get(Guid id)
{
// implementation
}
Get By Enum Type:
public ResponseType Get(EnumType type)
{
// implementation
}
Get By Other Enum Type:
public ResponseType Get(OtherEnumType otherType)
{
// implementation
}
etc..
Now when I do something like that, I get the next error message:
Multiple actions were found that match the request
I understand why I get the message and I was thinking how is the best way to do something like that (I want to stick with REST).
I know I can add a route like this:
routeTemplate: "api/{controller}/{action}/{id}"
But then I would need to change the action names and the urls - And this seems like a workaround when we are talking about rest.
Another thing I thought was to create multiple controllers with one Get - But that seems even wronger.
The third workaround was to handle one Get action with an input param that will have the state:
public ResponseType Get(ReqeustObj obj)
{
switch(obj.RequestType)
{
case RequestType.GetById:
// etc...
}
}
Anyway, I would like to know whats the best way to do something like that in REST (WebApi).
As you now, when Web API needs to choose an action, if you don't specify the action name in the route, it looks for actions whose name starts with the method name, GET in this case. So in your case, it will find multiple methods.
But it also try to match the parameters. So, if you include the parameters as part of the url (route parameters) or the query string, the action selector will be able to choose one of the available methods.
If you don't specify a parameter or specify the id in the url (or even in the query string) it should invoke the first overload. If you add the parameter name of the second action in the query string like this: ?type=VALUE it should choose the corresponding overload, and so on.
The question is that the parameter names must be different, or it will not be able to choose one or the other among all the overloads.
For example, if you use the urls in the comments in your browser, you'll see how the right method is chosen:
public class TestController : ApiController
{
// GET api/Test
public string Get()
{
return "without params";
}
// GET api/Test/5
public string Get(int id)
{
return "id";
}
// GET api/Test?key=5
public string Get(string key)
{
return "Key";
}
// GET api/Test?id2=5
public string Get2(int id2)
{
return "id2";
}
}
NOTE: you can also use route constraints to invoke differet methods without using query string parameters, but defining different route parameter names with different constraints. For example you could add a constraint for id accepting only numbers "\d+" and then a second route which accepts "key" for all other cases. In this way you can avoid using the query string

currentUser as a modelbinded parameter

Right now I'm thinking about a pattern to have the 'current user' as a modelbinded parameter in my actions.
My actions would look something like this:
public JsonResult ListStuff(User currentUser, string paramter1, int parameter2)
{
}
And I have a very simple ModelBinder that looks like this:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if ( bindingContext.ModelName == "currentUser" )
return Globals.HttpContextItems.User;
return null;
}
I really like that the action is less dependent on another Controller Property. It makes it more clear what the 'input parameters' of the functions are, it's more reusable, and will make it a bit more easily testable in the future.
I'm a bit affraid of security issues though. I probably have to make very sure (i.e. in the DefaultModelBinder) that the currentUser will never be automatically bound by other ModelBinders.
Can anyone shine a light if this might be a good pattern, and if there is stuff that I'm not thinking about at the moment, but that will give problems in the future.
If you are concerned that other ModelBinders will set that parameter, why not create an ActionFilterAttribute so that you'll explicitly have to decorate your action method:
public class GetCurrentUserAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.ActionParameters["currentUser"] = filterContext.HttpContext.User;
}
}
Then to use it:
[GetCurrentUser]
public ActionResult Index(User currentUser)
{
}
Definitely not as clean as the default model binder, but a lot more explicit.
Interesting idea. And I like how it keeps with IoC by injection the requirements into the method (people often forget that you can inject dependencies into methods, not just constructors).
Your security concerns would be abstracted to the location of where you would populate this context item. My recommendation would be to put that very same logic, to populate the context item, at this same location. Having that kind of logic in two places (a model binder, and then say a controller method) would have you chasing two places to track down a bug. I say this model binder should be responsible for loading that context item, if it is null.
Lastly, this would abstract away even more complex "user verification" services in the future if you ever wanted to do that. For example, I had a project requirement at one time to ensure every piece of data and ID being passed into the domain belonged to that user with a security check. What this method you describe opens you up to is have a custom User object that inherits from you base User object, called something like UserContext : User that can have a number of additional UI related functions and properties on it - including addition security boolean verifications.
I may try this in my next project.

Is it possible to pass a method parameter to an attribute for that method?

I want to make a custom AuthorizeAttribute class as described here: Override Authorize Attribute in ASP.NET MVC.
The trick is, whether or not the user is authorized depends not just on what method he is executing, but the parameter as well:
[MyCustomAuthorize(id)]
[HttpGet]
public ActionResult File(Guid id)
{
}
Basically, the user will be authorized to see some Files, but not others, so I want to pass the id to my custom authorize attribute. But I can't seem to do this because the name 'id' isn't in scope.
I know I could do the logic to make sure the user has access to that File at the beginning of the method, but I have several methods that would all need this done, and it would be much cleaner if I could do it as part of the [Authorize] attribute.
No, that's not legal C#.
You can access the RouteValues inside the AuthorizeAttribute subtype, however.
Inside, e.g., OnAuthorization, you can do:
object id = filterContext.RouteData.Values["id"];
Be careful, though. You really need to know what you're doing here. The interaction between authentication, authorization, and caching is complex.
Maybe the last answer in 2011 was correct for that version HOWEVER it is possible. Create a public int (or whatever it is you need) and use that. Example:
public class RestrictToTemplateManagers : AuthorizeAttribute
{
public string GUID { get; set; }
}
[RestrictToTemplateManagers(GUID="ABC")]
public class ImportTemplatesController : Controller
{
}

ASP.NET MVC Route All Requests

We need to present the same site, with a different theme (and different data) depending on the "route" into the site.
www.example.com/Trade
www.example.com/Public
NB: These are not dynamic themes which the user can pick. Certain customers will be coming via a third party link which will always point them to one of the urls.
The value trade/public also needs to be used in queries from the UI to the database (to pull back varying data depending on the route into the site).
So what are my options?
Create custom view engine which uses the querystring (mvc route param) value to load the relevant master page.
In each controller action, grab the parameter (trade/public etc) and pass it off to the database queries.
public ActionResult List(string siteType){
products.ListFor(siteType);
}
The problem here is having to alter every controller action to pass the querystring value through.
This also presents an issue with any route defined in global.asax having to accept the parameter.
I'm wondering if there's another way, perhaps some combination of a custom controller base and host name e.g. trade.example.com, public.example.com?
First define routing:
routes.MapRoute(
"Default", // Route name
"{siteType}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
and then use it in controller:
RouteData.Values["siteType"]
How about this:
Create a base controller that all your controllers inherit from
Add a property: public string SiteType { get; protected set; }
Add an OnActionExecuting method to set it before any action is called, e.g.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//use filterContext to set SiteType somehow, e.g. you can look at the URL or the route data
}

Resources