Nested resources in ASP.net MVC 4 WebApi - asp.net-mvc

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.

Related

Custom Route for handling Root level pages added using Kentico CMS ASP.NET MVC

We're creating a custom site using Kentico CMS. I'm a front-end developer for the most part but I'm lead on this project. I understand most Routing concepts in ASP.NET MVC but I'm not sure what to do about pages that get added at a Root Level since I won't be creating pages it would be the Content Entry person creating them through the UI. So I'm trying to figure out what kind of approach would be best and if there is an option to write a route that could even handle it?
I'm stumped because generally if you have root level pages you would have to define them:
routes.MapRoute(
name:"About",
url: "about",
new { controller = "Home", action = "About"}
);
But in the context of a CMS any number of pages can be added to the root level. Is there some way to feed it a collection of pages that occur from the root down to create the routes?
I'll be very active with this post as I'm proofing out ideas as I come up with them but I really have no idea where to start looking. If I need to provide more information please let me know.
EDIT:
Also, is there a way to make Dynamic Controllers even? Then it could just use the default route for the most part and the controller decide where to send the content.
Hey I wrote a blog post about this. The answer is briefly, "Write a route constraint." This isn't really a Kentico specific answer, and more of a general ASP.NET MVC thing.
Route constraints will basically define "Pages that I return true for get routed to this controller." It can look/feel scary to dig in to the more advanced parts of routing in asp, but the code you end up with is generally pretty simple.
https://www.bizstream.com/blog/july-2020/how-to-implement-custom-urls-in-kentico-xperience
So for an example (and only an example, don't use this in prod) if you ONLY wanted root level pages made by a content editor of a type "Client.AboutUsPageType" your route constraint would look like:
public class RootLevelAboutUsTypeConstraint : IRouteConstraint
{
public bool Match( HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var alias = values[parameterName].ToString();
var results = CacheHelper.Cache(cs =>
{
var tree = new TreeProvider();
List<TreeNode> nodes = tree.SelectNodes()
.WhereLike("NodeAliasPath", alias)
.Type("Client.AboutUsPageType")
.OnSite("CorporateSite", true)
.ToList();
if (cs.Cached)
{
cs.GetCacheDependency = () => CacheHelper.GetCacheDependency($"nodes|CorporateSite");
}
return nodes;
}, new CacheSettings(3600, "pagesnamed|get", alias, "CorporateSite"));
return results.Any();
}
}
You would then add it to your route.
routes.MapRoute(
name:"About",
url: "{url}",
new { controller = "Home", action = "About"},
constraints: new { url = new RootLevelAboutUsConstraint() })
);
Again, warning that doing this for every single page type would be awful. You can and should setup a parameterized route constraint that can be reused between actions. Read the blog post and you should be able to get it down to:
public class HomeController : Controller
{
[HttpGet]
[Route("{nodeName:nodealias(Client.AboutUsPageType)}")]
public ActionResult About(string nodeName)
{
return View(nodeName);
}
}

asp.net webapi url to identify same resource

I want to be able to identify a resource by different id types. for example:
GET http://example.com/customers/internalId=34 to go to
public Customer GetByInternalId(int internalId){...}
and
GET http://example.com/customers/externalId='JohnDoe' to go to
public Customer GetByExternalId(string externalId){...}
I know I can do this by having some parsing logic in a generic controller method but I don't want to do that. How do I achieve this using the routing feature of asp.net webapi if that is possible.
I would suggest that you try and avoid doing what you are suggesting. Creating two distinct URIs for the same resource will make it harder to use caching. Instead I would suggest using one URL to redirect to the other.
e.g.
> GET /customers/34
< 200 OK
> GET /Customers?name=JohnDoe
< 303 See Other
< Location: http://example.com/customers/34
Your methods do not make much of a sense, why would you return void from a method that begins with Get....?
Also, these routes:
http://example.com/customers/internalId=34
http://example.com/customers/externalId='JohnDoe
Are invalid from MVC/Web API perspective. This is how they should look like:
http://example.com/customers?internalId=34
http://example.com/customers?externalId=John
Default Web API routing should differentiate between the two and route it to different actions.
EDIT:
Create action with the following template:
[HttpGet]
public string InternalId(int id)
{
return id.ToString();
}
Define route for Web Api:
config.Routes.MapHttpRoute(
name: "Weird",
routeTemplate: "{controller}/{action}={id}",
defaults: new { id = RouteParameter.Optional }
);
This allows you to write:
http://localhost:7027/values/internalId=12
Try it...
Then you can just add another method:
[HttpGet]
public string ExternalId(string id)
{
return id;
}
And this:
http://localhost:7027/values/externalId=bob
Will work as well.
Clearly name of my controller is ValuesController as I've just tested this with default Web Api template.

Value cannot be null. Parameter name: String in asp.net mvc C#

I am pretty new to asp.net mvc. I want to get the parameter string in my url
http://localhost/Item/ItemSpec/3431?dep=62&cat=129&tab=2
How can I get the value=3431?
I tried to used HttpContext.Current.Request.QueryString["id"], but it's not work. 3431 is the id of the item that display in my page, that's why I used ["id"].
Thanks you so much.
The 3431 is part of the path of the request, not part of the query string. You could use HttpRequest.Path to get at the path, but MVC routing should allow you to simply write a controller method which accepts the ID as a parameter. I suggest you read up on how to configure routing. (Just searching for ASP.NET routing or MVC routing will give you lots of articles.)
Assuming the default route is configured in Global.asax ({controller}/{action}/{id}) you could have your controller action take an id parameter and the default model binder will automatically set its value:
public ActionResult Foo(string id)
{
...
}
If you want to fetch this id value from some other portion of your code that does have access to an HttpContext you need to fetch it from the RouteData:
var id = HttpContext.Request.RequestContext.RouteData["id"];
RouteData is available in all standard MVC locations. In your example you have used the static HttpContext.Current property which is something that you should never use. I suspect that you are trying to fetch this id from a portion of your code where you are not supposed to have access to the HttpContext. So you'd better fetch this id using standard techniques and then pass it as parameter to other parts of your code.
If Item is your controller, and ItemSpec is action, you can get the Id just by
public ActionResult ItemSpec(int id) { }
You routing have to be setup to:
context.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index" }
);
If you haven't changed your routing so it's still defined as it was when you created Asp.net MVC Web project then this should be one of your controllers:
public class ItemController : ControllerBase
{
...
public ActionResult ItemSpec(int id, int dep, int cat, int tab)
{
// implementation that uses all four values
}
...
}
This is of course just one of the actions in it. There may be others as well. Most likely the Index one that's generated by default and is also used by default routing...

ASP.NET MVC Areas or Defined Routes?

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.

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