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;
}
}