Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to extend ComplexTypeModelBinder

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?

like image 547
lencharest Avatar asked Dec 04 '17 22:12

lencharest


1 Answers

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));
  })
like image 195
lencharest Avatar answered Sep 20 '22 15:09

lencharest