问题
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