I have a request model class that I'm trying to use the default Web API 2 model binding (.NET 4.6.1). Some of the query string parameters match the model properties, but some do not.
public async Task<IHttpActionResult> Get([FromUri]MyRequest request) {...}
Sample query string:
/api/endpoint?country=GB
Sample model property:
public class MyRequest
{
[JsonProperty("country")] // Did not work
[DataMember(Name = "country")] // Also did not work
public string CountryCode { get; set; }
// ... other properties
}
Is there a way to use attributes on my model (like you might use [JsonProperty("country")]) to avoid implementing a custom model binding? Or is best approach just to use create a specific model for the QueryString to bind, and then use AutoMapper to customize for the differences?
Late answer but I bumped into this issue recently also. You could simply use the BindProperty attribute:
public class MyRequest
{
[BindProperty(Name = "country")]
public string CountryCode { get; set; }
}
Tested on .NET Core 2.1 and 2.2
Based on further research, the default model binding behavior in Web API does not support JsonProperty or DataMember attributes, and most likely solutions seem to be either (1) custom model binder or (2) maintaining 2 sets of models and a mapping between them.
JSON to Model property binding using JsonProperty
Changing the parameter name Web Api model binding
I opted for the custom model binder (implementation below) so I could re-use this and not have to duplicate all my models (and maintain mappings between every model).
Usage
The implementation below allows me to let any model optionally use JsonProperty for model binding, but if not provided, will default to just the property name. It supports mappings from standard .NET types (string, int, double, etc). Not quite production ready, but it meets my use cases so far.
[ModelBinder(typeof(AttributeModelBinder))]
public class PersonModel
{
[JsonProperty("pid")]
public int PersonId { get; set; }
public string Name { get; set; }
}
This allows the following query string to be mapped in a request:
/api/endpoint?pid=1&name=test
Implementation
First, the solution defines a mapped property to track the source property of the model and the target name to use when setting the value from the value provider.
public class MappedProperty
{
public MappedProperty(PropertyInfo source)
{
this.Info = source;
this.Source = source.Name;
this.Target = source.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? source.Name;
}
public PropertyInfo Info { get; }
public string Source { get; }
public string Target { get; }
}
Then, a custom model binder is defined to handle the mapping. It caches the reflected model properties to avoid repeating the reflection on subsequent calls. It may not be quite production ready, but initial testing has been promising.
public class AttributeModelBinder : IModelBinder
{
public static object _lock = new object();
private static Dictionary<Type, IEnumerable<MappedProperty>> _mappings = new Dictionary<Type, IEnumerable<MappedProperty>>();
public IEnumerable<MappedProperty> GetMapping(Type type)
{
if (_mappings.TryGetValue(type, out var result)) return result; // Found
lock (_lock)
{
if (_mappings.TryGetValue(type, out result)) return result; // Check again after lock
return (_mappings[type] = type.GetProperties().Select(p => new MappedProperty(p)));
}
}
public object Convert(Type target, string value)
{
try
{
var converter = TypeDescriptor.GetConverter(target);
if (converter != null)
return converter.ConvertFromString(value);
else
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
catch (NotSupportedException)
{
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
}
public void SetValue(object model, MappedProperty p, IValueProvider valueProvider)
{
var value = valueProvider.GetValue(p.Target)?.AttemptedValue;
if (value == null) return;
p.Info.SetValue(model, this.Convert(p.Info.PropertyType, value));
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
try
{
var model = Activator.CreateInstance(bindingContext.ModelType);
var mappings = this.GetMapping(bindingContext.ModelType);
foreach (var p in mappings)
this.SetValue(model, p, bindingContext.ValueProvider);
bindingContext.Model = model;
return true;
}
catch (Exception ex)
{
return false;
}
}
}
Related
I'm building a restful server to handle post requests. However, there is a dot (.) in one of the parameter names which I don't know how to handle since C# does not allow dot (.) in their variable names. The parameter name is "data.json" without the quotation.
I read some posts about C# converting dots (.) into underscores (_), so I tried to name the variable "data_json", which doesn't work, the string is empty.
Object Class:
public class Lead {
public string data_json { get; set; }
public string page_id { get; set; }
public string page_url { get; set; }
}
Post Handler:
public HttpResponseMessage Post(Lead value) {
try {
Log.CreatePostLog(page_id + value.data_json);
} catch (Exception e) {
return Request.CreateResponse(HttpStatusCode.BadRequest, e.Message);
}
return Request.CreateResponse(HttpStatusCode.OK, "Done!");
}
Post Request Body (Cannot be changed):
page_url=http://ramdomurl/
&page_id=123456
&data.json={"time_submitted":["04:34 PM UTC"],"full_name":["John Doe"]}
When the request is made, the log shows page_id but nothing after.
It should show page_id and the Json string after it.
One possible solution is to create a custom model binder, which handles fields whose names contain the "." character, and apply this binder to the model class.
The code of the binder:
// this binder assigns form fields with dots to properties with underscores:
// e.g. data.json -> data_json
public class Dot2UnderscoreModelBinder : IModelBinder
{
// for regular fields, we will use the default binder
private readonly DefaultModelBinder _default = new DefaultModelBinder();
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
// handle the regular fields
var model = _default.BindModel(controllerContext, bindingContext);
// handle the special fields
if (model != null)
{
var modelType = model.GetType();
var form = controllerContext.HttpContext.Request.Form;
foreach (var key in form.AllKeys)
{
if (key.Contains(".")) // special field
{
// model property must be named by the convention "." -> "_"
var propertyName = key.Replace(".", "_");
var propertyInfo = modelType.GetProperty(propertyName);
propertyInfo?.SetValue(model, form[key]);
}
}
}
return model;
}
}
Note that this is a simplistic implementation, it only supports string properties, and its performance is not optimal. But it is a working starting point.
Now you need to apply the above binder to the model class:
[ModelBinder(typeof(Dot2UnderscoreModelBinder))]
public class Lead
{
//... properties
}
It worth noting that the controller must derive from Controller in System.Web.Mvc namespace, and not ApiController in System.Web.Http, because that latter doesn't trigger model binders:
using System.Web.Mvc;
....
public class MyController : Controller
{
[HttpPost]
public ActionResult Post(Lead value)
{
//... do some stuff
return base.Content("Done!");
}
}
ASP.NET Core
Just as a side note, in ASP.NET Core the same can be achieved in a very simple way, by applying FromForm attribute:
public class Lead
{
[FromForm(Name = "data.json")] // apply this attribute
public string data_json { get; set; }
//... other properties
}
Use NewtonsoftJson PropertyName attribute:
public class Lead
{
[JsonProperty(PropertyName = "data.json")]
public string data_json { get; set; }
public string page_id { get; set; }
public string page_url { get; set; }
}
Add nuget package:
https://www.nuget.org/packages/Newtonsoft.Json/
Tools
Visual Studio 2017
ASP.NET Core 2.2
Postman v7.2.0
What I'm trying to do
Send FormData from Postman to an ASP.NET Core controller and have the data from the request bind to to a command class that has properties with private setters.
I've sent JSON data using the same setup (private setters) with no problem. The FromBody attribute deserialises the JSON string to the model without errors.
The Problem
Properties that are primitive types do not bind if the model has a private setter. However, complex types do regardless of the access modifier.
Controller
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> CreateItemAsync([FromForm]CreateItemCommand command)
{
bool result = false;
commandResult = await _mediator.Send(command);
if (!commandResult)
{
return BadRequest();
}
return Ok();
}
Command
Note: The Title property has been left with a public setter deliberately to illustrate the behviour
[DataContract]
public class CreateItemCommand
:IRequest<bool>
{
[DataMember]
public string Title { get; set; }
[DataMember]
public string Description { get; private set; }
[DataMember]
public int Count { get; private set; }
[DataMember]
public HashSet<string> Tags { get; private set; }
[DataMember]
public string ItemDate { get; private set; }
[DataMember]
public List<IFormFile> Documents { get; private set; }
public CreateItemCommand()
{
Skills = new HashSet<string>();
Systems = new HashSet<string>();
}
public CreateItemCommand(string title, string description,
int count, HashSet<string> tags, string itemDate,
List<IFormFile> documents)
: this()
{
Title = title;
Description = description;
Count = count
Tags = tags;
ItemDate = itemDate;
Documents = documents;
}
}
In Postman I now setup the request as follows:
I've had to obfuscate some of the information, but you can see that the primitive types with private setters are not set.
Questions
Why does the property access modifier only affect properties with primitive types?
Why does this happens when the parameter attribute is set to FromForm but not when it's set to FromBody
Why does the property access modifier only affect properties with primitive types?
For Asp.Net Core ModelBinder, it will check whether the property is private access setter by ComplexTypeModelBinder code below:
protected virtual object CreateModel(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
// If model creator throws an exception, we want to propagate it back up the call stack, since the
// application developer should know that this was an invalid type to try to bind to.
if (_modelCreator == null)
{
// The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as
// reflection does not provide information about the implicit parameterless constructor for a struct.
// This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression
// compile fails to construct it.
var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
if (modelTypeInfo.IsAbstract || modelTypeInfo.GetConstructor(Type.EmptyTypes) == null)
{
var metadata = bindingContext.ModelMetadata;
switch (metadata.MetadataKind)
{
case ModelMetadataKind.Parameter:
throw new InvalidOperationException(
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForParameter(
modelTypeInfo.FullName,
metadata.ParameterName));
case ModelMetadataKind.Property:
throw new InvalidOperationException(
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForProperty(
modelTypeInfo.FullName,
metadata.PropertyName,
bindingContext.ModelMetadata.ContainerType.FullName));
case ModelMetadataKind.Type:
throw new InvalidOperationException(
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForType(
modelTypeInfo.FullName));
}
}
_modelCreator = Expression
.Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
.Compile();
}
return _modelCreator();
}
Why does this happens when the parameter attribute is set to FromForm but not when it's set to FromBody
For FromBody, it is used JsonInputFormatter to bind the model from the body request, it's used JsonConvert.DeserializeObject to deserilize the object and Newtonsoft.Json support deserize the object which contains private setter from json string.
I would like to enhance final result that ModelBinder returns.
For example:
public class MyModel
{
public int Order {get;set;}
[MyUpperCaseAttribute]
public string Title {get;set;}
}
In API method I expect that all string properties in MyModel which has MyUpperCaseAttribute is in upper case.
For example:
[HttpPost("AddRecord")]
public async Task<ActionResult<int>> AddRecord(MyModel model)
{
model.Title should be upper case, even if send from client in lower case.
}
My idea was to override default ModelBinder and enumerate through all properties and check if property is string and has MyUpperCaseAttribute and correct property value to upper case. I check documentation, but doesn't examples doesn't fill right, since they completely redesign what is returned. I would like to just modify result properties.
What would be the best option to achieve desired behaviour?
Important: (edited):
It would be nice if directive attributes could be stacked:
public class MyModel
{
public int Order {get;set;}
[MyUpperCaseAttribute]
[RemoveSpacesAttribute]
public string Title {get;set;}
}
Edited:
It looks similar to this, but if not other, this is ASP.NET Core, and on link is just ASP.NET. Method, properties, interfaces... are not the same.
I should say, that it would be nice if JSON case rule would work:
public class MyModel
{
public int Order {get;set;}
public string Title {get;set;}
}
should work if {order: 1, title: "test"} (notice lowercase) is send from JavaScript.
This might not be the 'best' option, but I would just use .ToUpper() extension method instead of a custom attribute filter.
public class MyModel
{
private string _title;
public int Order {get;set;}
public string Title { get => _title.ToUpper(); set => _title = value.ToUpper(); }
}
There's a big red herring here, and that's the fact that it appears that this is the sort of thing that could and should be accomplished via model binding. Unfortunately, that's not the case in ASP.Net Core Web API: because the incoming data is JSON, it is in fact handled by input formatters, not model binders. Therefore, in order to achieve the desired effect, you need to create your own custom input formatter that replaces the standard JsonInputFormatter.
First the attribute:
[AttributeUsage(AttributeTargets.Property)]
public class ToUppercaseAttribute : Attribute
{
}
Then we decorate our model class with it:
public class MyModel
{
public int Order { get; set; }
[ToUppercase]
public string Title { get; set; }
}
Now create our custom input formatter that checks for that attribute and transforms the output if necessary. In this case, it simply wraps and delegates to JsonInputFormatter to do the heavy lifting as normal, then modifies the result if it finds our ToUppercaseAttribute attribute on any string property:
public class ToUppercaseJsonInputFormatter : TextInputFormatter
{
private readonly JsonInputFormatter _jsonInputFormatter;
public ToUppercaseJsonInputFormatter(JsonInputFormatter jsonInputFormatter)
{
_jsonInputFormatter = jsonInputFormatter;
foreach (var supportedEncoding in _jsonInputFormatter.SupportedEncodings)
SupportedEncodings.Add(supportedEncoding);
foreach (var supportedMediaType in _jsonInputFormatter.SupportedMediaTypes)
SupportedMediaTypes.Add(supportedMediaType);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
var result = _jsonInputFormatter.ReadRequestBodyAsync(context, encoding);
foreach (var property in context.ModelType.GetProperties().Where(p => p.PropertyType.IsAssignableFrom(typeof(string))
&& p.CustomAttributes.Any(a => a.AttributeType.IsAssignableFrom(typeof(ToUppercaseAttribute)))))
{
var value = (string)property.GetValue(result.Result.Model);
property.SetValue(result.Result.Model, value.ToUpper());
}
return result;
}
}
Next we create an extension method that makes it simple to substitute the default JsonInputFormatter with our custom formatter:
public static class MvcOptionsExtensions
{
public static void UseToUppercaseJsonInputFormatter(this MvcOptions opts)
{
if (opts.InputFormatters.FirstOrDefault(f => f is JsonInputFormatter && !(f is JsonPatchInputFormatter)) is JsonInputFormatter jsonInputFormatter)
{
var jsonInputFormatterIndex = opts.InputFormatters.IndexOf(jsonInputFormatter);
opts.InputFormatters[jsonInputFormatterIndex] = new ToUppercaseJsonInputFormatter(jsonInputFormatter);
}
}
}
And finally, call that method to effect the replacement in Startup.cs:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc(options => options.UseToUppercaseJsonInputFormatter());
}
}
Et voilĂ !
You can do this thing inside your MyUpperCaseAttribute as follows:
public class MyUpperCaseAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if(value != null)
{
validationContext.ObjectType
.GetProperty(validationContext.MemberName)
.SetValue(validationContext.ObjectInstance, value.ToString().ToUpper(), null);
}
return null;
}
}
Property value will be converted to UpperCase during Model Binding. I have checked it in my side and it works perfectly.
In MVC when we post a model to an action we do the following in order to validate the model against the data annotation of that model:
if (ModelState.IsValid)
If we mark a property as [Required], the ModelState.IsValid will validate that property if contains a value or not.
My question: How can I manually build and run custom validator?
P.S. I am talking about backend validator only.
In .NET Core, you can simply create a class that inherits from ValidationAttribute. You can see the full details in the ASP.NET Core MVC Docs.
Here's the example taken straight from the docs:
public class ClassicMovieAttribute : ValidationAttribute
{
private int _year;
public ClassicMovieAttribute(int Year)
{
_year = Year;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
Movie movie = (Movie)validationContext.ObjectInstance;
if (movie.Genre == Genre.Classic && movie.ReleaseDate.Year > _year)
{
return new ValidationResult(GetErrorMessage());
}
return ValidationResult.Success;
}
}
I've adapted the example to exclude client-side validation, as requested in your question.
In order to use this new attribute (again, taken from the docs), you need to add it to the relevant field:
[ClassicMovie(1960)]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
Here's another, simpler example for ensuring that a value is true:
public class EnforceTrueAttribute : ValidationAttribute
{
public EnforceTrueAttribute()
: base("The {0} field must be true.") { }
public override bool IsValid(object value) =>
value is bool valueAsBool && valueAsBool;
}
This is applied in the same way:
[EnforceTrue]
public bool ThisShouldBeTrue { get; set; }
Edit: Front-End Code as requested:
<div asp-validation-summary="All" class="text-danger"></div>
The options are All, ModelOnly or None.
To create a custom validation attribute in .Net Core, you need to inherit from IModelValidator and implement Validate method.
Custom validator
public class ValidUrlAttribute : Attribute, IModelValidator
{
public string ErrorMessage { get; set; }
public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
{
var url = context.Model as string;
if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
return Enumerable.Empty<ModelValidationResult>();
}
return new List<ModelValidationResult>
{
new ModelValidationResult(context.ModelMetadata.PropertyName, ErrorMessage)
};
}
}
The model
public class Product
{
public int ProductId { get; set; }
[Required]
public string ProductName { get; set; }
[Required]
[ValidUrl]
public string ProductThumbnailUrl { get; set; }
}
Will this approach give opportunity to work with "ModelState.IsValid" property in controller action method?
Yes! The ModelState object will correctly reflect the errors.
Can this approach be applied to the model class? Or it can be used with model class properties only?
I don't know if that could be applied onto class level. I know you can get the information about the class from ModelValidationContext though:
context.Model: returns the property value that is to be validated
context.Container: returns the object that contains the property
context.ActionContext: provides context data and describes the action method that processes the request
context.ModelMetadata: describes the model class that is being validated in detail
Notes:
This validation attribute doesn't work with Client Validation, as requested in OP.
I am expecting a POST request with content type set to:
Content-Type: application/x-www-form-urlencoded
Request body looks like this:
first_name=john&last_name=banana
My action on controller has this signature:
[HttpPost]
public HttpResponseMessage Save(Actor actor)
{
....
}
Where Actor class is given as:
public class Actor
{
public string FirstName {get;set;}
public string LastName {get;set;}
}
Is there a way to force Web API to bind:
first_name => FirstName
last_name => LastName
I know how to do it with requests with content type set to application/json, but not with urlencoded.
I'm 98% certain (I looked the source code) that WebAPI doesn't support it.
If you really need to support different property names, you can either:
Add additional properties to the Actor class which serves as alias.
Create your own model binder.
Here is a simple model binder:
public sealed class ActorDtoModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var actor = new Actor();
var firstNameValueResult = bindingContext.ValueProvider.GetValue(CreateFullPropertyName(bindingContext, "First_Name"));
if(firstNameValueResult != null) {
actor.FirstName = firstNameValueResult.AttemptedValue;
}
var lastNameValueResult = bindingContext.ValueProvider.GetValue(CreateFullPropertyName(bindingContext, "Last_Name"));
if(lastNameValueResult != null) {
actor.LastName = lastNameValueResult.AttemptedValue;
}
bindingContext.Model = actor;
bindingContext.ValidationNode.ValidateAllProperties = true;
return true;
}
private string CreateFullPropertyName(ModelBindingContext bindingContext, string propertyName)
{
if(string.IsNullOrEmpty(bindingContext.ModelName))
{
return propertyName;
}
return bindingContext.ModelName + "." + propertyName;
}
}
If you are up for the challenge, you can try to create a generic model binder.
It's an old post but maybe this could helps other people.
Here is a solution with an AliasAttribute and the associated ModelBinder
It could be used like this :
[ModelBinder(typeof(AliasBinder))]
public class MyModel
{
[Alias("state")]
public string Status { get; set; }
}
Don't hesitate to comment my code :)
Every Idea / comment is welcome.