1
\$\begingroup\$

Edit Found the original contract name resolution by .NET MEF, the code below is not useful anymore. See response!

My issue was to use existing code, to make a MEF ExportFactory, imported via [Import] attribute to a property, return instances of Moq mocks (for unit testing). Problem was, that the original code always used the Type.FullName property value as contract name. This worked fine for non-generic types, but not for generics.

Generic FullName is like:

My.Assembly.GenericType`1[My.Assembly.InnerType]

but MEF expects a name like:

My.Assembly.GenericType(My.Assembly.InnerType)

The complete unit test code actually extends the MEF ExportProvider class, to register ExportDefinition instances for an ExportFactory and it's ProductDefinition. The contract name goes to a metadata dictionary, using the key CompositionConstants.ExportTypeIdentityMetadataName.

This following code is only for the contract name conversion. It calls itself recursively, for generic arguments which are generic themselves. The regular expression matches by the `[digits] name extension, used by .NET for generic types. It would be nice to see a generally better way, or already existing functions from MEF, if such exists.

Edit: I added the incrementor and code for open (unspecified) generics, which have names with indexed curly brackets: instead of IMyType<string,int> IMyType<,>, resolved to contract name IMyType({0},{1}). Regex has also been changed to match, if no type names come after `[digits].

private static readonly Regex GenericRegex = 
    new Regex(@"^(?<fullNameNonGeneric>\S+?)`\d+(\[.*\])*$", 
        RegexOptions.Compiled);


public static string ResolveGenericMefContractName(this Type type)
{
    return ResolveGenericMefContractName(type, new IndexIncrementor());
}

private static string ResolveGenericMefContractName(Type type, IndexIncrementor incrementor)
{
    var fullName = type?.FullName;
    if (fullName == null)
    {
        return null;
    }
    var match = GenericRegex.Match(fullName);
    if (!match.Success)
    {
        return type.FullName;
    }

    var fullNameNonGeneric = match.Groups["fullNameNonGeneric"].Value;
    var genericArgs = type.GetGenericArguments();

    return fullNameNonGeneric + "(" +
        string.Join(",", genericArgs.Select(
            ga => ResolveGenericMefContractName(ga, incrementor) 
            ?? "{" + incrementor.GetAndIncrementValue() + "}")) 
        + ")";
}

private class IndexIncrementor
{
    private int _value;

    public int GetAndIncrementValue() => _value++;
}
\$\endgroup\$
0
\$\begingroup\$

The code in the question was an attempt to generate type contract names, from the way I saw how MEF created them. Especially the difference from Type.FullName, which was previously used, when getting a contract name. For this reason, the code is a probably inferior re-implementation of MEF's own name generation, and should not be used for this purpose.

This is the contract name resolution used by MEF in .NET framework (maybe different namespace in Core, Apps etc.):

Namespace:

System.ComponentModel.Composition

Static method:

AttributedModelServices.GetContractName(Type)

Unfortunately, it's often easier to re-invent the wheel, than to find out about existing implementations...

\$\endgroup\$
0
\$\begingroup\$

I see you've found the right solution but let's make a review anyway as there are few things that I'm not particularly fond of.


var fullName = type?.FullName;
if (fullName == null)
{
  return null;
}

Instead of allowing type to be null, you should throw the ArgumentNullException. I cannot imagine any use-case where type being null would make sense. Usually this would mean a bug. You could return null later when resolving it fails but arguments should be always valid. Otherwise there are too many possibilities for the null result and it's could be difficult to tell why it didn't work.


The method name is confusing becaues it says Resolve while it actually creates a MefContractName thus at first I thougt that when the regex fails here

if (!match.Success)
{
  return type.FullName;
}

then it should return null. While in the first casae this would make sense, it wouldn't for creation so I suggest naming this extension as ToMefContractName. Now it all makes sense again.

I'm now sure what you need this IndexIncrementor for as the type should never be null so it will never be called.

var genericArgs = type.GetGenericArguments();

return fullNameNonGeneric + "(" +
  string.Join(",", genericArgs.Select(
      ga => ToMefContractName(ga, incrementor)
      ?? "{" + incrementor.Next() + "}"))
  + ")";

You can turn this into a more readable query without the helper variable genericArgs that this time doesn't help in any way.

This is how I think it should look like (with IndexIncrementor removed because I could figure out what it is for):

private static string ToMefContractName(Type type)
{
    if (type == null) throw new ArgumentNullException(nameof(type));

    var match = GenericRegex.Match(type.FullName);
    if (!match.Success)
    {
        return type.FullName;
    }

    var fullNameNonGeneric = match.Groups["fullNameNonGeneric"].Value;

    var genericArguments =
        from genericArgument in type.GetGenericArguments()
        select ToMefContractName(genericArgument);

    return $"{fullNameNonGeneric}({string.Join(",", genericArguments)})";
}

The IndexIncrementor should be called just Index and have one read-only property named Value and its method should be called Next. Saying GetAndIncrementValue reveals the internal implementation and this is not important to the user, he just wants to have the next value.

private class Index
{
    public int Value { get; private set; }

    public int Next() => Value++;
}
\$\endgroup\$
  • \$\begingroup\$ OK, thanks. But on Type, method GetGenericArguments() returns null for open generics; in this case, the code worked as desired. This is different from the GenericTypeArguments property, which returns only specified generic arguments. I just noticed this later, when trying again with the property. \$\endgroup\$ – Erik Hart Dec 6 '18 at 9:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.