Managed Extensibility Framework (MEF)

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

O que é MEF?

A Managed Extensibility Framework ou MEF é uma biblioteca para a criação de aplicações leves e extensíveis. Permite que os desenvolvedores de aplicações descubram e utilizem extensões sem necessidade de configuração. Também permite que os desenvolvedores de extensão facilmente encapsulam o código e evitem dependências frágeis e duras. O MEF não só permite que as extensões sejam reutilizadas dentro das aplicações, como também através de aplicações.

O problema da extensibilidade

Imagine que é o arquiteto de uma grande aplicação que deve fornecer apoio à extensibilidade. A sua aplicação tem de incluir um número potencialmente grande de componentes menores, e é responsável pela sua criação e execução.

A abordagem mais simples para o problema é incluir os componentes como código fonte na sua aplicação, e chamá-los diretamente do seu código. Isto tem uma série de inconvenientes óbvios. O mais importante é que não é possível adicionar novos componentes sem modificar o código fonte, uma restrição que pode ser aceitável, por exemplo, numa aplicação Web, mas que não é viável numa aplicação do cliente. Igualmente problemático, pode não ter acesso ao código fonte para os componentes, porque podem ser desenvolvidos por terceiros, e pela mesma razão não pode permitir que acedam ao seu.

Uma abordagem um pouco mais sofisticada seria fornecer um ponto de extensão ou interface, para permitir a dissociação entre a aplicação e os seus componentes. Ao abrigo deste modelo, poderá fornecer uma interface que um componente pode implementar, e uma API para que possa interagir com a sua aplicação. Isto resolve o problema de exigir o acesso ao código fonte, mas ainda tem as suas próprias dificuldades.

Uma vez que a aplicação não tem qualquer capacidade para descobrir componentes por si só, deve ainda ser explicitamente dito quais os componentes disponíveis e deve ser carregado. Isto é normalmente conseguido registando explicitamente os componentes disponíveis num ficheiro de configuração. Isto significa que assegurar que os componentes estão corretos torna-se um problema de manutenção, especialmente se for o utilizador final e não o desenvolvedor que se espera que faça a atualização.

Além disso, os componentes são incapazes de comunicar uns com os outros, exceto através dos canais rígidos definidos da própria aplicação. Se o arquiteto de aplicações não previu a necessidade de uma determinada comunicação, é geralmente impossível.

Finalmente, os desenvolvedores de componentes devem aceitar uma dependência difícil em que conjunto contém a interface que implementam. Isto dificulta a utilização de um componente em mais de uma aplicação, e também pode criar problemas quando se cria um quadro de teste para componentes.

O que o MEF fornece

Em vez deste registo explícito dos componentes disponíveis, a MEF fornece uma forma de os descobrir implicitamente, através da composição. Um componente MEF, chamado de parte, especifica declarativamente tanto as suas dependências ( conhecidas como importações) como as capacidades (conhecidas como exportações) que disponibiliza. Quando uma peça é criada, o motor de composição MEF satisfaz as suas importações com o que está disponível de outras partes.

Esta abordagem resolve os problemas discutidos na secção anterior. Como as partes MEF especificam declarativamente as suas capacidades, são detetáveis no tempo de execução, o que significa que uma aplicação pode utilizar peças sem referências codificadas ou ficheiros de configuração frágeis. A MEF permite que as aplicações descubram e examinem peças através dos seus metadados, sem as instantanear ou mesmo carregar os seus conjuntos. Como resultado, não há necessidade de especificar cuidadosamente quando e como as extensões devem ser carregadas.

Para além das exportações previstas, uma parte pode especificar as suas importações, que serão preenchidas por outras partes. Isto torna a comunicação entre as partes não só possível, mas fácil, e permite uma boa tão boa mente de código. Por exemplo, os serviços comuns a muitos componentes podem ser considerados numa parte separada e facilmente modificados ou substituídos.

Uma vez que o modelo MEF não requer uma dependência difícil de uma determinada assembleia de aplicações, permite que as extensões sejam reutilizadas de aplicação para aplicação. Isto também facilita o desenvolvimento de um arnês de teste, independente da aplicação, para testar componentes de extensão.

Uma aplicação extensível escrita através da utilização do MEF declara uma importação que pode ser preenchida por componentes de extensão, podendo também declarar exportações a fim de expor os serviços de aplicação a extensões. Cada componente de extensão declara uma exportação e pode igualmente declarar importações. Desta forma, os próprios componentes de extensão são automaticamente extensíveis.

Onde o MEF está disponível

A MEF é parte integrante do .NET Framework 4, e está disponível onde quer que o .NET Framework seja utilizado. Pode utilizar o MEF nas aplicações do seu cliente, quer utilizem Windows Forms, WPF ou qualquer outra tecnologia, ou em aplicações de servidores que utilizem ASP.NET.

MEF e MAF

Versões anteriores do .NET Framework introduziram o Quadro de Ad adicionação gerido (MAF), concebido para permitir que as aplicações isolem e geram extensões. O foco do MAF é ligeiramente superior ao MEF, concentrando-se no isolamento extensão e na montagem de cargas e descargas, enquanto o mef se centra na descoberta, extensibilidade e portabilidade. Os dois quadros interoperam sem problemas, e uma única aplicação pode tirar partido de ambos.

SimpleCalculator: Uma aplicação de exemplo

A forma mais simples de ver o que a MEF pode fazer é construir uma aplicação MEF simples. Neste exemplo, constrói-se uma calculadora muito simples chamada SimpleCalculator. O objetivo do SimpleCalculator é criar uma aplicação de consola que aceite comandos aritméticos básicos, na forma "5+3" ou "6-2", e devolve as respostas corretas. Utilizando o MEF, poderá adicionar novos operadores sem alterar o código de aplicação.

Para fazer o download do código completo para este exemplo, consulte a amostra SimpleCalculator (Visual Basic).

Nota

O objetivo do SimpleCalculator é demonstrar os conceitos e sintaxe da MEF, em vez de necessariamente proporcionar um cenário realista para a sua utilização. Muitas das aplicações que mais beneficiariam do poder da MEF são mais complexas do que o SimpleCalculator. Para exemplos mais extensos, consulte o Managed Extensibility Framework na GitHub.

  • Para começar, em Visual Studio, crie um novo projeto de Aplicação de Consola e nomeie-o SimpleCalculator.

  • Adicione uma referência à montagem, onde reside a System.ComponentModel.Composition MEF.

  • Abrir Módulo1.vb ou Programa.cs e adicionar ou adicionar Imports ou using declarações para System.ComponentModel.Composition e System.ComponentModel.Composition.Hosting. Estes dois espaços de nome contêm tipos DEF que necessitará para desenvolver uma aplicação extensível.

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

Recipiente de composição e catálogos

O núcleo do modelo de composição MEF é o recipiente de composição, que contém todas as peças disponíveis e executa a composição. A composição é a correspondente das importações às exportações. O tipo de recipiente de composição mais comum é CompositionContainer, e você vai usá-lo para SimpleCalculator.

Se estiver a utilizar Visual Basic, adicione uma classe pública chamada Program.vb Módulo1.

Adicione a seguinte linha à Program classe no Módulo1.vb ou Programa.cs:

Dim _container As CompositionContainer
private CompositionContainer _container;

Para descobrir as peças disponíveis, os recipientes de composição utilizam um catálogo. Um catálogo é um objeto que disponibiliza peças descobertas de alguma fonte. A MEF disponibiliza catálogos para descobrir peças de um tipo fornecido, um conjunto ou um diretório. Os desenvolvedores de aplicações podem facilmente criar novos catálogos para descobrir peças 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 indica ao ComposeParts recipiente de composição para compor um conjunto específico de peças, neste caso, o caso atual de Program. Neste ponto, porém, nada acontecerá, uma vez Program que não tem importações para preencher.

Importações e Exportações com atributos

Primeiro, importa Program uma calculadora. Isto permite separar as preocupações da interface do utilizador, como a entrada e saída da consola que irão entrar Program, a partir da lógica da calculadora.

Adicione o seguinte código à Program classe:

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

Note que a declaração do calculator objeto não é incomum, mas que está decorada com o ImportAttribute atributo. Este atributo declara algo como uma importação; ou seja, será preenchido pelo motor de composição quando o objeto estiver composto.

Cada importação tem um contrato, que determina com que exportações será acompanhado. O contrato pode ser uma cadeia explicitamente especificada, ou pode ser gerada automaticamente pela MEF a partir de um determinado tipo, neste caso a interface ICalculator. Qualquer exportação declarada com um contrato correspondente cumprirá esta importação. Note que, embora o calculator tipo de objeto seja, de facto ICalculator, isso não é necessário. O contrato é independente do tipo do objeto importador. (Neste caso, pode deixar de fora o typeof(ICalculator). A MEF assumirá automaticamente que o contrato se baseará no tipo de importação, a menos que o especifique explicitamente.)

Adicione esta interface muito simples ao módulo ou SimpleCalculator espaço de nome:

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

Agora que definiu ICalculator, precisa de uma classe que a implemente. Adicione a seguinte classe ao módulo ou SimpleCalculator espaço de nome:

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

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

}

Aqui está a exportação que irá corresponder à importação em Program. Para que a exportação corresponda à importação, a exportação deve ter o mesmo contrato. A exportação ao abrigo de um contrato baseado em typeof(MySimpleCalculator) produziria uma incompatibilidade, e a importação não seria preenchida; o contrato tem de corresponder exatamente.

Uma vez que o recipiente de composição será povoado com todas as peças disponíveis nesta montagem, a MySimpleCalculator peça estará disponível. Quando o construtor realizar Program a composição no objeto, a Program sua importação será preenchida com um MySimpleCalculator objeto, que será criado para o efeito.

A camada de interface do utilizador (Program) não precisa de saber mais nada. Pode, portanto, preencher o resto da lógica da interface do utilizador 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));
    }
}

Este código simplesmente lê uma linha de entrada e chama a Calculate função do ICalculator resultado, que ele escreve de volta para a consola. É todo o código que precisa Program. Todo o resto do trabalho vai acontecer nas peças.

Importações e ImportAgensM Muitos atributos

Para que o SimpleCalculator seja extensível, tem de importar uma lista de operações. Um atributo comum ImportAttribute é preenchido por um e apenas um ExportAttribute. Se houver mais de um, o motor de composição produz um erro. Para criar uma importação que pode ser preenchida por qualquer número de exportações, pode usar o ImportManyAttribute atributo.

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

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

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

Adicione as seguintes interfaces simples ao módulo ou SimpleCalculator espaço de nome:

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

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

<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, sob a forma de um par de valor-nome, a essa exportação. Enquanto a Add classe implementa IOperation, uma classe que implementa IOperationData não está explicitamente definida. Em vez disso, uma classe é implicitamente criada pela MEF com propriedades baseadas nos nomes dos metadados fornecidos. (Esta é uma das várias formas de aceder aos metadados no MEF.)

A composição em MEF é recursiva. Compôs explicitamente o Program objeto, que importou um ICalculator que acabou por ser 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 nova importação, isso também teria de ser preenchido, e assim por diante. Qualquer importação deixada por preencher resulta num erro de composição. (No entanto, é possível declarar as importações como facultativas ou atribuir-lhes valores de incumprimento.)

Lógica de calculadora

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

Os passos iniciais analisam a cadeia de entrada em óperas esquerda e direita e um personagem de operador. foreach No loop, todos os membros da operations coleção são examinados. Estes objetos são de tipo Lazy<T,TMetadata>, e os seus valores de metadados e objeto exportado podem ser acedidos com a Metadata propriedade e a Value propriedade, respectivamente. Neste caso, se a Symbol propriedade do IOperationData objeto for descoberta como uma correspondência, a calculadora chama o Operate método do IOperation objeto e devolve o resultado.

Para completar a calculadora, também precisa de um método de ajuda que devolva a posição do primeiro personagem não dígito numa corda. Adicione o seguinte método de ajuda à 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 deve ser capaz de compilar e executar o projeto. Em Visual Basic, certifique-se de que adicionou a Public palavra-chave a Module1. Na janela da consola, digite uma operação de adição, como "5+3", e a calculadora devolve os resultados. Qualquer outro operador resulta na mensagem "Operação Não Encontrada!".

Estender o 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 SimpleCalculator espaço de nome:

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

Compilar e executar o projeto. Digite uma operação de subtração, como "5-3". A calculadora suporta agora a subtração, bem como a adição.

Estender o SimpleCalculator usando um novo conjunto

Adicionar aulas ao código fonte é bastante simples, mas o MEF fornece a capacidade de olhar para fora da própria fonte de peças de uma aplicação. Para demonstrar isto, terá de modificar o SimpleCalculator para pesquisar um diretório, bem como a sua própria montagem, para peças, adicionando um DirectoryCatalog.

Adicione um novo diretório nomeado Extensions para o projeto SimpleCalculator. Certifique-se de adicioná-lo ao nível do projeto, e não ao nível da solução. Em seguida, adicione um novo projeto de Biblioteca de Classes à solução, denominado ExtendedOperations. O novo projeto será compilado numa assembleia separada.

Abra o Project Properties Designer para o projeto Operações Estendidas e clique no separador Compile ou Build. Mude o percurso de saída de Construção ou o caminho de saída para apontar para o diretório de extensões no diretório do projeto SimpleCalculator (.. \SimpleCalculator\Extensões\).

No Módulo1.vb ou Programa.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 do exemplo pelo caminho para o diretório de extensões. (Este caminho absoluto é apenas para depurar propósitos. Numa aplicação de produção, usaria um caminho relativo.) O DirectoryCatalog testamento irá agora adicionar todas as peças encontradas em quaisquer conjuntos do diretório de extensões ao contentor de composição.

No projeto Operações Estendidas, adicione referências ao SimpleCalculator e system.ComponentModel.Composition. No ficheiro da classe Operações Estendidas, adicione uma Imports ou uma using declaração para System.ComponentModel.Composition. Em Visual Basic, adicione também uma Imports declaração para SimpleCalculator. Em seguida, adicione a seguinte classe ao ficheiro da classe Operações Estendidas:

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

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

Compilar e executar o projeto. Teste o novo operador Mod (%)

Conclusão

Este tema abordou os conceitos básicos do MEF.

  • Peças, catálogos e o recipiente de composição

    As peças e o recipiente de composição são os blocos básicos de construção de uma aplicação MEF. Uma parte é qualquer objeto que importe ou exporta um valor, até mesmo. Um catálogo fornece uma coleção de peças de uma fonte particular. O contentor de composição utiliza as peças fornecidas por um catálogo para efetuar a composição, a ligação das importações às exportações.

  • Importações e exportações

    As importações e as exportações são a forma pela qual os componentes se comunicam. Com uma importação, o componente especifica a necessidade de um determinado valor ou objeto, e com uma exportação especifica a disponibilidade de um valor. Cada importação é acompanhada de uma lista de exportações através do seu contrato.

Passos seguintes

Para fazer o download do código completo para este exemplo, consulte a amostra SimpleCalculator (Visual Basic).

Para mais informações e exemplos de código, consulte Managed Extensibility Framework. Para obter uma lista dos tipos MEF, consulte o espaço de System.ComponentModel.Composition nomes.