
Mixed model binding in ASP.NET Core using custom model binders
IModelBinder interface implementations usage for multiple source model binding
Patterns, conventions and standards are invented and enforced in order to make coding easier and more understandable for the developers, but in some cases certain patterns and conventions do not couple so easily and you have to rely on more on the actual technology you are using in order to overcome these complications.
One of the cases this happens is using CQRS, especially commands and REST. Although commands give clear separation of intention of some action, at the same time commands are especially tough to handle in soma cases because of their immutability. Commands are almost exclusively used with POST/PUT HTTP methods, but not all parameters (if you are following REST) are supplied in the command immutable model.
I will use simple ASP.NET Core project for this scenario which accepts one command and still follows REST by keeping command model immutability but binds command properties from multiple sources (route and body). Let's start with the command first
public class UpdateProduct { public Guid Id { get; } public String Name { get; } public String Description { get; } public UpdateProduct(Guid id, string name, string description) { Id = id; Name = name; Description = description; } }
For the command, main principle of the CQRS which is immutability is in place and instance of a command cannot be modified once it is created. All property values are supplied in the class constructor.
As for the REST, we need to focus a bit on the controller now (command handling is omitted for the clarity but you can read more about registering command handlers in Automatic CQRS handler registration in ASP.NET Core with reflection article and if you want to take it to the next level, you can check out Using dispatcher class to resolve CQRS commands and queries in ASP.NET Core. )
[Route("api/[controller]")] [ApiController] public class ProductsController : ControllerBase { [HttpPut("{Id}")] public IActionResult Put(UpdateProduct update) { //TODO: Handle the command return Ok(); } }
Here we are supplying the product id in the route. This will allow us to build GET/PUT/DELETE on the same route with the different HTTP command and therefore we embrace REST concepts for our API facade.
{ "name": "Logitech MX Master 2S", "description": "Wireless mouse for Windows and MAC" }
Unfortunately, although we are following both concepts of CQRS and REST at the same time, this code just simply does not work. ASP.NET Core does not recognize that part of the data (Id) is coming from the route and the rest is coming from the HTTP PUT message body. We just simply wont get Id binded to the command class instance
Guid is struct and therefore it is non-nullable, so instead of having property Id equals to null, it will be set to Guid.Empty which is a structure which has all zeros 00000000-0000-0000-0000-000000000000
To the rescue comes custom model binder which is simple implementation of IModelBinder interface where we can put our binding logic and involve multiple sources (route/querystring/headers/body) in order to build out immutable model (command) instance. Here is a simple logic that reads Id property value from the route, then reads the value from the request body and finally sets the id property value of the instance deserialized from the body
public class RouteIdBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { //Get Id value from the route var routeIdStringValue = bindingContext.ActionContext.RouteData.Values["Id"] as String; //Get command model payload (JSON) from the body String valueFromBody; using (var streamReader = new StreamReader(bindingContext.HttpContext.Request.Body)) { valueFromBody = streamReader.ReadToEnd(); } //Deserilaize body content to model instance var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType; var modelInstance = JsonConvert.DeserializeObject(valueFromBody, modelType); //If Id is available and it is Guid then try to set the model instance property if (!String.IsNullOrWhiteSpace(routeIdStringValue) && Guid.TryParse(routeIdStringValue, out var routeIdValue)) { var idProperty = modelType.GetProperties().FirstOrDefault(p => p.Name.Equals("Id", StringComparison.InvariantCultureIgnoreCase)); if (idProperty != null) { //NOTE: Throws System.ArgumentException idProperty.SetValue(modelInstance, routeIdValue); } } bindingContext.Result = ModelBindingResult.Success(modelInstance); return Task.CompletedTask; } }
You can see that the line which actually sets the property is actually not working (commented in the code snippet). It looks like it is just a simple reflection call for setting the property value, but it will not work because command instance is, if you remember, immutable and therefore we will get System.ArgumentException.
Let's see this scenario in a runtime, but first, we need to involve our binder in the controller action in order to have our binder in the request flow
[Route("api/[controller]")] [ApiController] public class ProductsController : ControllerBase { [HttpPut("{Id}")] public IActionResult Put([ModelBinder(BinderType = typeof(RouteIdBinder))]UpdateProduct update) { //TODO: Handle the command return Ok(); } }
Once we run the project and hit it with the same message we initialy used from the POSTMAN, we will get the following exception
It is clear from the exception message that we have problem because of the immutability of the model. Our command model Id property does not have the setter and therefore we cannot set the value. Seems like the dead end, but if you worked with older versions of .NET Framework you would know that every property has it's own, private backing field which actually holds the data fro the property. This is still the case, but over time, Microsoft added syntactic sugar to make properties more readable, while still relying on the auto-generated backing fields in the background. Therefore we can still set the property, not through the setter method (because in our case it does not exist at all) but through the backing field by setting it's value.
I already wrote about accessing backing fields with reflection back in 2014, you can check article Access auto property backing field with reflection. For our purpose, I made it more reusable by putting the backing fields setting logic in an extension method, so it makes it a lot easier to re-use
public static class ReflectionExtensions { public static void ForceSetValue(this PropertyInfo propertyInfo, object obj, object value) { var backingFieldInfo = obj.GetType().GetField($"<{propertyInfo.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); if (backingFieldInfo != null) { backingFieldInfo.SetValue(obj, value); } } }
And we just use our new extension method in the custom IModelBinder implementation class
public class RouteIdBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { //Get Id value from the route var routeIdStringValue = bindingContext.ActionContext.RouteData.Values["Id"] as String; //Get command model payload (JSON) from the body String valueFromBody; using (var streamReader = new StreamReader(bindingContext.HttpContext.Request.Body)) { valueFromBody = streamReader.ReadToEnd(); } //Deserilaize body content to model instance var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType; var modelInstance = JsonConvert.DeserializeObject(valueFromBody, modelType); //If Id is available and it is Guid then try to set the model instance property if (!String.IsNullOrWhiteSpace(routeIdStringValue) && Guid.TryParse(routeIdStringValue, out var routeIdValue)) { var idProperty = modelType.GetProperties().FirstOrDefault(p => p.Name.Equals("Id", StringComparison.InvariantCultureIgnoreCase)); if (idProperty != null) { idProperty.ForceSetValue(modelInstance, routeIdValue); } } bindingContext.Result = ModelBindingResult.Success(modelInstance); return Task.CompletedTask; } }
We are not ready for the test run of the mixed model binding. I will set the breakpoint in the controller method and quick-watch the model property values. For test I use the same POSTMAN request from previous runs
Now we have both body and route values bind to the command model instance and at the same time we are stick to both REST and CQRS principles. With decorative style of model binders we can reuse the same binder without expanding our code significantly and keep it clean and at the same time follow conventions and patterns.
References
- Automatic CQRS handler registration in ASP.NET Core with reflection
- Using dispatcher class to resolve commands and queries in ASP.NET Core
- IModelBinder Interface
- Ignoring properties from controller action model in Swagger using JsonIgnore
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article