nfplee
nfplee

Reputation: 7997

ASP.NET Core MVC - Binding to model at runtime

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

Upvotes: 1

Views: 2612

Answers (1)

hiiru
hiiru

Reputation: 456

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.

Upvotes: 4

Related Questions