Search code examples
asp.net-coremodel-binding

Custom DateTime model binder for ApiController


I am creating API using ASP.NET Web Api and ApiController. There is a need to adjust all datetime values to utc. Let's say there is a model

    public class Product
    {
        public string Name { get; set; }

        public DateTime Created { get; set; }
    }

And Controller:

    [ApiController]
    public class SomeController : ControllerBase
    {
        [HttpPost]
        [Route("api/profucts")]
        public async Task<ActionResult> Create(Product product)
        {
            //.... do smth
            return Ok();
        }

Is there a way to plug into binding of DateTime properties of model classes and do some adjustments there?

Creating custom datetime model binder and and registering it globally via ...config.ModelBinderProviders.Insert(0, new MyDateTimeBinderProvider()); doesn't work as model binder never gets hit.

Binder code:

public class DateTimeBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (string.IsNullOrEmpty(valueProviderResult.FirstValue))
        {
            return null;
        }
        DateTime value;
        if (DateTime.TryParse(valueProviderResult.FirstValue, null, DateTimeStyles.AdjustToUniversal, out value))
        {
            bindingContext.Result = ModelBindingResult.Success(value.ToUniversalTime());
        }
        else
        {
          //add error
        }
        return Task.CompletedTask;
    }
}

Solution

  • Creating custom datetime model binder and and registering it globally via ...config.ModelBinderProviders.Insert(0, new MyDateTimeBinderProvider()); doesn't work as model binder never gets hit.

    Based on your scenario and description, it seems you were not able to hit your model binder. To call model binder within your MyDateTimeBinderProvider we ought to define your modelbinder name in BinderTypeModelBinder.

    In my test I have successfully able to call, I am mostly concentrating why not your binder get hitted rather the conversion stuff. You could have a look below:

    Model:

    Let's assume we have following model:

    public class MyProductClass
        {
            public string Name { get; set; }
    
            public DateTime Created { get; set; }
        }
    

    Model Binder:

    public class DateConversionModelBinder : IModelBinder
        {
    
            public DateConversionModelBinder()
            {
    
            }
    
            public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                if (bindingContext == null)
                {
                    throw new ArgumentNullException(nameof(bindingContext));
                }
    
                var model = new MyProductClass();
    
                bindingContext.Result = ModelBindingResult.Success(model);
                return Task.CompletedTask;
            }
        }
    

    Note: I have simply created a demo DateConversionModelBinder. No additional logic has written.

    In case you need conversion code:

    public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                if (bindingContext == null)
                {
                    throw new ArgumentNullException(nameof(bindingContext));
                }
                var request = bindingContext.HttpContext.Request;
                var stream = request.Body;// At the begining it holding original request stream                    
                var originalReader = new StreamReader(stream);
                var originalContent = originalReader.ReadToEndAsync();
    
               
                var retrieveObject = JsonConvert.DeserializeObject<dynamic>(originalContent.Result);
    
                var model = new MyProductClass();
    
    
                DateTime dateTime = Convert.ToDateTime(retrieveObject.created);
    
    
                TimeZoneInfo cstZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");
    
                DateTime cstTime = TimeZoneInfo.ConvertTime(dateTime, TimeZoneInfo.Local, cstZone);
    
                DateTime fromLocalTimeToUTC = TimeZoneInfo.ConvertTimeToUtc(cstTime, cstZone);
    
    
    
                model.Created = fromLocalTimeToUTC;
                model.Name = retrieveObject.name;
    
    
    
                bindingContext.Result = ModelBindingResult.Success(model);
                return Task.CompletedTask;
            }
    

    Model Binding Provider:

    public class ProductEntityBinderProvider : IModelBinderProvider
        {
            public IModelBinder GetBinder(ModelBinderProviderContext context)
            {
                if (context == null)
                {
                    throw new ArgumentNullException(nameof(context));
                }
    
                if (context.Metadata.ModelType == typeof(MyProductClass))
                {
    
                    return new BinderTypeModelBinder(typeof(DateConversionModelBinder));
                }
    
                return null;
            }
        }
    

    Note: As you can see, I have defined Model and ModelBinder Name within th provider.

    Program.cs File:

    builder.Services.AddControllers(options => { options.ModelBinderProviders.Insert(0, new ProductEntityBinderProvider()); });
    

    Output:

    enter image description here

    Note: As you can see, I can hit DateConversionModelBinder via ProductEntityBinderProvider. Please have a look on the official document here for more details.