asp.net-coreasp.net-core-mvcmodel-bindingasp.net-core-2.1defaultmodelbinder

Custom model binding using reflection in ASP.NET Core


I have the following Model

public class MyModel
{
   public string Name {get;set;}
   public int? Age {get;set;}
   public string City {get;set;}
   public decimal? Salary {get;set;}
   public JObject ExtraFields {get;set;}
}

I am trying to implement Custom Model Binder. If the submitted form has key that matches with the Model's propery then set model's property value else add the key and value to ExtraFields. Note that ExtraFields is JObject

public class MyModelBinder: IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }


        MyModel model = new MyModel()
        {
            ExtraFields = new JObject()
        };

        var form = bindingContext.HttpContext.Request.Form;

        var properties = typeof(MyModel).GetProperties();
        foreach (var key in form.Keys)
        {
            var p = properties.FirstOrDefault(x => x.Name == key);
            var val = form[key];
            if (p != null)
            {
                p.SetValue(model, val); // throws exception
            }
            else
            {
                var v = StringValues.IsNullOrEmpty(val) ? null : val.ToString();
                model.ExtraFields.Add(key, v);
            }
        }

        bindingContext.Model = model;
        bindingContext.Result = ModelBindingResult.Success(model);

        return Task.CompletedTask;
    }
}

Issue
I am getting exception while setting the value of the model

Object of type 'Microsoft.Extensions.Primitives.StringValues' cannot be converted to type 'System.String

If possible, i would like to avoid checking type of the model's target property and then convert value to target type. This should happen implicitly.

Basically, for all the matching keys invoke ASP.NET's default binder, and for all remaining keys add value to ExtraFields


Solution

  • Object of type 'Microsoft.Extensions.Primitives.StringValues' cannot be converted to type 'System.String

    val is of type Microsoft.Extensions.Primitives.StringValues which cannot directly assign it's value to model fields.

    First,you can get the string form of the corresponding value directly through val.ToString().

    Since the type of each field is different, you can create a custom Convert.ChangeType method to dynamically convert the type by passing val.ToString() and p.PropertyType.

    More details, refer to this demo:

      public class MyModelBinder : IModelBinder
        {
            public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                if (bindingContext == null)
                {
                    throw new ArgumentNullException(nameof(bindingContext));
                }
     
                MyModel model = new MyModel()
                {
                    ExtraFields = new JObject()
                };
    
                var form = bindingContext.HttpContext.Request.Form;
    
                var properties = typeof(MyModel).GetProperties();
                foreach (var key in form.Keys)
                {
                    var p = properties.FirstOrDefault(x => x.Name == key);
                    var val = form[key];
                    if (p != null)
                    {
                       // call custom method ChangeType and pass two parameters.
                        p.SetValue(model, ChangeType(val.ToString(),p.PropertyType)); 
    
                    }
                    else
                    {
                        var v = StringValues.IsNullOrEmpty(val) ? null : val.ToString();
                        model.ExtraFields.Add(key, v);
                    }
                }
    
                bindingContext.Model = model;
                bindingContext.Result = ModelBindingResult.Success(model);
    
                return Task.CompletedTask;
            }
    
            // custom method
            public static object ChangeType(object value, Type conversion)
            {
                var t = conversion;
    
                if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
                {
                    if (value == null)
                    {
                        return null;
                    }
    
                    t = Nullable.GetUnderlyingType(t);
                }
    
                return Convert.ChangeType(value, t);
            }
        }
    

    Update

    @LP13 provides a simpler solution:

    TypeConverter typeConverter = TypeDescriptor.GetConverter(p.PropertyType); 
    object propValue = typeConverter.ConvertFromString(val); p.PropertyType));  
    p.SetValue(model, propValue); 
    

    Here is my test result:

    enter image description here