ASP.NET Ядро MVC — привязка к модели во время выполнения

#c# #asp.net-core #asp.net-core-mvc

#c# #asp.net-ядро #asp.net-core-mvc

Вопрос:

У меня есть следующая модель представления:

 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;
        }
    }
}
 

Где CustomParameters выглядит так:

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

Теперь, если я опубликую следующие данные формы:

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

Свойство «Name» задано правильно, однако свойству «Parameters.Text» присвоено значение null.

Пожалуйста, обратите внимание, что приведенный выше сценарий был упрощен, и параметры должны поддерживать привязку к нескольким пользовательским типам.

Редактировать — я добавил следующий код, который я использовал в ASP.NET MVC , но ASP.NET Привязка модели ядра, похоже, была переписана, и я не вижу, что мне нужно делать:

 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);
    }
}
 

Я был бы признателен, если бы кто-нибудь мог помочь.

Спасибо

Комментарии:

1. Я не вижу необходимости в этом? Является CustomParameters общим, или вы заранее не знаете входящие поля?

2. Я жестко запрограммировал тип в приведенном выше примере. Но тип параметров может измениться. Например, скажем, я отправляю обратно параметры для виджета, параметры которого варьируются в зависимости от типа виджета.

3. Есть ли у вас какие-либо метаданные во входящем запросе, которые сообщают вам, какая это модель? То есть, как вы это выясняете? Отражение?

4. На самом деле я публикую тип параметров вместе с именем и параметрами. Это отправляется, когда пользователь выбирает соответствующий тип виджета. Он также выполняет вызов ajax для заполнения представления соответствующим классом параметров. Также стоит отметить в ASP.NET MVC Я сделал класс parameters реализующим интерфейс IRuntimeBindable (это будет класс «CustomParameters» в приведенном выше примере, и у него нет членов). Однако я открыт для изменения этого, чтобы использовать атрибут против свойства или какой-либо другой идеи.

5. Я думаю, вы можете сделать это, внедрив пользовательскую привязку модели.. Я провожу несколько тестов.. давайте посмотрим 🙂

Ответ №1:

Это может быть сделано с помощью пользовательского ModelBinder. Проблема здесь в том, что .NET не знает, какой тип хранится в свойстве object, поэтому по умолчанию он равен null.

Вам нужно знать целевой тип (либо по имени, либо по дополнительному свойству типа), тогда вы можете создать ModelBinder следующим образом:

 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; }
}
 

ПРИМЕЧАНИЕ: При выполнении пользовательской десериализации с типом важно ограничить список разрешенных типов для десериализации. Если вы принимаете полное имя типа и используете отражение для активации, это может привести к уязвимости RCE, поскольку в .NET есть некоторые типы, которые выполняют код при установке свойства.

Комментарии:

1. Спасибо, я проверю это первым делом утром. Пожалуйста, обратите внимание, что безопасность не является проблемой, поскольку он размещен на выделенном сервере, и доступ к нему будут иметь только администраторы. Пожалуйста, не могли бы вы предложить альтернативный вариант, если безопасность не является проблемой. Спасибо

2. Опять же, я не рекомендую это. если возникает такая ошибка, то у каждого есть потенциальный доступ к этому серверу, который имеет доступ к вашему контроллеру с этим ModelBinder. — Однако вот пример активации типа с отражением: gist.github.com/hiiru/f106fd974564b8d74f9ed3253d3ec632 В этом примере существует ограничение, заключающееся в том, что он находит только типы, основанные на базовом типе, но вы можете легко изменить его, если вам это действительно нужно. (примечание: работает только с классами, которые имеют конструктор без параметров)

3. У меня только что была возможность протестировать это, и это работает отлично, спасибо. Я просматривал исходный код для некоторых других связующих, и у меня возникло ощущение привязки Context. В игру вступит EnterNestedScope. Еще раз спасибо.