Связывание моделей JSON в .Net Core 2.2 Web API - PullRequest
0 голосов
/ 26 ноября 2018

Я просто потратил много времени, пытаясь заставить работать пользовательский ComplexTypeModelBinder.Что бы я ни делал, это никогда не работало.Оказывается, это работает только тогда, когда данные помещаются в виде данных формы;когда вы публикуете объект JSON (в моем случае из формы Swagger "try"), ComplexTypeModelBinder никогда не вызывает метод SetProperty.

У меня много моделей, некоторые более сложные, чем другие,и я аннотировал некоторые свойства с пользовательским атрибутом.Всякий раз, когда это свойство связано, я хочу, чтобы оно было « normalized » (применить к нему некоторое «форматирование»), чтобы к моменту проверки модели валидатор мог видеть «нормализованные» данные вместо пользователя-неданные данные.

Я действительно, действительно, хочу сохранить текущее поведение привязки модели, потому что оно в настоящее время работает нормально, но с одним исключением, что аннотированные свойства обрабатываются некоторым кодом, реализованным мной.Все остальные свойства и поведение должны быть сохранены как есть.Вот почему я надеялся наследовать от ComplexTypeModelBinder, но, как оказалось, это не сработает, если данные помещены в формате JSON.

Моя (пример) модель выглядит следующим образом:

public class MyComplexModel
{
    public int Id { get; set; }
    public string Name { get; set; }

    [FormatNumber(NumberFormat.E164)]
    public string PhoneNumber { get; set; }
}

Мой метод контроллера выглядит следующим образом:

[HttpPost]
public MyComplexModel Post(MyComplexModel model)
{
    return model;
}

Мой (не работает) пользовательский ComplexTypeModelBinder выглядит так:

public class MyModelBinder : ComplexTypeModelBinder
{
    private readonly INumberFormatter _numberformatter;

    private static readonly ConcurrentDictionary<Type, Dictionary<string, FormatNumberAttribute>> _formatproperties = new ConcurrentDictionary<Type, Dictionary<string, FormatNumberAttribute>>();

    public MyModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, INumberFormatter numberFormatter, ILoggerFactory loggerFactory)
        : base(propertyBinders, loggerFactory)
    {
        _numberformatter = numberFormatter;
    }

    protected override object CreateModel(ModelBindingContext bindingContext)
    {
        // Index and cache all properties having the FormatNumber Attribute
        _formatproperties.GetOrAdd(bindingContext.ModelType, (t) =>
        {
            return t.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(FormatNumberAttribute))).ToDictionary(pi => pi.Name, pi => pi.GetCustomAttribute<FormatNumberAttribute>(), StringComparer.OrdinalIgnoreCase);
        });
        return base.CreateModel(bindingContext);
    }

    protected override Task BindProperty(ModelBindingContext bindingContext)
    {
        return base.BindProperty(bindingContext);
    }

    protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
    {
        if (_formatproperties.TryGetValue(bindingContext.ModelType, out var props) && props.TryGetValue(modelName, out var att))
        {
            // Do our formatting here
            var formatted = _numberformatter.FormatNumber(result.Model as string, att.NumberFormat);
            base.SetProperty(bindingContext, modelName, propertyMetadata, ModelBindingResult.Success(formatted));
        } else
        {
            // Do nothing
            base.SetProperty(bindingContext, modelName, propertyMetadata, result);
        }
    }
}

(фактические MyModelBinder проверки дляFormatNumber атрибут и обрабатывает свойство соответственно, но я оставил его для краткости, так как это на самом деле не имеет значения).

И мой ModelBinderProvider:

public class MyModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        var modelType = context.Metadata.ModelType;
        if (!typeof(MyComplexModel).IsAssignableFrom(modelType))
            return null;

        if (!context.Metadata.IsComplexType || context.Metadata.IsCollectionType)
            return null;

        var propertyBinders = context.Metadata.Properties
            .ToDictionary(modelProperty => modelProperty, context.CreateBinder);

        return new MyModelBinder(
            propertyBinders,
            (INumberFormatter)context.Services.GetService(typeof(INumberFormatter)),
            (ILoggerFactory)context.Services.GetService(typeof(ILoggerFactory))
        );
    }
}

И, конечно,Я добавил провайдера в класс StartUp:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(config =>
    {
        config.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
    }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Опять же, это прекрасно работает, когда данные публикуются в виде данных формы, а не в виде JSON.Каков будет правильный способ реализовать это?Я где-то читал, что я должен смотреть не в направлении ModelBinding, а в направлении «преобразователей JSON», но я не нашел ничего, что действительно работало (пока).


Edit: Я создал git-репозиторий, чтобы продемонстрировать мою проблему здесь .Чтобы увидеть мою проблему, установите точку останова здесь в TestController, где модель возвращается в методе Post.Начать проект;простая веб-страница будет показана с двумя кнопками.Слева будут размещены данные формы в виде данных формы, и вы увидите, что модель возвращается с обратным номером телефона (в качестве примера).Нажмите правую кнопку, и данные будут опубликованы в виде модели JSON.Обратите внимание, что возвращаемая модель имеет значения 0 id и null для обоих свойств.

...