Return CreatedAtRoute location from ODataController - c#

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})"

Related

Issue using .Net Core 2.2 LinkGenerator

In my controller.Post I am using a link generator to return the endpoint of the newly added resource to the user. The link generator always returns null and I cant quite put my finger on why?
I included controller code that is adding to the database. Note the line calling _linkGenerator.GetPathByAction - the parameters passed in are (in order) 1. the action (in this case the name of the method that does the 'Get'), 2, the controller name and 3 an anonymous type containing the correct Id. I honestly cant see a problem with this.
how the controller name is defined :
namespace CarPriceComparison.Controllers.Api{
[Route("api/vehicles")]
[ApiController]
public class VehicleController : Controller
'Get' Action - function header :-
[HttpGet("{vehicleId_:int}")]
public IActionResult GetVehicle(int vehicleId_)
{
'problem' code that does the get. To me, the parameters passed into _linkGenerator.GetPathByAction look perfectly fine. Suggestions as to why I am always returned a null value?
[HttpPost("")]
public async Task<IActionResult> PostNewVehicleData(VehicleViewModel vehicleData_)
{
try
{
if (ModelState.IsValid)
{
var newVehicle = _mapper.Map<Vehicle>(vehicleData_);
_vehicleRepository.AddVehicle(newVehicle);
var location = _linkGenerator.GetPathByAction("GetVehicle", "vehicles",
new {vehicleId_ = newVehicle.Id});
if (string.IsNullOrWhiteSpace(location))
{
return BadRequest("could not use vehicleId_ to create a new vehicle in the dataase");
}
if (await _vehicleRepository.SaveChangesAsync())
{
var vehicleModel = _mapper.Map<VehicleViewModel>(newVehicle);
return Created(location, _mapper.Map<VehicleViewModel>(newVehicle));
}
}
return BadRequest("Failed to save the vehicle");
}
catch(Exception ex)
{
return BadRequest($"Exception Thrown : {ex}");
}
}
You have to try this:
var location = _linkGenerator.GetPathByAction("GetVehicle", "Vehicle",
new {vehicleId_ = newVehicle.Id});
Or remane your controller to VehiclesController. Then try this:
var location = _linkGenerator.GetPathByAction("GetVehicle", "Vehicles",
new {vehicleId_ = newVehicle.Id});

How to use a parameter in Web API v2?

I have no idea where to start with this. I asked a question previously, and someone suggested I look at attribute routing. I read up on it, and while it helped me to create the below code, I'm still not sure how to limit it like I want to.
public class ReviewController : ApiController
{
private Review db = new Review();
////This GET works. It was auto-generated by Visual Studio.
// GET: api/Review
public IQueryable<Review> GetReview()
{
return db.Review;
}
////This is the GET that I'm trying to write, but don't know what to do
// GET: api
[Route("api/review/site")]
[HttpGet]
public IEnumerable<Review> FindStoreBySite(int SiteID)
{
return db.Review
}
////This GET also works and was generated by VS.
// GET: api/Review/5
[ResponseType(typeof(Review))]
public IHttpActionResult GetReview(int id)
{
Review review = db.Review.Find(id);
if (review == null)
{
return NotFound();
}
return Ok(review);
}
Essentially what I'm aiming to do is to limit what's returned by the API to only the results where the SiteID is equal to whatever value is passed into the URL. I'm not even sure where to get started with this, and googling/searching stack overflow for "what to put in web api return" has been fruitless.
How do I tell the API what I want to have returned based off a parameter besides ReviewID?
Edit: I've updated the code per the suggestions in the answer below, but now I'm getting a new error.
Here's the current code:
private ReviewAPIModel db = new ReviewAPIModel();
// GET: api/Review
[Route("api/Review")]
[HttpGet]
public IQueryable<Review> GetReview()
{
return db.Review;
}
// GET: api
[Route("api/Review/site/{siteid}")]
[HttpGet]
public IEnumerable<Review> FindStoreBySite(int siteid)
{
return db.Review.Where(Review => Review.SiteID == siteid);
}
// GET: api/Review/5
[ResponseType(typeof(Review))]
public IHttpActionResult GetReview(int id)
{
Review review = db.Review.Find(id);
if (review == null)
{
return NotFound();
}
return Ok(review);
}
}
Here's the error that I get:
Multiple actions were found that match the request
When I google it, it takes me to this question: Multiple actions were found that match the request in Web Api
However, I've tried the answers there (I've confirmed that I'm using Web API V2, and my webapiconfig.cs file includes the config.MapHttpAttributeRoutes(); line.
In addition, as you can see in my code above, I've included the appropriate routing. However, I'm still getting an error telling me that it's returning two conflicting API calls.
To pass parameters to a WebApi controller you need to add a [Route()] attribute to that controller and mark the part of the link that's used as the attribute with this {}.
To return reviews that only match the passed in parameter you need to use LINQ to filter the data.
Here is an example:
[Route("api/getFoo/{id}")]
public IHttpActionResult GetFoo(int id)
{
return db.Foo.Where(x => x.Id == id);
}
The {id} part of the string represents the id that will be in the url in your browser: http://localhost:51361/api/getFoo/2. The "2" in the url IS the {id} property that you marked in your [Route("api/getFoo/{id}")] attribute.
I also modified your code:
public class ReviewController : ApiController
{
...
[Route("api/review/site/{siteId}")]
[HttpGet]
public IEnumerable<Review> FindStoreBySite(int SiteID)
{
return db.Review.Where(review => review.Id == SiteID);
}
...
Your request url should look somewhat like this: http://localhost:51361/api/review/site?SiteID=2
This can be difficult to wrap your head around at first but you'll get used to it eventually. It's how arguments are passed to Controller Action parameters.
if you want to get parameters for GET, it's like a simple overload, but if it's done, POST is with [fromBody], because the URL is in the tag [Route ("/abc/123/{id}")]
example
code
[Route ("/abc/123/{idSite}")]
[HttpGet]
public HttpResponseMessage ControllerIdSite(int IdSite){
//CODE . . .
return Request.CreateResponse<int>(HttpStatusCode.OK, IdSite);
}
call
/abc/123/17
return
17
OR
[Route ("/abc/123")]
[HttpGet]
public HttpResponseMessage ControllerIdSite(int IdSite){
//CODE . . .
return Request.CreateResponse<int>(HttpStatusCode.OK, IdSite);
}
call
/abc/123?IdSite=17
return
17

Why is my attribute being fired on all actions, including ones that don't have the attribute?

I have a controller in my web api. Let's call it TimeController.
I have a GET action and a PUT action. They look like this:
public class TimeController : ApiController
{
[HttpGet]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.OK, DateTime.UtcNow);
}
[HttpPut]
public HttpResponseMessage Put(int id)
{
_service.Update(id);
return Request.CreateResponse(HttpStatusCode.OK);
}
}
I also have a route config as follows:
routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional });
so I can access it in a restful manner.
Now I also want to version the GET action using a custom Route attribute. I'm using code very similar to what Richard Tasker talks about in this blog post.
(the difference being that I use a regular expression to get the version from the accept header. Everything else is pretty much the same)
So my controller now looks like this:
public class TimeController : ApiController
{
private IService _service;
public TimeController(IService service)
{
_service = service;
}
[HttpGet, RouteVersion("Time", 1)]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.Ok, DateTime.UtcNow);
}
[HttpGet, RouteVersion("Time", 2)]
public HttpResponseMessage GetV2()
{
return Request.CreateResponse(HttpStatusCode.Ok, DateTime.UtcNow.AddDays(1));
}
[HttpPut]
public HttpResponseMessage Put(int id)
{
_service.Update(id);
return Request.CreateResponse(HttpStatusCode.OK);
}
}
However, now when I try to access the PUT endpoint I'm getting a 404 response from the server. If I step through the code in debug mode, I can see that the RouteVersion attribute is being fired, even though I haven't decorated the action with it.
If I add the attribute to the PUT action with a version of 1, or I add the built in Route attribute like this: Route("Time") then it works.
So my question is: why is the attribute firing even though I haven't decorated the action with it?
Edit: Here is the code for the attribute:
public class RouteVersion : RouteFactoryAttribute
{
private readonly int _allowedVersion;
public RouteVersion(string template, int allowedVersion) : base(template)
{
_allowedVersion = allowedVersion;
}
public override IDictionary<string, object> Constraints
{
get
{
return new HttpRouteValueDictionary
{
{"version", new VersionConstraint(_allowedVersion)}
};
}
}
}
public class VersionConstraint : IHttpRouteConstraint
{
private const int DefaultVersion = 1;
private readonly int _allowedVersion;
public VersionConstraint(int allowedVersion)
{
_allowedVersion = allowedVersion;
}
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
if (routeDirection != HttpRouteDirection.UriResolution)
{
return true;
}
int version = GetVersionFromHeader(request) ?? DefaultVersion;
return (version == _allowedVersion);
}
private int? GetVersionFromHeader(HttpRequestMessage request)
{
System.Net.Http.Headers.HttpHeaderValueCollection<System.Net.Http.Headers.MediaTypeWithQualityHeaderValue> acceptHeader = request.Headers.Accept;
var regularExpression = new Regex(#"application\/vnd\.\.v([0-9]+)",
RegexOptions.IgnoreCase);
foreach (var mime in acceptHeader)
{
Match match = regularExpression.Match(mime.MediaType);
if (match.Success)
{
return Convert.ToInt32(match.Groups[1].Value);
}
}
return null;
}
}
Edit2: I think there is some confusion so I've updated the Put action to match the route config
So my question is: why is the attribute firing even though I haven't decorated the action with it?
It is clear from both the way your question is phrased "when I try to access the PUT endpoint" and the fact that it matches the GET action (and then subsequently runs its constraint) that you have not issued a PUT request to the server. Most browsers are not capable of issuing a PUT request, you need a piece of code or script to do that.
Example
using (var client = new System.Net.WebClient())
{
// The byte array is the data you are posting to the server
client.UploadData(#"http://example.com/time/123", "PUT", new byte[0]);
}
Reference: How to make a HTTP PUT request?
I think its because of your action signature in combination with the default route
In your default route you specify the Id attribute as optional, however in your action you use the parameter days, in this case the framework can't resolve it. you either have to add it as a query string parameter eg:
?days={days}
Or change the signature to accept id as input.
Since it can't resove the action with days in the url it will return a 404
Personally i don't use the default routes and always use Attribute routing to prevent this kinda behavior
So my question is: why is the attribute firing even though I haven't decorated the action with it?
Any controller methods that do not have a route attribute use convention-based routing. That way, you can combine both types of routing in the same project.
Please see this link :
attribute-routing-in-web-api-2
Also as method is not decorated with route attribute, When the Web API framework receives an HTTP request, it tries to match the URI against one of the route templates in the routing table. If no route matches, the client receives a 404 error. That is why you are getting 404
Please see this one as well : Routing in ASP.NET Web API

Can anyone explain CreatedAtRoute() to me?

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

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