Introduction
This article gives readers a fair idea of how an application can be designed using the Managed Extensions Framework and make the application design open to adopt frequent changes in business requirements.
Background
In one of our projects, the application was designed in such a way that the application contains sequences of steps, and the steps contains modules, and each page displays a step. Modules are reusable across the steps. We can do the design for this in many ways, one of which is data driven; using this approach, we can very much control the steps and modules order in the database, and use the data from the database to orderly render the steps and the modules in each step. That said, often we might have a requirement to have a module be reusable across applications. In that case, different applications might have different data models. If we tightly couple the module meta data to be data driven to one particular data model, then while reusing the modules across different applications, we need to take two things into consideration: one is to find a way to integrate our data model with the new application data model so that the module designed to be data driven would automatically start appearing in the new application, and second, for example, if we find a particular module is malfunctioning, then in order to remove the module from the page, the build manager needs to know the associated metadata of the module to be removed (database table details), otherwise it may give a module not found exception. Basically, this model expects a user to know the database tables that are responsible for rendering the steps and modules and carefully remove them in order to remove the module from the application.
Let us consider another way of doing it. Designing the application using the Managed Extensibility Framework. If an pplication is targeting the Managed Extensions Framework, the above said problems can be solved with low maintenance costs.
What is MEF all about? In simple terms, Exports/Imports. For more details about MEF, go to this link: http://mef.codeplex.com/wikipage?title=Guide&referringTitle=Overview.
Managed Extensibility Framework provides the flexibility of exporting functionality using Exports, and imports the exported functionality using Imports. We can use the Export/Import concept of MEF to design our steps and modules based application pattern. We can assume each module as an Export, and each step imports one or more modules. So how does a step knows what are its modules? We provide a contract and the associated metadata (module name, step name in which the module can go in, application name to which this module belongs to) to each module developer. Module developers just need to implement the contract and pass the necessary metadata to the module metadata, like which step/application it belongs to. Each module developer will produce a DLL as the output for each module. Application integrators just need to copy the module DLL to a given folder location where MEF is watching for the Exports. That's it, it automatically integrates the module with the right step and with the right application.
This solves the above two problems. For example, how to integrate the module with a different application. The module developer just needs to change the application name in the metadata to which the module belongs to (it can be configurable in App.config) and ship the DLL to the new application, which will be integrated into that application automatically (if that application design also targets MEF). I am really excited with this feature. If you feel a particular module is malfunctioning, then as a build engineer, you can just remove the module DLL from the MEF folder location. You don't need to know the associated metadata and the location where the metadata is stored.
Simple Introduction to MEF
The fundamental unit of MEF is a Composable part. Composable parts are of two types: Import and Export. In the Managed Extensibility Framework programming model, a type can be made as an Export service by attributing the type as [System.ComponentModel.Composition.Export]
, and a type can be made an import of a certain Export type by attributing it with [System.ComponentModel.Composition.Import]
.
Exports and imports don't have direct dependency on each other; they depend on a contract. Every Export has a contract, and every import declares the contract it needs. For example, if my Export has a contract IA
(Interface) which is declared as follows:
[Export(typeof(IA))]
Public class Export1 : IA
{
}
[Export(typeof(IA))]
Public class Export2:IA
{
}
Once the above parts are exported, the exports can be imported using the following code:
public class Import1
{
[ImportMany]
Public IEnumerable<IA> _exports
}
The ImportMany
attribute in the above code is used to import all the composable parts which are exported with the contract IA
. There is also one attribute [Import]
which does the import for a matching Export.
Catalogs
MEF discovers the parts with the help of Catalogs. Catalogs allow applications to easily consume all the parts that have been registered with the Export
attribute. Below are the types of catalogs.
AssemblyCatalog
: [System.ComponentModel.Composition. Hosting. AssemblyCatalog]
to discover all the parts in a given assembly.
DirectoryCatalog
: Which is used in our demo sample, [System.ComponentModel.Composition.Hosting.DirectoryCatalog]
. It discovers all the Exports that are found in the assemblies under the given folder.
AggregateCatalog
: Basically, this catalog is required to combine more than one catalog when a single catalog is not self sufficient.
TypeCatalog
: To discover all the exports in a specific set of types, [System.ComponentModel.Composition.Hosting.TypeCatalog]
.
CompositionContainer
: Once the catalog is prepared by choosing one from above, the catalog will be passed to the composition container, and uses the GetExports()
etc. methods of the CompositionContainer
to get the Exports as per the catalog settings.
For example, consider the code below:
string relativePath = AppDomain.CurrentDomain.BaseDirectory + @"\Modules";
AggregateCatalog agrCatalog = new AggregateCatalog(new DirectoryCatalog(relativePath));
CompositionContainer objContainer = new CompositionContainer(agrCatalog);
TypeCatalog moduleCatalog = new TypeCatalog(typeof(IModule));
agrCatalog.Catalogs.Add(moduleCatalog);
_Modules = objContainer.GetExports<IModule, IModuleMetadata>();
Here in the above code, relativePath
is set to the baseDirectory\Modules folder. A DirectoryCatalog
is created to this relative path, and a TypeCatalog
also created to create all the Exports under the IModule
type, and added to the Aggregate Catalog. What this Aggregate Catalog means is in the basedirectory\Modules folder location, search in all the assemblies for the type IModule
which are self-registered for Export attributes. Pass this AggregateCatalog
to CompositionContainer
and call the GetExports
method to get the Exports of type IModule
.
Using the Code
For simplicity, in this article, I have done a sample for a Company dash board using the Managed Extensibility Framework, which basically displays a page (a step), which contains company events, policies, and news in three separate grids (modules). So we have identified three different modules in the company dashboard step. Removing any module DLL from the Modules folder (MEF watch folder) will remove the module from the company dashboard step.
Below is the code structure of the sample MEF:
Here, the project SampleMEF.Contract defines the contract for the modules we give to the module developers. This project contains Module contract IModule
which basically defines onModuleLoad()
; module implementers can override this method to implement the functionality on load of the module. MEF watches for all modules that implement the IModule
interface type.
public interface IModule : IComponent
{
void OnModuleLoad();
}
IModule
inherits from IComponent
(implementation specific, normally it is not mandatory) because all the modules in this sample are UserControls.
IModuleMetadata
provides the metadata information of the module (like to which step and application the module belongs to). Module metadata is very useful at the time of integrating the module with the application step to know where the module should be placed.
public interface IModuleMetadata {
string StepName { get; }
string ApplicationName { get; }
string ModuleName{get;}
}
IModuleMetadata
is the metadata about the module; for our demo, each module has an associated step and an application into which it needs to get displayed.
StepName
: Step name of the module into which the module belongs to; e.g.: Company Dashboard.
ModuleName
: Name of the module like Company News, Company Events etc.
ApplicationName
: Name of the application to which the current module belongs to; e.g., Company Portal.
The BaseModule
class implements the IModule
interface. The purpose of having the BaseModule
class is we can put common code for all the modules in the BaseModule
class. We know that a module is a DLL for this demo; a DLL can have more than one ASCX control. The BaseModule
class has a method LoadModules(..)
. Given a list of ASCX control names, it hands over each control loading to a custom virtual path provider and loads the control. This also implements the IModule
interface and provides an empty implementation to the OnModuleLoad()
method. Modules required to implement IModule
can now just inherit from the BaseModule
class. We can also think about the purpose of the BaseModule
class as, we as application designers will ship the prepackaged software a module developer can use along with the contract DLL; in this case, it is the LoadModules()
method.
public class BaseModule:System.Web.UI.UserControl,IModule
{
public void LoadModules(List<string> modules)
{
foreach (string module in modules)
{
string virtualPath = "~/Modules/" +
System.Reflection.Assembly.GetCallingAssembly().GetName().Name +
"," + System.Reflection.Assembly.GetCallingAssembly().GetName().Name +
"."+module;
this.Controls.Add(this.LoadControl(virtualPath));
}
}
#region IModule Members
public virtual void OnModuleLoad()
{
}
#endregion
}
ModuleMetadaAttribute
extends from the MEF Export attribute, and implements IModuleMetadata
. Module developers need to decorate their modules with this attribute in order to make the module an Export.
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ModuleMetadataAttribute : ExportAttribute, IModuleMetadata {
public ModuleMetadataAttribute() : base(typeof(IModule)) { }
public string StepName { get; set; }
public string ApplicationName { get; set; }
public string ModuleName { get; set; }
}
In the above code, we have created a custom Export attribute called [ModuleMetadata]
to apply on the modules that are going to be developed for the current step. It basically extends from the MEF class ExportAttribute
and implements IModuleMetadata
. The purpose of this attribute is when it is applied on a class by passing the StepName
, ApplicationName
, and ModuleName
values, that class becomes MEF exportable and the metadata provides the details of the export.
The solution contains the SampleMEF.module1, SampleMEF.module2, and SampleMEF.module3 projects for Events, Policies, and News Modules. Each module is decorated with the ModuleMetadtaAttribute
to tell the step that it is a possible import for your step. There is a control in each module called ParentModule
(in each module project, you will find a control with the name ParentModule.ascx), which stands up primary among all the other controls in the module, and takes the responsibility of loading other controls using the loadcontrol()
method.
Below is the code structure for SampleMEF.module1:
SampleMEF.module1.dll is basically for the Events module. SampleMEF.module1 contains the CompanyEventsModule.ascx control, which actually gets the Company Events data and displays it on the grid. Below is the code for CompanyEvents
:
CompanyEventsModule.ascx <script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
this.OnModuleLoad();
}
#region IModule Members
public void OnModuleLoad()
{
this.CompanyEventsDataGrid.DataSource =
CompanyDataClient.GetCompanyEventsData(1);
this.CompanyEventsDataGrid.DataBind();
}
#endregion
</script>
The above ASCX content will be loaded in the below ParentModule
code; the module developer needs to make a List<string>
of control names (in this case, CompanyEventsModule.ascx etc.). BaseModule
will take care of loading the controls.
[ModuleMetadata(StepName = "CompanyPortalPage",
ApplicationName = "CompanyApp",ModuleName="Company News" )]
public partial class ParentModule : BaseModule
{
protected void Page_Load(object sender, EventArgs e)
{
this.OnModuleLoad();
}
#region IModule Members
public override void OnModuleLoad()
{
List<string> modules =new List<string>();
modules.Add("CompanyEventsModule.ascx");
base.LoadModules(modules);
}
#endregion
}
Why do we need to do this step at all? Why can not MEF create all the controls in the ASCX page when we keep all the subcontrols as part of the ParentModule.ascx control? Because MEF creates only the exported object, in this case, the ParentModule
user control. MEF will not compile the ASCX content to create child objects, which is the expected behavior. Because of this reason, I have come up with a solution where we keep all the ASCX sub control content (CompanyEventsModule.ascx in SampleMEF.module1, CompanyPoliciesModule.ascx in SampleMEF.module2, and CompanyNewsModule.ascx in SampleMEF.module3) as embedded to the corresponding module DLLs, and at runtime, read the content of the subcontrols using the GetResourceManifestStream()
method.
Similarly for the other two modules as well. Below is the code structure for the other two modules.
SampleMEF.module2 which is PoliciesModule
SampleMEF.module3 which is NewsModule
Most of the CMS frameworks will use the VirtualPath mechanism to get the ASCX, ASPX content from either a database or a Content Management System. In the same way, in this sample, we have used the VirtualPath provider concept to supply the content of ASCX, ASPX to the ASP.NET build system from the embedded resource (here, the module DLL). The providers project does the CMS framework job, which is self-explanatory.
The MEFSampleWeb project contains the company dashboard page. Default.aspx initializes MEF to the Modules folder in the website so that any module copied to the Modules folder will be automatically picked up by the step.
public static IEnumerable<Lazy<IModule, IModuleMetadata>> _Modules;
public _Default()
{
InitializeComponentContainer();
}
protected void Page_Load(object sender, EventArgs e)
{
foreach (var module in _Modules)
{
if (module.Metadata.ApplicationName.Equals("CompanyApp") &&
module.Metadata.StepName.Equals("CompanyPortalPage"))
{
this.div1.Controls.Add((UserControl)module.Value);
}
}
}
private static void InitializeComponentContainer()
{
try
{
string relativePath = AppDomain.CurrentDomain.BaseDirectory + @"\Modules";
AggregateCatalog agrCatalog =
new AggregateCatalog(new DirectoryCatalog(relativePath));
CompositionContainer objContainer = new CompositionContainer(agrCatalog);
TypeCatalog moduleCatalog = new TypeCatalog(typeof(IModule));
agrCatalog.Catalogs.Add(moduleCatalog);
_Modules = objContainer.GetExports<IModule, IModuleMetadata>();
}
catch (Exception objException)
{
}
}
Here, we have used DirectoryCatalog
to listen to the Modules folder of the website. We are also using a TypeCatalog
of type IModule
. Basically, among all Modules which are possible exports for the MEF, it will pick only Exports which are implementing the IModule
interface (in our case, the BaseModule
class, as we are already extending it from IModule
). The _Modules
property which is lazy loaded defers the loading of the modules until the property is accessed explicitly. A div
tag is declared in the ASPX page to hold all the modules targeted in this step (company dash board). The above foreach
basically collects all the modules (Exports) and adds them to the main div1
child controls.
SampleMEFProxies
is the client wrapper for CompanyDataService
. The Provider project basically implements a new virtual path provider to load the ASCX content from the DLL. Below is sample code for AssemblyVirtualPathProvider
. Again, it is implementation specific to the module.
public class AssemblyVirtualPathProvider :VirtualPathProvider
{
public AssemblyVirtualPathProvider()
{
}
private bool IsPathVirtual(string virtualPath)
{
String checkPath =
VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith("~/Modules".ToLower().ToString(),
StringComparison.InvariantCultureIgnoreCase);
}
public override bool FileExists(string virtualPath)
{
if (IsPathVirtual(virtualPath))
{
return true;
}
else
return Previous.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsPathVirtual(virtualPath))
return new AssemblyVirtualFile(virtualPath);
else
return Previous.GetFile(virtualPath);
}
}
Registering the above virtual path provider with ASP.NET can be done in the global.asax file, and it is shown below.
protected void Application_Start(object sender, EventArgs e)
{
System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(
new AssemblyVirtualPathProvider());
}
Here is how the whole thing works:
- The requests for the company portal page comes to the
Applicatoin_Start
event and registers a new virtual path provider with ASP.NET, and it will call Page_Init()
of the default.aspx page of the MEFSampleWeb project.
- Inside
Page_Init()
, MEF will be configured to watch the Modules folder for all Exports of type IModule
.
- In the
page_Load
method, foreach
iterates through the IEnumerable
collection of _Modules
.
- Consider that our MEFSampleWeb\Modules folder contains the above three modules: SampleMEF.module1.dll, SampleMEF.module2.dll, and SampleMEF.module3.dll.
- MEF finds three Export modules from each module DLL, an Export type
ParentModule
which implements the IModule
interface.
- Each
ParentModule
is again a user control, and MEF initiates the ParentModule
controls and calls the Page_Load()
method, which in turn calls the OnModuleLoad()
method.
OnModuleLoad()
prepares the list<string>
of ASCX control names and calls the BaseModule.LoadModules()
method.
- In the
BaseModule.LoadModules()
method, it prepares a VirtualPath and calls the LoadControl()
method.
- If the virtual path starts with ~Modules\, then the
AssemblyVirtualPathProvider
of the Providers assembly will return the ASCX content from the current module DLL; else it will fall back to normal behavior. It loads all the content using the LoadControl()
method and adds it to the Controls
collection. That is how the exported type loading is finished.
The picture below depicts how MEF provides the extension points to the demo sample:
The picture below shows how we have used MEF:
The sample output when we run the application by keeping all the three module DLLs in the MEFSampleWeb\Modules folder is shown below (basically, grids will come up with data):
If we remove a DLL from the Modules folder, e.g., the Policies module, then the output will be as shown below (basically, grids come up with data):
If we design a fourth module and copy it to the Modules folder, then MEF will integrate the new module into the step.
Setup steps
This article uses the Entity Framework for company associated data. Download Mefscripts.txt from the given link and execute it on the database and update the connection string (MVCDBEntities, ApplicationServices) in the CompanyDataService web.config file.
After compiling the entire solution, copy the module1, module2, and module3 DLLs from the SampleMEF\Libraries folder to the MEFSampleWeb\Modules folder. We can now start playing around with the module DLLs.
Points of Interest
- Module development is clearly separated. The module developer just needs to know the MEF contract that he/she needs to implement.
VirtualPathProvider
is used to get the content of ASCX from the DLL. Register the new VirtualPathProvider
with ASP.NET.
- This mode of design/development is well suited for projects following agile methodology which can possibly have many sprints and changes to business requirements.
- Suitable for projects where step/module based design is required and modules need to be reusable across steps and applications.
Please vote for this article if you feel it was useful.
Disclaimer
Any application design (for that matter, data driven or MEF) will have its own merits and demerits. We as designers need to take many parameters into consideration before proposing a technical solution to the business problem. This article is not in favor of any particular framework. It is entirely the designer's/developer's responsibility to choose/evaluate the technical architecture based on their business requirements.