1

I have the following view model:

public class FormViewModel {
    [Required, StringLength(100)]
    public string Name { get; set; }

    private object _parameters = null;
    public object Parameters {
        get {
            if (_parameters == null)
                _parameters = Activator.CreateInstance(Type.GetType("CustomParameters"));
            return _parameters;
        }
        set {
            _parameters = value;
        }
    }
}

Where CustomParameters looks like:

public class CustomParameters {
    [Required]
    public string Text { get; set; }
}

Now If I post the following form data:

"Name" => "Foo"
"Parameters.Text" => "Bar"

The "Name" property is correctly set, however the "Parameters.Text" property is set to null.

Please note that the above scenario has been simplified and the Parameters needs to support binding to multiple custom types.

Edit - I've added the following code I used in ASP.NET MVC but ASP.NET Core's model binding looks to have been rewritten and I can't see what I need to do:

public class IRuntimeBindableModelBinder : DefaultModelBinder {
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        var newBindingContext = new ModelBindingContext() {
            // In the original method you have:
            // ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => bindingContext.Model, typeof(TModel)),
            ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => bindingContext.Model, bindingContext.Model.GetType()),
            ModelName = bindingContext.ModelName,
            ModelState = bindingContext.ModelState,
            PropertyFilter = bindingContext.PropertyFilter,
            ValueProvider = bindingContext.ValueProvider
        };

        return base.BindModel(controllerContext, newBindingContext);
    }
}

I'd appreciate it if someone could help.

Thanks

8
  • I'm failing to see the need for this? Is CustomParameters generic, or you don't know the incoming fields beforehand? Commented Apr 19, 2019 at 20:28
  • I have hard coded the type in the above example. But the parameters type can change. For example say I am posting back the parameters for a widget, whose parameters vary based on the type of widget. Commented Apr 20, 2019 at 8:50
  • DO you have any metadata in the incoming request which tells you which model it is? Meaning, how do you figure it out? Reflection? Commented Apr 23, 2019 at 7:11
  • I actually post the paramters type alongside the name and parameters. This is sent when the user selects the appropriate widget type. It also does an ajax call to populate the view with the appropriate parameters class. Also it's worth noting in ASP.NET MVC I made the parameters class implement the interface IRuntimeBindable (this would be the "CustomParameters" class in the above example and it has no members). However I'm open to changing this to use an attribute against the property or some other idea. Commented Apr 23, 2019 at 7:53
  • I'm thinking you can do this by implementing a custom model binder.. I'm doing some tests.. let's see :) Commented Apr 23, 2019 at 7:54

1 Answer 1

4
+50

This can be done by a custom ModelBinder. The problem here is that .NET doesn't know which type is stored into the object Property, so by default it's null.

You need to know the target Type (either by the Name or an additional Type property), then you can create a ModelBinder like this:

public class MyModelBinder : IModelBinder
{
    private readonly IModelMetadataProvider _modelMetadataProvider;
    private readonly IModelBinderFactory _modelBinderFactory;

    public MyModelBinder(IModelMetadataProvider modelMetadataProvider, IModelBinderFactory modelBinderFactory)
    {
        _modelMetadataProvider = modelMetadataProvider;
        _modelBinderFactory = modelBinderFactory;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var typeValue = bindingContext.ValueProvider.GetValue(nameof(ComplexModel.Type)).Values;
        var nameValue = bindingContext.ValueProvider.GetValue(nameof(ComplexModel.Name)).Values;
        var finalModel = new ComplexModel
        {
            Name = nameValue,
            Type = typeValue
        };
        var innerType = LookupType(typeValue);
        if (innerType != null)
        {
            finalModel.Parameters = Activator.CreateInstance(innerType);
            var modelMetadata = _modelMetadataProvider.GetMetadataForType(innerType);
            var modelBinder = _modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
            {
                Metadata = modelMetadata,
                CacheToken = modelMetadata
            });

            var modelName = bindingContext.BinderModelName == null ? "Parameters" : $"{bindingContext.BinderModelName}.Parameters";

            using (var scope = bindingContext.EnterNestedScope(modelMetadata, modelName, modelName, finalModel.Parameters))
            {
                await modelBinder.BindModelAsync(bindingContext);
            }
        }

        bindingContext.Result = ModelBindingResult.Success(finalModel);
        return;
    }

    //NOTE: this maps a type string to a Type.  
    //DO NOT transmit a type FullName and use reflection to activate, this could cause a RCE vulnerability.
    private Type LookupType(string type)
    {
        switch (type)
        {
            case "text":
                return typeof(TextParam);

            case "int":
                return typeof(IntParam);
        }
        return null;
    }
}

//Sample of ComplexModel classes
[ModelBinder(typeof(MyModelBinder))]
public class ComplexModel
{
    public string Name { get; set; }

    public string Type { get; set; }

    public object Parameters { get; set; }
}

public class TextParam
{
    public string Text { get; set; }
}

public class IntParam
{
    public int Number { get; set; }
}

NOTE: When doing custom deserialization with a Type, it is important to limit the list of allowed types to be deserialized. If you would accept a type's FullName and use reflection to activate, this could cause a RCE vulnerability since there are some types in .NET that execute code when a property is set.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, I’ll check this first thing in the morning. Please note that security is not an issue as it’s hosted on a dedicated server and only admins will have access to it. Please could you offer an alternative option if security is not an issue. Thanks
Again, I don't recommend it. if such a bug occurs then everyone has potentially access to that server, that has access to your Controller with that ModelBinder. - However here is an example to activate a Type with reflection: gist.github.com/hiiru/f106fd974564b8d74f9ed3253d3ec632 In this exmaple there is a restriction that it only finds types based on a base-type, but you could easily change it if you really need it. (note: only works with classes that have a parameter-less constructor)
I've just had a chance to test this and it works perfectly, thanks. I was looking at the source code for some of the other binders and I had a feeling bindingContext.EnterNestedScope would come into play. Thanks once more.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.