Issue during serialization/deserialization of N-1 relations using json.net

Posts   
 
    
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 12-Nov-2015 23:09:53   

using LLBLGen 4.2 / SQL 2012 / Adapter

Hey,

This is actually a "spin-off" of the issue from awhile ago, discussed in the thread https://www.llblgen.com/TinyForum/Messages.aspx?ThreadID=23482 . But since that thread is marked as solved, I'm creating a new thread...

I'll quickly recap the original issue, because it was a quite tricky problem, but it's needed to understand this new related issue.

In the original problem, I had a simple datamodel with a 1-N relationship between the entities SampleEntity and SampleParamEntity. The problem was that after serializing and deserializing SampleEntities using JSON.NET, it always had doubled the number of SampleParamEntity subobjects. After some research and debugging the JSON.NET code, it was clear that one line of code (line 1405 in JsonInternalReader.cs => "list.Add(value);") was called twice per subobject, once when adding the child to the parent, and again when setting the parent object on the child, effectively doubling the number of subobjects.

The simple but effective solution for this problem was to use the JSON.NET provided "JsonConstructorAttribute" to tell the deserializer to use a specific constructor, where I could then set the entities' "IsDeserializing=true", circumventing the double adding of subobjects.

I've adapted my code generation entityadapter.template so that every entity now has an extra constructor :

[JsonConstructor]
        public xxxEntity(bool callViaJson) : base("xxxEntity")
        {
         //callViaJson is ignored, was just to have a unique constructor signature
         this.IsDeserializing = true;
         InitClassEmpty(null, null);
        }

This changed ultimately made my unittest succeed :

 [TestMethod]
        public void TestSubobjects()
        {
            SampleEntity entity = new SampleEntity() {Code = "testcode", Description = "testdescription"};
            entity.SampleParams.Add(new SampleParamEntity() {Param = "param1", Value = "value1"});

            string json = JsonConvert.SerializeObject(entity, LLBLgenJsonSerializerSettings.Settings);
            SampleEntity deserialized = JsonConvert.DeserializeObject<SampleEntity>(json, LLBLgenJsonSerializerSettings.Settings);

            Assert.IsTrue(entity.Code == deserialized.Code);
            Assert.IsTrue(entity.Description == deserialized.Description);
            Assert.IsTrue(entity.SampleParams.Count == deserialized.SampleParams.Count););//this now succeeds !!!
        }

This seemed to work quite nice (tested a few cases), so I parked this part of my application ... until yesterday (a day off so I could continue working on it simple_smile

It seems that there is another issue when trying to serialize/deserialize when there is a N-1 relationship. I've pinpointed the problem (and will propose a fix).

Imagine the same datamodel, but the SampleParam is now the parent, and the Sample the child (effectively inverting the relationship to N-1) Trying to serialize/deserialize such an entitymodel just crashes... I've reordered the previous unittest to :

  [TestMethod]
        public void TestSubobjects_inverted()
        {

            SampleEntity entity = new SampleEntity() { Code = "testcode", Description = "testdescription" };
            SampleParamEntity param = new SampleParamEntity() {Param = "param1", Value = "value1"};
            param.Sample = entity;

            string json = JsonConvert.SerializeObject(param, LLBLgenJsonSerializerSettings.Settings);
            SampleParamEntity deserialized = JsonConvert.DeserializeObject<SampleParamEntity>(json, LLBLgenJsonSerializerSettings.Settings);

            Assert.IsTrue(param.Sample.Code == deserialized.Sample.Code);
            Assert.IsTrue(param.Sample.Description == deserialized.Sample.Description);
        }

And it crashes on the DeserializeObject method. Ironically, the offending line of code is again the line 1405 in JsonInternalReader.cs, but now the value == null. So basically the deserializer is trying to add a null value as child to the parent, and LLBLgen throws an exception that this cannot happen (ArgumentNullException at SD.LLBLGen.Pro.ORMSupportClasses.CollectionCore`1.System.Collections.IList.Add(Object value)). At first sight, this seems a json.net bug which could be fixed by changing that line 1405 to "if (value!==null) list.add(value);)" and indeed it works when I add this, but the value shouldn't be null there in the first place. And I suspect that has something to do with LLBLGen.

The reason I suspect this is two-fold : 1) If I removed the JsonConstructor from the entities, the deserializer uses the default constructor, and returns the correct deserialized object. But this is not an option, I need the JsonConstructor for the deserialization previous issue with 1-N relations.

2) If I create simple poco's mimicking the same setup, the whole system works. So json can use the non-default constructor during deserialization, illustrated in these 2 tests : (basically the same as above, but with poco's)

 public class Sample
    {
        public Sample()
        {
            SampleParams = new List<SampleParam>();
        }

        [JsonConstructor]
        public Sample(bool dummy)
        {
            SampleParams = new List<SampleParam>();
        }


        public int Id { get; set; }
        public string Code { get; set; }
        public string Description { get; set; }
        public IList<SampleParam> SampleParams { get; set; }
    }

    public class SampleParam
    {
        public SampleParam()
        {
            
        }

        [JsonConstructor]
        public SampleParam(bool dummy)
        {
            
        }

        public int Id { get; set; }
        public int SampleId { get; set; }
        public Sample Sample { get; set; }
        public string Param { get; set; }
        public string Value { get; set; }
    }

    
    [TestClass]
    public class POCOTests
    {
        [TestMethod]
        public void TestSubobjects_POCO()
        {

            Sample entity = new Sample() {Code = "testcode", Description = "testdescription"};
            SampleParam param = new SampleParam() { Param = "param1", Value = "value1" };
            entity.SampleParams.Add(param);
            param.Sample = entity;

            string json = JsonConvert.SerializeObject(entity, LLBLgenJsonSerializerSettings.Settings);
            Sample deserialized = JsonConvert.DeserializeObject<Sample>(json, LLBLgenJsonSerializerSettings.Settings);

            Assert.IsTrue(entity.Code == deserialized.Code);
            Assert.IsTrue(entity.Description == deserialized.Description);
            Assert.IsTrue(entity.SampleParams.Count == deserialized.SampleParams.Count);
        }

        [TestMethod]
        public void TestSubobjects_POCO_inverted()
        {

            Sample entity = new Sample() { Code = "testcode", Description = "testdescription" };
            SampleParam param = new SampleParam() { Param = "param1", Value = "value1" };
            entity.SampleParams.Add(param);
            param.Sample = entity;

            string json = JsonConvert.SerializeObject(param, LLBLgenJsonSerializerSettings.Settings);
            SampleParam deserialized = JsonConvert.DeserializeObject<SampleParam>(json, LLBLgenJsonSerializerSettings.Settings);

            Assert.IsTrue(param.Sample.Code == deserialized.Sample.Code);
            Assert.IsTrue(param.Sample.Description == deserialized.Sample.Description);
        }
    }

The way I see it, there are 2 possible fixes : 1) In JSON.NET, I could submit a github issue and ask to change the line 1405 so it checks for null before adding the value to the list. But this would probably break stuff in cases where nulls are intentionally used, so that's a bad idea. 2) In LLBLGen, don't throw an exception but ignore nulls added as subobjects (only when IsDeserializing == true of course).

Ultimately , ideally would be to find out WHY json.net suddenly creates a null object at that point in time, but I've just spent 5 hours stepping through the json.net code and my head is spinning ...

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 13-Nov-2015 07:17:44   

HcD wrote:

Ultimately , ideally would be to find out WHY json.net suddenly creates a null object at that point in time, but I've just spent 5 hours stepping through the json.net code and my head is spinning

IMHO this is the real point we have to look at. Why this happen in the first place... We will try to figured it out...

David Elizondo | LLBLGen Support Team
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 14-Nov-2015 10:18:05   

When you set a breakpoint with a condition 'value==null' on that line in json.net, then run the test, it breaks (as value == null) and then see where the call came from using the stacktrace overview in vs.net, you should be able to determine what made it decide to create a null instead of a real object...

Frans Bouma | Lead developer LLBLGen Pro
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 17-Nov-2015 08:43:18   

I have spent way too much time on it, but actually could reproduce a case for it when using poco's too, so I have raised an issue for it on Github. https://github.com/JamesNK/Newtonsoft.Json/issues/715

I'll let it know here when it's fixed.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 17-Nov-2015 10:14:44   

Glad you could get a repro that illustrates the issue so they can get it fixed! I already wondered how it could happen as it would IMHO otherwise also occur during normal binary deserialization.

Frans Bouma | Lead developer LLBLGen Pro
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 29-Nov-2015 12:08:20   

The issue was closed on GitHub without a fix for it : https://github.com/JamesNK/Newtonsoft.Json/issues/715#event-476721348

This sucks because my concept was based on sharing the entity model in the frontend/backend and serializing to json for the communication inbetween. (which seems a pretty standard approach ?)

So what's left for me is not using the [JsonConstructor], but then I have to set "IsDeserializing = true" in the regular constructor, and remember to set it false in code everywhere I construct entities the normal way.

Another solution, but that not something I can do, is that somehow the framework could be signaled to not throw an exception when a null subobject is added to the collection, but just ignore it. Eg. a property "IgnoreAddNullSubobjects" that is false by default, but can be set to true so the framework won't throw an exception but just ignores it.

Of course, one could argue whether the choice of sharing the entity model on both back- and frontend is a good idea (considering Fowler's first law of distributed object design, which is 'don't distribute', but that was back in 2003). An alternative here would be creating interfaces and DTO's for all entities, and use these on the frontend. But in my opinion, this creates just a lot of new files, mapping code, and rewriting the same code on the frontend that LLBLGen provides out of the box for free (like the bidirectional sync of parent-childs relations in the DTO's)

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 30-Nov-2015 03:43:31   

IMHO your two options (A. Use entities as model and DTOs using your Ctor workaround; B. Write new DTOs) are ok. Personally I use the entities as model objects to pass through the services, but there are occasions when it's useful to have another tier of independent objects.

David Elizondo | LLBLGen Support Team
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 18-Feb-2016 16:18:36   

Hello,

I'm sorry to post again on this thread, but after having a lot of problems, I would again like to request if this would be a possibility (in LLBLGen 4.2) :

Another solution, but that not something I can do, is that somehow the framework could be signaled to not throw an exception when a null subobject is added to the collection, but just ignore it. Eg. a property "IgnoreAddNullSubobjects" that is false by default, but can be set to true so the framework won't throw an exception but just ignores it.

I've tried all kind of workaraounds with the JsonConstructor, setting Isdeserializing in every constructor, etc .. but every time there is some other problem needing another workaround. I see no other solution then to use the [JSonConstructor] on a non-default constructor, so everything else still works, but in that constructor I would put IgnoreAddNullSubobjects=true to fix the adding of nulls caused by json

Kind regards, Sven

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 18-Feb-2016 17:41:09   

You want us to add a workaround so crap code in JSon.NET doesn't crash?

Frans Bouma | Lead developer LLBLGen Pro
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 18-Feb-2016 20:08:49   

Haha, you have a point of course simple_smile

Today I played a bit with different 'DataMember' attribute settings on my entities, maybe I can solve it that way ..It leads also to cleaner json and I can get rid of the "ReferenceHandling" that generates those ugly $id's in the json