Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding properties to nested objects during JSON serialization of EF model

We have a situation where we are using JSON to serialize EF models for use in data synchronization. For the synchronization to work properly we need the table name of the models. That's not hard, and we have code that gives us that already. The main issue is transmitting that data in the JSON.

For example let's say we have the following models.

public class Foo
{
    // ...
    public virtual ICollection<Bar> Bars { get; set; }
}

public class Bar
{
    // ...
    public virtual ICollection<FooBar> FooBars { get; set; }
}

public class FooBar
{
    // ...
}

We pull down all the nested items via includes and then serialize it. The issue is this, we need to insert the table name for the entities as metadata into the JSON without adding it to the models themselves.

For example, in the above scenario the JSON would look something like

{
    "__tableName": "Foos",
    // ...
    "Bars": [
        {
           "__tableName": "Bars"
           // ...
           "FooBars": [
               {
                   "__tableName": "FooBars"
                   // ...
               }
            ]
        }
    ]
}

I figured a custom serializer in JSON.Net would be the best way to go, but either I'm not plugging in at the correct spot, or they don't work the way I thought they do.

I attempted to make a custom JsonConverter as that seems to be the default way of handling custom serialization scenarios. However, it only seems to be called on the base object to be serialized, not any of the child objects.

Is there a place I need to plug in to JSON.Net to actually put in this metadata? Almost everything I have found on this topic points to JsonConverter but I'm not sure that actually does what I need in this situation.

One thought I had was loading the object into a JObject in the JsonConverter, then walking the model tree myself and inserting keys as needed, but I was hoping for something a bit more elegant than that.

Thanks.

like image 620
Bradford Dillon Avatar asked Oct 31 '22 17:10

Bradford Dillon


1 Answers

Although a JsonConverter seems like the appropriate choice here, it actually doesn't work that well in practice for this type of problem. The issue is that you are wanting to programmatically apply the converter to a broad set of classes, and those classes can contain other classes which use the same converter. So you will probably get into issues with recursive loops in the converter, which you will need to work around. It can be done, but it might get a little messy.

Fortunately, there is a better alternative for this situation. You can use a custom IContractResolver in combination with an IValueProvider to insert the __tableName property into the JSON for each object that has a table name. The resolver is responsible for checking whether a particular object type has an associated table name, and if so, setting up the extra property for the type. The value provider simply returns the table name when it is asked.

Here is the code you would need:

class TableNameInsertionResolver : DefaultContractResolver
{
    private Dictionary<string, string> tableNames;

    public TableNameInsertionResolver(Dictionary<string, string> tableNames)
    {
        this.tableNames = tableNames;
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);

        // If there is an associated table name for this type, 
        // add a virtual property that will return the name
        string tableName;
        if (tableNames.TryGetValue(type.FullName, out tableName))
        {
            props.Insert(0, new JsonProperty
            {
                DeclaringType = type,
                PropertyType = typeof(string),
                PropertyName = "__tableName",
                ValueProvider = new TableNameValueProvider(tableName),
                Readable = true,
                Writable = false
            });
        }

        return props;
    }

    class TableNameValueProvider : IValueProvider
    {
        private string tableName;

        public TableNameValueProvider(string tableName)
        {
            this.tableName = tableName;
        }

        public object GetValue(object target)
        {
            return tableName;
        }

        public void SetValue(object target, object value)
        {
            throw new NotImplementedException();
        }
    }
}

To plug this into the serialization pipeline, create an instance of JsonSerializerSettings and set the ContractResolver property to an instance of the custom resolver. Then pass the settings to the serializer. And that's it; it should "just work".

Here is a demo:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo
        {
            Id = 1,
            Bars = new List<Bar>
            {
                new Bar
                {
                    Id = 10,
                    FooBars = new List<FooBar>
                    {
                        new FooBar { Id = 100 },
                        new FooBar { Id = 101 }
                    }
                },
                new Bar
                {
                    Id = 11,
                    FooBars = new List<FooBar>
                    {
                        new FooBar { Id = 110 },
                        new FooBar { Id = 111 },
                    }
                }
            }
        };

        // Dictionary mapping class names to table names.
        Dictionary<string, string> tableNames = new Dictionary<string, string>();
        tableNames.Add(typeof(Foo).FullName, "Foos");
        tableNames.Add(typeof(Bar).FullName, "Bars");
        tableNames.Add(typeof(FooBar).FullName, "FooBars");

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ContractResolver = new TableNameInsertionResolver(tableNames);
        settings.Formatting = Formatting.Indented;

        string json = JsonConvert.SerializeObject(foo, settings);
        Console.WriteLine(json);
    }
}

public class Foo
{
    // ...
    public int Id { get; set; }
    public virtual ICollection<Bar> Bars { get; set; }
}

public class Bar
{
    // ...
    public int Id { get; set; }
    public virtual ICollection<FooBar> FooBars { get; set; }
}

public class FooBar
{
    // ...
    public int Id { get; set; }
}

Output:

{
  "__tableName": "Foos",
  "Id": 1,
  "Bars": [
    {
      "__tableName": "Bars",
      "Id": 10,
      "FooBars": [
        {
          "__tableName": "FooBars",
          "Id": 100
        },
        {
          "__tableName": "FooBars",
          "Id": 101
        }
      ]
    },
    {
      "__tableName": "Bars",
      "Id": 11,
      "FooBars": [
        {
          "__tableName": "FooBars",
          "Id": 110
        },
        {
          "__tableName": "FooBars",
          "Id": 111
        }
      ]
    }
  ]
}

Fiddle: https://dotnetfiddle.net/zG5Zmm

like image 115
Brian Rogers Avatar answered Nov 08 '22 20:11

Brian Rogers