Managed Extensibility Framework(MEF)入門 -ExportMetadata-

MEFではエクスポートするサービスに『メタデータ(エクスポート属性)』を付ける事が出来ます。これを使用する事によって、同一コントラクトのサービス群を区別する事が出来ます。主にImportMany属性を使用して取り込んだ同一のサービス群やコンテナーより取得した同一コントラクトのサービス群に使用します。メタデータを識別する為に『Lazy<T, TMetadata>・Lazy(Of T, TMetadata)』クラスを使用します。遅延エクスポートで使用する「Lazy<T>・Lazy(Of T)」クラスを継承したクラスであり、遅延エクスポートの恩恵も得ます。サービス群を区別する際にサービスインスタンスを都度生成していては効率が良くなく、サービスの区別後にインスタンス生成を行うのが望ましい事から「Lazy<T>・Lazy(Of T)」クラスを拡張して作成されてます。

 

exportmetadata_details

 

このようにサービスにメタデータとして「属性名(キー)、属性値(値)」を設定する事が出来ます。これには『System.ComponentModel.Composition.ExportMetadataAttribute』クラスを使用し、エクスポートするサービスに属性として定義します。サービスにはメタデータを複数設定する事も出来ます。それではサンプルコードを元に見ていきましょう。

 

基本実装

C#
    1 using System;
    2 using System.Collections.Generic;
    3 using System.ComponentModel;
    4 using System.ComponentModel.Composition;
    5 using System.ComponentModel.Composition.Hosting;
    6 
    7 namespace ExportMetadata.CSharp
    8 {
    9    public class Program
   10    {
   11       private CompositionContainer container;
   12 
   13       private Program()
   14       {
   15          container = new CompositionContainer();
   16          CompositionBatch batch = new CompositionBatch();
   17          batch.AddPart(new MessageSender());
   18          container.Compose(batch);
   19       }
   20 
   21       private void Run()
   22       {
   23          //Metadataは既定ではIDictionary<string, object>で取得出来る。
   24          Lazy<IMessageSender, Dictionary<string, object>> messageSender = this.container.GetExport<IMessageSender, Dictionary<string, object>>();
   25          //Lazy<T, TMetadata>のIMetadataプロパティからエクスポート属性値を取得する。
   26          Console.WriteLine(string.Format("Value={0}", messageSender.Metadata["MetadataName"]));
   27          //メタデータにアクセスしている時はまだインスタンスは生成されていない。
   28          Console.WriteLine(string.Format("instance is created={0}", messageSender.IsValueCreated.ToString()));
   29          Console.WriteLine(messageSender.Value.GetMessage());
   30       }
   31 
   32       public static void Main(string[] args)
   33       {
   34          Program program = new Program();
   35          program.Run();
   36       }
   37    }
   38 
   39    public interface IMessageSender
   40    {
   41       string GetMessage();
   42    }
   43 
   44    //メタデータに属性名="MetadataName"、属性値="MetadataValue"を設定。
   45    [ExportMetadata("MetadataName", "MetadataValue")]
   46    [Export(typeof(IMessageSender))]
   47    public class MessageSender : IMessageSender
   48    {
   49       public string GetMessage()
   50       {
   51          return "メッセージです。";
   52       }
   53    }
   54 }
VB
    1 Imports System
    2 Imports System.ComponentModel.Composition
    3 Imports System.ComponentModel.Composition.Hosting
    4 
    5 Public Class Program
    6    Private container As CompositionContainer
    7 
    8    Private Sub New()
    9       Me.container = New CompositionContainer()
   10       Dim batch As New CompositionBatch()
   11       batch.AddPart(New MessageSender())
   12       Me.container.Compose(batch)
   13    End Sub
   14 
   15    Private Sub Run()
   16       'Metadataは既定ではIDictionary<string, object>で取得出来る。
   17       Dim messageSender As Lazy(Of IMessageSender, IDictionary(Of String, Object)) = Me.container.GetExport(Of IMessageSender, IDictionary(Of String, Object))()
   18       'Lazy(Of T, TMetadata)のIMetadataプロパティからエクスポート属性値を取得する。
   19       Console.WriteLine(String.Format("Value={0}", messageSender.Metadata("MetadataName")))
   20       'メタデータにアクセスしている時はまだインスタンスは生成されていない。
   21       Console.WriteLine(String.Format("instance is created = {0}", messageSender.IsValueCreated.ToString()))
   22       Console.WriteLine(messageSender.Value.GetMessage())
   23    End Sub
   24 
   25    Public Shared Sub Main()
   26       Dim program As New Program()
   27       program.Run()
   28    End Sub
   29 End Class
   30 
   31 Public Interface IMessageSender
   32    Function GetMessage() As String
   33 End Interface
   34 
   35 'メタデータに属性名="MetadataName"、属性値="MetadataValue"を設定。
   36 <ExportMetadata("MetadataName", "MetadataValue")> _
   37 <Export(GetType(IMessageSender))> _
   38 Public Class MessageSender
   39    Implements IMessageSender
   40    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   41       Return "メッセージです。"
   42    End Function
   43 End Class

上記サンプルコードの出力結果は以下の通りになります。

 

exportmetadata_details_output

 

サンプルコードではMessageSenderクラスをサービスとしてエクスポートしており、ExportMetadataAttributeクラスを使用して「属性名=”MetadataName”、属性値=”MetadataValue”」をサービスに付与しています。エクスポートされたサービスを使用する側ではLazy<T, TMetadata>・Lazy(Of T, TMetadata)クラスを使用してサービスを受け取り、『Metadataプロパティ』を通じてエクスポートされたサービスのメタデータを見ています。Lazy<T, TMetadata>・Lazy(Of T, TMetadata)クラスは遅延エクスポートが行えるのでValueプロパティにアクセスするまでサービスのインスタンスが生成されておりません。同一コントラクトのサービス群を区別している時にインスタンスが生成されない事は理にかなっており、区別後の必要なサービスのみインスタンスが作られるわけです。又、ExportMetadataAttributeのコンストラクタ引数に属性名と属性値が設定されていますが、ExportMetadataAttributeクラスのコンストラクタ引数は『属性名=string型、属性値=object型』となっています。よってTMetadataに「IDictionary<string, object>・IDictionary(Of String, Object)」が使用出来るのです。

 

メタデータビューの使用

エクスポートしたサービスにメタデータを設定する方法と取得する方法は上記サンプルで分かりました。しかし上記サンプルコードには幾つかの問題があります。



  1. メタデータの属性名がstring型である為、エクスポート側と使用する側で同一の文字列を管理する必要がある。
  2. メタデータの属性値がobject型である為、使用する側ではエクスポート側で設定された型を意識する必要がある。
  3. コンパイル時の検出が行えない。

ExportMetadataAttributeを使用した場合にメタデータへアクセス用する為にIDictionary<string, object>・IDictionary(Of String, Object)を使用していましたが、上記問題を抱えてしまう為に推奨されていません。これらの問題を『使用する側で解決する』為の機構として『メタデータビュー』と言う物があります。メタデータビューとは以下のように定義されています。



メタデータ ビューは、エクスポートされるメタデータのキーに対応する、読み取り専用のプロパティを定義するインターフェイスです。metadata プロパティにアクセスすると、MEF では TMetadata が動的に実装され、エクスポートから提供されたメタデータを基に値が設定されます。


初めのサンプルコードではIDictionary<string, object>・IDictionary(Of String, Object)を使用していた部分をメタデータビューに変える事が出来ます。「属性名と同じ名前のプロパティを持ったインターフェース」を作成しメタデータビューとして使います。このサンプルコードを見てみましょう。

C#
    1 using System;
    2 using System.Collections.Generic;
    3 using System.ComponentModel;
    4 using System.ComponentModel.Composition;
    5 using System.ComponentModel.Composition.Hosting;
    6 
    7 namespace ExportMetadataUsingIMetadataView.CSharp
    8 {
    9    public class Program
   10    {
   11       private CompositionContainer container;
   12 
   13       private Program()
   14       {
   15          container = new CompositionContainer();
   16          CompositionBatch batch = new CompositionBatch();
   17          batch.AddPart(new MessageSender());
   18          container.Compose(batch);
   19       }
   20 
   21       private void Run()
   22       {
   23          //IMetadataViewインターフェースをメタデータビューとして使用している。
   24          Lazy<IMessageSender, IMetadataView> messageSender = this.container.GetExport<IMessageSender, IMetadataView>();
   25          Console.WriteLine(string.Format("Value={0}", messageSender.Metadata.FirstMetadataName));
   26          Console.WriteLine(string.Format("Value={0}", messageSender.Metadata.SecondMetadataName));
   27          Console.WriteLine(messageSender.Value.GetMessage());
   28       }
   29 
   30       static void Main(string[] args)
   31       {
   32          Program program = new Program();
   33          program.Run();
   34       }
   35    }
   36 
   37    /// <summary>
   38    /// メタデータビュー用インターフェース
   39    /// </summary>
   40    public interface IMetadataView
   41    {
   42       //プロパティ名が属性名に対応している。
   43       ViewTypes FirstMetadataName { get; }
   44       ViewTypes SecondMetadataName { get; }
   45    }
   46 
   47    public interface IMessageSender
   48    {
   49       string GetMessage();
   50    }
   51 
   52    [ExportMetadata(ViewMetadata.FirstMetadataName, ViewTypes.FirstValue)]
   53    [ExportMetadata(ViewMetadata.SecondMetadataName, ViewTypes.SecondValue)]
   54    [Export(typeof(IMessageSender))]
   55    public class MessageSender : IMessageSender
   56    {
   57       public string GetMessage()
   58       {
   59          return "メッセージです。";
   60       }
   61    }
   62 
   63    /// <summary>
   64    /// 属性名識別用クラス
   65    /// </summary>
   66    public class ViewMetadata
   67    {
   68       public const string FirstMetadataName = "FirstMetadataName";
   69       public const string SecondMetadataName = "SecondMetadataName";
   70    }
   71 
   72    /// <summary>
   73    /// 属性値識別用列挙体
   74    /// </summary>
   75    public enum ViewTypes
   76    {
   77       FirstValue,
   78       SecondValue
   79    }
   80 }
VB
    1 Imports System
    2 Imports System.ComponentModel.Composition
    3 Imports System.ComponentModel.Composition.Hosting
    4 
    5 Public Class Program
    6    Private container As CompositionContainer
    7 
    8    Private Sub New()
    9       Me.container = New CompositionContainer()
   10       Dim batch As New CompositionBatch()
   11       batch.AddPart(New MessageSender())
   12       Me.container.Compose(batch)
   13    End Sub
   14 
   15    Private Sub Run()
   16       'IMetadataViewインターフェースをメタデータビューとして使用している。
   17       Dim messageSender As Lazy(Of IMessageSender, IMetadataView) = Me.container.GetExport(Of IMessageSender, IMetadataView)()
   18       Console.WriteLine(String.Format("Value={0}", messageSender.Metadata.FirstMetadataName))
   19       Console.WriteLine(String.Format("Value={0}", messageSender.Metadata.SecondMetadataName))
   20       Console.WriteLine(messageSender.Value.GetMessage())
   21    End Sub
   22 
   23    Public Shared Sub Main()
   24       Dim program As New Program()
   25       program.Run()
   26    End Sub
   27 End Class
   28 
   29 ''' <summary>
   30 ''' メタデータビュー用インターフェース
   31 ''' </summary>
   32 Public Interface IMetadataView
   33    'プロパティ名が属性名に対応している
   34    ReadOnly Property FirstMetadataName As ViewTypes
   35    ReadOnly Property SecondMetadataName As ViewTypes
   36 End Interface
   37 
   38 Public Interface IMessageSender
   39    Function GetMessage() As String
   40 End Interface
   41 
   42 <ExportMetadata(ViewMetadata.FirstMetadataName, ViewTypes.FirstValue)> _
   43 <ExportMetadata(ViewMetadata.SecondMetadataName, ViewTypes.SecondValue)> _
   44 <Export(GetType(IMessageSender))> _
   45 Public Class MessageSender
   46    Implements IMessageSender
   47    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   48       Return "メッセージです。"
   49    End Function
   50 End Class
   51 
   52 ''' <summary>
   53 ''' 属性名識別用クラス
   54 ''' </summary>
   55 Public Class ViewMetadata
   56    Public Const FirstMetadataName As String = "FirstMetadataName"
   57    Public Const SecondMetadataName As String = "SecondMetadataName"
   58 End Class
   59 
   60 ''' <summary>
   61 ''' 属性値識別用列挙体
   62 ''' </summary>
   63 Public Enum ViewTypes
   64    FirstValue
   65    SecondValue
   66 End Enum

これを実行すると以下のように出力されます。

 

exportmetadata_details_output2

 

上記ではExportMetadataAttributeクラスに指定する属性名をリテラル文字ではなく定数を使用して指定しています。これにより同じ値を持った複数の属性名を一度に変更する事が出来ます。又、属性値も列挙体を用いて指定する値を限定させています。メタデータビューとしてIMetadataViewインターフェースを作成しました。このインターフェースを定義する場合には以下のルールがあります。



  • 読み取り専用のプロパティである
  • プロパティ名は属性名と同じ名前である
  • プロパティ型は属性値と同じ型である

サンプルでは上記ルールに沿ったプロパティを2つ実装させています。このルールは必ず守る必要があります。ここでどれか規則を守っていない物が存在すると、サービスの取得時に例外が発生します。ルールに基づいたメタデータビューを作成したあと、Lazy<T, TMetadata>・Lazy(Of T, TMetadata)のTMetadataに指定する事でMetadataプロパティにアクセスした際に動的にMetadataインスタンスを作成してくれます。これは遅延エクスポートと同じ機構です。メタデータビューを使用する事で、使用する側へのタイプセーフな機構を提供する事が出来ます。

メタデータビューを使用する事で幾つかの問題は解消されましたが、未だまだ問題を抱えています。それは以下の通りです。



  • エクスポートのメタデータの属性名を明示的に都度定義する必要がある。

上記問題はExportMetadataAttributeのコンストラクタ引数がstring型の属性名、object型の属性値と限定されている事によって発生しています。これを解決する為にはExportAttributeを継承したカスタムエクスポートクラスを作成する必要があります。このカスタムエクスポートクラスについては別途取り上げたいと思います。

 

Source Code