Prioritizing MEF Parts – Or When Things Are Easy
So much of what I do with technology turns out ten times harder than I think it should be that I’ll admit I walked very softly and fearful of the quicksand when I wanted to implement my own prioritization strategy for MEF parts. In the end, the MEF interaction was dirt simple.
For review, MEF has a prioritization strategy. When you are looking for parts, the first export provider that fulfills the request (contract and cardinality) ends the part search process (the GetExport(s) variation). However, the generation harness has more complex requirements that this allows.
To create an application, you’ll use a set of templates. Templates will have a specific purpose, such as a Select stored procedure. I’ll supply a default for this, but you may not like my default and will want to replace it.
I want to do this while maintaining complete ignorance in the template about what contracts you are working with. I can’t use the multiple export provider strategy because I want all the templates in all of the catalogs considered.
You also will probably want to replace metadata and service parts. This scenario is different because the template knows the specific contract. Because there is a specific contract request, the MEF native prioritization could potentially work, except that I want the simplicity of a single export provider to manage the templates and do not want multiple export providers/containers.
Parts, including templates, have a contract, which in MEF is a string. There will be many templates doing multiple jobs in the generation process. MEF just grabs everything that implements IProcessWrapper when it grabs templates. I want another level of selection.
All contracts involved in the prioritization strategy have an Id and a priority as MEF metadata. MEF metadata is just a dictionary of values so I can check whether an Id or priority exist in the export definition. I want to run only the highest priority template for each Id. An Id might be “SelectStoredProc” or some other descriptive name. For convenience I’ll supply constants for the common templates. My templates will have a priority of 0 and will only run if no other templates are available. I’ll include a different post about why I think we can all play nice with priorities and about how to override priorities, after I figure out how I’m going to do that.
To accomplish this…
I created a new aggregate catalog that derived from the MEF AggregateCatalog and override the GetExports method.
Public Class PrioritizedPartCatalog
Public Overrides Function GetExports( _
ByVal definition As Primitives.ImportDefinition) _
As IEnumerable(Of TempTuple(Of Primitives.ComposablePartDefinition, Primitives.ExportDefinition))
[[ In case I confused the C# folks, VB has “partial namespaces” which means it tries to tack namespace pieces like “Primitives” onto the stated namespaces and applies a little more clarity to namespace organization. I could have alternatively imported System.ComponentModel.Composition.Primitives]]
I want to grab all of the matching parts and do some additional filtering.
Dim allExports = MyBase.GetExports(definition)
The next part involves some KinkyLinq (I should probably grab that url). When I’m doing hard stuff in Linq, I split things up into multiple queries. Linq will optimize this if it can, and my brain won’t explode from one overly complex statement. The first query creates groups by Id. The second grabs the highest priority item in each group:
Dim q1 = From y In allExports _
Group y By key = GetId(y) _
Into Group _
Select key, Group
Dim q2 = From g In q1 _
From y In g.Group _
Where GetPriority(y) = _
g.Group.Max(Function(z) GetPriority(z)) _
Not all parts will opt into prioritization. This is entirely optional. Managing this with priority is relatively easy. Any part without a priority has a priority of zero. However, managing Id’s for things that opt out of prioritization is a little more complex.
Private Function GetId( _
ByVal tuple As TempTuple( _
Of Primitives.ComposablePartDefinition, _
' The guid means any export that opted out of prioritization is included
Dim idObject As Object = Nothing
Dim id = CStr(idObject)
If String.IsNullOrEmpty(id) Then
If the ID exists, it’s used. If the Id does not exist, a Guid takes its place for the purpose of the filter. That means anything without an Id will always be considered the sole member of a unique group and will always be included since one member is selected for each Id.
The amusing thing about this solution is looking back in retrospect at where I spent my time. I spent a lot of time figuring out what could be done with MEF. I spent some time doing something wrong with Id’s at the metadata attribute level (which does not need to be complex) and spend some time on that KinkyLinq statement. Thanks to Glenn Block, Jay Harlow, Rob Teixaira, and Andreas for their help.
What I didn’t spend much time on is actually fitting the solution into LINQ. Adding this additional filtering involved only the code here (and the helper function for GetId() which you can predict). How simple can you get? The extensibility of MEF is what really makes playing with it worthwhile. If you hit a problem, like my unique filtering needs, you can probably find a way to work around it.