Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use circular references in RestKit using $ref and $id

I have an API using the json.net serialization library. It uses $ref and $id fields for circular references. RestKit does not realize that these $ref fields are referring back to another object that has already been serialized.

Is there a way to tell RestKit to use these fields so empty objects are not created?

Here is an example of my json:

{
      "$id":"1",
      "WorkOrder":{
         "$id":"2",
         "Location":{
            "$id":"3",
            "Address":{
               "$id":"4",
               "Guid":"8086990f-13a0-4f93-8a9b-043ff247ae66"
            },
            "WorkOrders":[
               {
                  "$ref":"2"
               }
            ],
            "Guid":"ae58698d-4fcf-4c31-82bf-529077b6d059"
         },
         "Appointments":[
            {
               "$ref":"1"
            }
         ],
         "Guid":"94140fc6-9885-4395-a79d-2b60452f2bf4",
      },
      "Calendar":{
         "$id":"5",
         "OwnerID":"1bbda60d-0bda-4b97-b6e5-24460106bc54",
         "IsActive":true,
         "Appointments":[
            {
               "$ref":"1"
            }
         ],
         "Guid":"e6c91678-290d-4d12-b52f-9f6ad36dd679",
      },
      "Guid":"731f20c6-6ecb-4515-ade3-df47bc929c86",
}
like image 613
Kyle Redfearn Avatar asked Jan 15 '14 00:01

Kyle Redfearn


1 Answers

The answer lies not in RestKit, but in your JSON.Net Serialization. Here are the issues:

  • RestKit has no idea what to do with $ref, $id
  • You have a WCF Service Layer which returns your objects to your API Layer (MVC APIController).
  • Your API Layer wants to serialize the objects as Json to the client.
  • WCF Needs to manage objects as references or a stack overflow will occur and your service will puke just getting the objects over to the API.
  • Api layer does not want $ref $id references as RestKit can't handle that.
  • RestKit CAN handle an empty object as a reference that only contains the primary key of the object. RestKit will do an Upsert with that object, having only the primary key, it will see that the object already exists, and has no other data to update, hence it will handle the reference perfectly.

So, to accomplish this, we need to get the objects from our Service Layer over to our API layer, using WCF References, then create our own json serialization using only the primary key of the object as a reference and not the $id $ref crap that WCF uses.

so, here are the details.

First of all, you are getting your $ref $id because you are calling into a WCF Service who's classes are decorated with a [DataContract(IsReference=true)] attribute. The JSON.Net serializer will read this attribute and create the $id, $ref references IF you have NOT also decorated your classes with a [JsonObject(IsReference=false)]. You need the DataContract IsReference to get your WCF service to serialize your objects over to your ApiController. Your ApiController now wants to serialize the objects up to the client as Json... your objects therefore also need the Json(IsReference=false) to prevent to $id, $ref

So here is how the data model class definition now looks

[JsonObject(IsReference=false)]
[DataContract(IsReference=true)]
public class SomeClassToSerialize

DataContract is from the System.Runtime.Serialization lib and JsonObject is from the NewtonSoft.Json lib

Ok... this is halfway solved. At this point, we will get the objects from the service back to our api using WCF References. And we have told our API NOT to use references, so our API service will now crash with a stack overflow, as it will try to serialize the circular references.

The next step is to create a custom JsonConverter that will render the circular references using only the primary key. In my example below, all my objects inherit from an EntityBase. And the primary key of all these is a Guid called Guid. So, here is the logic...

Keep track of all rendered objects in a HashSet. If the object has not been rendered, then loop through each property and render the object. If the object HAS been rendered, then only render the primary key, (Guid).

public class GuidRefJsonConverter : JsonConverter
    {
        public override bool CanRead { get { return false; } }
        public override bool CanWrite { get { return true; } }

        public override bool CanConvert(Type objectType)
        {
            return typeof(EntityBase).IsAssignableFrom(objectType);
        }

        private HashSet<EntityBase> serializedObjects = new HashSet<EntityBase>();

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            EntityBase eb = (EntityBase)value;

            JObject jo = new JObject();
            jo.Add("Guid", eb.Guid.ToString());

            if (serializedObjects.Add(eb))
            {
                foreach (PropertyInfo prop in value.GetType().GetProperties())
                {
                    if (prop.GetCustomAttribute<JsonIgnoreAttribute>() != null) continue;

                    if (prop.CanRead)
                    {
                        object propVal = prop.GetValue(value);
                        if (propVal != null && prop.Name!="Guid")
                        {
                            jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
                        }
                    }
                }

            }

            jo.WriteTo(writer);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }

lastly, we just need to register our JsonConverter. In the WebApiConfig.Register() method, just register the serializer.

var json = config.Formatters.JsonFormatter;
json.SerializerSettings.Converters=new List<JsonConverter>(){new GuidRefJsonConverter()};

and that does it. The API will now serialize a circular reference with just the primary key, which RestKit will pick up as an empty Upsert, and will map all its pointers correctly.

like image 55
Jason Cragun Avatar answered Sep 30 '22 16:09

Jason Cragun