forward request to controller action - silverstripe

i'd like to forward a request to a controller to some specific action without redirecting to another url.
so, for example, the following url:
/home/peter
should internally become
/home/people/peter
i know i could catch the action name ('peter') in the 'init' function, then do a Controller::redirect, but i'd like to stay on the same url, just have the request internally forwarded to the 'people' action.
2 things to note:
'peter' is a dynamic string, so i can't just hardcode a route for this
any other action of the controller shall still work, so only a couple of strings should get forwared (eg. 'peter', 'ann' get forwarded to the 'people' action, whereas 'otherAction' is still callable)
i already found the 'handleAction' and 'handleRequest' methods in the Controller class, but just can't imagine how make use of them for my task.
SilverStripe version: 3.0.3

I suggest the use of static $url_handlers described here, that allows you to define specific routes to actions. Your use case could be achieved by using a catch-all url-handler.
But therefore we have to pay attention to the order of each route. The catch-all route has to be defined at the end. All other actions need a seperate url handler before:
public static $url_handlers = array(
'otherAction' => 'otherAction',
// ...
'people/$Name' => 'people',
'$Name' => 'people' // catch-all
);
This should fit to your explanation. The last route catches all dynamic routes which weren't handled before.
But as Ingo mentioned, both routes would deliver the same content and that could be bad for SEO. You could target the second route people/$Name to a different action which does a 301 redirect to the url with $Name only. I would recommend this if you already have public content and you want to replace the existing URLs with the short version.
I hope this solves your problem (temporarily).

First of all, I assume you have good reasons for making duplicate URLs (bad for SEO) :)
I've tried around a bit with Director::direct(), which would be the cleanest, but didn't get anywhere. Director::test() resets most request state, so doesn't qualify either.
Calling handleRequest() or handleAction() on a new controller instance is tricky, because the Director has built up a lot of state by that time already (for example pushed to the controller stack).
So, unfortunately the SilverStripe routing isn't that flexible, anything you do will go deep into system internals, and potentially break with the next release.

Related

How to get rid of route-name conflicts when ensuring api works both with and without the api-version?

I want to apply asp.net api-versioning to my web app (which didn't have versioning). However, the tricky issue is that I must ensure that APIs should work both with and without the api-version.
[ApiVersion("1.0")]
[Route("api/products/{productId}/[controller]")]
[Route("api/v{version:apiVersion}/products/{productId}/[controller]")]
[ValidateModel]
[Produces("application/json")]
public partial class ProductController : ControllerBase {
internal const string GetLatestRoute = "GET Product/GetLatestAsync";
[HttpGet(Name = GetLatestRoute)]
public async Task<IActionResult> GetLatestAsync() {
}
}
I have a controller with multiple actions, each of them is defined with a unique route name. When I add two routes (with and without versions) to the controller, there comes a route-name conflict error:
Attribute routes with the same name 'GET Products/GetLatestAsync' must have the same template:
Action: 'Service.Controllers.ProductController.GetLatestAsync (ProductFD)' - Template: 'api/products/{productId}/Product'
Action: 'Service.Controllers.ProductController.GetLatestAsync (ProductFD)' - Template: 'api/v{version:apiVersion}/products/{productId}/Product'
There are several answers on StackOverflow that say the issue can be solved by removing the route names defined for the action methods. However, in my scenario, the route names are used to create Url Links in several places in the project.
Is there an approach that I can get rid of the issue? I'm wondering whether I could append version to the route name variable or mapping the non-version api to the version/1.0 ...? On the other hand, there is a rare case that I update all the methods in a controller. So is it possible that I only define a route-prefix on the top-level of the controller and only apply the api-version on the method-level?
Route names and the route table are not API version aware. In order for this to work, you need to use double route registration like you have because you are versioning by URL segment (not recommended). If clients are properly following the links returned by the server, then always using the route generated with the explicit version in it will do. If the client doesn't honor that and just calls the APIs directly without the API version, the second template will handle that for you. If you are only generating links with the same controller, then I would suggest using CreatedAtAction instead because it will not rely on the route name. If memory serves me correct, you can specify the order of each [Route] for precedence. If unspecified, it will be the first attribute specified - which matters.
You'll also need to enable:
services.AddApiVersioning(options => options.AssumeDefaultVersionWhenUnspecified = true);
If you haven't already.
Last, but not least, beware the known, breaking change: Async suffix trimmed from controller action names. This has snared many people.

What's the right way to call an action from a different controller in ASP.Net MVC 6

I'm getting a
No route matches the supplied values
while trying to return a RedirectToAction("Action", "Controller"). The method signature says "actionName" and "ControllerName". I'm assuming actionName is the method name in the Controller, Am I correct? For ControllerName I'm using the Controller File Name without the Controller Sufix. Ex.:
return RedirectToAction("Index", "WebApp")
where Index is a method of WebAppController and the command is being issued from a method of AnotherController
Both the caller controller and the called one are on the same Controllers directory on the same application.
I'm cofused because in this ASP.net MVC application there is also Route attributes and Action attributes where you can put names on methods, different than the real method name. In my case I have no Route["Name"] nor [httpXXX("route", Name="dasdasdas")] configured for the methods involved in my attempt.
I have been reading MS docs and some examples but It appears I'm doing the thing right but for strange reasons it's not working. I even tried using Redirect("Controller/Action") and with it the problem vanishes but the new problem is this way of redirect doesn't support passing data parameters to the target route.
At this point I'm not working with Action links in Views, different from Form related ones.
I would really appreciate if at least anyone can give me a hint about where can I find info.
The right way to call an action from a different controller is the one I was using:
return RedirectToAction("AnActionMethodName", "AControllerWithoutControllerSufix"[, object values]);
My problem, after several hour spent was that I added two useMvc calls in the Startup.Configure(...)method:
app.UseMvc();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=MyApp}/{action=Index}/{id?}");
});
This was due to copy + paste of code. It was obviously confusing the MVC component in charge of attending the redirection part, as the right way I supose is tho use only one. (I'm newbie on .Net Platform and its frameworks)
Anyway I leave this as a reminder about the risks and consequences of copying and pasting code. At some point something weird can happen to your app and the cause can be a simple copy + paste error.

Symfony2 dynamically select controller based on query result

I'm trying to create an extendible cms. I have URLs in my database like "/menu-item/content" for each of these urls there is an action specified in format "AcmeDemoBundle:Default:index".
I would like to resolve this action and call the appropriate controller based on the matched URL.
My main goal is to call the controller stored in the db directly. Before i used a method when i put a route in the end of the routings which matched all urls associate a controller with it that found out the target controller from the db, than used Controller::forward() to get the expected results. The main problem with this approach is that i need to create an unnecessary subrequest for each page load. The other problem is passing post variables to this new subrequest.
So far i've tried to listen on kernel.request event and modify $request->attributes to contain the matching _controller value, but it seems like it has no effect.
Any advice in the subject is appreciated.
I think you mixed up the forward and redirect calls. Redirect performs 301 or 302 redirects (permanent and temporary respectfully), which creates the subrequest, as you call it. Forward performs internal call (same request) to the new action. As for your second problem, as forward is an internal call within the same request, everything global stays the same (including POST variables). In my opinion, using forward might be a good solution for you (if you don't have any other low-level requirements or if you want to be even more flexible than the usual MVC operation allows).
Hope this helps.

Custom catch-all parameter in routing

I recently want to have a special routing rule : {*whatever}/details/{Id}/{itemName}
I know an exception will be thrown once I run the application. In my application, for example my url pattern is www.domain.com/root/parent/child/.../child/details/30/itemname
but the current routing doesnot support this. How can custom the routing handler to make it work?
A class has been written that supports this
I've written such a class that can handle catch-all segment anywhere in the URL. There's quite some code to it, but it works as expected and I've used it on a real life project.
Check it out yourself and see if it fulfils your needs.
The problem is... how will it know when to stop?
the {*whatever} segment will match:
/foo/
/foo/bar
/foo/bar/details/4/moreFoo
/foo/bar/andmore/details/4/moreFoo
Because the catch-all parameter includes anything, it will never stop.
The only way to implement this would be to create a different route for each place you use details...
eg:
games/details/{id}/{itemName}
widgets/details/{id}/{itemName}
books/details/{id}/{itemName}
Of course, that is already provided in the default {controller}/{action}/{id} route
I think you may want to look at extending the System.Web.Routing.RouteBase class and override the GetRouteData() method. With it you can look at the requested url and decide if matches your pattern and if so construct and return a new instance of RouteData that points to the controller and action that you want to handle the request. Otherwise if you don't match the requested url you return null.
See the following for examples:
Pro ASP.NET MVC Framework
By Steve Sanderson
Custom RouteBase

Translate a url to a route

I feel like I'm re-inventing the wheel here, but I need to find a way of taking a URL from a catchall,and redirecting that to another route.
The reason for this is because I need to do some things like adding session cookies for certain urls, and then pass them on to their relevant action.
What's the best way of implementing this?
Thanks in advance for any help!
You can try changing the master page at runtime. It's not the most beautiful solution though.
Theming Support
If I understand question...
Looking at your previous question (redirect-to-controller-but-with-a-different-master-using-a-catchall-wildcard) you want to take the wildcard from:
routes.MapRoute(
"Partner",
"partners/{partner}/{*wildcard}",
new { controller = "Partners", action = "PartnerRedirect" }
);
And in the Partners/PartnerRedirect action, send it to the right controller/action specified in the wildcard?
I have no idea the best way to do this, but looking at ways to unit test url routing brought up this:
http://weblogs.asp.net/stephenwalther/archive/2008/07/02/asp-net-mvc-tip-13-unit-test-your-custom-routes.aspx
I'm not sure if it is an exact fit (ie. might not have to mock stuff), but it looks like all the data is returned from GetRouteData to pass to RedirectToAction method.
Oh, so you don't want a redirect? You want the URL to stay:
mysite.com/partners/pepsi/products/cola.htm
but serve up:
mysite.com/products/cola.htm
I don't have MVC at home, but couldn't you instead define your route differently?
Instead of:
routes.MapRoute(
"Partner",
"partners/{partner}/{*wildcard}",
new { controller = "Partners", action = "PartnerRedirect" }
);
do:
routes.MapRoute(
"Partner",
"partners/{partner}/{controller}/{action}/{other stuff you need}",
new { /* whatever defaults you want */ }
);
The action now has the partner variable to do whatever with, and you could add route constraints to partner so only valid ones match.
(I think) you could then use an action filter on your actions/controllers to do appropriate action based on the partner paramter (it should be in httpcontext of the filter, again I think!) so that you don't have to repeat code in every action if you want to do some basic checks/actions on partner.
Here is a decent write up on a lot of ways to skin that cat:
http://weblogs.asp.net/stephenwalther/archive/2008/08/12/asp-net-mvc-tip-31-passing-data-to-master-pages-and-user-controls.aspx
No, it is effectively a redirect, but the redirect is not visible to the user (in much the same way as a Server.Transfer() is done in Web Forms.
The link you pointed at only uses one master page, whereas I have many master pages (close to 200).
I have thought of having separate route handlers for these partners, but the master page choice came into the equation.
I think I'm going to have to create a ViewExtension to overcome these problems...

Resources