C# Corner

ASP.NET MVC Extensibility with MEF

How to use MEF to add validation rule components to an ASP.NET MVC Web application.

The Managed Extensibility Framework (MEF) has been part of the .NET Framework since .NET 4.0. There are many tutorials across the Web on how to use MEF with Windows Forms, WPF and Windows Store. MEF allows an application to be extended through a set of components that may either be imported or exported. Today I'll show how to add business rule plug-ins to a new ASP.NET MVC application.

To get started, create a new ASP.NET MVC 4 Internet Application in Visual Studio 2012 or 2013. Then install the MEF.MVC4 NuGet package, as seen in Figure 1.

[Click on image for larger view.] Figure 1. Installing MEF.MVC4 NuGet package.

Next,  add a new Class Library named BusinessRules to the Visual Studio solution. The BusinessRules project will contain the IValidate interface and some base business rule components that will imported in the MVC project. Then add a new class named ValidationResult that has an IsValid boolean property and an ErrorMessage string property. Your ValidationResult class should look like this:

namespace VSMMefMvc.BusinessRules
{
    public class ValidationResult
    {
        public bool IsValid { get; set; }
        public string ErrorMessage { get; set; }
    }
}

Next, add the IValidate generic interface. This interface defines a Validate method that accepts a generic input type and returns a ValidationResult:

namespace VSMMefMvc.BusinessRules
{
    Public interface IValidate<in T>
    {
        ValidationResult Validate(T input);
    }
}

Now add the IValidateMetaData class, which contains a Name string property:

namespace VSMMefMvc.BusinessRules
{
    public interface IValidateMetaData
    {
        string Name { get; }
    }
}

Now it's time to add the first validation component, which validates a user's email address. The ValidateEmail class uses a regular expression to validate the email, and returns a ValidationResult object with IsValid set to false and an error message set if the email is not valid. The ValidateEmail class exports the IValidate<string> interface and exports a metadata name of "Email":

using System.ComponentModel.Composition;
using System.Text.RegularExpressions;

namespace VSMMefMvc.BusinessRules
{
    [Export(typeof(IValidate<string>))]
    [ExportMetadata("Name", "Email")]
    public class ValidateEmail : IValidate<string>
    {
        const string EMAIL_PATTERN = @"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";

        public ValidationResult Validate(string input)
        {
            var result = new ValidationResult();
            if (input == null || !Regex.IsMatch(input, EMAIL_PATTERN))
            {
                result.ErrorMessage = string.Format("{0} is not a valid email address.", input);
            }
            else
            {
                result.IsValid = true;
            }
            return result;
        }
    }
}

It's now time to add the ValidateUsPhone class. It's very similar to the ValidateEmail class, except is uses a U.S. phone number regular expression. I set the export metadata name to "U.S. Phone" for the class:

using System.ComponentModel.Composition;
using System.Text.RegularExpressions;

namespace VSMMefMvc.BusinessRules
{
    [Export(typeof(IValidate<string>))]
    [ExportMetadata("Name", "U.S. Phone")]
    public class ValidateUsPhone : IValidate<string>
    {
        const string PHONE_PATTERN = @"^((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}$";

        public ValidationResult Validate(string input)
        {
            var result = new ValidationResult();
            if (input == null || !Regex.IsMatch(input, PHONE_PATTERN))
            {
                result.ErrorMessage = string.Format("{0} is not a valid phone number.", input);
            }
            else
            {
                result.IsValid = true;
            }
            return result;
        }
    }
}

Now that the BusinessRules class is completed, it's time to wire up MEF in the MVC project to load the validation rules. Open up the MefConfig class file, located at App_State/MefConfig.cs, and modify the ConfigureContainer method:

private static CompositionContainer ConfigureContainer()
{
    var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    var businessRulesCatalog = new AssemblyCatalog(typeof(BusinessRules.IValidateMetaData).Assembly);
    var catalogs = new AggregateCatalog(assemblyCatalog, businessRulesCatalog);
    var container = new CompositionContainer(catalogs);
    return container;
}

The business rule components are loaded into MEF through the businessRulesCatalog. In addition, I load in any MEF components contained within the MVC project assembly through the assemblyCatalog object. The catalogs object is an AggregateCatalog collection that contains both the executing assembly and the business rules assembly. Lastly, a new CompositionContainer is created from the AggregateCatalog and returned.

The completed MefConfig class file should now look like this:

using System.ComponentModel.Composition.Hosting;
using System.Reflection;
using System.Web.Mvc;
using MEF.MVC4;

namespace VSMMefMvc
{
    public static class MefConfig
    {
        public static void RegisterMef()
        {
            var container = ConfigureContainer();

            ControllerBuilder.Current.SetControllerFactory(new MefControllerFactory(container));
            
            var dependencyResolver = System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver;
            System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver = new MefDependencyResolver(container);
        }

        private static CompositionContainer ConfigureContainer()
        {
            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            var businessRulesCatalog = new AssemblyCatalog(typeof(BusinessRules.IValidateMetaData).Assembly);
            var catalogs = new AggregateCatalog(assemblyCatalog, businessRulesCatalog);
            var container = new CompositionContainer(catalogs);
            return container;
        }
    }
}

Next, open up the Global.asax.cs file and call MefConfig.RegisterMef() from within the Application_Start method:

using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace VSMMefMvc
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth();
            MefConfig.RegisterMef();
        }
    }
}

The last step is to load all the validation rules into an MVC form. Create a ViewModels directory within the MVC project, then add a new class named ValidationFormModel to the ViewModels folder. The ValidationFormModel class contains properties for each of the input fields and labels for the validation test form.

There's an Input property that will store the user's value to validate. The Rules property will contain all the valid validation rules imported through MEF. The Rule property will be loaded with the user's selected rule, and the StatusLabel property will notify the user if their submitted input is valid:

using System.Collections.Generic;
using System.Web.Mvc;

namespace VSMMefMvc.ViewModels
{
    public class ValidationFormModel
    {
        public string Input { get; set; }
        public List<SelectListItem> Rules { get; set; }
        public string Rule { get; set; }
        public string StatusLabel { get; set; }
    }
}

Now that the view model class is completed, let's set up the Razor view for the test page. Open up the Home controller's Index action view at Views/Home/Index.cshtml, and copy in the following markup:

@model VSMMefMvc.ViewModels.ValidationFormModel

@{
    ViewBag.Title = "MEF Demo";
}
@using (Html.BeginForm())
{
    <strong>@Html.DisplayFor(m => m.StatusLabel)</strong>
    @Html.ValidationSummary(false)
    <fieldset>
        <legend>Validation Demo</legend>
        @Html.LabelFor(m => m.Input)
        @Html.TextBoxFor(m => m.Input)
        @Html.LabelFor(m => m.Rule)
        @Html.DropDownListFor(m => m.Rule, Model.Rules)
    </fieldset>
     <input type="submit"/>
}

The final step is to implement the Index action on the HomeController class. First I add using statements for MEF and the ViewModels namepsaces:

using System.ComponentModel.Composition;
using VSMMefMvc.ViewModels;

Next I add Validators property that uses the MEF ImportMany attribute to import all validators that implement IValidate<string>:

[ImportMany]
public IEnumerable<Lazy<BusinessRules.IValidate<string>, BusinessRules.IValidateMetaData>> Validators { get; private set; }

Then I implement the Index HttpGet action, which creates a new ValidationFormModel with a loaded Rules drop-down list and loads it into the Index view:

[HttpGet]
public ActionResult Index()
{
    var vm = new ValidationFormModel();
    vm.Rules = new List<SelectListItem>(from v in Validators
               select new SelectListItem() {Text = v.Metadata.Name, Value = v.Metadata.Name});
    return View(vm);
}

Finally, I implement the HttpPost Index controller action that accepts a ValidationFormModel. First I get the selected rule from the Validators collection by name:

var rule = (from v in Validators
            where v.Metadata.Name == vm.Rule
            select v.Value).FirstOrDefault();

Then I get validation and store its result:

        
var result = rule.Validate(vm.Input);

If the result didn't pass the validation check, I add a model state error with the set error message and set the StatusLabel to let the user know there are errors to fix:

if (!result.IsValid)
{
    vm.StatusLabel = "Fix the following errors:";
    ModelState.AddModelError("Input", result.ErrorMessage);
}

Otherwise, I set the StatusLabel to say "Input is valid":

else
{
    vm.StatusLabel = "Input is valid";
}

Finally, I reload the Rules drop-down and return the set view:

vm.Rules = new List<SelectListItem>(from v in Validators
                                     select new SelectListItem() 
                                     { Text = v.Metadata.Name,
                                         Value = v.Metadata.Name });
 return View(vm);

The completed Index HttpPost action:

[HttpPost]
public ActionResult Index(ValidationFormModel vm)
{
    var rule = (from v in Validators
                where v.Metadata.Name == vm.Rule
                select v.Value).FirstOrDefault();
    var result = rule.Validate(vm.Input);
    if (!result.IsValid)
    {
        vm.StatusLabel = "Fix the following errors:";
        ModelState.AddModelError("Input", result.ErrorMessage);
    }
    else
    {
        vm.StatusLabel = "Input is valid";
    }

    vm.Rules = new List<SelectListItem>(from v in Validators
                                        select new SelectListItem() { Text = v.Metadata.Name, Value = v.Metadata.Name });

    return View(vm);
}

Here's the completed HomeController class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.ComponentModel.Composition;
using VSMMefMvc.ViewModels;

namespace VSMMefMvc.Controllers
{
    public class HomeController : Controller
    {
        [ImportMany]
        public IEnumerable<Lazy<BusinessRules.IValidate<string>, BusinessRules.IValidateMetaData>> Validators { get; private set; }

        [HttpGet]
        public ActionResult Index()
        {
            var vm = new ValidationFormModel();
            vm.Rules = new List<SelectListItem>(from v in Validators
                       select new SelectListItem() {Text = v.Metadata.Name, Value = v.Metadata.Name});
            return View(vm);
        }

        [HttpPost]
        public ActionResult Index(ValidationFormModel vm)
        {
            var rule = (from v in Validators
                        where v.Metadata.Name == vm.Rule
                        select v.Value).FirstOrDefault();
            var result = rule.Validate(vm.Input);
            if (!result.IsValid)
            {
                vm.StatusLabel = "Fix the following errors:";
                ModelState.AddModelError("Input", result.ErrorMessage);
            }
            else
            {
                vm.StatusLabel = "Input is valid";
            }

            vm.Rules = new List<SelectListItem>(from v in Validators
                                                select new SelectListItem() 
                                                { Text = v.Metadata.Name,
                                                    Value = v.Metadata.Name });
            return View(vm);
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your app description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }
    }
}

The application is now complete, and you should be able to successfully validate a invalid email,  as seen in Figure 2.

[Click on image for larger view.] Figure 2. Validating an Invalid Email Address.

You should also be able to validate a valid email, as seen in Figure 3.

[Click on image for larger view.] Figure 3. Validating an Valid Email Address

You've now seen how to use MEF components within an ASP.NET MVC application to load validation rules at runtime. As this tutorial demonstrates, MEF is a very useful framework to have in your ASP.NET MVC toolset. You can use it to easily add points of extensibility to all your .NET apps.

About the Author

Eric Vogel is a Senior Software Developer for Red Cedar Solutions Group in Okemos, Michigan. He is the president of the Greater Lansing User Group for .NET. Eric enjoys learning about software architecture and craftsmanship, and is always looking for ways to create more robust and testable applications. Contact him at [email protected].

comments powered by Disqus

Featured

Subscribe on YouTube