かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Managed Extensibility Framework入門 その9「メタデータ」

かなり間が空いてしまいましたがMEFです!MEFとはなんぞやというところから色々やって、ついに遅延初期化まで完了しました。次は、メタデータというものをやってみようと思います。

メタデータって?

メタデータとはなんでしょう?という疑問なのですが、Wikipediaさんによると以下のようなもののことみたいです。

メタデータ(metadata)、メタ情報とは、メタなデータ、すなわちデータについてのデータという意味で、あるデータが付随して持つそのデータ自身についての抽象度の高い付加的なデータを指す。

正直イマイチわかりません。この説明の中で一番的を得てると思うのは"データについてのデータ"という所でしょうか。MEFでExport属性とかを使って、このクラスはこの型でExportしますという情報を付けていましたが、これを拡張して追加で情報をつけれるという機能がMEFにあります。このような機能をメタデータと呼んでます。

メタデータのつけ方

では、メタデータの使い方を見てみようと思います。メタデータを付けるには、名前の通りExportMetadataAttributeという属性を使います。使い方は簡単で、Exportしてるものにつけるだけです。メタデータには名前と値を渡す形で指定出来ます。小さなサンプルプログラムを書いてみます。

// Labelという名前であいさつマシーンというデータをつけてる
[Export]
[ExportMetadata("Label", "あいさつマシーン")]
public class Greeter
{
    public void Execute()
    {
        Console.WriteLine("こんにちは");
    }
}

コメントにも書いてますが、ExportMetadataでメタデータを追加しています。簡単ですね。さて、C#などの.NET Frameworkの言語でもAttributeを使ってメタデータをプロパティやクラスにつけることが出来ますが、これらの情報は活用するプログラムが無いと何の効果も発揮しません。ということで、MEFのメタデータも同じでメタデータをつけるだけでは何の効果もありません。プログラムから読み取れなければただのゴミです。

メタデータの読み方

ということで、読み取り方です。読み取るにはインターフェースを定義します。先ほどLabelという名前のメタデータを作成したのでLabelという名前の読み取り専用プロパティを持ったインターフェースの定義を行います。

// publicじゃないとダメ
public interface IGreeterMetadataView
{
    // メタデータと同じ名前、型の読み取り専用プロパティを定義する
    string Label { get; }
}

メタデータを読み取るためのインターフェースを定義したので、実際に読み取ってみます。読み取るコードは以下のようになります。

// 現在のアセンブリからコンテナを作る
var c = new CompositionContainer(
    new AssemblyCatalog(Assembly.GetExecutingAssembly()));

// GetExportで取得する型とメタデータの型を定義する
Lazy<Greeter, IGreeterMetadataView> g = c.GetExport<Greeter, IGreeterMetadataView>();
            
// LazyのMetadataプロパティでメタデータを取得できる
Console.WriteLine(g.Metadata.Label);

// もちろんGreeterの処理も実行できる
g.Value.Execute();

ポイントは、今までGetExportedValueメソッドを使っていた所を、GetExportを使ってる所です。こいつは前に遅延初期化で使ったLazy型を返してくれます。前と違うのは型パラメータの2つ目にメタデータを読み取るためのインターフェースを指定するところです。ここでは、先ほど定義したIGreeterMetadataViewがそれにあたります。

型パラメータにメタデータの型を指定したLazy型にはMetadataというプロパティがあり、ここからLabelプロパティにアクセスすることでメタデータに指定した値が取得されます。ということで、上記のプログラムを実行すると以下のような結果になります。

あいさつマシーン
こんにちは

一行目がメタデータに指定した値で、二行目がGreeterで出力している文字列になります。

メタデータもタイプセーフに指定したい

ということで、メタデータの指定方法や取得方法もわかったので、あとはアイデア次第!!ということでもOKなのですがメタデータの読み取りはタイプセーフなのに指定の仕方がタイプセーフじゃないじゃん??という疑問が残ります。これもちゃんとタイプセーフに指定することが出来ます。簡単なのはExportAttribute継承して拡張してしまう方法です。

// Greeter専用Export属性(汎用性のかけらもないけど例のために・・・)
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
// IGreeterMetadataViewを実装しておくとプロパティ名の間違いとかいうくだらないミスが防げてOK
public class GreetreExportAttribute : ExportAttribute, IGreeterMetadataView
{
    // コンストラクタでLabelを指定する、Exportする型はとりあえずGreeterで固定
    public GreetreExportAttribute(string label) : base(typeof(Greeter))
    {
        this.Label = label;
    }

    public string Label { get; private set; }
}

ポイントはMetadataAttributeをつけることと、IGreeterMetadataViewを実装しているところです。こんな風に属性を定義すると、先ほどのGreeterクラスは以下のようにタイプセーフな感じに出来ます。

// さくっとメタデータを定義しつつExport
[GreetreExport("挨拶ましーん")]
public class Greeter
{
    public void Execute()
    {
        Console.WriteLine("こんにちは");
    }
}

使う側は、同じコードですね。

// コンテナ作って
var c = new CompositionContainer(
    new AssemblyCatalog(Assembly.GetExecutingAssembly()));

// メタデータ指定してコンテナから取得して出力
var g = c.GetExport<Greeter, IGreeterMetadataView>();
Console.WriteLine(g.Metadata.Label);
g.Value.Execute();

実行すると以下のような結果になります。

挨拶ましーん
こんにちは

もうちょっとうまく使いたい

さて、超基本的なことは上記ですべてなのですが、じゃぁどういう風に使えるだろうということで、もうちょっとだけ複雑なプログラムを組んでみようと思います。まぁプログラムの題材はただの挨拶プログラムなのですがIGreeterというインターフェースを実装しているクラスを全てかきあつめて、全員に挨拶をさせてまわるプログラムです。その時、どんな人なのかという情報をメタデータで設定しておいて、挨拶の前に表示するって寸法です。

まず、IGreeterインターフェースとメタデータのインターフェースの定義からです。さくっとね。

// 挨拶インターフェース
interface IGreeter
{
    void Execute();
}

// メタデータのインターフェース
public interface IGreeterMetadataView
{
    string Label { get; }
}

そしたら、IGreeterとしてExportしつつIGreeterMetadataViewで定義したメタデータを指定する属性を作成します。

// IGreeterでExportして追加でラベルというメタデータを指定するカスタム属性
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
class GreetreExportAttribute : ExportAttribute, IGreeterMetadataView
{
    public GreetreExportAttribute(string label)
        : base(typeof(IGreeter))
    {
        this.Label = label;
    }

    public string Label { get; private set; }
}

ここまで準備が出来たので、IGreeterの実装クラスを適当に3つくらい作りました。

// 適当に3つほどIGreeterを実装してGreeterExport属性をつけておく
[GreetreExport("日本人")]
class JapaneseGreeter : IGreeter
{
    public void Execute()
    {
        Console.WriteLine("こんにちは");
    }
}

[GreetreExport("割と普通")]
class NormalianGreeter : IGreeter
{
    public void Execute()
    {
        Console.WriteLine("エロース、エロース");
    }
}

[GreetreExport("アメリカ人")]
class EnglishGreeter : IGreeter
{
    public void Execute()
    {
        Console.WriteLine("Hello.");
    }
}

そして、こいつらをImportするClientという名前のクラスを定義します。ポイントは複数ImportするのでIEnumerableで型パラメータをLazyにしているところです。

[Export]
class Client
{
    // 複数受け取る
    [ImportMany]
    public IEnumerable<Lazy<IGreeter, IGreeterMetadataView>> Greeters { get; set; }

    public void Run()
    {
        foreach (var g in this.Greeters)
        {
            // メタデータを表示してから挨拶をしてもらう
            Console.Write(g.Metadata.Label + " : ");
            g.Value.Execute();
        }
    }
}

そして、Mainでは上記のClientクラスを取得してRunメソッドを呼んでいます。実行結果は以下のようになります。

日本人 : こんにちは
割と普通 : エロース、エロース
アメリカ人 : Hello.

以上!メタデータでした。

サンプルプロジェクトの入手

コチラからダウンロードできます。