Can anyone explain CreatedAtRoute() to me? - c#

From the template for Web API 2, a post method is always like this:
[ResponseType(typeof(MyDTO))]
public IHttpActionResult PostmyObject(MyDTO myObject)
{
...
return CreatedAtRoute("DefaultApi", new { id = myObject.Id }, myObject);
}
I don't understand this CreatedAtRoute() method. Can anyone explain it to me?

The CreatedAtRoute method is intended to return a URI to the newly created resource when you invoke a POST method to store some new object.
So if you POST an order item for instance, you might return a route like 'api/order/11' (11 being the id of the order obviously).
BTW I agree that the MSDN article is of no use in understanding this. The route you actually return will naturally depend on your routing setup.

When you use CreatedAtRoute, the first argument is the route name of the GET to the resource. The trick that is not so obvious is that, even with the correct method name specified, you must thus use the Name param on the HttpGet attribute for it to work.
So if the return in your POST is this:
return CreatedAtRoute("Get", routeValues: new { id = model.Id }, value: model);
Then your Get method attribute should look like this even if your method is named Get:
[HttpGet("{id}", Name = "Get")]
Calls to your Post method will not only return the new object (normally as JSON), it will set the Location header on the response to the URI that would get that resource.
NOTE the field names in the routeValues field names need to match the binding names in the target route, i.e. there needs to be a field named id to match the {id} in HttpGet("{id}"
Finally, in some cases, it should be mentioned that the CreatedAtAction helper can be a more direct solution.

In .net core WebAPI, you use this method to return a 201 code, which means that the object was created.
[Microsoft.AspNetCore.Mvc.NonAction]
public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult CreatedAtRoute (string routeName, object routeValues, object content);
As you can see above, the CreatedAtRoute can receive 3 parameters:
routeName
Is the name that you must put on the method that will be the URI that would get that resource after created.
routeValues
It's the object containing the values that will be passed to the GET method at the named route. It will be used to return the created object
content
It's the object that was created.
The above example shows the implementation of two methods of a simple controller with a simple GET method with the bonded name and the POST method that creates a new object.
[Route("api/[controller]")]
[ApiController]
public class CompanyController : Controller
{
private ICompanyRepository _companyRepository;
public CompanyController(ICompanyRepository companyRepository)
{
_companyRepository = companyRepository;
}
[HttpGet("{id}", Name="GetCompany")]
public IActionResult GetById(int id)
{
Company company = _companyRepository.Find(id);
if (company == null)
{
return NotFound();
}
return new ObjectResult(company);
}
[HttpPost]
public IActionResult Create([FromBody] Company company)
{
if (company == null)
{
return BadRequest();
}
_companyRepository.Add(company);
return CreatedAtRoute(
"GetCompany",
new { id = company.CompanyID },
company);
}
}
IMPORTANT
Notice that the first parameter at CreatedAtRoute (routeName), must be the same at the definition of the Name at the Get method.
The object on the second parameter will need to have the necessary fields that you use to retrieve the resource on the Get method, you can say that it's a subset of the object created itself
The last parameter is the company object received in the body request in it's full form.
FINALY
As final result, when the Post to create a new company got made to this API, you will you return a route like 'api/company/{id}' that will return to you the newly created resource

Related

In MVC, why does my controller need to receive a "new { id = restaurant.Id }", as opposed to "restaurant.Id"? Are the not both simply int's?

So my mvc controller method here take an int id as an argument. Im wondering why new "{ id = restaurant.Id}" is required and "restaurant.Id" is not sufficient.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Restaurant restaurant)
{
if (ModelState.IsValid)
{
db.Update(restaurant);
return RedirectToAction("Details", new { id = restaurant.Id });
}
return View(restaurant);
}
That is because MVC expects an object from which to retrieve the values to bind to the action parameters. In other words, you could do:
return RedirectToAction("Details", restaurant);
Mvc will crawl your object for properties that match the name and type of your action's arguments and should therefore bind to the restaurant's 'Id' property.
If you just pass the restaurant's Id, though, MVC will crawl the type of whatever you restaurant's id property is.
Hope this helps. If it does, please mark it as the answer ;)

Return CreatedAtRoute location from ODataController

I have ODataController with a Post method in it which should return a URL to a newly created OData resource, something like the following:
public class TasksController: ODataController
{
[HttpPost]
public IActionResult Post([FromBody] Request request)
{
...
return CreatedAtRoute("GetTask", new Dictionary<string, object>{{"id", id}}, new object());
}
[ODataRoute(RouteName = "GetTask")]
public IActionResult Get(int key)
{
...
}
}
In my case I'm getting "InvalidOperationException: No route matches the supplied values" when returning CreatedAtRoute. I can fix the issue by changing code to:
return Created($"{baseUri}/odata/Task({id})", new object());
Is there any way to use CreatedAtRoute instead and making it return correct OData path?
I had this problem also. I was able to get it working by adding "odataPath" to the routeValues:
return CreatedAtAction(nameof(Get), new { id, odataPath = $"{baseUri}/odata/Task({id})" }, new object());
UPDATE:
I did find an alternate/better approach. When inheriting from ODataController, you have access to two additional result types: CreatedODataResult<TEntity> and UpdatedODataResult<TEntity>. So this works:
return Created(new object());
That returns a 201 with the OData-specific create route in the location header.
The route your are returning in the Created method: "{baseUri}/odata/Task({id})" doesn't exist. The simplest fix would be to change your URL to match your controller method.
Change: $"{baseUri}/odata/Task({id})"
to match $"{baseUri}/odata/GetTask({id})"

Cannot generate correct links

I have the following code in a razor page:
#Url.Action("ArticleDetails", "Information", new { slug = article.Slug })
The page url where this code is placed has the form of http://localhost/category/6/category-name where 6 is the ID of the category
In the InformationController I have the following actions:
[HttpGet("article/{id}/{slug}")]
public IActionResult ArticleDetails(int id, string slug)
{
// some code ...
return View(data);
}
[HttpGet("article/{slug}")]
public IActionResult ArticleDetails(string slug)
{
// some code ...
return View(data);
}
How can I reach URL of form article/article-slug because #Url.Action(...) that I have in the page always try to reach controller action with id even if ID is not supplied as an anonymous type.
Links take the form of article/6/article-slug instead I want them to be article/article-slug without removing action with id in the controller.
I have noticed that 6 is from the id of the category. Also if I delete the controller action with Id i get the correct format of URL.
When resolving the action you're linking to, the IUrlHelper instance is using the current value of id in your current route (http://localhost/category/6/category-name), which has a value of 6, as you stated in your OP. Because there exists an ArticleDetails action that takes both an id and a slug (which you provide explicitly), the ArticleDetails action that takes both of these parameters is selected.
In order to resolve this, there are a couple of options. The first option is to clear out the RouteData value once you've used it in the action invoked when reaching http://localhost/category/6/category-name. In order to do that, you can use the following code within said action:
RouteData.Values.Remove("id");
I'm not a fan of doing it this way, but it does work. IMO, a better approach would be to simply use different names for the id parameter: e.g. categoryId and articleId in the respective controllers. This both fixes your issue and makes the code more readable in the corresponding actions. Your ArticleDetails action would simply change to:
[HttpGet("article/{articleId}/{slug}")]
public IActionResult ArticleDetails(int articleId, string slug)
{
// some code ...
return View(data);
}
Routing can be finicky when trying to do overloads like this. Your best bet is to use named routes:
[HttpGet("article/{id}/{slug}", Name = "ArticleDetailsIdSlug")]
public IActionResult ArticleDetails(int id, string slug)
[HttpGet("article/{slug}", Name = "ArticleDetailsSlug")]
public IActionResult ArticleDetails(string slug)
Then, in your view:
#Url.RouteUrl("ArticleDetailsSlug", new { slug = article.Slug })
Now, the routing framework doesn't have to try to figure out which route you actually want (and guess incorrectly apparently), as you'd told it exactly which route to use.

MVC passing complex object to action

I have two actions in my controller when a user call one i need to redirect it to another one and pass a complex object :
first action :
public virtual ActionResult Index(string Id) {
var input = new CustomInput();
input.PaymentTypeId = Id;
return RedirectToAction(MVC.Ops.SPS.Actions.Test(input));
}
second action :
public virtual ActionResult Test(CustomInput input) {
return View();
}
The probelm is that the input arrives null at the second action. how can i solve it?
You can solve this using temp data to temporarily hold a value from which the second method retrieves that value.
public virtual ActionResult Index(string Id)
{
var input = new CustomInput();
input.PaymentTypeId = Id;
TempData["TheCustomData"] = input; //temp data, this only sticks around for one "postback"
return RedirectToAction(MVC.Ops.SPS.Actions.Test());
}
public virtual ActionResult Test()
{
CustomInput = TempData["TheCustomData"] as CustomInput;
//now do what you want with Custom Input
return View();
}
You can keep your tempData going so long as it is never null using the .keep() method like this,
if (TempData["TheCustomData"] != null)
TempData.Keep("TheCustomData");
I want to make sure that you know that RedirectToAction creates new HTTP request so this create another GET and all you can pass is RouteValueDictionary object which is like having query string parameters which is list of key and value pairs of string. That said, You can't pass complex object with your way of code however the TempData solution mentioned by #kyleT will work.
My recommendation based on your code is to avoid having two actions and redirect from one another unless you are doing something more than what you have mentioned in your question. Or you can make your Test action accepting the id parameter the your Index action will contain only RedirectToAction passing the id as route parameter.
Edit:
Also, If you have no primitive properties you can pass your object like the following (Thanks to #Stephen Muecke)
return RedirectToAction("Test",(input);

Web API 2 controller does not accept QueryString parameters

I have a GET() controller to retrieve a list of entities. I want to pass a parameter to the action to filter the list of objects returned as follows:
Mysite.com/Users?nameContains=john
This is my action definition:
public IEnumerable<object> Get(string nameContains)
{
// I want to use nameContains here
}
I get an error:
The requested resource does not support http method 'GET'.
If I revert the method to not get that parameter, it works.
Try this
public IEnumerable<object> Get([FromUri] string nameContains)
{
// I want to use nameContains here
}
Also since you are working in Web Api 2, you can make use of attribute routing
[Route("users")]
public IEnumerable<object> Get([FromUri] string nameContains)
{
Sorry, it was my mistake, I used 2 parameters and I didn't pass one of them (nor assigned it a default value) so it returned an error. Cheers.
You can add a new route to the WebApiConfig entries.
For instance, your method definition:
public IEnumerable<object> Get(string nameContains)
{
// I want to use nameContains here
}
add:
config.Routes.MapHttpRoute(
name: "GetSampleObject",
routeTemplate: "api/{controller}/{nameContains}"
);
Then add the parameters to the HTTP call:
GET //<service address>/Api/Data/test
or use HttpUtility.ParseQueryString in your method
// uri: /Api/Data/test
public IEnumerable<object> Get()
{
NameValueCollection nvc = HttpUtility.ParseQueryString(Request.RequestUri.Query);
var contains = nvc["nameContains"];
// BL with nameContains here
}

Categories