Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9533

永続化としてのシリアライズ

$
0
0
モデル永続化の課題 アプリケーションの実行時・終了時問わず、モデルの状態を保存しない日はありません。 その際、サーバーアプリケーションなどでは最終的にデータベースに保存するのが定石ですが、デスクトップアプリなどでは簡易的にファイルに保存することがポピュラーだと思います。 ファイルに保存する場合、インスタンスをファイル保存可能な形式(バイナリ, XML, JSON, etc...)にシリアライズすることになりますが、これがなかなか曲者です。 単純なプリミティブ型で表せるものであれば適当な既定のシリアライザを使えばいいですが、アプリケーション内のモデル全般となると簡単ではありません。デフォルトコンストラクタのないクラスや、privateのフィールド、public setterのないプロパティといった、既定のシリアライザでは対応しないものも扱います。特にDDDなんかをやっていると、publicのsetterはほとんど見かけなくなりしばしばこの問題に突き当たる気がします。 そんなとき、シリアライズ用のDTOを作るのは悪手です。DTOとモデルの変換は定型コードの大量生産になり、うんざりすることが目に見えています。リファクタリングでモデルが変われば変換コードでバグもでるでしょう。できればモデルのインスタンスをそのままシリアライズしたい。 今回のシリアライズ対象のクラス 最初にテスト用のクラスを示します。これをシリアライズ・デシリアライズして元の状態を復元できることが目標です。 class Serialized { private string field; public string GetFieldProperty => this.field; public string AutoProperty { get; set; } public string AutoPropertyWithoutPublicSet { get; private set; } public Serialized(string field) { this.field = field; } public void Set(string s) { this.AutoPropertyWithoutPublicSet = s.ToUpper(); } } テストデータとして下記初期化を行ったインスタンスを使います。 var s = new Serialized("FieldValue") { AutoProperty = "AutoPropertyValue" }; s.Set("WithoutSetValue"); DataContract なんとなく、不遇な扱いを受けている印象のあるDataContractですが、こういう需要にぴったりだったりします。MSDocsのシリアル化のガイドラインでも 使用する型のインスタンスを Web サービスで永続化させる、または使用する必要がある場合は、データ コントラクトのシリアル化 をサポートすることを検討してください。 とありますが、実際今回の需要にはぴったりです。 まずテストクラスを次のように書き換えます。 [DataContract] class Serialized { [DataMember] private string field; public string GetFieldProperty => this.field; [DataMember] public string AutoProperty { get; set; } [DataMember] public string AutoPropertyWithoutPublicSet { get; private set; } // 以下略 } シリアライズ化したいデータにのみDataMemberAttributeをつけます。GetFieldPropertyプロパティについては、フィールドのほうを直接シリアライズしているのでスルーします。 続いてDataContractSerializerを使ってシリアライズ var serializer = new DataContractSerializer(typeof(Serialized)); serializer.WriteObject(anystream, t); シリアライズ結果は <Serialized xmlns="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <AutoProperty>AutoPropertyValue</AutoProperty> <AutoPropertyWithoutPublicSet>WITHOUTSETVALUE</AutoPropertyWithoutPublicSet> <field>FieldValue</field> </Serialized> となりAttributeで設定した通りです。コードは省略しますが、デシリアライズもきちんと動作します。 長所 .NET組み込み モデルのバージョンアップにも対応(データ コントラクトのバージョン管理) JSON形式も可(DataContractJsonSerializer) コンストラクタが呼び出されない。DataContextではコンストラクタの代わりにFormatterServices.GetUninitializedObjectを使います。オブジェクトのライフサイクルとして、一度生成され、保存され、復元されるものがコンストラクタを再び呼ばないのはとても自然 短所 モデルの変更が必要。とはいえドメインロジックやインタフェースに影響する変更じゃないので許容範囲か 特定の技術にモデルが依存する。とはいえドメインロジックやインタフェースに影響する変更じゃないので許容範囲か フィールド・プロパティを追加したときにうっかりDataMemberAttributeを付け忘れるとシリアライズされない。当然テストで見つけてください Json.NET その① DataContractでできることはJson.NETでもだいたいできます(それがDataContract不遇の理由だったりして…)。まずクラスを書き換えます。 [JsonObject] class Serialized { [JsonProperty] private string field; [JsonIgnore] public string GetFieldProperty => this.field; [JsonProperty] public string AutoProperty { get; set; } [JsonProperty] public string AutoPropertyWithoutPublicSet { get; private set; } // 以下略 シリアライズ結果は { "field": "FieldValue", "AutoProperty": "AutoPropertyValue", "AutoPropertyWithoutPublicSet": "WITHOUTSETVALUE" } となります。JsonIgnoreAttributeをつける理由は、Json.NETがデフォルトでopt-outな挙動をするから。つまりpublicなプロパティは全部出力します。その挙動を変えたいときにはJsonObjectAttributeにMemberSerialization.OptInを渡しましょう(参考)。コードは省略しますが、デシリアライズもきちんと動作します。 長所 ライブラリとして非常にポピュラー モデルのバージョンアップにも対応(JsonExtensionDataAttribute) 短所 モデルの変更が必要。とはいえドメインロジックやインタフェースに影響する変更じゃないので許容範囲か 特定の技術にモデルが依存する。とはいえドメインロジックやインタフェースに影響する変更じゃないので許容範囲か フィールドを追加したときにうっかりJsonPropertyAttributeを付け忘れるとシリアライズされない。当然テストで見つけてください プロパティを追加したときにうっかりJsonIgnoreAttributeを付け忘れるとシリアライズされてしまう。でもこれはデシリアライズ時に無視されるだけなのであまり悪影響はないかも コンストラクタが呼び出される。すべての引数をdefault値で呼び出します。なのでコンストラクタ内でnullチェックとかしてると例外が出ます。限定的な回避策としては、 コンストラクタの引数名とプロパティ名を同じにしておく。そうすればデシリアライズ時に、コンストラクタ引数としてそのプロパティの値を渡してくれます。 JsonConstructorAttributeをつけたprivateコンストラクタを作っておく。これはpublicにするとモデルのインタフェースが変わってしまうprivateにしておきます(参考)。 Json.NETが内部でインスタンス生成する仕組みをoverride。これはあまり詳しく見てないですが、可能なようです Json.NET その② 要は、インスタンスのデータを保存したいんですから、フィールドを全部シリアライズできればいいんですよ。自動実装プロパティにも、コンパイラが自動生成したフィールドがあるじゃないですか。 ということでテストクラスを書き換えます。 [Serializable] class Serialized // 以下略 SerializableAttributeをつけるだけです。あとはSerializerのほうで var serializer = new Newtonsoft.Json.JsonSerializer { ContractResolver = new DefaultContractResolver { IgnoreSerializableAttribute = false, }, }; IgnoreSerializableAttributeをfalseにします。シリアライズ結果がこちら。 { "field": "FieldValue", "<AutoProperty>k__BackingField": "AutoPropertyValue", "<AutoPropertyWithoutPublicSet>k__BackingField": "WITHOUTSETVALUE" } コンパイラが自動生成したBackingFieldがきっちり出力されています。Reflectionを使うと実行時にこれらのフィールドが取得できちゃうんですね。それを出力すれば万事解決、フィールドをラップしているプロパティとか余計なことを考えなくて済む! ちなみにこの方法はJson.NETのソースコードを読んでいて知った方法で、使用方法を説明するドキュメントは見つけられませんでした。一番近いのはMemberSerializationの"Fields"の説明だと思います。 コードは省略しますが、デシリアライズもきちんと動作します。 長所 モデルの修正量が少ない。SerializableAttributeならそれほど不自然じゃないし特定技術に依存しているわけでもない モデルのフィールド・プロパティが増えてもAttributeとかの追記漏れがない 短所 フィールド名やプロパティ名が変わると、モデルとシリアライズ結果に互換性がなくなる。前2つの方法では、DataMemberAttributeのName引数やJsonPropertyAttributeのPropertyName引数で、シリアライズ時のプロパティ名を変更し、名前変更の影響を吸収できます。今回の方法でも、C# 7.3以降、自動実装プロパティのBackingFieldにAttributeをつけることで回避できますが、そこまでするなら前2つの方法でいいかなという気がします 自動実装プロパティのBackingFieldの命名方法が変わるとモデルとシリアライズ結果に互換性がなくなる。これは完全にコンパイラ依存なので何も保証が効きません 他のシリアライザ その他シリアライザについてちょっと見た限りだとあまりいい方法はなさそうです。 System.Text.Json: privateなフィールドを出力できない? System.Xml.Serialization.XmlSerializer: privateなフィールドを出力できない? なにかわかったら追記します。 その他 他クラスのインスタンスをフィールド・プロパティとして持っている場合、今回のやり方は再帰的に適用されます。 サードパーティのクラスを内部で使っていたらどうすればいいのか。それは諦めます。カプセル化されていて実装が隠蔽されている以上、シリアライズは不可能です。それこそがカプセル化だから。最低限復元可能にするためのデータを自前で保存するとかになると思います まとめ 最初の要件を満たせる範囲だと3つ方法が見つかりましたがどれも一長一短となりました。個人的にはDataContractがトータル悩むことが少なくなりそうだなと印象です。 DDDのリポジトリにファイルを使う場合、トランザクション管理とかどうすればいいのかな…というのが次の悩み(ファイルを使うこと自体が正しいかはありますが…) 参考文献 シリアル化のガイドライン データ コントラクトのバージョン管理 Json.NET Documentation

Viewing all articles
Browse latest Browse all 9533

Trending Articles