I'm working on a Symfony2 project and am trying to figure out how to pass parameters from the route configuration to the controller. I know I can configure default values in the route configuration, and retrieve the values in the controller using the appropriate var name in the function declaration, but that isn't exactly what I want.
My use case is the following. I have a standard method in my controller that I want to access from 2 or 3 different routes. Depending on which route is being called, I want to "configure" the method differently. I can accomplish this in a few ways:
In my controller, check the route name using `$this->container->get("request")->get("_route"), but that is ugly, and then I am hardcoded to the route name. Moves configuration to the controller, which should just be logic - not configuration.
Create a base controller class, and subclass each method for my different routes. Each subclassed method would then have the necessary configuration within the method. Cleaner soln than #1, but still "heavy" in the sense of having multiple classes for a simple need and still pushes configuration data into the business logic.
Put configuration data into the route configuration. In the controller, access the configuration data as required. Ideal solution, but don't know how.
I can use the route default array to specify my arguments, but then must make sure to use a regex to ensure that the params are not overridden at the URL level (security risk). This is functional, but still kinda cludgy and not a pretty hack.
I presume that there must a better way to do this, but I can't seem to figure it out. Is there a way to access the routing object from the controller, and access the different configuration parameters?
You can pull the actual route from the router service. Something like:
$routeName = $this->container->get("request")->get("_route");
$router = $this->container->get("router");
$route = $router->getRouteCollection()->get($routeName);
Not sure if this would be such a great design though. Consider passing a $configName to your controller method, adding a parameter with the same name in a config file then using getParameter to access it. That would eliminate the route stuff from the equation.
Something like:
zayso_arbiter_import:
pattern: /import
defaults: { _controller: ZaysoArbiterBundle:Import:index, configName: 'someConfigName' }
public function importAction(Request $request, $configName)
Related
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.
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.
I got a problem to add custom logic to some Symfony classes.
SwitchUserListener
I want to add a check, that a user cannot switch to a another user, which have more rights/roles, than the initial user.
First attempt
Overwrite the parameter in the security_listeners.xml with the key:
security.authentication.switchuser_listener.class But where can I overwrite it?
In the security.yml it didn't work:
security:
...
authentication:
switchuser_listener:
class: Symfony\Component\Security\Http\Firewall\SwitchUserListener
Second attempt
Overwrite the service for the SwitchUserListner service id: security.authentication.switchuser_listener
I create the same service in my service.xml of my bundle, but my class was not used / called.
Another idea was to overwrite only the class, but that only works for bundles, but the SwitchUserListener was not in the SecurityBundle, it was in the symfony component directory and that seemed to me as a really bad idea to overwrite the SecurityBundle
Third attempt
Now I get the solution: First time I didn't realize that the dispatcher call listener for the SWTICH_USER event in the SwitchUserListener:
$switchEvent = new SwitchUserEvent($request, $token->getUser());
$this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
So I need only to create a service with the special tag for this event type:
<tag name="kernel.event_listener" event="security.switch_user" method="onSecuritySwitchUser" />
And do the check in the given method.
This seems to be a better solution thatn the other two. But there is still a problem. In my listener for the SwitchUserEvent I need to ignore my custom check if the user wants to exit the switched user.
So I need to check the requested path: ignore if path containts '?switch_user=_exit'
But the path (URL parameter) can be changed:
# app/config/security.yml
security:
firewalls:
main:
# ...
switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
But in my bundle I can't read this parameter, because it will not be passed to the service container. It will be passed to the constructor of the SwitchUserListner class and will be saved there as private attribute, never accessable (without Reflection) from outside. (that happens here: SecurityExtension.php line 591) So what to do? Define the parameter twice go against DRY. Use Reflection?
And the other point is that there aren' every time events that will be fired on which I write a subscriber class. So what would be another / best solution for it?
I ask this question because I will get some similar problem where I want to add or overwrite something of the symfony intern components.
TemplateGuesser
I wanted to modify the TemplateGuesser: For a specific bundle all Templates which has the annotation #Tempalte the tempate file should be located with the controller TestController#showAction at this path:
Resources/views/customDir/Test/show.html.twig
So the guesser should be put and locate everything into a additional folder customDir instead of using only views. When using the render function with a specific template, the guesser should ignore the annotation.
I created my own Guesser and overwrite the service id: sensio_framework_extra.view.guesser and in comparision to the SwitchUserListener this time my class is really called instead of the original guesser. Why it works here but not with the SwitchUserListener?
Is this a good solution at all? I also tried to add a second listener, which calls the TemplateGuesser, its the service sensio_framework_extra.view.listener with the class Sensio\Bundle\FrameworkExtraBundle\EventListener\TemplateListener But that didn't work.
Whenever you need to add custom logic or extend the framework behaviour, you can use and abuse the container configuration. That means you can overwrite pretty much every service Symfony defines by just creating a new class that extends that service – or not, really – and creating the service definition for it with the same key as the original service you wanted to extend or change behaviour.
For instance, Symfony has a base template guesser registered as a service with the sensio_framework_extra.view.guesser id. If you want to extend that or change behaviour, you only need to create your own class and register it with the same id of the original service – remember that the bundles loading order affects the service definitons with the same id, where the last one loaded is the one that will be created.
That should solve both of your problems.
I'm using ASP.NET with MVC4. In a simple controller I wish to use a URL such as
www.sitename.com/controller/action/param1/param2/param3
Can I do this?
The controller would be ControllerController and have a method
Action(param1, param2, param3)
etc
Yes, just add a route such as:
routes.MapRoute(name: "MyRoute", url: "{controller}/{action}/{p1}/{p2}/{p3}");
If you want it for a specific controller then you can use "hard" names instead of the {controller} and {action} placeholders.
Make sure you add this before the more specific routes though (i.e. before the "Default" route).
You may also find the RouteDebugger on NuGet helpful.
I am creating a custom route by subclassing RouteBase. I have a dependency in there that I'd like to wire up with IoC. The method GetRouteData just takes HttpContext, but I want to add in my unit of work as well....somehow.
I am using StructureMap, but info on how you would do this with any IoC framework would be helpful.
Well, here is our solution. Many little details may be omitted but overall idea is here. This answer may be a kind of offtop to original question but it describes the general solution to the problem.
I'll try to explain the part that is responsible for plain custom HTML-pages that are created by users at runtime and therefore can't have their own Controller/Action. So the routes should be either somehow built at runtime or be "catch-all" with custom IRouteConstraint.
First of all, lets state some facts and requirements.
We have some data and some metadata about our pages stored in DB;
We don't want to generate a (hypothetically) whole million of routes for all of existing pages beforehand (i.e. on Application startup) because something can change during application and we don't want to tackle with pushing the changes to global RouteCollection;
So we do it this way:
1. PageController
Yes, special controller that is responsible for all our content pages. And there is the only action that is Display(int id) (actually we have a special ViewModel as param but I used an int id for simplicity.
The page with all its data is resolved by ID inside that Display() method. The method itself returns either ViewResult (strongly typed after PageViewModel) or NotFoundResult in case when page is not found.
2. Custom IRouteConstraint
We have to somewhere define if the URL user actually requested refers to one of our custom pages. For this we have a special IsPageConstraint that implements IRouteConstraint interface. In the Match() method of our constraint we just call our PageRepository to check whether there is a page that match our requested URL. We have our PageRepository injected by StructureMap. If we find the page then we add that "id" parameter (with the value) to the RouteData dictionary and it is automatically bound to PageController.Display(int id) by DefaultModelBinder.
But we need a RouteData parameter to check. Where we get that? Here comes...
3. Route mapping with "catch-all" parameter
Important note: this route is defined in the very end of route mappings list because it is very general, not specific. We check all our explicitly defined routes first and then check for a Page (that is easily changeable if needed).
We simply map our route like this:
routes.MapRoute("ContentPages",
"{*pagePath}",
new { controller = "Page", action = "Display" }
new { pagePath = new DependencyRouteConstraint<IsPageConstraint>() });
Stop! What is that DependencyRouteConstraint thing appeared in mapping? Well, thats what does the trick.
4. DependencyRouteConstraint<TConstraint> class
This is just another generic implementation of IRouteConstraint which takes the "real" IRouteConstraint (IsPageConstraint) and resolves it (the given TConstraint) only when Match() method called. It uses dependency injection so our IsPageConstraint instance has all actual dependencies injected!
Our DependencyRouteConstraint then just calls the dependentConstraint.Match() providing all the parameters thus just delegating actual "matching" to the "real" IRouteConstraint.
Note: this class actually has the dependency on ServiceLocator.
Summary
That way we have:
Our Route clear and clean;
The only class that has a dependency on Service Locator is DependencyRouteConstraint;
Any custom IRouteConstraint uses dependency injection whenever needed;
???
PROFIT!
Hope this helps.
So, the problem is:
Route must be defined beforehand, during Application startup
Route's responsibility is to map the incoming URL pattern to the right Controller/Action to perform some task on request. And visa versa - to generate links using that mapping data. Period. Everything else is "Single Responsibility Principle" violation which actually led to your problem.
But UoW dependencies (like NHibernate ISession, or EF ObjectContext) must be resolved at runtime.
And that is why I don't see the children of RouteBase class as a good place for some DB work dependency. It makes everything closely coupled and non-scalable. It is actually impossible to perform Dependency Injection.
From now (I guess there is some kind of already working system) you actually have just one more or less viable option that is:
To use Service Locator pattern: resolve your UoW instance right inside the GetRouteData method (use CommonServiceLocator backed by StructureMap IContainer). That is simple but not really nice thing because this way you get the dependency on static Service Locator itself in your Route.
With CSL you have to just call inside GetRouteData:
var uow = ServiceLocator.Current.GetService<IUnitOfWork>();
or with just StructureMap (without CSL facade):
var uow = ObjectFactory.GetInstance<IUnitOfWork>();
and you're done. Quick and dirty. And the keyword is "dirty" actually :)
Sure, there is much more flexible solution but it needs a few architectural changes. If you provide more details on exactly what data you get in your routes I can try to explain how we solved our Pages routing problem (using DI and custom IRouteConstraint).