I have a problem concerning entities in ASP.NET Core.
I use Entity Framework Core as data access library.
The issue I've come across happens when I'm trying to update an entity. After I modify the properties and call SaveChanges, the entity gets deleted and I don't understand why.
Here's the entity:
public class Contract
{
public int Id { get; set; }
[Required]
public DateTime ExpiryDate { get; set; }
[Required]
[Range(0, float.MaxValue)]
public float MonthlyFee { get; set; }
[Required]
public string UserId { get; set; }
[Required]
public int CarId { get; set; }
public User User { get; set; }
public Car Car { get; set; }
}
Here's the related entities for reference:
public class User : IdentityUser
{
[Required]
[PersonalData]
public string Name { get; set; }
[Required]
[PersonalData]
public string Surname { get; set; }
[Required]
[PersonalData]
public string TaxCode { get; set; }
[Required]
[PersonalData]
[DataType(DataType.Date)]
public DateTime DateOfBirth { get; set; }
public string ProfilePictureUrl { get; set; }
public Contract Contract { get; set; }
public ICollection<CarAccident> CarAccidents { get; set; }
}
public class Car
{
public int Id { get; set; }
[Required]
[RegularExpression("[A-Z][A-Z][0-9][0-9][0-9][A-Z][A-Z]")]
public string LicensePlate { get; set; }
public int CarModelId { get; set; }
public string FittingDescription { get; set; }
public Contract Contract { get; set; }
public ICollection<CarAccident> CarAccidents { get; set; }
public CarModel CarModel { get; set; }
}
Here's my update method in repository:
public async Task<Contract> Update(Contract entity)
{
var dbContract = await GetById(entity.Id);
if (dbContract == null)
return null;
var dbUser = await _userRepository.GetById(entity.UserId);
if (dbUser == null)
return null;
var dbCar = await _carRepository.GetById(entity.CarId);
if (dbCar == null)
return null;
dbContract.ExpiryDate = entity.ExpiryDate;
dbContract.User = entity.User;
dbContract.Car = dbCar;
dbContract.User = dbUser;
//_context.Contracts.FromSqlInterpolated($"UPDATE dbo.Contracts SET ExpiryDate={entity.ExpiryDate}, MonthlyFee={entity.MonthlyFee} WHERE Id={entity.Id}");
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
return null;
}
return await GetById(entity.Id);
}
Has anyone got any idea how to solve this?
UPDATE:
This is the new Update method:
public async Task<Contract> Update(Contract entity)
{
var dbContract = await GetById(entity.Id);
if (dbContract == null)
return null;
var dbUser = await _userRepository.GetById(entity.UserId);
if (dbUser == null)
return null;
var dbCar = await _carRepository.GetById(entity.CarId);
if (dbCar == null)
return null;
dbContract.ExpiryDate = entity.ExpiryDate;
dbContract.Car = dbCar;
dbContract.User = dbUser;
//_context.Contracts.FromSqlInterpolated($"UPDATE dbo.Contracts SET ExpiryDate={entity.ExpiryDate}, MonthlyFee={entity.MonthlyFee} WHERE Id={entity.Id}");
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
return null;
}
return await GetById(entity.Id);
}
Here's the Fluent API configuration:
private void _configureUsers(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOne(u => u.Contract)
.WithOne(c => c.User)
.HasForeignKey<Contract>(c => c.UserId);
}
private void _configureCars(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasAlternateKey(c => c.LicensePlate);
modelBuilder.Entity<Car>()
.HasOne(c => c.Contract)
.WithOne(c => c.Car)
.HasForeignKey<Contract>(c => c.CarId);
}
Both this methods get called in the OnModelCreating method of the context.
I've finally managed to solve my issue.
I was already tracking the entity in my api controller like that:
[HttpPut("{id}")]
[Authorize(Roles = "Backoffice")]
public async Task<ActionResult<ContractDTO>> PutContract(int id, [FromBody] PutContractViewModel viewModel)
{
if (viewModel == null || !ModelState.IsValid)
return BadRequest(new { message = "Your model is wrong" });
var contract = await _contractService.GetContractDTO(id);
if (contract == null)
return NotFound();
var modifiedContract = await _contractService.UpdateContract(viewModel);
if (modifiedContract == null)
return BadRequest(new { message = "User or car may be busy in another contract" });
return Ok(modifiedContract);
}
This type of approach works in one to many relationships, but evidently when you have one to one relationship and you have to objects that rapresent the same entity the ChangeTracker cannot track the changes correctly.
I post my new controller and repository code if someone will burst into my same problem.
Controller:
[HttpPut("{id}")]
[Authorize(Roles = "Backoffice")]
public async Task<ActionResult<ContractDTO>> PutContract(int id, [FromBody] PutContractViewModel viewModel)
{
if (viewModel == null || !ModelState.IsValid)
return BadRequest(new { message = "Your model is wrong" });
ContractDTO modifiedContract;
try
{
modifiedContract = await _contractService.UpdateContract(viewModel);
}
catch (EntityNotFoundException)
{
return NotFound();
}
if (modifiedContract == null)
return BadRequest(new { message = "User or car may be busy in another contract" });
return Ok(modifiedContract);
Service:
public async Task<ContractDTO> UpdateContract(PutContractViewModel viewModel)
{
try
{
return await ParseContractToContractDTO(await _contractRepository.Update(ParsePutContractViewModelToContract(viewModel)));
}
catch(EntityNotFoundException)
{
throw;
}
}
Repository:
public async Task<Contract> Update(Contract entity)
{
var dbContract = await _context.Contracts.Include(c => c.User).Include(c => c.Car).FirstOrDefaultAsync(c => c.Id == entity.Id);
if (dbContract == null)
{
throw new EntityNotFoundException();
}
var dbUser = await _context.Users.Include(u => u.Contract).FirstOrDefaultAsync(u => u.Id == entity.UserId);
if (dbUser == null)
return null;
var dbCar = await _context.Cars.Include(c => c.Contract).FirstOrDefaultAsync(c => c.Id == entity.CarId);
if (dbCar == null)
return null;
dbContract.ExpiryDate = entity.ExpiryDate;
dbContract.MonthlyFee = entity.MonthlyFee;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
return null;
}
return await GetById(entity.Id);
}
I want to thank you all, you've been very helpful and patient with me.
Related
I started learning C# and I want to update my model using the [HttpPost] annotation. I tried removing the [FromBody]Item itm parameter and the fields on the repository but it's not working either. Below is my code.
Controller:
[HttpPost("{id}")]
public ActionResult<Item> UpdateItem([FromBody]Item itm, int id)
{
var getItem = _repository.GetItemById(id);
if (getItem == null)
{
return NotFound();
}
_repository.UpdateItem(itm);
_repository.SaveChanges();
return Ok(getItem);
}
Repository:
public void UpdateItem(Item itm)
{
if (itm == null)
{
throw new ArgumentNullException(nameof(itm));
}
var itemToUpdate = this.GetItemById(itm.Id);
if (itm.Name != null)
{
itemToUpdate.Name = itm.Name;
}
itemToUpdate.Price = itm.Price;
itemToUpdate.Condition = itm.Condition;
itemToUpdate.Size = itm.Size;
itemToUpdate.DateSold = itm.DateSold;
itemToUpdate.SellMethod = itm.SellMethod;
_context.Items.Update(itemToUpdate);
}
Interface:
void UpdateItem(Item itm);
Model:
public int Id { get; set; }
[Required]
public string Name { get; set; }
public int Price { get; set; }
public string Condition { get; set; }
public string Size { get; set; }
public string DateSold { get; set; }
public string SellMethod { get; set; }
First of all verify that you're sending that item correctly:
Is the form correct and pointing to that method of your controller?
Are you sending that item via the form (have you used the provided methods for this) ?
After that, if you're sending the item in the body of your post request, then verify the item in the method's parameter is available.
EDIT:
Well, as already discussed with Panagiotis you should rather directly use the DbContext itself as it already provides everything you need.
[HttpPost("{id}")]
public ActionResult<Item> UpdateItem(int id, [FromBody]Item itemData)
{
var foundItem = _dbContext.Items.SingleOrDefault(x => x.Id == id);
if (foundItem == null)
{
return NotFound();
}
foundItem.Name = itemData.Name;
foundItem.Size = itemData.Size;
// and so on
_dbContext.SaveChanges();
return Ok(foundItem);
}
Another way to keep your current structure, but it's not recommended, would be the following:
[HttpPost("{id}")]
public ActionResult<Item> UpdateItem(int id, [FromBody]Item itemData)
{
var updatedItem = _repository.UpdateItem(id, itemData);
if (updatedItem == null)
{
return NotFound();
}
return Ok(updatedItem);
}
public void UpdateItem(int id, Item itemData)
{
// you can validate parameters and throw errors if e.g. itemData == null
var originalItem = GetItemById(id); // this should internally get the item e.g. _dbContext.Items.Where(x => x.id == itemData.Id);
if(originalItem == null)
{
return null;
}
originalItem.Name = itemData.Name;
originalItem.Price = itemData.Price;
originalItem.Condition = itemData.Condition;
originalItem.Size = itemData.Size;
originalItem.DateSold = itemData.DateSold;
originalItem.SellMethod = itemData.SellMethod;
SaveChanges(); // guess this will be _dbContext.SaveChanges() instead
return originalItem;
}
Well, you could also change it to first load the item and then pass the originalItem and the itemData into the UpdateItem method inside your repository. But as you see the better way to directly use the DbContext is more clearer and shorter.
I'm running into a loss with this one and new to Blazor and trying to learn it (.NET 5)
I have a simple model:
public class Status
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime LastUpdatedOn { get; set; }
}
In Startup.cs, I register my db and services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Default")),(ServiceLifetime.Scoped));
services.AddAutoMapper(typeof(Startup));
services.AddScoped<IStatusRepo, StatusRepo>();
services.AddScoped<IStatusService, StatusService>();
The code behind file of my component is below. The GetStatus call returns a different dto than the update status service method accepts so I convert it in the UpdateStatus method.
public partial class EditStatus : ComponentBase
{
[Parameter]
public int StatusId { get; set; }
[Inject]
IStatusService StatusService { get; set; }
[Inject]
NavigationManager navigationManager { get; set; }
public bool ShowLoadError { get; set; } = false;
private StatusDetailDto StatusToEdit { get; set; } = new StatusDetailDto();
protected override async Task OnInitializedAsync()
{
await GetStatusToEdit(StatusId);
}
public async Task GetStatusToEdit(int statusId)
{
try
{
StatusToEdit = await StatusService.GetStatusById(statusId);
}
catch (Exception ex)
{
ShowLoadError = true;
}
}
protected async void UpdateStatus()
{
UpdateStatusDto updatedStatus = new UpdateStatusDto();
updatedStatus.Id = StatusToEdit.Id;
updatedStatus.Name = StatusToEdit.Name;
StatusDetailDto statusUpdated = await StatusService.UpdateStatus(updatedStatus);
if (statusUpdated != null)
{
navigationManager.NavigateTo("status/manage");
}
else
{
Console.WriteLine("No here");
}
}
}
The service layer is:
public async Task<StatusDetailDto> UpdateStatus(UpdateStatusDto statusDto)
{
Status statusToUpdate = mapper.Map<UpdateStatusDto, Status>(statusDto);
Status updatedStatus = await statusRepo.UpdateStatus(statusToUpdate);
return mapper.Map<Status, StatusDetailDto>(updatedStatus);
}
And the repo
public async Task<Status> UpdateStatus(Status Status)
{
if (Status is null)
throw new ArgumentNullException("Nulls are not allowed.");
try
{
Status.LastUpdatedOn = DateTime.Now;
// THIS LINE THROWS THE ERROR!
_dbContext.Entry(Status).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
return Status;
}
catch (Exception)
{
throw;
}
}
Perhaps my service lifetimes are wrong? I've tried transient lifetime for my dbcontext too. What could throw this error?
The instance of entity type 'Status' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.
At first fix startup file by removing ServiceLifetime.Scoped from AddDBContext
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Default")));
Then change code of your UpdateStatus
public async Task<Status> UpdateStatus(Status status)
{
if (status is null) throw new ArgumentNullException("Nulls are not allowed.");
try
{
var existedItem=_dbContext.Set<Status>().FirstOrDefaultAsync(i=i.Id==status.id);
if(existedItem==null) throw new ArgumentNullException("Item is not found.");
existedItem.LastUpdatedOn = DateTime.Now;
_dbContext.Entry(existedItem).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
return Status;
}
catch (Exception)
{
throw;
}
}
We have visited quite a few links on EF Core many to many update, yet could not figure a concrete answer to our question and clear our understanding.
Scenario:
We wish to add/update an entity and its related many to many relations in one go like (dbset.Add() or dbset.Update())
We were trying the following and could only add/update the parent entity and not the many-to-many relation list. Can you help us know where we are wrong? and what can be done?
Current Model Structure:
public class Teacher
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity), Required]
public long Id { get; set; }
public string Name { get; set; }
public List<TeacherDuty> TeacherDuties { get; set; }
}
public class Duty
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity), Required]
public long Id { get; set; }
public string Description { get; set; }
public List<TeacherDuty> TeacherDuties { get; set; }
}
public class TeacherDuty
{
public long TeacherId { get; set; }
public Teacher Teacher { get; set; }
public long DutyId { get; set; }
public Duty Duty { get; set; }
}
And we are trying to add/update using following methods:
public async Task<Teacher> AddTeacher(Teacher pTeacher)
{
try
{
return await _teacher.AddAsync(pTeacher);
}
catch (Exception ex) { throw ex; }
}
public async Task<Teacher> UpdateTeacher(Teacher pTeacher)
{
try
{
return await _teacher.Update(pTeacher);
}
catch (Exception ex) { throw ex; }
}
Kindly point us to our misinterpretation of concept and solution if possible.
Thanks.
I create a demo to add and edit a teacher.(_context is database context)
Add a teacher:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Teacher teacher)
{
//get your desired dutyId with your own logic
var SelectedDutyIds = new int[] { 1 };
var teacherDuties = new List<TeacherDuty>();
if (ModelState.IsValid)
{
_context.Add(teacher);
await _context.SaveChangesAsync();
foreach (var id in SelectedDutyIds)
{
var item = new TeacherDuty()
{
TeacherId = teacher.Id,
DutyId = id,
};
teacherDuties.Add(item);
}
_context.AddRange(teacherDuties);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(teacher);
}
Edit the teacher: remove all the existing TeacherDuties of the teacher firstly and then add new ones.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(long id, Teacher teacher)
{
if (id != teacher.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
//your new dutyIds
var newSelectedDutyIds = new int[] { 3 };
var teacherDuties = new List<TeacherDuty>();
var tdList = await _context.TeacherDuties.Where(td => td.TeacherId == teacher.Id).ToListAsync() ;
_context.RemoveRange(tdList);
foreach (var newid in newSelectedDutyIds)
{
var item = new TeacherDuty()
{
TeacherId = teacher.Id,
DutyId = newid,
};
teacherDuties.Add(item);
}
_context.AddRange(teacherDuties);
_context.Update(teacher);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TeacherExists(teacher.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(teacher);
}
Refer to Entity framework core update many to many
I´m working on a database with movies and genres (Many to Many). I have created an API and I´m struggling with the DTO's and the Many to Many relationship.
public class MovieDto
{
public int Id { get; set; }
public string Name { get; set; }
public string OriginalTitel { get; set; }
public DateTime ReleseDate { get; set; }
public int Duration { get; set; }
public string Description { get; set; }
public int? Rating { get; set; }
public int? PersonalRating { get; set; }
public int AgeRestrictionId { get; set; }
public ICollection<GenreDto> Genres { get; set; }
}
public class GenreDto
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsChecked { get; set; }
}
With the help of another Post, I have created this Mapping
public MappingProfile()
{
Mapper.CreateMap<Movie, MovieDto>();
Mapper.CreateMap<Genre, GenreDto>()
.ForMember(d => d.Id, opt => opt.MapFrom(s => s.Id))
.ForMember(d => d.Name, opt => opt.MapFrom(s => s.Name));
Mapper.CreateMap<MovieDto, Movie>()
.AfterMap((s, d) =>
{
foreach (var Genre in d.Genres)
Genre.Id = s.Id;
});
Mapper.CreateMap<GenreDto, Movie>()
.ForMember(d => d.Id, opt => opt.MapFrom(s => s.Id));
}
And this is my API Controller
public class MoviesController : ApiController
{
private ApplicationDbContext _context;
public MoviesController()
{
_context = new ApplicationDbContext();
}
// GET /api/movies
public IHttpActionResult GetMovies()
{
return Ok(_context.Movies.ToList().Select(Mapper.Map<Movie, MovieDto>));
}
//GET /api/movies/1
public IHttpActionResult GetMovie(int id)
{
var movie = _context.Movies.SingleOrDefault(m => m.Id == id);
if (movie == null)
return NotFound();
return Ok(Mapper.Map<Movie, MovieDto>(movie));
}
//POST /api/movies
[HttpPost]
public IHttpActionResult CreateMovie (MovieDto movieDto)
{
if (!ModelState.IsValid)
return BadRequest();
var movie = Mapper.Map<Movie>(movieDto);
_context.Movies.Add(movie);
_context.SaveChanges();
return Created(new Uri(Request.RequestUri + "/" + movie.Id), movieDto);
}
//PUT /api/movies/1
[HttpPut]
public void UpdateMovie(int id, MovieDto movieDto)
{
if (!ModelState.IsValid)
throw new HttpResponseException(HttpStatusCode.BadRequest);
var movieInDb = _context.Movies.SingleOrDefault(m => m.Id == id);
if (movieInDb == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
Mapper.Map(movieDto, movieInDb);
_context.SaveChanges();
}
//DELETE api/movies/1
[HttpDelete]
public void DeleteMovie(int id)
{
var movieInDb = _context.Movies.SingleOrDefault(m => m.Id == id);
if (movieInDb == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
_context.Movies.Remove(movieInDb);
_context.SaveChanges();
}
}
If I check the functionality with Postman, I can get the Data from the Database but if I try to Post something I get an error. The reason is the Genre but I don´t know if my mapping is wrong or if I need a kind of connection to my viewModel for the combination of the Movie and the Genre.
I tried a lot of things but I don´t really understand how to go with an API, Many to Many, Dtos, viewModel and Automapper. Can somebody give me an example and an explanation?
Thanks a lot!
My entity looks as follows:
public class AddPatientReportDentalChartInput : IInputDto
{
[Required]
[MaxLength(PatientReportDentalChart.TeethDesc)]
public string Image { get; set; }
[Required]
public virtual int PatientID { get; set; }
[Required]
public virtual int TeethNO { get; set; }
public string SurfaceDefault1 { get; set; }
public string SurfaceDefault2 { get; set; }
public string SurfaceDefault3 { get; set; }
public string SurfaceDefault4 { get; set; }
public string SurfaceDefault5 { get; set; }
}
And the method by which i want to update is:
public async Task addPatientReportDentalChart(AddPatientReportDentalChartInput input)
{
var pid = input.PatientID;
var chartdetails = _chartReportRepository
.GetAll()
.WhereIf(!(pid.Equals(0)),
p => p.PatientID.Equals(pid)).ToList();
if (chartdetails.Count>0)
{
//Update should be apply here
//please suggest me the solution using updatesync
}
else
{
var patientinfo = input.MapTo<PatientReportDentalChart>();
await _chartReportRepository.InsertAsync(patientinfo);
}
}
What is the equivalent of InsertAsync when I want to update an existing entity? Is there an UpdateAsync equivalent method?
Updating an entity in Entity Framework requires you to retrieve the record, update it and then save changes. It will look roughly like this:
public async Task AddPatientReportDentalChartAsync(AddPatientReportDentalChartInput input)
{
var pid = input.PatientID;
var chartdetails = _chartReportRepository
.GetAll()
.WhereIf(!(pid.Equals(0)),
p => p.PatientID.Equals(pid)).ToList();
if (chartdetails.Count > 0)
{
var entity = await _chartReportRepository
.YourTableName
.FindAsync(entity => entity.SomeId == matchingId);
entity.PropertyA = "something"
entity.PropertyB = 1;
await _chartReportRepository.SaveChangesAsync();
}
else
{
var patientinfo = input.MapTo<PatientReportDentalChart>();
await _chartReportRepository.InsertAsync(patientinfo);
}
}
Try this if you're using .NET CORE 3.1
public async Task<int> UpdateChat(MChat mChat)
{
try
{
return await Task.Run(() =>
{
BDContext.Chat.Update(new Chat
{
Id = mChat.id,
UsuarioIdInicia = mChat.usuarioIdInicia,
UsuarioIdFinaliza = mChat.usuarioIdFinaliza,
EstadoChatId = mChat.estadoChatId
});
return BDContext.SaveChanges();
});
}
catch (Exception ex)
{
Console.WriteLine(Constantes.ERROR_DETECTADO + ex.InnerException.ToString());
return Constantes.ERROR_1;
}
}