Updating a many to many collection with EF code first - asp.net-mvc

I'm using EF Code First with the following configuration
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserProfile>()
.HasMany(x => x.TeamLeaders)
.WithMany()
.Map(m => m.MapLeftKey("UserId")
.MapRightKey("TeamLeaderId")
.ToTable("UserTeamLeaders"));
}
TeamLeaders is an ICollection of Users i.e. it is a self referencing many-many relationship.
In plain English, a user can have more than one team leader. This configuration appears to be correct, as it creates the duel FK/PK link table as I would expect.
I have an MVC4 application that allows editing, adding and removal of team leaders from the collection.
In my controller, I originally had the following:
var original = context.UserProfiles
.Include("TeamLeaders")
.Single(x => x.UserId == model.UserId);
context.Entry(original).CurrentValues.SetValues(model);
However, that last line was failing to mark the TeamLeaders collection as updated, and when I called SaveChanges(), no changes were recorded.
Instead I wrote a simple CopyProperties method on my User class, which uses reflection to manually copy the properties across, so in my controller I now have:
var original = context.UserProfiles
.Include("TeamLeaders")
.Single(x => x.UserId == model.UserId);
//context.Entry(original).CurrentValues.SetValues(model);
original.CopyProperties(model);
However, this goes too far, and SaveChanges attempts to add a new user to the system matching the profile of the selected team leader.
Can anyone advise as to which part of this I am doing wrong? I am not sure whether I need to update the mappings, or change the way I copy properties from the view model to the model

You must modify the loaded TeamLeaders collection in the original user according to your changes so that change detection can recognize which leaders have been removed and which have been added. When you call SaveChanges then EF will write the appropriate DELETE and INSERT statements for the join table based on the detected changes. The simplest way would look like this:
var original = context.UserProfiles
.Include("TeamLeaders")
.Single(x => x.UserId == model.UserId);
original.TeamLeaders.Clear();
foreach (var teamLeader in model.TeamLeaders)
{
var user = context.UserProfiles.Find(teamLeader.UserId);
if (user != null)
original.TeamLeaders.Add(user)
}
context.SaveChanges();
Find will fetch the team leaders from the context if they are already loaded. If they aren't loaded it will query the database.
If you want to avoid additional queries you can attach the team leaders manually to the context:
var original = context.UserProfiles
.Include("TeamLeaders")
.Single(x => x.UserId == model.UserId);
original.TeamLeaders.Clear();
foreach (var teamLeader in model.TeamLeaders)
{
var user = context.UserProfiles.Local
.SingleOrDefault(o => o.UserId == teamLeader.UserId);
if (user == null)
{
user = new User { UserId = teamLeader.UserId };
context.UserProfiles.Attach(user);
}
original.TeamLeaders.Add(user)
}
context.SaveChanges();
Except the first query to load the original here is no further database query involved.
BTW: You should be able to use the strongly typed Include version with EF Code First:
Include(u => u.TeamLeaders)
You just need using System.Data.Entity; in your code file to get access to this version.

Related

EF6 Dbcontext.SaveChanges works from development machine but fails when published

I have two repository functions for adding and removing favorite products for a customer.
public void AddProductToCustomer(int customerId, int productId) {
var customer = _customers.GetById(customerId,
new []{ModelContents.FavoriteProducts});
var product = _products.GetById(productId);
customer.FavoriteProducts.Add(product);
_context.SaveChanges();
}
and
public void RemoveProductFromCustomer(int customerId, int productId) {
var customer = _customers.GetById(customerId,
new[] { ModelContents.FavoriteProducts });
var product = _products.GetById(productId);
customer.FavoriteProducts.Remove(product);
_context.SaveChanges();
}
When I run locally against the production database it works fine but once I publish the code the _context.SaveChanges() fails to update the database. I have debugged this code remotely and there is no error thrown or returned from the function. Has anyone else seen this behavior before?
[UPDATE] By fails I mean that the database is not updated with the changes to customer.FavoriteProducts. Products are not added or removed from the list respectively and the total count of FavoriteProducts never changes, but only when the code is published, not when I am running it on my dev machine against the same database.
I hope that helps
[UPDATE] It occurred to me that people might wonder what the line new []{ModelContents.FavoriteProducts} refers to. This is a enumeration that tells the customer repository which navigation properties to populate for the model. The Customer is a large object and I don't always want all of its data. I have verified that customer.FavoriteProducts is populated in these cases.
[UPDATE] One thing that might be related to the issue is that the Product Model also has a Product.FavoritedBy Navigation Collection that contains the number of Customers that have Favorited that Product. Just like Customer.FavoriteProducts it is always up-to-date when I add or remove from a Customers FavoriteProducts collection except after the application is published to the server. The Entity Relationship is:
modelBuilder.Entity<CustomerUser>()
.HasMany(u => u.FavoriteProducts)
.WithMany(f => f.FavoritedBy)
.Map(t => t.MapLeftKey("UserId")
.MapRightKey("ProductId")
.ToTable("FavoriteProducts"));
Try the following in your AddProductToCustomer method:
_context.Entry(customer).State = EntityState.Modified;
_context.SaveChanges();

how to get an unsaved entity on server but not for saving?

i need to send my unsaved entity from the client to the server but not for saving changes
but inorder to do a process using the data on the entity and then change some of it's values and pass it back to the client
is this possible?
if not what are my options?
i tried to export the entity and then send it to a method on the webapi controller that gets a JObject but didn't find a way to deserialize it to the server entity
We did have a similar problem and found a solution as follows:
You need to take into consideration the way breeze manages it's objects.
1.Create custom saveBundle.
Consider complex order object.You need to fill your save bundle with each nested object inside order.
Like:
var saveBundle = new Array();
saveBundle.push(order.SaleAccountingInfo);
saveBundle.push(order.CostAccountingInfo);
saveBundle.push(order);
2.Create custom save options, where you can point to your custom Save Method on server
Like:
var so = new breeze.SaveOptions({ resourceName: "BookOrder" });
3.Call standard breeze function and pass it created params
manager.saveChanges(saveBundle, so).fail(function () {
// manager.rejectChanges();TODO check what needed
deferred.resolve(true);
});
On server you need to have you custom function ready and hook some breeze delegates
[HttpPost]
public SaveResult BookOrder(JObject orderBundle)
{
context.BeforeSaveEntityDelegate = OrderBeforeSaveEntity;
context.BeforeSaveEntitiesDelegate = SaveOrder;
context.AfterSaveEntitiesDelegate = BookOrderAfterSave;
try
{
return context.SaveChanges(orderBundle);
}
catch (Exception)
{
throw;
}
}
You can a lot of stuff in first two delegates but it is the last one you are looking for
private void BookOrderAfterSave(Dictionary<Type, List<EntityInfo>> orderSaveMap, List<KeyMapping> orderKeyMappings)
{
var orderEntity = orderSaveMap.Where(c => c.Key == typeof(BL.Orders.Order)).Select(d => d.Value).SingleOrDefault();
BL.Orders.Order order = (BL.Orders.Order)orderEntity[0].Entity; //your entity
//logic here
}
Hope it points to right direction.
we are doing something similar here. it'll save the entity so i'm not sure if this fits your question.
you can do:
entity.entityAspect.setModified()
then issue a saveChange()
then you can do your calculations on the server.
in our case we are using breeze.webapi so we are doing this in the beforeSave(entity) method.
breeze by design sends the changed entity then back to the client where the cache gets updated with your changes done on the server.

Reloading bind entity cause "An object with the same key already exists in the ObjectStateManager"

This has been asked before, but none of the available answers seem to fit to my case.
In order to perform some validation, I have to reload from DB the same entity, which already bound to the model. The following causes the error. I'm almost losing my mind.
[HttpPost]
public ActionResult Edit(Tekes tekes, FormCollection fc)
{
...
Tekes myTekes = db.Tkasim.Find(tekes.TeksID);
<some validation here>
if (ModelState.IsValid)
{
db.Entry(tekes).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Details", new { id = tekes.TekesID });
}
}
Not sure why you have to grab the same entity from the data base. Anyway, the problem is that the Find statement adds the Tekes object to the context, subsequently the statement db.Entry(tekes).State = EntityState.Modified tries to do the same. It's not clear what the validation is all about, but I think you can solve this by replacing the Find by
var myTekes = db.Tkasim.AsNoTracking().Single(x => x.TeksId == tekes.TeksID);
AsNoTracking tells EF to fetch entities without adding them to the cache and the state manager.

Detaching objects that have collections

I have the following code in my DAL:
public List<User> Getuser(int userId)
{
using (var context = this.GetDataContext())
{
var user = (from u in context.Users.Include("UserRoles")
where u.UserId == userId
select u).FirstOrDefault();
context.Detach(user);
return user;
}
}
When detach is called I lose my UserRole Collection that I am trying to send back to the client VIA WCF. If I don't detach the object from the context I get 'The underlying connection was closed: The connection was closed unexpectedly.'. What is the best way of doing so I can preserve the collection without having to re-query it again?
Turn off lazy loading for this operation. Your entities most probably contains other relations and WCF tries to serialize them as well.

How to stop WCF data Service to fetch data when SavingChanges (during update)

I am trying to update an entity from a WCF client as follows:
Ctxt.MergeOption = MergeOption.NoTracking;
var q = Ctxt.Customers.Where(p => p.MasterCustomerId == "JEFFERSON").Select(o => o);
//DataServiceCollection<Customer> oCustomers = new DataServiceCollection<Customer>(q, TrackingMode.None);
DataServiceCollection<Customer> oCustomers = new DataServiceCollection<Customer>(q);
oCustomers[0].FirstName = "KEFFERSON";
//Ctxt.SaveChanges(SaveChangesOptions.ReplaceOnUpdate);
//ctxt.SaveChangesDefaultOptions = SaveChangesOptions.ReplaceOnUpdate;
Ctxt.SaveChanges();
When I try to save the modified entity, it first tries to load that entity using a select query (to database) and then issues update statement to database.
In my case, I simply want to have the entity to be directly updated in the database without fetching it first. I don't mind if it overwrites the data in database
I tried the following at WCF service:
protected override EF.Model.DataModel.PersonifyEntities CreateDataSource()
{
var ctxt = new EF.Model.DataModel.PersonifyEntities();
ctxt.Customers.MergeOption = System.Data.Objects.MergeOption.NoTracking;
ctxt.ContextOptions.ProxyCreationEnabled = false;
ctxt.ContextOptions.LazyLoadingEnabled = false;
return ctxt;
}
But, no luck. Can anyone help me on this?
For WCF DataServices, the client can only update entities that it tracks. So it has to have the entity downloaded in the client before it can make any changes and save it back. Thats why you see the fetch (I am assuming that this is the first fetch that you are seeing for that specific entity) before the update. Hope this helps.

Resources