Reputation: 7800
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
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
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 Track
s are created in one transaction, EF will try to use the generated What.Id
to insert the new Track
s 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 Track
s 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