I am trying to apply custom model binder for DateTime type property of model. Here is the IModelBinder and IModelBinderProvider implementations.
public class DateTimeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(DateTime))
{
return new BinderTypeModelBinder(typeof(DateTime));
}
return null;
}
}
public class DateTimeModelBinder : IModelBinder
{
private string[] _formats = new string[] { "yyyyMMdd", "yyyy-MM-dd", "yyyy/MM/dd"
, "yyyyMMddHHmm", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm"
, "yyyyMMddHHmmss", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss"};
private readonly IModelBinder baseBinder;
public DateTimeModelBinder()
{
baseBinder = new SimpleTypeModelBinder(typeof(DateTime), null);
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
if (DateTime.TryParseExact(value, _formats, new CultureInfo("en-US"), DateTimeStyles.None, out DateTime dateTime))
{
bindingContext.Result = ModelBindingResult.Success(dateTime);
}
else
{
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, $"{bindingContext} property {value} format error.");
}
return Task.CompletedTask;
}
return baseBinder.BindModelAsync(bindingContext);
}
}
And here is the model class
public class Time
{
[ModelBinder(BinderType = typeof(DateTimeModelBinder))]
public DateTime? validFrom { get; set; }
[ModelBinder(BinderType = typeof(DateTimeModelBinder))]
public DateTime? validTo { get; set; }
}
And here is the controller action method.
[HttpPost("/test")]
public IActionResult test([FromBody]Time time)
{
return Ok(time);
}
When tested, the custom binder is not invoked but the default dotnet binder is invoked. According to the official documentation,
ModelBinder attribute could be applied to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.
But it seems not working with my code.
1. Reason
According to the [FromBody]Time time
in your action, I guess you're sending a payload with Content-Type
of application/json
. In that case, when a josn payload received, the Model Binding System will inspect the parameter time
and then try to find a proper binder for it. Because the context.Metadata.ModelType
equals typeof(Time)
instead of the typeof(DateTime)
, and there's no custom ModelBinder for typeof(Time)
, your GetBinder(context)
method will return a null
:
public class DateTimeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(DateTime)) // not typeof(Time)
{
return new BinderTypeModelBinder(typeof(DateTime));
}
return null;
}
}
Thus it falls back to the default model binder for application/json. The default json model binder uses Newtonsoft.Json
under the hood and will simply deserialize the hole payload as an instance of Time
. As a result, your DateTimeModelBinder
is not invoked.
2. Quick Fix
One approach is to use application/x-www-form-urlencoded
(avoid using the application/json
)
Remove the [FromBody]
attribute:
[HttpPost("/test2")]
public IActionResult test2(Time time)
{
return Ok(time);
}
and send the payload in the format of application/x-www-form-urlencoded
POST https://localhost:5001/test2
Content-Type: application/x-www-form-urlencoded
validFrom=2018-01-01&validTo=2018-02-02
It should work now.
3. Working with JSON
Create a custom converter as below :
public class CustomDateConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public static string[] _formats = new string[] {
"yyyyMMdd", "yyyy-MM-dd", "yyyy/MM/dd"
, "yyyyMMddHHmm", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm"
, "yyyyMMddHHmmss", "yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss"
};
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var dt= reader.Value;
if (DateTime.TryParseExact(dt as string, _formats, new CultureInfo("en-US"), DateTimeStyles.None, out DateTime dateTime))
return dateTime;
else
return null;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value as string);
}
}
I simply copy your code to format date.
Change your Model as below :
public class Time
{
[ModelBinder(BinderType = typeof(DateTimeModelBinder))]
[JsonConverter(typeof(CustomDateConverter))]
public DateTime? validFrom { get; set; }
[ModelBinder(BinderType = typeof(DateTimeModelBinder))]
[JsonConverter(typeof(CustomDateConverter))]
public DateTime? validTo { get; set; }
}
And now you can receive the time using [FromBody]
[HttpPost("/test")]
public IActionResult test([FromBody]Time time)
{
return Ok(time);
}
来源:https://stackoverflow.com/questions/54661799/apply-custom-model-binder-to-object-property-in-asp-net-core