Marko
Marko

Reputation: 1638

ASP.NET undesirable model binding issue - not getting all the data bound

Model binding in ASP.NET is not binding all the received data if the request contains the parmeter object name. I don't know how to describe it in a better way, but the sample I'm providing will make my issue clear. I start from a new "ASP.NET Core Web API" (.NET 8) project and change the content of WeatherForecastController.cs to:

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get([FromQuery] P p)
        {
            return Ok();
        }

        public class P
        {
            [BindProperty]
            public DateTime? Time { get; set; }
        }
    }
}

Sending the request GET https://localhost:7164/Test?time=2025-01-01 (use your local port) will result in P.Time to have the value 2025-01-01. As expected.

However, if the request changes to GET https://localhost:7164/Test?time=2025-01-01&p (note the second query parameter) P.Time will remain null.

Now, this is an oversimplified sample of my code (which is an API), but the essense of the issue is that, if the method parameter object name is for any reason added to the query, it will invalidate all the object properties, which will in turn result in incorrect errors sent back to the caller (like "you forgot to send the time", while in reality it was sent in the request), thus making the debugging much more complicated.

Is there any simple solution to this issue?

Upvotes: 1

Views: 51

Answers (1)

Zhi Lv
Zhi Lv

Reputation: 21636

However, if the request changes to GET https://localhost:7164/Test?time=2025-01-01&p (note the second query parameter) P.Time will remain null.

You could try to use the following methods to receive the parameter:

  1. Use [FromQuery] to specify the binding with a DateTime parameter instead of object parameter.

     [HttpGet]
     public IActionResult Get([FromQuery] DateTime Time)
     {
         return Ok();
     }
    

    The result as below:

    result1

  2. Use a custom model binder.

    Create a custom binder to filter data from the query string and bind to the object parameter:

     public class FilterParamsBinder : IModelBinder
     {
         public Task BindModelAsync(ModelBindingContext bindingContext)
         {
             var query = bindingContext.HttpContext.Request.Query;
    
             var filterParams = new P
             {
                 Time = query.ContainsKey("time") ? Convert.ToDateTime(query["time"]) : (DateTime?)null, 
             };
    
             bindingContext.Result = ModelBindingResult.Success(filterParams);
             return Task.CompletedTask;
         }
     }
     public class FilterParamsBinderProvider : IModelBinderProvider
     {
         public IModelBinder GetBinder(ModelBinderProviderContext context)
         {
             if (context.Metadata.ModelType == typeof(P))
             {
                 return new FilterParamsBinder();
             }
             return null;
         }
     }
    

    Then register the model binder:

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

    and apply it in the controller:

     [HttpGet]
     public IActionResult Get([FromQuery][ModelBinder(BinderType = typeof(FilterParamsBinder))] P p, [FromQuery] DateTime Time)
     { 
    
         return Ok();
     }
    

    The result as below:

    result2

Note: in the above samples, the request url is: https://localhost:7164/Test?time=2025-01-01&p

Upvotes: 1

Related Questions