Search code examples
c#jsonentity-frameworkef-code-first

Saving deserialized JSON objects to database with duplicate child entities


I am retrieving some JSON from an API call and deserializing it into it's component objects. Everything works absolutely fine, until I come to saving to the database. The reason is, there are child objects with duplicate keys (which is absolutely correct as far as the data is concerned) but when I save the top level object, it throws a primary key violation error on the child object.

Here is a sample of my JSON (I know it is not complete);

{
"count": 149,
"filters": {},
"competitions": [
    {
        "id": 2006,
        "area": {
            "id": 2001,
            "name": "Africa",
            "countryCode": "AFR",
            "ensignUrl": null
        },
        "name": "WC Qualification",
        "code": null,
        "emblemUrl": null,
        "plan": "TIER_FOUR",
        "currentSeason": {
            "id": 555,
            "startDate": "2019-09-04",
            "endDate": "2021-11-16",
            "currentMatchday": null,
            "winner": null
        },
        "numberOfAvailableSeasons": 2,
        "lastUpdated": "2018-06-04T23:54:04Z"
    },
    {
        "id": 2025,
        "area": {
            "id": 2011,
            "name": "Argentina",
            "countryCode": "ARG",
            "ensignUrl": null
        },
        "name": "Supercopa Argentina",
        "code": null,
        "emblemUrl": null,
        "plan": "TIER_FOUR",
        "currentSeason": {
            "id": 430,
            "startDate": "2019-04-04",
            "endDate": "2019-04-04",
            "currentMatchday": null,
            "winner": null
        },
        "numberOfAvailableSeasons": 2,
        "lastUpdated": "2019-05-03T05:08:18Z"
    },
    {
        "id": 2023,
        "area": {
            "id": 2011,
            "name": "Argentina",
            "countryCode": "ARG",
            "ensignUrl": null
        },
        "name": "Primera B Nacional",
        "code": null,
        "emblemUrl": null,
        "plan": "TIER_FOUR",
        "currentSeason": {
            "id": 547,
            "startDate": "2019-08-16",
            "endDate": "2020-06-14",
            "currentMatchday": 30,
            "winner": null
        },
        "numberOfAvailableSeasons": 3,
        "lastUpdated": "2020-05-15T00:00:02Z"
    },

Currently I am just saving the top level object, and I expect/want all of the child objects to save too. If I turn off the primary keys on the child objects (make them identidy columns rather than their actual values), this all works fine and all of the child objects save perfectly. As you can see from the JSON, "area" 2011 is a duplicate, there are two competitions that have the same area, so data wise it is correct, but with the proper primary keys of the "area" turned on, it trips out as it is trying to insert a duplicate record.

So I understand perfectly what is going on and why it is erroring, what I want to know is, is there a simple way of just telling EF to ignore duplicate key errors. I can't add a try catch to saving the top level object as it just saves nothing when it hits the error.

I have tried saving the individual child objects, testing for their existence prior to the save, but when it tries to save the parent level object, it tries to save the child object too, leaving me with the same problem.

Here is my code for saving the top level object (cut down for simplicity);

public class Area
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int id { get; set; }
    public string name { get; set; }
    public string countryCode { get; set; }
    public string ensignUrl { get; set; }
}

public class Winner
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int id { get; set; }
    public string name { get; set; }
    public string shortName { get; set; }
    public string tla { get; set; }
    public string crestUrl { get; set; }
}

public class CurrentSeason
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int id { get; set; }
    public string startDate { get; set; }
    public string endDate { get; set; }
    public int? currentMatchday { get; set; }
    public Winner winner { get; set; }
}

public class Competition
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int id { get; set; }
    public Area area { get; set; }
    public string name { get; set; }
    public string code { get; set; }
    public string emblemUrl { get; set; }
    public string plan { get; set; }
    public CurrentSeason currentSeason { get; set; }
    public int numberOfAvailableSeasons { get; set; }
    public DateTime lastUpdated { get; set; }
}

public class Example
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int count { get; set; }
    public IList<Competition> competitions { get; set; }
}


static void Main(string[] args)
    {

        string json = GET(@"http://my.url.com/api/stuff");

        Example example = JsonConvert.DeserializeObject<Example>(json);

        using(var db = new ExampleContext())
        {
            db.Examples.Add(example);
            db.SaveChanges();
        }
    }

Thanks in anticipation.


Solution

  • Unfortunately there isn't any straight way to overcome your problem.

    EF Change Tracker tracks entities by their reference and the only way to solve your problem is to create same object for all identical areas.

    For this you have two choices:

    1- Loop over example after this line

    Example example = JsonConvert.DeserializeObject<Example>(json);
    

    and find all identical areas and replace all with one of them.

    2- Use PreserveReferencesHandling feature of NewtonSoft. But it needs to apply on both Serialize and Deserialize sides:

    Server(Api) side:

    string json = JsonConvert.SerializeObject(data, Formatting.Indented,
       new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects });
    

    Client side:

    var example = JsonConvert.DeserializeObject<Example>(json,
       new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects });