In our database we have a user. A user can have certain metadata (Like their location, age, etc) associated with them. In addition to that they have a profile image.
A user can edit any of these things at any time.
The problem we run into is that when a user goes to edit their information, but they don't choose an image -- the previous image gets erased (Null).
I figure that when we go to save the modified user, we should be able to say if(user.profileImage == null) don't save it
I figure that would come into play in our user repository which uses the following code:
public void SaveUser(Domain.Entities.User user)
{
if (user.UserId == 0)
{
context.Users.Add(user);
}
else
{
context.Entry(user).State = EntityState.Modified;
//The logic would be here
}
context.SaveChanges();
}
However I seems that no matter what we try it doesn't work.
So my question is: Is there a way to check changes to individual fields instead of the entire EntityState?
I ended up storing image data in the Session and then if the image upload was null I simply inserted the session attribute into my image, effectively keeping the old image if left empty.
Session["image"] = productService.GetProduct(id).Image;
Then in the post I said...
if (image != null)
{
product.ImageType = image.ContentType;
product.Image = new byte[image.ContentLength];
image.InputStream.Read(product.Image, 0, image.ContentLength);
}
else
{
product.Image = (byte[])Session["image"];
}
Yes, there is a way:
context.Users.Attach(user);
context.Entry(user).Property(u => u.Location).IsModified = true;
context.Entry(user).Property(u => u.Age).IsModified = true;
// etc.
Be aware that you cannot set IsModified to false once the property is marked as modified. So, something like this ...
context.Entry(user).State = EntityState.Modified;
context.Entry(user).Property(u => u.Image).IsModified = false;
... does not work.
Edit Alternative solution:
public void SaveUser(Domain.Entities.User user)
{
if (user.UserId == 0)
{
context.Users.Add(user);
}
else
{
var userInDb = context.Users.Single(u => u.UserId == user.UserId);
if (user.profileImage == null)
user.profileImage = userInDb.profileImage;
context.Entry(userInDb).CurrentValues.SetValues(user);
}
context.SaveChanges();
}
This keeps the value of profileImage as it is in the database if the user didn't post a new image (= user.profileImage == null), otherwise it saves the new image.
Related
I'm teaching myself C# and MVC but have a background in SQL. When updating an existing master-detail set of records in a single action (let's say for instance a customer order and order details), updating the master record is no problem. Regarding the detail records, I'm seeing examples that simply delete all existing details and then add them back in rather than add, delete or update only what's changed. That seems easy and effective but involves unnecessary changes to database records and might be an issue in complex relationships.
I've tried writing code that checks the existing values against posted values to determine the right EntityState (Added, Deleted, Modified, Unchanged) for each detail. Accomplishing this using LINQ Except and Intersect works but seems to cause an unexpected performance hit.
(Instead, I could load the original values in an "oldValue" hidden field in the original GET request to compare to the POST values except that would be unreliable in a multi-user environment and seems like a bad idea.)
I'll be happy to provide code examples, but my question is more about best practices. Is there a preferred method for updating existing master-detail sets of records?
EDIT: I've added the code below in response to questions. In this example, our application allows additional attributes to be attached to a product, kept in a separate table ProductAttributes. The view allows the user to edit both the product and the attributes on the same webpage and save at the same time. The code works fine but seems slow and lags at SaveChanges.
public ActionResult Edit(Product product)
{
if (ModelState.IsValid)
{
db.Entry(product).State = EntityState.Modified;
// Establish entity states for product attributes.
List<ProductAttribute> existingAttributes = new List<ProductAttribute>();
existingAttributes = db.ProductAttributes.AsNoTracking()
.Where(x => x.Sku == product.Sku).ToList();
// Review each attribute that DID NOT previously exist.
foreach (ProductAttribute pa in product.ProductAttributes
.Except(existingAttributes, new ProductAttributeComparer()))
{
if (pa.Value is null)
{
// Value didn't exist and still doesn't.
db.Entry(pa).State = EntityState.Unchanged;
}
else
{
// New value exists that didn't before.
db.Entry(pa).State = EntityState.Added;
}
}
// Review each attribute that DID previously exist.
foreach (ProductAttribute pa in product.ProductAttributes
.Intersect(existingAttributes, new ProductAttributeComparer()))
{
if (pa.Value is null)
{
// Value existed and has been cleared.
db.Entry(pa).State = EntityState.Deleted;
}
else
{
if (pa.Value != existingAttributes
.FirstOrDefault(x => x.Attribute == pa.Attribute).Value)
{
// Value has been altered.
db.Entry(pa).State = EntityState.Modified;
}
else
{
// Value has not been altered.
db.Entry(pa).State = EntityState.Unchanged;
}
}
}
db.SaveChanges();
return RedirectToAction("Details", new { id = product.ProductId });
}
return View(product);
}
internal class ProductAttributeComparer : IEqualityComparer<ProductAttribute>
{
public bool Equals(ProductAttribute x, ProductAttribute y)
{
if (string.Equals(x.Attribute, y.Attribute,
StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
public int GetHashCode(ProductAttribute obj)
{
return obj.Attribute.GetHashCode();
}
}
}
The following code successfully inserts an INKitRegister record.
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
#region Event Handlers
public virtual void _(Events.RowPersisting<SOLine> e)
{
SOLine soLine = e.Row;
InventoryItem inItem = PXSelect<InventoryItem, Where<InventoryItem.inventoryID, Equal<Required<SOLine.inventoryID>>>>
.Select(new PXGraph(), soLine.InventoryID);
if (inItem == null)
return;
if (inItem.KitItem.HasValue && inItem.KitItem.Value)
{
SOLineExt soLineExt = soLine.GetExtension<SOLineExt>();
INKitRegister kit = null;
bool kitIsNew = false;
if (soLineExt.INKitRegisterDocType != null && soLineExt.INKitRegisterRefNbr != null)
{
kit = PXSelect<INKitRegister, Where<INKitRegister.docType, Equal<Required<SOLineExt.iNKitRegisterDocType>>,
And<INKitRegister.refNbr, Equal<Required<SOLineExt.iNKitRegisterRefNbr>>>>>
.Select(new PXGraph(), soLineExt.INKitRegisterDocType, soLineExt.INKitRegisterRefNbr);
}
// If kit mix doesn't exist, create it
if (kit == null)
{
kit = new INKitRegister();
kitIsNew = true;
}
// Insert/update INKitRegister (new mix)
var graph = PXGraph.CreateInstance<KitAssemblyEntry>();
graph.Document.Current = kit;
graph.Document.Cache.SetValueExt<INKitRegister.tranDate>(kit, soLine.RequestDate);
if (kitIsNew)
{
graph.Document.Cache.SetValueExt<INKitRegister.kitInventoryID>(kit, soLine.InventoryID);
}
graph.Document.Cache.SetValueExt<INKitRegister.uOM>(kit, soLine.UOM);
graph.Document.Cache.SetValueExt<INKitRegister.qty>(kit, soLine.Qty);
//graph.Document.Cache.SetValueExt<INKitRegister.tranDesc>(kit, string.Format("{0} {1}", soLine.TranDesc));
kit = graph.Document.Cache.Update(kit) as INKitRegister;
graph.Actions.PressSave();
// Save the mix props to the SOLine
soLineExt.INKitRegisterDocType = kit.DocType;
soLineExt.INKitRegisterRefNbr = kit.RefNbr;
}
}
#endregion
}
When I attempt to change the Qty on an SOOrder, the same code is supposed to update the existing INKitRegister record.
Instead, I receive this error:
Error: Updating 'IN Kit' record raised at least one error. Please review the errors. Error: 'Total Cost' cannot be empty. Error: 'Ext. Cost' cannot be empty.
The Total Cost and Ext Cost are (presumably) automatically calculated when the record is inserted.
Q: Why are the values not again calculated on Update? I wouldn't know where to begin to calculate them manually, and that doesn't really seem like a good idea anyway.
When I try to update this object in EF6 I get an error stating more than 1 entity has this primary key. Looking at this DB I know this to be untrue(from what I can see).
I need to be able to update a second object based on one of the properties on the posted object. The code below produces the error. I have left in commented out pieces that I have tried to get this to work.
public async Task<ActionResult> Edit(PricingRule pricingRule)
{
if(ModelState.IsValid)
{
var currentUser = await serv.UserManager.FindByIdAsync(User.Identity.GetUserId());
var company = currentUser.Company;
//var entityRule = serv.PricingService.PricingRules.Get(pricingRule.PricingRuleId);
//If this is the first rule, set it to the company default
var rulesCount = company.PricingRules.Count;
if (rulesCount <= 1 || company.DefaultPricingRule == null)
pricingRule.DefaultPricingRule = true;
//Make sure no other rules are marked as default, and update the company with this rule as default
if (pricingRule.DefaultPricingRule)
{
if (company.DefaultPricingRule != null)
{
var oldRule = serv.PricingService.PricingRules.Get(company.DefaultPricingRule.PricingRuleId);
oldRule.DefaultPricingRule = false;
//serv.PricingService.PricingRules.Update(oldRule);
}
company.DefaultPricingRule = pricingRule;
serv.CoreService.Companies.Update(company);
}
serv.PricingService.PricingRules.Update(pricingRule);
await serv.SaveAllChangesAsync();
return RedirectToAction("Index");
}
return View(pricingRule);
}
Whether or not it is the best practice or how it should technically be done, this is how I solved my problem.
The edited object I was passing in, needed to be marked as modified first, before doing any other operations. I am assuming this is because the context could then grab it and all other operations regarding it would be done "within context". Other wise I think it was trying to add a new object if I tried to attach it to company.DefaultPricingRule.
public async Task<ActionResult> Edit(PricingRule pricingRule)
{
if(ModelState.IsValid)
{
serv.PricingService.PricingRules.Update(pricingRule);
var currentUser = await serv.UserManager.FindByIdAsync(User.Identity.GetUserId());
var company = currentUser.Company;
//If this is the first rule, set it to the company default
var rulesCount = company.PricingRules.Count;
if (rulesCount <= 1 || company.DefaultPricingRule == null)
pricingRule.DefaultPricingRule = true;
//Make sure no other rules are marked as default, and update the company with this rule as default
if (pricingRule.DefaultPricingRule)
{
if (company.DefaultPricingRule != null)
{
var oldRule = serv.PricingService.PricingRules.Get(company.DefaultPricingRule.PricingRuleId);
oldRule.DefaultPricingRule = false;
serv.PricingService.PricingRules.Update(oldRule);
}
company.DefaultPricingRule = pricingRule;
serv.CoreService.Companies.Update(company);
}
await serv.SaveAllChangesAsync();
return RedirectToAction("Index");
}
return View(pricingRule);
}
If Anyone has a comment on if this is best practice or if there is a better way to do it, I gladly take criticism.
Currently I am attempting to add a new row to a database table through AJAX which is working fine. But then I try to update a different table and I get an error. Here is my code and the error I am encountering.
Error
The object cannot be attached because it is already in the object context. An object can only be reattached when it is in an unchanged state.
Line 41: _db.ChampionCounters.Attach(champion);
Code
[HttpPost]
public ActionResult VoteYes(int id)
{
string results;
if (Request.IsAuthenticated)
{
var checkFirst =
from c in _db.UserCounterLinks
where c.counterId == id && c.userName == User.Identity.Name
select c;
if (checkFirst.Any())
{
results = "You have already voted on this counter.";
return Json(results);
}
var userVoteLink = new UserCounterLink { counterId = id, userName = User.Identity.Name, userAgree = true };
_db.UserCounterLinks.AddObject(userVoteLink);
var champion = _db.ChampionCounters.SingleOrDefault(c => c.id == id);
if (champion != null)
{
champion.positiveVotes++;
_db.ChampionCounters.Attach(champion);
}
_db.SaveChanges();
results = "Voted";
} else
{
results = "You must be logged in to vote.";
}
return Json(results);
}
Summary
The code above is from the controller that handles the Ajax post. Like I said the userVoteLink table creates a record just fine. But when I try to update the other table ChampionCounters the error is thrown.
Thanks in advance!
You don't need to attach the instance because the context is already tracking that instance. Just remove the _db.ChampionCounters.Attach(champion); line.
I have simple query that loads data from two tables into GUI. I'm saving loaded data to widely available object Clients currentlySelectedClient.
using (var context = new EntityBazaCRM())
{
currentlySelectedClient = context.Kliencis.Include("Podmioty").FirstOrDefault(d => d.KlienciID == klientId);
if (currentlySelectedClient != null)
{
textImie.Text = currentlySelectedClient.Podmioty.PodmiotOsobaImie;
textNazwisko.Text = currentlySelectedClient.Podmioty.PodmiotOsobaNazwisko;
}
else
{
textNazwa.Text = currentlySelectedClient.Podmioty.PodmiotFirmaNazwa;
}
}
So now if I would like to:
1) Save changes made by user how do I do it? Will I have to prepare something on database side? How do I handle modifying multiple tables (some data goes here, some there)? My current code seems to write .KlienciHaslo just fine, but it doesn't affect Podmioty at all. I tried different combinations but no luck.
2) Add new client to database (and save information to related tables as well)?
currentClient.Podmioty.PodmiotOsobaImie = textImie.Text; // not saved
currentClient.Podmioty.PodmiotOsobaNazwisko = textNazwisko.Text; // not saved
currentClient.KlienciHaslo = "TEST111"; // saved
using (var context = new EntityBazaCRM())
{
var objectInDB = context.Kliencis.SingleOrDefault(t => t.KlienciID == currentClient.KlienciID);
if (objectInDB != null)
{
// context.ObjectStateManager.ChangeObjectState(currentClient.Podmioty, EntityState.Modified);
//context.Podmioties.Attach(currentClient.Podmioty);
context.Kliencis.ApplyCurrentValues(currentClient); // update current client
//context.ApplyCurrentValues("Podmioty", currentClient.Podmioty); // update current client
}
else
{
context.Kliencis.AddObject(currentClient); // save new Client
}
context.SaveChanges();
}
How can I achieve both?
Edit for an answer (doesn't save anything ):
currentClient.Podmioty.PodmiotOsobaImie = textImie.Text; // no save
currentClient.Podmioty.PodmiotOsobaNazwisko = textNazwisko.Text; // no save
currentClient.KlienciHaslo = "TEST1134"; // no save
using (var context = new EntityBazaCRM())
{
if (context.Kliencis.Any(t => t.KlienciID == currentClient.KlienciID))
{
context.Kliencis.Attach(currentClient); // update current client
}
else
{
context.Kliencis.AddObject(currentClient); // save new Client
}
context.SaveChanges();
}
Apparently ApplyCurrentValues only works with scalar properties.
If you attach the currentClient then associated objects should also attach, which means they'll be updated when you SaveChanges()
But you'll get an Object with the key exists exception because you are already loading the object from the database into the objectInDB variable. The context can only contain one copy of a Entity, and it knows that currentClient is the same as objectInDB so throws an exception.
Try this pattern instead
if (context.Kliencis.Any(t => t.KlienciID == currentClient.KlienciID))
{
context.Kliencis.Attach(currentClient); // update current client
}
else
{
context.Kliencis.AddObject(currentClient); // save new Client
}
or if you're using an identity as the ID, then
// if the ID is != 0 then it's an existing database record
if (currentClient.KlienciID != 0)
{
context.Kliencis.Attach(currentClient); // update current client
}
else // the ID is 0; it's a new record
{
context.Kliencis.AddObject(currentClient); // save new Client
}
After some work and help from Kirk about the ObjectStateManager error that I was getting I managed to fix this. This code allows me to save both changes to both tables.
currentClient.Podmioty.PodmiotOsobaImie = textImie.Text;
currentClient.Podmioty.PodmiotOsobaNazwisko = textNazwisko.Text;
currentClient.KlienciHaslo = "TEST1134";
using (var context = new EntityBazaCRM()) {
if (context.Kliencis.Any(t => t.KlienciID == currentClient.KlienciID)) {
context.Podmioties.Attach(currentClient.Podmioty);
context.Kliencis.Attach(currentClient);
context.ObjectStateManager.ChangeObjectState(currentClient.Podmioty, EntityState.Modified);
context.ObjectStateManager.ChangeObjectState(currentClient, EntityState.Modified);
} else {
context.Kliencis.AddObject(currentClient); // save new Client
}
context.SaveChanges();
}