EF update the child relation of a relation - c#

EF tracking confuses me. Here is a scenario that I am trying to achieve:
public class CentralPoint
{
public Guid ID { get; set; }
public virtual BIDatabase BIDatabase { get; set; }
}
public class BIDatabase
{
public Guid ID { get; set; }
public Guid CentralPointID { get; set; }
public virtual CentralPoint CentralPoint { get; set; }
public Guid ConnectionID { get; set; }
public virtual Connection Connection { get; set; }
}
public class Connection
{
public Guid ID { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
These are my entity models. I am having a one-one relationship between CentralPoint and BIDatabase entities, also a one-one relationship between BIDatabase and Connection entities. Obviously there is a separate table for each of these entities.
Inside the controller I am updating the BIDatabase property of the CentralPoint with a new instance
centralPoint.BIDatabase = biDatabase;
Here is the controller part:
public async Task<IActionResult> AddBIDatabaseAsync(Guid cpId, BIDatabase biDatabase)
{
// context is available through Dependency Injection (.net core)
var centralPoint = _context.CentralPoint.Where(cp => cp.ID == cpId)
.Include(cp => cp.BIDatabase)
.ThenInclude(biDb => biDb.Connection)
.FirstOrDefault();
centralPoint.BIDatabase = biDatabase;
await _context.SaveChangesAsync();
// more code ...
}
After this:
CentralPoint Table -> Remains Unchanged: Normal
BIDatabase Table -> Updated with new IDs: Normal
Connection Table -> // A new row is added instead of updating the old one
What I wanted is the connection entity to be updated and not added every time for the same database.

I think I see the issue.
When you replace centralPoint.BIDatabase = biDatabase; your new BIDatabase object probably doesnt have the Id linking it to Connection
Try mapping the fields you need onto the existing object instead of replacing it (or replace it, but map any Ids and existing fields that you want to be persisted through onto the new object first).

What code created this new biDatabase? Was it persisted to a dbContext prior to this method?
Your expectation around the BIDatabase and connection table is a bit suspicious. Given an existing CentralPoint /w BIDatabase /w Connection, if you "change" the CentralPoint's BIDatabase to a new one, the existing record is not updated, it is replaced. This means that the old record and it's associated connection would be removed, and a new BIDatabase /w a new Connection would be inserted. Assuming these entities are configured with DB generated keys:
Example:
CentralPoint(id:1) -> BIDatabase(id:1) -> Connection(id:1)
Create new BIDatabase (id:x) with new Connection (id:x) then:
CentralPoint.BIDatabase = newBIDatabase
BIDatabase(id:1) & Connection(id:1) are marked for deletion.
new BIDatabase & Connection will be persisted with new IDs when saved.
CentralPoint(id:1) -> BIDatabase(id:2) -> Connection(id:2)
If you want to replace the BIDatabase but keep the same connection ID/reference:
Create the new BIDatabase(id:x) /w Connection(id:x)
but before saving it to the context:
var centralPoint = _context.CentralPoint.Where(cp => cp.ID == cpId)
.Include(cp => cp.BIDatabase)
.ThenInclude(biDb => biDb.Connection)
.FirstOrDefault();
biDatabase.Connection = (centralPoint?.BIDatabase?.Connection) ?? biDatabase.Connection;
centralPoint.BIDatabase = biDatabase;
await _context.SaveChangesAsync();
What that extra statement does is look to preserve an existing connection already on the existing CentralPoint.BIDatabase by copying the reference over to the new BIDatabase. If the existing BIDatabase didn't have a connection (or the existing CP didn't have a BIDatabase) then the new connection created would be used. Provided you haven't saved the new BIDatabase to a dbContext prior to this code, the "new" Connection (id:x) will never get inserted where an existing one is substituted.

There were a couple of mistakes I was doing. I am detailing them so it might be helpful to others.
First mistake (pointed out by #Steveland83)
centralPoint.BIDatabase = biDatabase;
Since the CentralPoint entity is being tracked by the context (DbContext) we cannot simply replace a property by another. We need to modify the property by copying values.
So the following should be done instead
_context.Entry(centralPoint.BIDatabase).CurrentValues.SetVal‌​ues(biDatabase);
Second mistake
With the fix above, I was expecting the context to track all property changes in the centralPoint.BIDatabase and update those as well by itself (In my case the Connection property eg. centralPoint.BIDatabase.Connection)
However unfortunately this does not happen for non primitive types. You have to tell the context explicitly which of the non-primitive properties changed.
Here is the second fix
_context.Entry(centralPoint.BIDatabase).CurrentValues.SetVal‌​ues(biDatabase);
_context.Entry(centralPoint.BIDatabase.Connection).CurrentValues.SetVal‌​ues(biDatabase.Connection);
This Updates the BIDatabase and the Connection tables with the changes.

Related

Why is Entity Framework Core attempting to insert records into one of the tables from many to many relationships and NOT the join table?

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.

EntityFramework load / update Entities

i am struggeling for a while now to understand how EF loads / updates entities.
First of all i wanna explain what my app (WPF) is about. I am developing
an application where users can store Todo Items in Categories, these categories are predefined by the application. Each user can read all items but can only delete / update his own items. It's a multiuser system, means the application is running multiple times in the network accessing the same sql server database.
When a user is adding/deleting/updating items the UI on all the other running apps has to update.
My model looks like this:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public List<Todo> Todos { get; set; }
}
public class Todo
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime LastUpdate { get; set; }
public string Owner { get; set; }
public Category Category { get; set; }
public List<Info> Infos { get; set; }
}
public class Info
{
public int Id { get; set; }
public string Value { get; set; }
public Todo Todo { get; set; }
}
I am making the inital load like this, which works fine:
Context.dbsCategories.Where(c => c.Id == id).Include(c => c.Todos.Select(t => t.Infos)).FirstOrDefault();
Now i was trying to load only the Todos which are from the current user therefore i tried this:
Context.dbsCategories.Where(c => c.Id == id).Include(c => c.Todos.Where(t => t.Owner == Settings.User).Select(t => t.Infos)).FirstOrDefault();
This does not work because it's not possible to filter within include, so I tried this:
var cat = Context.dbsCategories.Where(c => c.Id == id).FirstOrDefault();
Context.dbsTodos.Where(t => t.Category.Id == cat.Id && t.Owner == Settings.User).Include(t=>t.Infos);
After executing the second line where i look for the Todo Items, these Items were automatically added to cat's Todos collection. Why? I would have expected that i have to add them manually to cat's Todos collection.
Just for my understanding what is EF doing here exactly?
Now to my main problem -> the synchronization of the data between database and client. I am using a long running Context which lives as long as the application is running to save changes to the database which are made on owned items. The user does not have the possibility to manipulate / delete data from other users this is guarantee by the user interface.
To synchronize the data i build this Synch Method which will run every 10 second, right now it's triggere manually.
Thats my synchronization Code, which only synchronizes Items to the client that do not belong to it.
private async Task Synchronize()
{
using (var ctx = new Context())
{
var database = ctx.dbsTodos().Where(x => x.Owner != Settings.User).Select(t => t.Infos).AsNoTracking();
var loaded = Context.dbsTodos.Local.Where(x => x.Owner != Settings.User);
//In local context but not in database anymore -> Detachen
foreach (var detach in loaded.Except(database, new TodoIdComparer()).ToList())
{
Context.ObjectContext.Detach(detach);
Log.Debug(this, $"Item {detach} detached");
}
//In database and local context -> Check Timestamp -> Update
foreach (var update in loaded.Intersect(database, new TodoIdTimeStampComparer()))
{
await Context.Entry(update).ReloadAsync();
Log.Debug(this, $"Item {update} updated");
}
//In database but not in local context -> Attach
foreach (var attach in database.ToList().Except(loaded, new TodoIdComparer()))
{
Context.dbsTodos().Attach(attach);
Log.Debug(this, $"Item {attach} attached");
}
}
}
I am having following problems / issues of unknow origin with it:
Detaching deleted Items seems to work, right now i am not sure if only the Todo Items are detached or also the Infos.
Updating Items works only for the TodoItem itsself, its not reloading the Infos within? How can i reload the whole entity with all it's relations?
I am thankful for every help on this, even if you are saying it's all wrong what i am doing here!
Attaching new Items and Infos does not work so far? What am i doing wrong here?
Is this the right approach to synchronize data between client and database?
What am i doing wrong here? Is there any "How to Sync" Tutorial? I have not found anything helpful so far?
Thanks!
My, you do like to deviate from entity framework code-first conventions, do you?
(1) Incorrect class definitions
The relations between your tables are Lists, instead of ICollections, they are not declared virtual and you forgot to declare the foreign key
There is a one-to-many relation between Todo and Category: every Todo belongs to exactly one Category (using a foreign key), every Category has zero or more Todos.
You choose to give Category a property:
List<Todo> Todos {get; set;}
Are you sure that category.Todos[4] has a defined meaning?
What would category.Todos.Insert(4, new Todo()) mean?
Better stick to an interface where you can't use functions that have no proper meaning in your database: use ICollection<Todo> Todos {get; set;}. This way you'll have only access to functions that Entity Framework can translate to SQL.
Besides, a query will probably be faster: you give entity framework the possibility to query the data in its most efficient way, instead of forcing it to put the result into a List.
In entity framework the columns of a table are represented by non-virtual properties; the virtual properties represent the relations between the tables (one-to-many, many-to-many)
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
... // other properties
// every Category has zero or more Todos (one-to-many)
public virtual ICollection<Todo> Todos { get; set; }
}
public class Todo
{
public int Id { get; set; }
public string Content { get; set; }
... // other properties
// every Todo belongs to exactly one Category, using foreign key
public int CategoryId { get; set }
public virtual Category Category { get; set; }
// every Todo has zero or more Infos:
public virtual ICollection<Info> Infos { get; set; }
}
You'll probably guess Info by now:
public class Info
{
public int Id { get; set; }
public string Value { get; set; }
... // other properties
// every info belongs to exactly one Todo, using foreign key
public int TodoId {get; set;}
public virtual Todo Todo { get; set; }
}
Three major improvements:
ICollections instead of Lists
ICollections are virtual, because it is not a real column in your table,
foreign key definitions non-virtual: they are real columns in your tables.
(2) Use Select instead of Include
One of the slower parts of a database query is the transport of the selected data from the Database Management System to your local process. Hence it is wise to limit the amount of transported data.
Suppose Category with Id [4] has a thousand Todos. Every Todo of this Category will have a foreign key with a value 4. So this same value 4 will be transported 1001 times. What a waste of processing power!
In entity framework use Select instead of Include to query data and select only the properties you actually plan to use. Only use Include if you plan to update the Selected data.
Give me all Categories that ... with their Todos that ...
var results = dbContext.Categories
.Where(category => ...)
.Select(category => new
{
// only select properties that you plan to use
Id = category.Id,
Name = category.Name,
...
Todos = category.Todos
.Where(todo => ...) // only if you don't want all Todos
.Select(todo => new
{
// again, select only the properties you'll plan to use
Id = todo.Id,
...
// not needed, you know the value:
// CategoryId = todo.CategoryId,
// only if you also want some infos:
Infos = todo.Infos
.Select(info => ....) // you know the drill by now
.ToList(),
})
.ToList(),
});
(3) Don't keep DbContext alive for such a long time!
Another problem is that you keep your DbContext open for quite some time. This is not how a dbContext was meant. If your database changes between your query and your update, you'll have troubles. I can hardly imagine that you query so much data that you need to optimize it by keeping your dbContext alive. Even if you query a lot of data, the display of this huge amount of data would be the bottle-neck, not the database query.
Better fetch the data once, dispose the DbContext, and when updating fetch the data again, update the changed properties and SaveChanges.
fetch data:
RepositoryCategory FetchCategory(int categoryId)
{
using (var dbContext = new MyDbContext())
{
return dbContext.Categories.Where(category => category.Id == categoryId)
.Select(category => new RepositoryCategory
{
... // see above
})
.FirstOrDefault();
}
}
Yes, you'll need an extra class RepositoryCategory for this. The advantage is, that you hide that you fetched your data from a database. Your code would hardly change if you'd fetch your data from a CSV-file, or from the internet. This is way better testable, and also way better maintainable: if the Category table in your database changes, users of your RepositoryCategory won't notice it.
Consider creating a special namespace for the data you fetch from your database. This way you can name the fetched Category still Category, instead of RepositoryCategory. You even hide better where you fetched your data from.
Back to your question
You wrote:
Now i was trying to load only the Todos which are from the current user
After the previous improvements, this will be easy:
string owner = Settings.User; // or something similar
var result = dbContext.Todos.Where(todo => todo.Owner == owner)
.Select(todo => new
{
// properties you need
})

A 'Required' Collection field could not be updated via `AddOrUpdate`

I'm using Asp.net web api 2 + entity framework 6.
Basically I have 2 models:
public class MyOrderModel
{
public int Id { get; set; }
public string OrderNumber { get; set;}
public string AuthCode { get; set; }
[Required]
public List<MyOrderDetailModel> Details { get; set; }
}
public class MyOrderDetailModel
{
public int Id { get; set; }
public decimal Amount{ get; set;}
}
After ran the Package Manager Console command Enable-Migration, in Configuration.Seed(WaynyCloudTest.Models.ApplicationDbContext context), I was trying to add some pre-loaded data:
context.MyOrderModels.AddOrUpdate(
s => s.OrderNumber,
new MyOrderModel
{
OrderNumber = "0001",
AuthCode = "ABCDE",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
};
After the first(initial) Update-database command, everything is fine and I can see above data persisted to database 2 tables.
Later, I want to update the AuthCode property value from ABCDE to ABCDEXXX,
the only change is the value assignment:
context.MyOrderModels.AddOrUpdate(
s => s.OrderNumber,
new MyOrderModel
{
OrderNumber = "0001",
// THE ONLY CHANGE!
AuthCode = "ABCDEXXX",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
};
I would expect the EntityFramework will find the target data row in database by searching condition on OrderNumber and then update the AuthCode, but now I always got this exception in Seed function:
Entity Validation Failed - errors follow:
MyTest.Models.MyOrderModel failed validation
Details : The Details field is required.
Obviously the value was supplied for field Details, so what I've missed?
The problem is with the Id field of PostPayQRCodeFuelOrderModel. In your environment, the database uses this field as an identity (primary key) field and wants to generate the value itself.
In your case, there is an easy workaround:
context.MyOrderModel.AddOrUpdate(
p => p.OrderNumber,
new PostPayQRCodeFuelOrderModel
{
OrderNumber = "00001",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
}
);
Assumably, OrderNumber is unique, so this should work fine. In addition, running the seed again will not duplicate this data.
UPDATE:
It is possible to keep the original MyOrderModel.AddOrUpdate(), i.e., give the Id explicitly:
First, you need to prevent the auto-generation of the primary key value for MyOrderModel:
public class MyOrderModel
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
Then, you need to re-create the MyOrderModel table. NOTE: the usual approach of adding a migration just to modify the Id field will not work, you need to re-create the table.
Running the seed multiple times works, i.e., there are no duplicates (just checked it myself).
UPDATE 2:
I don't have a full explanation, why your code is not working, but with the code below it should be possible for you to construct the creation and the update of the database objects as you wish.
This code can be run in Seed(), multiple times without duplication. The update of AuthCode is, of course, artificial, but my point was to separate the creation and the update (just in case these need to be separated in the final implementation).
The whole project is available in https://github.com/masa67/AspNetNg, branch SO34252506-1.
Here's the code:
var mom = context.MyOrderModel.Where(m => m.OrderNumber == "00001").FirstOrDefault();
if (mom == null)
{
mom = new PostPayQRCodeFuelOrderModel
{
OrderNumber = "00001",
AuthCode = "ABCDE",
Details = new List<MyOrderDetailModel>() {
new MyOrderDetailModel
{
Amount = 5.67M
}
}
};
context.MyOrderModel.AddOrUpdate(p => p.OrderNumber, mom);
context.SaveChanges();
}
mom.AuthCode = "ABCDEXXX";
context.SaveChanges();
UPDATE 3:
A couple of suggestions, if this is still not working:
Consider dropping the Required constraint for Details and handle the consistency programmatically. I would not use this constraint on navigation properties anyways (but I am only familiar with EF to the extent of how we are using it in our current project at work, so there might be different views on this).
Test your code by re-creating the database first.
Test my Solution (link above). It is working for me, so there might be a difference in configuration somewhere.
Since Details is not virtual, EF is not using lazy loading. I was expecting this to cause problems, as Details becomes null when the object is read from the database, but that was not the case in my environment. You might try eager loading, but I doubt if this has any impact:
Eager loading:
var mom = context.MyOrderModel.Where(m => m.OrderNumber == "00001").Include(m => m.Details).FirstOrDefault();
UPDATE 4:
If this is still not working, then delete the database, but re-create the migrations in addition:
Delete the existing migrations.
Do not let EF make the assumption that it knows the state of your database, but force it to create the migrations from scratch (see other SO questions for advice). However, BEFORE doing that, please do notice that this operation will most probably overwrite the Seed() function as well, so take a copy of that file before the operation.

Persisting Entity Framework nested/related entities

I have the following entities:
public class ModuleCriteria
{
public int ModuleCriteriaId { get; set; }
public string Criteria { get; set; }
public List<ModuleCriteriaLookup> ModuleCriteriaLookups { get; set;
}
}
public class ModuleCriteriaLookup
{
public int ModuleCriteriaLookupId { get; set; }
public int ModuleCriteriaId { get; set; } // ** (Foreign Key) **
public int SiteId { get; set; }
public string CategoryId { get; set; }
public ModuleCriteria ModuleCriteria { get; set; }
}
I have the following EF configuration in my Context class (edited for brevity):
protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
base.OnModelCreating( modelBuilder );
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<ModuleCriteriaLookup>().HasRequired( mc => mc.ModuleCriteria );
// I tried adding the below line but it made no difference.
//modelBuilder.Entity<ModuleCriteria>().HasMany( mc => mc.ModuleCriteriaLookups );
}
...and I have the following DbSet properties defined in my Context class:
public DbSet<ModuleCriteria> ModuleCriteria { get; set; }
public DbSet<ModuleCriteriaLookup> ModuleCriteriaLookup { get; set; }
I have a CriteriaRepository class, which has a Save method, for persisting changes for my ModuleCriteria entities:
public void Save( ModuleCriteria moduleCriteria )
{
using ( var ctx = new MyAppContext() )
{
ctx.Entry( moduleCriteria ).State = moduleCriteria.ModuleCriteriaId == 0 ? EntityState.Added : EntityState.Modified;
ctx.SaveChanges();
}
}
A ModuleCriteria object can exist without a ModuleCriteriaLookup object, but ModuleCriteriaLookup object has to relate to an existing ModuleCriteria object (related on ModuleCriteriaId).
You can have multiple ModuleCriteraLookup objects all relating to the same one ModuleCriteria object.
The behaviour that I would like, and expect with EF, is:
1) If I create a new ModuleCriteria object (without any ModuleCriteriaLookups), call the Save method in my repository, I would expect to see new ModuleCriteria record in the db with no related ModuleCriteriaLookup records in the db.
2) If I create a new ModuleCriteria object and assign a List<ModuleCriteriaLookup> to it, call the Save method in my repository, I would expect to see new ModuleCriteria record in the db and x new ModuleCriteriaLookup rows in the db which relate to that particular ModuleCriteria.
3) If I add/edit/remove any of the ModuleCriteriaLookup objects that related to one of my ModuleCriteria objects, then call the Save method in my repository, I would expect to see any of the ModuleCriteria's deleted ModuleCriteriaLookup objects to get removed from the db, any new ones added and any edited ones simply to get updated.
So all I ever need worry about is that whatever the ModuleCriteria.ModuleCriteriaLookups property contains for a given ModuleCriteria, that's what will be reflected in the 2 tables in my DB by simply calling the Save method for the ModuleCriteria object in my repository.
Unfortunately at the moment, if I'm adding a new ModuleCriteria object with associated List<ModuleCriteriaLookup> it adds both ModuleCriteria and x ModuleCriteriaLookup rows in the db nicely. But when I want to edit or delete entries in the ModuleCriteria.ModuleCriteriaLookups property, this is not being reflected in the db. Nothing is happening with the ModuleCriteriaLookups rows.
I'm not sure where exactly the problem is, whether its whether the EF mapping configuration, or something to do with how the repository works?
The problem is located in the repository. The DbContext needs to be aware of the existence of entities. So when editing and/or deleting entities the entities need to be fetched from the database first.
This description clearly states:
The .Entry property returns objects from the context that are being
tracked by the context.
Because you directly use this properties right after creating the context, the context isn't tracking these entities and is therefore not aware that something has changed. And thereby is unable to generate the correct SQL statements.
There are several ways to deal with this, depending on the rest of your design.
One way to delete it would be:
public void DeleteModuleCriteriaLookup(ModuleCriteriaLookup[] lookups)
{
using (var ctx = new MyAppContext())
{
var moduleCriteriaId = lookups.First().ModuleCriteriaId;
var moduleCritria = (
from criteria in ctx.ModuleCriteria
where criteria.ModuleCriteriaId == moduleCriteriaId
select criteria
).Single();
var lookupIdsToDelete = lookups.Select(l => l.ModuleCriteriaLookupId);
var lookupsToDelete = (
from lookup in moduleCritria.ModuleCriteriaLookups
where lookupIdsToDelete.Contains(lookup.ModuleCriteriaLookupId)
select lookup
).ToArray();
foreach (var lookup in lookupsToDelete)
{
moduleCritria.ModuleCriteriaLookups.Remove(lookup);
}
ctx.SaveChanges();
}
}

Entity Framework 6: Adding child object to parent's list vs. setting child's navigation property to parent

I have an existing database with two tables MailServers and MailDomains in it. MailDomains has the foreign key column MailServerId pointing to the Id primary key column in MailServers. So we have a one-to-many-relationship here.
I followed this article and created my Entity Framework POCOs via the "Code first from database" model in the Entity Data Model Wizard. This produced the following two C# classes:
public partial class MailServer
{
public MailServer()
{
MailDomains = new HashSet<MailDomain>();
}
public int Id { get; set; }
public virtual ICollection<MailDomain> MailDomains { get; set; }
}
public partial class MailDomain
{
public MailDomain()
{
}
public int Id { get; set; }
public string DomainName { get; set; }
public int MailServerId { get; set; }
public virtual MailServer MailServer { get; set; }
}
Now my question is whether there is any difference between the following two approaches of creating and inserting new objects to the database.
Approach (A): Adding new child to the parent's list:
var mailServer = new MailServer();
var mailDomain = new MailDomain() {
DomainName = "foobar.net",
};
mailServer.MailDomains.Add(mailDomain);
using(var context = new MyContext){
context.MailServers.Add(mailServer);
context.SaveChanges();
}
Approach (B): Setting the child's navigation property to the parent:
var mailServer = new MailServer();
var mailDomain = new MailDomain() {
DomainName = "foobar.net",
MailServer = mailServer,
};
using(var context = new MyContext){
context.MailDomains.Add(mailDomain);
context.SaveChanges();
}
I also assume that in approach (A) the new MailDomain instance is automatically added to the collection context.MailDomains while in approach (B) the new MailServer instance is automatically added to the collection context.MailServers. Is that correct or do I have to do that manually?
So again, my question is: are the two approaches interchangeable?
It just confuses me that in the database there is only one property/column to set (namely the foreign key in MailDomains) while in the C# code there are two properties (one in each class) that could be modified.
Yes, the two approaches are interchangeable. This allows you to create and save your object graph to the database from either the perspective of the MailServer or the MailDomain.
If you do code-first, you have the option of removing the properties and mappings if they're not needed.
I also assume that in approach (A) the new MailDomain instance is
automatically added to context.MailDomains while in approach (B) the
new MailServer instance is automatically added to context.MailServers.
Is that correct or do I have to do that manually?
It depends what you mean by "added to the context". If you mean: does it automatically get saved to the database when you persist, the answer is yes. One of the big benefits to using an ORM like EF is that it handles saving a full object graph automatically (and syncing PK/FK relations, etc.).
If you mean: will the entity be available via the context before saving, I don't think so (I'm not 100% sure).

Categories

Resources