Tired of looking for errors in log files? Use OneTrueError - Automatic exception management for .NET.

How to dynamically modify model meta data in ASP.NET MVC

Normally you just add the [Required] attribute to a view model to make it required. But I needed a way to configure whether a field to be required or not. The requirement was that it should be configured through web.config:

<appSettings>
    <add key="ticket-cat1-required" value="true" />
</appSettings>

Having to modify the view or the controller would not be very clean. Instead it’s much better to take advantage of the ModelValidatorProvider. I could have just done like this:

public class ConfigurableModelValidatorProvider : LocalizedModelValidatorProvider
{
    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<System.Attribute> attributes)
    {
        bool isRequired = metadata.ContainerType == typeof (CreateViewModel)
                            && ConfigurationManager.AppSettings["ticket-cat1-required"] == "true";


        var theAttributes = attributes.ToList();
        if (!theAttributes.Any(x => x is RequiredAttribute) && isRequired)
            theAttributes.Add(new RequiredAttribute());

        return base.GetValidators(metaDataContext.Metadata, context, attributes);
    }
}

And then assigned it in global.asax:

protected void Application_Start()
{
    ModelValidatorProviders.Providers.Clear();
    ModelValidatorProviders.Providers.Add(new ConfigurableModelValidatorProvider());

    //...
}

But that would have created a tightly coupled provider.

The loosely coupled way

Instead I decided to take advantage of my inversion of control container and define some interfaces.

/// <summary>
/// Can adapt the generated metadata before it's sent to the view
/// </summary>
public interface IModelMetadataAdapter
{
    /// <summary>
    /// Adapt the meta data
    /// </summary>
    /// <param name="context">Context information</param>
    void Adapt(MetadataContext context);
}

The context used to modify the meta data:

/// <summary>
/// context for <see cref="IModelMetadataAdapter"/>
/// </summary>
public class MetadataContext
{
    /// <summary>
    /// Initializes a new instance of the <see cref="MetadataContext"/> class.
    /// </summary>
    /// <param name="metadata">The metadata.</param>
    public MetadataContext(ModelMetadata metadata)
    {
        if (metadata == null) throw new ArgumentNullException("metadata");
        Metadata = metadata;
    }

    /// <summary>
    /// See MSDN for info
    /// </summary>
    public ModelMetadata Metadata { get; set; }
}

Which allowed me to create this class (which is automatically registered in Griffin.Container):

[Component]
public class ToggleRequiredOnCreateModel : IModelMetadataAdapter
{
    public void Adapt(MetadataContext context)
    {
        if (context.Metadata.ContainerType != typeof(CreateViewModel))
            return;
        context.Metadata.IsRequired = false;
        if (context.Metadata.PropertyName != "Category1")
            return;

        context.Metadata.IsRequired = ConfigurationManager.AppSettings["ticket-cat1-required"] == "true";
    }
}

To make it all possible I’ve also have to modify the validator provider:

public class ConfigurableModelValidatorProvider : LocalizedModelValidatorProvider
{
    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<System.Attribute> attributes)
    {
        var services = DependencyResolver.Current.GetServices<IModelMetadataAdapter>();

        var metaDataContext = new MetadataContext(metadata);
        foreach (var service in services)
        {
            service.Adapt(metaDataContext);
        }

        var theAttributes = attributes.ToList();
        if (!theAttributes.Any(x => x is RequiredAttribute) && metaDataContext.Metadata.IsRequired)
            theAttributes.Add(new RequiredAttribute());

        return base.GetValidators(metaDataContext.Metadata, context, attributes);
    }
}

In my case I’m using my Griffin.MvcContrib project to handle the localization, that’s why I inherit LocalizedModelValidatorProvider and not DataAnnotationsModelValidatorProvider.

This entry was posted in Architecture and tagged . Bookmark the permalink.