Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.0k views
in Technique[技术] by (71.8m points)

c# - How can I deserialize instances of a type that has read-only back-references to some container type also being deserialized?

Let's say I have two types, a Document and a Child. The Child is nested fairly deeply within the Document, and contains a back-reference to the parent that must needs be passed into its constructor. How can I deserialize such an object graph with Json.NET and pass the parent into the child's constructor?

Here's a concrete example, inspired by Pass constructor arguments when deserializing into a List(Of T) by Ama:

Class Document
    Public Property MyObjects as List(Of Child) = new List(Of Child)()
End Class

Class Child
    Private ReadOnly _Parent As Document

    Sub New(Parent As Document)
        _Parent = Parent
    End Sub

    Property Foo As String
    Property Bar As String

    Function GetParent() As Document
        Return _Parent
    End Function
End Class

With the corresponding JSON:

{
  "MyObjects": [
    {
      "Foo": "foo",
      "Bar": "bar"
    }
  ]
}

Notes:

  • The parent reference in Child is read-only and must be passed into the constructor.

  • I cannot modify the class definitions for Document and Child.

  • Document and Child are more complicated that shown here, so loading into a JToken hierarchy then constructing manually is not preferred.

How can I deserialize JSON to such a data model, constructing the list of children with the parent properly initialized?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Since the definitions for Document and Child cannot be modified, one way to do this would be with a custom contract resolver that returns contracts that track the current document being deserialized in some ThreadLocal(Of Stack(Of Document)) stack, and allocate instances of MyObject using the topmost document.

The following contract resolver does the job:

Public Class DocumentContractResolver
    Inherits DefaultContractResolver

    Private ActiveDocuments As ThreadLocal(Of Stack(Of Document)) = New ThreadLocal(Of Stack(Of Document))(Function() New Stack(Of Document))

    Protected Overrides Function CreateContract(ByVal objectType As Type) As JsonContract
        Dim contract = MyBase.CreateContract(objectType)
        Me.CustomizeDocumentContract(contract)
        Me.CustomizeMyObjectContract(contract)
        Return contract
    End Function

    Private Sub CustomizeDocumentContract(ByVal contract As JsonContract)
        If GetType(Document).IsAssignableFrom(contract.UnderlyingType) Then
            contract.OnDeserializingCallbacks.Add(Sub(o, c) ActiveDocuments.Value.Push(CType(o, Document)))
            contract.OnDeserializedCallbacks.Add(Sub(o, c) ActiveDocuments.Value.Pop())
        End If
    End Sub

    Private Sub CustomizeMyObjectContract(ByVal contract As JsonContract)
        If (GetType(Child) = contract.UnderlyingType) Then
            contract.DefaultCreator = Function() New Child(ActiveDocuments.Value.Peek())
            contract.DefaultCreatorNonPublic = false
        End If
    End Sub
End Class

And then use it like:

Dim contractResolver = New DocumentContractResolver() ' Cache this statically somewhere
Dim settings = New JsonSerializerSettings() With { .ContractResolver = contractResolver }

Dim doc2 = JsonConvert.DeserializeObject(Of Document)(jsonString, settings)

And in c#:

public class DocumentContractResolver : DefaultContractResolver
{
    ThreadLocal<Stack<Document>> ActiveDocuments = new ThreadLocal<Stack<Document>>(() => new Stack<Document>());

    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        CustomizeDocumentContract(contract);
        CustomizeMyObjectContract(contract);
        return contract;
    }

    void CustomizeDocumentContract(JsonContract contract)
    {
        if (typeof(Document).IsAssignableFrom(contract.UnderlyingType))
        {
            contract.OnDeserializingCallbacks.Add((o, c) => ActiveDocuments.Value.Push((Document)o));
            contract.OnDeserializedCallbacks.Add((o, c) => ActiveDocuments.Value.Pop());
        }
    }

    void CustomizeMyObjectContract(JsonContract contract)
    {
        if (typeof(Child) == contract.UnderlyingType)
        {
            contract.DefaultCreator = () => new Child(ActiveDocuments.Value.Peek());
            contract.DefaultCreatorNonPublic = false;
        }
    }
}

Notes:

  • If an exception occurs during deserialization the ActiveDocuments might not get cleared properly. You might want to add a serialization error handler to do that.

  • As explained in Newtonsoft's performance tips,

    To avoid the overhead of recreating contracts every time you use JsonSerializer you should create the contract resolver once and reuse it.

  • ThreadLocal<T> is disposable, so if you don't plan to cache your WordContractResolver you should probably make it disposable also, and dispose of the threadlocal in the dispose method.

Demo fiddles here (vb.net) and here (c#).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...