I have a .NET Core 2.2 web-api that accepts a PersonDto, it is getting validated with Model Validation, but it does not check for illegal fields. It only checks if matching fields are valid.
I want to make sure that the supplied JSON contains only the fields that are in my Dto (Class).
public class PersonDto
{
public string firstname { get; set; }
public string lastname { get; set; }
}
My controller looks simplified like this:
public async Task<ActionResult<Person>> Post([FromBody] PersonDto personDto)
{
// do things
}
I send it incorrect fields (name does not exist in my dto) and the ModelState is valid.
{
"name": "Diego"
}
I expected the Model Validation to complain that the field "Name" does not exist.
How can I check for illegal fields?
You could use ActionFilter and Reflection to compare the request body content to the model fields. If there are unexpected fields, manually add model errors and the ModelState.IsValid will be false.
1.Create an ActionFilter
public class CompareFieldsActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//get all fields name
var listOfFieldNames = typeof(PersonDto).GetProperties().Select(f => f.Name).ToList();
var request = context.HttpContext.Request;
request.Body.Position = 0;
using (var reader = new StreamReader(request.Body))
{
//get request body content
var bodyString = reader.ReadToEnd();
//transfer content to json
JObject json = JObject.Parse(bodyString);
//if json contains fields that do not exist in Model, add model error
foreach (JProperty property in json.Children())
{
if (!listOfFieldNames.Contains(property.Name))
{
context.ModelState.AddModelError("Filed", "Field does not exist");
}
}
}
base.OnActionExecuting(context);
}
}
2.Use the filter on your action:
[HttpPost]
[CompareFieldsActionFilter]
public async Task<ActionResult<Person>> Post([FromBody] PersonDto personDto)
{
if (ModelState.IsValid)
{
// do things
}
// do things
}
Related
I have a asp .net core API with simple REST methods like this:
[Route("api/[controller]")]
public class SomeController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Insert([FromBody] ItemClass newItem)
{
[...]
return Ok();
}
}
The model class is pretty simple as well:
public class ItemClass
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; }
}
When I POST this JSON to the service
{
"id": null,
"name": "Some name",
"description": "Some description",
"isActive": null
}
the method is called with newItem set to null. No error, no exception, the the object was just null. It took me quite a while to figure out the parameter isActive was the cause of the problem. In the class I defined it as bool, but JSON defined it as null. When I either change the value to true or false or when I leave it out completely or when I change the parameter to bool? it works again. The object is deserialized correctly.
I'm used to asp net core APIs to be very resilient. Usually when a parameter is simply set to its default when it can not be parsed correctly from the message.
But why does the whole object become null here?
Is there at least a way to tell asp net core to throw an exception when the deserialization fails?
When null is mapped to the bool, resulting in a type mismatch exception, it will cause the whole object become null.
If this api is 3.x, it will trigger 400. But in 2.x and 5.0, they have no exception. You can handle the null value through NewtonSoft.json.
A simple way to handel it with this configuration, it can assign a default value to bool, but it doesn't trigger exception.
In 2.x
services.AddMvc()
.AddJsonOptions(options=>
{
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
5.0
services.AddControllers()
.AddNewtonsoftJson(option=>
{
option.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
If must return an error, you have to use a custom model binding.
public class CustomBindClassBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var itemClass = new ItemClass();
using (var reader = new StreamReader(bindingContext.HttpContext.Request.Body))
{
var body = reader.ReadToEndAsync();
var mydata = JsonConvert.DeserializeObject<JObject>(body.Result);
if(mydata["isActive"].ToString()==""|| mydata["isActive"].ToString() == null)
{
bindingContext.Result= ModelBindingResult.Failed();
throw new Exception("isActive is not correct");
}
else
{
itemClass.Id = mydata["id"].ToString();
itemClass.Name = mydata["name"].ToString();
itemClass.Description = mydata["description"].ToString();
itemClass.IsActive = bool.Parse(mydata["isActive"].ToString());
bindingContext.Result = ModelBindingResult.Success(itemClass);
}
}
return Task.CompletedTask;
}
}
In action
[HttpPost]
public async Task<IActionResult> Insert([FromBody][ModelBinder(typeof(CustomBindClassBinder))] ItemClass newItem)
{
return Ok(newItem);
}
I am using .NET Core 2.2 with Web API. I have created one class, i.e., as below:
public class NotificationRequestModel
{
[Required]
public string DeviceId { get; set; }
[Required]
public string FirebaseToken { get; set; }
[Required]
public string OS { get; set; }
public int StoreId { get; set; }
}
Using the above class I have created one method. Now I want to return a custom object, but it's returning its own object.
API method is:
public ActionResult<bool> UpdateFirebaseToken(NotificationRequestModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(FormatOutput(ModelState.Values));
}
var result = _notificationService.InsertOrUpdateFirebaseToken(model);
return Ok(result);
}
Here FormatOutput method is format the output.
protected Base FormatOutput(object input, int code = 0, string message = "", string[] details = null)
{
Base baseResult = new Base();
baseResult.Status = code;
baseResult.Error = message;
baseResult.TimeStamp = CommonHelper.CurrentTimeStamp;
baseResult.Code = code;
baseResult.Details = details;
baseResult.Message = message; //Enum.Parse<APIResponseMessageEnum>(code.ToString(), true); // (enum of code get value from language)
return baseResult;
}
But the issue is it returns:
{
"errors": {
"DeviceId": [
"The DeviceId field is required."
]
},
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "80000049-0001-fc00-b63f-84710c7967bb"
}
I want to customize this error with my model. I need error message and details from return output and passed it to my model. How can I do that? I had try to debug my code and found that breakpoint on API method is not calling. So I can't handle my custom method. Is there any solution? What am I doing wrong?
When using a controller with the ApiController attribute applied, ASP.NET Core automatically handles model validation errors by returning a 400 Bad Request with ModelState as the response body. As such, your conditional testing ModelState.IsValid is essentially always false (and therefore not entered) because the only requests that will ever get this far are valid ones.
You could simply remove the ApiController attribute, but that removes a bunch of other beneficial stuff the attributes adds as well. The better option is to use a custom response factory:
services.Configure<ApiBehaviorOptions>(o =>
{
o.InvalidModelStateResponseFactory = actionContext =>
new BadRequestObjectResult(actionContext.ModelState);
});
That's essentially what's happening by default, so you'd simply need to change the action provided there accordingly to customize it to your whims.
As Chris analyzed, your issue is caused by Automatic HTTP 400
responses.
For the quick solution, you could suppress this feature by
services.AddMvc()
.ConfigureApiBehaviorOptions(options => {
options.SuppressModelStateInvalidFilter = true;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
For an efficient way, you could follow the suggestion from Chris, like below:
services.AddMvc()
.ConfigureApiBehaviorOptions(options => {
//options.SuppressModelStateInvalidFilter = true;
options.InvalidModelStateResponseFactory = actionContext =>
{
var modelState = actionContext.ModelState.Values;
return new BadRequestObjectResult(FormatOutput(modelState));
};
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
And, there isn't any need to define the code below any more in your action.
if (!ModelState.IsValid)
{
return BadRequest(FormatOutput(ModelState.Values));
}
I'm trying to make a WebAPI controller on .NET Core 3.1 witch supports both JSON and XML as request/response content-type.
Controller works perfectly when it receive JSON with "application/json", but when it receive XML with "application/xml", method argument are created with default values, not values that was posted in request body.
Example project - https://github.com/rincew1nd/ASPNetCore_XMLMethods
Additional XML serializer in startup:
services.AddControllers().AddXmlSerializerFormatters();
Controller with method and test model:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
[HttpPost, Route("v1")]
[Consumes("application/json", "application/xml")]
[Produces("application/json", "application/xml")]
public TestRequest Test([FromBody] TestRequest data)
{
return data;
}
}
[DataContract]
public class TestRequest
{
[DataMember]
public Guid TestGuid { get; set; }
[DataMember]
public string TestString { get; set; }
}
P.S. Project contains Swagger for API testing purposes.
Your xml post request body uses camel cases which results in the model binding as null.
Add using Swashbuckle.AspNetCore.SwaggerGen; in starup.cs and try to configure like below code:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddXmlSerializerFormatters();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Neocase <-> 1C Integration", Version = "v1" });
c.SchemaFilter<XmlSchemaFilter>();
});
}
public class XmlSchemaFilter : Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (model.Properties == null) return;
foreach (var entry in model.Properties)
{
var name = entry.Key;
entry.Value.Xml = new OpenApiXml
{
Name = name.Substring(0, 1).ToUpper() + name.Substring(1)
};
}
}
}
Don't use FromBody attribute for application/xml.
When a parameter has [FromBody], Web API uses the Content-Type header to select a formatter. In this example, the content type is "application/json" and the request body is a raw JSON string (not a JSON object).
Using [FromBody]
After some more research i found that swagger generates wrong xml examples without even noticing custom naming of classes or properties.
I wrote custom schema for naming xml attributes as they are named by XML attributes.
Only problem i faced is that SchemaFilterContext doesn't provide description of properties of Enum type. So to name Enums i use custom attribute for swagger name and XMLElementAttribute on property with same names (yeah, it's junky but works).
public class XmlSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
//Try to find XmlRootAttribute on class
var xmlroot = context.Type.GetAttributeValue((XmlRootAttribute xra) => xra);
if (xmlroot != null)
{
schema.Xml = new OpenApiXml
{
Name = xmlroot.ElementName
};
}
//Try to find XmlElementAttribute on property
if (context.MemberInfo != null)
{
var xmlelement = context.MemberInfo.GetAttributeValue((XmlElementAttribute xea) => xea);
if (xmlelement != null)
{
schema.Xml = new OpenApiXml
{
Name = xmlelement.ElementName
};
}
}
//Try to find XmlEnumNameAttribute on enums
if (context.Type.IsEnum)
{
var enumname = context.Type.GetAttributeValue((XmlEnumNameAttribute xea) => xea);
if (enumname != null)
{
schema.Xml = new OpenApiXml
{
Name = enumname.ElementName
};
}
}
}
}
public static class AttributeHelper
{
public static TValue GetAttributeValue<TAttribute, TValue>(
this Type type,
Func<TAttribute, TValue> valueSelector)
where TAttribute : Attribute
{
var att = type.GetCustomAttributes(
typeof(TAttribute), true
).FirstOrDefault() as TAttribute;
if (att != null)
{
return valueSelector(att);
}
return default(TValue);
}
public static TValue GetAttributeValue<TAttribute, TValue>(
this MemberInfo mi,
Func<TAttribute, TValue> valueSelector)
where TAttribute : Attribute
{
var att = mi.GetCustomAttributes(
typeof(TAttribute), true
).FirstOrDefault() as TAttribute;
if (att != null)
{
return valueSelector(att);
}
return default(TValue);
}
}
I'm having a problem trying to get custom model binders to work as a query parameter like I have gotten to work previously in .net framework 4.7.
To ensure this wasn't a scenario where my object was too complex, I reduced the model to a simple string but even then I cannot get this to work.
I have a simple model I would like to be binded from query parameters.
public class SearchModel {
public string SearchTerms { get; set; }
}
And I have configured the ModelBinder and ModelBinderProvider as shown here like so.
public class TestModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext.ModelType != typeof(SearchModel)) {
throw new ArgumentException($"Invalid binding context supplied {bindingContext.ModelType}");
}
var model = (SearchModel)bindingContext.Model ?? new SearchModel();
var properties = model.GetType().GetProperties();
foreach(var p in properties) {
var value = this.GetValue(bindingContext, p.Name);
p.SetValue(model, Convert.ChangeType(value, p.PropertyType), null);
}
return Task.CompletedTask;
}
protected string GetValue(ModelBindingContext context, string key) {
var result = context.ValueProvider.GetValue(key);
return result.FirstValue;
}
}
public class TestModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context) {
if (context == null) {
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(SearchModel)) {
var returnType = new BinderTypeModelBinder(typeof(TestModelBinder));
return returnType;
}
return null;
}
}
As stated in the last step in Microsoft documentation I updated my ConfigureServices method in Startup.cs to include the BinderProvider.
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
But when I call my Search endpoint with a url such as "https://localhost:44387/api/testbinding?searchTerms=newSearch" I am always seeing a return of "request == null True" even though I see it properly hit the custom binding and bind correctly if I step through debugging, can anyone please point me in the right direction as to what I am doing wrong?
[Route("api/[controller]")]
[ApiController]
public class TestBindingController : ControllerBase {
[HttpGet()]
public IActionResult GetResult([FromQuery] SearchModel request) {
return Ok($"request == null {request == null}");
}
}
I think what you're missing if the statement that sets the result of the model binding operation, as you can see in the AuthorEntityBinder code sample in this section of the docs:
bindingContext.Result = ModelBindingResult.Success(model);
Your implementation of the model binder does create an instance of SearchModel, but doesn't feed it back to the model binding context.
As a separate note, I don't think you need to add a custom model binder is the query string segments match the properties names of the model you're trying to bind.
I'm attempting to build an ASP.NET Core 1.1 Controller method to handle an HTTP Request that looks like the following:
POST https://localhost/api/data/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------625450203542273177701444
Host: localhost
Content-Length: 474
----------------------------625450203542273177701444
Content-Disposition: form-data; name="file"; filename="myfile.txt"
Content-Type: text/plain
<< Contents of my file >>
----------------------------625450203542273177701444
Content-Disposition: form-data; name="text"
Content-Type: application/json
{"md5":"595f44fec1e92a71d3e9e77456ba80d0","sessionIds":["123","abc"]}
----------------------------625450203542273177701444--
It's a multipart/form-data request with one part being a (small) file and the other part a json blob that is based on a provided specification.
Ideally, I'd love my controller method to look like:
[HttpPost]
public async Task Post(UploadPayload payload)
{
// TODO
}
public class UploadPayload
{
public IFormFile File { get; set; }
[Required]
[StringLength(32)]
public string Md5 { get; set; }
public List<string> SessionIds { get; set; }
}
But alas, that doesn't Just Work {TM}. When I have it like this, the IFormFile does get populated, but the json string doesn't get deserialized to the other properties.
I've also tried adding a Text property to UploadPayload that has all the properties other than the IFormFile and that also doesn't receive the data. E.g.
public class UploadPayload
{
public IFormFile File { get; set; }
public UploadPayloadMetadata Text { get; set; }
}
public class UploadPayloadMetadata
{
[Required]
[StringLength(32)]
public string Md5 { get; set; }
public List<string> SessionIds { get; set; }
}
A workaround that I have is to avoid model binding and use MultipartReader along the lines of:
[HttpPost]
public async Task Post()
{
...
var reader = new MultipartReader(Request.GetMultipartBoundary(), HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
var filePart = section.AsFileSection();
// Do stuff & things with the file
section = await reader.ReadNextSectionAsync();
var jsonPart = section.AsFormDataSection();
var jsonString = await jsonPart.GetValueAsync();
// Use $JsonLibrary to manually deserailize into the model
// Do stuff & things with the metadata
...
}
Doing the above bypasses model validation features, etc. Also, I thought maybe I could take that jsonString and then somehow get it into a state that I could then call await TryUpdateModelAsync(payloadModel, ...) but couldn't figure out how to get there either - and that didn't seem all that clean either.
Is it possible to get to my desired state of "transparent" model binding like my first attempt? If so, how would one get to that?
The first problem here is that the data needs to be sent from the client in a slightly different format. Each property in your UploadPayload class needs to be sent in its own form part:
const formData = new FormData();
formData.append(`file`, file);
formData.append('md5', JSON.stringify(md5));
formData.append('sessionIds', JSON.stringify(sessionIds));
Once you do this, you can add the [FromForm] attribute to the MD5 property to bind it, since it is a simple string value. This will not work for the SessionIds property though since it is a complex object.
Binding complex JSON from the form data can be accomplished using a custom model binder:
public class FormDataJsonBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// Fetch the value of the argument by name and set it to the model state
string fieldName = bindingContext.FieldName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);
if(valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;
else bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);
// Do nothing if the value is null or empty
string value = valueProviderResult.FirstValue;
if(string.IsNullOrEmpty(value)) return Task.CompletedTask;
try
{
// Deserialize the provided value and set the binding result
object result = JsonConvert.DeserializeObject(value, bindingContext.ModelType);
bindingContext.Result = ModelBindingResult.Success(result);
}
catch(JsonException)
{
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
You can then use the ModelBinder attribute in your DTO class to indicate that this binder should be used to bind the MyJson property:
public class UploadPayload
{
public IFormFile File { get; set; }
[Required]
[StringLength(32)]
[FromForm]
public string Md5 { get; set; }
[ModelBinder(BinderType = typeof(FormDataJsonBinder))]
public List<string> SessionIds { get; set; }
}
You can read more about custom model binding in the ASP.NET Core documentation: https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding
I'm not 100% clear on how this would work for ASP.NET Core but for Web API (so I assume a similar path exists here) you'd want to go down the road of a Media Formatter. Here's an example (fairly similar to your question) Github Sample with blog post
Custom formatters might be the ticket? https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-formatters