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
Related
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;
}
}
I have the following dotnet core code and I'm trying to return a TestResponse JSON object that has a few nodes under it. However, using the return Enumerable.Range(1, 4).Select(index => new Entities.TestResponse call in the post return for some reason all the attributes of response are not found in the enclosure when clearly Entities.TestResponse has the response definition. I'm probably not configuring the Enumerable enclosure correctly. Does anyone know how to resolve this, so I can set the response.result & response.exception and return response JSON from my REST POST method?
namespace TestApi.Entities
{
public class TestResponse
{
public TestResponseNodes response { get; set; }
}
public class TestResponseNodes
{
public string result { get; set; }
public string exception { get; set; }
}
}
[HttpPost]
public Task<IEnumerable<Entities.TestResponse>> Post([FromBody] String input)
{
return Enumerable.Range(1, 4).Select(index => new Entities.TestResponse
{
response.result = "No Error",
response.exception = "None"
}).ToArray();
}
Your syntax is wrong, you need to also new up the inner object, for example:
new Entities.TestResponse
{
response = new Entities.TestResponseNodes
{
result = "No Error",
exception = "None"
}
}
As an aside, you should follow common C# conventions and capitalise your property names, for example:
public class TestResponse
{
public TestResponseNodes Response;
}
public class TestResponseNodes
{
public string Result { get; set; }
public string Exception { get; set; }
}
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()); });
I am using Postman to make a Get call. In the URL I have optional parameters with underscore in it. I want to assign those values to a class by using DataContract but I can't-do it. If I read them separately then there is no issue.
This is new to me so exploring what is the best approach to do it. Found some links where the suggestion is to go for an individual parameter but want to make sure I am not missing anything here.
Call: http://{{url}}/Host?host_name=Test&host_zipcode=123&host_id=123
Working: I can read these parameter values if I read them as an individual parameter.
[HttpGet]
[Route("api/Host")]
public async Task<HostResponse> GetHostInfo([FromUri (Name = "host_name")] string hostName, [FromUri (Name = "host_zipcode")] string hostZipCode, [FromUri(Name = "host_id")] string hostId)
{
}
Not Working: When I try to use class by using DataContract, I can't read it.
[HttpGet]
[Route("api/Host")]
public async Task<HostResponse> GetHostInfo([FromUri] HostInfo hostInfo)
{
}
[DataContract]
public class HostInfo
{
[DataMember(Name = "host_name")]
public string HostName { get; set; }
[DataMember(Name = "host_zipcode")]
public string HostZipCode { get; set; }
[DataMember(Name = "host_id")]
public string HostId { get; set; }
}
I also tried:
public class DeliveryManagerStatus
{
[JsonProperty(PropertyName = "country")]
public string Country { get; set; }
[JsonProperty(PropertyName = "delivery_provider")]
public string DeliveryProvider { get; set; }
[JsonProperty(PropertyName = "job_id")]
public string JobId { get; set; }
}
How can I assign these properties to a class?
You can use IModelBinder (details) implementation to parse it. Here is a DataMember based example (takes key names from DataMember attribute):
public class DataMemberBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var props = bindingContext.ModelType.GetProperties();
var result = Activator.CreateInstance(bindingContext.ModelType);
foreach (var property in props)
{
try
{
var attributes = property.GetCustomAttributes(typeof(DataMemberAttribute), true);
var key = attributes.Length > 0
? ((DataMemberAttribute)attributes[0]).Name
: property.Name;
if (bindingContext.ValueProvider.ContainsPrefix(key))
{
var value = bindingContext.ValueProvider.GetValue(key).ConvertTo(property.PropertyType);
property.SetValue(result, value);
}
}
catch
{
// log that property can't be set or throw an exception
}
}
bindingContext.Model = result;
return true;
}
}
and usage
public async Task<HostResponse> GetHostInfo([FromUri(BinderType = typeof(DataMemberBinder))] HostInfo hostInfo)
In quick search I wasn't able to find any AttributeBased binder try and share if you will find it
Using your HostInfo class change your call to:
http://{{url}}/Host?hostInfo.host_name=Test&hostInfo.host_zipcode=123&hostInfo.host_id=123.
I would recommend changing your route to accept the complex type in the body if possible due to the limitations in query string length.
How can I write custom model binder for complex model with collection of polymorphic objects?
I have the next structure of models:
public class CustomAttributeValueViewModel
{
public int? CustomAttributeValueId { get; set; }
public int CustomAttributeId { get; set; }
public int EntityId { get; set; }
public CustomisableTypes EntityType { get; set; }
public string AttributeClassType { get; set; }
}
public class CustomStringViewModel : CustomAttributeValueViewModel
{
public string Value { get; set; }
}
public class CustomIntegerViewModel : CustomAttributeValueViewModel
{
public int Value { get; set; }
}
And if I want to bind CustomAttributeValueViewModel to some of it's inheritors, I use such custom model binder:
public class CustomAttributeValueModelBinder : DefaultModelBinder
{
protected override object CreateModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType)
{
if (modelType == typeof(CustomAttributeValueViewModel))
{
var attributeClassType = (string)bindingContext.ValueProvider
.GetValue("AttributeClassType")
.ConvertTo(typeof(string));
Assembly assembly = typeof(CustomAttributeValueViewModel).Assembly;
Type instantiationType = assembly.GetType(attributeClassType, true);
var obj = Activator.CreateInstance(instantiationType);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);
bindingContext.ModelMetadata.Model = obj;
return obj;
}
return base.CreateModel(controllerContext, bindingContext, modelType);
}
}
It works great. But now I want to bind such models as items of collection of another model. For instance:
public class SomeEntity
{
// different properties here
public IList<CustomAttributeValueViewModel> CustomAttributes { get; set; }
}
How can I do that?
EDITED:
I want to bind a posted data which I received from a client. For more clarity it is an example of my POST HTTP request:
POST someUrl HTTP/1.1
User-Agent: Fiddler
Host: localhost
Content-Type: application/json; charset=utf-8
Content-Length: 115
{
"ProductName": "Product Name",
"CustomAttributeValues": [
{
"CustomAttributeId": "1",
"Value": "123",
"AttributeClassType": "namespace.CustomStringViewModel"
}
]
}
And I receive this data in my action:
public void Save([ModelBinder(typeof(SomeBinder))] SomeEntity model)
{
// some logic
}
I want to write such binder for getting collection of inheritors.
You need to include full path to the AttributeClassType,
var valueProviderResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName + ".AttributeClassType");
please take a look at this working Github sample