I'm trying to implement custom binder to allow comma separated list in query string. Based on this blog post and official documentation I have created some solution. But instead of using attributes to decorate wanted properties I want to make this behavior default for all collections of simple types (IList<T>, List<T>, T[], IEnumerable<T>
... where T
is int, string, short
...)
But this solution looks very hacky because of manual creation of ArrayModelBinderProvider
, CollectionModelBinderProvider
and replacing bindingContext.ValueProvider
with CommaSeparatedQueryStringValueProvider
and I believe there should be a better way to achieve the same goal.
public class CommaSeparatedQueryBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var bindingSource = context.BindingInfo.BindingSource;
if (bindingSource != null && bindingSource != BindingSource.Query)
{
return null;
}
if (!context.Metadata.IsEnumerableType)
{
return null;
}
if (context.Metadata.ElementMetadata.IsComplexType)
{
return null;
}
IModelBinderProvider modelBinderProvider;
if (context.Metadata.ModelType.IsArray)
{
modelBinderProvider = new ArrayModelBinderProvider();
}
else
{
modelBinderProvider = new CollectionModelBinderProvider();
}
var binder = modelBinderProvider.GetBinder(context);
return new CommaSeparatedQueryBinder(binder);
}
}
public class CommaSeparatedQueryBinder : IModelBinder
{
private readonly IModelBinder _modelBinder;
public CommaSeparatedQueryBinder(IModelBinder modelBinder)
{
_modelBinder = modelBinder;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var valueProviderLazy = new Lazy<CommaSeparatedQueryStringValueProvider>(() =>
new CommaSeparatedQueryStringValueProvider(bindingContext.HttpContext.Request.Query));
if (bindingContext.ValueProvider is CompositeValueProvider composite
&& composite.Any(provider => provider is QueryStringValueProvider))
{
var queryStringValueProvider = composite.First(provider => provider is QueryStringValueProvider);
var index = composite.IndexOf(queryStringValueProvider);
composite.RemoveAt(index);
composite.Insert(index, valueProviderLazy.Value);
await _modelBinder.BindModelAsync(bindingContext);
composite.RemoveAt(index);
composite.Insert(index, queryStringValueProvider);
}
else if(bindingContext.ValueProvider is QueryStringValueProvider)
{
var originalValueProvider = bindingContext.ValueProvider;
bindingContext.ValueProvider = valueProviderLazy.Value;
await _modelBinder.BindModelAsync(bindingContext);
bindingContext.ValueProvider = originalValueProvider;
}
else
{
await _modelBinder.BindModelAsync(bindingContext);
}
}
}
public class CommaSeparatedQueryStringValueProvider : QueryStringValueProvider
{
private const string Separator = ",";
public CommaSeparatedQueryStringValueProvider(IQueryCollection values)
: base(BindingSource.Query, values, CultureInfo.InvariantCulture)
{
}
public override ValueProviderResult GetValue(string key)
{
var result = base.GetValue(key);
if (result == ValueProviderResult.None)
{
return result;
}
if (result.Values.Any(x => x.IndexOf(Separator, StringComparison.OrdinalIgnoreCase) > 0))
{
var splitValues = new StringValues(result.Values
.SelectMany(x => x.Split(Separator))
.ToArray());
return new ValueProviderResult(splitValues, result.Culture);
}
return result;
}
}
Startup.cs
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new CommaSeparatedQueryBinderProvider());
})