Manually invoking modelstate validation fails in nested classes - c#

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()); });

Related

Why is my OData POST body to model not working?

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.

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;
}
}

Custom error objects for .Net Core 3 web api

I am currently developing a web api in .NET Core 3. I currently have the following model for my error response object:
public class ErrorRo
{
public string Message { get; set; }
public int StatusCode { get; set; }
public string Endpoint { get; set; }
public string Parameters { get; set; }
public string IpAddress { get; set; }
}
This is a mandated response I need to implement, management has pushed this. It allows more verbose error messages for people hitting our API so that they know what went wrong.
At the moment I am currently populating this object manually in the methods themselves. Is there a way where I can overwrite the response methods. I.e. can I override the BadRequest of IActionResult to automatically populate these fields?
Thanks!
You can use result filters for this purpose. Add a filter which repalces result before sending it back
Model
public class CustomErroModel
{
public string Message { get; set; }
public int StatusCode { get; set; }
public string Endpoint { get; set; }
public string Parameters { get; set; }
public string IpAddress { get; set; }
}
Filter
public class BadRequestCustomErrorFilterAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
//todo: check for BadRequestObjectResult if anything is returned for bad request
if (context.Result is BadRequestResult)
{
var result = new CustomErroModel
{
StatusCode = 200, //you status code
Endpoint = context.HttpContext.Request.GetDisplayUrl(),
Message = "some message",
IpAddress = context.HttpContext.Connection.RemoteIpAddress.ToString(), //find better implementation in case of proxy
//this returns only parameters that controller expects but not those are not defined in model
Parameters = string.Join(", ", context.ModelState.Select(v => $"{v.Key}={v.Value.AttemptedValue}"))
};
context.Result = new OkObjectResult(result); // or any other ObjectResult
}
}
}
Then apply filter per action or globally
[BadRequestCustomErrorFilter]
public IActionResult SomeAction(SomeModel model)
or
services
.AddMvc(options =>
{
options.Filters.Add<BadRequestCustomErrorFilterAttribute>();
//...
}
Well it depends on the scenario, but one possible approach could be to use a middleware using a similar strategy like the one described in this question, so that you complete the response with extra information.

400 Bad Request submitting Json to WebApi via HttpClient.PutAsync

Normally, serialized objects would be used from the services to the webapi calls but in this instance I have to use a json representation for the call.
The process would be to deserialize the json to the proper class, then process as usual.
HttpClient Put
Method is called from within a console app
public async Task<ApiMessage<string>> PutAsync(Uri baseEndpoint, string relativePath, Dictionary<string, string> headerInfo, string json)
{
HttpClient httpClient = new HttpClient();
if (headerInfo != null)
{
foreach (KeyValuePair<string, string> _header in headerInfo)
_httpClient.DefaultRequestHeaders.Add(_header.Key, _header.Value);
}
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json-patch+json"));
var content = new StringContent(json, Encoding.UTF8, "application/json-patch+json");
var response = await httpClient.PutAsync(CreateRequestUri(relativePath, baseEndpoint), content);
var data = await response.Content.ReadAsStringAsync();
...
}
Endpoint
The call never hits the endpoint. The endpoint is hit if I remove the [FromBody] tag but as expected, the parameter is null. There seems to be some sort of filtering happening.
[HttpPut()]
[Route("")]
[SwaggerResponse(StatusCodes.Status200OK)]
[SwaggerResponse(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> UpdatePaymentSync([FromBody] string paymentSyncJson)
{
if (string.IsNullOrEmpty(paymentSyncJson))
return BadRequest();
//hack: don't have access to models so need to send json rep
var paymentSync = JsonConvert.DeserializeObject<PaymentSync>(paymentSyncJson);
....
}
This is the json payload. I thought [FromBody] took care of simple types but this is proving me wrong.
{
"paymentSyncJson": {
"id": 10002,
"fileName": "Empty_20190101.csv",
"comments": "Empty File",
"processingDate": "2019-01-02T19:43:11.373",
"status": "E",
"createdDate": "2019-01-02T19:43:11.373",
"createdBy": "DAME",
"modifiedDate": null,
"modifiedBy": null,
"paymentSyncDetails": []
}
}
Just expanding on my Comment.
The OP did:
[HttpPut()]
[Route("")]
[SwaggerResponse(StatusCodes.Status200OK)]
[SwaggerResponse(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> UpdatePaymentSync([FromBody] string paymentSyncJson)
{
if (string.IsNullOrEmpty(paymentSyncJson))
return BadRequest();
//hack: don't have access to models so need to send json rep
var paymentSync = JsonConvert.DeserializeObject<PaymentSync>(paymentSyncJson);
....
}
Where they have put [FromBody] string paymentSyncJson, FromBody will try and deserialise into the type you specify, in this case string. I suggest doing:
public async Task<IActionResult> UpdatePaymentSync([FromBody] JObject paymentSyncJson)
Then you can change this line:
var paymentSync = JsonConvert.DeserializeObject<PaymentSync>(paymentSyncJson);
To:
var paymentSync = paymentSyncJson.ToObject<PaymentSync>();
Your payload is not a string, it's a json, that's why the runtime can't parse the body to your requested string paymentSyncJson.
To solve it, create a matching dto which reflects the json
public class PaymentDto
{
public PaymentSyncDto PaymentSyncJson { get; set; }
}
public class PaymentSyncDto
{
public int Id { get; set; }
public string FileName { get; set; }
public string Comments { get; set; }
public DateTime ProcessingDate { get; set; }
public string Status { get; set; }
public DateTime CreatedDate { get; set; }
public string CreatedBy { get; set; }
public DateTime ModifiedDate { get; set; }
public string ModifiedBy { get; set; }
public int[] PaymentSyncDetails { get; set; }
}
Then use it in the controller method to read the data from the request body
public async Task<IActionResult> UpdatePaymentSync([FromBody] PaymentDto payment)

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

Categories