Apr 5 2017

Model binding interfaces: fixing server side validation

Category: MVC | Asp.net core | Asp.netFrancesco @ 20:55

 

Custom Model Binding in Asp.net Core, 3: Model Binding Interfaces

This is a follow-up of my previous post on model binding interfaces, so please read it before going on with this post. I suppose also you have available the VS solution built in the previous post.

The problem

We use interfaces when we don’t want dependencies on how a “surface behavior” is implemented. As a matter of fact the same class may implement several interfaces and when we access it through one of these interfaces we want that each effect of the computation depends just on the way the interface is defined and not on the specific class peculiarities. In particular, we want that object validation performed through that interface depends only on the way the interface is defined.

That is why in my previous post I defined all validation attributes in the interface, instead of the interface implementation:

public interface ITestInterface
{
    [Range(1, 10)]
    int TestProperty { get; set; }
}

Unluckily, this doesn’t ensure model binder uses that validation rule. As a matter of fact the model binder has a “strange” behavior, namely if the interface is the root model received by the action method, it uses the validation attributes defined in the class that implements the interface,while if the interface is nested inside the root ViewModel received by the action method it do uses the validation attributes defined on the interface.

The reason of this behavior is quite simple: during validation the type of the root ViewModel is inferred by calling ‘GetType()’ on the model returned by the model binder, while the type of nested objects is inferred by the type of the property they are in.

About client side validation, it always uses the attributes defined in the interface.

 

In my previous post I suggested using the ‘ModelMetadataTypeAttribute’ to force usage of validation attributes defined in the interface:

[ModelMetadataType(typeof(ITestInterface))]
public class TestViewModel : ITestInterface
{
    public int TestProperty { get; set; }
}

Unluckily, this solution suffers of several problems:

  1. All validation attributes defined in the interfaces doesn’t substitute the ones defined in the class but they are simply added to them.
  2. A class may have just a single ‘ModelMetadataTypeAttribute’ so if a class implements several interfaces this technique can’t be used
  3. We cant make the class that implements the interface depends on peculiarities of the presentation layer since this would break separation of concerns between layers, thus adding the ‘ModelMetadataTypeAttribute’  just to solve a technical problem we have in the presentation layer is not a good practice.

Therefore, we are forced to modify the default behavior of the built-in validation system.

The solution

In all previous versions of Asp.net Mvc validation were performed during the model-binding recursive steps. In asp.net core, validation is performed after model binding has returned an objects tree, with a recursive visit of the whole tree. This way, the same validation process may be applied also to models that have been built by formatters (such as, for instance, the Json formatter that processes Json objects). Namely, as soon as model binding returns an objects tree an implementation of the “IObjectModelValidator” interface is retrieved through dependency injection and its “Validate” model is called passing it the model returned by the model binder and other parameters we will analyze later on in this section.

The “IObjectModelValidator” default implementation performs some preparation steps, then calls GetTipe on the model passed to its Validate method to get the root type to validate, and finally it calls a recursive visitor that traverses the whole tree for validation.

We need to substitute the default implementation with an implementation that instead of calling GetType to get the root type uses the type declared in the action method. This way instead of validating the interface implementation we might validate the interface. No modification is needed to the recursive visitor since it doesn’t use GetType but the types declared in the type properties.

Unluckily the “Validate” method is not passed the type declared in the action method but just the overall ActionContext.

The method below is able to extract the type declared in the action method from the model type extracted with GetType, the ActionContext, and initial prefix passed to the validate method. This initial prefix is either the empty string or the name of the parameter of the action method that is being processed (more details on this, later on):

private Type referenceType(string prefix, Type modelType, ActionContext actionContext)
{
    var parameterDescriptors = actionContext.ActionDescriptor.Parameters;
    ParameterDescriptor parameter;
    if (string.IsNullOrWhiteSpace(prefix))
        parameter = parameterDescriptors
            .Where(m => m.ParameterType.GetTypeInfo().IsAssignableFrom(modelType))
            .FirstOrDefault();
    else
    {
        parameter = parameterDescriptors
            .Where(m => m.Name == prefix && m.ParameterType.GetTypeInfo().IsAssignableFrom(modelType))
            .FirstOrDefault();
    }
    if (parameter != null && parameter.ParameterType.GetTypeInfo().IsInterface)
        return parameter.ParameterType;
    else return modelType;
}

We look for the parameter we need the type of in the list of all parameter descriptors.If the initial prefix is the empty string we don’t have the name of the parameter we are looking the type of, so we try to find it through the type of the model (model type must be “assignable” to the parameter type), otherwise we try a match for both name and type.

Finally if the parameter is an interface we use its type otherwise we revert to the default behavior an use the model type obtained with GetType.

The whole code of our custom implementation is :

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.Linq;
using System.Reflection;

namespace ModelBindingInterfaces.Validation
{
    public class EnhancedObjectValidator: IObjectModelValidator
    {
        private readonly IModelMetadataProvider _modelMetadataProvider;
        private readonly ValidatorCache _validatorCache;
        private readonly IModelValidatorProvider _validatorProvider;


        public EnhancedObjectValidator(
            IModelMetadataProvider modelMetadataProvider,
            IList<IModelValidatorProvider> validatorProviders)
        {
            if (modelMetadataProvider == null)
            {
                throw new ArgumentNullException(nameof(modelMetadataProvider));
            }

            if (validatorProviders == null)
            {
                throw new ArgumentNullException(nameof(validatorProviders));
            }

            _modelMetadataProvider = modelMetadataProvider;
            _validatorCache = new ValidatorCache();

            _validatorProvider = new CompositeModelValidatorProvider(validatorProviders);
        }

        private Type referenceType(string prefix, Type modelType, ActionContext actionContext)
        {
            var parameterDescriptors = actionContext.ActionDescriptor.Parameters;
            ParameterDescriptor parameter;
            if (string.IsNullOrWhiteSpace(prefix))
                parameter = parameterDescriptors
                    .Where(m => m.ParameterType
                        .GetTypeInfo().IsAssignableFrom(modelType))
                    .FirstOrDefault();
            else
            {
                parameter = parameterDescriptors
                    .Where(m => m.Name == prefix && m.ParameterType.GetTypeInfo()
                        .IsAssignableFrom(modelType))
                    .FirstOrDefault();
            }
            if (parameter != null && parameter.ParameterType.GetTypeInfo().IsInterface)
                return parameter.ParameterType;
            else return modelType;
        }
        public void Validate(
            ActionContext actionContext,
            ValidationStateDictionary validationState,
            string prefix,
            object model)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException(nameof(actionContext));
            }

            var visitor = new ValidationVisitor(
                actionContext,
                _validatorProvider,
                _validatorCache,
                _modelMetadataProvider,
                validationState);

            var metadata = model == null ? null :
                _modelMetadataProvider
                    .GetMetadataForType(referenceType(prefix, model.GetType(),
                        actionContext));
            visitor.Validate(metadata, prefix, model);
        }
    }
}

Our custom implementation may be installed easily in the DI section of our project Startup.cs.

        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddApplicationInsightsTelemetry(Configuration);
            services.AddTransient<ITestInterface, TestViewModel>();

            services.AddMvc(o =>
            {
                o.ModelBinderProviders.Insert(0,
                    new ModelBinders.InterfacesModelBinderProvider());
            });
            services.TryAddSingleton<IObjectModelValidator>(s =>
            {
                var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
                var metadataProvider = s.GetRequiredService<IModelMetadataProvider>();
                return new EnhancedObjectValidator(metadataProvider,
                    options.ModelValidatorProviders);
            });
        }

We use the possibility offered by DI to get an instance through a Lambda, since, in the constructor of our implementation, we need the current model metadata provider that is contained in the MvcOptions object.

Proof of correctness of parameter type extraction algorithm

If you are not interested in the details of why the parameter extraction algorithm I propose always works, that is,why  it never guesses the wrong parameter you may jump this section.

In case the algorithm receives the exact parameter name, no error is possible, but what happens when it receives the empty string? There might be several parameters that are assignable from the model type, how are we sure to select the right one?

The answer is very simple: when when the algorithm receives the empty string there can’t be several parameters compatible with the model type! In fact, we receives the empty string instead of the parameter name when the name of the action method parameter is not used as a prefix in the name of any input field submitted. In fact the model binder always attempts matching field names with and without the parameter names as prefixes. Usually, we don’t add the parameter name prefix to the field names when the action method has an unique parameter, but this is not compulsory.

However, for sure, when we omit parameter names in field names we can’t have several parameters whose types share properties with the same names, otherwise the model binding process would be ambiguous. Therefore, when parameter names do not prefix field names we can’t have several parameters compatible with the same type (otherwise the properties of the model compatible with both parameters would create ambiguities).

Cautions!

Our algorithm guesses the right correctly interface type when the model binding process is invoked directly by the framework during action method parameters instantiation. However, it doesn’t work if you invoke manually model binding by calling helper functions like “TryUpdateModelAsync” from within an action method, so please, in this case do not use interfaces as parameter types.

Moreover, pay attention when using Interfaces! Validation attributes defined on interfaces are not inherited by derived interfaces or classes implementing them, since interfaces are just prescriptions and can’t furnish any “code” to the types that implement them.

 

That’s all for now

Stay tuned!

Francesco

Tags: , ,