Self Reference when serializing nested Object

.net-core c# entity-framework-core json.net

Question

I want to store a snapshot of a nested model in my database as sort of a change history. Therefore I made a model that serializes the whole object into a JSON string for easier storage.

Simplified Data class I want to store:

public class Data
{
    public int Id { get; set; }
    public string SomeInfo { get; set; }
    public virtual ICollection<DataObject> DataObject { get; set; }
}

The DataObject for the collection inside Data:

public class DataObject
{
    public int Id { get; set; }
    public string SomeMoreInfo { get; set; }
    public int DataId { get; set; }
    public virtual Data Data { get; set; }
}

My snapshot class looks something like this:

public class DataHistory
{
    public int Id { get; set; }
    private string _Data;

    [NotMapped]
    public Data Data
    {
        get { return _Data == null ? null : JsonConvert.DeserializeObject<Data>(_Data); }
        set { _Data = JsonConvert.SerializeObject(value , Formatting.Indented, 
                new JsonSerializerSettings { 
                        ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                        PreserveReferencesHandling = PreserveReferencesHandling.None
                });
        }
    }
}

Inside my controller I do:

var data = await _repo.GetData(id);
var historyEntry = new DataHistory();
historyEntry.Data= data;
_repo.Add(historyEntry); 

GetData() method inside the repository:

public async Task<Data> GetData(int id)
{
   return await _context.Data
   .Include(d => d.DataObject)
   .FirstOrDefaultAsync(d => d.Id == id);
}

The problem is when I try to serialize one Data entry I get a self reference inside the DataObject so it includes the Data object again and also the DataObjects. Even with ReferenceLoopHandling.Ignore the produced JSON looks something like this:

{
  "Id": 1051,
  "SomeInfo": "asdasd",
  "DataObject": [
    {
      "Id": 121,
      "SomeMoreInfo": "asdasd",
      "Data": {
        "Id": 1051,
        "SomeInfo": "asdasd",
        "DataObject": [
          {
            "Id": 122,
            "SomeMoreInfo": "asdasd",
            "DataId": 1051
          }
        ]
      }
    },
    {
      "Id": 122,
      "SomeMoreInfo": "asdasd",
      "Data": {
        "Id": 1051,
        "SomeInfo": "asdasd",
        "DataObject": [
          {
            "Id": 121,
            "SomeMoreInfo": "asdasd",
            "DataId": 1051
          }
        ]
      }
    }
  ]
}

EDIT: Expected output would be something like this:

{
    "Id": 1051,
    "SomeInfo": "Data",
    "DataObject": [
        {
            "Id": 121,
            "SomeMoreInfo": "DataObject1"
            "DataId": 1051
        },
        {
            "Id": 122,
            "SomeMoreInfo": "DataObject2"
            "DataId": 1051
        }
    ]
}

How can I stop it from including Data a second time without using DTOs?

EDIT:

If I try it without Entity Framework, ReferenceLoopHandling.None works as expected. See Dotnet Fiddle https://dotnetfiddle.net/bmAoAW. So there seems to be a problem with my EF Core configuration or something.

1
1
10/8/2019 5:17:25 PM

Accepted Answer

You said in the comments that effectively you want to DataObject.Data property to be ignored whenever you are serializing Data from within DataHistory. You can do this by using a custom ContractResolver to ignore the property programmatically.

Here is the code you would need for the resolver:

public class CustomResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty prop = base.CreateProperty(member, memberSerialization);
        if (prop.DeclaringType == typeof(DataObject) && prop.PropertyName == nameof(DataObject.Data))
        {
            prop.Ignored = true;
        }
        return prop;
    }
}

Then apply it within the JsonSerializerSettings in DataHistory.Data:

    set { _Data = JsonConvert.SerializeObject(value , Formatting.Indented, 
            new JsonSerializerSettings { 
                    ContractResolver = new CustomResolver(),
                    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                    PreserveReferencesHandling = PreserveReferencesHandling.None
            });
    }
1
10/8/2019 4:21:15 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow