Cascading MVC controllers with CatchAll Routes - c#

I have an MVC app which has its routes defined with the final route being a catch all route to a "PageController" for a database driven collection of pages. What I want to achieve is to be able to plugin to the app a second controller to the catch all route which the first controller passes on to if it does not find the url recieved in the database.
Effectively I want to queue up controllers with catch all actions:
public ActionResult PageCatchall(string url)
{
var page = repository.Get<Page>(string url);
if (page != null)
{
// Handle the request
return View(page)
}
// Otherwise pass to a new controller
????
}
Anyone have any good ideas as to how to solve this? I have tried RedirectToAction but that requires that the next controller has a different route to the action. I have tried ActionInvoker but this failed to work the way I did it.

While I would avoid complex routing like this one, I guess best thing to do is to use custom route constraint that uses cached list of pages to match correct route (have done that with success).
I'll leave exact implementation for You as an exercise. ;o)

Related

ASP.NET Core controller usefull errormessage return

My application is an ASP.NET Core 1.0 Web API.
[HttpGet("{someData:MinLength(5):MaxLength(5)}")]
[Produces("application/json")]
public async Task<IActionResult> GetSomeData(string someData)
{
return this.Ok(JsonConvert.SerializeObject("Data is: " + someData));
}
So when i pass a parameter with the length 6, the controller returns the Response Code 404 and the Response body no content because the parameter doesnt have the length 5.
This information is pretty useless for me, is there a way to return a more usefull error message?
I know i could just hardcode the error message in every controller like that:
[HttpGet("{someData}")]
[Produces("application/json")]
public async Task<IActionResult> GetSomeData(string someData)
{
if (someData.Length != 5)
{
return this.StatusCode(404, JsonConvert.SerializeObject("The data has to be 5 digits long."));
}
return this.Ok(JsonConvert.SerializeObject("Data is: " + someData));
}
The problem about this is, I have many controllers in my application and I dont want to validate the parameters everytime.
Is there a way the controller returns a more usefull Responsebody by itself? Or do I really have to add a validation method in each controller and forget about the [HttpGet] parameters and its error return?
Thank you very much
Route constraints are used to restrict which requested paths make it to a given action / route destination. They're not meant to perform model validation - that's done later and through a separate mechanism. If the request doesn't conform to your route constraints, it simply won't match that route. If it doesn't match any route, you'll get a 404.
As you noted in your own answer, this is covered in the routing docs: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#route-template-reference
Alright I found an answer:
It's not possible to get better error messages.
Warning
Avoid using constraints for input validation, because doing so means
that invalid input will result in a 404 (Not Found) instead of a 400
with an appropriate error message. Route constraints should be used to
disambiguate between similar routes, not to validate the inputs for a
particular route.
found here

Update URL from action filter ASP.NET MVC

Hellp all,
Generally speaking I want to "update" URL from action filter in OnActionExecuting method. I registered action filter to be global and that method will be executed before every action is executed.
In OnActionExecuting method I am processing some things and I want to update URL with accountName after that.
I have configured routes which can accept four segments route patterns - something like these: {accountName}/{controller}/{action}, {controller}/{action}...
Basically if I receive request which doesn't contain {accountName} segment or if is that segment empty I would like to update URL with new accountName and be able to see updated URL when request is ended.
I am trying to do that like in following:
filterContext.RouteData.Values["accountName"] = accountName;
filterContext.Result = new RedirectToRouteResult("AccountRoute", filterContext.RouteData.Values);
I added new route data value to filterContext and after I am trying to redirect to specific route with route data values.
That approach doesn't properly because I am encountered with "TO MANY REQUESTS" and it broke my app.
Any idea how I can update URL and avoid to many requests?
Thanks

MVC Routes... How do I set up my controller to deal with /configuration/building/add

I have an architecture where I have numerous objects I can configure. Examples of URLs I want are as follows:
/configuration/building/add
/configuration/building/edit/1
/configuration/group/add
/configuration/group/edit/1
I have a Configuration controller but how do I intercept or deal with building/add and building/edit/1 etc... If it were AddBuilding I could simply add an AddBuilding() function, and similarily how do I get it to work for configuration/building/edit/
Here's what you can do for the first one - open up the Global.asax.cs file of your site and put this in RegisterRoutes before the standard MVC catch-all route (the one that uses the route "{controller}/{action}/{id}"):
routes.MapRoute("AddBuilding", "configuration/building/add",
new { controller = "Configuration", action = "AddBuilding" });
The others will be the same, but different names (first parameter) and action, whislt the edit routes but would include an {id} route placeholder and route parameter (but not optional - unlike the MVC default route):
routes.MapRoute("EditBuilding", "configuration/building/edit/{id}",
new { controller = "Configuration", action = "EditBuilding" });
By leaving the id off the route defaults we make it required. I'm assuming this, because I'm guessing the Url /Building/Edit doesn't logically map to anything.
As a side node - including verbs in your urls isn't really in keeping with REST methodology, however you're not the first to do it by a long way (I include myself in that too). That said - trying to keep to it usually makes your life a lot easier, as you'll find your controllers will be cleaner, as will your route table, and your site's URL space will be a lot smaller and more obviously hierarchical. This last point is - handy for zooming around the site at dev time, but more importantly it's crucial for SEO.
So (I've commented this code heavily, hopefully enough to provide some nuggets of knowledge!):
public class ConfigurationController{
////HTTP GET /Buildings
/// DISPLAYS BUILDINGS
public ActionResult Buildings(){
//get model and return view that shows all buildings with perhaps a
//partial in that for creating a new one (or you can use another action)
//The HTML form on that view will POST to the URL handled by the method below.
}
////HTTP POST /Buildings
/// CREATES A NEW BUILDING
//use ActionName here to make this and the method above accessible through
//the same URL
[ActionName("Buildings")]
[HttpPost]
public ActionResult CreateBuilding(BuildingModel model){
//validate the model, create the object and return the same
//view as the Buildings() method above (after re-loading all the
//buildings. Or, you can issue a redirect, effectively transferring
//control back to the method above.
}
////HTTP GET /Configuration/Building/id
///DISPLAYS A BUILDING
public ActionResult Building(int id){
//get building and return view, which also contains Edit functionality
}
////HTTP POST /Configuration/Building/id
///EDITS A BUILDING
[HttpPost]
public ActionResult Building(int id, BuildingModel model){
//very similar to the CreateBuilding method - and again you might
//either simply return a building view at the end, or redirect
//to the method above.
//Note that we don't need [ActionName] here because this and the
//get method can have the same method names, because they are overloads
//i.e. since they have different method signatures we can call them the same
//thing in code.
}
}
I've left off the group stuff to keep it short, and hopefully you'll be able to see how to do it from there.
With this in place, we only need at most two routes in Global.asax.cs - although I think the order will be important:
//handles both GET and POST to this URL, i.e. our view & edit operations
routes.MapRoute("IndividualBuilding", "/configuration/buildings/{id}",
new { controller = "Configuration", action = "Building" });
routes.MapRoute("Buildings", "/configuration/buildings",
new { controller = "Configuration", action = "Buildings" });
Now we are using the HTTP verbs to signify what we intend to do with a particular request, and our URLs have become more 'logical'.
Another refactor
If you want to be 'clever' you can lump both buildings and groups under two routes
//handles both GET and POST to this URL, i.e. our view & edit operations
routes.MapRoute("Individual", "/configuration/{controller}/{id}",
new { controller = "Configuration", action = "List" });
//again, handles GET and POST
routes.MapRoute("Collection", "/configuration/{controller}",
new { controller = "Configuration", action = "Single" });
Now you do both buildings and groups controllers as I showed above, but replace Buildings (remembering the ActionName attribute on the second method) with List and Building with Single.
One final thing to consider is that because of the default MVC route:
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Default", action="Home", id = UrlParameter.Optional });
Both of your two controllers can still be routed via /Buildings/Single/1 or /Groups for example. This is a minor issue (dupe content isn't great SEO) but it can be something that people can use to sniff your site.
If you absolutely want to prevent this other url format; you can take out the default route, meaning you'd have to explicitly route other stuff that might already work (not a great issue).
Or you can use a little trick that will make it far harder: use explicit [ActionName] attributes with characters in the route name that won't be allowed through IIS - e.g. ":Single" or ":List", and then adjust our two routes from a couple of code blocks back accordingly.
So firstly you can create a controller action called AddBuilding() as you have hinted.
Then in your Global.asax file in the RegisterRoutes method you can add a route like so:
routes.MapRoute(
"AddBuilding", // Route name
"configuration/building/add", // URL with parameters
new { controller = "Configuration", action = "AddBuilding" }
);
You should not though that you will likely still be able to access the page using "/configuration/addbuilding" because of your default route mapping.
You edit one will be similar expect you will want to map the ID value for this:
routes.MapRoute(
"EditBuilding", // Route name
"configuration/building/edit/{id}", // URL with parameters
new { controller = "Configuration", action = "AddBuilding", id = UrlParameter.Optional }
);
I think you will need to add this code with the default MapRoute setup to ensure that one does not take priority
Another approach would be to create a Configuration MVC area, and then have a building and group controller in that Area.
You can do that by Attribute Routing in ASP.NET MVC 5. Something like following;
// eg: /reviews
[Route(“reviews”)]
public ActionResult Index() { … }
// eg: /reviews/5
[Route(“reviews/{reviewId}”)]
public ActionResult Show(int reviewId) { … }
// eg: /reviews/5/edit
[Route(“reviews/{reviewId}/edit”)]
public ActionResult Edit(int reviewId) { … }
You can add multiple route for the same controller as well. For details please check here

ASP.NET MVC: how to make all unspecified URLs direct to one controller

I've got an ASP.NET MVC app that's working nicely with a handful of controllers, e.g. "Home", "Services" and "Go". The "Go" controller is where all the content is.
Now the marketing folks have come and said they don't want to see the word "go" in the URL. In other words, instead of:
http://mydomain.com/go/geography/africa
they want to have:
http://mydomain.com/geography/africa
I cannot create a controller for every path that they might want... so is there any way of writing my routing in Global.asax.cs in such a way that requests to "services" and "home" will be treated the normal way, but anything else will implicitly be routed to the "go" controller?
Are you on IIS7? It might be easiest to just implement URL rewriting on the server for this, rather than hacking about with your routes in Global.asax.cs.
EDIT: I've only ever done URL rewriting in Apache. For what it's worth that would be done using something like this:
RewriteEngine On
RewriteRule ^go/(.+)$ /$1 [R=301,L]
Have a look at http://learn.iis.net/page.aspx/460/using-the-url-rewrite-module/ and http://learn.iis.net/page.aspx/461/creating-rewrite-rules-for-the-url-rewrite-module/. Hopefully they'll give you enough info to be able to get this working in IIS 7
Hey, I worked it out myself, without URL rewriting!
Inside RegisterRoutes() in Global.asax.cs:
routes.MapRoute("Services", "services/{action}/{*qualifier}",
new { controller = "Services", action = "Index", qualifier = UrlParameter.Optional });
// and other controllers that I want to work the normal way
routes.MapRoute("Default", "{*path}",
new { controller = "Go", action = "Index", path = UrlParameter.Optional });
And in my GoController class
public ActionResult Index(string path) { ... }
Works perfectly!
You could try adding a mapping that does "geography/{country}" and have it specify the controller manually and add the country as a parameter. I've seen it done to prevent things like "Dashboard/Dashboard" etc.
An example can be seen at Kazi Manzur Rashid's Blog - ASP.NET MVC Best Practices (Part 2) #15 for what I am describing.
Have you seen this: http://www.iridescence.no/post/Defining-Routes-using-Regular-Expressions-in-ASPNET-MVC.aspx ?
you could try mapping a route of "{action}/{id}" with a default set for the controller. But that'll also match anything of the form "{controller}/{action}" too - unless you can do some clever constraining.

Change ASP.NET MVC Routes dynamically

usually, when I look at a ASP.Net MVC application, the Route table gets configured at startup and is not touched ever after.
I have a couple of questions on that but they are closely related to each other:
Is it possible to change the route table at runtime?
How would/should I avoid threading issues?
Is there maybe a better way to provide a dynamic URL? I know that IDs etc. can appear in the URL but can't see how this could be applicable in what I want to achieve.
How can I avoid that, even though I have the default controller/action route defined, that default route doesn't work for a specific combination, e.g. the "Post" action on the "Comments" controller is not available through the default route?
Background: Comment Spammers usually grab the posting URL from the website and then don't bother to go through the website anymore to do their automated spamming. If I regularly modify my post URL to some random one, spammers would have to go back to the site and find the correct post URL to try spamming. If that URL changes constantly I'd think that that could make the spammers' work more tedious, which should usually mean that they give up on the affected URL.
I would consider to implement my own IRouteHandler and put some custom logic in my custom ControllerActionInvoker. How it would work ? The route table wouldn't dynamically change but you could check in your custom ControllerActionInvoker for a random parameter in the route path and invoke or not the corresponding action.
My route :
routes.Add
(
new Route
(
"blog/comment/{*data}",
new RouteValueDictionary(new {controller = "blog", action = "comment", data = ""}),
new MyRouteHandler()
)
);
My I route handler :
class MyRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MyHttpHandler(requestContext);
}
}`
My handler :
class MyHttpHandler : MvcHandler
{
public MyHttpHandler(RequestContext requestContext) : base(requestContext)
{
}
protected override void ProcessRequest(HttpContextBase httpContext)
{
IController controller = new BlogController();
(controller as Controller).ActionInvoker = new MyActionInvoker();
controller.Execute(RequestContext);
} }`
and my action ivoker where the custom logic for handling an action or not should be coded :
class MyActionInvoker : ControllerActionInvoker
{
protected override ActionResult InvokeActionMethod(MethodInfo methodInfo, IDictionary<string, object> parameters)
{
var data = ControllerContext.RouteData.GetRequiredString("data");
// put my custom logic to check whetever I'll handle the action or not. The data could be a parameter in the database for that purpose.
return base.InvokeActionMethod(methodInfo, parameters);
}
}
I don't know it it's the best solution but for now it's the one that comes to my mind.
Considering the actual problem background, the usual approach is to include a dynamically created transaction number. It should be stored in a hidden form field as well as in the server side session dictionary and only be valid for exactly one request.
I think today a lot of frameworks provide such a security mechanism; whereas this attack type is known as Cross-Site-Request-Forgery (csrf).

Categories

Resources