I'm trying to update my UserRoles table but it won't update. I'm trying to update two things : 1. the email 2. the user role. Because the update needs to happen in 2 tables I'm using two separate commands. When I run the update on the Email alone (Users) it works but if I update the role (AspUserRoles) it does nothing. When I run it both it doesn't work either because UserRoles.Attach(userRole) is preventing it from updating. I also get no errors.
I checked if ApplicationRole.Id and ApplicationUser.Id has a value and it does return the value I want.
Here's my UserController.cs :
public async Task<IActionResult> Edit(UserViewModel model, Guid id)
{
var alert = new Alert();
try
{
if(!ModelState.IsValid)
{
alert.Message = alert.ExceptionMessage = ApplicationDbContextMessage.INVALID;
throw new Exception();
}
var originalModel = ApplicationDbContext.Users.FirstOrDefault(u => u.Id == id);
var userRole = ApplicationDbContext.UserRoles.FirstOrDefault(i => i.UserId == id);
if(originalModel == null)
{
alert.Message = alert.ExceptionMessage = ApplicationDbContextMessage.NOTEXISTS;
throw new Exception();
}
originalModel.Email = model.ApplicationUser.Email;
userRole.RoleId = model.ApplicationRole.Id;
ApplicationDbContext.Users.Attach(originalModel);
ApplicationDbContext.UserRoles.Attach(userRole);
ApplicationDbContext.Entry(originalModel).State = EntityState.Modified;
if (await ApplicationDbContext.SaveChangesAsync() == 0)
{
alert.Message = alert.ExceptionMessage = ApplicationDbContextMessage.EDITNOK;
throw new Exception();
}
alert.Message = ApplicationDbContextMessage.EDITOK;
return RedirectToAction("Index");
}
catch(Exception ex)
{
alert.Type = AlertType.Error;
alert.ExceptionMessage = ex.Message;
model = await ViewModel(model.ApplicationUser);
ModelState.AddModelError(string.Empty, alert.ExceptionMessage);
}
return View(model);
}
The way you are modifying data in this code, you don't need to call Attach or Add on the Context to let it know about changes to entities, that will happen automatically.
From the moment you pull an entity out of a DbSet of the DbContext it is being tracked (attached) by that DbContext. When you call SaveChanges on the DbContext it will scan any entities that it is tracking, comparing current values to old values, to find changes. Those changes then get sent to the data base.
You should literally be able to remove 3 lines of code from what you originally posted and have it work.
...
originalModel.Email = model.ApplicationUser.Email;
userRole.RoleId = model.ApplicationRole.Id;
ApplicationDbContext.Users.Attach(originalModel); // <--- Delete this line
ApplicationDbContext.UserRoles.Attach(userRole); // <--- Delete this line
ApplicationDbContext.Entry(originalModel).State = EntityState.Modified; // <--- Delete this line
if (await ApplicationDbContext.SaveChangesAsync() == 0)
...
A little something else I noticed. It looks like you might be using one single DbContext instance for the entire application. That is usually considered an "Anti-Patern" in Entity Framework. You should create a new DbContext instance (with using) for every "logical" operation you perform. That instance should only be alive for the life of that operation.
In MVC, this is usually one DbContext instance per ActionMethod.
Related
This is the simplified partial pseudo code from the calling method:
using (var context = new ModelContext())
{
var transaction = context.Database.BeginTransaction();
try
{
// Delete stuff here
Users.DeleteAllByClient(context, clientToDelete.ClientId);
// And delete stuff here
transaction.Commit();
}
catch (Exception e)
{
transaction.Rollback();
return;
}
}
Take the example method, used to delete user details and their profile.
The ModelContext context is passed through to the method, which means that if the calling method does not commit transaction.Commit(); causing a rollback, this will not occur for the roles. They will have already been deleted.
Is it possible to add the Roles.RemoveUserFromRoles(item.User.UserName, roles); to the rollback?
I can't use TransactionScope (nested or not) as opposed to passing the context through.
public static void DeleteAllByClient(ModelContext context, int clientId)
{
// Users to delete
var models = context.UserDetails.Where(o => o.ClientId == clientId).ToList();
// list of user profiles
List<UserProfile> userProfileList = new List<UserProfile>();
foreach (var item in models)
{
var userProfile = context.UserProfiles.FirstOrDefault(o => o.UserId == item.UserId);
var roles = Roles.GetRolesForUser(userProfile.UserName);
// these need to roll back if commit doesn't happen
Roles.RemoveUserFromRoles(item.User.UserName, roles);
userProfileList.Add(userProfile);
}
if (models.Count > 0)
{
context.UserDetails.BulkDelete(models); // db tables specified for clarity
context.UserProfiles.BulkDelete(userProfileList);
}
}
I am sending the user-edited details from the angular app. In Edit form, I am allowing the user to edit only a few fields rest remail the same as previous. In this method, I am assigning the previous non changed values to the object with edited values.
public async Task<IActionResult> PutUsers(int id,[FromForm] Users users)
{
var user = _context.Users.FirstOrDefault(x => x.Email == users.Email);
if (id != users.Id)
{
return BadRequest();
}
users.Password = user.Password;
users.UserImageGuid = user.UserImageGuid;
users.UserImageName = user.UserImageName;
users.DOB = user.DOB;
try
{
_context.Entry(users).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
catch (Exception e)
{
if (!UsersExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
here I am having the exception "The instance of entity type 'Users' 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."
Please help me out.
Thank you in advance.
Problem is that you are trying to save one entity which has primary key same as the entity that you got from the database (in this case user and users objects).
There are several ways for you to fix this, simplest would be to simply detach users object from context before saving:
_context.Entry(user).State = EntityState.Detached;
You can also retrieve users object unattached (in which case you don't have to set State to Detached later):
var user = _context.Users.AsNoTracking().FirstOrDefault(x => x.Email == users.Email);
Other one would be to update user with data from users and just save changes.
You got the same entity tracked twich.
One "user" come from _context.Users.FirstOrDefault(x => x.Email == users.Email);
The other "users" from _context.Entry(users).State = EntityState.Modified;
You cannot have the same entity tracked multiple time on same context.
Just use => _context.Users.AsNoTracking().FirstOrDefault(x => x.Email == users.Email); or change the user attribute and avoid _context.Entry(users).State = EntityState.Modified;
In my ASP.NET Web API application, I get the following error in one of my actions, at the line below:
var dateExists = await _unitOfWork.GetRepository<Booking>().All()
.AnyAsync(b => b.User.UserName == booking.User.UserName && b.Date == model.Date);
Here's the error:
A second operation started on this context before a previous
asynchronous operation completed. Use 'await' to ensure that any
asynchronous operations have completed before calling another method
on this context. Any instance members are not guaranteed to be thread
safe.
But, as you can see, I am using await. I'm also using await on all the other async operations in the action. Then, what might be the problem?
Edit:
Here's my entire action:
[HttpPut]
[Route("Update")]
public async Task<IHttpActionResult> Put(BookingEditBindingModel model)
{
if (!ModelState.IsValid)
{
// Return a bad request response if the model state is invalid...
return BadRequest(ModelState);
}
try
{
var booking = await _unitOfWork.GetRepository<Booking>().FindAsync(model.BookingId);
if (booking == null)
{
return NotFound();
}
// Only Admin is allowed to update other users' data...
if (!User.IsInRole("Admin") && booking.User.UserName != User.Identity.Name)
{
return Unauthorized();
}
// There can only be one entry per date/user.
if (model.Date != booking.Date)
{
var dateExists = await _unitOfWork.GetRepository<Booking>().All()
.AnyAsync(b => b.User.UserName == booking.User.UserName && b.Date == model.Date);
if (dateExists)
{
ModelState.AddModelError("Date", "Data already exists for this date.");
return BadRequest(ModelState);
}
}
booking.Date = model.Date;
// Change other properties...
await _unitOfWork.SaveAsync();
return Ok();
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}
Update:
I disabled lazy loading, but I still had a problem. It looks like, as other people had guessed, the issue was with booking.User, because the FindAsync() in the first query does not include the User. I replaced the first query with the one below, and the issue was resolved.
var booking = await _unitOfWork.GetRepository<Booking>().All()
.Include(b => b.User)
.SingleOrDefaultAsync(b => b.BookingId == model.BookingId);
I am going to go out on a limb and guess that you have lazy loading enabled. This would cause a call like booking.User.UserName to go to the database to retrieve the value and putting that inside of a call like AnyAsync could cause the DbContext to try to open a second connection to retrieve that value after it already opened one for the AnyAsync statement.
The quick fix in this case is to retrieve the user name before the call to AnyAsync.
var bookingUserName = booking.User.UserName;
var dateExists = await _unitOfWork.GetRepository<Booking>().All()
.AnyAsync(b => b.User.UserName == bookingUserName && b.Date == model.Date);
A better fix would be to retrieve this information in advance in the call where you get the booking instance so you do not have to make an additional round trip.
Personally I always disable lazy loading on my DbContext types and use Include statements to explicitly specify what I want to be retrieved with any call. This gives me more control over my execution.
I'm using Entity Framework 5 with MySQL Database and just wanted to update a row attribute "user_loginstatus" between 0 and 1. The first time when I log in via client it updates just fine for the first attempt, after trying to update again it doesn't do anything with no exception.
I log in like this:
public async void LoginExecute()
{
// Checking Connection before etc...
if (await _dataService.IsLoginDataValidTask(UserObj.Username, md5))
{
Trace.WriteLine("LoginCommand Execute: Eingeloggt");
UserObj = await _dataService.GetUserDataTask(UserObj.Username);
await _dataService.SetUserStatusTask(UserObj.Id, 1);
await _dataService.WriteLog(UserObj.Id, "login", "Programm", GetLocalAdress());
Messenger.Default.Send(UserObj);
Messenger.Default.Send(new NotificationMessage("GoToMenuPage"));
}
else
{
// Error Stuff...
}
}
SetUserStatus Method in DataService Class
public Task SetUserStatusTask(int id, int status)
{
return Task.Factory.StartNew(() =>
{
try
{
var user = _entities.users.Find(id);
user.user_loginstatus = status;
_entities.SaveChanges();
}
catch (Exception ex)
{
Trace.WriteLine("DataService SetUserStatusTask: " + ex.Message);
}
});
}
GetUserData Method in DataService Class
public Task<User> GetUserDataTask(string username)
{
return Task.Factory.StartNew(() =>
{
try
{
var user = from us in _entities.users
where us.user_name.Equals(username)
select new User
{
Id = us.user_id,
Username = us.user_name,
FirstName = us.user_firstname,
LastName = us.user_lastname,
Gender = us.user_gender,
Email = us.user_mail,
Group = us.user_usergroup,
Avatar = us.user_avatar,
LoginStatus = 1
};
return user.FirstOrDefault();
}
catch (Exception ex)
{
Trace.WriteLine("DataService GetUserDataTask: " + ex);
return null;
}
});
}
So "users" is my table from the database and "User" / "UserObj" my custom Object.
With the Messenger (from MVVM Light) I just set via MainViewModel the Views, reset the unused ViewModels (ViewModel = new VieModel(...); or ViewModel = null;) and pass the current / logged in User Object.
With the same strategy I just Logout like this
public ICommand LogoutCommand
{
get
{
return new RelayCommand(async () =>
{
await _dataService.SetUserStatusTask(CurrentUser.Id, 0);
if(CurrentUser.Id > 0 && IsLoggedIn)
await _dataService.WriteLog(CurrentUser.Id, "logout", "Programm", GetLocalAdress());
IsLoggedIn = false;
CurrentUser = new User();
Messenger.Default.Send(new NotificationMessage("GoToLoginPage"));
});
}
}
So I can log in with my running Client so often I want, but the "user_loginStatus" only sets the changes the first login time to 1 and back to 0, but when I log out then and login back with the same user, it wont change it anymore. When I login (still same running Client) with another user it sets again the first time the "user_loginstatus" to 1 and back to 0 and then only again when I restart my Client..
What could I do wrong?
This is just basically from my comment regarding the original question:
I had similiar problems several times. Usually it is based on the fact that the entity you modified can't be validated properly and your dbContext fails without a proper exception because it still holds on to false entity. If this is the case you could circumvent this problem by using scoped contexts and embedding your data access operations in a using statement.
Alternatively you could try to explicitly tell EF that the entity has changes e.g.:
_entities.Entry(user).State = EntityState.Modified;
Regarding your other question:
In theory you shouldn't have to tell EF explicitly that the entity's values have changed. Change tracking should do that automatically. The only exception i could think of, is when you try to modify an entity that is explicitly not tracked anymore. When you call _entities.Find(id) it will look in the context if it finds the object with the matching primary key value and load it. Since you already modified this object before, the context will simply get the old object you already modified to set the login status the first time.
This "old" object is probably not tracked anymore and you have to tell EF explicitly that it has changed, by changing it's state from attached to modified.
in LoginExecute() you have UserObj, but in LogoutCommand() you have CurrentUser. Is it OK?
I'm having some trouble using a helper method to perform an update to a set of model objects. The table uses a lookup table to hold 5 records per agent/user. If I want to save the record for the agent, I need to save that record onto the AgentTransmission table, and up to 5 other records on the RelationshipCodeLookup table.
Since I have to do this five times per agent, and we must do the process in the Create and Edit methods, I created a helper method to save the records. This works fine during the create process as we're simply doing a DbContext.Add(). However when I need to perform an update, I get the error message
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
I think this has to do with the fact I'm passing the model object to my helper method, and therefore the DbContext thinking that it has two separate objects to keep track of. I say this because the lines of code that are commented out work just fine and allow me to save the object. Passing the object to the helper method, however, gets the above error.
Does anyone know of a way around this (using a helper method to perform an update)?
Controller Action
//Save relationship codes in lookup table
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode1))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode1, agenttransmission.ID);
}
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode2))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode2, agenttransmission.ID);
}
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode3))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode3, agenttransmission.ID);
}
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode4))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode4, agenttransmission.ID);
}
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode5))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode5, agenttransmission.ID);
}
Helper Method
public void SaveRelationshipCodes(RelationshipCodeLookup relCode, int id)
{
if (relCode.AgentId == 0) relCode.AgentId = id;
relCode.LastChangeDate = DateTime.Now;
relCode.LastChangeId = Security.GetUserName(User);
//Check to see if record exists and if not add it
if (db.RelationshipCodeLookup.Find(id, relCode.RelCodeOrdinal) != null)
{
db.Entry(relCode).State = EntityState.Detached;
}
else
{
if(relCode.RelCodeOrdinal == 0) relCode.RelCodeOrdinal = FindOrdinal(relCode);
db.RelationshipCodeLookup.Add(relCode);
}
db.SaveChanges();
}
EDIT
After scouring the web I attempted to save via this method
//Check to see if record exists and if not add it
if (db.RelationshipCodeLookup.Find(id, relCode.RelCodeOrdinal) != null)
{
db.Entry(relCode).CurrentValues.SetValues(relCode);
}
else
{
Member 'CurrentValues' cannot be called for the entity of type 'RelationshipCodeLookup because the entity does not exist in the context. To add an entity to the context call the Add or Attach method of DbSet<RelationshipCodeLookup>
However.... doing that only puts me back at the start with the following error on db.RelationshipCodeLookup.Attach(relCode);
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
Try this:
db.RelationshipCodeLookup.Attach(relCode);
db.Entry(relCode).State = EntityState.Modified;
For updates you want to attach the detached object then set it's state to modified.
The issue here seems to be that the Entity Framework cannot track two objects of the same kind at the same time. Because of that I find the solution to this problem more than a little weird. By calling .Find() on the DbContext and instantiating a second copy of the model object I was finally able to save. Seems to break all the rules the EF was laying out for me in the error messages, but hey it works.
public void SaveRelationshipCodes(int id, RelationshipCodeLookup relCode)
{
if (relCode.AgentId == 0) relCode.AgentId = id;
relCode.LastChangeDate = DateTime.Now;
relCode.LastChangeId = Security.GetUserName(User);
//Check to see if record exists and if not add it
if (db.RelationshipCodeLookup.Find(id, relCode.RelCodeOrdinal) != null)
{
//Need to call .Find to get .CurrentValues method call to work
RelationshipCodeLookup dbRelCode = db.RelationshipCodeLookup.Find(id, relCode.RelCodeOrdinal);
db.Entry(dbRelCode).CurrentValues.SetValues(relCode);
}
else
{
if(relCode.RelCodeOrdinal == 0) relCode.RelCodeOrdinal = FindOrdinal(relCode);
db.RelationshipCodeLookup.Add(relCode);
}
db.SaveChanges();
}