tschmit007
tschmit007

Reputation: 7800

Entity Framework circular dependency for last entity

Please consider the following entities

public class What {
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Track> Tracks { get; set; }
    public int? LastTrackId { get; set; }]
    public Track LastTrack { get; set; }
}

public class Track {
    public Track(string what, DateTime dt, TrackThatGeoposition pos) {
        What = new What { Name = what, LastTrack = this };
    }

    public int Id { get; set; }

    public int WhatId { get; set; }
    public What What { get; set; }
}

I use the following to configure the entities:

builder.HasKey(x => x.Id);
builder.HasMany(x => x.Tracks).
    WithOne(y => y.What).HasForeignKey(y => y.WhatId);
builder.Property(x => x.Name).HasMaxLength(100);
builder.HasOne(x => x.LastTrack).
    WithMany().HasForeignKey(x => x.LastTrackId);

Has you can see there is a wanted circular reference:

What.LastTrack <-> Track.What

when I try to add a Track to the context (on SaveChanges in fact):

Track t = new Track("truc", Datetime.Now, pos);
ctx.Tracks.Add(t);
ctx.SaveChanges();

I get the following error:

Unable to save changes because a circular dependency was detected in the data to be saved: ''What' {'LastTrackId'} -> 'Track' {'Id'}, 'Track' {'WhatId'} -> 'What' {'Id'}'.

I would like to say... yes, I know but...

Is such a configuration doable with EF Core ?

Upvotes: 26

Views: 14896

Answers (2)

novaXire
novaXire

Reputation: 136

I encountered the same problem, but i solved it differently.

In my case, it was about a list of status and a reference to the last status. So with the following case :

public class What {
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Status> StatusList { get; set; }
    public int? LastStatusId { get; set; }
    public Status LastStatus { get; set; }

    public void AddStatus(Status s)
    {
        StatusList.Add(s);
        LastStatus = s;
    }
}

public class Status{
    public int Id { get; set; }

    public int WhatId { get; set; }
    public What What { get; set; }
}

In my program, i changed my code to use StatusList as an history that doesn't include the lastStatus, so :

public class What {
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Status> StatusHistory { get; set; }
    public int? LastStatusId { get; set; }
    public Status LastStatus { get; set; }

    public void AddStatus(Status s)
    {
        if(LastStatus) StatusList.Add(LastStatus);
        LastStatus = s;
    }

    public List<Status> GetStatusList(Status s) // If needed, a method, not a property because i got an error with lazyLoading
    {
        return new List<Status>(StatusHistory) { LastStatus}; // List of all status (history + last)
    }
}

public class Status{
    public int Id { get; set; }

    public int? WhatId { get; set; }
    public What What { get; set; }
}

and don't forget to put in your context IsRequired(false) on the foreignKey :

builder.HasMany(x => x.Status).
    WithOne(y => y.What).HasForeignKey(y => y.WhatId).IsRequired(false);

Like this, no more circular reference.

Upvotes: 0

Gert Arnold
Gert Arnold

Reputation: 109137

This is what I like to call the favored child problem: a parent has multiple children, but one of them is extra special. This causes problems in real life... and in data processing.

In your class model, What (is that a sensible name, by the way?) has Tracks as children, but one of these, LastTrack is the special child to which What keeps a reference.

When both What and Tracks are created in one transaction, EF will try to use the generated What.Id to insert the new Tracks with WhatId. But before it can save What it needs the generated Id of the last Track. Since SQL databases can't insert records simultaneously, this circular reference can't be established in one isolated transaction.

You need one transaction to save What and its Tracks and a subsequent transaction to set What.LastTrackId.

To do this in one database transaction you can wrap the code in a TransactionScope:

using(var ts = new TransactionScope())
{
    // do the stuff
    ts.Complete();
}

If an exception occurs, ts.Complete(); won't happen and a rollback will occur when the TransactionScope is disposed.

Upvotes: 43

Related Questions