'.NET 6 Custom model binding - fall back to default binding?
I am trying to change the way one specific property of my Specification models are bound in .NET 6. I have a bunch of Specification classes that inherit from SpecificationBase<>. There is an int[]? Ids { get; set; } property that is part of the base class. I created a custom binder to take a CSV string of numbers and convert them to an int[]? array. (Note: I am aware the arrays can be passed to controller actions in the form ?Ids=1&Ids3=&Ids=5 which will bind correctly to arrays).
In older examples I've seen on the web, custom model binders inherited from DefaultModelBinder. We could then use base.BindModel or base.BindProperty. I cannot find anything similar for .Net 6. There is a ComplexObjectModelBinder but it is sealed.
Everything below works okay as long as the Specification properties are simple types. The TypeDescriptor conversion seems to work well for those. I'm worried about when there are complex types. Is there a way to fall back to default binding for everything other than my Ids property?
public class SpecificationModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
ArgumentNullException.ThrowIfNull(bindingContext);
// If not a specification, skip
if (bindingContext.ModelType.BaseType?.GetGenericTypeDefinition() != typeof(SpecificationBase<>))
return Task.CompletedTask;
// Type of specification
var type = bindingContext.ModelType;
// Create default instance of the specification
var model = Activator.CreateInstance(type);
// We are passing the specification parameters via the querystring, so loop over the querystring keys to set specification properties
foreach (var name in bindingContext.HttpContext.Request.Query.Keys)
{
// Check to make sure there is a matching property. If not continue.
var property = type.GetProperty(name);
if (property is null)
continue;
if (property.Name == "Ids")
{
// Custom binding to convert int csv string into int[] array
string? idsString = bindingContext.ValueProvider.GetValue("Ids").FirstValue;
int[]? ids = null;
if (!string.IsNullOrEmpty(idsString))
ids = idsString.Split(',', StringSplitOptions.RemoveEmptyEntries).Where(x => int.TryParse(x, out _)).Select(x => int.Parse(x)).ToArray();
property.SetValue(model, ids);
}
else
{
// Is there a standard way to bind all of the other properties? This will only handle simple types
var value = bindingContext.ValueProvider.GetValue(property.Name).FirstValue;
var converter = TypeDescriptor.GetConverter(property.PropertyType);
var convertedValue = converter.ConvertFrom(value!);
property.SetValue(model, convertedValue);
}
}
// Set the result to our populated model
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Solution 1:[1]
If you are really just looking to have a custom model binder for just one property.
You can actually set the Binder on the property level specifically. https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-6.0#modelbinder-attribute
Going from what I can from your example:
class CountrySpecification : SpecificationBase<>
{
//Country specific properties handled by the default Binders
}
class SpecificationBase<>
{
[ModelBinder(typeof(SpecificationModelBinder))]
int[]? Ids { get; set; }
//Extra properties handled by the default Binders
}
Another added benefit is now your custom Binder handle your one property as well.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Cameron Thibeaux |
