rksoftware

Visual Studio とか C# とかが好きです

城東.NET #41 勉強会を開催しました

■ 城東.NET

城東.NET #41 を開催しました。

城東.NET は東京の最近は秋葉原で毎月第 3 水曜日に開催している .NET 系の勉強会です。
発表を中心として、発表でなくとも最近やった事や新しい情報などを参加者で共有している会です。
f:id:rksoftware:20200220195806j:plain

私は Mobile Blazor Bindings というタイトルで話をしました。

最近注目度の非常に高い Blazor でモバイルアプリが作れる Mobile Blazor Bindings の最新アップデートについての話です。

■ 次回予定

来月は 03月18日(水)に開催の予定です。

.NET に関心のある方、是非遊びに来てください。

秋葉原 C# もくもく会 #118 勉強会を開催しました

■ C# もくもく会

C# もくもく会 #118 を開催しました。

C# もくもく会 は東京の秋葉原で毎週木曜日に開催している .NET 系の勉強会です。
もくもく自習を基本とし、分からないことを教えあったり情報共有したりしている会です。 定期開催していますので、お時間のある時に遊びに来ていただければと思います。
ちょっと詰まった時、ネット上で聞くのははずかしいなぁ、という課題のできた時などにも思い出していただけると嬉しいです。
f:id:rksoftware:20200213193938j:plain

■ 今週の成果発表

今週は、もくもくの日でした。

  • Mobile Blazor Binding などしたりしました
  • IoT でうまくネットワークがつながらなくてもやもやしてました
  • GUI 頑張ってます

ちなみにこの勉強会ですが、実は公序良俗に反しなければどのような技術を扱っても大丈夫です。そもそも C# エンジニアが C# だけしか使わないというわけではありませんし。

■初心者歓迎

このもくもく会には、入門者の方も多くご参加いただいています。 突然 C# やらなければならなくなって途方に暮れている方、何となく C# をやってきたけど改めて見直してみたい方なども大歓迎です。
入門セミナー代わりでのご参加も歓迎です。プログラミング入門者の方も是非遊びに来てください。

特に C# で課題をお持ちでなくても是非遊びに来てください。

■ 目指す勉強会スタイル

世界一敷居の低い勉強会を目指しています。

何か聞きたいことがある場合は、聞く相手を決めずに独り言のようにつぶやくと誰かが拾ってくれる

何か共有したい情報を見つけた場合も、聞く相手を決めずに独り言のようにつぶやくと誰かが拾ってくれる

そんなスタイルでやっています。

■ 次回予定

次回は 2020/02/20 に開催予定です。

C# に関心のある方、是非遊びに来てください。

dotnet のテンプレートのバージョンを確認する

dotnet のテンプレートのバージョンを確認する方法は、次の stackoverflow にありました。

方法だけ抜き出すと

dotnet new -u

または

dotnet new --debug:showconfig

前者のほうが行間などあって見やすい気がしますが、私は標準的な日本人らしく一覧性の高いものを好むので後者のほうが好みな気がします。
私の好みなど、どうでもいいかと思いますが。

C# で定義が未知の Json を扱う (.NET Framework / .NET Core) まとめ

以前に書いた一連の記事をまとめた記事です。

C# で定義が未知の Json を扱う (.NET Framework / JObject) - rksoftware
C# で定義が未知の Json を扱う (.NET Framework / ExpandoObject) - rksoftware
C# で定義が未知の Json を扱う (.NET Core / System.Text.Json) - rksoftware
C# で定義が未知の Json を扱う (.NET Core / System.Text.Json / System.Dynamic.ExpandoObject) - rksoftware

■ 【過去】 .NET Framework 時代

Json を扱う際には .NET Framework 時代には Json.NET というライブラリがよく使われていました。このライブラリの Newtonsoft.Json.JsonConvert.DeserializeObject メソッドで未知の Json を扱うことができました。
メソッドの定義は次のようになっています。

public static object? DeserializeObject(string value);

使う際には次のように dynamic 型変数に代入して使っていた方も多いのではないでしょうか?

dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.prop1);  // value1 と出力される
Console.WriteLine(document.prop2);  // value2 と出力される

実際にはこの際、DeserializeObject からの戻りは何だろうと次のコードで検証。

dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(((object)document).GetType().FullName);  // Newtonsoft.Json.Linq.JObject

Newtonsoft.Json.Linq.JObject になっていました。

プロパティを表示する

Newtonsoft.Json.Linq.JObjectIDictionary<string, JToken?> を実装しているので、Key と Value を foreach で回せます。

Newtonsoft.Json.Linq.JObject document = 
    (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(
        @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop1, Value:value1
// key:prop2, Value:value2
// と出力される

プロパティを追加する

IDictionaryAdd メソッドでプロパティを追加できます。

Newtonsoft.Json.Linq.JObject document = 
    (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(
        @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, Newtonsoft.Json.Linq.JToken?>)document).Add("prop3", "value3");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

Console.WriteLine($"key:prop3, Value:{((dynamic)document).prop3}");

// key:prop1, Value:value1
// key:prop2, Value:value2
// key:prop3, Value:value3
// key:prop3, Value:value3
// と出力される

dynamic 型にして次のようにしてもできます。

((dynamic)document).prop3 = "value3";

プロパティを削除する

IDictionaryRemove メソッドでプロパティを削除できます。

Newtonsoft.Json.Linq.JObject document = 
    (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(
        @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, Newtonsoft.Json.Linq.JToken?>)document).Remove("prop1");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop2, Value:value2
// と出力される

IDictionary<string, Newtonsoft.Json.Linq.JToken?> を実装しているので特にキャストしなくても OK です。

document.Remove("prop1");

■ System.Dynamic.ExpandoObject を使ってみる

C# には実は dynamic の中身として使う System.Dynamic.ExpandoObject クラスがいます。ここからははこの型を使ってみましょう。

型の指定、型の確認

dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");
Console.WriteLine(((object)document).GetType().FullName);  // System.Dynamic.ExpandoObject

System.Dynamic.ExpandoObject になりました。

プロパティを表示する

普通に dynamic っぽく。

dynamic document = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.prop1);  // value1 と出力される
Console.WriteLine(document.prop2);  // value2 と出力される

System.Dynamic.ExpandoObjectIDictionary<string, object> を実装しているので、Key と Value を foreach で回せます。

System.Dynamic.ExpandoObject document = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop1, Value:value1
// key:prop2, Value:value2
// と出力される

プロパティを追加する

IDictionaryAdd メソッドでプロパティを追加できます。

System.Dynamic.ExpandoObject document = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, object>)document).Add("prop3", "value3");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

Console.WriteLine($"key:prop3, Value:{((dynamic)document).prop3}");
// key:prop1, Value:value1
// key:prop2, Value:value2
// key:prop3, Value:value3
// key:prop3, Value:value3
// と出力される

dynamic 型にして次のようにしてもできます。

((dynamic)document).prop3 = "value3";

プロパティを削除する

IDictionaryRemove メソッドでプロパティを削除できます。

System.Dynamic.ExpandoObject document = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, object>)document).Remove("prop1");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop2, Value:value2
// と出力される

IDictionary<string, object> を実装しているので特にキャストしなくても OK です。

document.Remove("prop1", out var _);

■ 現行世代の Json API / System.Text.Json

さて、これまでは .NET Framework 時代の話をしてきました。今の時代 (.NET Core 3 以降) は System.Text.Json を使用します。
補足 Json.NET は .NET Core でも使えます。パフォーマンスは System.Text.Json に劣るとのことですが使い勝手としてまだまだ Json.NET にメリットがある場面はありそうです

ここからは System.Text.Json

  • System.Text.Json.JsonDocument.Parse メソッドによる System.Text.Json.JsonDocument で扱う
  • TValue System.Text.Json.JsonSerializer.Deserialize<TValue> メソッドで System.Dynamic.ExpandoObject を扱う

ことで未知の Json を扱ってみましょう。
これまでと同じようなことを System.Text.Json で試していきます。

■ System.Text.Json.JsonDocument

メソッドの定義は次のようになっています。

public static JsonDocument Parse(string json, JsonDocumentOptions options = default)

とりあえず以前の記事のように、dynamic 型変数に代入して使ってみましょう。

dynamic の中身

using dynamic document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(((object)document).GetType().FullName);  // System.Text.Json.JsonDocument

中身の型は当たり前ですが、System.Text.Json.JsonDocument です。

プロパティ

プロパティを取り出そうとしてみましょう

using dynamic document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

// 実行時エラー
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonDocument' does not contain a definition for 'prop1''
Console.WriteLine(document.prop1);
Console.WriteLine(document.prop2);

残念ながら、実行時に例外が発生します。

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonDocument' does not contain a definition for 'prop1''

プロパティは次のようにして取り出せます。

System.Text.Json.JsonDocument document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.RootElement.GetProperty("prop1").GetRawText());  // "value1" と出力される
Console.WriteLine(document.RootElement.GetProperty("prop2").GetRawText());  // "value2" と出力される
Console.WriteLine(document.RootElement.GetProperty("prop1").GetString());   // value1 と出力される
Console.WriteLine(document.RootElement.GetProperty("prop2").GetString());   // value2 と出力される

■ System.Text.Json.JsonSerializer.Deserialize

最初に結論を出しておくと、Json に階層化された構造があると、嬉しい結果になりません。その詳細は後述します。

メソッドの定義は次のようになっています。

public static TValue Deserialize<TValue>(string json, JsonSerializerOptions options = null);

とりあえず以前の記事のように、dynamic 型変数に代入して使ってみましょう。

dynamic の中身

もう敢えて確認する必要もないですが、System.Dynamic.ExpandoObject です。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.GetType().FullName);  // System.Dynamic.ExpandoObject

プロパティを表示する

普通に dynamic っぽく。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.prop1);  // value1 と出力される
Console.WriteLine(document.prop2);  // value2 と出力される

System.Dynamic.ExpandoObjectIDictionary<string, object> を実装しているので、Key と Value を foreach で回せます。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop1, Value:value1
// key:prop2, Value:value2
// と出力される

プロパティを追加する

IDictionaryAdd メソッドでプロパティを追加できます。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, object>)document).Add("prop3", "value3");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

Console.WriteLine($"key:prop3, Value:{((dynamic)document).prop3}");
// key:prop1, Value:value1
// key:prop2, Value:value2
// key:prop3, Value:value3
// key:prop3, Value:value3
// と出力される

dynamic 型にして次のようにしてもできます。

((dynamic)document).prop3 = "value3";

プロパティを削除する

IDictionaryRemove メソッドでプロパティを削除できます。

System.Dynamic.ExpandoObject document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, object>)document).Remove("prop1");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop2, Value:value2
// と出力される

IDictionary<string, object> を実装しているので特にキャストしなくても OK です。

document.Remove("prop1", out var _);

■ System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject> の嬉しくないところ

Json に階層化された構造があると、嬉しい結果になりません。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

// 実行時エラー
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonElement' does not contain a definition for 'p31''
Console.WriteLine(document.prop3.p31);

実行時に例外が発生します。

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonElement' does not contain a definition for 'p31''

階層化構造の子の要素も System.Dynamic.ExpandoObject になってくれると嬉しいのですが、System.Text.Json.JsonElement になっています。もじ p31 プロパティの値が欲しければ次のような感じです。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

Console.WriteLine(document.prop3.GetProperty("p31").GetRawText());  // 31 と出力される);
Console.WriteLine(document.prop3.GetProperty("p31").GetInt32());    // 31 と出力される);

もう少し丁寧に書くと

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

Console.WriteLine(document.prop3.GetProperty("p31").TryGetInt32(out int value) ? value : default); // 31 と出力される);

こんな感じです。

■ やっぱり System.Text.Json でも System.Dynamic.ExpandoObject を使いたい

System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject> は多段に構造化された Json の場合、あまり嬉しくなかったので、雑に JsonDucument で デシリアライズした後に ExpandoObject に変えるコードを書いてみました。

### コンバートするコード

static System.Dynamic.ExpandoObject Parse(string json)
{
    using var document = System.Text.Json.JsonDocument.Parse(json);
    return toExpandoObject(document.RootElement);

    static object propertyValue(System.Text.Json.JsonElement elm) =>
        elm.ValueKind switch
        {
            System.Text.Json.JsonValueKind.Null => null,
            System.Text.Json.JsonValueKind.Number => elm.GetDecimal(),
            System.Text.Json.JsonValueKind.String => elm.GetString(),
            System.Text.Json.JsonValueKind.False => false,
            System.Text.Json.JsonValueKind.True => true,
            System.Text.Json.JsonValueKind.Array => elm.EnumerateArray().Select(m => propertyValue(m)).ToArray(),
            _ => toExpandoObject(elm),
        };

    static System.Dynamic.ExpandoObject toExpandoObject(System.Text.Json.JsonElement elm) =>
        elm.EnumerateObject()
        .Aggregate(
            new System.Dynamic.ExpandoObject(),
            (exo, prop) => { ((IDictionary<string, object>)exo).Add(prop.Name, propertyValue(prop.Value)); return exo; });
}

理屈は、まあ普通の C# コードなので読んでもらえば分かると思います。

使い方

メソッドなので普通に使えます。

dynamic expandoObject = Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": 2, ""prop3"": { ""p31"": 31 , ""p32"": [1, ""2"", { ""p321"": ""value321"", ""p322"": ""value322"" } ] }}");

試しに、プロパティを追加したり削除したりなどして、シリアライズして結果を見てみましょう。

dynamic expandoObject = Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": 2, ""prop3"": { ""p31"": 31 , ""p32"": [1, ""2"", { ""p321"": ""value321"", ""p322"": ""value322"" } ] }}");

// プロパティを追加してみる
expandoObject.prop3.p33 = 33;

// プロパティを削除してみる
((IDictionary<string, object>)expandoObject.prop3).Remove("p31");

// シリアライズしてみる
Console.WriteLine(
    System.Text.Json.JsonSerializer.Serialize(expandoObject)
);
// {"prop1":"value1","prop2":2,"prop3":{"p32":[1,"2",{"p321":"value321","p322":"value322"}],"p33":33}} と出力される

出力結果

{"prop1":"value1","prop2":2,"prop3":{"p32":[1,"2",{"p321":"value321","p322":"value322"}],"p33":33}}

それっぽく動いているようです。

このコンバートするコードは GitHub にもアップしています。 何か面白い活用のアイデアがあったら教えてください。

でもやっぱり

未知の構造の Json を扱う場合は、Json.NET を使ってしまうのが手っ取り早いかもしれません。

Visual Studio の 16.4.5 がリリースされました

Visual Studio のアップデート 16.4.5 がリリースされました。
いつも通り頑張って見ていきましょう。

■ 更新内容

問題の解決

https://docs.microsoft.com/ja-jp/visualstudio/releases/2019/release-notes#16.4.5 より

  • (ソース管理で?) 競合のマージを行う際に、"Take Source" ボタンおよび "Keep Target" ボタン ("ソースの取得"? ボタンおよび "変更の保持"? ボタン)がなくなった問題が対策されました。
  • XAML デザイナーのプレビューが開かれていると? UWP のデバッグをしようとするとクラッシュする問題が対策されました。
  • SQL Server データベース プロジェクトを Azure に publish する際に Azure SQL Database v12 が選択肢に出てこない問題が対策されました。
  • 再起動が必要になるインストールアクションで再起動した後にクラッシュまたはエラーが起こる問題が対策されました。
  • Xamarin Android でアプリを配置するときに、デバイスまたはエミュレーターからアプリを手動でアンインストールする必要があった問題が対策されました。
  • コンパイラが時々誤って C++ のコルーチン内の命令を削除することがある問題が対策されました。

今回のアップデートはついつい何度も踏んでしまいそうな問題が結構あります。素早くアップデートしましょう。

■ 更新方法

Visual Studio の更新はメニューの ツール > ツールと機能を取得 で開くインストーラーから行えます。

C# で定義が未知の Json を扱う (.NET Core / System.Text.Json / System.Dynamic.ExpandoObject)

未知の Json を扱う一連の記事をまとめた記事を書きました。
こちらの記事で一気読みできます。

■ 現行世代の Json API / System.Text.Json

Json を扱う際には .NET Framework 時代には Json.NET というライブラリがよく使われていました。今の時代 (.NET Core 3 以降) は System.Text.Json を使用します。

未知の Json を扱う記事を以前に書きました。 しかし、少し嬉しくないことに多段の構造を持った Json を System.Dynamic.ExpandoObject にデシリアライズしてもあまり嬉しい感じではありませんでした。

■ コンバートするコード

あまり嬉しくなかったので、雑に JsonDucument で デシリアライズした後に ExpandoObject に変えるコードを書いてみました。

static System.Dynamic.ExpandoObject Parse(string json)
{
    using var document = System.Text.Json.JsonDocument.Parse(json);
    return toExpandoObject(document.RootElement);

    static object propertyValue(System.Text.Json.JsonElement elm) =>
        elm.ValueKind switch
        {
            System.Text.Json.JsonValueKind.Null => null,
            System.Text.Json.JsonValueKind.Number => elm.GetDecimal(),
            System.Text.Json.JsonValueKind.String => elm.GetString(),
            System.Text.Json.JsonValueKind.False => false,
            System.Text.Json.JsonValueKind.True => true,
            System.Text.Json.JsonValueKind.Array => elm.EnumerateArray().Select(m => propertyValue(m)).ToArray(),
            _ => toExpandoObject(elm),
        };

    static System.Dynamic.ExpandoObject toExpandoObject(System.Text.Json.JsonElement elm) =>
        elm.EnumerateObject()
        .Aggregate(
            new System.Dynamic.ExpandoObject(),
            (exo, prop) => { ((IDictionary<string, object>)exo).Add(prop.Name, propertyValue(prop.Value)); return exo; });
}

理屈は、まあ普通の C# コードなので読んでもらえば分かると思います。

使い方

メソッドなので普通に使えます。

dynamic expandoObject = Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": 2, ""prop3"": { ""p31"": 31 , ""p32"": [1, ""2"", { ""p321"": ""value321"", ""p322"": ""value322"" } ] }}");

試しに、プロパティを追加したり削除したりなどして、シリアライズして結果を見てみましょう。

dynamic expandoObject = Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": 2, ""prop3"": { ""p31"": 31 , ""p32"": [1, ""2"", { ""p321"": ""value321"", ""p322"": ""value322"" } ] }}");

// プロパティを追加してみる
expandoObject.prop3.p33 = 33;

// プロパティを削除してみる
((IDictionary<string, object>)expandoObject.prop3).Remove("p31");

// シリアライズしてみる
Console.WriteLine(
    System.Text.Json.JsonSerializer.Serialize(expandoObject)
);
// {"prop1":"value1","prop2":2,"prop3":{"p32":[1,"2",{"p321":"value321","p322":"value322"}],"p33":33}} と出力される

出力結果

{"prop1":"value1","prop2":2,"prop3":{"p32":[1,"2",{"p321":"value321","p322":"value322"}],"p33":33}}

それっぽく動いているようです。

C# で定義が未知の Json を扱う (.NET Core / System.Text.Json)

未知の Json を扱う一連の記事をまとめた記事を書きました。
こちらの記事で一気読みできます。

■ 現行世代の Json API / System.Text.Json

Json を扱う際には .NET Framework 時代には Json.NET というライブラリがよく使われていました。今の時代 (.NET Core 3 以降) は System.Text.Json を使用します。 今回は System.Text.Json

  • System.Text.Json.JsonDocument.Parse メソッドによる System.Text.Json.JsonDocument で扱う
  • TValue System.Text.Json.JsonSerializer.Deserialize<TValue> メソッドで System.Dynamic.ExpandoObject を扱う

ことで未知の Json を扱うことができました。

.NET Framework 時代で未知の Json を扱う記事を以前に書きました。 今回は同じようなことを System.Text.Json で試していきます。

■ System.Text.Json.JsonDocument

メソッドの定義は次のようになっています。

public static JsonDocument Parse(string json, JsonDocumentOptions options = default)

とりあえず以前の記事のように、dynamic 型変数に代入して使ってみましょう。

dynamic の中身

using dynamic document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(((object)document).GetType().FullName);  // System.Text.Json.JsonDocument

中身の型は当たり前ですが、System.Text.Json.JsonDocument です。

プロパティ

プロパティを取り出そうとしてみましょう

using dynamic document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

// 実行時エラー
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonDocument' does not contain a definition for 'prop1''
Console.WriteLine(document.prop1);
Console.WriteLine(document.prop2);

残念ながら、実行時に例外が発生します。

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonDocument' does not contain a definition for 'prop1''

プロパティは次のようにして取り出せます。

System.Text.Json.JsonDocument document = System.Text.Json.JsonDocument.Parse(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.RootElement.GetProperty("prop1").GetRawText());  // "value1" と出力される
Console.WriteLine(document.RootElement.GetProperty("prop2").GetRawText());  // "value2" と出力される
Console.WriteLine(document.RootElement.GetProperty("prop1").GetString());   // value1 と出力される
Console.WriteLine(document.RootElement.GetProperty("prop2").GetString());   // value2 と出力される

■ System.Text.Json.JsonSerializer.Deserialize

最初に結論を出しておくと、Json に階層化された構造があると、嬉しい結果になりません。その詳細は後述します。

メソッドの定義は次のようになっています。

public static TValue Deserialize<TValue>(string json, JsonSerializerOptions options = null);

とりあえず以前の記事のように、dynamic 型変数に代入して使ってみましょう。

dynamic の中身

もう敢えて確認する必要もないですが、System.Dynamic.ExpandoObject です。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.GetType().FullName);  // System.Dynamic.ExpandoObject

プロパティを表示する

普通に dynamic っぽく。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

Console.WriteLine(document.prop1);  // value1 と出力される
Console.WriteLine(document.prop2);  // value2 と出力される

System.Dynamic.ExpandoObjectIDictionary<string, object> を実装しているので、Key と Value を foreach で回せます。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop1, Value:value1
// key:prop2, Value:value2
// と出力される

プロパティを追加する

IDictionaryAdd メソッドでプロパティを追加できます。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, object>)document).Add("prop3", "value3");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

Console.WriteLine($"key:prop3, Value:{((dynamic)document).prop3}");
// key:prop1, Value:value1
// key:prop2, Value:value2
// key:prop3, Value:value3
// key:prop3, Value:value3
// と出力される

dynamic 型にして次のようにしてもできます。

((dynamic)document).prop3 = "value3";

プロパティを削除する

IDictionaryRemove メソッドでプロパティを削除できます。

System.Dynamic.ExpandoObject document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""value1"", ""prop2"": ""value2""}");

((IDictionary<string, object>)document).Remove("prop1");

foreach (var m in document)
    Console.WriteLine($"key:{m.Key}, Value:{m.Value}");

// key:prop2, Value:value2
// と出力される

IDictionary<string, object> を実装しているので特にキャストしなくても OK です。

document.Remove("prop1", out var _);

■ System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject> の嬉しくないところ

Json に階層化された構造があると、嬉しい結果になりません。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

// 実行時エラー
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonElement' does not contain a definition for 'p31''
Console.WriteLine(document.prop3.p31);

実行時に例外が発生します。

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''System.Text.Json.JsonElement' does not contain a definition for 'p31''

階層化構造の子の要素も System.Dynamic.ExpandoObject になってくれると嬉しいのですが、System.Text.Json.JsonElement になっています。もじ p31 プロパティの値が欲しければ次のような感じです。

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

Console.WriteLine(document.prop3.GetProperty("p31").GetRawText());  // 31 と出力される);
Console.WriteLine(document.prop3.GetProperty("p31").GetInt32());    // 31 と出力される);

もう少し丁寧に書くと

dynamic document = System.Text.Json.JsonSerializer.Deserialize<System.Dynamic.ExpandoObject>(
    @"{ ""prop1"": ""a"", ""prop2"": 1, ""prop3"": { ""p31"":31 , ""p32"":32 }}");

Console.WriteLine(document.prop3.GetProperty("p31").TryGetInt32(out int value) ? value : default); // 31 と出力される);

こんな感じです。