Compartilhar via


MEF (Managed Extensibility Framework)

Este artigo fornece uma visão geral do Managed Extensibility Framework que foi introduzido no .NET Framework 4.

O que é MEF?

A MEF (Managed Extensibility Framework) é uma biblioteca para criar aplicativos leves e extensíveis. Ele permite que os desenvolvedores de aplicativos descubram e usem extensões sem a necessidade de configuração. Ele também permite que os desenvolvedores de extensão encapsulem facilmente o código e evitem dependências rígidas frágeis. O MEF não só permite que as extensões sejam reutilizados em aplicativos, mas também entre aplicativos.

O problema da extensibilidade

Imagine que você é o arquiteto de um aplicativo grande que deve fornecer suporte para extensibilidade. Seu aplicativo precisa incluir um número potencialmente grande de componentes menores e é responsável por criá-los e executá-los.

A abordagem mais simples para o problema é incluir os componentes como código-fonte em seu aplicativo e chamá-los diretamente do seu código. Isso tem várias desvantagens óbvias. Mais importante, você não pode adicionar novos componentes sem modificar o código-fonte, uma restrição que pode ser aceitável em, por exemplo, um aplicativo Web, mas é inviável em um aplicativo cliente. Igualmente problemático, talvez você não tenha acesso ao código-fonte dos componentes, pois eles podem ser desenvolvidos por terceiros e, pelo mesmo motivo, você não pode permitir que eles acessem o seu.

Uma abordagem um pouco mais sofisticada seria fornecer um ponto de extensão ou interface para permitir a desacoplamento entre o aplicativo e seus componentes. Nesse modelo, você pode fornecer uma interface que um componente pode implementar e uma API para habilitá-lo a interagir com seu aplicativo. Isso resolve o problema de exigir acesso ao código-fonte, mas ainda tem suas próprias dificuldades.

Como o aplicativo não tem capacidade para descobrir componentes por conta própria, ele ainda deve ser explicitamente informado quais componentes estão disponíveis e devem ser carregados. Isso normalmente é feito registrando explicitamente os componentes disponíveis em um arquivo de configuração. Isso significa que assegurar que os componentes estão corretos torna-se um problema de manutenção, especialmente se for o usuário final e não o desenvolvedor que deverá fazer a atualização.

Além disso, os componentes são incapazes de se comunicar uns com os outros, exceto pelos canais rigidamente definidos do próprio aplicativo. Se o arquiteto de aplicativos não antecipou a necessidade de uma comunicação específica, geralmente é impossível.

Por fim, os desenvolvedores de componentes devem aceitar uma dependência rígida sobre qual assembly contém a interface que implementam. Isso dificulta o uso de um componente em mais de um aplicativo e também pode criar problemas ao criar uma estrutura de teste para componentes.

O que o MEF fornece

Em vez desse registro explícito de componentes disponíveis, o MEF fornece uma maneira de descobri-los implicitamente, por meio da composição. Um componente MEF, chamado de parte, especifica declarativamente suas dependências ( conhecidas como importações) e quais recursos (conhecidos como exportações) ele disponibiliza. Quando uma peça é criada, o mecanismo de composição do MEF atende às suas importações com o que está disponível de outras peças.

Essa abordagem resolve os problemas discutidos na seção anterior. Como as partes MEF especificam declarativamente suas funcionalidades, elas são detectáveis em runtime, o que significa que um aplicativo pode usar partes sem referências codificadas ou arquivos de configuração frágeis. O MEF permite que as aplicações descubram e examinem partes por meio de seus metadados, sem instanciá-las ou mesmo carregar suas assemblies. Como resultado, não é necessário especificar cuidadosamente quando e como as extensões devem ser carregadas.

Além das exportações fornecidas, uma parte pode especificar suas importações, que serão preenchidas por outras partes. Isso torna a comunicação entre partes não apenas possível, mas fácil e permite um bom fatoramento de código. Por exemplo, serviços comuns a muitos componentes podem ser levados em conta em uma parte separada e facilmente modificados ou substituídos.

Como o modelo MEF não requer nenhuma dependência rígida em um assembly de aplicativo específico, ele permite que as extensões sejam reutilizadas entre aplicativos. Isso também facilita o desenvolvimento de um arreio de teste, independente do aplicativo, para testar componentes de extensão.

Um aplicativo extensível escrito usando MEF declara uma importação que pode ser preenchida por componentes de extensão e também pode declarar exportações para expor os serviços de aplicativos a extensões. Cada componente de extensão declara uma exportação e também pode declarar importações. Dessa forma, os próprios componentes de extensão são automaticamente extensíveis.

Onde o MEF está disponível

O MEF é uma parte integral do .NET Framework 4 e está disponível em qualquer lugar onde o .NET Framework é utilizado. Você pode usar o MEF em seus aplicativos cliente, sejam eles windows forms, WPF ou qualquer outra tecnologia, ou em aplicativos de servidor que usam ASP.NET.

MEF e MAF

As versões anteriores do .NET Framework introduziram o MAF (Managed Add-in Framework), projetado para permitir que os aplicativos isolem e gerenciem extensões. O foco do MAF é em um nível ligeiramente mais elevado que o MEF, concentrando-se no isolamento de extensão e carregamento e descarregamento do assembly, enquanto o foco do MEF é a descoberta, extensibilidade e portabilidade. As duas estruturas interoperam sem problemas e um único aplicativo pode tirar proveito de ambas.

SimpleCalculator: um aplicativo de exemplo

A maneira mais simples de ver o que o MEF pode fazer é criar um aplicativo MEF simples. Neste exemplo, você cria uma calculadora muito simples chamada SimpleCalculator. O objetivo do SimpleCalculator é criar um aplicativo de console que aceite comandos aritméticos básicos, no formato "5+3" ou "6-2", e retorna as respostas corretas. Usando o MEF, você poderá adicionar novos operadores sem alterar o código do aplicativo.

Para baixar o código completo deste exemplo, consulte o exemplo simplecalculator (Visual Basic).

Observação

A finalidade do SimpleCalculator é demonstrar os conceitos e a sintaxe do MEF, em vez de fornecer necessariamente um cenário realista para seu uso. Muitos dos aplicativos que mais se beneficiariam do poder do MEF são mais complexos do que o SimpleCalculator. Para obter exemplos mais extensos, consulte a Estrutura de Extensibilidade Gerenciada no GitHub.

  • Para começar, no Visual Studio, crie um novo projeto de Aplicativo de Console e nomeie-o SimpleCalculator.

  • Adicione uma referência ao System.ComponentModel.Composition assembly, onde o MEF está localizado.

  • Abra Module1.vb ou Program.cs e adicione Imports ou using diretivas para System.ComponentModel.Composition e System.ComponentModel.Composition.Hosting. Esses dois namespaces contêm tipos MEF que você precisará para desenvolver um aplicativo extensível.

  • Se você estiver usando o Visual Basic, adicione a Public palavra-chave à linha que declara o Module1 módulo.

Catálogos e contêiner de composição

O núcleo do modelo de composição do MEF é o contêiner de composição, que contém todas as partes disponíveis e executa a composição. A composição é a correspondência entre importações e exportações. O tipo mais comum de contêiner de composição é CompositionContainer, e você usará isso para SimpleCalculator.

Se você estiver usando o Visual Basic, adicione uma classe pública nomeada Program em Module1.vb.

Adicione a seguinte linha à Program classe em Module1.vb ou Program.cs:

Dim _container As CompositionContainer
private CompositionContainer _container;

Para descobrir as partes disponíveis, os contêineres de composição usam um catálogo. Um catálogo é um objeto que disponibiliza partes descobertas de alguma origem. O MEF fornece catálogos para descobrir peças de um tipo fornecido, um assembly ou de um diretório. Os desenvolvedores de aplicativos podem facilmente criar novos catálogos para descobrir partes de outras fontes, como um serviço Web.

Adicione o seguinte construtor à 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());
    }
}

A chamada para ComposeParts indica ao contêiner de composição que componha um conjunto específico de partes, neste caso, a instância atual de Program. Neste ponto, no entanto, nada acontecerá, já que Program não tem importações para preencher.

Importações e exportações com atributos

Primeiro, você deve Program importar uma calculadora. Isso permite a separação das preocupações da interface do usuário, como a entrada e a saída do console que serão inseridas em Program, da lógica da calculadora.

Adicione o código a seguir à classe Program:

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

Observe que a declaração do calculator objeto não é incomum, mas que ela é decorada com o ImportAttribute atributo. Esse atributo declara algo como uma importação; ou seja, ele será preenchido pelo mecanismo de composição quando o objeto for composto.

Cada importação tem um contrato, que determina com quais exportações ela será correspondida. O contrato pode ser uma cadeia de caracteres especificada explicitamente ou pode ser gerado automaticamente pelo MEF de um determinado tipo, nesse caso, a interface ICalculator. Qualquer exportação declarada com um contrato correspondente atenderá a essa importação. Observe que, embora o tipo do calculator objeto seja de fato ICalculator, isso não é necessário. O contrato é independente do tipo do objeto de importação. (Nesse caso, você pode deixar de fora o typeof(ICalculator). O MEF assumirá automaticamente que o contrato seja baseado no tipo da importação, a menos que você o especifique explicitamente.)

Adicione essa interface simples ao módulo ou ao namespace SimpleCalculator:

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

Agora que você definiu ICalculator, precisa de uma classe que a implemente. Adicione a seguinte classe ao módulo ou ao namespace SimpleCalculator:

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

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

}

Aqui está a exportação que corresponderá à importação em Program. Para que a exportação corresponda à importação, a exportação deve ter o mesmo contrato. Exportando sob um contrato com base em typeof(MySimpleCalculator) produziria uma incompatibilidade e a importação não seria preenchida. O contrato deve ter correspondência exata.

Dado que o contêiner de composição será preenchido com todas as partes disponíveis nesta montagem, a MySimpleCalculator parte estará disponível. Quando o construtor do Program executa a composição do objeto Program, sua importação será preenchida por um objeto MySimpleCalculator, que será criado para este fim.

A camada de interface do usuário (Program) não precisa saber mais nada. Portanto, você pode preencher o restante da lógica da interface do usuário no Main método.

Adicione o seguinte código ao método 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));
    }
}

Esse código simplesmente lê uma linha de entrada e chama a função Calculate do ICalculator no resultado, que ele grava de volta ao console. Esse é todo o código que você precisa.Program Todo o resto do trabalho ocorrerá nas peças.

Importações e atributos ImportMany

Para que SimpleCalculator seja extensível, ele precisa importar uma lista de operações. Um atributo comum ImportAttribute é preenchido por exatamente um ExportAttribute. Se mais de um estiver disponível, o mecanismo de composição produzirá um erro. Para criar uma importação que pode ser preenchida por qualquer número de exportações, você pode usar o ImportManyAttribute atributo.

Adicione a seguinte propriedade de operações à MySimpleCalculator classe:

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

Lazy<T,TMetadata> é um tipo fornecido pelo MEF para manter referências indiretas às exportações. Aqui, além do próprio objeto exportado, você também obtém metadados de exportação ou informações que descrevem o objeto exportado. Cada Lazy<T,TMetadata> um contém um IOperation objeto, representando uma operação real e um IOperationData objeto, representando seus metadados.

Adicione as interfaces simples a seguir ao módulo ou ao namespace 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; }
}

Nesse caso, os metadados de cada operação são o símbolo que representa essa operação, como +, -, *e assim por diante. Para disponibilizar a operação de adição, adicione a seguinte classe ao módulo ou SimpleCalculator namespace:

<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;
    }
}

O ExportAttribute atributo funciona como antes. O ExportMetadataAttribute atributo anexa metadados, na forma de um par nome-valor, a essa exportação. Enquanto a Add classe implementa IOperation, uma classe que implementa IOperationData não é definida explicitamente. Em vez disso, uma classe é criada implicitamente pelo MEF com propriedades baseadas nos nomes dos metadados fornecidos. (Esta é uma das várias maneiras de acessar metadados no MEF.)

A composição no MEF é recursiva. Você compôs explicitamente o Program objeto, que importou um ICalculator que acabou sendo do tipo MySimpleCalculator. MySimpleCalculator, por sua vez, importa uma coleção de IOperation objetos e essa importação será preenchida quando MySimpleCalculator for criada, ao mesmo tempo que as importações de Program. Se a Add classe declarasse uma importação adicional, isso também teria que ser preenchido e assim por diante. Qualquer importação não preenchida resulta em um erro de composição. (No entanto, é possível declarar importações como opcionais ou atribuí-las a valores padrão.)

Lógica da calculadora

Com essas partes no lugar, tudo o que resta é a lógica da calculadora em si. Adicione o seguinte código na MySimpleCalculator classe para implementar o Calculate método:

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!";
}

As etapas iniciais analisam a cadeia de caracteres de entrada e a dividem em operandos à esquerda e à direita e em um caractere de operador. Dentro do foreach loop, cada membro da operations coleção é examinado. Esses objetos são do tipo Lazy<T,TMetadata>, e seus valores de metadados e objeto exportado podem ser acessados com a Metadata propriedade e a Value propriedade, respectivamente. Nesse caso, se a Symbol propriedade do IOperationData objeto for descoberta como uma correspondência, a calculadora chamará o Operate método do IOperation objeto e retornará o resultado.

Para concluir a calculadora, você também precisa de um método auxiliar que retorne a posição do primeiro caractere sem dígito em uma cadeia de caracteres. Adicione o seguinte método auxiliar à 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;
}

Agora você deve ser capaz de compilar e executar o projeto. No Visual Basic, verifique se você adicionou a Public palavra-chave a Module1. Na janela do console, digite uma operação de adição, como "5+3", e a calculadora retorna os resultados. Qualquer outro operador resulta na mensagem "Operação não encontrada!".

Estender SimpleCalculator usando uma nova classe

Agora que a calculadora funciona, adicionar uma nova operação é fácil. Adicione a seguinte classe ao módulo ou ao namespace SimpleCalculator:

<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;
    }
}

Compile e execute o projeto. Digite uma operação de subtração, como "5-3". A calculadora agora dá suporte à subtração, bem como à adição.

Estender a SimpleCalculator usando um novo assembly

Adicionar classes ao código-fonte é bastante simples, mas a MEF oferece a capacidade de procurar partes fora da fonte de um aplicativo. Para demonstrar isso, você precisará modificar a SimpleCalculator para pesquisar um diretório, bem como seu próprio assembly, por peças, adicionando um DirectoryCatalog.

Adicione um novo diretório chamado Extensions ao projeto SimpleCalculator. Adicione-o no nível do projeto e não no nível da solução. Em seguida, adicione um novo projeto da Biblioteca de Classes à solução, denominada ExtendedOperations. O novo projeto será compilado em um assembly separado.

Abra o Designer de Propriedades do Projeto para o projeto ExtendedOperations e clique na guia Compilar ou Criar . Altere o caminho de saída de build ou o caminho de saída para apontar para o diretório Extensões no diretório do projeto SimpleCalculator (.. \SimpleCalculator\Extensions\).

Em Module1.vb ou Program.cs, adicione a seguinte linha ao Program construtor:

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

Substitua o caminho de exemplo pelo caminho para o diretório de Extensions. (Este caminho absoluto é apenas para fins de depuração. Em um aplicativo em produção, você usaria um caminho relativo.) O DirectoryCatalog agora irá adicionar quaisquer partes encontradas em qualquer assembly no diretório Extensões ao contêiner de composição.

ExtendedOperations No projeto, adicione referências a SimpleCalculator e System.ComponentModel.Composition. No arquivo de classe ExtendedOperations, adicione uma diretiva Imports ou using para System.ComponentModel.Composition. No Visual Basic, adicione também uma instrução Imports para SimpleCalculator. Em seguida, adicione a seguinte classe ao ExtendedOperations arquivo 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;
    }
}

Observe que, para que o contrato corresponda, o ExportAttribute atributo deve ter o mesmo tipo que o ImportAttribute.

Compile e execute o projeto. Teste o novo operador Mod (%).

Conclusão

Este tópico abordou os conceitos básicos do MEF.

  • Peças, catálogos e o contêiner de composição

    Partes e o contêiner de composição são os blocos de construção básicos de um aplicativo MEF. Uma parte é qualquer objeto que importa ou exporta um valor, inclusive a si mesma. Um catálogo fornece uma coleção de partes de uma fonte específica. O contêiner de composição usa as peças fornecidas por um catálogo para executar a composição, a associação de importações a exportações.

  • Importações e exportações

    Importações e exportações são a maneira pela qual os componentes se comunicam. Com uma importação, o componente especifica a necessidade de um valor ou objeto específico e, com uma exportação, especifica a disponibilidade de um valor. Cada importação é compatível com uma lista dos exportações por meio do seu contrato.

Próximas etapas

Para baixar o código completo deste exemplo, consulte o exemplo simplecalculator (Visual Basic).

Para obter mais informações e exemplos de código, consulte a Estrutura de Extensibilidade Gerenciada. Para obter uma lista dos tipos MEF, consulte o System.ComponentModel.Composition namespace.