Background
I wrote an REST-like API service controller for a simple Blog app. I use two routes to handle basic CRUD:
// Actions should handle: GET, POST, PUT, DELETE
routes.MapRoute("Api-SingleThing", "thing/{id}",
new { controller = "ThingService", action = "SingleThing" });
// Action should handle: GET
routes.MapRoute("Api-AllThings", "things",
new { controller = "ThingService", action = "AllThings" });
The matching controller code looks like:
[HttpGet]
public ActionResult AllThings() {}
[HttpGet]
[ActionName("SingleThing")]
public ActionResult Get(string id) {}
[HttpPost]
[ActionName("SingleThing")]
public JsonResult Create(Thing thing) {}
[HttpPut]
[ActionName("SingleThing")]
public ActionResult Update(Thing thing) {}
[HttpDelete]
[ActionName("SingleThing")]
public ActionResult Delete(int id) {}
The [ActionName()] attribute is used to avoid route constraints, so that the route when triggered always calls the "SingleThing" action on the controller - regardless of the HTTP verb. This lets the controller action methods that share the name, decide who handles the request based on the [HttpVerb] attributes.
In my blog app, this works like a charm, but only because the {id} route parameter (aka. the slug) is always present, even on POST and PUT requests.
With this new API shown above, the POST and PUT actions do not trigger the top route (ex. no {id} value), and when they trigger the second route, there is no method to handle them because of the verbs.
Question
What's the best way to maintain this REST-ful URL architecture and verb handling, and ensure I trigger my POST and PUT actions?
Related
I need to change the name of the controller in MVC 5 I used to do the following:
[RouteArea("Dispatch")]
[RoutePrefix("TrackedAssets")]
[Route("{action=index}")]
public class TrackedItemsController : MainControllerBase
{
When i try to hit Index action its OK.
But there are some actions for CRUD, when i tried to hit them by the new URL /Dispatch/TrackedAssets/Edit/47
It gives me:
Description: HTTP 404. The resource you are looking for (or one of its
dependencies) could have been removed, had its name changed, or is
temporarily unavailable. Please review the following URL and make
sure that it is spelled correctly.
Requested URL: /Lynx/Dispatch/TrackedAssets/Edit/47
Below is the signature of the action:
public ActionResult Edit(int? id)
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit
Your requested route i.e /Lynx/Dispatch/TrackedAssets/Edit/47 will not hit any action method since you have not prefixed your Edit action method with route attribute just like you have set route attribute for index method.
[Route("{action=edit}")]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int? id)
Try this and please mark this as answer if it helped you.
I'm new to ASP.NET Web API but I'm hitting an issue where if I'm trying to add two get methods to a single controller it is failing at trying to get the action. Are we allowed only one get per controller, unless we are adding an overload of the same method?
This works if I only have one get method, e.g. the first one. As soon as I add Get to the second method name, it gives me a 500.
[HttpGet]
public string GetToday()
{
return "Hello today";
}
[HttpGet]
public string GetPending()
{
return "Hello Pending";
}
The calls I'm making:
http://abc.com/api/tasks/gettoday
http://abc.com/api/tasks/getpending
I can make this call if the method name is just 'Today', as long as I put the [HttpGet] attribute. But only if I don't have the second action. Which means if I remove the [HttpGet] attribute from the second method as well as remove 'Get' from the method name so it is just Pending, then it works.
As soon as there are two get methods I get this error:
{"Message":"An error has occurred.","ExceptionMessage":"Multiple actions were found that match the request: \r\nSystem.String TodaysTasks() on type TaskTrackerService.Controllers.TasksController\r\nSystem.String PendingTasks() on type TaskTrackerService.Controllers.TasksController","ExceptionType":"System.InvalidOperationException","StackTrace":" at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)\r\n at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext)\r\n at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsyncInternal(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)"}
The idea behind a Web API is that it represents a ReSTful resource. The semantics of ReST imply that a controller corresponds to the idea of a 'resource' and that behaviour is determined by the HTTP verb used to access the resource — means GET, POST, PUT, DELETE, …
The model used for these resources is that you treat them somewhat "CRUD-y"; that you query for elements of the resource, put new elements into the resource, update elements in the resource – you get the idea.
For each (relevant) verb you should have only one action that deals with requests of that nature. The idea of having multiple actions accessed by GET is not very ReSTful and more a MVC-ish RPC-style mode of thinking that is not well-suited for ASP.NET Web API.
So your controller might instead be written:
public class FooController : ApiControlle
{
[HttpGet]
public Account Get(string flavour)
{
switch (flavour) {
case "greeting": return "Hello today";
case "pending": return "Hello Pending";
default: throw new InvalidOperationException("Cannot get your flavour of foo :(");
}
}
}
But as you have written in a comment, the trouble was that you never intended to use the controller as Web API controller, but MVC controller.
When Urls are autogenerated using the Url.Action helper, if a page contains a line similar to
#Url.Action("Edit","Student")
is expected to generate a url like domain/student/edit and its working as expected.
But if the requested url contains some parameters, like domain/student/edit/210, the above code uses these parameters from the previous request and generates something similar even though I've not provided any such parameter to the Action method.
In short, if the requested url contains any parameters, any auto generated links of the page (served for that request) will include those parameters as well no matter if I specify them or not in the Url.Action method.
What's going wrong?
Use Darin's answer from this similar question.
#Url.Action("Edit","Student", new { ID = "" })
Weird, can't seem to reproduce the problem:
public class HomeController : Controller
{
public ActionResult Index(string id)
{
return View();
}
public ActionResult About(string id)
{
return View();
}
}
and inside Index.cshtml:
#Url.Action("About", "Home")
Now when I request /home/index/123 the url helper generates /home/about as expected. No ghost parameters. So how does your scenario differs?
UPDATE:
Now that you have clarified your scenario it seems that you have the following:
public class HomeController : Controller
{
public ActionResult Index(string id)
{
return View();
}
}
and inside Index.cshtml you are trying to use:
#Url.Action("Index", "Home")
If you request /home/index/123 this generates /home/index/123 instead of the expected /home/index (or simply / taken into account default values).
This behavior is by design. If you want to change it you will have to write your own helper which ignores the current route data. Here's how it might look:
#UrlHelper.GenerateUrl(
"Default",
"index",
"home",
null,
Url.RouteCollection,
// That's the important part and it is where we kill the current RouteData
new RequestContext(Html.ViewContext.HttpContext, new RouteData()),
false
)
This will generate the proper url you were expecting. Of course this is ugly. I would recommend you encapsulating it into a reusable helper.
Use ActionLink overload that uses parameters and supply null
You could register custom route for this action for example:
routes.MapRoute("Domain_EditStudentDefault",
"student/edit",
new {
controller = MVC.Student.Name,
action = MVC.Student.ActionNames.Edit,
ID = UrlParameter.Optional
},
new object(),
new[] { "MySolution.Web.Controllers" }
);
you then could use url.RouteUrl("Domain_EditStudentDefault") url RouteUrl helper override with only routeName parameter which generates url without parameters.
Hey guys,
What is the best mechansims for persisting viewmodel data from one controller to another.
For instance
return RedirectToAction("SomeAction", "SomeController");
I need to have some data from the previous controller available to the new controller I am redirecting to.
If you are not passing an object or something complex, make use of parameters. Just make sure redirected action gets parameters to display what it should.
return RedirectToAction("SomeAction", "SomeController",new { id=someString} );
Get the parameter in the action:
public ActionResult SomeAction(string id)
{
//do something with it
}
#Ufuk Hacıoğulları: You can't share information between 2 controllers using ViewData. ViewData only shares information between Controller and View.
If you need to share complex information between multiple Controllers while redirection, use "TempData" instead.
Here is how you use "TempData" - http://msdn.microsoft.com/en-us/library/dd394711.aspx
A redirect is going to send an http response to the client that directs it to then make a new http request to /SomeController/SomeAction. An alternative would be for you to call a method on your other controller directly... new SomeController().SomeAction(someData) for example.
I think this will be helpfull to you to pass value from one action to another action .
public ActionResult ActionName(string ToUserId)
{
ViewBag.ToUserId = ToUserId;
return View();
}
public ActionResult ssss(string ToUserId)
{
return RedirectToAction("ActionName", "ControllerName", new { id = #ToUserId });
}
We have an a PHP application that we are converting to MVC. The goal is to have the application remain identical in terms of URLs and HTML (SEO and the like + PHP site is still being worked on). We have a booking process made of 3 views and in the current PHP site, all these view post back to the same URL, sending a hidden field to differentiate which page/step in the booking process is being sent back (data between pages is stored in state as the query is built up).
To replicate this in MVC, we could have a single action method that all 3 pages post to, with a single binder that only populates a portion of the model depending on which page it was posted from, and the controller looks at the model and decides what stage is next in the booking process. Or if this is possible (and this is my question), set up a route that can read the POST parameters and based on the values of the POST parameters, route to a differen action method.
As far as i understand there is no support for this in MVC routing as it stands (but i would love to be wrong on this), so where would i need to look at extending MVC in order to support this? (i think multiple action methods is cleaner somehow).
Your help would be much appreciated.
I have come upon two solutions, one devised by someone I work with and then another more elegant solution by me!
The first solution was to specify a class that extends MVcRouteHandler for the specified route. This route handler could examine the route in Form of the HttpContext, read the Form data and then update the RouteData in the RequestContext.
MapRoute(routes,
"Book",
"{locale}/book",
new { controller = "Reservation", action = "Index" }).RouteHandler = new ReservationRouteHandler();
The ReservationRouteHandler looks like this:
public class ReservationRouteHandler: MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var request = requestContext.HttpContext.Request;
// First attempt to match one of the posted tab types
var action = ReservationNavigationHandler.GetActionFromPostData(request);
requestContext.RouteData.Values["action"] = action.ActionName;
requestContext.RouteData.Values["viewStage"] = action.ViewStage;
return base.GetHttpHandler(requestContext);
}
The NavigationHandler actually does the job of looking in the form data but you get the idea.
This solution works, however, it feels a bit clunky and from looking at the controller class you would never know this was happening and wouldn't realise why en-gb/book would point to different methods, not to mention that this doesn't really feel that reusable.
A better solution is to have overloaded methods on the controller i.e. they are all called book in this case and then define your own custome ActionMethodSelectorAttribute. This is what the HttpPost Attribute derives from.
public class FormPostFilterAttribute : ActionMethodSelectorAttribute
{
private readonly string _elementId;
private readonly string _requiredValue;
public FormPostFilterAttribute(string elementId, string requiredValue)
{
_elementId = elementId;
_requiredValue = requiredValue;
}
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (string.IsNullOrEmpty(controllerContext.HttpContext.Request.Form[_elementId]))
{
return false;
}
if (controllerContext.HttpContext.Request.Form[_elementId] != _requiredValue)
{
return false;
}
return true;
}
}
MVC calls this class when it tries to resolve the correct action method on a controller given a URL. We then declare the action methods as follows:
public ActionResult Book(HotelSummaryPostData hotelSummary)
{
return View("CustomerDetails");
}
[FormFieldFilter("stepID", "1")]
public ActionResult Book(YourDetailsPostData yourDetails, RequestedViewPostData requestedView)
{
return View(requestedView.RequestedView);
}
[FormFieldFilter("stepID", "2")]
public ActionResult Book(RoomDetailsPostData roomDetails, RequestedViewPostData requestedView)
{
return View(requestedView.RequestedView);
}
[HttpGet]
public ActionResult Book()
{
return View();
}
We have to define the hidden field stepID on the different pages so that when the forms on these pages post back to the common URL the SelectorAttributes correctly determines which action method to invoke. I was suprised that it correctly selects an action method when an identically named method exists with not attribute set, but also glad.
I haven't looked into whether you can stack these method selectors, i imagine that you can though which would make this a pretty damn cool feature in MVC.
I hope this answer is of some use to somebody other than me. :)