I am getting the error below when I try to edit a user. I have no problem creating or deleting users. As far as I understand, somehow uniqueMail method inside fluentvalidator class and Edit method inside UserController class doesn't go well together.
Exception thrown at the:
_context.Update(user)
I think problem occurs due to incorrect usage of dbcontext inside the fluentvalidator class. I would say something is off with the disposal of the context within fluentvalidator class.
Could you please help me understand what is wrong?
I also added the StartUp class in case something is off with the implementation of FluentValidation
Error:
System.InvalidOperationException: The instance of entity type 'User' 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.
FluentValidatorClass:
using FluentValidation;
using System.Linq;
namespace UserDirectory.Models
{
public class UserValidator : AbstractValidator<User>
{
readonly MvcUserContext _context;
public UserValidator(MvcUserContext context)
{
_context = context;
RuleFor(x => x.Email).Must(uniqueMail).WithMessage("This email is already in use");
RuleFor(x => x.Phone).Must(uniquePhone).WithMessage("This phone is already in use");
}
private bool uniqueMail(User user, string email)
{
var mail = _context.User.Where(x => x.Email.ToLower() == user.Email.ToLower()).SingleOrDefault();
if (mail == null) return true;
return mail.Id == user.Id;
}
private bool uniquePhone(User user, string phone)
{
var phoneNumber = _context.User.Where(x => x.Phone == user.Phone).SingleOrDefault();
if (phoneNumber == null) return true;
return phoneNumber.Id == user.Id;
}
}
}
UserController.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using UserDirectory.Models;
namespace UserDirectory.Controllers
{
public class UsersController : Controller
{
private readonly MvcUserContext _context;
public UsersController(MvcUserContext context)
{
_context = context;
}
// GET: Users
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["SurnameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "surname_desc" : "";
var users = from u in _context.User
select u;
switch (sortOrder)
{
case "surname_desc":
users = users.OrderByDescending(u => u.Surname);
break;
case "name_desc":
users = users.OrderByDescending(u => u.Name);
break;
}
return View(await users.AsNoTracking().ToListAsync());
}
// GET: Users/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var user = await _context.User
.FirstOrDefaultAsync(m => m.Id == id);
if (user == null)
{
return NotFound();
}
return View(user);
}
// GET: Users/Create
public IActionResult Create()
{
return View();
}
// POST: Users/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Name,Surname,Email,BirthDate,Phone,Location")] User user)
{
if (ModelState.IsValid)
{
_context.Add(user);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(user);
}
// GET: Users/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var user = await _context.User.FindAsync(id);
if (user == null)
{
return NotFound();
}
return View(user);
}
// POST: Users/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Name,Surname,Email,BirthDate,Phone,Location")] User user)
{
if (id != user.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(user);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!UserExists(user.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(user);
}
// GET: Users/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var user = await _context.User
.FirstOrDefaultAsync(m => m.Id == id);
if (user == null)
{
return NotFound();
}
return View(user);
}
// POST: Users/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var user = await _context.User.FindAsync(id);
_context.User.Remove(user);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
private bool UserExists(int id)
{
return _context.User.Any(e => e.Id == id);
}
}
}
StartUp.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using FluentValidation.AspNetCore;
using UserDirectory.Models;
namespace UserDirectory
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddMvc().AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Models.UserValidator>());
//services.AddTransient<FluentValidation.IValidator<User>, UserValidator>();
services.AddDbContext<MvcUserContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("MvcUserContext")));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
On Validator you select from user and Track the same entity you would update, this cause the issue.
Instead of
var mail = _context.User.Where(x => x.Email.ToLower() == user.Email.ToLower()).SingleOrDefault();
Write
// The AsNoTracking do not add to the context the entity you select
var mail = _context.User.AsNoTracking().Where(x => x.Email.ToLower() == user.Email.ToLower()).SingleOrDefault();
With the Where
clause you materialize the entire entity User (adding AsNoTracking do not add it to context), but if you want to check only if the entity exists you instead use the Any
clause that translates to a SQL like this
SELECT TOP 1
FROM TABLE
WHERE Email = 'myEmail'
The result is not a User entity and thus is not added to context.
return _context.User.Any(x => x.Email.ToLower() == user.Email.ToLower());
This is must be done for uniqueMail
and uniquePhone