Попытка реализовать шаблон репозитория для приложения MVC вызывает исключение параллелизма

#c# #asp.net-mvc #asp.net-core #repository-pattern #database-concurrency

#c# #asp.net-mvc #asp.net-core #репозиторий-шаблон #база данных-параллелизм

Вопрос:

Я изучаю ASP .NET Core и пытаюсь использовать шаблон репозитория для очистки своих контроллеров. Я думал об этом так:

  • создайте интерфейс репозитория, содержащий основные методы для репозитория
  • реализовать ранее созданный интерфейс
  • создайте интерфейс репозитория на основе модели
  • создайте репозиторий на основе модели, расширяющий базовый репозиторий, созданный на шаге 2, и реализовал интерфейс на основе модели
  • создайте класс-оболочку, который содержит контекст и все репозитории приложений, и внедрите его в каждый контроллер

К сожалению, полный метод ‘Edit’ вызывает DbConcurrencyException проблему, которую я пытался решить с помощью этого. использование предыдущего решения приводит 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();
        }
    }
 

Спасибо!

Ответ №1:

Есть много вещей, на которые следует обратить внимание. Но я укажу только на самый большой. DbContext или ApplicationDbContext классы не должны быть долговечными и перекрестными. Я предполагаю ApplicationDbContext , что это синглтон. Который является долгоживущим объектом и является общим для разных классов, а также может быть потоками. Это именно тот шаблон проектирования, которого вам следует избегать. С точки зрения Microsoft —

Ядро Entity Framework не поддерживает несколько параллельных операций, выполняемых в одном экземпляре DbContext. Одновременный доступ может привести к неопределенному поведению, сбоям приложения и повреждению данных. Из-за этого важно всегда использовать отдельные экземпляры DbContext для операций, которые выполняются параллельно.

На этой странице описывается проблема — https://docs.microsoft.com/en-us/ef/core/miscellaneous/configuring-dbcontext#avoiding-dbcontext-threading-issues

Короче говоря, используйте dbcontext с ограниченной областью действия.

Если вы учитесь, я бы посоветовал реализовать его самостоятельно и изменить реализации ваших классов. Создавайте и удаляйте контексты, когда они вам нужны. Не сохраняйте долговременные контексты.

Если вам просто нужен репозиторий, вы можете использовать этот пакет, я использую его для себя — https://github.com/Activehigh/Atl.GenericRepository

Ответ №2:

Мне удалось это исправить. Исключение параллелизма было вызвано тем, что я создавал пользователей (которые наследовали IDentityUser ) без использования a UserManager<User> . После проверки полей базы данных я обнаружил, что Identityuser связанные поля (например, адрес электронной почты, имя пользователя и т. Д.) Были пустыми. Это было связано с тем, что я добавлял информацию только для унаследованного класса IDentityUser .