Given the following set up where there are many Teams and there are many LeagueSessions. Each Team belongs to zero or more LeagueSessions but only ever one LeagueSession is active. LeagueSessions have many teams, and the teams will be repeated. Many-to-many relationship is established between Teams and LeagueSessions with a join table called TeamsSessions.
Team model looks like this:
public class Team
{
public string Id { get; set; }
public string Name { get; set; }
public League League { get; set; }
public string LeagueID { get; set; }
public bool Selected { get; set; }
public ICollection<Match> Matches { get; set; }
public virtual ICollection<TeamSession> TeamsSessions { get; set; }
}
Team model fluent api configuration:
`
public class TeamConfiguration
{
public TeamConfiguration(EntityTypeBuilder<Team> model)
{
// The data for this model will be generated inside ThePLeagueDataCore.DataBaseInitializer.DatabaseBaseInitializer.cs class
// When generating data for models in here, you have to provide it with an ID, and it became mildly problematic to consistently get
// a unique ID for all the teams. In ThePLeagueDataCore.DataBaseInitializer.DatabaseBaseInitializer.cs we can use dbContext to generate
// unique ids for us for each team.
model.HasOne(team => team.League)
.WithMany(league => league.Teams)
.HasForeignKey(team => team.LeagueID);
}
}
`
Each team belongs to a single League. League model looks like this:
`public class League
{
public string Id { get; set; }
public string Type { get; set; }
public string Name { get; set; }
public IEnumerable<Team> Teams { get; set; }
public bool Selected { get; set; }
public string SportTypeID { get; set; }
public SportType SportType { get; set; }
public IEnumerable<LeagueSessionSchedule> Sessions { get; set; }
}`
fluent API for the League:
`public LeagueConfiguration(EntityTypeBuilder<League> model)
{
model.HasOne(league => league.SportType)
.WithMany(sportType => sportType.Leagues)
.HasForeignKey(league => league.SportTypeID);
model.HasMany(league => league.Teams)
.WithOne(team => team.League)
.HasForeignKey(team => team.LeagueID);
model.HasData(leagues);
}`
SessionScheduleBase class looks like this:
public class SessionScheduleBase
{
public string LeagueID { get; set; }
public bool ByeWeeks { get; set; }
public long? NumberOfWeeks { get; set; }
public DateTime SessionStart { get; set; }
public DateTime SessionEnd { get; set; }
public ICollection<TeamSession> TeamsSessions { get; set; } = new Collection<TeamSession>();
public ICollection<GameDay> GamesDays { get; set; } = new Collection<GameDay>();
}
Note: LeagueSessionSchedule inherits from SessionScheduleBase
The TeamSession model looks like this:
`public class TeamSession
{
public string Id { get; set; }
public string TeamId { get; set; }
public Team Team { get; set; }
public string LeagueSessionScheduleId { get; set; }
public LeagueSessionSchedule LeagueSessionSchedule { get; set; }
}`
I then configure the relationship with the fluent API like this:
`public TeamSessionConfiguration(EntityTypeBuilder<TeamSession> model)
{
model.HasKey(ts => new { ts.TeamId, ts.LeagueSessionScheduleId });
model.HasOne(ts => ts.Team)
.WithMany(t => t.TeamsSessions)
.HasForeignKey(ts => ts.TeamId);
model.HasOne(ts => ts.LeagueSessionSchedule)
.WithMany(s => s.TeamsSessions)
.HasForeignKey(ts => ts.LeagueSessionScheduleId);
}`
The problem arises whenever I attempt to insert a new LeagueSessionSchedule. The way I am adding a new TeamSession object onto the new LeagueSessionSchedule is like this:
`foreach (TeamSessionViewModel teamSession in newSchedule.TeamsSessions)
{
Team team = await this._teamRepository.GetByIdAsync(teamSession.TeamId, ct);
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
}`
Saving the new LeagueSessionSchedule code:
public async Task<LeagueSessionSchedule> AddScheduleAsync(LeagueSessionSchedule newLeagueSessionSchedule, CancellationToken ct = default)
{
this._dbContext.LeagueSessions.Add(newLeagueSessionSchedule);
await this._dbContext.SaveChangesAsync(ct);
return newLeagueSessionSchedule;
}
Saving the new LeagueSessionSchedule object throws an error by Entity Framework Core that it cannot INSERT a duplicate primary key value into the dbo.Teams table. I have no idea why its attempting to add to dbo.Teams table and not into TeamsSessions table.
ERROR:
INSERT INTO [LeagueSessions] ([Id], [Active], [ByeWeeks], [LeagueID], [NumberOfWeeks], [SessionEnd], [SessionStart])
VALUES (#p0, #p1, #p2, #p3, #p4, #p5, #p6);
INSERT INTO [Teams] ([Id], [Discriminator], [LeagueID], [Name], [Selected])
VALUES (#p7, #p8, #p9, #p10, #p11),
(#p12, #p13, #p14, #p15, #p16),
(#p17, #p18, #p19, #p20, #p21),
(#p22, #p23, #p24, #p25, #p26),
(#p27, #p28, #p29, #p30, #p31),
(#p32, #p33, #p34, #p35, #p36),
(#p37, #p38, #p39, #p40, #p41),
(#p42, #p43, #p44, #p45, #p46);
System.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_Teams'. Cannot insert duplicate key in object 'dbo.Teams'. The duplicate key value is (217e2e11-0603-4239-aab5-9e2f1d3ebc2c).
My goal is to create a new LeagueSessionSchedule object. Along with the creation of this object, I also have to create a new TeamSession entry to the join table (or not if join table is not necessary) to then be able to pick any given team and see what session it is currently a part of.
My entire PublishSchedule method is the following:
`
public async Task<bool> PublishSessionsSchedulesAsync(List<LeagueSessionScheduleViewModel> newLeagueSessionsSchedules, CancellationToken ct = default(CancellationToken))
{
List<LeagueSessionSchedule> leagueSessionOperations = new List<LeagueSessionSchedule>();
foreach (LeagueSessionScheduleViewModel newSchedule in newLeagueSessionsSchedules)
{
LeagueSessionSchedule leagueSessionSchedule = new LeagueSessionSchedule()
{
Active = newSchedule.Active,
LeagueID = newSchedule.LeagueID,
ByeWeeks = newSchedule.ByeWeeks,
NumberOfWeeks = newSchedule.NumberOfWeeks,
SessionStart = newSchedule.SessionStart,
SessionEnd = newSchedule.SessionEnd
};
// leagueSessionSchedule = await this._sessionScheduleRepository.AddScheduleAsync(leagueSessionSchedule, ct);
// create game day entry for all configured game days
foreach (GameDayViewModel gameDay in newSchedule.GamesDays)
{
GameDay newGameDay = new GameDay()
{
GamesDay = gameDay.GamesDay
};
// leagueSessionSchedule.GamesDays.Add(newGameDay);
// create game time entry for every game day
foreach (GameTimeViewModel gameTime in gameDay.GamesTimes)
{
GameTime newGameTime = new GameTime()
{
GamesTime = DateTimeOffset.FromUnixTimeSeconds(gameTime.GamesTime).DateTime.ToLocalTime(),
// GameDayId = newGameDay.Id
};
// newGameTime = await this._sessionScheduleRepository.AddGameTimeAsync(newGameTime, ct);
newGameDay.GamesTimes.Add(newGameTime);
}
leagueSessionSchedule.GamesDays.Add(newGameDay);
}
// update teams sessions
foreach (TeamSessionViewModel teamSession in newSchedule.TeamsSessions)
{
// retrieve the team with the corresponding id
Team team = await this._teamRepository.GetByIdAsync(teamSession.TeamId, ct);
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
}
// update matches for this session
foreach (MatchViewModel match in newSchedule.Matches)
{
Match newMatch = new Match()
{
DateTime = match.DateTime,
HomeTeamId = match.HomeTeam.Id,
AwayTeamId = match.AwayTeam.Id,
LeagueID = match.LeagueID
};
leagueSessionSchedule.Matches.Add(newMatch);
}
try
{
leagueSessionOperations.Add(await this._sessionScheduleRepository.AddScheduleAsync(leagueSessionSchedule, ct));
}
catch(Exception ex)
{
}
}
// ensure all leagueSessionOperations did not return any null values
return leagueSessionOperations.All(op => op != null);
}
`
This is not a many-to-many relationship.
It is two separate one-to-many relationships, which happen to refer to the same table on one end of the relationship.
While it is true that on the database level, both use cases are represented by three tables, i.e. Foo 1->* FooBar *<-1 Bar, these two cases are treated differently by Entity Framework's automated behavior - and this is very important.
EF only handles the cross table for you if it is a direct many-to-many, e.g.
public class Foo
{
public virtual ICollection<Bar> Bars { get; set; }
}
public class Bar
{
public virtual ICollection<Foo> Foos { get; set; }
}
EF handles the cross table behind the scenes, and you are never made aware of the existence of the cross table (from the code perspective).
Importantly, EF Core does not yet support implicit cross tables! There is currently no way to do this in EF Core, but even if there were, you're not using it anyway, so the answer to your problem remains the same regardless of whether you're using EF or EF Core.
However, you have defined your own cross table. While this is still representative of a many-to-many relationship in database terms, it has ceased to be a many-to-many relationship as far as EF is concerned, and any documentation you find on EF's many-to-many relationships no longer applies to your scenario.
Unattached but indirectly added objects are assumed to be new.
By "indirectly added", I mean you that it was added to the context as part of another entity (which you directly added to the context). In the following example, foo is directly added and bar is indirectly added:
var foo = new Foo();
var bar = new Bar();
foo.Bar = bar;
context.Foos.Add(foo); // directly adding foo
// ... but not bar
context.SaveChanges();
When you add (and commit) a new entity to the context, EF adds it for you. However, EF also looks at any related entities that the first entity contains. During the commit in the above example, EF will look at both the foo and bar entities and will handle them accordingly. EF is smart enough to realize that you want bar to be stored in the database since you put it inside the foo object and you explicitly asked EF to add foo to the database.
It is important to realize that you've told EF that foo should be created (since you called Add(), which implies a new item), but you never told EF what it should do with bar. It's unclear (to EF) what you expect EF to do with this, and thus EF is left guessing at what to do.
If you never explained to EF whether bar already exists or not, Entity Framework defaults to assuming it needs to create this entity in the database.
Saving the new LeagueSessionSchedule object throws an error by Entity Framework Core that it cannot INSERT a duplicate primary key value into the dbo.Teams table. I have no idea why its attempting to add to dbo.Teams table
Knowing what you now know, the error becomes clearer. EF is trying to add this team (which was the bar object in my example) because it has no information on this team object and what its state in the database is.
There are a few solutions here.
1. Use the FK property instead of the navigational property
This is my preferred solution because it leaves no room for error. If the team ID does not yet exist, you get an error. At no point will EF try to create a team, since it doesn't even know the team's data, it only knows the (alleged) ID you're trying to create a relationship with.
Note: I am omitting LeagueSessionSchedule as it is unrelated to the current error - but it's essentially the same behavior for both Team and LeagueSessionSchedule.
TeamSession newTeamSession = new TeamSession()
{
TeamId = team.Id
};
By using the FK property instead of the nav prop, you are informing EF that this is an existing team - and therefore EF no longer tries to (re)create this team.
2. Ensure that the team is tracked by the current context
Note: I am omitting LeagueSessionSchedule as it is unrelated to the current error - but it's essentially the same behavior for both Team and LeagueSessionSchedule.
context.Teams.Attach(team);
TeamSession newTeamSession = new TeamSession()
{
Team = team
};
By attaching the object to the context, you are informing it of its existence. The default state of a newly attached entity is Unchanged, meaning "this already exists in the database and has not been changed - so you don't need to update it when we commit the context".
If you have actually made changes to your team that you want to be updated during commit, you should instead use:
context.Entry(team).State = EntityState.Modified;
Entry() inherently also attaches the entity, and by setting its state to Modified you ensure that the new values will be committed to the database when you call SaveChanges().
Note that I prefer solution 1 over solution 2 because it's foolproof and much less likely to lead to unexpected behavior or runtime exceptions.
String primary keys are undesirable
I'm not going to say that it doesn't work, but strings cannot be autogenerated by Entity Framework, making them undesirable as the type of your entity's PK. You will need to manually set your entity PK values.
Like I said, it's not impossible, but your code shows that you're not explicitly setting PK values:
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
If you want your PK's to be automatically generated, use an appropriate type. int and Guid are by far the most commonly used types for this.
Otherwise, you're going to have to start setting your own PK values, because if you don't (and the Id value thus defaults to null), your code is going to fail when you add a second TeamSession object using the above code (even though you're doing everything else correctly), since PK null is already taken by the first entity you added to the table.
See following simplified example:
Student Class:
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public Grade Grade { get; set; }
}
Grade Class:
public class Grade
{
public int GradeId { get; set; }
public string GradeName { get; set; }
public ICollection<Student> Students { get; set; }
}
Context class:
public class SchoolContext : DbContext
{
public SchoolContext() : base()
{
Configuration.LazyLoadingEnabled = true;
}
public DbSet<Student> Students { get; set; }
public DbSet<Grade> Grades { get; set; }
}
Program:
static void Main(string[] args)
{
using (var ctx = new SchoolContext())
{
Grade[] grades = ctx.Grades.ToArray();
Console.WriteLine(grades[0].Students == null); // True - As expected
var students = ctx.Students.ToArray();
Console.WriteLine(grades[0].Students == null); // False - ? Did not expect that
}
Console.Read();
}
The following happens:
Lazy loading was enabled
List of Grades was saved into an array
As expected, Students navigation property of grade objects was null
Made a separate query to get Students
EF somehow filled up the Students navigation property of the array in memory.
This could end up in very expensive payloads to clients if not used with care.
Can anyone explain why and how did the navigation properties got populated in the array?
The reason the grades[0].Students is loaded after doing a query to get students from your database with ctx.Students.ToArray(); is that you DbContext is tracking changes.
This is explained in Entity Framework docs:
Tracking behavior controls whether or not Entity Framework Core will keep information about an entity instance in its change tracker. If an entity is tracked, any changes detected in the entity will be persisted to the database during SaveChanges(). Entity Framework Core will also fix-up navigation properties between entities that are obtained from a tracking query and entities that were previously loaded into the DbContext instance.
This is the EF Core docs, but this also applies to EF6 for .NET Framework.
If you want to disable this behavior, you may load your entities as no tracking:
ctx.Grades.AsNoTracking().ToArray();
...you could also disable it by default (e.g inside the DbContext constructor), the same way you do for lazy load.
Another way you could do that is to manually detach an object from the context.
Then if you ever intended to make any changes and persist it to database, you should reattach your entity after querying students, and before making your changes:
using (var ctx = new SchoolContext())
{
Grade[] grades = ctx.Grades.ToArray();
Grade firstGrade = grades[0];
Console.WriteLine(firstGrade.Students == null); // True - as expected
ctx.Grades.Detach(firstGrade); // stop tracking changes for this entity
var students = ctx.Students.ToArray();
Console.WriteLine(firstGrade.Students == null); // True - still null
// Let's reattach so we can track changes and save to database
ctx.Grades.Attach(firstGrade);
firstGrade.GradeName = "Some new value"; // will be persisted, as this is being tracked again
ctx.SaveChanges();
}
Also, it's worth mentioning with lazy load enabled, accessing grades[0].Students the first time should make EF load that navigation property if it was not loaded yet (which is precisely its purpose), however it seems this is not happening because your navigation property is not virtual.
I have seen numerous questions and advice on calling dbContext.SaveChanges multiple Times in a transaction.
Some say this should be avoided. This in depth post is really worth a read http://mehdi.me/ambient-dbcontext-in-ef6/
In my particular scenario entity B has Id reference to Entity A
During a creation scenario i Create A and call savechanges in order to get A.Id assigned by database.
Then I create Entity B like new B(A.Id,....)
and call savechanges again. It could look like this in pseudo code
using(var tx = dbContext.BeginTransaction())
{
var a = new A();
dbContext.Add(a); //a.Id is null
dbContext.saveChanges(); // a.Id has now been initialized
var b = new B(a.Id); //I want to create b in a valid state so a.Id cannot be null
dbContext.SaveChanges();
tx.Commit();
}
(I know the pseudo code lacks exception handling logic...)
Why is this a problem ?
Is it because the savechanges cannot be rolled back?
I know I could remodel but that is not possible because we share database with some legacy systems, so the database cannot easily be changed, if possible at all!
What is an alternative solution ?
Use a navigation property like this:
public class A
{
[Key]
public int Id { get; set; }
public int BId { get; set; }
[ForeignKey("BId")]
public B B { get; set; }
}
public class B
{
[Key]
public int Id { get; set; }
}
then simply assign the newly created B to A's navigation property:
using (var transaction = dbContext.BeginTransaction())
{
var a = new A();
a.B = new B();
dbContext.Add(a);
dbContext.saveChanges();
transaction.Commit();
}
I have used EF (now with 6.1.1) for a relevant time. But every time I need to work with more than one entity inside a controller (using MVC) I lose myself.
The state management of entities causes me great confusion and when I think I understand the operation then comes another surprise.
For example (my current confusion):
I have the following classes:
public class A
{
public int Id { get; set; }
public C c { get; set; }
}
public class B
{
public int Id { get; set; }
public C c { get; set; }
}
public class C
{
public int Id { get; set; }
public string anotherProperty { get; set; }
}
And I have a Controller:
[HttpPost]
public void CopyEntityAction(A a){
var b = new B() {
c = a.c // <== here is the problem
};
db.Bs.Add(b);
}
What I want to do is copy (create another entity) C from A to B, and not link to the same C.Id
How can I achieve this?
Thanks for your time
* I think somethings EF could take care automatically
Edit 1
I tried this too:
[HttpPost]
public void CopyEntityAction(A a){
var b = new B();
var c2 = a.c;
db.Entry(c2).State = EntityState.Added;
b.c = c2;
db.Bs.Add(b);
db.SaveChanges();
}
I think you need something like this. Otherwise you are dealing with the same exact C object, not creating a new one.
var b = new B() {
c = new C { Id = a.c.Id, anotherProperty = a.c.Anotherproperty } // <== here is the problem
};
You may also need to explicitly need to add the new C to the context's C collection.
It's really not all that confusing. It boils down to where that data comes from. When you retrieve an object from Entity Framework it is "attached" to your context. However, when you post data to an action and have the modelbinder new up an instance of your entity with that data (via including it as a parameter), it is not attached. That means Entity Framework knows nothing about this object. It doesn't know where it came from, whether it's ever been saved before or not (is this going to be an update or an insert?), etc. So, it's on you to tell it what to do at that point. That is what the EntityState enum is for.
I am just starting to teach myself Entity Framework (6) from the code-first scenario. My problem is I don't understand how to correctly instruct EF that certain entities may be shared among other entities.
Here's a very simple scenario with a three-level tree structure. Class A objects owns class B objects, which owns, or references, class C objects:
public class A
{
[Key]
public string Id { get; set; }
public ICollection<B> Bees { get; set; }
public override string ToString() { return Id; }
}
public class B
{
[Key]
public string Id { get; set; }
public ICollection<C> Cees { get; set; }
public override string ToString() { return Id; }
}
public class C
{
[Key]
public string Id { get; set; }
public override string ToString() { return Id; }
}
I now create a scenario where a single A object owns five B objects which, in turn, shares a collection of five C objects. Here's the DbContext I've used:
public class Context : DbContext
{
public DbSet<A> As { get; set; }
public DbSet<C> Cs { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// SHOULD I INSTRUCT EF HOW TO HANDLE THIS HERE?
base.OnModelCreating(modelBuilder);
}
public Context(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
}
And here's some simple code to set up this scenario:
internal class Program
{
private static void Main(string[] args)
{
var context = new Context(#"Data Source=(localdb)\v11.0; Initial Catalog=TestEF6.Collections; Integrated Security=true");
var sharedCees = createCees("A1.B1", 5, context);
context.As.Add(new A { Id = "A1", Bees = createBees("A1", 5, sharedCees) });
context.SaveChanges();
}
private static ICollection<B> createBees(string aId, int count, ICollection<C> cees = null)
{
var list = new List<B>();
for (var i = 0; i < count; i++)
{
var id = aId + ".B" + (i + 1);
list.Add(new B { Id = id, Cees = cees ?? createCees(id, count) });
}
return list;
}
private static ICollection<C> createCees(string bId, int count, Context saveInContext = null)
{
var list = new List<C>();
for (var i = 0; i < count; i++)
{
var id = bId + ".C" + (i+1);
var c = new C {Id = id};
list.Add(c);
if (saveInContext != null)
saveInContext.Cs.Add(c);
}
if (saveInContext != null)
saveInContext.SaveChanges();
return list;
}
}
As EF creates the database ('TestEF6.SharedEntities') it assumes that A -> B, as well as B -> C are one-to-many relationships and that the child entities are components, fully owned and controlled by its master entity.
So, how do I make EF "see" that instances of C can be shared by instances of B?
In all honesty, I'm asking to get a quick start into EF. I do realize I can plow through the web and figure this out but if anyone can point me in the right direction I would very much appreciate it.
Cheers
Gert set me on track by suggesting I'd add a collection property to the C class. Indeed, this is enough for EF/CF to automatically recognize the many-to-many relationship between B and C. But the whole point with code-first is to design a domain model of classes and then have EF persist them, without having to bog them down with peristency information.
I have no issues with annotating Key properties, or even Required ones. This kind of information actually makes sense in the domain model. They can be used by the domain code, but even if they're not they add clarityto the code. But to add properties that serves no other function than to act as hints to EF's auto-magic didn't seem like a very elegant solution.
So I experimented some more with the fluent API and realized I had missed the parameterless overload of .WithMany(). I was looking for a generic form, that would make it possible to specify the other type of a many-to-many relationship. In my mind I would then write: modelBuilder.Entity<B>.HasMany(b => b.Cees).WithMany<B>(). As it turned out, the parameterless overload of WithMany() serves this exact purpose, as type C in this scenario is already specified by the first HasMany() part.
So, to summarize, EF will recognize the many-to-many relationship between classes B and C, without the need to "litter" the C clas with an extra generic collection of type B items. So, this is how we can describe the many-to-many relationsship between the two classes without having to touch the domain classes at all:
public class Context : DbContext
{
public DbSet<A> As { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<B>().HasMany(b => b.Cees).WithMany(); // <-- THE SOLUTION
base.OnModelCreating(modelBuilder);
}
public Context(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
}
I realize this is very basic stuff to anyone seasoned in EF but I'll go ahead and mark this as an answer so that others may save some time looking for this information in the future.
Thanks