Why is Entity Framework trying to update my child entity? - c#

I have a Blazor server project where we have a Game and it has related child entities such as Venue, Season and GameType.
public Game()
{
Results = new HashSet<Result>();
}
public int GameId { get; set; }
public int SeasonId { get; set; }
public int VenueId { get; set; }
public int GameTypeId { get; set; }
[Required]
public DateTime GameDate { get; set; } = DateTime.Today;
public int BuyIn { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public virtual Season Season { get; set; }
public virtual Venue Venue { get; set; }
public virtual GameType GameType { get; set; }
public virtual ICollection<Result> Results { get; set; }
I have a repo that is injected into my page to handle the update:
...
public async Task UpdateGame(Game game)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
context.Games.Update(game);
await context.SaveChangesAsync();
}
}
...
#page "/Settings/Games/EditGame/{GameId:int}"
#using SuttonPokerBlazor.Components
#using SuttonPokerBlazor.Models
#using SuttonPokerBlazor.Repositories.Interfaces
#inject IGameRepository gamesRepository
#inject NavigationManager NavigationManager
#inject ISeasonRepository seasonsRepository
#inject IVenueRepository venueRepository
#inject IGameTypeRepository gameTypeRepository
#if (Game != null)
{
<h3>Add new game</h3>
<EditForm Model="#Game" OnValidSubmit="Save">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="Season" class="form-label">Season</label>
<div class="col-md-4">
<InputSelect #bind-Value="#Game.SeasonId" class="form-control">
#foreach (Season season in seasons)
{
<option value="#season.SeasonId">#season.SeasonDescription</option>
}
</InputSelect>
</div>
<ValidationMessage For="#(() => Game.SeasonId)" />
</div>
<div class="mb-3">
<label for="Season" class="form-label">Venue</label>
<div class="col-md-4">
<InputSelect #bind-Value="#Game.VenueId" class="form-control">
#foreach (Venue venue in venues)
{
<option value="#venue.VenueId">#venue.VenueName</option>
}
</InputSelect>
</div>
<ValidationMessage For="#(() => Game.VenueId)" />
</div>
<div class="mb-3">
<label for="Season" class="form-label">Game Type</label>
<div class="col-md-4">
<InputSelect #bind-Value="#Game.GameTypeId" class="form-control">
#foreach (GameType gameType in gameTypes)
{
<option value="#gameType.GameTypeId">#gameType.GameTypeDescription</option>
}
</InputSelect>
</div>
<ValidationMessage For="#(() => Game.GameTypeId)" />
</div>
<div class="mb-3">
<label for="GameDate" class="form-label">Game Date</label>
<div class="col-md-4">
<InputDate class="form-control" #bind-Value="Game.GameDate" />
</div>
<ValidationMessage For="#(() => Game.GameDate)" />
</div>
<div class="mb-3">
<label for="BuyIn" class="form-label">Buy In</label>
<div class="col-md-4">
<InputNumber class="form-control" #bind-Value="Game.BuyIn" />
</div>
<ValidationMessage For="#(() => Game.BuyIn)" />
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" #onclick="Cancel">Back</button>
<button type="button" class="btn btn-danger" #onclick="Delete">Delete</button>
</div>
</EditForm>
<Confirm #ref="DeleteConfirmation" ConfirmationChanged="ConfirmDelete"
ConfirmationMessage=#($"Are you sure you want to delete {Game.GameDate} - {Game.GameType.GameTypeDescription}?")>
</Confirm>
}
else
{
<h3>Not found</h3>
}
#code {
[Parameter]
public int GameId { get; set; }
public Game Game { get; set; }
ConfirmBase DeleteConfirmation { get; set; }
List<Season> seasons { get; set; }
List<Venue> venues { get; set; }
List<GameType> gameTypes { get; set; }
protected override async Task OnInitializedAsync()
{
Game = await gamesRepository.GetGame(GameId);
seasons = await seasonsRepository.GetSeasons();
venues = await venueRepository.GetVenues();
gameTypes = await gameTypeRepository.GetGameTypes();
}
private async Task Save()
{
await gamesRepository.UpdateGame(Game);
}
private void Cancel()
{
NavigationManager.NavigateTo("/Settings/Games/");
}
private void Delete()
{
DeleteConfirmation.Show();
}
private async void ConfirmDelete(bool deleteConfirmed)
{
if (deleteConfirmed)
{
await gamesRepository.DeleteGame(Game);
NavigationManager.NavigateTo("/Settings/Games/");
}
}
}
However, when the Game entity is updated it sets the Id of Season, GameType and/or Venue back to what they were before the update occurred.
For example:
Pre SaveChangesAsync():
Post SaveChangesAsync():
SQL produced:
In the SQL above, my assumption was that I would only see a update request to my Game entity. Why is it making updates to the other related tables and then why is it reverting anything that was changed back to what it was pre Save?
Any other changes to things like dates or strings is persisted as expected. It just seems that where I've used a drop down <InputSelect> this effect is taking place.
Update:
This is an updated version of my repo that seems to work but
Caius Jard was asking why I was doing what had fixed my issue. I'm happy to correct something if what I've done is incorrect for some reason:
public class GameRepository : IGameRepository
{
private readonly IDbContextFactory<SuttonPokerDbContext> _suttonPokerDbContext;
public GameRepository(IDbContextFactory<SuttonPokerDbContext> suttonPokerDbContext)
{
_suttonPokerDbContext = suttonPokerDbContext;
}
public async Task<Game> GetGame(int GameId)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
return await context.Games.Include(q => q.Results).Include(q => q.GameType).Include(q => q.Venue).Include(q => q.Season).FirstOrDefaultAsync(q => q.GameId == GameId); ;
}
}
public async Task<List<Game>> GetGames()
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
return await context.Games.Include(q => q.GameType).Include(q => q.Venue).Include(q => q.Season).OrderByDescending(q => q.GameDate).ToListAsync();
}
}
public async Task<Game> AddGame(Game game)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
context.Games.Add(game);
await context.SaveChangesAsync();
return game;
}
}
public async Task<Game> UpdateGame(Game game)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
// First we need to get the game from the database as we need to see if it's been modified already
Game dbGame = await context.Games.Include(q => q.Results).Include(q => q.GameType).Include(q => q.Venue).Include(q => q.Season).FirstOrDefaultAsync(q => q.GameId == game.GameId);
// Compare the byte arrays
if (!dbGame.RowVersion.SequenceEqual(game.RowVersion))
{
game = dbGame;
return game;
}
else
{
// We have to detach the dbGame version otherwise we get a conflict of tracked games.
context.Entry(dbGame).State = EntityState.Detached;
context.Entry(game).State = EntityState.Modified;
await context.SaveChangesAsync();
return game;
}
}
}
public async Task DeleteGame(Game game)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
context.Remove(game);
await context.SaveChangesAsync();
}
}
}

Related

Creating a complex object using Web Api in Blazor

I am trying to create a complex object using the Post method within my web api. However I'm struggling to do this as when I create a Board object I require it to have a Board.Company.Name which associates it with a company. However when I select an already existing company name and handle the valid submit a new company is created with the Board.Company.Name I have chosen. I then display the board I have created and it appears like no company is in fact associated with it. Below I have included the relevant code. This is my first project with C# and Blazor so let me know if I have left out anything important and I will include it.
Company Model
public class Company
{
[Key]
public Guid Id { get; set; }
[Required]
public string Name { get; set; }
public DateTime Founded { get; set; }
}
Board Model
public class Board
{
[Key]
public Guid Id { get; set; }
[ValidateComplexType]
public Company Company { get; set; } = new();
[Required]
public string Name { get; set; }
public string Description { get; set; }
public List<Ticket> Tickets { get; set; }
public Board()
{
}
}
Api POST Method
[HttpPost]
public async Task<ActionResult<Board>> PostBoard(Board board)
{
_context.Boards.Add(board);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBoard", new { id = board.Id }, board);
}
Create_Board
#page "/create_board"
#inject NavigationManager Navigation
#inject HttpClient Http
<div>
<button class="btn btn-outline-secondary oi oi-arrow-left" #onclick="GoToHome"></button>
<h3 class="text-center">Create a board</h3>
</div>
<hr />
<EditForm Model="Board" OnValidSubmit="#HandleValidSubmit">
<ObjectGraphDataAnnotationsValidator />
<div class="form-group row">
<label for="Company" class="col-sm-2 col-form-label">Company</label>
<div class="col-sm-10">
<InputSelect id="Company" class="form-control" #bind-Value="Board.Company.Name">
<option value="" disabled selected>Company</option>
#foreach (var company in Companies)
{
<option>#company.Value.Name</option>
}
</InputSelect>
<ValidationMessage For="#(() => Board.Company.Name)" />
</div>
</div>
<div class="form-group row">
<label for="Name" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<InputText id="Name" class="form-control" placeholder="Name" #bind-Value="Board.Name" />
<ValidationMessage For="#(() => Board.Name)" />
</div>
</div>
<div class="form-group row">
<label for="Description" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<InputText id="Description" class="form-control" placeholder="Description" #bind-Value="Board.Description" />
<ValidationMessage For="#(() => Board.Description)" />
</div>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
#code {
private void GoToHome()
{
Navigation.NavigateTo("/");
}
private Board Board { get; set; } = new Board();
private Dictionary<Guid, Company> Companies = new Dictionary<Guid, Company>();
protected override async Task OnInitializedAsync()
{
try
{
Companies = await Http.GetFromJsonAsync<Dictionary<Guid, Company>>("api/Companies");
}
catch (Exception)
{
Console.WriteLine("Exception occurred for GET companies");
}
}
private async void HandleValidSubmit()
{
try
{
var response = await Http.PostAsJsonAsync("/api/Boards", Board);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var board = JsonConvert.DeserializeObject<Board>(content);
Navigation.NavigateTo($"/read_board/{board.Id}");
}
catch (Exception)
{
Console.WriteLine("Exception occurred for POST board");
}
}
}
I don't think you'll get any joy out of setting the name; even if the context is living long enough (it shouldn't; contexts should only live as long as a request to the API does) to see you using a company name it has previously downloaded it'll be seeing a Guid.Empty (the default) and (presumably you've told EF that it's database generated) that will make the context think the company is new with Name X
Instead, I think I'd have the entity follow the typical "have CompanyId be a member of the Board and set it there" route, rather than setting the name on a new related entity:
<InputSelect id="Company" class="form-control" #bind-Value="Board.CompanyId">
<option value="" disabled selected>Company</option>
#foreach (var company in Companies)
{
<option value="#company.Key">#company.Value.Name</option>
}
</InputSelect>
This should save, and EF will see the company id and wire up the related company.
If you're averse to this (adding a CompanyId entity to Board) you can adopt either:
download that company by ID before you save, and assign it as the Company - you'll then be using a Company instance the change tracker has seen before and it will know how to wire up to the existing company rather than creating a new e.g.
<InputSelect id="Company" class="form-control" #bind-Value="Board.Company.Id">
<option value="" disabled selected>Company</option>
#foreach (var company in Companies)
{
<option value="#company.Key">#company.Value.Name</option>
}
</InputSelect>
[HttpPost]
public async Task<ActionResult<Board>> PostBoard(Board board)
{
board.Company = _context.Companies.Find(board.Company.Id); // download existing co with that ID
_context.Boards.Add(board);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBoard", new { id = board.Id }, board);
}
or
look at tricking the change tracker/context into thinking it's already seen the new company you created with Id X. Personally I'm not a fan, but:
[HttpPost]
public async Task<ActionResult<Board>> PostBoard(Board board)
{
_context.Boards.Add(board);
_context.Entries(board.Company).State = EntityState.Unchanged; //don't try to save the Company
await _context.SaveChangesAsync();
return CreatedAtAction("GetBoard", new { id = board.Id }, board);
}

How can I add Blog post with PostId and CategoryId in PostCategory?

i have a question. Im doing Blog on mvc core. I created 3 entities.Post, Category and PostCategory. When i want to create blog post, I wanna add on PostCategory tables with PostId and CategoryId.I done for EditPost its working but I didnt for CreatePost method. I need help. Let me show my codes.
Its my entities.
public class Post : IEntity
{
public int Id { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public DateTime DateTime { get; set; }
public string ImageUrl { get; set; }
public List<PostCategory> PostCategories { get; set; }
}
public class Category : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public List<PostCategory> PostCategories { get; set; }
}
public class PostCategory:IEntity
{
public int PostId { get; set; }
public Post Post { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
I done settings in BlogContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostCategory>()
.HasKey(c => new { c.CategoryId, c.PostId });
}
public DbSet<Post> Posts { get; set; }
public DbSet<Category> Categories { get; set; }
I wanna show PostService in EfCorePostDal.(I think i should be fix here)
public void Create(Post entity, int [] categoryIds)
{
using (var context = new BlogContext())
{
var post = context.Posts
.Include(x => x.PostCategories)
.ThenInclude(x => x.Category)
.FirstOrDefault();
if (post != null)
{
post.Title = entity.Title;
post.Text = entity.Text;
post.ImageUrl = entity.ImageUrl;
post.DateTime = entity.DateTime;
post.PostCategories = categoryIds.Select(categoryId => new PostCategory()
{
CategoryId = categoryId,
PostId = entity.Id
}).ToList();
}
context.SaveChanges();
}
}
Its my AdminController.I try to take categoryIds.When i check in debug.I can do it.I created in PostModel in WebUI by the way
public ActionResult CreatePost()
{
ViewBag.Categories = _categoryService.GetAll();
return View(new PostModel() { });
}
[HttpPost]
public async Task<ActionResult> CreatePost(PostModel model, IFormFile file,int[] categoryIds)
{
if (ModelState.IsValid)
{
var entity = new Post
{
Title = model.Title,
Text = model.Text,
DateTime = model.DateTime,
ImageUrl = model.ImageUrl,
};
if (file != null)
{
entity.ImageUrl = file.FileName;
var path = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot\\img", file.FileName);
using (var stream = new FileStream(path, FileMode.Create))
{
await file.CopyToAsync(stream);
}
}
if (_postService.Create(entity,categoryIds))
{
return RedirectToAction("ListPosts", "Admin");
}
return View(model);
}
ViewBag.Categories = _categoryService.GetAll();
return View(model);
}
My PostModel
public class PostModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public DateTime DateTime { get; set; }
public string ImageUrl { get; set; }
public List<Category> SelectedCategories { get; set; }
}
and Its my Createpost.cshtml
<form asp-controller="Admin" asp-action="CreatePost" method="post" enctype="multipart/form-data">
<div asp-validation-summary="All" class="text-danger"></div>
<input type="hidden" name="Id" value="#Model.Id" />
<div class="row">
<div class="col-md-8">
<div class="form-group row">
<label asp-for="Title" class="col-md-2 col-form-label"></label>
<div class="col-md-10">
<input asp-for="Title" value="" class="form-control" />
</div>
</div>
<div class="form-group row">
<label asp-for="DateTime" class="col-md-2 col-form-label"></label>
<div class="col-md-10">
<input asp-for="DateTime" value="" class="form-control" />
</div>
</div>
<div class="form-group row">
<label asp-for="ImageUrl" class="col-md-2 col-form-label"></label>
<div class="col-md-10">
<input type="file" name="file" value="" />
</div>
</div>
</div>
<div class="col-md-4">
#foreach (var item in (List<Category>)ViewBag.Categories)
{
<div class="form-check">
<input type="checkbox"
name="categoryIds"
value="#item.Id"
class="form-check-input"
id="category#(item.Id)">
<label class="form-check-label" for="category#(item.Id)">#item.Name</label>
</div>
}
</div>
<div class="col-md-12">
<div class="form-group row">
<div class="col-md-12">
<textarea asp-for="Text" class="form-control"></textarea>
</div>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<button type="submit" class="btn btn-success btn-block">Share</button>
</div>
</div>
Finally what I should ? When i add post its not working. (Im new developer sorry for my basic error)
According to your description and codes, I found you update the post instead of creating a new post record.
I suggest you could try to below codes instead to create the new post record.
Notice: I directly use the dbcontext with DI in the CreatePost method, you could modify the Create method by yourself according to my codes.
[HttpPost]
public async Task<ActionResult> CreatePost(PostModel model, IFormFile file, int[] categoryIds)
{
int[] test = new int[] {1,2,3,4,5 };
if (ModelState.IsValid)
{
var entity = new Post
{
Title = model.Title,
Text = model.Text,
DateTime = model.DateTime,
ImageUrl = model.ImageUrl,
};
var reparePart1 = test.Select(categoryId => new PostCategory()
{
CategoryId = categoryId,
Post = entity
}).ToList();
entity.PostCategories = reparePart1;
_dbContext.Add(entity);
_dbContext.SaveChanges();
if (file != null)
{
entity.ImageUrl = file.FileName;
//var path = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot\\img", file.FileName);
//using (var stream = new FileStream(path, FileMode.Create))
//{
// await file.CopyToAsync(stream);
//}
}
//Create(entity, categoryIds);
return View(model);
}
//ViewBag.Categories = _categoryService.GetAll();
return View(model);
}

Many to Many relationship in ASP.NET CORE 3.1 by repository pattern

I've made many to many relationship in ASP.NET Core and there are two tables Category and Subject
This is Category Model
public class Category
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<CategorySubject> CategorySubjects { get; set; } = new List<CategorySubject>();
}
This is subject model
public class Subject
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Exam_Time { get; set; }
public List<CategorySubject> CategorySubjects { get; set; }
}
This is CategorySubject Model
public class CategorySubject
{
public int CategoryId { get; set; }
public int SubjectId { get; set; }
public Category Category { get; set; }
public Subject Subject { get; set; }
}
This is part of DatabaseContext
public DbSet<CategorySubject> CategorySubjects { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CategorySubject>().HasKey(pt => new { pt.CategoryId, pt.SubjectId });
modelBuilder.Entity<CategorySubject>().HasOne(pt => pt.Category)
.WithMany(pt => pt.CategorySubjects).HasForeignKey(p => p.CategoryId);
modelBuilder.Entity<CategorySubject>().HasOne(pt => pt.Subject)
.WithMany(pt => pt.CategorySubjects).HasForeignKey(p => p.SubjectId);
}
I made one helper class by the name of Helper
public class Helpers:Profile
{
public Helpers()
{
CreateMap<Subject, SubjectViewModel>().ReverseMap();
CreateMap<SubjectViewModel, Subject>();
CreateMap<Category, CategoryViewModel>().ReverseMap();
}
}
this is category service:
public void Insert(Category category)
{
_context.Categories.Add(category);
}
public void Update(Category category)
{
_context.Categories.Update(category);
}
This is CategoryController :
// GET: CategoryController/Create
public IActionResult Create()
{
var subjectFromRepo = _categorySubject.Subject.GetAll();
var selectList = new List<SelectListItem>();
foreach (var item in subjectFromRepo)
{
selectList.Add(new SelectListItem(item.Name, item.Id.ToString()));
}
var vm = new CategoryViewModel()
{
Subjects = selectList
};
return View(vm);
}
// POST: CategoryController/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CategoryViewModel vm )
{
try
{
Category category = new Category()
{
Name = vm.Name
};
foreach(var item in vm.SelectedSubjects)
{
category.CategorySubjects.Add(new CategorySubject()
{
SubjectId = Int32.Parse(item)
});
}
_categorySubject.Category.Insert(category);
_categorySubject.Save();
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
// GET: CategoryController/Edit/5
public IActionResult Edit(int id)
{
var category = _categorySubject.Category.GetCategoryById(id);
var subjects = _categorySubject.Subject.GetAll();
var selectsubjects = category.CategorySubjects.Select(x => new Subject()
{
Id = x.Subject.Id,
Name = x.Subject.Name
});
var selectlist = new List<SelectListItem>();
subjects.ForEach(i => selectlist.Add(new SelectListItem(i.Name, i.Id.ToString(),
selectsubjects.Select(x => x.Id).Contains(i.Id))));
var vm = new CategoryViewModel()
{
Id= category.Id,
Name = category.Name,
Subjects = selectlist
};
return View(vm);
}
// POST: CategoryController/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(CategoryViewModel vm)
{
try
{
var category = _categorySubject.Category.GetCategoryById(vm.Id);
category.Name = vm.Name;
var selectedSubjects = vm.SelectedSubjects;
var existingSubjects = category.CategorySubjects.Select(x => x.SubjectId.ToString()).ToList();
var toAdd = selectedSubjects.Except(existingSubjects).ToList();
var toRemove = existingSubjects.Except(selectedSubjects).ToList();
var CategorySubjects = category.CategorySubjects.Where(x => !toRemove.Contains(x.SubjectId.ToString())).ToList();
foreach (var item in toAdd)
{
category.CategorySubjects.Add(new CategorySubject()
{
SubjectId = Int32.Parse(item),
CategoryId = Int32.Parse(item)
});
}
_categorySubject.Save();
return RedirectToAction("Index", "Category");
}
catch
{
return View();
}
}
This is Create.cshtml of Category :
<div class="style-form">
<h2 class="text-center mt-3 mb-lg-3">Create New Category</h2>
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row">
<div class="col-md-3 col-lg-3 col-sm-3"></div>
<div class="col-md-6 col-lg-6 col-sm-6">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><strong>Name:</strong></span>
</div>
<input asp-for="Name" class="form-control input-hover" placeholder="Enter Name.." />
<span asp-validation-for="Name" class="text-danger"></span>
</div><br />
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><strong>Subject:</strong></span>
</div>
<select asp-for="SubjectId" class="form-control input-hover" asp-items="#Model.Subjects ">
<option value="">Please choose a Subject...</option>
</select>
<span asp-validation-for="SubjectId" class="text-danger"></span>
</div><br />
</div>
</div>
<div class="row">
<div class="col-md-3 col-lg-3 col-sm-3"></div>
<div class="col-md-6 col-lg-6 col-sm-6">
<div class="form-group">
<button type="button" class="btn btn-backToList">
<a asp-action="Index">Back to List</a>
</button>
<button type="submit" class="btn btn-create">Create</button>
</div>
</div>
</div>
</form>
There when I click on the create new category button I can get data of subject form drop down list, but when I want to submit it I face this error:
NullReferenceException: Object reference not set to an instance of an object.
AspNetCore.Views_Category_Create.b__20_0() in Create.cshtml, line 27
<select asp-for="SubjectId" class="form-control input-hover" asp-items="#Model.Subjects ">
I think there is an exception thrown in the Create (POST) method, it then goes to the catch, which returns a view without a model
catch
{
return View();
}
The next exception comes while rendering the page trying to bind to #Model.Subjects where Model is null.
Remove try/catch or handle the catch to find if there is any exception.

Can't update value: 'Primary Key' has a temporary value while attempting to change the entity's state to 'Modified'

This is my first ASP .Net Core project. It will hold directors. Each director has a page that shows a list of his/her movies.
I have two classes.
Movie:
public class Movie
{
public int MovieId { get; private set; }
public int DirectorId { get; set; }
[Required]
public string Title { get; set; }
public string Year { get; set; }
public string Description { get; set; }
}
And Director:
public class Director
{
public Director()
{
Movies = new List<Movie>();
}
public int DirectorId { get; private set; }
[Required]
public string Name { get; set; }
public string Country { get; set; }
public string Bio { get; set; }
public List<Movie> Movies { get; set; }
}
But I have a problem with editing Directors. As I want to save changes I get this error:
InvalidOperationException: The property 'DirectorId' on entity type
'Director' has a temporary value while attempting to change the
entity's state to 'Modified'. Either set a permanent value explicitly
or ensure that the database is configured to generate values for this
property.
I use this line of code in Index page to navigate to Edit page:
<a asp-page="./../Movies/Create" asp-route-DirectorId="#item.DirectorId">Add Movie</a>
Photo of Index page:
Please click to see the photo
The code in Edit.cshtml.cs:
public class EditModel : PageModel
{
private readonly MastersOfCinema.Data.Context _context;
public EditModel(MastersOfCinema.Data.Context context)
{
_context = context;
}
[BindProperty]
public Director Director { get; set; }
public async Task<IActionResult> OnGetAsync(int? directorId)
{
if (directorId == null)
{
return NotFound();
}
Director = await _context.Director.FirstOrDefaultAsync(m => m.DirectorId == directorId);
if (Director == null)
{
return NotFound();
}
return Page();
}
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(Director).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!DirectorExists(Director.DirectorId))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool DirectorExists(int id)
{
return _context.Director.Any(e => e.DirectorId == id);
}
}
Apparently, Something upsets this very line:
_context.Attach(Director).State = EntityState.Modified;
Perhaps it is about the primary key (DirectorId), As the error suggests.
Edit page screenshot:
Please Click to see Edit page
Edit.cshtml :
#page
#model MastersOfCinema.Pages.Directors.EditModel
#{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Director</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Director.DirectorId" class="control-label"></label>
<input asp-for="Director.DirectorId" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Director.Name" class="control-label"></label>
<input asp-for="Director.Name" class="form-control" />
<span asp-validation-for="Director.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Director.Country" class="control-label"></label>
<input asp-for="Director.Country" class="form-control" />
<span asp-validation-for="Director.Country" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Director.Bio" class="control-label"></label>
<input asp-for="Director.Bio" class="form-control" />
<span asp-validation-for="Director.Bio" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Additional information:
Context.cs (Uses EF Core) :
public class Context : DbContext
{
public Context (DbContextOptions<Context> options)
: base(options)
{
}
public DbSet<MastersOfCinema.Models.Director> Director { get; set; }
public DbSet<MastersOfCinema.Models.Movie> Movie { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
"Data Source = (localdb)\\MSSQLLocalDB; Initial Catalog = MastersOfCinama");
}
}
Thanks for reading and for any help.
Try removing the private setter from:
public int DirectorId { get; private set; }
Instead it should look like this:
public int DirectorId { get; set; }

How to create dropdown from model?

I have two models:
public class Question
{
public int Id { get; set; }
public string Title { get; set; }
public int ClosedReasonId { get; set; }
public CloseReasonType CloseReasonType { get; set; }
}
public class CloseReasonType
{
public int Id { get; set; }
public string Name { get; set; }
public List<Question> Questions { get; set; }
}
I would like to create a view which has a form for adding questions and a dropdown for CloseReasonType.
#page
#model RazorPagesQuestion.Pages.Questions.CreateModel
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Question</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Question.Title" class="control-label"></label>
<input asp-for="Question.Title" class="form-control" />
<span asp-validation-for="Question.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Question.CloseReasonType" class="control-label"></label>
<select asp-for="Question.CloseReasonType" class="form-control"
asp-items="Model.CloseReasonType">
</select>
<span asp-validation-for="Question.CloseReasonType" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
Of course when I just added asp-items="Model.CloseReasonType" to my select tag helper it didn't populate the dropdown with options. How can I populate the options?
I added this to my CreateModel class
[BindProperty]
public Question Question { get; set; }
[BindProperty]
public List<SelectListItem> CloseReasonType { get; }
All the examples I have seen show how to create the list out of hardcoded values.
The full class:
public class CreateModel : PageModel
{
private readonly RazorPagesQuestion.Data.RazorPagesQuestionContext _context;
public CreateModel(RazorPagesQuestion.Data.RazorPagesQuestionContext context)
{
_context = context;
}
public IActionResult OnGet()
{
return Page();
}
[BindProperty]
public Question Question { get; set; }
[BindProperty]
public List<SelectListItem> CloseReasonType { get; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Question.Add(Question);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
You would need to populate the select list for it to display on the page
Assuming your DbContext has a CloseReasonTypes property
//...
private void loadCloseReasonTypes() {
CloseReasonTypes = new SelectList(_context.CloseReasonTypes, nameof(CloseReasonType.Id), nameof(CloseReasonType.Name));
}
public IActionResult OnGet() {
loadCloseReasonTypes();
return Page();
}
public SelectList CloseReasonTypes { get; set; }
[BindProperty]
public Question Question { get; set; }
//...
Update the view to bind to the relevant property on the model.
<div class="form-group">
<label asp-for="Question.CloseReasonId" class="control-label">Close Reason</label>
<select asp-for="Question.CloseReasonId" class="form-control"
asp-items="Model.CloseReasonTypes">
</select>
<span asp-validation-for="Question.CloseReasonId" class="text-danger"></span>
</div>
The list will also need to be repopulated if the post was not successful as the page will reload, clearing the select list.
public async Task<IActionResult> OnPostAsync() {
if (!ModelState.IsValid) {
loadCloseReasonTypes();
return Page();
}
_context.Question.Add(Question);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

Categories