EF Core - Modify realations without loading entity - c#

I was trying to implement a function that will let a user like a comment. If the user has already liked it, it can't be liked again and vice versa.
This is what it looks like:
public async Task<ActionResult<CommentResponse>> LikeComment(LikeComment like)
{
if (like.HasNullProperty())
return BadRequest("Missing properties!");
var comment = await commentService.GetCommentWithLikes((int) like.CommentId);
if(comment is null)
return NotFound($"No comment with id {like.CommentId} was found");
try
{
var userId = User.GetUserID();
comment = await commentService.LikeComment(comment, userId, (bool)like.Liked);
return comment is not null ? Ok(comment.GetCommentResponse((bool)like.Liked)) : StatusCode(304);
}
catch(Exception e)
{
return StatusCode(500, $"Error while trying to {((bool)like.Liked ? "like" : "dislike")} comment");
}
}
Relevant functions:
public async Task<Comment> GetCommentWithLikes(int id) => await blogContext.Comments.IncludeLikes().FirstOrDefaultAsync(x => x.Id == id);
public static IQueryable<Comment> IncludeLikes(this IQueryable<Comment> source)
=> source.Select(x => new Comment
{
Id = x.Id,
ArticleId = x.ArticleId,
CreatedById = x.CreatedById,
CreatedAt = x.CreatedAt,
Likes = x.LikedBy.Count,
Text = x.Text,
});
And the main like logic:
public async Task<Comment> LikeComment(Comment comment, string userId, bool liked)
{
var user = new User { Id = userId };
var hasLiked = await blogContext.Comments.Where(x => x.Id == comment.Id && x.LikedBy.Any(x => x.Id == user.Id)).FirstOrDefaultAsync() is not null;
Action action = null;
if (!hasLiked && liked)
{
action = () => comment.LikedBy.Add(user);
comment.LikedBy = new List<User>();
comment.Likes++;
}
else if (hasLiked && !liked)
{
action = () => comment.LikedBy.Remove(user);
comment.LikedBy = new List<User> { user };
comment.Likes--;
}
if (action is null)
return null;
blogContext.Attach(user);
blogContext.Attach(comment);
action();
await blogContext.SaveChangesAsync();
return comment;
}
The idea was to not load the whole likedBy relation, but still notify EF Core that i have added or removed one user. Therefore i modify the Comment, then attach it so EF Core tracks the changes to the likedBy relation. Interestingly, it works fine when liking a comment. However, when disliking, i get an rrror that the comment is already attached. Using .AsNoTracking() in the GetCommentsWithLikes function didn't help.
The instance of entity type 'Comment' cannot be tracked because another instance with the same key value for {'Id'} 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.
This is the comment passed to the like func when linking (works):
This is the one when disliking (only diff is the like count...):
And this is it right before the failing attach:
Maybe someone knows the reason for this behaviour and can help me or suggest a different approach :)
Thanks

Using .AsNoTracking() in the GetCommentsWithLikes function didn't help
Due to the used projection, that function is already implicitly no tracking. It is the following call
var hasLiked = await blogContext.Comments
.Where(x => x.Id == comment.Id && x.LikedBy.Any(x => x.Id == user.Id))
.FirstOrDefaultAsync() is not null;
which is adding a Comment instance to the change tracker when the result is not null.
Since you don't need that instance and are just checking for existence, use the following instead which doesn't involve entity instances, but pure server side query:
var hasLiked = await blogContext.Comments
.AnyAsync(x => x.Id == comment.Id && x.LikedBy.Any(x => x.Id == user.Id));

Related

cannot convert from 'string' to 'Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole

I'm fairly new with updating databases, and I've constructed the below code to replace a user's role with a new role. I'm getting the error in the subject though.
public void UpdateRole(string id, string newRoleID)
{
var user = Users.FirstOrDefault(u => u.Id == id);
var oldRoleId = user.Roles.FirstOrDefault().RoleId;
if (user != null && oldRoleId != newRoleID)
{
user.Roles.Remove(oldRoleId);
user.Roles.Add(newRoleID);
}
}
Could someone please explain why I am getting this error? I am not trying to convert anything. I am attempting to delete the contents of RoleId for the user id specified, and replace it with the new ID that is sent from my post action.
user.Roles.Add method takes a IdentityUserRole object while you are passing it a string value (i.e. newRoleID). You need the following change in you code:
user.Roles.Add(new IdentityUserRole { RoleId = newRoleID });
Edit
The Remove method, needs an IdentityUserRole object too. But note that it must be attached to the context too. The simplest way you can do it is through the following code:
var user = Users.FirstOrDefault(u => u.Id == id);
var oldRole = user.Roles.FirstOrDefault();
if (user != null && oldRole.RoleId != newRoleID)
{
user.Roles.Remove(oldRole);
user.Roles.Add(new IdentityUserRole { RoleId = newRoleID });
}

Linq code doesn't return correct record

I have a table named dbo.EmployeeType with three records:
PK_EmployeetypeID EmployeeTypeName
1 Project Manager
2 Business Analyst
3 Developer
I have this piece of Linq code:
public static string GetTypeByID(int id)
{
using (ProjectTrackingEntities1 db = new ProjectTrackingEntities1())
{
var type = db.EmployeeTypes.Select(o => new LOOKUPEmployeeType
{
PK_EmployeeTypeID = id,
EmployeeTypeName = o.EmployeeTypeName
});
return type.FirstOrDefault().EmployeeTypeName;
}
}
No matter what id I send to it, it returns Project Manager, and I'm confused as to why.
You need to apply a filter, otherwise you're just returning the first record and hard coding the ID. Try this:
public static string GetTypeByID(int id)
{
using (ProjectTrackingEntities1 db = new ProjectTrackingEntities1())
{
//Here we apply a filter, the lambda here is what creates the WHERE clause
var type = db.EmployeeTypes
.FirstOrDefault(et => et.PK_EmployeeTypeID == id);
if(type != null)
{
return type.EmployeeTypeName;
}
else
{
return "";
}
}
}
Note that using FirstOrDefault means if there are no matches, or multiple matches, type will be null and you will get an empty string returned.
Set a breakpoint on type = ... and inspect it. You have no Where in there so you get all - and Select just makes LOOKUPEmployeeTypes out of all of them.
FirstOrDefault then returns the first of those 3 which is always the ProjManager
Fix:
var type = db
.EmployeeTypes
.Where( o => o.Id == id)
.Select(o => new LOOKUPEmployeeType
{
PK_EmployeeTypeID = id,
EmployeeTypeName = o.EmployeeTypeName
});
In your code you only return the first value. You need to tell EF which value you need to return.
Let us assume you need the value with Id=2. Instead of Select(), use Single(x => x.Id == 2) or First(x => x.Id == 2).

Unchanged objects in Entity Framework 6

I have silverlight application.
This is an invoke operation (in Open Ria Service - WCF Ria Service open source version) with Entity Framework 6.
public List<VaultAmount> GetCurrentVaultAmounts(Guid vaultId)
{
return this.DbContext.VaultAmounts
.Include(v => v.VaultAmountQuantities)
.Include(v => v.VaultCurrency)
.Include(v => v.Vault)
.Include(v => v.VaultAmountQuantities.Select(vaq => vaq.VaultCurrency))
.Where(v => v.VaultId == vaultId && v.IsCurrent).ToList();
}
[Invoke]
public void UpdateVaultRemainders(List<VaultAmountQuantity> updatedQuantities, string comment, Guid userId,
string friendlyName,
Guid vaultAmountId, int currencyId)
{
VaultAmount vaultAmount =
this.DbContext.VaultAmounts
.Include("Vault")
.SingleOrDefault(va => va.VaultAmountId == vaultAmountId);
if (vaultAmount == null && vaultAmount.Vault == null) return;
//Get FromVault and ToVault with amounts and updatedQuantities
List<VaultAmount> currentAmounts = GetCurrentVaultAmounts(vaultAmount.Vault.VaultId);
//Vault vault = GetVaultWithCurrentAmountsAndQuantitiesById(vaultAmount.Vault.VaultId);
var helper = new RemainderVAHelper(currentAmounts, userId, friendlyName, currencyId, updatedQuantities,
comment,
BS2VaultEventTypes.Correction);
//................................
foreach (var amount in currentAmounts)
{
if (amount.IsCurrent == false)
{
DbEntityEntry<VaultAmount> entityEntry = this.DbContext.Entry(amount);
entityEntry.State = EntityState.Modified;
}
}
this.DbContext.SaveChanges();
}
In RemainderVAHelper class I am changing currentAmounts objects, set IsCurrent property to false.
But currentAmounts objects entityEntry.State are still Unchanged. Why? Yes, I can set their states as Modified (as I am doing in method), but I think it is not very good thing. Can you tell me why my objects state doesn't changes to Modified?
If you are not using change tracking proxies EF has no way of knowing you modified a property until you call DetectChanges or call SaveChanges (which calls DetectChanges)

Update a Second Table or Model in EF - MVC

I need to update a second table in my EF, this table is my storage of purchase, i call this entity and search the value for change (Qty), i do the math comparasion and send back the data updated, but raise a error tell me "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key"
How fix the error? Thanks
My Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(PurchaseDetails purchasedetails)
{
ViewBag.PurchaseID = new SelectList(db.Purchases, "PurchaseID", "Notes", purchasedetails.PurchaseID);
ViewBag.idArt = new SelectList(db.Art, "idArt", "des_art", purchasedetails.IdArt);
ViewBag.idAlmacen = new SelectList(db.Almacens, "idAlmacen", "des_alma", purchasedetails.IdAlmacen);
var cant_details = db.PurchaseDetails.Where(p => p.PurchaseDetailsID == purchasedetails.PurchaseDetailsID).FirstOrDefault();
var cantidad = purchasedetails.Qty - cant_details.Qty;
if (ModelState.IsValid)
{
db.Entry(purchasedetails).State = EntityState.Modified;
db.SaveChanges();
var stock_id = db.Stock.Where(s => s.idAlmacen == purchasedetails.IdAlmacen && s.idArt == purchasedetails.IdArt).FirstOrDefault();
stock_id.stcActual = stock_id.stcActual + cantidad;
db.Stock.Attach(stock_id);
var entry = db.Entry(stock_id);
entry.Property(e => e.stcActual).IsModified = true;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(purchasedetails);
}
I found The solution in this post
How do I detach objects in Entity Framework Code First?
i used AsNoTracking() in my line code
var cant_details = db.PurchaseDetails.Where(p => p.PurchaseDetailsID == purchasedetails.PurchaseDetailsID).FirstOrDefault();
and work perfectly
var cant_details = db.PurchaseDetails.AsNoTracking().Where(p => p.PurchaseDetailsID == purchasedetails.PurchaseDetailsID).FirstOrDefault();

Clean method in order to update collection in entity framework

i'm sorry if this question has already been asked, but i'm in trouble with my method of updating collection in Entity Framework.
Let me explain the situation :
- I have for example one model CUSTOMER with some properties and a collection of ORDERS (for example).
- Let's imagine we have an admin page on wich we can edit all the ORDERS for a customer, and when we submit the form, it will send us back the object CUSTOMERS with updated ORDERS (some added, some updated and some deleted).
For the moment i use something like this in order to compare old collection and new collection and determine which object i need to delete/update/add
var toRemove = new List<ORDERS>();
var toAdd = new List<ORDERS>();
foreach (
var order in
oldList.Where(
order =>
newList.FirstOrDefault(t => t.link_id == order.link_id) == null))
{
toRemove.Add(order);
}
foreach (
var order in
newList.Where(
order =>
oldList.FirstOrDefault(t => t.link_id == order.link_id) == null))
{
toAdd.Add(order);
}
foreach (var ORDERSe in toRemove)
{
bdd.ORDERS.Remove(ORDERSe);
}
foreach (var ORDERSe in toAdd)
{
ORDERSe.pjt_id = project_id;
bdd.ORDERS.Add(ORDERSe);
}
foreach (
var order in
newList.Where(
order =>
oldList.FirstOrDefault(t => t.link_id == order.link_id) != null))
{
var child = oldList.FirstOrDefault(t => t.link_id == order.link_id);
bdd.Entry(child).CurrentValues.SetValues(order);
}
But i'm unconfortable with this, because in my mind, entity framework should be able to do the work for me !
I was hoping something like :
customer.orders = newOrders;
Did i missed anything about entity framework or ?
Because when i do this, it just duplicate my orders.
Thanks in advance for your answer.
You can certainly make it cleaner using .Except() and .Intersect(), but the concept doesn't really change, AFAIK you still have to individually remove, update & add the entries in loops...
var oldList = new List<ORDERS>();
var newList= new List<ORDERS>();
var IdsToRemove = oldList.Select(t => t.link_id).Except(newList.Select(t => t.link_id));
var IdsToAdd = newList.Select(t => t.link_id).Except(oldList.Select(t => t.link_id));
var IdsToUpdate = newList.Select(t => t.link_id).Intersect(oldList.Select(t => t.link_id));
//remove
bdd.orders.where(x => IdsToRemove.Contains(x.link_id)).ForEach(x => bdd.Remove(x));
//add
foreach(var order in newList.Where(x -> IdsToAdd.Contains(x.link_id))
{
bdd.Orders.Attach(order);
bdd.Entries(order).EntityState = EntityState.Added;
}
//update
foreach(var order in newList.Where(x -> IdsToUpdate .Contains(x.link_id))
{
bdd.Orders.Attach(order);
bdd.Entries(order).EntityState = EntityState.Modified;
}
bdd.SaveChanges();
But i'm unconfortable with this, because in my mind, entity framework
should be able to do the work for me !
In fact, EF does the Work for you. Using the data context SaveChanges method EF should be able to save all your changes at once:
DbContext.SaveChanges()
For your convinience you can still override this method. Internally you should use something like this:
public override int SaveChanges()
{
var changeSet = ChangeTracker.Entries<IAuditable>();
if (changeSet != null)
{
foreach (var entry in changeSet.Where(c => c.State != EntityState.Unchanged))
{
entry.Entity.ModifiedDate = DateProvider.GetCurrentDate();
entry.Entity.ModifiedBy = UserName;
}
}
return base.SaveChanges();
}

Categories

Resources