Compartir a través de


Managed Extensibility Framework (MEF) - Marco de Extensibilidad Gestionada

En este artículo se proporciona información general sobre Managed Extensibility Framework que se introdujo en .NET Framework 4.

¿Qué es MEF?

Managed Extensibility Framework (MEF) es una biblioteca para crear aplicaciones ligeras y extensibles. Permite a los desarrolladores de aplicaciones detectar y usar extensiones sin requisitos de configuración. También permite a los desarrolladores de extensiones encapsular código fácilmente y evitar dependencias difíciles frágiles. MEF no solo permite reutilizar las extensiones dentro de las aplicaciones, sino también en todas las aplicaciones.

El problema de extensibilidad

Imagina que eres el arquitecto de una aplicación grande que debe proporcionar soporte para la extensibilidad. La aplicación tiene que incluir un gran número de componentes más pequeños y es responsable de crearlos y ejecutarlos.

El enfoque más sencillo para el problema es incluir los componentes como código fuente en la aplicación y llamarlos directamente desde el código. Esto tiene una serie de desventajas obvias. Lo más importante es que no se pueden agregar nuevos componentes sin modificar el código fuente, una restricción que podría ser aceptable en, por ejemplo, una aplicación web, pero no se puede trabajar en una aplicación cliente. Igualmente problemático, es posible que no tenga acceso al código fuente de los componentes, ya que pueden ser desarrollados por terceros y, por el mismo motivo, no puede permitir que accedan a los suyos.

Un enfoque ligeramente más sofisticado sería proporcionar un punto de extensión o una interfaz, para permitir la desacoplamiento entre la aplicación y sus componentes. En este modelo, puede proporcionar una interfaz que un componente pueda implementar y una API para permitir que interactúe con la aplicación. Esto resuelve el problema de requerir acceso al código fuente, pero todavía tiene sus propias dificultades.

Dado que la aplicación carece de capacidad para detectar componentes por sí solos, debe indicarse explícitamente qué componentes están disponibles y deben cargarse. Normalmente, esto se logra registrando explícitamente los componentes disponibles en un archivo de configuración. Esto significa que asegurarse de que los componentes son correctos se convierte en un problema de mantenimiento, especialmente si es el usuario final y no el desarrollador que se espera que realice la actualización.

Además, los componentes no pueden comunicarse entre sí, excepto a través de los canales definidos rígidamente de la propia aplicación. Si el arquitecto de aplicaciones no ha previsto la necesidad de una comunicación determinada, suele ser imposible.

Por último, los desarrolladores de componentes deben aceptar una dependencia difícil en qué ensamblado contiene la interfaz que implementan. Esto dificulta el uso de un componente en más de una aplicación y también puede crear problemas al crear un marco de pruebas para componentes.

Qué proporciona MEF

En lugar de este registro explícito de los componentes disponibles, MEF proporciona una manera de detectarlos implícitamente a través de la composición. Un componente MEF, denominado parte, especifica declarativamente sus dependencias (conocidas como importaciones) y qué funcionalidades (conocidas como exportaciones) pone a disposición. Cuando se crea un elemento, el motor de composición de MEF cubre sus importaciones con lo que está disponible en otros elementos.

Este enfoque resuelve los problemas descritos en la sección anterior. Dado que las partes MEF especifican mediante declaración sus funcionalidades, se pueden detectar en tiempo de ejecución, lo que significa que una aplicación puede usar elementos sin referencias codificadas de forma rígida o archivos de configuración frágiles. MEF permite a las aplicaciones detectar y examinar elementos por sus metadatos, sin crear instancias de ellos ni incluso cargar sus ensamblados. Como resultado, no es necesario especificar cuidadosamente cuándo y cómo se deben cargar las extensiones.

Además de sus exportaciones proporcionadas, una parte puede especificar sus importaciones, que serán rellenadas por otras partes. Esto hace que la comunicación entre partes no solo sea posible, sino fácil, y permite una buena factorización del código. Por ejemplo, los servicios comunes a muchos componentes se pueden factorizar en una parte independiente y modificar o reemplazar fácilmente.

Dado que el modelo MEF no requiere dependencias difíciles en un ensamblado de aplicación determinado, permite reutilizar las extensiones de la aplicación a la aplicación. Esto también facilita el desarrollo de un arnés de prueba, independientemente de la aplicación, para probar los componentes de extensión.

Una aplicación extensible escrita mediante MEF declara una importación que se puede rellenar mediante componentes de extensión y también puede declarar exportaciones para exponer servicios de aplicación a extensiones. Cada componente de extensión declara una exportación y también puede declarar importaciones. De este modo, los propios componentes de extensión son automáticamente extensibles.

Dónde está disponible MEF

MEF es una parte integral de .NET Framework 4 y está disponible siempre que se use .NET Framework. Puede usar MEF en las aplicaciones cliente, tanto si usan Windows Forms, WPF como cualquier otra tecnología, o en aplicaciones de servidor que usan ASP.NET.

MEF y MAF

Las versiones anteriores de .NET Framework introdujeron managed Add-in Framework (MAF), diseñadas para permitir que las aplicaciones aíslen y administren extensiones. El enfoque de MAF es de nivel ligeramente superior a MEF, centrándose en el aislamiento de extensiones y la carga y descarga de ensamblados, mientras que el enfoque de MEF se centra en la detectabilidad, la extensibilidad y la portabilidad. Los dos marcos interoperan sin problemas y una sola aplicación puede aprovechar ambas ventajas.

SimpleCalculator: una aplicación de ejemplo

La manera más sencilla de ver lo que MEF puede hacer es crear una aplicación MEF sencilla. En este ejemplo, creará una calculadora muy sencilla denominada SimpleCalculator. El objetivo de SimpleCalculator es crear una aplicación de consola que acepte comandos aritméticos básicos, con el formato "5+3" o "6-2" y devuelva las respuestas correctas. Con MEF, podrá agregar nuevos operadores sin cambiar el código de la aplicación.

Para descargar el código completo de este ejemplo, consulte el ejemplo simpleCalculator (Visual Basic).

Nota:

El propósito de SimpleCalculator es demostrar los conceptos y la sintaxis de MEF, en lugar de proporcionar necesariamente un escenario realista para su uso. Muchas de las aplicaciones que se beneficiarían más de la potencia de MEF son más complejas que SimpleCalculator. Para obtener ejemplos más extensos, consulte Managed Extensibility Framework en GitHub.

  • Para empezar, en Visual Studio, cree un nuevo proyecto de aplicación de consola y asígnelo el nombre SimpleCalculator.

  • Agregue una referencia al ensamblado System.ComponentModel.Composition, donde se encuentra MEF.

  • Abra Module1.vb o Program.cs y agregue Imports directivas o using para System.ComponentModel.Composition y System.ComponentModel.Composition.Hosting. Estos dos espacios de nombres contienen tipos MEF que necesitará para desarrollar una aplicación extensible.

  • Si usa Visual Basic, agregue la Public palabra clave a la línea que declara el Module1 módulo.

Contenedor de composición y catálogos

El núcleo del modelo de composición MEF es el contenedor de composición, que contiene todas las partes disponibles y realiza la composición. La composición es la correspondencia entre las importaciones y las exportaciones. El tipo de contenedor de composición más común es CompositionContainery lo usará para SimpleCalculator.

Si usa Visual Basic, agregue una clase pública denominada Program en Module1.vb.

Agregue la siguiente línea a la Program clase en Module1.vb o Program.cs:

Dim _container As CompositionContainer
private CompositionContainer _container;

Con el fin de descubrir las partes disponibles, los contenedores de composición hacen uso de un catálogo. Un catálogo es un objeto que pone a disposición los elementos encontrados desde algún origen. MEF proporciona catálogos para detectar elementos de un tipo proporcionado, un ensamblado o un directorio. Los desarrolladores de aplicaciones pueden crear fácilmente catálogos para detectar elementos de otros orígenes, como un servicio web.

Agregue el siguiente constructor a la Program clase :

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

La llamada a ComposeParts le indica al contenedor de composición que componga un conjunto específico de partes, en este caso, la instancia actual de Program. Sin embargo, en este momento no ocurrirá nada, ya que Program no tiene elementos que importar.

Importaciones y exportaciones con atributos

En primer lugar, debes Program importar una calculadora. Esto permite separar los problemas de la interfaz de usuario, como la entrada y salida de la consola que entrarán en Program, desde la lógica de la calculadora.

Agregue el siguiente código a la clase Program:

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

Observe que la declaración del calculator objeto no es inusual, pero que está decorada con el ImportAttribute atributo . Este atributo declara algo que va a ser una importación; es decir, será rellenado por el motor de composición cuando se redacte el objeto.

Cada importación tiene un contrato, que determina con qué exportaciones coincidirá. El contrato puede ser una cadena especificada explícitamente, o puede ser generado automáticamente por MEF a partir de un tipo determinado, en este caso la interfaz ICalculator. Cualquier exportación declarada con un contrato coincidente cumplirá esta importación. Tenga en cuenta que, aunque el tipo del calculator objeto es de hecho ICalculator, esto no es necesario. El contrato es independiente del tipo del objeto de importación. (En este caso, podría dejar fuera el typeof(ICalculator). MEF asume automáticamente que el contrato se basará en el tipo de importación a menos que lo especifique explícitamente).

Agregue esta interfaz muy sencilla al módulo o al espacio de nombres SimpleCalculator.

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

Ahora que ha definido ICalculator, necesita una clase que la implemente. Agregue la siguiente clase al módulo o al espacio de nombres SimpleCalculator:

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

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

}

Esta es la exportación que coincidirá con la importación en Program. Para que la exportación coincida con la importación, la exportación debe tener el mismo contrato. La exportación según un contrato basado en typeof(MySimpleCalculator) produciría una discordancia y la importación no se completaría; el contrato debe coincidir exactamente.

Puesto que el contenedor de composición se rellenará con todas las partes disponibles en este ensamblado, la MySimpleCalculator parte estará disponible. Cuando el constructor para Program realiza la composición en el objeto Program, su importación se llenará con un objeto MySimpleCalculator que se creará para ese propósito.

La capa de interfaz de usuario (Program) no necesita saber nada más. Por lo tanto, puede rellenar el resto de la lógica de la interfaz de usuario en el método Main.

Agregue el código siguiente al 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 simplemente lee una línea de entrada y llama a la función Calculate de ICalculator en el resultado, que lo vuelve a escribir en la consola. Eso es todo el código que necesita en Program. El resto del trabajo se hará en los elementos.

Atributos Imports e ImportMany

Para que SimpleCalculator sea extensible, debe importar una lista de operaciones. Un atributo ImportAttribute ordinario lo completa un atributo ExportAttribute y solo uno. Si hay más de uno disponible, el motor de composición genera un error. Para crear una importación que se puede rellenar con cualquier número de exportaciones, puede usar el ImportManyAttribute atributo .

Agregue la siguiente propiedad de operaciones a la clase MySimpleCalculator:

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

Lazy<T,TMetadata> es un tipo proporcionado por MEF para contener referencias indirectas a las exportaciones. Aquí, además del propio objeto exportado, también se obtienen metadatos de exportación o información que describe el objeto exportado. Cada Lazy<T,TMetadata> contiene un IOperation objeto , que representa una operación real y un IOperationData objeto , que representa sus metadatos.

Agregue las siguientes interfaces simples al módulo o al espacio de nombres 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; }
}

En este caso, los metadatos de cada operación son el símbolo que representa esa operación, como +, -, *, etc. Para que la operación de suma esté disponible, agregue la siguiente clase al módulo o al espacio de nombres SimpleCalculator:

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

El ExportAttribute atributo funciona como lo hizo antes. El ExportMetadataAttribute atributo adjunta metadatos, en forma de par nombre-valor, a esa exportación. Aunque la Add clase implementa IOperation, una clase que implementa IOperationData no se define explícitamente. En su lugar, MEF crea implícitamente una clase con propiedades basadas en los nombres de los metadatos proporcionados. (Esta es una de las varias maneras de acceder a los metadatos en MEF).

La composición en MEF es recursiva. Compuso el objeto Program explícitamente, que importó un ICalculator que resultó ser del tipo MySimpleCalculator. MySimpleCalculator, a su vez, importa una colección de IOperation objetos y esa importación se rellenará cuando MySimpleCalculator se cree, al mismo tiempo que las importaciones de Program. Si la Add clase declaraba una importación adicional, también tendría que rellenarse, etc. Cualquier importación que no se haya rellenado genera un error de composición. (Sin embargo, es posible declarar las importaciones como opcionales o asignarles valores predeterminados).

Lógica de calculadora

Con estas partes en su lugar, todo lo que queda es la propia lógica de la calculadora. Agregue el código siguiente en la MySimpleCalculator clase para implementar el 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!";
}

Los pasos iniciales separan la cadena de entrada en operandos izquierdo y derecho y un carácter de operador. En el foreach bucle, se examinan todos los miembros de la operations colección. Estos objetos son de tipo Lazy<T,TMetadata>y se puede tener acceso a sus valores de metadatos y al objeto exportado con la Metadata propiedad y la Value propiedad respectivamente. En este caso, si se detecta que la Symbol propiedad del IOperationData objeto es una coincidencia, la calculadora llama al Operate método del IOperation objeto y devuelve el resultado.

Para completar la calculadora, también necesita un método auxiliar que devuelva la posición del primer carácter que no es de dígito en una cadena. Agregue el siguiente método auxiliar a la MySimpleCalculator clase :

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

Ahora debería poder compilar y ejecutar el proyecto. En Visual Basic, asegúrese de agregar la Public palabra clave a Module1. En la ventana de la consola, escriba una operación de adición, como "5+3" y la calculadora devuelve los resultados. Cualquier otro operador da como resultado el mensaje "Operation Not Found!" (Operación no encontrada).

Extender SimpleCalculator mediante una nueva clase

Ahora que la calculadora funciona, agregar una nueva operación es fácil. Agregue la siguiente clase al módulo o al espacio de nombres 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 y ejecute el proyecto. Escriba una operación de resta, como "5-3". La calculadora ahora admite la resta, así como la adición.

Extender SimpleCalculator mediante un nuevo ensamblado

Agregar clases al código fuente de la aplicación es lo suficientemente simple, pero MEF proporciona la capacidad de buscar componentes fuera del propio origen de una aplicación. Para demostrar esto, deberá modificar SimpleCalculator para buscar partes en un directorio, así como en su propio ensamblado, agregando un DirectoryCatalog.

Agregue un nuevo directorio denominado Extensions al proyecto SimpleCalculator. Asegúrese de agregarlo en el nivel de proyecto y no en el nivel de solución. A continuación, agregue un nuevo proyecto de biblioteca de clases a la solución, denominado ExtendedOperations. El nuevo proyecto se compilará en un ensamblado independiente.

Abra el Diseñador de propiedades de proyecto para el proyecto ExtendedOperations y haga clic en la pestaña Compilar o Compilar . Cambie la ruta de acceso de salida de compilación o ruta de acceso de salida para que apunte al directorio Extensiones del directorio del proyecto SimpleCalculator (.. \SimpleCalculator\Extensions\).

En Module1.vb o Program.cs, agregue la siguiente línea al Program constructor:

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

Reemplace la ruta de acceso del ejemplo por la ruta de acceso del directorio Extensions. (Esta ruta de acceso absoluta es solo para fines de depuración. En una aplicación de producción, usaría una ruta de acceso relativa). Ahora, el DirectoryCatalog agregará cualquier parte encontrada en cualquier ensamblado del directorio Extensions al contenedor de composición.

En el ExtendedOperations proyecto, agregue referencias a SimpleCalculator y System.ComponentModel.Composition. En el archivo de clase ExtendedOperations, agregue una directiva Imports o using para System.ComponentModel.Composition. En Visual Basic, agregue también una instrucción Imports para SimpleCalculator. A continuación, agregue la siguiente clase al archivo de ExtendedOperations clase:

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

Tenga en cuenta que para que el contrato coincida, el ExportAttribute atributo debe tener el mismo tipo que .ImportAttribute

Compile y ejecute el proyecto. Pruebe el nuevo operador Mod (%).

Conclusión

En este tema se trataron los conceptos básicos de MEF.

  • Elementos, catálogos y el contenedor de composición

    Las partes y el contenedor de composición son los bloques de creación básicos de una aplicación MEF. Una parte es cualquier objeto que importa o exporta un valor, incluido él mismo. Un catálogo proporciona una colección de elementos de un origen determinado. El contenedor de composición utiliza los elementos proporcionados por un catálogo para realizar la composición, el enlace de las importaciones a las exportaciones.

  • Importaciones y exportaciones

    Las importaciones y exportaciones son la forma en que se comunican los componentes. Con una importación, el componente especifica una necesidad de un valor o objeto determinado y, con una exportación, especifica la disponibilidad de un valor. Cada importación coincide con una lista de exportaciones por medio de su contrato.

Pasos siguientes

Para descargar el código completo de este ejemplo, consulte el ejemplo simpleCalculator (Visual Basic).

Para obtener más información y ejemplos de código, consulte Managed Extensibility Framework. Para obtener una lista de los tipos MEF, consulte el System.ComponentModel.Composition espacio de nombres.