DevExpress XPO object を Json Serialise する術
Entity Framework にて POCO で書いている場合、Json シリアライズはあまりにも簡単であり、POCOの威力を見ることになるが、DevExpress XPO の XPBaseObject は、そのままではSerialise できない。とはいえ、これを実現するには、さほどの手間はかからない。POCOに比べると、コードが少々汚れることはやむを得ず。
XPOのシリアライズ(その逆)には以下の処理を施さねばならない。
[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);
...
}
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は本当に良く練られている。
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 等を使うがよろしいかと思う。
XPOのシリアライズ(その逆)には以下の処理を施さねばならない。
- Deserialize、すなわちXPBaseObject の生成の際に、Session を渡したい場合(多くの場合そうであろう)、引数付きの constructor に Session (UnitOfWork) を渡さねばならない。ちなみに、この処理を実施しないと、XpoDefault.Session が使用される。
- 他クラスの object をReferenceしている property の処理。
- 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 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 等を使うがよろしいかと思う。
コメント
コメントを投稿