Mixed model binding in ASP.NET Core

While powerful, default model binding in ASP.NET Core handles the basic use-cases. Anything you want to do beyond that — such as mixed model binding — requires a little work to get there.

Today we’re going to go over a few approaches you can take in order to accomplish model binding a single object from multiple sources. All code for today’s post is found on Github. Click here to skip right to the “solution”. It won’t offend me but you’ll miss some interesting tidbits if you do.

Framing the problem with mixed model binding

If you’ve used ASP.NET Core to build RESTful APIs, you’ve likely encountered some scenarios that didn’t quite fit what you wanted. An extremely common signature is for PUT endpoints: PUT somepath/{id} along with a message body.

Traditional model binding might have your method signature looking something like this: [HttpPut("{id}")] public IActionResult Put(int id, [FromBody] SomeRequest request) { ... }. The convention would then bind the id parameter from the route, and SomeRequest from the message body.

While this is fine, we may start to run into issues as we expand functionality or requirements a bit. For example, what happens when your model includes the Id as a property and you want validation to occur automatically in the pipeline? The answer is that any annotations you put on the Id property will cause it to fail since it doesn’t know to bind the Id from the route and the rest from the body.

Granted, you could have constraints on your route parameters to exclude invalid input. In that case, a route would simply never match and the caller would not have information why–only an Http 404 response.

Small sample

Take this sample PUT as an example:

[HttpPut("regular/{id}")]
public IActionResult Regular(int id, [FromBody] InferredDemoModel demoModel)
{
	if (!demoModel.Id.HasValue)
	{
		_logger.LogInformation("Model missing ID (normal behavior)");
		demoModel.Id = id;
	}

	_logger.LogInformation("Incoming model: {0}", System.Text.Json.JsonSerializer.Serialize(demoModel));
	return StatusCode(StatusCodes.Status204NoContent);
}

In this example, if you want the Id property on the model to be there you have to copy it manually.

One thing to note is that [FromBody] is required if you’re binding from JSON. It’ll infer and bind form data automatically.

Introducing Mixed Model Binding, Exhibit A

What prompted this post was an obscure block of code I recently encountered in one of the APIs at work. At first I thought it was the panacea I had been looking for specifically to solve the above problem. As I began experimenting with it for this post, however, I learned that the one major use-case I wanted it to solve, it couldn’t. That said, it is still great information and something I want to share.

Enter CompositeBindingSource. In all my years working with .NET, I had never stumbled across this class. Documentation at Microsoft is sparse. What’s more, doing a code search on GitHub only turned up two references to it; one of them fairly in-depth trying to use it, the other only a passing reference. Even so, I only found these because I was specifically looking for them after learning of its existence.

I asked my co-worker who wrote the code–Orifjon Narkulov— where he learned about it. He said “Google knows everything.” All kidding aside, he couldn’t remember where he came across it. I told him I’d credit him the find nonetheless.

So what is it?

As the name implies, it allows creating a binding attribute similar to [FromQuery], [FromRoute], etc., that handles multiple sources. The first example I ran across, in fact, was specifically that one (Path/Route and Query). My mind raced with possibilities as I thought of the use-case I wanted to handle (Body and Route). Could this be the solution?

Example

Here is an ultra basic example of creating that new binding source:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
public class FromRouteAndQueryAttribute : Attribute, IBindingSourceMetadata
{
	public BindingSource BindingSource { get; } = CompositeBindingSource.Create(
		new[] { BindingSource.Path, BindingSource.Query }, nameof(FromRouteAndQueryAttribute));
}

And here’s an example of making use of that attribute in an HttpPut action:

[HttpGet("composite-compiled/{id}")]
public IActionResult CompiledMultiSource([FromRouteAndQuery] DemoDynamicModel demoModel)
{
	_logger.LogInformation("Incoming model: {0}", System.Text.Json.JsonSerializer.Serialize(demoModel));
	return StatusCode(StatusCodes.Status204NoContent);
}

Caveats?

Now, like me, you’re probably wondering what other sources it can use. Any of the “non-greedy” ones. Peeking source for BindingSource reveals the following options: Body, Custom, Form, Header, ModelBinding, Path, Query, Services, Special, and FormFile.

Try adding BindingSource.Body and you get a message that you can’t add greedy sources. What does that mean? Basically, anything that reads from the request stream and doesn’t rewind it after done.

Making it slightly more dynamic

You may have noticed something though. If you make a bunch of these, you may end up with a bunch of combinations. I was curious what it would take to not have to pre-define them. *spoiler: this is stupid and you should predefine them*:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
public class FromMultiSourceAttribute : Attribute, IBindingSourceMetadata
{
	private Lazy<BindingSource> _bindingSourceLazy;

	public FromMultiSourceAttribute(string displayName)
	{
		DisplayName = displayName;
		_bindingSourceLazy = new Lazy<BindingSource>(() => CompositeBindingSource.Create(GetBindingSources(), DisplayName));
	}

	public string DisplayName { get; set; }
	public string[] BindingSources { get; set; }

	public BindingSource BindingSource => _bindingSourceLazy.Value;

	private IEnumerable<BindingSource> GetBindingSources()
	{
		var bsType = typeof(BindingSource);

		return BindingSources.Select(source =>
		{
			return bsType.GetField(source).GetValue(null) as BindingSource;
		});
	}
}

Here’s a sample putting it in action:

[HttpGet("composite-dynamic/{id}")]
public IActionResult DynamicMultiSource([FromMultiSource("DynamicRouteAndQuery", BindingSources = new[]
											{
												nameof(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.Path),
												nameof(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.Query)
											})] DemoDynamicModel demoModel)
{
	_logger.LogInformation("Incoming model: {0}", System.Text.Json.JsonSerializer.Serialize(demoModel));
	return StatusCode(StatusCodes.Status204NoContent);
}

But how do you mix Body and Route?

Yeah yeah, I know. You came here wanting to find the panacea to mixed model binding just like I did. So there are actually two answers here:

  1. Use HybridModelBinding. (caveats, which I’ll briefly discuss)
  2. Roll your own mixed model binder. (also has caveats)

Mixed model binding with HybridModelBinding – a brief explanation

HybridModelBinding is a small package built initially for use with ASP.NET Core 1.0 in 2016. Currently, it supports up to .NET Core 3.1. I’d mark that as the primary caveat — it doesn’t officially support ASP.NET Core 5.0 yet. While you *could* still import it, I wouldn’t recommend doing so until it has first-class support.

This package achieves mixed model binding in a flexible way by allowing access to both the IModelBinder and IValueProvider during binding. Furthermore, it allows you to customize “fallback” order to ensure which order things get bound in the event multiple sources match.

I’ve additionally tested with FluentValidation in the pipeline. It works, so that’s good. Given it works with FluentValidation, it should also work with the built-in DataAnnotation validation mechanism.

HybridModelBinding, example

What follows is an ultra-simplistic example of using HybridModelBinding. Please note that I don’t actually need to use [FromHybrid] in this case since default behavior is to use it implicitly on single-parameter actions.

[HttpPut("hybrid-model/{id?}")]
public IActionResult HybridModel([FromHybrid]DemoHybridModel demoModel)
{
	_logger.LogInformation("Incoming model: {0}", System.Text.Json.JsonSerializer.Serialize(demoModel));
	return StatusCode(StatusCodes.Status204NoContent);
}

// models
public class DemoHybridModel
{
	public int? Id { get; set; }

	public string Blah { get; set; }

	public NestedModel Nested { get; set; }
}

public class NestedModel
{
	public string Value1 { get; set; }
	public bool Value2 { get; set; }
}

You may note that this example uses the route and body to populate the model. Perhaps the Blah property comes from a querystring as well. The point is, however, I didn’t have to specify any of it. It just works.

I don’t know about you but I don’t entirely like “it just works” type things. I prefer things to be explicit so another developer coming it knows exactly where everything is supposed to come from.

Well guess what? HybridModelBinding allows you to be more explicit. You can set an attribute HybridBindProperty and explicitly set the source(s). Here’s an example from their Github documentation:

[HybridBindProperty(Source.Header, "X-Name", order: 5)]
[HybridBindProperty(new[] { Source.Body, Source.Form, Source.QueryString, Source.Route }, order: 10)]
public string Name { get; set; }

Rolling your own mixed IModelBinder

Gotta admit this is territory I really didn’t want to arrive at. Yes, it accomplishes my mission of mixed model binding specifically from the route and body. But… well… you know.

Luckily as Orifjon previously mentioned to me: “Google knows everything.” So does StackOverflow, for that matter. Ironically, the afore-linked question/answer is the same one that led me to HybridModelBinding. But I digress.

The nuts and bolts of this solution are basically the same as HybridModelBinding, though not nearly as fleshed out.

The general premise is that we need to implement a custom BindingSource (hey, remember those from CompositeBindingSource?). Next, we need to implement an attribute to decorate our class or properties with. After that, we need the actual IModelBinder and IModelBinderProvider implementations to wire all this together. Finally, we need to instruct the pipeline to actually use it.

Since the StackOverflow answer I linked goes through exactly all that, I don’t think I need to rehash it here. Suffice it to say my example repo on Github applies it in both a .NET Core 3.1 and ASP.NET Core 5.0 application (with my changes for 5.0).

Conclusion

Today we discussed multiple approaches for mixed model binding in ASP.NET Core. Our first attempt failed at mixing Body with other types but was still useful in many cases. We then looked at the HybridModelBinding nuget package and a “roll your own” solution with a custom IModelBinder. Both of the latter two work with validation

Credits

Photo by Usman Yousaf on Unsplash

Other interesting links and sources

If my post hasn’t quite satiated your quest for knowledge, here is a small list of sources I used in compiling today’s post: