Implementing generic controllers in ASP.NET MVC and WebAPI – Part 2: binding to derived classes

In the first post of this series I covered the customisation of default MVC framework behaviour that needs to be done to reuse controllers for the Competition website. Apart from MVC controllers we need to have ApiControllers, as most of the front-end functionality in the website implemented in React.JS. And again, most of the basic functionality can be reusable, like, for example, saving Entry view model to the database.

When rendering the Competition Entry page our generic MVC controller will pass the implementation of BaseEntryViewModel base class. Derived viewmodel classes would share some basic properties (like Id, Email, etc.), but also have some specific ones – ImageUrl, Location, etc. Instead of writing ApiController or action to save entries for each competition, we want to make this POST method reusable.

The solution with inherited controllers that hold a custom implementation of competition service works here as well. The biggest challenge though was parameter binding. If we just tell the action to expect BaseEntryViewModel as a parameter, members of derived classes that are not part of base class will be simply cut off during the default parameter binding. So we need to customise this process.

Custom parameter binding for ViewModel classes in API Controllers

To accomplish this we need to do two things:

  • first, to write a custom implementation of HttpParameterBinding
  • second, to create a parameter binding attribute that hooks up our custom binding to a parameter

To extract ViewModel data from the request body, we need to extend HttpParameterBinding class, overriding ExecuteBindingAsync method, responsible for parsing the request content. Using Reflection we can create an instance of the required ViewModel. But in order to do that we need to know the name of the view model. Maybe not the most elegant, but simple and reliable, solution was to pass the name as one of the base class members. Knowing class name, we can extract ViewModel type like this:

binding.Descriptor
  .ParameterType
  .Assembly
  .ExportedTypes
  .FirstOrDefault(t => t.Name == viewModelClassName.ToString())

Here is the full implementation of the EntryParameterBinding:

public class EntryParameterBinding : HttpParameterBinding
{
    public EntryParameterBinding(HttpParameterDescriptor descriptor)
        : base(descriptor)
    {
    }

    public override async Task ExecuteBindingAsync(
         ModelMetadataProvider metadataProvider,
         HttpActionContext actionContext,
         CancellationToken cancellationToken)
    {
        var binding = actionContext.ActionDescriptor
             .ActionBinding
             .ParameterBindings
             .FirstOrDefault(t => t is EntryParameterBinding);

        if (binding != null)
        {
            var contents = await ParseRequestContent(actionContext);

            var viewModelType = GetViewModelType(binding, contents);

            if (viewModelType != null)
            {
                var viewModel = Activator.CreateInstance(viewModelType);

                var properties = viewModelType
                     .GetProperties()
                     .Where(t => t.CanWrite);

                foreach (var property in properties)
                {
                    if (contents.TryGetValue(property.Name, out var value))
                    {
                        var propType = property.PropertyType;
                        var parse = propType
                             .GetMethod("Parse", new[] { typeof(string) });
                        if (parse == null)
                        {
                            property.SetValue(viewModel, value);
                        }
                        else
                        {
                            var parsedItem = parse.Invoke(null, new[] { value });
                            property.SetValue(viewModel, parsedItem);
                        }
                    }
                }

                SetValue(actionContext, viewModel);
            }
        }
    }

    private async Task ParseRequestContent(HttpActionContext actionContext)
    {
        var contentString = await actionContext.Request.Content.ReadAsStringAsync();

        return contentString.Split('&')
                .Select(parameter => parameter.Split('='))
                .ToDictionary(keyValue => keyValue[0].Split('.').Last(), keyValue => keyValue[1]);
    }

    private Type GetViewModelType(HttpParameterBinding binding, Dictionary contents)
    {
        object viewModelClassName;
        if (contents.TryGetValue("ViewModelClassName", out viewModelClassName))
        {
            return binding.Descriptor
                .ParameterType
                .Assembly
                .ExportedTypes
                .FirstOrDefault(t =>
                    t.Name == viewModelClassName.ToString());
        }

        throw new NotSupportedException("Not supported competetion entry view model");
    }
}

In order to attach our custom binding to the parameters that are expected to be the implementations of BaseEntryViewModel we need to use an attribute that would tell framework to use EntryParameterBinding instead of default one. In the attribute we just need to override GetBinding method to return EntryParameterBinding:

[AttributeUsage(AttributeTargets.Parameter)]
public sealed class EntryViewModelAttribute : ParameterBindingAttribute
{
    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        if (parameter == null)
        {
            throw new ArgumentException("Invalid parameter");
        }

        return new EntryParameterBinding(parameter);
    }
}

Now all we need to do for the magic to happen is to apply the attribute like this:

[HttpPost]
[Route("{competitionId}/entries")]
public virtual async Task Post(int competitionId, [EntryViewModel] BaseEntryViewModel entryViewModel)

That concludes the small post series about my experience in creating generic MVC and Web API controller. You can see that with a little bit of extra work we made the website that can serve as a basis for our future competition and will save us from the biggest software evil – repetition. Hopefully the evolution of ASP.NET will bring these useful options out of the box, but for now we’re lucky to have enough points of extensibility to tailor framework to our needs.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s