DevExpress XPO object を Json Serialise する術

Entity Framework にて POCO で書いている場合、Json シリアライズはあまりにも簡単であり、POCOの威力を見ることになるが、DevExpress XPO の XPBaseObject は、そのままではSerialise できない。とはいえ、これを実現するには、さほどの手間はかからない。POCOに比べると、コードが少々汚れることはやむを得ず。

XPOのシリアライズ(その逆)には以下の処理を施さねばならない。
  1. Deserialize、すなわちXPBaseObject の生成の際に、Session を渡したい場合(多くの場合そうであろう)、引数付きの constructor に Session (UnitOfWork) を渡さねばならない。ちなみに、この処理を実施しないと、XpoDefault.Session が使用される。
  2. 他クラスの object をReferenceしている property の処理。
  3. XPOCollection の処理。
まずは、上の処理で作成した作品達を使う側の例:

        [TestMethod()]
        public void SerialiseWf()
        {
            TestHelper.ConnectToDataSource("UK Test");
            ManagedUnitOfWork uow = TestHelper.uow; // 単なるUnitOfWork と読んでください

            Enquiry en = uow.GetObjectByKey<Enquiry>(6915);

            var settings = new JsonSerializerSettings()
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                ObjectCreationHandling = ObjectCreationHandling.Replace,
                PreserveReferencesHandling = PreserveReferencesHandling.All,
                ContractResolver = new JsonWfContractResolver(uow),
            };
            settings.Converters.Add(new JsonXpoCreationConverter(uow));

            string j = JsonConvert.SerializeObject(en.WorkFlow, Formatting.Indented, settings);

            WfWorkFlow wf = (WfWorkFlow)JsonConvert.DeserializeObject<WfWorkFlow>(j, settings);

            XpoSession.CommitChanges(uow);

            Assert.AreNotEqual(wf.OID, en.WorkFlow.OID);
            Assert.AreEqual(wf.Processes.Count, en.WorkFlow.Processes.Count);
            ...
        }


ハイライトのところと、XPOクラス定義側の実装例を以下:

1. 引数付き constructor を持つ CustomCreationConverterの作成 

using Newtonsoft.Json;

    public class JsonXpoCreationConverter : CustomCreationConverter<XPBaseObject>
    {
        public JsonXpoCreationConverter(Session session)
        {
            this.Session = session;
        }

        public override XPBaseObject Create(Type type)
        {
            XPClassInfo info = Session.GetClassInfo(type);
            XPBaseObject xpo = info.CreateNewObject(Session) as XPBaseObject;
            return xpo;
        }

        readonly Session Session = null;
    }

これだけ。Newtonsoftは本当に良く練られている。

2. Reference の処理

Reference 型の properties は、そのままでは Newtonsoft さんは Serialise/Deserialise 出来ないので、参照先の key を使って実施する。

        private WfProcess fTargetProcess;
        public WfProcess TargetProcess
        {
            get { return fTargetProcess; }
            set { SetPropertyValue<WfProcess>("TargetProcess", ref fTargetProcess, value); }
        }

        [NonPersistent]
        public int TargetProcessRefId
        {
            get { return TargetProcess == null ? 0 : TargetProcess.OID; }
            set { TargetProcess = Session.GetObjectByKey<WfProcess>(value); }
        }

DefaultContractResolver を inherit してカスタム・リゾルバーを作成することは後述するが、そこでは Reference 型の properties は無視して処理しない。代わりに、XPO のプロパティー定義とリゾルバー間で約束事を決め、参照先の key を Newtonsoft に処理してもらう、という段取り。上の例では、約束事は [NonPersistent]は通常無視するが、そのプロパティー名が EndWith("RefId") だったらシリアライズの対象とする、というもの。だったら、最初から key を persistent にして、むしろ reference を [NonPersistent] で書いたら?と刺されるかもしれない。一理あるが、過去に書いた膨大なコードが既にこうなっちゃっているし、パフォーマンスにどう影響するのかは知らない、等々あって、ここでは議論は逃げる。

3. XPOCollection の処理
XPOCollection も、そのままでは Serialise/Deserialise は出来ない。仕方がないので、2) と同じような処理を実施する。

        [Aggregated, Association("WorkFlow-Processes", typeof(WfProcess))]
        public XPCollection Processes
        {
            get { return GetCollection("Processes"); }
        }
        [NonPersistent]
        public List<WfProcess> ListProcesses  // for Json serialisation
        {
            get
            {
                return CoreHelpers.XPCollectionToList<WfProcess>(Processes).OrderBy(p => p.SortIndex).ToList();
            }
            set
            {
                Processes.AddRange(value);
            }
        }
こう定義しておき、リゾルバーでは XPCollectionを無視し、約束事に合致した collection のみをSerialiseの対象とする。上の例の約束事は、   [NonPersistent] だが、StartWith("List")で、続く文字列をもつ persistent XPCollection が存在する、という条件である。


ここから、カスタム・リゾルバーの例。

   public class JsonWfContractResolver : DefaultContractResolver
    {
        public JsonWfContractResolver(Session session, params string[] excludes)
        {
            this.Session = session;
            this.Excludes = excludes;
        }

        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);
            props = props.Where(p => !this.Excludes.Contains(p.PropertyName)).ToList();

            List<JsonProperty> list = new List<JsonProperty>();
            XPClassInfo info = Session.GetClassInfo(type);
            var cols = (info.CollectionProperties).Cast<ReflectionPropertyInfo>().ToList();

            foreach (JsonProperty prop in props)
            {
                XPMemberInfo m = info.FindMember(prop.PropertyName);
                //if (m == null || m.IsKey)
                if (m == null)
                    continue;
                if (m.ReferenceType != null)
                    continue;
                if (cols.Any(p => p.Name == prop.PropertyName))
                    continue;
                if (!m.IsPersistent && !cols.Any(p => "List" + p.Name == prop.PropertyName) && !prop.PropertyName.EndsWith("RefId"))
                    continue;

                list.Add(prop);
            }
            return list;
        }

        readonly string[] Excludes;
        readonly Session Session = null;
    }

約束事のところは、上の例では安易に文字列に頼っているが、CustomAttribute 等を使うがよろしいかと思う。


コメント

このブログの人気の投稿

HiddenFor 要注意

SPA を IIS から流す際の ASP 側のルーティング

Jest の テスト・スクリプトをデバッグする術