April 25, 2024

SamTech 365

PowerPlatform, Power Apps, Power Automate, PVA, SharePoint, C#, .Net, SQL, Azure News, Tips ….etc

ASP.NET MVC Extensibility with MEF

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.

0314vsm_evogelFig1

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();

 

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.

0314vsm_evogelFig1

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();