I need to customize model binding for a particular class Foo
that involves extending the normal binding logic with some additional post-processing (e.g., conditionally set null collection fields to an empty collection). I want to add this logic to model binding so that the results are available to action filters, etc.
The most direct approach would be to derive from ComplexTypeModelBinder
and override BindModelAsync
. However, that method is unfortunately not virtual.
Composition is the next alternative. I'm trying to create a FooModelBinder
class that has or obtains an instance of ComplexTypeModelBinder
. However, I can't figure out how to inject or resolve a ComplexTypeModelBinder
. Is this possible? Is there a better way to extend the functionality of ComplexTypeModelBinder
?
I finally realized that model binders are obtained from model binder providers, not via dependency injection. To properly instantiate my FooModelBinder
, I need to create a FooModelBinderProvider
. And to properly obtain an instance of ComplexTypeModelBinder
for composition, my provider needs access to a ComplexTypeModelBinderProvider
. In other words, to compose over a model binder, you also need to compose over a model binder provider.
Here's the provider. Note that we don't need to specify the exact type of the injected provider because we're simply wrapping the existing functionality of another model binder.
public class FooModelBinderProvider : IModelBinderProvider
{
private readonly IModelBinderProvider workerProvider;
public FooModelBinderProvider(IModelBinderProvider workerProvider)
{
this.workerProvider = workerProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(Foo))
{
return new FooModelBinder(this.workerProvider.GetBinder(context));
}
return null;
}
}
And here is the binder. Note that the first thing we do in BindModelAsync
is trampoline into the "worker" binder.
public class FooModelBinder : IModelBinder
{
private readonly IModelBinder worker;
public FooModelBinder(IModelBinder worker)
{
this.worker = worker;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
await this.worker.BindModelAsync(bindingContext);
if (!bindingContext.Result.IsModelSet)
{
return;
}
var foo = bindingContext.Result.Model as Foo;
if (foo == null)
{
throw new InvalidOperationException($"Expected {bindingContext.ModelName} to have been bound by ComplexTypeModelBinder");
}
// NOW DO SOME INTERESTING POST-PROCESSING
}
}
Finally, here's how to register the custom binder:
services.AddMvc(options =>
{
var workerProvider = options.ModelBinderProviders.First(p => p.GetType() == typeof(ComplexTypeModelBinderProvider));
options.ModelBinderProviders.Insert(options.ModelBinderProviders.IndexOf(workerProvider), new FooModelBinderProvider(workerProvider));
})
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With