XMLドキュメントの部分編集ライブラリ

既存のXMLドキュメントを読み込んで一部分だけ参照・編集を行いたいという要望を受けました。

設定などのデータクラスをXMLシリアライズすることはありますが、一部を編集するなんて機会がなかったので冬休みの暇つぶしに考えてみました。

XElement

System.Xml.Linq名前空間にXElementというクラスがあります。

learn.microsoft.com

<Root>
  <Child1>1</Child1>  
  <Child2>2</Child2>  
  <Child3>3</Child3>  
</Root>  

こんなXMLドキュメントがあって、"Child2"の値を取得する場合は

var xml = XElement.Load(....);
var element = xml.Element("Child2");
Console.WriteLine($"{element.Value}");

こんなんでOKです。
このクラスを使えば要望を満たせますね。

ライブラリ XmlPicker

それでおしまいだと寂しいのでライブラリを作ってみました。

github.com

XElementをメンバに持つXmlPicker.Elementというクラスを派生して編集したいエレメントをプロパティ化して実装します。
プロパティ化していないエレメントは変更されずに保持されます。

プロパティを操作したあと、void Poke(...)で引数に渡したXElementに書き出します。

使い方

サブエレメントを持つ構造
<Root>
  <TestElement>
    <TestSubElement>
      <Child21>Text21</Child21>
      <Child22>Text22</Child22>
    </TestSubElement>
    <Child11>Text11</Child11>
    <Child12>Text12</Child12>
  </TestElement>
</Root>

このうちChild21Child12のみ操作するのでクラスを定義する場合は下記のように定義します。

class TestSubElement : Element {
  public TestSubElement(XElement? parent) : base(parent, "TestSubElement") { }

  public string? Child21 {
    get => GetContent<string>();
    set => SetContent(value);
  }
  // Child22はプロパティがないので何も操作しない
}

class TestElement : Element {
  public TestElement(XElement? parent) : base(parent, "TestElement") { }

  TestSubElement? _SubElement;
  public TestSubElement SubElement => _SubElement ??= new(XElement);

  public string? Child12 {
    get => GetContent<string>();
    set => SetContent(value);
  }
  // Child11はプロパティがないので何も操作しない
}
エレメントがない場合

プロパティを定義したエレメントが無かった場合、プロパティを操作しなければPokeで書き戻してもエレメントを出力しません。もちろん、プロパティを設定した場合は出力されます。

<Root>
  <TestElement>
    <Child2>2</Child2>  
    <Child3>3</Child3>  
  </TestElement>
</Root>
class TestElement : Element {
  public TestElement(XElement? parent) : base(parent, "TestElement") { }

  public string? Child1 {
    get => GetContent<string>();
    set => SetContent(value);
  }
}

var xml = XElement.Load(...);
var element = new TestElement(xml);

// 何も操作しなければ 元々無かった<Child1>は出力されない
element.Poke(xml);

element.Child1 = "foo";

// プロパティを設定したので<Child1>foo</Child1>が出力される
element.Poke(xml);
エレメントのリスト

同じエレメントをサブエレメントにする構造を表現するクラスも用意しました。

<Root>
  <ElementList>
    <Element>
      <Child>0</Child>
    </Element>
    <Element>
      <Child>1</Child>
    </Element>
    <Element>
      <Child>2</Child>
    </Element>
  </ElementList>
</Root>
var xml = XElement.Load(...);
var element = new ElementList<Element>(xml, "Element", "ElementList");

foreach(var e in element) {
  Console.WriteLine(e?.XElement?.Element("Child")?.Value);
}

IListを実装しているのでインデックスで操作できたりもします。

まとめ

なんとなく想定している範囲はテストを用意しているのでそこそこ使えると思いますが、XMLの自由さを考えるとテストケースが漏れてそうだなと感じます。

世界中で2人ぐらいは欲してるかもしれない需要がピンポイントなライブラリですね。欲してる人に届け〜!

あと、Peekに対応して出力のメソッド名をPokeにしたけどなんかイメージに合わないですね。どんな名前にすればよかったんだろう・・?