0

I have an issue that has been plaguing me for a few hours, I've been able to narrow it down to the exact code block, but I can't seem to make any further progress, I'm still fairly new to EF6 so I might not be 100% current on the best practices.

I have a User model;

public class User
{
    public Guid ID { get; set; }

    [DisplayName("Display Name")]
    [Required]
    public string DisplayName { get; set; }

    [DisplayName("Email Address")]
    [DataType(DataType.EmailAddress, ErrorMessage = "Please enter a valid email address")]
    [Required]
    public string EmailAddress { get; set; }

    public string Company { get; set; }

    [DisplayName("Internal ID")]
    public string InternalId { get; set; }

    [DisplayName("User Status")]
    public UserStatus Status { get; set; }

    [DisplayName("Access Request Notifications")]
    public bool SendNotifications { get; set; }

    [DisplayName("Admin")]
    public bool IsAdmin { get; set; }

    public virtual ICollection<Transaction> Submissions { get; set; }
}

Within my Views the users within the database will be enumerated and displayed to the user, this is all working wonderfully.

On my Create action on the User controller ([HTTP Post]), I am running the check below to see if the email address that is being passed already exists within the database and if it does, it returns a message back to the user informing them and prevents the user from being created.

    public ActionResult Create([Bind(Include = "DisplayName,EmailAddress,Company,InternalId,Status,SendNotifications,IsAdmin")] User user)
    {
        try
        {
            user.ID = Guid.NewGuid();

            if (ModelState.IsValid)
            {
                var existingUser = db.Users.FirstOrDefault(x => x.EmailAddress.Equals(user.EmailAddress, StringComparison.InvariantCultureIgnoreCase));

                if(existingUser == null)
                {
                    db.Users.Add(user);
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
                else
                {
                    StaticConfig.Trace.Trace(SFTraceEvents.DbFailedAddingUser1, string.Format("User with email address '{0}' already exists in database", user.EmailAddress));
                    ViewData.Add("DbError", string.Format("Creation failed. User with email address '{0}' already exists", user.EmailAddress));
                }
            }
        }
        catch (Exception ex)
        {
            StaticConfig.Trace.Trace(SFTraceEvents.DbFailedAddingUser1, ex);
            ViewData.Add("DbError", "Unable to create user, an internal error has occured. Please try again, if the problem persists, please contact your system administrator.");
        }

        return View(user);
    }

This process works fine, I'm not using the built in 'Find()' method since this only seems to search on the Primary key on an entity and I want to find on something other than the PK.

When I try and implement the same logic on my Edit method, I'm encountering the error below.

Exception: [InvalidOperationException : System.InvalidOperationException: Attaching an entity of type Models.User failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

My Edit method code is currently the following:

    public ActionResult Edit([Bind(Include = "ID,DisplayName,EmailAddress,Company,InternalId,Status,SendNotifications,IsAdmin")] User user)
    {
        try
        {
            if (ModelState.IsValid)
            {
                var existingUser = db.Users.FirstOrDefault(x => x.EmailAddress.Equals(user.EmailAddress, StringComparison.InvariantCultureIgnoreCase));

                if(existingUser == null || existingUser.ID.Equals(user.ID))
                {
                    db.Entry(user).State = EntityState.Modified;
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
                else
                {
                    StaticConfig.Trace.Trace(SFTraceEvents.DbFailedUpdatingUser2, user.DisplayName, string.Format("Email address '{0}' already exists in database", user.EmailAddress));
                    ViewData.Add("DbError", string.Format("Unable to save changes, email address '{0}' already exists", user.EmailAddress));
                }
            }
        }
        catch(Exception ex)
        {
            StaticConfig.Trace.Trace(SFTraceEvents.DbFailedUpdatingUser2, user.DisplayName, ex);
            ViewData.Add("DbError", "Unable to save changes, an internal error has occured. Please try again, if the problem persists, please contact your system administrator.");
        }
        return View(user);
    }

So I am checking to see if there is an existing user with the email address that has been entered on the edit page, I am then checking to see if the ID of the user being edited, matches that of the user found within the database, if so, then of course the same email address should be allowed since we are editing the user that this address belongs to. If however the IDs are different, then there is another user in the database using the email address that has been entered on the edit page.

If I remove the 'var existingUser' and the subsequent if() statement that carries out the ID checks then the edit goes through fine, but then I run the risk of having several users with the same email address on the system. When I put the check back in, then I get the exception above.

Anyone got any suggestions on what I might be doing wrong....is there a more efficient way I can check for entities that might already contain certain data?

Edit I have found that in EF6.1, it supports an 'Index' data annotation that seems to allow a unique property to be set within it as well. I need to have a look properly, but this might offer what I'm looking for.

EF6.1 Index Attribute

I also know I could do some raw SQL queries to resolve this, but if possible, I'd like to avoid this.

Jak Hammond
  • 1,460
  • 3
  • 11
  • 24
  • Also, Unique Constraints were triaged, and tagged as postponed: "Update: this feature has been postponed and will not be included in Entity Framework 5", pretty cool if it made it's way back in to EF 6.1 – Claies Nov 19 '14 at 23:23
  • Yeah I'm using EF 6.1 which is why I thought they could be a possible solution, but I've used the answer below and it works just as good, it looks like it'll be a lot more flexible as well so I can expand it and add additional checks in future should I need to – Jak Hammond Nov 20 '14 at 20:09
  • You might have a look at my answer on [ASP.NET MVC - Attaching an entity of type 'MODELNAME' failed because another entity of the same type already has the same primary key value](http://stackoverflow.com/questions/23201907/asp-net-mvc-attaching-an-entity-of-type-modelname-failed-because-another-ent/39557606#39557606). – Murat Yıldız Sep 18 '16 at 12:32

2 Answers2

1

If you want to make sure that there are no duplicate email addresses in the database, including preventing a user from entering a new email address that is already in use, I would solve it this way:

try
{
    // Make sure that there is an existing user, since this is an edit
    var existingUser = db.Users.FirstOrDefault(x => x.ID.Equals(user.Id));
    if (existingUser == null)
        throw new ValidationException("User not found");

    // Validate that the email address is unique, but only if it has changed
    if (!existingUser.EmailAddress.Equals(user.EmailAddress) &&
        db.Users.Any(x => x.EmailAddress.Equals(user.EmailAddress, StringComparison.InvariantCultureIgnoreCase)))
        throw new Exception(string.Format("Email address '{0}' is already in use", user.EmailAddress));

    // Move data to existing entity
    db.Entry(existingUser).CurrentValues.SetValues(user);

    db.SaveChanges();
}
catch (Exception ex)
{
    StaticConfig.Trace.Trace(SFTraceEvents.DbFailedUpdatingUser2, user.DisplayName,
        string.Format(ex.Message));
    ViewData.Add("DbError", ex.Message);
}

This way of pulling the existing data and mapping properties over is the best way to solve updates, in my experience. You'll be able to do extensive validation involving both the new and the old data etc.

There are several different ways to handle errors. I prefer to throw exceptions and then act on them in the catch clause, but there are other ways too, of course.

Daniel Persson
  • 2,171
  • 1
  • 17
  • 24
  • I already have '@Html.HiddenFor(model => model.ID)' present in my Edit.cshtml View. The existing user is allowed to be null as this is going to be a user that could already be present within the database, if it's null, then the email address that is being submitted is not currently in use within the database. If existingUser is not null, then a user within the database is already using this address and I need to see if the user is the user currently being edited or if it's a different one entirely. – Jak Hammond Nov 19 '14 at 22:13
  • OK, I see. So what you're trying to do is make sure that there are no duplicate email addresses in the database, and this should also cover a user having an old valid email address that tries to change the address to one that is already used? – Daniel Persson Nov 19 '14 at 22:25
  • That's right, sorry my initial description might have been a bit convoluted. There is a third case as well where the user updates some details, but not their email address, hence the checking on the user.Id. I have updated the question above with a resource I've just found that might solve my problem but I'm still looking into it. – Jak Hammond Nov 19 '14 at 22:31
  • I'll give this ago and see where it gets me, thanks! :) – Jak Hammond Nov 20 '14 at 14:07
  • Worked perfectly, there was a small change in how the current values of the existing users were updated, I've edited the code above to show the syntax I ended up using, but the issue looks all resolved now, thanks again! – Jak Hammond Nov 20 '14 at 14:45
1

The problem you have here is the expectation you have set. The ONLY time this function would run without error is if you are changing the e-mail address. If you are editing any other field, you have a consistency issue.

When you pass the user in to this function, Entity Framework isn't currently tracking that instance. You then go and do a search in the database, and find an existingUser from the database, which Entity Framework is now tracking. You then try to "attach" the first user instance to Entity Framework (via the .State change), but Entity Framework is already tracking the original, not modified version.

What you should do instead is merge the objects if there is an existing tracked item in the database, like so:

if(existingUser == null || existingUser.ID.Equals(user.ID))
{
    attachedUser = db.Entry(existingUser);
    attachedUser.CurrentValues.SetValues(user);
    db.SaveChanges();
    return RedirectToAction("Index");
}
Claies
  • 22,124
  • 4
  • 53
  • 77
  • But this doesn't work if existingUser is null, does it? `attachedUser = db.Entry(null);`, I mean... – Daniel Persson Nov 19 '14 at 22:46
  • 1
    honestly, the right way to do this is a combination of my code and the code of @DanielPersson. His code is more complete, and he updated his answer to integrate my part of the solution. – Claies Nov 19 '14 at 22:58
  • 1
    Yes, the CurrentValues.SetValues solution was a nice one! I wasn't aware that existed, so I've shamelessly updated my question with this now :) Thx – Daniel Persson Nov 19 '14 at 22:59
  • Ah ok, reading your explanation now the current error actually makes a lot more sense now! I've had a concurrency issue previously, but it went over my head this time. Thanks for the reasoning :) – Jak Hammond Nov 20 '14 at 14:07