尝试为 MVC 应用程序实现存储库模式会导致并发异常

Attempt to implement repository pattern for an MVC app causes a concurrency exception

我正在学习 ASP .NET Core,我正在尝试使用存储库模式来清理我的控制器。我的想法是:

不幸的是,'Edit' 方法的完成导致 DbConcurrencyException,我已尝试使用 this 解决该问题。使用以前的解决方案会导致 InvalidOperationException,因为其中一个属性是只读的。

一些代码:

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();
        }
    }

谢谢!

有很多需要注意的地方。但我只会指出最大的一个。 DbContextApplicationDbContext 类 并不意味着长期存在和跨越。我猜 ApplicationDbContext 是一个单身人士。这是一个长期存在的对象,在不同 类 之间共享,也可能是线程。这正是您应该避免的设计模式。就微软而言 -

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.

此页面描述了问题 - https://docs.microsoft.com/en-us/ef/core/miscellaneous/configuring-dbcontext#avoiding-dbcontext-threading-issues

简而言之,使用作用域 dbcontext。

如果你正在学习,我会说你自己实现它并改变你的 类 的实现。在需要时创建和处理上下文。不要保留长期存在的上下文。

如果你只需要一个仓库,你可以使用这个包,我自己用 - https://github.com/Activehigh/Atl.GenericRepository

我设法解决了这个问题。并发异常被抛出是因为我在没有使用 UserManager<User> 的情况下创建用户(继承 IDentityUser)。检查数据库字段后,我发现 Identityuser 相关字段(如电子邮件、用户名等)为空。这是因为我只为继承了 IDentityUser.

的 class 添加了信息