I've read a few SO posts and none of them quite cover my scenario so I'm going to post here.
Given the following route config registration:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
and these controller actions in a controller that inherits from ApiController:
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request)
{
}
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request)
{
}
public class GetDocumentsRequest
{
public string CorrelationId { get; set; }
public int Id { get; set; }
public string ObjectId { get; set; }
public string BusinessArea { get; set; }
public string UserId { get; set; }
public string SystemName { get; set; }
public string SystemToken { get; set; }
public Letter LetterDetails { get; set; }
public List<KeyValuePair<string, string>> KeyValue { get; set; }
}
public class FinishDocumentsRequest
{
public string CorrelationId { get; set; }
public string[] Documents { get; set; }
}
I thought doing it this way would be enough disambiguation for the IHttpActionSelector to correctly choose the route, but unfortunately it is not.
So my questions is "Is there a way to make this code work correctly, and keep it in the same controller?"
Thank you,
Stephen
You could use attribute routing for this.
Define the route as a string in the Route attribute ontop of the methods as this
[Route("api/controller/Post1")]
[HttpPost]
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request)
{
}
[Route("api/controller/Post2")]
[HttpPost]
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request)
{
}
The request routing pipeline isn't smart enough to determine if the body of the request matches the parameter type (aka overloading). (The compiler is smart enough, which is why this compiles and you have runtime issues.)
You have a couple of different options.
You can either add an [Route(<ActionName>)] attribute on both of your posts.
Make two controllers, one for GetDocuments and one for FinishDocuments
Make one Post method that is ambiguous. (I'd avoid this)
If you choose option 1, your API uri will have to be .../api/MyController/MyActionName rather than .../api/MyController/. It's also advisable to add [HttpGet] and [HttpPost] attributes on your methods.
Sample:
public class DocumentController : ApiController
{
// POST /api/Document/GetDocuments
[HttpPost]
[Route("GetDocuments")]
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request) { ... }
// POST /api/Document/FinishDocuments
[HttpPost]
[Route("FinishDocuments")]
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request){ ...}
}
If you choose option 2, you have to maintain an additional code file.
public class GetDocumentsController : ApiController
{
// POST /api/GetDocuments
[HttpPost]
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request) { ... }
}
public class FinishDocumentsController : ApiController
{
// POST /api/FinishDocuments/
[HttpPost]
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request){ ...}
}
If you choose option 3, may God have mercy on your soul you're going to have a bad time maintaining it.
Add the Route attribute decoration to your web api functions and that will assit the selector to choose the route:
[Route("Post1")]
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request)
{
}
[Route("Post2")]
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request)
{
}
I also recommend adding the http method decoration such as [HttpPost] or [HttpGet]
Related
Framework used is .Net Core 3.0 but tested in 2.2 and got the same behavior.
I am using a class to automatically bind the body request properties and that works pretty well, even without having the [FromBody] attribute on them.
Now, I added a new property in this class that will match a property from the header and it works if I use it directly into the controller, like this:
public IActionResult Test(TestRequest request, [FromHeader(Name = "Authorization")] string token)
However, when I try to get the same result by adding the [FromHeader] attribute into the class property, it doesn't work.
Here is a sample code to illustrate the issue:
[ApiController]
[Route("api")]
public class TestController : ControllerBase
{
[HttpPost]
[Route("Test")]
public IActionResult Test(TestRequest request)
{
Console.WriteLine("request.UserId: " + request.UserId);
Console.WriteLine("request.Token: " + request.Token);
return Ok();
}
}
public class TestRequest
{
[FromBody]
public string UserId { get; set; }
[FromHeader(Name = "Authorization")]
public string Token { get; set; }
}
Did anybody ever face the same issue?
You need to configure SuppressInferBindingSourcesForParameters as true in ConfigureServices in Startup.cs like below :
services.AddMvc().ConfigureApiBehaviorOptions(options =>
{
options.SuppressInferBindingSourcesForParameters = true;
});
Action:
[HttpPost]
[Route("Test")]
public IActionResult Test(TestRequest request)
And call the api with your Authorization header(not shown below) and body string, for postman
Update:
Since you use [FromBody] on the string property,it accepts a string instead of json object.
If you still would like to pass json object as { "userId" : "123" }, you could warp the userId into a model,for example:
public class User
{
public string UserId { get; set; }
}
public class TestRequest
{
[FromBody]
public User User { get; set; }
[FromHeader(Name = "Authorization")]
public string Token { get; set; }
}
I have this controller and action method:
[ApiController]
[Route("api/[controller]")]
public class AppointmentController : ControllerBase
{
[Route("{provider}/AvailableSlots")]
[HttpGet]
public Task<AvailableSlotsResponse> GetAvailableSlots(Request<AvailableSlotsRequest> request)
{
return null;
}
}
Here's the model:
public class Request<T> where T : class
{
[FromRoute]
public string Provider { get; set; }
[FromQuery(Name = "")]
public T Model { get; set; }
}
public class AvailableSlotsRequest
{
//[FromQuery(Name = "Location")] //Would prefer not to have to use this
public string Location { get; set; }
}
I need to use Location as the query param name in the URL in order to hit the endpoint, as expected.
eg. http://localhost/api/Appointment/Company/AvailableSlots?Location=SYD
However, when I view the Swagger page, the parameter is called Model.Location which is confusing for consumers of my API:
I can use [FromQuery(Name = "Location")] to force Swagger to display Location, however this feels very redundant and duplicates the property name.
Here is my Swagger set up in ConfigureServices():
services.AddSwaggerDocument(document =>
{
document.PostProcess = d =>
{
d.Info.Version = Configuration["APIVersion"];
d.Info.Title = $"{Configuration["ApplicationName"]} {Configuration["DomainName"]} API";
};
});
How can I make Swagger display Location instead of Model.Location, without having to duplicate the word "Location" in the [FromQuery] attribute?
Add to the controller parameter the attribute [FromRoute]:
public Task<AvailableSlotsResponse> GetAvailableSlots([FromRoute]Request<AvailableSlotsRequest> request)
Remove the attribute FromQuery in the Model property and uncomment the attribute FromQuery from de Location Property.
Unfortunately I had to use [FromQuery(Name = "<PropertyName>")].
However I found a better way:
[ApiController]
[Route("api/[controller]")]
public class AppointmentController : ControllerBase
{
[Route("{provider}/AvailableSlots")]
[HttpGet]
public Task<AvailableSlotsResponse> GetAvailableSlots(AvailableSlotsRequest request)
{
return null;
}
}
public class Request
{
[FromRoute]
public string ProviderName { get; set; }
}
public class AvailableSlotsRequest : Request
{
[FromQuery]
public string Location { get; set; }
}
This also means the model can use any attribute, compared to my first attempt where the T Model was decorated with [FromQuery]
And I wrote custom action names for ever process.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
In webapi config I wrote like this and in controller, I started like below
[System.Web.Http.HttpPost]
public HttpResponseMessage Control(List<string>codes,string updateperson,string refPeriod, int actualyear, int actualmonth)
{
For get methods, everything works well but for post method it doesn't work and gives error like below.
In body in post I send
{codes: ["CPM2018-004"], updateperson: "E3852", refPeriod: "SFC18", actualyear: 2018, actualmonth: 12}
Request URL:http://localhost:50941/Api/Investment/Control Request
Method:POST Status Code:404 Not Found Remote Address:[::1]:50941
Referrer Policy:no-referrer-when-downgrade
How can I sreceive post requests to web API with custom action name?
Create model to hold value being posted.
public class ControlViewModel {
public List<string> codes { get; set; }
public string updateperson { get; set; }
public string refPeriod { get; set; }
public int actualyear { get; set; }
public int actualmonth { get; set; }
}
And then update the action to expect the data in the BODY of the request
public class InvestmentController : ApiController {
[HttpPost]
public HttpResponseMessage Control([FromBody]ControlViewModel data) {
//...
}
//...
}
Reference Parameter Binding in ASP.NET Web API
In my WebAPI I have model
public class ListRequest
{
public int Skip { get; set; } = 0;
public int Take { get; set; } = 30;
}
My action is
[HttpGet]
[Route("api/users")]
public IHttpActionResult Get([FromUri] ListRequest request) {
...
}
I need to have possibility to not pass any query parameters, then default values should be used. But, when I go to http://localhost:44514/api/users the request is null. If I remove [Route("api/users")] then request is not null and has default values for parameters.
How can I reach that behavior with Route attribute?
If you want to init model using Route attributes try
Route("api/users/{*pathvalue}")]
Create your method on post request basis. Get type always receive null value.
[HttpGet]
[Route("api/users")]
public IHttpActionResult Get([FromUri] ListRequest request) {
}
Change to
[HttpPost]
[Route("api/users")]
public IHttpActionResult Get([FromUri] ListRequest request) {
...
}
Because Model (Class) type parameter does not support get type request.
Hope it will help.
Use data annotation. For more information visit Default value in mvc model using data annotation
Change
public class ListRequest
{
public int Skip { get; set; } = 0;
public int Take { get; set; } = 30;
}
To
public class ListRequest
{
[DefaultValue(0)]
public int Skip { get; set; }
[DefaultValue(30)]
public int Take { get; set; }
}
It works without removing [Route("api/users")] and request will not be null.
What's the right way to POST an Entity with a SPATIAL PROPERTY on ASP.NET Web API OData (v4)?
Serialization on GET works fine, but everything I try in the POST causes the model to go null.
Is POST supported at all?
Thanks in advance.
public class PlacesController : ODataController
{
[HttpGet]
[EnableQuery]
public virtual async Task<IHttpActionResult> Get([FromODataUri] string key)
{
var place = new Place()
{
Id = Guid.NewGuid().ToString(),
Location = GeographyPoint.Create(1, 1)
};
return Ok(place);
}
[HttpPost]
[EnableQuery]
public virtual async Task<IHttpActionResult> Post(Place place)
{
if (place == null)
{
return BadRequest();
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return Created(place);
}
}
[DataContract]
public class Place
{
[DataMember]
public string Id
{
get;
set;
}
[DataMember]
public GeographyPoint Location
{
get;
set;
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
var edmBuilder = new ODataConventionModelBuilder();
edmBuilder.EntitySet<Place>("Places");
var model = edmBuilder.GetEdmModel();
config.MapODataServiceRoute(routeName: "ODataRoute", routePrefix: "api", model: model);
}
}
After reading through below mentioned article it seems possible. You will need to use oDataActionParameters type as input to the post and later in your method cast it to the required type. Mehtod signature will something like below:
public async Task<IHttpActionResult> RateProduct(ODataActionParameters parameters)
check this link for examples and deep dive on this