0

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

LP13
  • 30,567
  • 53
  • 217
  • 400
  • Per documentation: `(StringValues) Represents zero/null, one, or many strings in an efficient way.` Therefore an implement conversion to a single string value is not possible. Maybe glance through the MVC source and see how it's done there? – asawyer Jul 14 '20 at 19:08
  • Hmm I might be wrong - This answer seems to point towards an implicit conversion existing - https://stackoverflow.com/a/48189292/426894 – asawyer Jul 14 '20 at 19:09
  • Yep I don't know how I missed this the first time around. https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.primitives.stringvalues.op_implicit?view=dotnet-plat-ext-3.1#Microsoft_Extensions_Primitives_StringValues_op_Implicit_Microsoft_Extensions_Primitives_StringValues__System_String – asawyer Jul 14 '20 at 20:53
  • yes but i am getting exception – LP13 Jul 14 '20 at 21:09

1 Answers1

1

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

LouraQ
  • 6,443
  • 2
  • 6
  • 16
  • ye thats what i end up doing. My change type code is `TypeConverter typeConverter = TypeDescriptor.GetConverter(p.PropertyType); object propValue = typeConverter.ConvertFromString(val);` – LP13 Jul 15 '20 at 15:05
  • @LP13, yes, the method you provided will be simpler, you can add a reply and accept it as an answer, or accept my reply as an answer (I have added your idea), this will help others with similar problems to find a solution more easier. – LouraQ Jul 16 '20 at 02:06
  • However this would not bind array or complex object. Thats why i was looking for option to use default binding for known properties, and add all remaining keys into JObject – LP13 Jul 16 '20 at 03:12