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
Related
I created ASP.net core 3.0 web API application and added HttpPost endpoint to it.
When I post using postman to this post endpoint, the endpoint doesn't get the JSON I pass to it and instead gets null.
Is there something that has changed in .NET Core 3.0 that has changed/broken HTTP post endpoints?
The JSON I posted:
{
"status": "0",
"operation":"",
"filter":"",
"currentOrderList": [
]
}
Controller code:
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET: api/<controller>
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/<controller>/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/<controller>
[HttpPost]
public void Post([FromBody]string value)
{
}
// PUT api/<controller>/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
The url I am posting to is https://localhost:44336/api/values. I can see that the endpoint is being hit by the fact that the method is being hit during debugging in visual studio. The only issue is that the parameter is coming in as null
Create a model to match the given data
public class MyClass {
[JsonProperty("status")]
public int Status { get; set; }
[JsonProperty("operation")]
public string Operation { get; set; }
[JsonProperty("filter")]
public string Filter { get; set; }
[JsonProperty("currentOrderList")]
public string[] CurrentOrderList { get; set; }
}
Then update the controller action to expect the desired type
//POST api/values
[HttpPost]
public IActionResult Post([FromBody]MyClass value) {
if(ModelState.IsValid) {
//...
return Ok();
}
return BadRequest();
}
In addition to adding the [JsonProperty] annotations as Nkosi said, I also had to add the nuget package
Microsoft.AspNetCore.Mvc.NewtonsoftJson
and append .AddNewtonsoftJson() into Startup.cs. I found that in another stackoverflow question, but neither change by itself was enough to get my models to hydrate. It took both to get it working.
services.AddMvc().AddRazorRuntimeCompilation().AddNewtonsoftJson();
I'm creating a web api with ASP.NET Core and I've encountered a problem. I have a post request and I want to pass a Municipality object to it with JSON format. The problem is that I have property Name with attribute [Required]. I call the endpoint by using Postman with this JSON payload {"Name": "London"} and when validating the model, it says "The Name field is required." even though it was definitely provided.
I've tried using [FromBody] attribute, but the problem with it is that it doesn't give me validation errors and only says, that "input was invalid" and gives a null object, so not using this attribute gives a lot better errors.
Github: https://github.com/DeividasBrazenas/Taxes/blob/master/Taxes/Taxes/Controllers/BaseController.cs
BaseModel.cs
public class BaseModel
{
public int Id { get; set; }
}
Municipality.cs
public class Municipality : BaseModel
{
[Required]
public string Name { get; set; }
public ICollection<Tax> Taxes { get; set; }
}
MunicipalitiesController.cs
[EnableQuery]
public async Task<IActionResult> Post(Municipality baseObject)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await Context.Set<Municipality>().AddAsync(baseObject);
await Context.SaveChangesAsync();
return Created(baseObject);
}
Screenshot of POST request -
Make changes below for your current MunicipalitiesController
Add public async Task<IActionResult> Post(Municipality baseObject) with FromBody
[EnableQuery]
[HttpPost]
public async Task<IActionResult> Post([FromBody]Municipality baseObject)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await Context.Set<Municipality>().AddAsync(baseObject);
await Context.SaveChangesAsync();
return Created(baseObject);
}
Change the json request to lowercase.
{
"name":"1231"
}
I am migrating an application from legacy asp.net webapi to asp.net core mvc. I have noticed an issue. For some requests, we send partial or even invalid values in the POST body. And asp.net core is refusing to deserialize it.
E.g. post model
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
public enum Category
{
Public,
Personal
}
action
[HttpPost]
public async Task<Response> Post([FromBody]PostModel model)
=> this.Service.Execute(model);
for the following sample request
POST /endpoint
{
id: 3,
category: "all"
}
The ModelState collection records an error - indicating that all is an invalid category, and the PostModel argument model is null. Is it possible to disable this behaviour and just attempt to bind all properties that are possible from the post body, and ignoring the ones it can't bind? This is how it was done for us in our legacy api and for now, I need to port this across.
Disabling the model validation did not help for us. The model argument is still null.
For FromBody, it will bind the request body to Model by JsonInputFormatter.
For JsonInputFormatter, it will call return InputFormatterResult.Success(model) when there is no error, and call return InputFormatterResult.Failure(); when there is any error. For return InputFormatterResult.Failure();, it will not bind the valid property.
For a solution, you could implement custom formatter to return return InputFormatterResult.Success(model).
Implement custom formatter CustomFormatter based on JsonInputFormatter.
Replace InputFormatterResult.Failure() with InputFormatterResult.Success(model).
if (!(exception is JsonException || exception is OverflowException))
{
var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception);
exceptionDispatchInfo.Throw();
}
return InputFormatterResult.Success(model);
Inject CustomFormatter in Startup.cs
services.AddMvc(o =>
{
var serviceProvider = services.BuildServiceProvider();
var customJsonInputFormatter = new CustomFormatter(
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<CustomFormatter>(),
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value.SerializerSettings,
serviceProvider.GetRequiredService<ArrayPool<char>>(),
serviceProvider.GetRequiredService<ObjectPoolProvider>(),
o,
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value
);
o.InputFormatters.Insert(0, customJsonInputFormatter);
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
Actually, your problem is related to Data Binding, not to validation, that's why disabling the model validation did not help. You can implement custom Binder and configure it to manually bind your properties, e.g.:
public class PostModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
string valueFromBody = string.Empty;
using (var sr = new StreamReader(bindingContext.HttpContext.Request.Body))
{
valueFromBody = sr.ReadToEnd();
}
if (string.IsNullOrEmpty(valueFromBody))
{
return Task.CompletedTask;
}
string idString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["id"]).Value);
string categoryString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["category"]).Value);
if (string.IsNullOrEmpty(idString) || !int.TryParse(idString, out int id))
{
return Task.CompletedTask;
}
Category? category = null;
if(Enum.TryParse(categoryString, out Category parsedCategory))
{
category = parsedCategory;
}
bindingContext.Result = ModelBindingResult.Success(new PostModel()
{
Id = id,
Category = category
});
return Task.CompletedTask;
}
}
Then you can apply this binder to your class:
[ModelBinder(BinderType = typeof(PostModelBinder))]
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
or to action:
[HttpPost]
public async Task<Response> Post([ModelBinder(BinderType = typeof(PostModelBinder))][FromBody]PostModel model)
=> this.Service.Execute(model);
or create CustomModelBinderProvider:
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(PostModel))
return new PostModelBinder();
return null;
}
}
and register it in ConfigureServices methods of Startup class:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc(
config => config.ModelBinderProviders.Insert(0, new CustomModelBinderProvider())
);
...
}
No you can't since the property tied to an enum. if you really want to be what you posted then change the model to be
public class PostModel
{
public int Id { get; set; }
public string Category { get; set; }
}
Then in your endpoint parse the string to enum like
Enum.TryParse("All", out Category cat);
I has a Controller that serves up Events. The Controller provides the route GET => /Events that will return an array of Events.
This Controller can serve Events to 3 different types of authentication. Admin, Api, and User.
If the requester is Authenticated as a User I want to return the Event object but scoped to the user. e.g.
class Event {
public string Title { get; set; }
}
class EventView {
public string Title { get; set; }
public bool RSVPed { get; set; }
}
How can I make this possible in my Controller-
[RoutePrefix("Events")]
class EventsController {
[#Authorize(AuthenticationType.Admin, AuthenticationType.Api)]
[HttpGet]
[Route("")]
public async Task<IHttpActionResult> Get() { }
[#Authorize(AuthenticationType.User)]
[HttpGet]
[Route("")]
public async Task<IHttpActionResult> Get() { }
}
Have one action with all the allowed permissions. within the action you perform desired behavior based on authorized principal.
[RoutePrefix("Events")]
public class EventsController : ApiController {
[Authorize(AuthenticationType.Admin, AuthenticationType.Api, AuthenticationType.User)]
[HttpGet]
[Route("")] //Matches GET => /Events
public async Task<IHttpActionResult> Get() {
var user = User.Identity;
if(user.AuthenticationType == AuthenticationType.User) {
//...User specific code
} else {
//...Admin, API specific code
}
}
}
Other wise you have to make the routes unique so as not to conflict with each other.
[RoutePrefix("Events")]
public class EventsController : ApiController {
[Authorize(AuthenticationType.Admin, AuthenticationType.Api)]
[HttpGet]
[Route("")] //Matches GET => /Events
public async Task<IHttpActionResult> Get() {
//...
}
[Authorize(AuthenticationType.User)]
[HttpGet]
[Route("{id:int}")] //Matches GET => /Events/12345
public async Task<IHttpActionResult> Get(int id) {
//...
}
}
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]