Modelbinding JSON in .Net Core 2.2 Web API

我们两清 提交于 2019-12-23 09:15:56

问题


I just wasted a lot of hours with trying to get a custom ComplexTypeModelBinder to work. Whatever I did, it never worked. As it turns out, this only works when the data is POSTed as form data; when you post a JSON object (in my case from a Swagger "try out" form) the ComplexTypeModelBinder never invokes the SetProperty method.

I have a lot of models, some more complex than others, and I have annotated some of the properties with a custom attribute. Whenever that property is bound I want it to 'normalized' (apply some 'formatting' to it) so that by the time the model gets validated the validator gets to see the 'normalized' data instead of the user-entered data.

I really, really, want to keep the current modelbinding behavior because that currently works fine but with the one exception that the annotated properties are processed by some code implemented by me. All other properties and behavior should be kept as-is. That is why I hoped to inherit from ComplexTypeModelBinder, but, as it turns out, this doesn't work if data is POSTed as JSON.

My (example) model looks like:

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

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

My controller method looks like this:

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

My (not working) custom ComplexTypeModelBinder looks like:

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

(The actual MyModelBinder checks for the FormatNumber attribute and handles the property accordingly, but I left it out for brevity since it doesn't really matter).

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

And ofcourse, I added the provider in the StartUp class:

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

Again, this works fine when data is posted as form-data but not when posted as JSON. What would be the correct way to implement this? I have read somewhere that I shouldn't be looking in the ModelBinding direction but in the "JSON converters" direction but I haven't found anything that actually worked (yet).


Edit: I have created a git repository to demonstrate my problem here. To see my problem, set a breakpoint here in the TestController where the model is returned in the Post method. Start the project; a simple webpage will be shown with two buttons. The left one will post the form data as, well, form-data and you will see the model being returned with a reversed phonenumber (as an example). Click the right button and the data will be posted as a JSON model. Notice the model being returned having a 0 id and null values for both properties.

来源:https://stackoverflow.com/questions/53482704/modelbinding-json-in-net-core-2-2-web-api

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!