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.
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
});
}