Asking same question differently!
Its seems clear I need to elaborate on this question because I have no viable responses.
Based on this AutoMapper registration code:
Mapper.Initialize(cfg =>
{
cfg.AddCollectionMappers();
cfg.SetGeneratePropertyMaps<GenerateEntityFrameworkPrimaryKeyPropertyMaps<DbContext>>();
});
AutoMapper adds support for "updating" DbSet collections using this line:
Mapper.Map<List<DTO>, List<Entity>>(dtoCollection, entityCollection);
Saving changes through an open context should result in updating the database:
using (var context = factory.CreateContext())
{
Mapper.Map<List<DTO>, List<Entity>>(dtoCollection, await
context.dbSet.TolistAsync());
await context.SaveChangesAsync();
}
This does nothing!
So back to my original question. If calling the mapper with the dto and current state of the entity collection returns an updated entity collection based on the comparison Mapping Created here:
cfg.SetGeneratePropertyMaps<GenerateEntityFrameworkPrimaryKeyPropertyMaps<DbContext>>();
produces entity collection here:
var entities = Mapper.Map<List<DTO>, List<Entity>>(dtoCollection, await
context.dbSet.TolistAsync());
Am I support to iterate the new collection and update EF manually using this new collection? Its not clear what I am suppose to do at this point? Is this what I am suppose to do with the resulting collection?
// map dto's to entities
var entities = Mapper.Map(collection, await dbSet.ToListAsync());
// add new records
var toAdd = entities.Where(e => e.Id == 0);
dbSet.AddRange(toAdd);
// delete old records
var toDelete = entities.Where(entity => collection.All(e => e.Id > 0 && e.Id != entity.Id));
dbSet.RemoveRange(toDelete);
// update existing records
var toUpdate = entities.Where(entity => collection.All(i => i.Id > 0 && i.Id == entity.Id)).ToList();
foreach (var entity in toUpdate)
{
context.Entry(entity).State = EntityState.Modified;
}
await context.SaveChangesAsync();
This is my original question. If so it seems redundant. So I feel like I am missing something.
I appreciate some useful feedback. Please help!
Thanks
EF DbSets are not collections. Basically they represent a database table and provide query and DML operations for it.
Looks like you want to synchronize the whole table with the DTO list. You can do that by loading the whole table locally using the Load or LoadAsync methods, and then Map the DTO collection to the entity DbSet.Local property. The difference with your attempts is that the Local property is not a simple list, but observable collection directly bound to the context local store and change tracker, so any modification (Add, Remove) will be applied to the database.
Something like this:
await dbSet.LoadAsync();
Mapper.Map(dtoCollection, dbSet.Local);
await context.SaveChangesAsync();
Related
I want to use bulk delete feature of ef core 7. I know we can give id and use executeDelete() like this _context.Set<T>().Where(t=>t.Id == id).ExecuteDeleteAsync().
But what if I want to delete a collection of entities? How can I complete this method?
public async Task BulkDeleteAsync(ICollection<T> entities)
{
await _context.Set<T>()......ExeuteDeleteAsync();
}
Should I use where? How?
Depended on number of entities you can try using collection of ids (there can be limit on number of parameters to be passed depended on the database provider):
var ids = entities.Select(e => e.Id)
.ToList();
await _context.Set<T>()
.Where(e => ids.Contains(e.Id))
.ExecuteDeleteAsync();
I have a disconnected scenario where I'm pulling data from a database, edit it and save it back.
I use two methods, one to get data and the other to save it.
Methods are as follows:
public async Task<IEnumerable<MasterObject>> GetDataAsync(int someId, CancellationToken ct)
{
await using var context = new dbContext(_connectionString);
return await context
.MasterObject
.Include(x => x.NavigationOne)
.Include(x => x.NawigationTwo)
.Include(x => x.NavigationThree)
.ThenInclude(x => x.SubNavigation)
.Where(x => x.NavigationOne.SomeId == someId)
.AsNoTracking()
.ToListAsync(ct)
.ConfigureAwait(false);
}
public async Task SaveMasterObjectAsync(MasterObject masterObject)
{
await using var context = new dbContext(_connectionString);
context.DailyPlan.Update(masterObject);
await context.SaveChangesAsync().ConfigureAwait(false);
}
Every time I call the GetDataAsync method and want to save one of the returned entries, I get an error:
System.InvalidOperationException
The instance of entity type 'SubNavigation' cannot be tracked because another instance with the same key value for {'SubNavigation'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
You can clear tracked entities in EF Core before updating
public async Task SaveMasterObjectAsync(MasterObject masterObject)
{
await using var context = new dbContext(_connectionString);
var changedEntries = context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added ||
e.State == EntityState.Modified ||
e.State == EntityState.Deleted)
.ToList();
foreach (var entry in changedEntries)
entry.State = EntityState.Detached;
context.DailyPlan.Update(masterObject);
await context.SaveChangesAsync();
}
OK, I found where is the problem. In my ViewModel I have two lists, one contains all the entities and second is used to filter the first list and display to DataGrid via Binding. When I loaded entities directly to the second list, other words DataGrid is Bind directly to the list that holds the data, then I can perform Save method.
Another discovery is that when UI is running then problem occur. When I force to run method before UI is initialized everything works fine.
Thanks!
In entity framework core 2.0, I have many-many relationship between Post and Category (the binding class is PostCategory).
When the user updates a Post, the whole Post object (with its PostCategory collection) is being sent to the server, and here I want to reassign the new received Collection PostCategory (the user may change this Collection significantly by adding new categories, and removing some categories).
Simplified code I use to update that collection (I just assign completely new collection):
var post = await dbContext.Posts
.Include(p => p.PostCategories)
.ThenInclude(pc => pc.Category)
.SingleOrDefaultAsync(someId);
post.PostCategories = ... Some new collection...; // <<<
dbContext.Posts.Update(post);
await dbContext.SaveChangesAsync();
This new collection has objects with the same Id of objects in the previous collection (e.g. the user removed some (but not all) categories). Because of the, I get an exception:
System.InvalidOperationException: The instance of entity type 'PostCategory' cannot be tracked because another instance with the same key value for {'CategoryId', 'PostId'} is already being tracked.
How can I rebuild the new collection (or simply assign a new collection) efficiently without getting this exception?
UPDATE
The answer in this link seems to be related to what I want, but it is a good and efficient method? Is there any possible better approach?
UPDATE 2
I get my post (to edit overwrite its values) like this:
public async Task<Post> GetPostAsync(Guid postId)
{
return await dbContext.Posts
.Include(p => p.Writer)
.ThenInclude(u => u.Profile)
.Include(p => p.Comments)
.Include(p => p.PostCategories)
.ThenInclude(pc => pc.Category)
.Include(p => p.PostPackages)
.ThenInclude(pp => pp.Package)
//.AsNoTracking()
.SingleOrDefaultAsync(p => p.Id == postId);
}
UPDATE 3 (The code in my controller, which tries to update the post):
var writerId = User.GetUserId();
var categories = await postService.GetOrCreateCategoriesAsync(
vm.CategoryViewModels.Select(cvm => cvm.Name), writerId);
var post = await postService.GetPostAsync(vm.PostId);
post.Title = vm.PostTitle;
post.Content = vm.ContentText;
post.PostCategories = categories?.Select(c => new PostCategory { CategoryId = c.Id, PostId = post.Id }).ToArray();
await postService.UpdatePostAsync(post); // Check the implementation in Update4.
UPDATE 4:
public async Task<Post> UpdatePostAsync(Post post)
{
// Find (load from the database) the existing post
var existingPost = await dbContext.Posts
.SingleOrDefaultAsync(p => p.Id == post.Id);
// Apply primitive property modifications
dbContext.Entry(existingPost).CurrentValues.SetValues(post);
// Apply many-to-many link modifications
dbContext.Set<PostCategory>().UpdateLinks(
pc => pc.PostId, post.Id,
pc => pc.CategoryId,
post.PostCategories.Select(pc => pc.CategoryId)
);
// Apply all changes to the db
await dbContext.SaveChangesAsync();
return existingPost;
}
The main challenge when working with disconnect link entities is to detect and apply the added and deleted links. And EF Core (as of the time of writing) provides little if no help to do that.
The answer from the link is ok (the custom Except method is too heavier for what it does IMO), but it has some traps - the existing links has to be retrieved in advance using the eager / explicit loading (though with EF Core 2.1 lazy loading that might not be an issue), and the new links should have only FK properties populated - if they contain reference navigation properties, EF Core will try to create new linked entities when calling Add / AddRange.
A while ago I answered similar, but slightly different question - Generic method for updating EFCore joins. Here is the more generalized and optimized version of the custom generic extension method from the answer:
public static class EFCoreExtensions
{
public static void UpdateLinks<TLink, TFromId, TToId>(this DbSet<TLink> dbSet,
Expression<Func<TLink, TFromId>> fromIdProperty, TFromId fromId,
Expression<Func<TLink, TToId>> toIdProperty, IEnumerable<TToId> toIds)
where TLink : class, new()
{
// link => link.FromId == fromId
Expression<Func<TFromId>> fromIdVar = () => fromId;
var filter = Expression.Lambda<Func<TLink, bool>>(
Expression.Equal(fromIdProperty.Body, fromIdVar.Body),
fromIdProperty.Parameters);
var existingLinks = dbSet.AsTracking().Where(filter);
var toIdSet = new HashSet<TToId>(toIds);
if (toIdSet.Count == 0)
{
//The new set is empty - delete all existing links
dbSet.RemoveRange(existingLinks);
return;
}
// Delete the existing links which do not exist in the new set
var toIdSelector = toIdProperty.Compile();
foreach (var existingLink in existingLinks)
{
if (!toIdSet.Remove(toIdSelector(existingLink)))
dbSet.Remove(existingLink);
}
// Create new links for the remaining items in the new set
if (toIdSet.Count == 0) return;
// toId => new TLink { FromId = fromId, ToId = toId }
var toIdParam = Expression.Parameter(typeof(TToId), "toId");
var createLink = Expression.Lambda<Func<TToId, TLink>>(
Expression.MemberInit(
Expression.New(typeof(TLink)),
Expression.Bind(((MemberExpression)fromIdProperty.Body).Member, fromIdVar.Body),
Expression.Bind(((MemberExpression)toIdProperty.Body).Member, toIdParam)),
toIdParam);
dbSet.AddRange(toIdSet.Select(createLink.Compile()));
}
}
It uses a single database query to retrieve the exiting links from the database. The overhead are few dynamically built expressions and compiled delegates (in order to keep the calling code simplest as possible) and a single temporary HashSet for fast lookup. The performance affect of the expression / delegate building should be negligible, and can be cached if needed.
The idea is to pass just a single existing key for one of the linked entities and list of exiting keys for the other linked entity. So depending of which of the linked entity links you are updating, it will be called differently.
In you sample, assuming you are receiving IEnumerable<PostCategory> postCategories, the process would be something like this:
var post = await dbContext.Posts
.SingleOrDefaultAsync(someId);
dbContext.Set<PostCategory>().UpdateLinks(pc =>
pc.PostId, post.Id, pc => pc.CategoryId, postCategories.Select(pc => pc.CategoryId));
await dbContext.SaveChangesAsync();
Note that this method allows you to change the requirement and accept IEnumerable<int> postCategoryIds:
dbContext.Set<PostCategory>().UpdateLinks(pc =>
pc.PostId, post.Id, pc => pc.CategoryId, postCategoryIds);
or IEnumerable<Category> postCategories:
dbContext.Set<PostCategory>().UpdateLinks(pc =>
pc.PostId, post.Id, pc => pc.CategoryId, postCategories.Select(c => c.Id));
or similar DTOs / ViewModels.
Category posts can be updated in a similar manner, with corresponding selectors swapped.
Update: In case you a receiving a (potentially) modified Post post entity instance, the whole update procedure cold be like this:
// Find (load from the database) the existing post
var existingPost = await dbContext.Posts
.SingleOrDefaultAsync(p => p.Id == post.Id);
if (existingPost == null)
{
// Handle the invalid call
return;
}
// Apply primitive property modifications
dbContext.Entry(existingPost).CurrentValues.SetValues(post);
// Apply many-to-many link modifications
dbContext.Set<PostCategory>().UpdateLinks(pc => pc.PostId, post.Id,
pc => pc.CategoryId, post.PostCategories.Select(pc => pc.CategoryId));
// Apply all changes to the db
await dbContext.SaveChangesAsync();
Note that EF Core uses separate database query for eager loading related collecttions. Since the helper method does the same, there is no need to Include link related data when retrieving the main entity from the database.
I try to reach the following:
Add a new entity to the DbContext's dataset.
Do no execute SaveChanges yet, as in case of any error after insert, the new entry should not go to database
However I need to search later if the newly entry is in the Customer collection
The following what I have now:
public virtual DbSet<Customer> Customers { get; set; }
_context.Customers.Add(customer);
// NO _context.SaveChanges();
// ...
var existsingUser = _context.Customers
.FirstOrDefault(x => x.CodeCustomer == customer.CodeCustomer);
// existsingUser = null, and should be valid populated object
If I execute SaveChanges, the existsingUser is not null anymore, but the data is saved to database.
_context.Customers.Add(customer);
_context.SaveChanges();
// ...
var existsingUser = _context.Customers
.FirstOrDefault(x => x.CodeCustomer == customer.CodeCustomer);
// existsingUser = has object
Is there any possible solution to 'Really' add the customer to the Customers collection, without saveChanges()?
If so, please let me know,
You can check the Local property. From the docs:
This property returns an ObservableCollection that contains all Unchanged, Modified, and Added objects that are currently tracked by the context for the given DbSet.
For example:
var existsingUser = _context.Customers
.Local
.FirstOrDefault(x => x.CodeCustomer == customer.CodeCustomer);
It's impossible to add to collection without SaveChanges(). _context.Customers.Add(customer) means that you mark this object to add to the database. Under the cabin EF context works like a transaction. You can make plenty of operation but only SaveChanges() save to database.
_context.Customers.FirstOrDefault requests data from database.
I hope it helps you.
I'm using Entity Framework from a couple of years and I have a little problem now.
I add an entity to my table, with
Entities.dbContext.MyTable.Add(obj1);
and here ok.
Then, I'd like to make a query on MyTable, like
Entities.dbContext.MyTable.Where(.....)
The code above will query on my MyTable in the db.
Is there a way to query also on the just added value, before the saveChanges? (obj1) How?
UPDATE
Why do I need this? Because, for each new element I add, I need to edit some values in the previous and the next record (there is a datetime field in this table)
UPDATE2
Let's say I have to add a lot of objects, but I call the saveChanges only after the last item is added. Every time I add the new item, I read its datetime field and I search in the database the previous and the next record. Here, I edit a field of the previous and of the next record. Now, here is problem: if I insert another item, and, for example, the next item is "Obj1", I have to find and edit it, but I can't find it since I haven't saved my changes. Is it clearer now?
You should be able to get your added entities out of the dbContext via the change tracker like this:
var addedEntities = dbContext.ChangeTracker.Entries()
.Where(x => x.State == EntityState.Added && x.Entity is Mytable)
.Select(x => x.Entity as MyTable)
.Where(t => --criteria--);
Or using the type testing with pattern matching in c# 7.0:
var addedEntities = dbContext.ChangeTracker.Entries()
.Where(x => x.State == EntityState.Added && x.Entity is Mytable t && --test t for criteria--)
.Select(x => x.Entity as MyTable);
because you are only querying added entities, you can combine this with
dbContext.MyTable.Where(t => --criteria--).ToList().AddRange(addedEntities);
to get all of the relevant objects
I think this is a good situation for Transactions. I am going to assume you are using EF 6 since you did not provide a version. =)
UPDATE2 changes
public void BulkInsertObj(List<TEntity> objList)
{
using (var context = new dbContext())
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
foreach(var obj1 in objList)
{
dbContext.MyTable.Add(obj1);
//obj1 should be on the context now
var previousEntity = dbContext.MyTable.Where(.....) //However you determine this
previousEntity.field = something
var nextEntity = dbContext.MyTable.Where(.....) //However you determine this
nextEntity.field = somethingElse
}
context.SaveChanges();
dbContextTransaction.Commit();
}
catch (Exception)
{
dbContextTransaction.Rollback();
}
}
}
}
MSDN EF6 Transactions