Search code examples
asp.net-mvcmefcomposition

What is a good way to compose parts that aren't imported by the class performing composition?


I have an ASP.NET MVC application which uses MEF to implement a plugin framework. Plugins are separate DLLs that exist in the application's bin directory. Plugins usually export one or more controllers like this...

<Export(GetType(IController))>
<MYAPP_Interfaces.Attributes.MVCPluginMetadata(
    "SomePlugin",
    "A Description for the plugin",
    "A Readable Name",
    {"ScriptsForThePlugin.js"},
    {"StylesForThePlugin.css"},
    Enumerations.MVCPluginType.DataView,
    "DefaultActionName")>
<PartCreationPolicy(CreationPolicy.NonShared)>
Public Class MyPluginController
     Inherits System.Web.Mvc.Controller

    <Import()>
    Private m_objHost As IWebHost

... and so on.

This all works fine, the host app includes all controllers in an ImportMany property, and composes itself upon creation in the usual way. So m_objHost is populated automagically and the controller has access to all the things the host application provides, like logging and information about the user and what they're currently working on and all that.

My question has to do with my models, and any DAL or utility classes that I have in a plugin. These classes usually have need of information from the IWebHost object. However, the host doesn't need to know about these classes, so they are not included in composition. Since they are not composed, if they want an IWebHost reference they each have to compose themselves upon instantiation, like this:

Public Class MyModel

<Import()>
Private m_objHost As IWebHost

<Import()>
Private m_objLog As ILog


Public Sub New()
    Compose()
End Sub

...

Private Sub Compose()
    Try
        Dim objCatalog As New AggregateCatalog
        objCatalog.Catalogs.Add(New DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory & "bin"))
        Dim container As New CompositionContainer(objCatalog)
        container.ComposeParts(Me)
    Catch ex As Exception
        If m_objLog IsNot Nothing Then
            m_objLog.writeError(ex)
        End If
    End Try
End Sub
End Class

So my main question can be broken into two parts:

  1. Is there any noticeable performance problem with having, say, 20 or so classes that perform composition whenever they are instantiated? I currently only have a few, and if there is a hit it's not noticeable. In other words, do I actually need to change this strategy? It violates DRY because I have a Compose method in every class, but I can learn to live with it.

  2. Is there a better way? How can I handle a single composition in the main application that takes care of populating all of the classes in the plugins, including those not imported in the class performing the main composition?

I've considered the following:

  • Having all models and utility classes and whatever implement a marker interface, export them all using that interface as a contract, and importing them in the host class, even though the host class doesn't need them. I think this is an even cruddier design than what I have, and I don't want to do it. I'm willing to listen to arguments in favor of this, though.

  • Having a class in each plugin that needs it that implements IWebHost that acts as a wrapper for the class exported by the main app. I'd still have to do composition in each plugin, but at least it would only be once per plugin. This one seems okay to me.

Thanks in advance for any help you can give, and for reading this whole novel of a question.


Solution

  • I wound up adding a class like the one below to the plugins that need it. I have a project template for plugins, so I'll probably just add this class to that template.

    Any class that needs something from the host can access it by calling PluginUtility.Host.

    Public Class PluginUtility
    
        <Import()>
        Private m_objHost As IWebHost
    
        Private Shared m_objInstance As PluginUtility
    
        Private Sub New()
           Compose()
        End Sub
    
        Public Shared ReadOnly Property Host As IWebHost
            Get
                If m_objInstance Is Nothing Then
                    m_objInstance = New PluginUtility
                End If
                Return m_objInstance.m_objHost
            End Get
        End Property
    
        Private Sub Compose()
            Try
                Dim objCatalog As New AggregateCatalog
                objCatalog.Catalogs.Add(New DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory & "bin"))
                Dim container As New CompositionContainer(objCatalog)
            container.ComposeParts(Me)
            Catch ex As Exception
                Console.Write("Could not compose to get a reference to the host")
            End Try
        End Sub
    End Class