Reputation: 363
I am learning ASP .NET Core and I am trying to use repository pattern to clean up my controllers. The way I thought this out was:
Unfortunately the Complete of 'Edit' method causes a DbConcurrencyException
which I have tried to solve using this. using the previous solution causes an InvalidOperationException
as one of the properties is read-only.
For some code:
public class User : IdentityUser
{
[PersonalData]
[DisplayName("First Name")]
[Required(ErrorMessage = "The first name is required!")]
[StringLength(30, MinimumLength = 3, ErrorMessage = "The first name must be between 3 and 30 characters long!")]
public string firstName { get; set; }
[PersonalData]
[DisplayName("Last Name")]
[Required(ErrorMessage = "The last name is required!")]
[StringLength(30, MinimumLength = 3, ErrorMessage = "The last name must be between 3 and 30 characters long!")]
public string lastName { get; set; }
[PersonalData]
[DisplayName("CNP")]
[Required(ErrorMessage = "The PNC is required!")]
[StringLength(13, MinimumLength = 13, ErrorMessage = "The last name must 13 digits long!")]
[RegularExpression(@"^[0-9]{0,13}$", ErrorMessage = "Invalid PNC!")]
public string personalNumericalCode { get; set; }
[PersonalData]
[DisplayName("Gender")]
[StringRange(AllowableValues = new[] { "M", "F" }, ErrorMessage = "Gender must be either 'M' or 'F'.")]
public string gender { get; set; }
public Address address { get; set; }
}
public class Medic : User
{
[DisplayName("Departments")]
public ICollection<MedicDepartment> departments { get; set; }
[DisplayName("Adiagnostics")]
public ICollection<MedicDiagnostic> diagnostics { get; set; }
[PersonalData]
[DisplayName("Rank")]
[StringLength(30, MinimumLength = 3, ErrorMessage = "The rank name must be between 3 and 30 characters long!")]
public string rank { get; set; }
}
public class MedicController : Controller
{
private readonly IUnitOfWork unitOfWork;
public MedicController(IUnitOfWork unitOfWork)
{
this.unitOfWork = unitOfWork;
}
// GET: Medic
public async Task<IActionResult> Index()
{
return View(await unitOfWork.Medics.GetAll());
}
// GET: Medic/Details/5
public async Task<IActionResult> Details(string id)
{
if (id == null)
{
return NotFound();
}
Medic medic = await unitOfWork.Medics.FirstOrDefault(m => m.Id == id);
if (medic == null)
{
return NotFound();
}
return View(medic);
}
// GET: Medic/Create
public IActionResult Create()
{
return View();
}
// POST: Medic/Create
// To protect from overposting attacks, please 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("rank,firstName,lastName,personalNumericalCode,Id,gender,Email")] Medic medic)
{
if (ModelState.IsValid)
{
unitOfWork.Medics.Add(medic);
await unitOfWork.Complete();
return RedirectToAction(nameof(Index));
}
return View(medic);
}
// GET: Medic/Edit/5
public async Task<IActionResult> Edit(string id)
{
if (id == null)
{
return NotFound();
}
Medic medic = await unitOfWork.Medics.Get(id);
if (medic == null)
{
return NotFound();
}
return View(medic);
}
// POST: Medic/Edit/5
// To protect from overposting attacks, please 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(string id, [Bind("rank,firstName,lastName,Id,personalNumericalCode,gender,Email")] Medic medic)
{
if (id != medic.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
var saved = false;
while (!saved)
{
try
{
unitOfWork.Medics.Update(medic);
await unitOfWork.Complete();
saved = true;
}
catch (DbUpdateConcurrencyException ex)
{
if (!MedicExists(medic.Id))
{
return NotFound();
}
else
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is Medic)
{
var proposedValues = entry.CurrentValues;
var databaseValues = entry.GetDatabaseValues();
foreach (var property in proposedValues.Properties)
{
var proposedValue = proposedValues[property];
var databaseValue = databaseValues[property];
proposedValues[property] = proposedValue;
// TODO: decide which value should be written to database
// proposedValues[property] = <value to be saved>;
}
// Refresh original values to bypass next concurrency check
entry.OriginalValues.SetValues(databaseValues);
}
else
{
throw new NotSupportedException(
"Don't know how to handle concurrency conflicts for "
+ entry.Metadata.Name);
}
}
}
}
}
return RedirectToAction(nameof(Index));
}
return View(medic);
}
// GET: Medic/Delete/5
public async Task<IActionResult> Delete(string id)
{
if (id == null)
{
return NotFound();
}
Medic medic = await unitOfWork.Medics.FirstOrDefault(m => m.Id == id);
if (medic == null)
{
return NotFound();
}
return View(medic);
}
// POST: Medic/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(string id)
{
Medic medic = await unitOfWork.Medics.Get(id);
unitOfWork.Medics.Remove(medic);
await unitOfWork.Complete();
return RedirectToAction(nameof(Index));
}
private bool MedicExists(string id)
{
return unitOfWork.Medics.Any(e => e.Id == id);
}
}
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
protected readonly ApplicationDbContext context;
public Repository(ApplicationDbContext context)
{
this.context = context;
}
public void Add(TEntity entity)
{
context.Set<TEntity>().AddAsync(entity);
}
public void AddRange(IEnumerable<TEntity> entities)
{
context.Set<TEntity>().AddRangeAsync(entities);
}
public bool Any(Expression<Func<TEntity, bool>> predicate)
{
return context.Set<TEntity>().Any(predicate);
}
public async Task<IEnumerable<TEntity>> Find(Expression<Func<TEntity, bool>> predicate)
{
return await context.Set<TEntity>().Where(predicate).ToListAsync();
}
public async Task<TEntity> FirstOrDefault(Expression<Func<TEntity, bool>> predicate)
{
return await context.Set<TEntity>().FirstOrDefaultAsync(predicate);
}
public async Task<TEntity> Get(string id)
{
return await context.Set<TEntity>().FindAsync(id);
}
public async Task<IEnumerable<TEntity>> GetAll()
{
return await context.Set<TEntity>().ToListAsync();
}
public void Remove(TEntity entity)
{
context.Set<TEntity>().Remove(entity);
}
public void RemoveRange(IEnumerable<TEntity> entities)
{
context.Set<TEntity>().RemoveRange(entities);
}
public TEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate)
{
return context.Set<TEntity>().SingleOrDefault(predicate);
}
public void Update(TEntity entity)
{
context.Set<TEntity>().Update(entity);
}
}
public class MedicRepository : Repository<Medic>, IMedicRepository
{
public MedicRepository(ApplicationDbContext _context) : base(_context) { }
//TODO: add medic repository specific methods
}
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
public IMedicRepository Medics { get; private set; }
public IPatientRepository Patients { get; private set; }
public IReceptionistRepository Receptionists { get; private set; }
public IDiagnosticRepository Diagnostics { get; private set; }
public IMedicationRepository Medications { get; private set; }
public IMedicineRepository Medicine { get; private set; }
public ILabTestRepository LabTests { get; private set; }
public ILabResultRepository LabResults { get; private set; }
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
Medics = new MedicRepository(_context);
Patients = new PatientRepository(_context);
Receptionists = new ReceptionistRepository(_context);
Diagnostics = new DiagnosticRepository(_context);
Medications = new MedicationRepository(_context);
Medicine = new MedicineRepository(_context);
LabTests = new LabTestRepository(_context);
LabResults = new LabResultRepository(_context);
}
public async Task<int> Complete()
{
return await _context.SaveChangesAsync();
}
public void Dispose()
{
_context.Dispose();
}
}
Thanks!
Upvotes: 0
Views: 448
Reputation: 363
I managed to fix this out. The concurrency exception was thrown because I was creating users (which were inheriting IDentityUser
) without using a UserManager<User>
. After inspecting the database fields, I have discovered that the Identityuser
related fields (like email, username, etc..) were empty. This was due to me only adding information for the class which inherited IDentityUser
.
Upvotes: 0
Reputation: 6430
There are a lot of things to notice. But I will only point to the biggest one. DbContext
or ApplicationDbContext
classes are not meant to be long lived and cross spanned. I am guessing ApplicationDbContext
is a singleton. Which is a long lived object and is shared among different classes and also may be threads. This is exactly the design pattern you should avoid. In terms of Microsoft -
Entity Framework Core does not support multiple parallel operations being run on the same DbContext instance. Concurrent access can result in undefined behavior, application crashes and data corruption. Because of this it's important to always use separate DbContext instances for operations that execute in parallel.
This page here describes the problem - https://learn.microsoft.com/en-us/ef/core/miscellaneous/configuring-dbcontext#avoiding-dbcontext-threading-issues
In short, use scoped dbcontext.
If you are learning, I would say implement it yourself and change the implementations of your classes. Create and dispose contexts when you need them. Do not keep long lived contexts.
If you just need a repository, you can use this package, I use it for myself - https://github.com/Activehigh/Atl.GenericRepository
Upvotes: 2