C# - XML - CDATAセクション

## はじめに

C# 特徴のひとつが、XML 使い勝手の良さです。
使い勝手の良さから、パッケージソフト設定情報などで、XML を多用してきました。

XML 活躍の場所として、公的機関の電子申請（e-Gov, e-Tex, eLTax、マイナポータル）申請データなどがあります。
電子公文書は、XML + XSL で、帳票フォームに基づいた表示/印刷を可能としています。

公的機関の電子申請に携わったことがあり、そこで CDATAセクションに初めて出会いました。
参考情報を踏襲した情報となりますが、CDATAセクションについて記載します。

### 参考情報
下記情報を参考にさせて頂きました。

- [XmlSerializer と CDATA セクション ](http://csharpvbcomparer.blogspot.com/2015/06/net-xmlserializer-and-cdata-section.html)
- [【C#】CDATAセクションを付けたり付けなかったりする](https://sweets-junkie.hatenablog.com/entry/2014/02/28/005929)

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

## CDATAセクション

### 目的
XMLでは、いくつかの特殊文字は、エスケープ記述（実体参照）が必要となります。  
`<` は `&lt;`  
`>` は `&gt;`  
`&` は `&amp;`  
`"` は `&quot;` 
`'` は `&apos;` 

`"`、`'` は、要素の内容として記述可能ですが、属性の値として記述する場合には、エスケープ記述が必要となります。

XMLを直接テキストエディタで確認するケースなどで、このようなエスケープ記述が存在すると可読性が低くなってしまうため、これらの文字をエスケープ記述とせず、そのままの文字で扱う手法が CDATAセクションです。 

CDATAセクションは、`<![CDATA[` という文字列で開始し、`]]>` という文字列で終了します。
CDATAセクション内には、基本的に XML で利用可能な文字を全て記述することが可能です。
唯一の例外は `]]>` という文字列だけで、この文字列はそのまま記述することできず、エスケープ記述が必要となります。

```xml
<Foo><![CDATA[直接 < > を記述可能]]></Foo>
```

::: note
パッケージソフト設定情報として XML を利用する際は、設定ツールを用意するので、テキストエディタでの可視性は重要視せず、CDATAセクションを自発的に利用することはありませんでした。
:::

### 形態
CDATAセクション利用については、下記２形態があります。

- 空の場合も CDATA を付与する
- 空の場合は CDATA を付与しない

::: note
双方のパターンに出会ったことがあります。
:::

### C# 実装

CDataSection は、[XmlSerializer と CDATA セクション ](http://csharpvbcomparer.blogspot.com/2015/06/net-xmlserializer-and-cdata-section.html) 記載内容を、そのまま利用させて頂いて、CDataSectionEmptyExclusion を、CDataSection を継承した派生クラスとして定義します。
.NET Framework と .NET の差異は、Null許容参照型の明示なので、.NET Framework ベースのコードのみを記載します。

```CSharp
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
```

```CSharp
// 空の場合も CDATA を付与する
public class CDataSection : IXmlSerializable
{
  public CDataSection() { }
  public CDataSection(string text) { Text = text; }
  public string Text { get; set; }

  public virtual XmlSchema GetSchema()
  {
    return null;
  }
  public virtual void ReadXml(XmlReader reader)
  {
    Text = reader.ReadElementString();
  }
  public virtual void WriteXml(XmlWriter writer)
  {
    writer.WriteCData(Text);
  }
}

// 空の場合は CDATA を付与しない
public class CDataSectionEmptyExclusion : CDataSection
{
  public CDataSectionEmptyExclusion() : base() { }
  public CDataSectionEmptyExclusion(string text) : base (text) { }

  public override void WriteXml(XmlWriter writer)
  {
    if (string.IsNullOrEmpty(Text))
    {
        writer.WriteWhitespace("");
    }
    else
    {
        writer.WriteCData(Text);
    }
  }
}
```


## サンプルコード

サンプルデータ、C# サンプルコードを以下に記載します。
.NET Framework と .NET の差異は、Null許容参照型の明示なので、.NET Framework ベースのサンプルコードのみを記載します。

### XML

```xml:Hoge.xml
<?xml version="1.0" encoding="utf-8"?>
<XmlDataRoot>
  <Foo><![CDATA[直接 < > を記述可能]]></Foo>
  <Bar><![CDATA[直接 < > を記述可能]]></Bar>
  <Baz>直接 &lt; &gt; を記述可能</Baz>
</XmlDataRoot>
```

```xml:Hoge-Empty.xml
<?xml version="1.0" encoding="utf-8"?>
<XmlDataRoot>
  <Foo><![CDATA[]]></Foo>
  <Bar></Bar>
  <Baz />
</XmlDataRoot>
```

```xml:Hoge-Null.xml
<?xml version="1.0" encoding="utf-8"?>
<XmlDataRoot />
```

### クラス定義

https://learn.microsoft.com/ja-jp/dotnet/standard/serialization/attributes-that-control-xml-serialization

```CSharp
[Serializable]
[XmlRoot]
public class XmlDataRoot
{
  [XmlElement]
  public CDataSection Foo { get; set; }
  [XmlElement]
  public CDataSectionEmptyExclusion Bar { get; set; }
  [XmlElement]
  public string Baz { get; set; }
}
```

### ファイル入出力

https://learn.microsoft.com/ja-jp/dotnet/api/system.xml.serialization.xmlserializer

```CSharp
// オブジェクト → XML
private bool SaveToFile(string filePath, object obj, Type type)
{
  XmlSerializer serializer = new XmlSerializer(type);

  // XML出力形式指定
  var settings = new XmlWriterSettings();
  settings.Encoding = Encoding.GetEncoding("utf-8");
  settings.Indent = true;

  // namespace出力抑止
  var xmlnsEmpty = new XmlSerializerNamespaces();
  xmlnsEmpty.Add(String.Empty, String.Empty);

  // シリアライズ
  using (var writer = XmlWriter.Create(filePath, settings))
  {
    serializer.Serialize(writer, obj, xmlnsEmpty);
  }
  return true;
}

// XML → オブジェクト
private T LoadFromFile<T>(string filePath) where T : class
{
  T obj = null;
  XmlSerializer serializer = new XmlSerializer(typeof(T));

  // ファイル確認
  if (File.Exists(filePath))
  {
    // デシリアライズ
    using (var fs = new FileStream(filePath, FileMode.Open))
    {
      obj = serializer.Deserialize(fs) as T;
    }
  }
  return obj;
}
```

### シリアライズ

```CSharp
// データ有：引数ありコンストラクタ利用
var obj = new XmlDataRoot();
obj.Foo = new CDataSection("直接 < > を記述可能");
obj.Bar = new CDataSectionEmptyExclusion("直接 < > を記述可能");
obj.Baz = "直接 < > を記述可能";
SaveToFile("Hoge.xml", obj, typeof(XmlDataRoot));

// データ空：引数なしコンストラクタ利用
obj = new XmlDataRoot();
obj.Foo = new CDataSection();
obj.Bar = new CDataSectionEmptyExclusion();
obj.Foo.Text = string.Empty;
obj.Bar.Text = string.Empty;
obj.Baz = string.Empty;
SaveToFile("Hoge-Empty.xml", obj, typeof(XmlDataRoot));

// 該当要素なし
obj = new XmlDataRoot();
obj.Foo = null;
obj.Bar = null;
obj.Baz = null;
SaveToFile("Hoge-Null.xml", obj, typeof(XmlDataRoot));
```


### デシリアライズ

```CSharp
var obj = LoadFromFile<XmlDataRoot>("Hoge.xml");
string foo = obj?.Foo?.Text;
string bar = obj?.Bar?.Text;
string baz = obj?.Baz;

obj = LoadFromFile<XmlDataRoot>("Hoge-Empty.xml");
// TODO

obj = LoadFromFile<XmlDataRoot>("Hoge-Null.xml");
// TODO
```