简体   繁体   中英

Azure Service-Fabric and DocumentDB message serialization issue

So, - in my DocumentDB I may have the following document:

{
  "id": 1,
  "type": "A",
  "content": {
    "x": 1,
    "y": 2
  }
}

That may be backed by this model:

   public class acontent
    {
        public int x { get; set; }
        public int y { get; set; }
    }

    public class document
    {
        public int id { get; set; }
        public string type { get; set; }
        public object content { get; set; }
    }

    public class documenta : document
    {
        public new acontent content { get; set; }
    }

The idea here is that document is a complex object where content may vary depending on type.

Now, - in my ServiceFabric application I have a stateless microservice that reads from DocumentDB and should return a document type object when called from the ServiceProxy .

The problem in this is that the DocumentQuery from the DocumentDB SDK, uses Json.NET serializer when querying the database, whilst servicefabric uses DataContractSerializer for serializing the service-messages.

So when the content part of document class is being deserialized from the DocumentDB it becomes:

Newtonsoft.Json.Linq.JObject

But when it is serialized back through the returned service-message you get the exception:

Type 'Newtonsoft.Json.Linq.JToken' is a recursive collection data contract which is not supported. Consider modifying the definition of collection 'Newtonsoft.Json.Linq.JToken' to remove references to itself.

To illustrate this issue try the folowing code:

using System;
using System.IO;
using System.Text;
using System.Runtime.Serialization.Json;
using Newtonsoft.Json;

namespace jsoinissue
{
    public class acontent
    {
        public int x { get; set; }
        public int y { get; set; }
    }

    public class document
    {
        public int id { get; set; }
        public string type { get; set; }
        public object content { get; set; }
    }

    public class documenta : document
    {
        public new acontent content { get; set; }
    }

    public class Program
    {
        private const string JSON_A = "{\"id\":1,\"type\":\"A\",\"content\":{\"x\":1,\"y\":2}}";

        private static string SerializeObject<T> (T obj)
        {
            try
            {
                DataContractJsonSerializer js = new DataContractJsonSerializer(typeof(T));
                using (var ms = new MemoryStream())
                {
                    js.WriteObject(ms, obj);
                    ms.Position = 0;
                    using (var sr = new StreamReader(ms))
                        return sr.ReadToEnd();
                }
            }
            catch (Exception e)
            {
                return String.Format("EXCEPTION: {0}",e.Message);
            }
        }

        public static void Main()
        {
            var A = JsonConvert.DeserializeObject<document>(JSON_A);
            var a = SerializeObject<document>(A);//HERE BE TROUBLE
            Console.WriteLine(a);
            Console.ReadKey();
        }
    }
}

How could I best resolve this issue?

Your basic problem is that DataContractJsonSerializer does not support untyped, free-form JSON data. As explained in Working with untyped JSON in a WCF service the System.Json namespace was added to Silverlight for this purpose, but it seems that it never made it into the full .Net class library.

Instead, in your stateless microservice can do a nested serialization where the free-form JSON is represented as an escaped string literal when serializing using the data contract serializer. Thus your classes would look something like this:

[DataContract]
[JsonObject]
public abstract class documentbase
{
    [DataMember]
    [JsonProperty]
    public int id { get; set; }

    [DataMember]
    [JsonProperty]
    public string type { get; set; }

    [IgnoreDataMember]
    [JsonProperty("content")]
    public abstract JToken JsonContent { get; set; }

    [JsonIgnore]
    [DataMember(Name = "content")]
    string DataContractContent
    {
        get
        {
            if (JsonContent == null)
                return null;
            return JsonContent.ToString(Newtonsoft.Json.Formatting.None);
        }
        set
        {
            if (string.IsNullOrEmpty(value))
                JsonContent = null;
            else
                JsonContent = JToken.Parse(value);
        }
    }
}

[DataContract]
[JsonObject]
public class document : documentbase
{
    JToken content;

    public override JToken JsonContent { get { return content; } set { content = value; } }
}

[DataContract]
[JsonObject]
public class document<T> : documentbase where T : class
{
    [IgnoreDataMember]
    [JsonIgnore]
    public T Content { get; set; }

    public override JToken JsonContent
    {
        get
        {
            if (Content == null)
                return null;
            return JToken.FromObject(Content);
        }
        set
        {
            if (value == null || value.Type == JTokenType.Null)
                Content = null;
            else
                Content = value.ToObject<T>();
        }
    }
}

Then the JSON generated by SerializeObject<document>(A) will look like:

{
   "content":"{\"x\":1,\"y\":2}",
   "id":1,
   "type":"A"
}

Then, on the receiving system, you can deserialize to a document using the data contract serializer, then query the deserialized JToken JsonContent with LINQ to JSON . Alternatively, if the receiving system knows to expect a document<acontent> it can deserialize the data contract JSON as such, since document and document<T> have identical data contracts.

Have you looked into changing away from DataContractSerializer to a serializer with better support instead? Here's how you'd plug in a different serializer.

class InitializationCallbackAdapter
{
    public Task OnInitialize()
    {
        this.StateManager.TryAddStateSerializer(new MyStateSerializer());
        return Task.FromResult(true);
    }

    public IReliableStateManager StateManager { get; set; }
}

class MyStatefulService : StatefulService
{
    public MyStatefulService(StatefulServiceContext context)
        : this(context, new InitializationCallbackAdapter())
    {
    }

    public MyStatefulService(StatefulServiceContext context, InitializationCallbackAdapter adapter)
        : base(context, new ReliableStateManager(context, new ReliableStateManagerConfiguration(onInitializeStateSerializersEvent: adapter.OnInitialize)))
    {
        adapter.StateManager = this.StateManager;
    }
}

This could be newtonsoft or whatever. Also I believe that the method is currently marked "Deprecated" however there's no alternative, so if it solves your problem go ahead and use it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM