Partager via


Managed Extensibility Framework (MEF) - Cadre de gérabilité extensible (MEF)

Cet article fournit une vue d’ensemble du Framework d’extensibilité managé qui a été introduit dans .NET Framework 4.

Qu’est-ce que MEF ?

L’infrastructure d’extensibilité managée (MEF) est une bibliothèque permettant de créer des applications légères et extensibles. Il permet aux développeurs d’applications de découvrir et d’utiliser des extensions sans configuration requise. Il permet également aux développeurs d’extension d’encapsuler facilement le code et d’éviter les dépendances difficiles fragiles. MEF permet non seulement de réutiliser les extensions au sein des applications, mais également entre les applications.

Le problème de l’extensibilité

Imaginez que vous êtes l’architecte d’une grande application qui doit fournir une prise en charge de l’extensibilité. Votre application doit inclure un nombre potentiellement important de composants plus petits et est responsable de la création et de l’exécution de ces composants.

L’approche la plus simple du problème consiste à inclure les composants en tant que code source dans votre application et à les appeler directement à partir de votre code. Cela présente un certain nombre d’inconvénients évidents. Plus important encore, vous ne pouvez pas ajouter de nouveaux composants sans modifier le code source, une restriction qui peut être acceptable dans, par exemple, une application web, mais qui n’est pas utilisable dans une application cliente. Tout aussi problématique, vous n’avez peut-être pas accès au code source des composants, car ils peuvent être développés par des tiers et, pour la même raison, vous ne pouvez pas les autoriser à accéder à la vôtre.

Une approche légèrement plus sophistiquée consisterait à fournir un point d’extension ou une interface pour permettre le découplage entre l’application et ses composants. Sous ce modèle, vous pouvez fournir une interface qu’un composant peut implémenter et une API pour lui permettre d’interagir avec votre application. Cela résout le problème de l’exigence d’accès au code source, mais il a toujours ses propres difficultés.

Étant donné que l’application n’a pas la capacité de découvrir des composants par elle-même, elle doit toujours être explicitement informée des composants disponibles et de ceux qui doivent être chargés. Cela s’effectue généralement en inscrivant explicitement les composants disponibles dans un fichier de configuration. Cela signifie que l’assurance que les composants sont corrects devient un problème de maintenance, en particulier s’il s’agit de l’utilisateur final et non du développeur qui est censé effectuer la mise à jour.

En outre, les composants sont incapables de communiquer entre eux, sauf par le biais des canaux définis rigidement de l’application elle-même. Si l’architecte d’applications n’a pas prévu la nécessité d’une communication particulière, il est généralement impossible.

Enfin, les développeurs de composants doivent accepter une dépendance difficile sur l’assembly qui contient l’interface qu’ils implémentent. Cela rend difficile l’utilisation d’un composant dans plusieurs applications et peut également créer des problèmes lorsque vous créez une infrastructure de test pour les composants.

Ce que MEF fournit

Au lieu de cette inscription explicite des composants disponibles, MEF permet de les découvrir implicitement via la composition. Un composant MEF, appelé partie, spécifie de manière déclarative ses dépendances (appelées importations) et les fonctionnalités (appelées exportations) qu’il met à disposition. Lorsqu’un composant est créé, le moteur de composition MEF satisfait ses importations avec ce qui est disponible à partir d’autres parties.

Cette approche résout les problèmes abordés dans la section précédente. Étant donné que les composants MEF spécifient de manière déclarative leurs fonctionnalités, ils sont détectables au moment de l’exécution, ce qui signifie qu’une application peut utiliser des parties sans références codées en dur ou fichiers de configuration fragiles. MEF permet aux applications de découvrir et d'examiner des composants grâce à leurs métadonnées, sans les instancier ni même charger leurs assemblies. Par conséquent, il n’est pas nécessaire de spécifier soigneusement quand et comment les extensions doivent être chargées.

En plus de ses exportations fournies, une partie peut spécifier ses importations, qui seront remplies par d’autres parties. Cela permet et facilite la communication entre les parties, tout en garantissant une bonne factorisation du code. Par exemple, les services communs à de nombreux composants peuvent être pris en compte dans une partie distincte et facilement modifiés ou remplacés.

Étant donné que le modèle MEF ne nécessite aucune dépendance matérielle sur un assembly d’application particulier, il permet aux extensions d’être réutilisées de l’application à l’application. Cela facilite également le développement d’un harnais de test, indépendamment de l’application, pour tester les composants d’extension.

Une application extensible écrite à l’aide de MEF déclare une importation qui peut être remplie par des composants d’extension et peut également déclarer des exportations pour exposer les services d’application aux extensions. Chaque composant d’extension déclare une exportation et peut également déclarer des importations. De cette façon, les composants d’extension eux-mêmes sont automatiquement extensibles.

Où MEF est disponible

MEF fait partie intégrante du .NET Framework 4 et est disponible partout où le .NET Framework est utilisé. Vous pouvez utiliser MEF dans vos applications clientes, qu’elles utilisent Windows Forms, WPF ou toute autre technologie, ou dans les applications serveur qui utilisent ASP.NET.

MEF et MAF

Les versions précédentes du .NET Framework ont introduit le Framework de complément managé (MAF), conçu pour permettre aux applications d’isoler et de gérer les extensions. L'objectif de MAF est légèrement plus élevé que celui de MEF, en se concentrant sur l'isolation des extensions et le chargement et déchargement des assemblages, tandis que l'objectif de MEF est la découvrabilité, l'extensibilité et la portabilité. Les deux frameworks interagissent en douceur et une application unique peut tirer parti des deux.

SimpleCalculator : exemple d’application

La façon la plus simple de voir ce que MEF peut faire consiste à créer une application MEF simple. Dans cet exemple, vous créez une calculatrice très simple nommée SimpleCalculator. L’objectif de SimpleCalculator est de créer une application console qui accepte les commandes arithmétiques de base, sous la forme « 5+3 » ou « 6-2 », et retourne les réponses correctes. À l’aide de MEF, vous pourrez ajouter de nouveaux opérateurs sans modifier le code de l’application.

Pour télécharger le code complet de cet exemple, consultez l’exemple SimpleCalculator (Visual Basic).

Remarque

L’objectif de SimpleCalculator est de démontrer les concepts et la syntaxe de MEF, plutôt que de fournir un scénario réaliste pour son utilisation. La plupart des applications qui bénéficieraient le plus de la puissance de MEF sont plus complexes que SimpleCalculator. Pour obtenir des exemples plus détaillés, consultez le Framework d’extensibilité managé sur GitHub.

  • Pour commencer, dans Visual Studio, créez un projet d’application console et nommez-le SimpleCalculator.

  • Ajoutez une référence à l’assembly System.ComponentModel.Composition , où SE TROUVE MEF.

  • Ouvrez Module1.vb ou Program.cs et ajoutez les directives Imports ou using pour System.ComponentModel.Composition et System.ComponentModel.Composition.Hosting. Ces deux espaces de noms contiennent des types MEF dont vous aurez besoin pour développer une application extensible.

  • Si vous utilisez Visual Basic, ajoutez le Public mot clé à la ligne qui déclare le Module1 module.

Catalogues et conteneur de composition

Le cœur du modèle de composition MEF est le conteneur de composition, qui contient toutes les parties disponibles et effectue la composition. La composition correspond à la mise en correspondance des importations aux exportations. Le type de conteneur de composition le plus courant est CompositionContainer, et vous allez l’utiliser pour SimpleCalculator.

Si vous utilisez Visual Basic, ajoutez une classe publique nommée Program dans Module1.vb.

Ajoutez la ligne suivante à la Program classe dans Module1.vb ou Program.cs :

Dim _container As CompositionContainer
private CompositionContainer _container;

Pour découvrir les parties disponibles, les conteneurs de composition utilisent un catalogue. Un catalogue est un objet qui rend les parties disponibles découvertes à partir d’une source. MEF fournit des catalogues pour découvrir des parties à partir d’un type fourni, d’un assembly ou d’un répertoire. Les développeurs d’applications peuvent facilement créer de nouveaux catalogues pour découvrir des composants à partir d’autres sources, comme un service web.

Ajoutez le constructeur suivant à la Program classe :

Public Sub New()
    ' An aggregate catalog that combines multiple catalogs.
     Dim catalog = New AggregateCatalog()

    ' Adds all the parts found in the same assembly as the Program class.
    catalog.Catalogs.Add(New AssemblyCatalog(GetType(Program).Assembly))

    ' Create the CompositionContainer with the parts in the catalog.
    _container = New CompositionContainer(catalog)

    ' Fill the imports of this object.
    Try
        _container.ComposeParts(Me)
    Catch ex As CompositionException
        Console.WriteLine(ex.ToString)
    End Try
End Sub
private Program()
{
    try
    {
        // An aggregate catalog that combines multiple catalogs.
        var catalog = new AggregateCatalog();
        // Adds all the parts found in the same assembly as the Program class.
        catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));

        // Create the CompositionContainer with the parts in the catalog.
        _container = new CompositionContainer(catalog);
        _container.ComposeParts(this);
    }
    catch (CompositionException compositionException)
    {
        Console.WriteLine(compositionException.ToString());
    }
}

L'appel à ComposeParts indique au conteneur de composition de composer un ensemble spécifique de composants, dans ce cas, l'instance actuelle de Program. À ce stade, cependant, rien ne se produira, car Program n’a pas d’importations à remplir.

Importations et exportations avec attributs

Tout d'abord, vous devez Program importer une calculatrice. Cela permet la séparation des problèmes d’interface utilisateur, tels que l’entrée et la sortie de la console qui entreront dans Program, de la logique de la calculatrice.

Ajoutez le code suivant à la classe Program :

<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;

Notez que la déclaration de l’objet calculator n’est pas inhabituelle, mais qu’elle est décorée avec l’attribut ImportAttribute . Cet attribut déclare quelque chose à importer ; autrement dit, il sera rempli par le moteur de composition lorsque l’objet est composé.

Chaque importation a un contrat, qui détermine les exportations avec lesquelles il sera mis en correspondance. Le contrat peut être une chaîne spécifiée explicitement, ou elle peut être générée automatiquement par MEF à partir d’un type donné, dans ce cas l’interface ICalculator. Toute exportation déclarée avec un contrat correspondant répond à cette importation. Notez que si le type de l’objet calculator est en fait ICalculator, cela n’est pas obligatoire. Le contrat est indépendant du type de l’objet d’importation. (Dans ce cas, vous pouvez quitter le typeof(ICalculator). MEF suppose automatiquement que le contrat doit être basé sur le type de l’importation, sauf si vous le spécifiez explicitement.)

Ajoutez cette interface très simple au module ou à l’espace de noms SimpleCalculator.

Public Interface ICalculator
    Function Calculate(input As String) As String
End Interface
public interface ICalculator
{
    string Calculate(string input);
}

Maintenant que vous avez défini ICalculator, vous avez besoin d’une classe qui l’implémente. Ajoutez la classe suivante au module ou SimpleCalculator à l’espace de noms :

<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
   Implements ICalculator

End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{

}

Voici l’exportation qui correspond à l’importation dans Program. Pour que l’exportation corresponde à l’importation, l’exportation doit avoir le même contrat. L'exportation en vertu d'un contrat basé sur typeof(MySimpleCalculator) entraînerait un décalage, et l'importation ne serait pas exécutée ; le contrat doit être exactement conforme.

Étant donné que le conteneur de composition sera rempli avec tous les composants disponibles dans cet assembly, le MySimpleCalculator composant sera disponible. Lorsque le constructeur pour Program effectue la composition sur l'objet Program, son importation sera remplie d'un objet MySimpleCalculator, qui sera créé à cet effet.

La couche d’interface utilisateur (Program) n’a pas besoin de savoir quoi que ce soit d’autre. Vous pouvez donc renseigner le reste de la logique d’interface utilisateur dans la Main méthode.

Ajoutez le code suivant à la méthode Main :

Sub Main()
    ' Composition is performed in the constructor.
    Dim p As New Program()
    Dim s As String
    Console.WriteLine("Enter Command:")
    While (True)
        s = Console.ReadLine()
        Console.WriteLine(p.calculator.Calculate(s))
    End While
End Sub
static void Main(string[] args)
{
    // Composition is performed in the constructor.
    var p = new Program();
    Console.WriteLine("Enter Command:");
    while (true)
    {
        string s = Console.ReadLine();
        Console.WriteLine(p.calculator.Calculate(s));
    }
}

Ce code lit simplement une ligne d’entrée et appelle la fonction Calculate de ICalculator sur le résultat, qu’il écrit dans la console. C’est tout le code dont vous avez besoin dans Program. Tout le reste du travail se déroulera dans les sections.

Attributs Imports et ImportMany

Pour que SimpleCalculator soit extensible, il doit importer une liste d’opérations. Un attribut ImportAttribute standard est rempli par une seule exportation ExportAttribute. Si plusieurs sont disponibles, le moteur de composition génère une erreur. Pour créer une importation qui peut être remplie par n’importe quel nombre d’exportations, vous pouvez utiliser l’attribut ImportManyAttribute .

Ajoutez la propriété d’opérations suivante à la MySimpleCalculator classe :

<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;

Lazy<T,TMetadata> est un type fourni par MEF pour contenir des références indirectes aux exportations. Ici, en plus de l’objet exporté lui-même, vous obtenez également des métadonnées d’exportation ou des informations qui décrivent l’objet exporté. Chacun Lazy<T,TMetadata> contient un IOperation objet, représentant une opération réelle et un IOperationData objet, représentant ses métadonnées.

Ajoutez les interfaces simples suivantes au module ou à l'espace de noms SimpleCalculator :

Public Interface IOperation
    Function Operate(left As Integer, right As Integer) As Integer
End Interface

Public Interface IOperationData
    ReadOnly Property Symbol As Char
End Interface
public interface IOperation
{
     int Operate(int left, int right);
}

public interface IOperationData
{
    char Symbol { get; }
}

Dans ce cas, les métadonnées de chaque opération sont le symbole qui représente cette opération, telle que +, -, *, etc. Pour rendre l’opération d’ajout disponible, ajoutez la classe suivante au module ou SimpleCalculator à l’espace de noms :

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "+"c)>
Public Class Add
    Implements IOperation

    Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
        Return left + right
    End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
class Add: IOperation
{
    public int Operate(int left, int right)
    {
        return left + right;
    }
}

L’attribut ExportAttribute fonctionne comme avant. L’attribut ExportMetadataAttribute attache des métadonnées, sous la forme d’une paire nom-valeur, à cette exportation. Alors que la Add classe implémente IOperation, une classe qui implémente IOperationData n’est pas définie explicitement. Au lieu de cela, une classe est implicitement créée par MEF avec des propriétés basées sur les noms des métadonnées fournies. (Il s’agit de l’une des différentes façons d’accéder aux métadonnées dans MEF.)

La composition dans MEF est récursive. Vous avez composé explicitement l’objet Program , qui a importé un ICalculator qui s’est avéré être de type MySimpleCalculator. MySimpleCalculator, à son tour, importe une collection d’objets IOperation, et cette importation sera effectuée lors de la création de MySimpleCalculator, simultanément aux importations de Program. Si la Add classe avait déclaré un import supplémentaire, cela devrait également être rempli, etc. Toute importation laissée sans remplissage entraîne une erreur de composition. (Toutefois, il est possible de déclarer des importations pour qu’elles soient facultatives ou pour leur attribuer des valeurs par défaut.)

Logique de calculatrice

Avec ces parties en place, tout ce qui reste est la logique de calculatrice elle-même. Ajoutez le code suivant dans la MySimpleCalculator classe pour implémenter la Calculate méthode :

Public Function Calculate(input As String) As String Implements ICalculator.Calculate
    Dim left, right As Integer
    Dim operation As Char
    ' Finds the operator.
    Dim fn = FindFirstNonDigit(input)
    If fn < 0 Then
        Return "Could not parse command."
    End If
    operation = input(fn)
    Try
        ' Separate out the operands.
        left = Integer.Parse(input.Substring(0, fn))
        right = Integer.Parse(input.Substring(fn + 1))
    Catch ex As Exception
        Return "Could not parse command."
    End Try
    For Each i As Lazy(Of IOperation, IOperationData) In operations
        If i.Metadata.symbol = operation Then
            Return i.Value.Operate(left, right).ToString()
        End If
    Next
    Return "Operation not found!"
End Function
public String Calculate(string input)
{
    int left;
    int right;
    char operation;
    // Finds the operator.
    int fn = FindFirstNonDigit(input);
    if (fn < 0) return "Could not parse command.";

    try
    {
        // Separate out the operands.
        left = int.Parse(input.Substring(0, fn));
        right = int.Parse(input.Substring(fn + 1));
    }
    catch
    {
        return "Could not parse command.";
    }

    operation = input[fn];

    foreach (Lazy<IOperation, IOperationData> i in operations)
    {
        if (i.Metadata.Symbol.Equals(operation))
        {
            return i.Value.Operate(left, right).ToString();
        }
    }
    return "Operation Not Found!";
}

Les étapes initiales analysent la chaîne d'entrée dans les opérandes de gauche et de droite, et un caractère d'opérateur. Dans la foreach boucle, chaque membre de la operations collection est examiné. Ces objets sont de type Lazy<T,TMetadata>, et leurs valeurs de métadonnées et objet exporté sont accessibles respectivement avec la Metadata propriété et la Value propriété. Dans ce cas, si la Symbol propriété de l’objet IOperationData est découverte comme une correspondance, la calculatrice appelle la Operate méthode de l’objet IOperation et retourne le résultat.

Pour terminer la calculatrice, vous avez également besoin d’une méthode d’assistance qui retourne la position du premier caractère non numérique dans une chaîne. Ajoutez la méthode d’assistance suivante à la MySimpleCalculator classe :

Private Function FindFirstNonDigit(s As String) As Integer
    For i = 0 To s.Length - 1
        If Not Char.IsDigit(s(i)) Then Return i
    Next
    Return -1
End Function
private int FindFirstNonDigit(string s)
{
    for (int i = 0; i < s.Length; i++)
    {
        if (!char.IsDigit(s[i])) return i;
    }
    return -1;
}

Vous devez maintenant être en mesure de compiler et d’exécuter le projet. Dans Visual Basic, vérifiez que vous avez ajouté le Public mot clé à Module1. Dans la fenêtre de console, tapez une opération d’ajout, telle que « 5+3 », et la calculatrice retourne les résultats. Tout autre opérateur génère le message « Opération introuvable ! ».

Étendre SimpleCalculator à l’aide d’une nouvelle classe

Maintenant que la calculatrice fonctionne, l’ajout d’une nouvelle opération est facile. Ajoutez la classe suivante au module ou SimpleCalculator à l’espace de noms :

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
    Implements IOperation

    Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
        Return left - right
    End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : IOperation
{
    public int Operate(int left, int right)
    {
        return left - right;
    }
}

Compilez et exécutez le projet. Tapez une opération de soustraction, telle que « 5-3 ». La calculatrice prend désormais en charge la soustraction ainsi que l’ajout.

Étendre SimpleCalculator à l’aide d’un nouvel assembly

L’ajout de classes au code source est relativement simple, mais MEF offre la possibilité de chercher des éléments au-delà de la source propre à l'application. Pour illustrer cela, vous devez modifier SimpleCalculator pour rechercher un répertoire, ainsi que son propre assembly, pour les parties, en ajoutant un DirectoryCatalog.

Ajoutez un nouveau répertoire nommé Extensions au projet SimpleCalculator. Veillez à l’ajouter au niveau du projet, et non au niveau de la solution. Ajoutez ensuite un nouveau projet de bibliothèque de classes à la solution, nommé ExtendedOperations. Le nouveau projet sera compilé dans un assemblage distinct.

Ouvrez le Concepteur de propriétés du projet ExtendedOperations, puis cliquez sur l’onglet Compiler ou Générer . Modifiez le chemin de sortie de build ou le chemin de sortie pour pointer vers le répertoire Extensions dans le répertoire du projet SimpleCalculator (.. \SimpleCalculator\Extensions\).

Dans Module1.vb ou Program.cs, ajoutez la ligne suivante au Program constructeur :

catalog.Catalogs.Add(
    New DirectoryCatalog(
        "C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(
    new DirectoryCatalog(
        "C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));

Remplacez l’exemple de chemin d’accès par le chemin d’accès à votre répertoire Extensions. (Ce chemin absolu est à des fins de débogage uniquement. Dans une application de production, vous utiliseriez un chemin relatif.) L’élément DirectoryCatalog ajoute désormais toutes les parties trouvées dans tous les assemblys du répertoire Extensions au conteneur de composition.

Dans le ExtendedOperations projet, ajoutez des références à SimpleCalculator et System.ComponentModel.Composition. Dans le ExtendedOperations fichier de classe, ajoutez une Imports ou une using directive pour System.ComponentModel.Composition. En Visual Basic, ajoutez également une Imports instruction pour SimpleCalculator. Ajoutez ensuite la classe suivante au ExtendedOperations fichier de classe :

<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
    Implements IOperation

    Public Function Operate(left As Integer, right As Integer) As Integer Implements IOperation.Operate
        Return left Mod right
    End Function
End Class
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '%')]
public class Mod : SimpleCalculator.IOperation
{
    public int Operate(int left, int right)
    {
        return left % right;
    }
}

Notez que pour que le contrat corresponde, l’attribut ExportAttribute doit avoir le même type que le ImportAttribute.

Compilez et exécutez le projet. Testez le nouvel opérateur Mod (%).

Conclusion

Cette rubrique a abordé les concepts de base de MEF.

  • Composants, catalogues et conteneur de composition

    Les parties et le conteneur de composition sont les blocs de construction de base d’une application MEF. Une partie est un objet qui importe ou exporte une valeur, y compris lui-même. Un catalogue fournit une collection de parties provenant d’une source particulière. Le conteneur de composition utilise les parties fournies par un catalogue pour effectuer la composition, la liaison des importations aux exportations.

  • Importations et exportations

    Les importations et les exportations sont la façon dont les composants communiquent. Avec une importation, le composant spécifie la nécessité d’une valeur ou d’un objet particulier, et avec une exportation, elle spécifie la disponibilité d’une valeur. Chaque importation est associée à une liste d’exportations par le biais de son contrat.

Étapes suivantes

Pour télécharger le code complet de cet exemple, consultez l’exemple SimpleCalculator (Visual Basic).

Pour plus d’informations et des exemples de code, consultez Managed Extensibility Framework. Pour obtenir la liste des types MEF, voir l’espace de noms System.ComponentModel.Composition.