Following the N-tier architecture, I'm currently having issues while trying to update an entity. In my Controller, if I set the properties manually, the update works perfectly, if I set the properties mapping the ViewModel into the entity it generate the exception "...cannot track multiple objects with the same key". How can I solve this?
This is my UnitOfWork:
public class UnitOfWork : IUnitOfWork
{
private readonly CoreContext _context;
private IGenericRepository<Currency> currencyRepository;
private static string DataConnectionString => new DatabaseConfiguration().GetDataConnectionString();
public UnitOfWork(CoreContext context)
{
var optionsBuilder = new DbContextOptionsBuilder<CoreContext>();
optionsBuilder.UseSqlServer(DataConnectionString);
_context = new CoreContext(optionsBuilder.Options);
}
public int Commit()
{
return _context.SaveChanges();
}
public async Task CommitAsync()
{
await _context.SaveChangesAsync();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
_context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public IGenericRepository<Currency> CurrencyRepository
{
get { return this.currencyRepository ?? (this.currencyRepository = new GenericRepository<Currency>(_context)); }
}
}
public interface IUnitOfWork : IDisposable
{
int Commit();
Task CommitAsync();
IGenericRepository<Currency> CurrencyRepository { get; }
}
This is my Generic Repository + CurrencyRepository:
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
internal CoreContext _context;
internal DbSet<T> _entities;
public GenericRepository(CoreContext context)
{
_context = context;
_entities = context.Set<T>();
}
public virtual void Update(T entityToUpdate)
{
_context.Entry(entityToUpdate).State = EntityState.Modified;
}
public IEnumerable<T> GetBy(Expression<Func<T, bool>> predicate)
{
IEnumerable<T> query = _entities.Where(predicate).AsEnumerable();
return query;
}
}
public interface IGenericRepository<T> where T : class
{
void Update(T entity);
IEnumerable<T> GetBy(Expression<Func<T, bool>> predicate);
}
public class CurrencyRepository : GenericRepository<Currency>, ICurrencyRepository
{
public CurrencyRepository(CoreContext context)
: base(context) {
}
}
This is my Service:
public class CurrencyService : ICurrencyService
{
private readonly IUnitOfWork _unitOfWork;
public void UpdateCurrency(Currency currency)
{
_unitOfWork.CurrencyRepository.Update(currency);
}
public Currency GetCurrencyById(int Id)
{
return _unitOfWork.CurrencyRepository.GetBy(x => x.CurrencyId == Id).Single();
}
public int SaveChanges()
{
return _unitOfWork.Commit();
}
}
public interface ICurrencyService
{
Currency GetCurrencyById(int Id);
void UpdateCurrency(Currency currency);
int SaveChanges();
}
And finally, This is my Controller:
public class CurrencyController : Controller
{
private readonly ICurrencyService _currencyService;
private readonly IMapper _mapper;
public CurrencyController(ICurrencyService currencyService, IMapper mapper)
: base()
{
_currencyService = currencyService;
_mapper = mapper;
}
[HttpPost]
public ActionResult UpdateCurrency([DataSourceRequest] DataSourceRequest dsRequest, CurrencyViewModel currency)
{
if (currency != null && ModelState.IsValid)
{
var currencyToUpdate = _currencyService.GetCurrencyById(currency.CurrencyId);
if (currencyToUpdate != null)
{
//UPDATE NOT WORKING
//currencyToUpdate = _mapper.Map<CurrencyViewModel, Currency>(currency);
//UPDATE WORKING
currencyToUpdate.Description = currency.Description;
currencyToUpdate.Code= currency.Code;
currencyToUpdate.Symbol = currency.Symbol;
_currencyService.UpdateCurrency(currencyToUpdate);
_currencyService.SaveChanges();
}
}
return Json(ModelState.ToDataSourceResult());
}
It doesn't work in your case because you are using
IMapper.Map
overload which creates a new instance of thecurrency
object. Because of the fact that yourGetCurrencyById
track your entity after successful fetch, you'll end up with the exception since you'll have to instances with the same key (One tracked already in DbContext, and new one which mapper created).There are two things you can do to prevent it:
AsNoTracking()
method when fetching from databaseUse
IMapper.Map
overload which takes both destination and source instances ofcurrency
:_mapper.Map(currency, currencyToUpdate);