Why is my OData POST body to model not working? - c#

I am using OData v4 in an ASP.NET application. Every time I use Postman to post to my endpoint, I get the following error. The issue is with this line public IHttpActionResult Post([FromBody] HolidayPost data) because the function will run when I remove [FromBody] HolidayPost data. I cannot seem to get my POST body to align with the HolidayPost model. I am not sure what I am doing wrong.
Message
"No MediaTypeFormatter is available to read an object of type 'HolidayPost' from content with media type 'application/json'.",
type
System.Net.Http.UnsupportedMediaTypeException
stacktrace
" at System.Net.Http.HttpContentExtensions.ReadAsAsync[T](HttpContent content, Type type, IEnumerable1 formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)\r\n at System.Web.Http.ModelBinding.FormatterParameterBinding.ReadContentAsync(HttpRequestMessage request, Type type, IEnumerable1 formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)"
My Postman POST request
URL: http://localhost/PortOData4/Holidays
Headers: Content-Type: application/json
Body:
{
"Dates": "[\"2022-01-20T00:00:00.000-06:00\",\"2022-02-20T00:00:00.000-06:00\",\"2022-03-23T00:00:00.000-05:00\"]",
"apikey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
WebApiConfig.cs
namespace PortalogicOData
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.EnableCors();
// Create OData entities
ODataModelBuilder builder = new ODataConventionModelBuilder();
config.Count().Filter().OrderBy().Expand().Select().MaxTop(null); // enable OData Model Bound Attributes
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
builder.EntitySet<Holiday>("Holidays");
// map entities
config.MapODataServiceRoute("ODataRoute", null, builder.GetEdmModel());
}
}
}
HolidaysController.cs
namespace PortalogicOData.Controllers
{
[EnableCors("*", "*", "*")]
public class HolidaysController : ODataController
{
private readonly HolidayService m_service = new HolidayService();
[EnableQuery]
public IList<Holiday> Get([FromUri] string apikey)
{
if (GenericHelper.ValidateApi(apikey))
{
return m_service.Getdata(); // this works fine
}
else
{
return null;
}
}
[HttpPost]
public IHttpActionResult Post([FromBody] HolidayPost data)
{
// this function fails to execute everytime
}
}
}
HolidaysService.cs
namespace PortalogicOData.Business
{
public class HolidayService
{
public List<Holiday> Holidays => Getdata();
public List<Holiday> Getdata()
{
// this works fine
}
public Holiday Update(HolidayPost data)
{
// my update code
}
}
}
HolidayPost.cs
namespace PortalogicOData.Models
{
public class HolidayPost
{
public string apikey { get; set; }
public string Dates { get; set; }
}
}
Holiday.cs
namespace PortalogicOData.Models
{
public class Holiday
{
public int HolidayID { get; set; }
public string FunctionResult { get; set; }
public int ReturnCode { get; set; }
public DateTime Date { get; set; }
}
}

Apparently, OData ASP.NET forces you to only allow the body in the format of route Type (e.g. Holiday not HolidayPost). I had to create a Generic class path used for posts instead. That is easier than trying to figure out how to use OData actions and functions.

Related

JsonExtensionData attribute with default DotNetCore binder

I am trying to pass JSON with some dynamic fields to a controller action method in DotNetCore 3.1 Web API project. The class I am using when sending the payload looks something like this:
public class MyDynamicJsonPayload
{
public string Id { get; set; }
[JsonExtensionData]
public IDictionary<string, object> CustomProps { get; set; }
}
I can see that object serialized correctly with props added to the body of JSON. So I send it from one service to another:
using var response = await _httpClient.PostAsync($"/createpayload", new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json"));
On the receiving end, however, when using same class in the controller action:
public async Task<ActionResult> CreatePayload([FromBody] MyDynamicJsonPayload payload)
{
var payload = JsonConvert.SerializeObject(payload);
return Ok(payload);
}
The object is parsed as something different where customProps is an actual field with JSON object containing my properties, plus instead of a simple value I get a JSON object {"valueKind":"string"} for string properties for example. I tried with both Newtonsoft.Json and System.Text.Json.Serialization nothing works as expected. Anyone has any ideas?
Thank you dbc for pointing me in the right direction, the problem was Newtownsoft vs System.Text.Json serialization/deserialization. I could not change the serializer in the Startup class because the service had many other methods and I didn't want to break existing contracts. However, I managed to write a custom model binder that did the trick:
public class NewtonsoftModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
string body = string.Empty;
using (var reader = new StreamReader(bindingContext.HttpContext.Request.Body))
{
body = await reader.ReadToEndAsync();
}
var result = JsonConvert.DeserializeObject(body, bindingContext.ModelType);
bindingContext.Result = ModelBindingResult.Success(result);
}
}
And usage:
public async Task<ActionResult> CreatePayload([ModelBinder(typeof(NewtonsoftModelBinder))] MyDynamicJsonPayload payload)
{
// Process payload...
return Ok();
}
First of all welcome to the StackOverflow.
You can try this solution;
Fist create a class like you did but without dictionary;
public class History
{
public int Id { get; set; }
public string DeviceName { get; set; }
public int DeviceId { get; set; }
public string AssetName { get; set; }
}
After that please add this attribute to your controller class;
[Produces("application/json")]
Your method should be like this;
[Produces("application/json")]
public class ExampleController: Controller
{
[HttpGet]
public Task<IEnumerable<History>> Get()
{
List<History> historyList = new List<History>()
{
new History()
{
...
},
new History()
{
...
}
}
return historyList;
}
}

Manually invoking modelstate validation fails in nested classes

So, I have a class
public class Inventory
{
[Required]
public Routing Routing { get; set; }
[Required]
public List<Items> Items { get; set; }
}
And, Routing and Items are individual classes with their own validation parameters.
public class Routing
{
[Required]
public string SenderId { get; set; }
[Required]
public string ReceiverId { get; set; }
public string PartnerId { get; set; }
[Required]
public string MessageType { get; set; }
}
Now, I was using model validation in web API, it was working just fine.
public async Task<IActionResult> Post([FromBody] Inventory request, [FromQuery(Name = "CorrelationId")] string correlationId)
{
....
// Working just fine, validating the incoming request schema as defined by Inventory class
}
If user/consumer sends POST request with the wrong schema, it fails at HTTP level and shows 400 BAD Request, and control does not even come inside the Post method body. I want the control to come inside Post method.
Now, I am doing manual validation
public async Task<IActionResult> Post([FromBody] string request, [FromQuery(Name = "CorrelationId")] string correlationId)
{
Inventory obj = JsonConvert.DeserializeObject<Inventory>(request);
var context = new ValidationContext(obj, serviceProvider: null, items: null);
var validationResults = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(obj, context, validationResults);
if (!isValid)
{
// Valid even if I omit some parameters of nested classes Routing or Items
foreach (var validationResult in validationResults)
{
Console.WriteLine(validationResult.ErrorMessage);
}
}
}
Now, if I do, say, omit a SenderId from Routing class, it is showing Valid in the above manual validation.
What I want:- Schema/Model validation inside the Post method, and if invalid, list of all errors inside the Post method.
if you really want that design then you have to
in your client you need to pass it as a text/plain. example:
POST /api/values?correlationId=123 HTTP/1.1 Host: localhost:5551
Content-Type: text/plain cache-control: no-cache Postman-Token:
b766b3d6-9478-43b1-b49c-2677e0b08dec { "routing": { "senderId":
"123", "receiverId": "456", "partnerId": "777", "messageType":
"888" }, "items": [] }
in your asp.net core you need to accept text/plain
public class TextPlainInputFormatter : TextInputFormatter
{
public TextPlainInputFormatter()
{
SupportedMediaTypes.Add("text/plain");
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
}
protected override bool CanReadType(Type type)
{
return type == typeof(string);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
string data = null;
using (var streamReader = context.ReaderFactory(context.HttpContext.Request.Body, encoding))
{
data = await streamReader.ReadToEndAsync();
}
return InputFormatterResult.Success(data);
}
}
add the TextPlainInputFormatter:
services.AddMvc(options => { options.InputFormatters.Add(new TextPlainInputFormatter()); });

ASP.NET Core FromBody Model Binding: Bind a Class with Interafece Field

I've been struggling a lot with that, I found some questions but none could answer my needs. I will try to post a better question and some of the things I tried.
Here is the situation:
I have an APIGateway and a WebApp. The WebApp sends POST requests to the APIGateway, so far so good. I use the FromBody attribute to send larger objects, and that was fine too until I introduced interfaces :))
Here's some code:
WebApp:
public interface ICommand
{
Guid CorrelationId { get; set; }
Guid SocketId { get; set; }
}
public class Command : ICommand
{
public Command(Guid CorrelationId, Guid SocketId)
{
this.CorrelationId = CorrelationId;
this.SocketId = SocketId;
}
public Guid CorrelationId { get; set; } = new Guid();
public Guid SocketId { get; set; } = new Guid();
}
public interface IDocument
{
Guid Id { get; set; }
ulong Number { get; set; }
}
public class Document : IDocument
{
public Guid Id { get; set; } = new Guid();
public ulong Number { get; set; } = 0;
}
public interface ICreateDocumentCommand : ICommand
{
IDocument Document { get; set; }
}
public class CreateDocumentCommand : Command, ICreateDocumentCommand
{
public CreateDocumentCommand(IDocument Document, ICommand Command) : base(Command.CorrelationId, Command.SocketId)
{
this.Document = Document;
}
public IDocument Document { get; set; }
}
APIGateway:
[HttpPost]
public async Task<IActionResult> Create([FromBody]CreateDocumentCommand documentCommand)
{
if (documentCommand == null)
{
return StatusCode(403);
}
return Json(documentCommand.Document.Id);
}
Use case:
public class InventoryList : Document
{
public Guid WarehouseId { get; set; } = new Guid();
}
// Example document class
////////////////////////////////////////
// Example POST Request
ICommand command = new Command(messageId, socketId);
switch (item.GetType().Name)
{
case "InventoryList":
command = new CreateDocumentCommand((InventoryList)item, command);
break;
}
string result = await PostAsync($"{apiGatewayAddress}{item.GetType().BaseType.Name}/Create", command, accessToken);
My POST sending function:
public async Task<string> PostAsync<T>(string uri, T item, string authorizationToken = null, string authorizationMethod = "Bearer")
{
JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item, typeof(T), jsonSerializerSettings), System.Text.Encoding.UTF8, "application/json");
return await _client.SendAsync(requestMessage).Result.Content.ReadAsStringAsync();
}
As you can see I have included TypeNameHandling.All in the JSON serialization settings, the request is sent and the Create in the APIGateway gets called. However the parameter documentCommand is NULL.
I've read this: Asp.Net Core Post FromBody Always Null
This: ASP.NET Core MVC - Model Binding : Bind an interface model using the attribute [FromBody] (BodyModelBinder)
This: Casting interfaces for deserialization in JSON.NET
Tried all kind of magic tricks, created new constructors, marked them with [JSONConstructor], still no success. Also I tried changing the APIGateway Cerate method parameter type to ICreateDocumentCommand and again I got a null. I've been searching some model binding tricks online however I couldn't find anything for binding with FromBody. I also found some solution including DI but I am looking for a simple solution. I hope that we will be able to find one :)
Turns out, passing interfaces or classes with interfaces inside as JSON is not that easy. I added a custom JSONConverter and it works now!

Strict mapping in web Web API (Over-Posting)

I just realized that the mapping between the JSON send from a query and my API is not strict.
I give you more explanations:
Here is my C# POCO
public partial class AddressDto
{
public string AddrId { get; set; }
public string Addr1 { get; set; }
public string Addr2 { get; set; }
public string PostalCode { get; set; }
public string City { get; set; }
public string Country { get; set; }
}
And the REST JSON query
PUT http://Localhost:55328/api/ClientAddr/ADD-2059-S002 HTTP/1.1
content-type: application/json
{
"AddrID": "ADD-2059-S002",
"addr1": "B-1/327",
"addr2": "1ST FLOOR",
"city": "Paris",
"Zip_Code": "78956",
"country": "France",
}
The web client send a PUT with Zip_Code in place of PostalCode. PostalCode is not madatory/required. But Zip_Code does not exist in my DTO.
So in my C# code testing the model state won't help.
public HttpResponseMessage Put(string id, AddressDto address)
{
if (!ModelState.IsValid)
return BadRequest(ModelState); // This wont help
}
How can I raise exception when the client is using something in the JSON that is not existing in my DTO (model) ?
if you need to identify extra columns and handle that as an error you have to extend IModelBinder interface and tell json deserializer to treat extra column as an error and add that error to ModelState. By that way you can check in controller for ModelState.IsValid. Checkout the below Code
CustomModelBinder
public class CustomModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.MissingMemberHandling = MissingMemberHandling.Error;
ObjToPass obj = new ObjToPass();
;
try
{
ObjToPass s =
JsonConvert.DeserializeObject<ObjToPass>(actionContext.Request.Content.ReadAsStringAsync().Result,
settings);
bindingContext.Model = obj;
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError("extraColumn", ex.Message);
}
return true;
}
}
public class CustomerOrderModelBinderProvider : ModelBinderProvider
{
public override IModelBinder GetBinder(System.Web.Http.HttpConfiguration configuration, Type modelType)
{
return new CustomModelBinder();
}
}
Object Class that is passed to webapi
public class ObjToPass
{
public int Id { get; set; }
public string Name { get; set; }
}
Controller
[HttpPost]
public void PostValues([ModelBinder(typeof(CustomerOrderModelBinderProvider))] ObjToPass obj)
{
if(!ModelState.IsValid)
{ }
else
{
}
}
This sample holds good for HttpPut as well.
"Over-Posting": A client can also send more data than you expected. For example:
Here, the JSON includes a property ("Zip_Code") that does not exist in the Address model. In this case, the JSON formatter simply ignores this value. (The XML formatter does the same.)
https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api

WebAPI - Why is the HttpRequestMessage null for some HttpRequests

I have a simple WebAPI controller. I've added the AutoMapper nuget package for mapping between a DataModel type and a corresponding Dto type like so:
namespace WebApi.Controllers
{
public class Contact
{
public int ID { get; set; }
public string Name { get; set; }
}
public class ContactDto
{
public int ID { get; set; }
public string Name { get; set; }
}
public class ValuesController : ApiController
{
public ValuesController()
{
SetupMaps();
}
private void SetupMaps()
{
Mapper.CreateMap<ContactDto, Contact>();
Mapper.CreateMap<Contact, ContactDto>()
.AfterMap((c, d) =>
{
//Need to do some processing here
if (Request == null)
{
throw new ArgumentException("Request was null!");
}
}
);
}
public ContactDto Get(int id)
{
Contact c = new Contact { ID = id, Name = "test" };
ContactDto dto = Mapper.Map<Contact, ContactDto>(c);
return dto;
}
}
}
I'd like to run some logic after the mapping completes and need to use the HttpRequestMessage object in "AfterMap"
When I hit the ValuesController from Fiddler it returns the JSON representation of the Dto as expected.
The fun begins if I issue a bunch of simultaneous requests to simulate load and hit the endpoint;
Some requests succeed and some fail because the "Request" property of the HttpController is null!
Question is why is the Request null?
I have also tried using async controller methods and the behavior is identical:
private async Task<Contact> GetContact(int id)
{
Task<Contact> task = Task.Factory.StartNew<Contact>(
() => new Contact { ID = id, Name = "test" }
);
return await task;
}
public async Task<ContactDto> Get(int id)
{
Contact c = await GetContact(id);
ContactDto dto = Mapper.Map<Contact, ContactDto>(c);
return dto;
}
I've attached a screenshot of the Fiddler calls indicating some requests succeeding with a 200 and the debugger break in Visual studio when the calls fails when the HttpRequestMessage is null.
Any insights as to why is this happening?
I do not think you are supposed to use the Request property from the constructor. Controllers can be initialized before the context is available.
Try refactoring the AfterMap delegate into a separate method and calling it after the ContactDto dto = Mapper.Map<Contact, ContactDto>(c); call.

Categories