#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
.