Apply Custom Model Binder to Object Property in asp.net core

十年热恋 提交于 2019-12-03 22:28:49

问题


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:


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

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