Entity Framework attach trouble in many to many update scenario - entity-framework

I have a scenario where I wish to update a movie entity and its many to many relationship with Genres.
The navigation property Genres in movie contains stub Genre objects that only contain the GenreID because I want to save querying the DB for all genres.
See the code below, its fairly self explanatory. The problem is that I need to attach my "Stub" genres to the context so the EF will only modify the M:M relationship and not try to create new Genre records. This causes an error if a genre is already attached to the context from when I load the movie's current genres. It can't track multiple objects with the same keys.
How should this be handled? Is there a way to check if the context is already tracking the entity, or is there a better solution to this problem?
The code is below:
public override void Update(Movie movie)
{
//Backup new genres before clearing genres from movie
var newGenres = movie.Genres;
movie.Genres = new List<Genre>();
_ctx.Entry(movie).State = System.Data.EntityState.Modified;
//Load movie's current genres from DB and remove them
_ctx.Entry(movie).Collection(m => m.Genres).Load();
movie.Genres.Clear();
//Add new genres to movie
foreach (var genre in newGenres)
{
_ctx.Genres.Attach(genre); //Problem if genre already attached
movie.Genres.Add(genre);
}
}
Thanks for any help.

You could try to avoid to attach the stubs if an entity with the same key is already attached:
public override void Update(Movie movie)
{
//Backup new genres before clearing genres from movie
var newGenres = movie.Genres;
movie.Genres = new List<Genre>();
_ctx.Entry(movie).State = System.Data.EntityState.Modified;
//Load movie's current genres from DB and remove them
ctx.Entry(movie).Collection(m => m.Genres).Load();
var currentGenres = movie.Genres.ToList();
movie.Genres.Clear();
//Add new genres to movie
foreach (var genre in newGenres)
{
var currentGenre = currentGenres.SingleOrDefault(g => g.Id == genre.Id);
if (currentGenre != null)
movie.Genres.Add(currentGenre);
else
{
_ctx.Genres.Attach(genre);
movie.Genres.Add(genre);
}
}
}

Related

Update an already tracked entities

I want to update the itemsToUpdate collection.
This collection is already used in a query thus the resulting entities are already tracked in the context local property.
What is the most efficient way of overriding properties of the context.items.Local property from the itemsToUpdate collection?
private async Task<IEnumerable<item>> GetitemsAsync(IEnumerable<item> itemIds)
{
return await context.items.Where(t => itemIds.Select(x => x.Id).Contains(t.Id)).ToListAsync();
}
public async Task Update(...)
{
// Update
var queryUpdateitems = await GetitemsAsync(itemsToUpdate);
bool canUpdate = queryUpdateitems.All(t => t.UserId == userId);
if (!canUpdate)
{
throw new NotAuthorizedException();
}
else
{
// update here the itemsToUpdate collection
}
context.SaveChangesAsync();
}
In your case, you know that you have to update all these items, you just want to make sure that current user can update all items (by comparing Item.UserId). Instead of fetching all the existing items from database to make the check, you can query database to give result of the check and then you can just send update to database if check is true.
var itemIds = itemsToUpdate.Select(x => x.Id).ToList();
var canUpdate = await db.Blogs.Where(b => itemIds.Contains(b.Id)).AllAsync(t => t.UserId == userId);
if (canUpdate)
{
db.UpdateRange(itemsToUpdate);
}
else
{
throw new NotSupportedException();
}
await db.SaveChangesAsync();
Here, you have to make list of itemIds first because EF cannot inline list of items in a query and will do evaluation on client otherwise. That means EF is fetching whole table. Same is true for your GetitemsAsync method. It also queries whole table. Consider creating itemIds locally in that method too.
Once you pass in List<int> in the method EF will be happy to inline it in query and for query of canUpdate it will sent single query to database and fetch just true/false from database. Then you can use UpdateRange directly since there are nomore tracking records. Since it does not fetch all items from database, it will be faster too.

How to obtain grandchildren from grandparent primary key in Entity Framework

In Entity Framework, three entities have 1 to many relationships as grandparent, children and grandchildren. How do you obtain an object list of all grandchildren given the grandparent's primary key?
Thank you,
Newby to EF
Well if you have two one-to-many relationships between those three entities, I guess you could do something like this:
int grandparentId=1;
using(var ctx=new YourContext())
{
var grandparent=ctx.GrandParents.FirstOrDefault(gp=>gp.Id==grandparentId);
if(grandparent!=null)
{
// a list with all the grandchildren
var grandchildren=grandparent.Children.SelectMany(c=>c.GrandChildren).ToList();
}
}
And, if you are not using lazy loading then you need to use the Include extension method:
int grandparentId=1;
using(var ctx=new YourContext())
{
var grandparent=ctx.GrandParents.Include(gp=>gp.Children.Select(c=>c.GrandChildren)).FirstOrDefault(gp=>gp.Id==grandparentId);
if(grandparent!=null)
{
// a list with all the grandchildren
var grandchildren=grandparent.Children.SelectMany(c=>c.GrandChildren).ToList();
}
}
But, as #ErikPhilips said, you need to give more information about your model. Without that information it's difficult to give a concrete answer to your real problem.

EF 6 - many-to-many - Join table without duplicates

I'm using EF6 have some confusion on seeding a many to many relationship.
I have the following:
A User has many saved ChartQueries (that they can execute to get a chart).
A ChartQuery typically belongs to only one user, but there are several "shared" ChartQuerys that every User can execute. As a result I set up a many to many relationship using a join table UserChartQuery. The tables are up in the database just fine at 1-to-many on each side of the join table.
However, I'm not quite understanding how to seed or use this relationship. I don't want to end up with several duplicates of the "shared" ChartQuerys (a duplicate for each User). Instead, there should only be a single row for each "shared" ChartQuery that is a part of each User's SavedChartQueries collection (along with other, non-shared ChartQuerys that belong to that User only).
It seems like I'm forced to duplicate for each user:
var sharedChartQuery = new ChartQuery { ... };
var nonSharedChartQuery = new ChartQuery { ... };
var userOneChartQueryOne = new UserChartQuery { User = userOne, ChartQuery = sharedChartQuery };
var userTwoChartQueryOne = new UserChartQuery { User = userTwo, ChartQuery = sharedChartQuery };
var userTwoChartQueryTwo = new UserChartQuery { User = userTwo, ChartQuery = nonSharedChartQuery };
context.UserChartQueries.Add(userOneChartQueryOne);
context.UserChartQueries.Add(userOneChartQueryTwo);
context.UserChartQueries.Add(userTwoChartQueryTwo);
So first of all is this the right way to seed (through UserChartQueries table directly) or should I seed each User's SavedChartQueries navigation property?
And will this result in duplicate sharedChartQuery in the join table for each User? If so is there any way to avoid this?
Ok I understand how this works now. The following works as expected:
var userOne = new User {};
var userTwo = new User {};
var chartQuery = new ChartQuery { };
context.Users.Add(userOne);
context.Users.Add(userTwo);
context.UserChartQueries.Add(new UserChartQuery { User = userOne, ChartQuery = chartQuery });
context.UserChartQueries.Add(new UserChartQuery { User = userTwo, ChartQuery = chartQuery });
context.ChartQueries.Add(chartQuery);
The last line adds it to the table where the record actually resides. Checking the join table in SSMS shows that it only holds the foreign keys and nothing else. There are no duplicates.

how do I return entities from delimited list of Ids in EF5 Code First

I want to hydrate a collection of entities by passing in a comma delimited list of Ids using EF5 Code First.
I would previously have created a table function in t-sql, passed in the comma delimited list of Ids, I'd then join this table to the target table and return my collection of records.
What is the most performant way of using EF5 Code First to achieve the same?
Update: I want to avoid having the full set of entities in memory first.
Update2: I'd ideally like the order of the entities to match the delimited list.
I'd say to start out by converting the comma delimited list into a List<int> that contains all of the IDs that you would be going for. At that point, using your particular DbContext class you would do the following:
var entities = db.MyEntities.Where(e => myListOfIds.Contains(e.ID)).ToList();
Note: I only put the ToList at the end there because you were talking about hydrating the collection. Otherwise, with IEnumerable, there will be deferred execution of the query, and so it will not populate right away.
You could do it like this, where you restrict the set of Entity objects by checking if their IDs belong to your list of IDs:
// Dummy list of POCO 'entity' objects (i.e. the Code first objects) just for the sake of this snippet
var entities = new List<Entity>();
entities.Add(new Entity() { ID = 1, Title = "Ent1" });
entities.Add(new Entity() { ID = 2, Title = "Ent2" });
entities.Add(new Entity() { ID = 3, Title = "Ent3" });
// List of ids to match
var ids = new List<int>();
ids.Add(1);
ids.Add(2);
// LINQ:
var selected = (from e in entities where ids.Contains(e.ID) select e).ToList();
Just for completeness, this is the dummy class used above:
// POCO (Code first) object
private class Entity
{
public int ID { get; set; }
public string Title { get; set; }
}

Selecting & Updating Many-To-Many in Entity Framework 4

I am relatively new to the EF and have the following entity model above which consists of an Asset and a Country. An Asset can belong in many countries and thus has a many-to-many relationship with country (has a join table in the database with two fields both as primary keys).
I want to be able to do the following:
Firstly when I retrieve an asset (or assets) from the model I want to get the respective countries that its associated with. I would then like to be able to bind the countries list to an IEnumerable. Retrieving the countries in this way provides me with an EntityCollection of country objects which has extension method for ToList(). Therefore not sure If I am going down the right avenue with this one. Here is my GetAll method:
public IEnumerable<Asset> GetAll()
{
using (var context = CreateAssetContext())
{
var assetEntities = context.Assets.Include("Countries").ToList();
return AssetMapper.FromEntityObjects(assetEntities);
}
}
Secondly I want to be able to select a list of countries where the AssetId == some value.
Finally I want to be able to update the list of countries for a given Asset.
Many thanks.
Firstly when I retrieve an asset (or assets) from the model I want to
get the respective countries that its associated with. I would then
like to be able to bind the countries list to an IEnumerable.
Not sure if I understand that correctly, but EntityCollection<T> implements IEnumerable<T>, so you don't have to do anything special, you just can use Asset.Countries after you have loaded the assets including the countries.
Secondly I want to be able to select a list of countries where the
AssetId == some value.
using (var context = CreateAssetContext())
{
var countries = context.Countries
.Where(c => c.Assets.Any(a => a.AssetId == givenAssetId))
.ToList();
}
Or:
using (var context = CreateAssetContext())
{
var countries = context.Assets
.Where(a => a.AssetId == givenAssetId)
.Select(a => a.Countries)
.SingleOrDefault();
}
The second option is OK (not sure if it's better than the first from SQL viewpoint) because AssetId is the primary key, so there can be only one asset. For querying by other criteria - for example Asset.Name == "XYZ" - where you could expect more than one asset I would prefer the first option. For the second you had to replace Select by SelectMany and SingleOrDefault by ToList and use Distinct to filter out possible duplicated countries. The SQL would probably be more complex.
Finally I want to be able to update the list of countries for a given
Asset.
This is more tricky because you need to deal with the cases: 1) Country has been added to asset, 2) Country has been deleted from asset, 3) Country already related to asset.
Say you have a list of country Ids ( IEnumerable<int> countryIds ) and you want to relate those countries to the given asset:
using (var context = CreateAssetContext())
{
var asset = context.Assets.Include("Countries")
.Where(a => a.AssetId == givenAssetId)
.SingleOrDefault();
if (asset != null)
{
foreach (var country in asset.Countries.ToList())
{
// Check if existing country is one of the countries in id list:
if (!countryIds.Contains(country.Id))
{
// Relationship to Country has been deleted
// Remove from asset's country collection
asset.Countries.Remove(country);
}
}
foreach (var id in countryIds)
{
// Check if country with id is already assigned to asset:
if (!asset.Countries.Any(c => c.CountryId == id))
{
// No:
// Then create "stub" object with id and attach to context
var country = new Country { CountryId = id };
context.Countries.Attach(country);
// Then add to the asset's country collection
asset.Countries.Add(country);
}
// Yes: Do nothing
}
context.SaveChanges();
}
}
Edit
For the price of a second roundtrip to the database you can probably use this simpler code:
using (var context = CreateAssetContext())
{
var asset = context.Assets.Include("Countries")
.Where(a => a.AssetId == givenAssetId)
.SingleOrDefault();
if (asset != null)
{
// second DB roundtrip
var countries = context.Countries
.Where(c => countryIds.Contains(c.CountryId))
.ToList();
asset.Countries = countries;
context.SaveChanges();
}
}
EF's change detection should recognize which countries have been added or deleted from the asset's country list. I am not 100% sure though if the latter code will work correctly.
Whats the specific question here? Are you not being able to do that?
do you want to select the countries in an asset or the countries that have a certain asset?
to update its simple, just change stuff and then context.SaveChanges() will commit to the database.