ASP.NET MVC Areas or Defined Routes? - asp.net-mvc

I'd like to achieve the following (with ASP.NET MVC 3):
A controller called "apps" with the following actions:
/apps/my
/apps/agency
/apps/new
Within the last action I really want some sub-actions, e.g.:
/apps/new/product
/apps/new/tariff
I could write the New() action to take some kind of parameter to say which view I should render (i.e. product or tariff) but that feels a bit dirty.
What I really want is separate action methods for product and tariff.
What's the best way to go about this?
I think I could use Areas but this seems overkill for what I want - is the solution just to write a custom route?
Many thanks!
Sam

You could use Areas but for this small amount I agree that it is probably overkill. I'd say making the New action take in a parameter is fine for what you need. It may get more complicated if you want to pass more information in but its still do able. If you want to keep the code clean(er) you can have the action do all the complicated bits in separate private methods.
However, doing it with a custom route way (and with a separate controller as well) ...
Global.asax.cs
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("newControllerRoute",
"apps/new/{action}",
new {controller = "NewApps"});
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
The add a controller (NewAppsController).
public class NewAppsController : Controller
{
public ActionResult Product()
{
/* used as example */
return Content("NewApps controller - Product");
}
public ActionResult Tariff()
{
/* used as example */
return Content("NewApps controller - Tariff");
}
}
Hope this helps.

If you have two distinct things that you want to be able to add then that is two distinct actions on your controller. It sounds like you are just wanting to control your URL schema so I think some sort custom routing is the answer.
Why not have separate controllers for tariffs and products? That will lead to a natural URL schema.

Related

Nested resources in ASP.net MVC 4 WebApi

Is there a better way in the new ASP.net MVC 4 WebApi to handle nested resources than setting up a special route for each one? (similar to here: ASP.Net MVC support for Nested Resources? - this was posted in 2009).
For example I want to handle:
/customers/1/products/10/
I have seen some examples of ApiController actions named other than Get(), Post() etc, for example here I see an example of an action called GetOrder(). I can't find any documentation on this though. Is this a way to achieve this?
Sorry, I have updated this one multiple times as I am myself finding a solution.
Seems there is many ways to tackle this one, but the most efficient I have found so far is:
Add this under default route:
routes.MapHttpRoute(
name: "OneLevelNested",
routeTemplate: "api/{controller}/{customerId}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
This route will then match any controller action and the matching segment name in the URL. For example:
/api/customers/1/orders will match:
public IEnumerable<Order> Orders(int customerId)
/api/customers/1/orders/123 will match:
public Order Orders(int customerId, int id)
/api/customers/1/products will match:
public IEnumerable<Product> Products(int customerId)
/api/customers/1/products/123 will match:
public Product Products(int customerId, int id)
The method name must match the {action} segment specified in the route.
Important Note:
From comments
Since the RC you'll need to tell each action which kind of verbs that are acceptable, ie [HttpGet], etc.
EDIT: Although this answer still applies for Web API 1, for Web API 2 I strongly advise using Daniel Halan's answer as it is the state of the art for mapping subresources (among other niceties).
Some people don't like to use {action} in Web API because they believe that in doing so they will be breaking the REST "ideology"... I contend that. {action} is merely a construct that helps in routing. It is internal to your implementation and has nothing to do with the HTTP verb used to access a resource.
If you put HTTP verb constraints on the actions and name them accordingly you're not breaking any RESTful guidelines and will end up with simpler, more concise controllers instead of tons of individual controllers for each sub-resource. Remember: the action is just a routing mechanism, and it is internal to your implementation. If you struggle against the framework, then something is amiss either with the framework or your implementation. Just map the route with an HTTPMETHOD constraint and you're good to go:
routes.MapHttpRoute(
name: "OneLevelNested",
routeTemplate: "api/customers/{customerId}/orders/{orderId}",
constraints: new { httpMethod = new HttpMethodConstraint(new string[] { "GET" }) },
defaults: new { controller = "Customers", action = "GetOrders", orderId = RouteParameter.Optional, }
);
You can handle these in the CustomersController like this:
public class CustomersController
{
// ...
public IEnumerable<Order> GetOrders(long customerId)
{
// returns all orders for customerId!
}
public Order GetOrders(long customerId, long orderId)
{
// return the single order identified by orderId for the customerId supplied
}
// ...
}
You can also route a Create action on the same "resource" (orders):
routes.MapHttpRoute(
name: "OneLevelNested",
routeTemplate: "api/customers/{customerId}/orders",
constraints: new { httpMethod = new HttpMethodConstraint(new string[] { "POST" }) },
defaults: new { controller = "Customers", action = "CreateOrder", }
);
And handle it accordingly in the Customer controller:
public class CustomersController
{
// ...
public Order CreateOrder(long customerId)
{
// create and return the order just created (with the new order id)
}
// ...
}
Yes, you still have to create a lot of routes just because Web API still can't route to different methods depending on the path... But I think it is cleaner to declaratively define the routes than to come up with a custom dispatching mechanisms based on enums or other tricks.
For the consumer of your API it will look perfectly RESTful:
GET http://your.api/customers/1/orders (maps to GetOrders(long) returning all orders for customer 1)
GET http://your.api/customers/1/orders/22 (maps to GetOrders(long, long) returning the order 22 for customer 1
POST http://your.api/customers/1/orders (maps to CreateOrder(long) which will create an order and return it to the caller (with the new ID just created)
But don't take my word as an absolute truth. I'm still experimenting with it and I think MS failed to address properly subresource access.
I urge you to try out http://www.servicestack.net/ for a less painful experience writing REST apis... But don't get me wrong, I adore Web API and use it for most of my professional projects, mainly because it is easier to find programmers out there that already "know" it... For my personal projects I prefer ServiceStack.
Since Web API 2 you can use Route Attributes to define custom routing per Method, allowing for hierarchical routing
public class CustomersController : ApiController
{
[Route("api/customers/{id:guid}/products")]
public IEnumerable<Product> GetCustomerProducts(Guid id) {
return new Product[0];
}
}
You also need to initialize Attribute Mapping in WebApiConfig.Register(),
config.MapHttpAttributeRoutes();
I don't like using the concept of "actions" in the route of an ASP.NET Web API. The action in REST is supposed to be the HTTP Verb. I implemented my solution in a somewhat generic and somewhat elegant way by simply using the concept of a parent controller.
https://stackoverflow.com/a/15341810/326110
Below is that answer reproduced in full because I'm not sure what to do when one post answers two SO questions :(
I wanted to handle this in a more general way, instead of wiring up a ChildController directly with controller = "Child", as Abhijit Kadam did. I have several child controllers and didn't want to have to map a specific route for each one, with controller = "ChildX" and controller = "ChildY" over and over.
My WebApiConfig looks like this:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "ChildApi",
routeTemplate: "api/{parentController}/{parentId}/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
My parent controllers are very standard, and match the default route above. A sample child controller looks like this:
public class CommentController : ApiController
{
// GET api/product/5/comment
public string Get(ParentController parentController, string parentId)
{
return "This is the comment controller with parent of "
+ parentId + ", which is a " + parentController.ToString();
}
// GET api/product/5/comment/122
public string Get(ParentController parentController, string parentId,
string id)
{
return "You are looking for comment " + id + " under parent "
+ parentId + ", which is a "
+ parentController.ToString();
}
}
public enum ParentController
{
Product
}
Some drawbacks of my implementation
As you can see, I used an enum, so I'm still having to manage parent controllers in two separate places. It could have just as easily been a string parameter, but I wanted to prevent api/crazy-non-existent-parent/5/comment/122 from working.
There's probably a way to use reflection or something to do this on the fly without managing it separetly, but this works for me for now.
It doesn't support children of children.
There's probably a better solution that's even more general, but like I said, this works for me.

Why do you need a route defined for Html.Action?

I have created an otherwise empty ASP.NET MVC 3 application with 2 controllers, HomeController and OtherController.
HomeController.cs looks like this:
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
Index.cshtml looks like this:
#Html.Action("Index", "Other")
And, of course, Othercontroller.cs:
public class OtherController : Controller
{
[ChildActionOnly]
public ActionResult Index()
{
return Content("OK!");
}
}
So far, so good. I run the app, and it tells me everything is OK!
Now, I take the default RegisterRoutes from Global.asax.cs:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
And I crumple it up, so that no routes match OtherController:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("Default", "", new { controller = "Home", action = "Index" });
}
Now, when I run the page, it crashes with this error message:
System.InvalidOperationException: No route in the route table matches the supplied values.
Source Error:
Line 1: #Html.Action("Index", "Other")
I specified the controller name and action name in the call to .Action. No URLs are being generated, and no requests are being made. Why does routing even need to get involved?
I think that this blog post will help you understand a little more:
http://blogs.charteris.com/blogs/gopalk/archive/2009/01/20/how-does-asp-net-mvc-work.aspx.
Essentially, routing is involved in determining which controller to 'fire up' to handle the request and appropriate action to invoke based on the parameters that you sent and the MVCRouteHandler uses those to make a decision. Just because you tell it which controller in your action does not make it magically ignore the routing table, go straight to that controller class and bypass all the other MVC goodness that happens in the back-end. Remember, these #HTML.Action methods can take a whole lot of overloads which could influence which route in the routing table to use (think URL structure for instance).
The MVC paths are not to static content and as such, have to be parsed through the URLRoutingModule which uses the routing table to decide on what to do. Since you have no matching route - you get an error.
EDIT
In my diatribe, I did not actually address your final statement. You're right, no URL was generated, but a request to the application was generated. HTML.Action still will use routing to determine what controller, action, area, parameters to use. I think it be fair to say in simple terms that it's like generating an ActionLink and clicking it for you.
Routing got involved by using Html.Action on a controller. When the engine couldn't find the "Other" HtmlHelper with an action of "Index" it defaulted to executing that path which means passing through routing. No route matched so it threw an error.
InvalidOperationException The required virtual path data cannot be found.
http://msdn.microsoft.com/en-us/library/ee721266.aspx
The solution for me was as follows:
I had the following line that was giving me an error:
#{Html.RenderAction("ActionName", "ControllerName");}
Adding a thrid parameter solved the error:
#{Html.RenderAction("ActionName", "ControllerName", new { area = string.Empty });}

Removing direct reference to URL in view when two different URLs map to single controller action?

Basically I'm looking for the best way to remove the need to hard code any actual URLs within views when two different URLs call the same controller action. As shown below, two routes define different URLs, one for creating a new customer, whilst the other is for editing an existing customer. Inside the controller action it determines what to do by checking the value of AccountNumber - if it was not provided (null) then it knows that it is creating a new customer.
routes.MapRoute(
"Customer_Edit",
"Customer/Edit/{CustNum}",
new { controller = "Customer", action = "Edit", AccountNumber = "{CustNum}" }
);
routes.MapRoute(
"Customer_Create",
"Customer/Create",
new { controller = "Customer", action = "Edit" }
);
The downside to this approach is that you can't use Url.Action(...) in the views to determine the URL because there are two different URLs mapping to the controller action. I had hoped that the call to Url.Action(...) would determine which to use by the "RouteValues" provided, but it just appears to use the 1st matching route defined. So in this case if you call
Url.Action("Edit", "Customer", new { CustNum = 2 })
you get "Customer/Edit/2" as you'd expect, but if you call
Url.Action("Edit", "Customer")
the view outputs the URL "Customer/Edit/{CustNum}".
Is there anyway using this configuration that you would not need to hard code URLs into the view? Or is it considered better practise to have a separate action for creating new customers and another action for editing customers, so you end up with the following
public class CustomerController : Controller
{
public ActionResult Create()
{ return CreateOrEdit(null); }
public ActionResult Edit(int custNum)
{ return CreateOrEdit(custNum); }
private ActionResult CreateOrEdit(int? custNum)
{....}
}
Use Url.RouteUrl instead and specify the route you want. In addition to actually working, RouteUrl is on the order of 10* faster than Action.

asp.net mvc - dynamic controller based on authenticated user

If I want the default url of my web app to display completely different UIs depending on the user, what is the best way to accomplish this? I don't really want to use the same controller for every type of user. To put it another way, if a user is logged in and goes to http://mysweetapp.com and is an admin user, they should get what they would see the same thing as if they had gone to http://mysweetapp.com/admin. If the user is logged in as a normal user, they should see the same thing as if they had gone to http://mysweetapp.com/normaluser
Should I just make a "redirect" controller as my default and have it send the client to the appropriate controller?
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Redirect", action = "Index", id = "0" });
I've also tried creating my own ControllerFactory, but I don't think I was clear on the concept and couldn't get it to work.
Thanks
The cleanest way in my opinion would be to create a custom route handler to be used by your default route. Then you can separate out which controller to be used if the controller name is your default controller name, in the example below, it is: Home. Then check if the user is an administrator or not and process the request with the controller you would like to use.
Here is the code:
public class CustomHttpHandler : IHttpHandler
{
public RequestContext RequestContext { get; private set; }
public CustomHttpHandler(RequestContext requestContext)
{
try
{
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
if (controllerName.Equals("home", StringComparison.CurrentCultureIgnoreCase))
{
bool isAdmin = RequestContext.HttpContext.User.IsInRole("Admin");
controllerName = isAdmin ? "admin" : "normaluser";
}
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(RequestContext, controllerName);
if (controller != null)
{
controller.Execute(RequestContext);
}
}
finally
{
factory.ReleaseController(controller);
}
}
}
public class CustomRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new CustomHttpHandler(requestContext);
}
}
// Now use the CustomRouteHandler when you map your default route.
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
).RouteHandler = new CustomRouteHandler();
Hope this helps.
For simplicity, in your HomeController Index method (or whatever default controller you are using) you could put some code like this and then the links from the AdminIndex view or the Index view can send the users to appropriate areas when they start navigating round your site - that way you have one shared controller and the other controllers can be specific to the user type.
return user.IsAdministrator ? View("AdminIndex") : View("Index");
the user.IsAdministrator call is pseudocode of course - replace this with whatever method you are using to work out if the user is an admin user
If you don't want to use the same controller set up individual controllers and views for each item first - mysweetapp.com/admin and mysweetapp.com/normaluser.
You can then redirect specific users to this page through a default controller based on their logged in role.
if (User.IsInRole("Admin")
{
return RedirectToAction("Index", "admin");
}
else if (User.IsInRole("Standard")
{
return RedirectToAction("Index", "normaluser");
}
What you might want to consider is areas. This would allow you to have separate controllers for each area. Then permit access to those areas based on roles or whatever you wish.
This will give you routes like '/admin/controller/action', '/users/controller/action', etc. The 'pattern' separates all your controllers by namespace, and handles the routing quite well. Separate master pages easily, etc.
It won't give you the (potentially confusing, IMO) '/' and '/admin/' looking the same to an admin user, but it will let you separate the content and controllers.
What you are describing would lead to potentially tons of methods for each controller, something that is generally frowned upon by the MVC/REST crowd. It's not horrible, but its not considered best practice either.
You can read about areas at this blog here. Google 'asp.net mvc areas' for more.
--------edit-----------
To expand a bit:
Without custom routes or some other shenanigans, actions are mapped to controllers by the url. So if you want to keep all admin actions and views different, but on the root url, along with normal user actions, this would lead to one big controller that has to handle all these actions, or some strange "if this role, this view; if that role, that view" sort of nonsense that would have to happen in each action. Kind of a mess to debug potentially.
Similarly, the default view engine finds the views based on the url as well.
This would mean that all of your views are going to sit in one big ugly directory full of all sorts of weird similarly named but differently behaving views.
In short, this would become a potentially horrific maintenance nightmare, depending on complexity of the application.
Could you create a class that extends from DefaultControllerFactory and overrides CreateController?
public class RedirectControllerFactory : DefaultControllerFactory
{
public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
if (controllerName.Equals("Redirect"))
{
controllerName = requestContext.HttpContext.User.IsInRole("Admin") ? "Admin" : "NormalUser";
}
return base.CreateController(requestContext, controllerName);
}
}
Then in your Application_Start():
protected void Application_Start()
{
// ...
ControllerBuilder.Current.SetControllerFactory(new RedirectControllerFactory());
}

Categories of controllers in MVC Routing? (Duplicate Controller names in separate Namespaces)

I'm looking for some examples or samples of routing for the following sort of scenario:
The general example of doing things is: {controller}/{action}/{id}
So in the scenario of doing a product search for a store you'd have:
public class ProductsController: Controller
{
public ActionResult Search(string id) // id being the search string
{ ... }
}
Say you had a few stores to do this and you wanted that consistently, is there any way to then have: {category}/{controller}/{action}/{id}
So that you could have a particular search for a particular store, but use a different search method for a different store?
(If you required the store name to be a higher priority than the function itself in the url)
Or would it come down to:
public class ProductsController: Controller
{
public ActionResult Search(int category, string id) // id being the search string
{
if(category == 1) return Category1Search();
if(category == 2) return Category2Search();
...
}
}
It may not be a great example, but basically the idea is to use the same controller name and therefore have a simple URL across a few different scenarios, or are you kind of stuck with requiring unique controller names, and no way to put them in slightly different namespaces/directories?
Edit to add:
The other reason I want this is because I might want a url that has the categories, and that certain controllers will only work under certain categories.
IE:
/this/search/items/search+term <-- works
/that/search/items/search+term <-- won't work - because the search controller isn't allowed.
I actually found it not even by searching, but by scanning through the ASP .NET forums in this question.
Using this you can have the controllers of the same name under any part of the namespace, so long as you qualify which routes belong to which namespaces (you can have multiple namespaces per routes if you need be!)
But from here, you can put in a directory under your controller, so if your controller was "MyWebShop.Controllers", you'd put a directory of "Shop1" and the namespace would be "MyWebShop.Controllers.Shop1"
Then this works:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
var shop1namespace = new RouteValueDictionary();
shop1namespace.Add("namespaces", new HashSet<string>(new string[]
{
"MyWebShop.Controllers.Shop1"
}));
routes.Add("Shop1", new Route("Shop1/{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
action = "Index",
id = (string)null
}),
DataTokens = shop1namespace
});
var shop2namespace = new RouteValueDictionary();
shop2namespace.Add("namespaces", new HashSet<string>(new string[]
{
"MyWebShop.Controllers.Shop2"
}));
routes.Add("Shop2", new Route("Shop2/{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
action = "Index",
id = (string)null
}),
DataTokens = shop2namespace
});
var defaultnamespace = new RouteValueDictionary();
defaultnamespace.Add("namespaces", new HashSet<string>(new string[]
{
"MyWebShop.Controllers"
}));
routes.Add("Default", new Route("{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
DataTokens = defaultnamespace
});
}
The only other thing is that it will reference a view still in the base directory, so if you put the view into directories to match, you will have to put the view name in when you return it inside the controller.
The best way to do this without any compromises would be to implement your own ControllerFactory by inheriting off of IControllerFactory. The CreateController method that you will implement handles creating the controller instance to handle the request by the RouteHandler and the ControllerActionInvoker. The convention is to use the name of the controller, when creating it, therefore you will need to override this functionality. This will be where you put your custom logic for creating the controller based on the route since you will have multiple controllers with the same name, but in different folders. Then you will need to register your custom controller factory in the application startup, just like your routes.
Another area you will need to take into consideration is finding your views when creating the controller. If you plan on using the same view for all of them, then you shouldn't have to do anything different than the convention being used. If you plan on organizing your views also, then you will need to create your own ViewLocator also and assign it to the controller when creating it in your controller factory.
To get an idea of code, there are a few questions I have answered on SO that relate to this question, but this one is different to some degree, because the controller names will be the same. I included links for reference.
Views in separate assemblies in ASP.NET MVC
asp.net mvc - subfolders
Another route, but may require some compromises will be to use the new AcceptVerbs attribute. Check this question out for more details. I haven't played with this new functionality yet, but it could be another route.

Resources