ASP.NET Core MVC中EF多对多关系保存报InvalidOperationException求助
我是ASP.NET Core MVC和Entity Framework的新手,开发游戏注册系统时遇到问题:游戏需关联主机及一种或多种游戏类型,先尝试单类型关联,保存时游戏与主机信息能正常存入数据库,但关联游戏类型时触发以下错误:
An unhandled exception occurred while processing the request.
InvalidOperationException: The value of 'GameGameGenre.GameId' is unknown when attempting to save changes. This is because the property is also part of a foreign key for which the principal entity in the relationship is not known.
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()
以下是相关代码:
Game类
namespace ProjectC01.Models { public class Game { public int GameId { get; set; } public string GameName { get; set; } public string GameDeveloper { get; set; } public string GamePublisher { get; set; } [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}")] public DateTime GameRelease { get; set; } public string GameCover { get; set; } public string ImageLocation { get; set; } public VideoGameConsole GameConsole { get; set; } public int VideoGameConsoleId { get; set; } public ICollection<GameGenre> GameGenres { get; set; } = new List<GameGenre>(); public ICollection<GameGameGenre> GameGameGenres { get; set; } = new List<GameGameGenre>(); public Game() { } public Game(int gameId, string gameName, string gameDeveloper, string gamePublisher, DateTime gameRelease, string gameCover, VideoGameConsole gameConsole, string imageLocation) { GameId = gameId; GameName = gameName; GameDeveloper = gameDeveloper; GamePublisher = gamePublisher; GameRelease = gameRelease; GameCover = gameCover; GameConsole = gameConsole; ImageLocation = imageLocation; } } }
GameGenre类
namespace ProjectC01.Models { public class GameGenre { [Key] public int GenreId { get; set; } public string GenreName { get; set; } public ICollection<Game> Games { get; set; } = new List<Game>(); public ICollection<GameGameGenre> GameGameGenres { get; set; } = new List<GameGameGenre>(); public GameGenre() { } public GameGenre(int genreId, string genreName) { GenreId = genreId; GenreName = genreName; } } }
GameGameGenre关联类
namespace ProjectC01.Models { public class GameGameGenre { public Game Game { get; set; } public int GameId { get; set; } public GameGenre GameGenre { get; set; } public int GameGenreId { get; set; } public GameGameGenre() { } public GameGameGenre(Game game, int gameId, GameGenre gameGenre, int gameGenreId) { Game = game; GameId = gameId; GameGenre = gameGenre; GameGenreId = gameGenreId; } } }
Game控制器Create方法
namespace ProjectC01.Controllers { public class GameController : Controller { private readonly GameService _gameService; private readonly VideoGameConsoleService _videoGameConsoleService; private readonly GameGenreService _gameGenreService; private readonly ProjectC01Context _context; private readonly IWebHostEnvironment _environment; public GameController(GameService gameService, VideoGameConsoleService videoGameConsoleService, GameGenreService gameGenreService, ProjectC01Context context, IWebHostEnvironment environment) { _gameService = gameService; _videoGameConsoleService = videoGameConsoleService; _gameGenreService = gameGenreService; _context = context; _environment = environment; } public IActionResult Index() { var list = _gameService.FindAll(); return View(list); } public async Task<IActionResult> Create() { var consoles = await _videoGameConsoleService.FindAllAsync(); var genres = await _gameGenreService.FindAllAsync(); var viewModel = new GameFormViewModel { Consoles = consoles, GameGenres = genres }; return View(viewModel); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create(GameFormViewModel model, IFormFile imageUpload) { if (imageUpload == null || imageUpload.Length == 0) { return Content("Nenhuma imagem selecionada"); } var cover = Path.Combine(_environment.WebRootPath, "Images\\GameCovers", imageUpload.FileName); using (FileStream stream = new FileStream(cover, FileMode.Create)) { await imageUpload.CopyToAsync(stream); stream.Close(); } string imgPath = "\\Images\\GameCovers\\" + imageUpload.FileName; model.Game.GameCover = imageUpload.FileName; if (model != null) { var game = new Game { GameName = model.Game.GameName, GameDeveloper = model.Game.GameDeveloper, GamePublisher = model.Game.GamePublisher, GameRelease = model.Game.GameRelease, GameCover = model.Game.GameCover, VideoGameConsoleId = model.Game.VideoGameConsoleId, ImageLocation = imgPath, }; var gameGameGenre = new GameGameGenre { GameId = model.GameGameGenre.GameId, GameGenreId = model.GameGameGenre.GameGenreId, }; _context.Add(game); _context.Add(gameGameGenre); await _context.SaveChangesAsync(); //错误触发处 } return RedirectToAction("Index"); } } }
Create视图
@model ProjectC01.Models.ViewModels.GameFormViewModel @{ ViewData["Title"] = "Cadastrar"; } <h1>@ViewData["Title"]</h1> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Create" method="post" enctype="multipart/form-data"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Game.GameName" class="control-label"></label> <input asp-for="Game.GameName" class="form-control" /> <span asp-validation-for="Game.GameName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Game.GameDeveloper" class="control-label"></label> <input asp-for="Game.GameDeveloper" class="form-control" /> <span asp-validation-for="Game.GameDeveloper" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Game.GamePublisher" class="control-label"></label> <input asp-for="Game.GamePublisher" class="form-control" /> <span asp-validation-for="Game.GamePublisher" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Game.GameRelease" class="control-label"></label> <input asp-for="Game.GameRelease" class="form-control" /> <span asp-validation-for="Game.GameRelease" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Game.GameCover" class="control-label"></label> <input asp-for="Game.GameCover" type="file" name="imageUpload" accept="image/*" class="form-control" /> <span asp-validation-for="Game.GameCover" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Game.VideoGameConsoleId" class="control-label"></label> <select asp-for="Game.VideoGameConsoleId" class="form-select" asp-items="@(new SelectList(Model.Consoles,"ConsoleId","ConsoleName"))"></select> </div> <div class="form-group"> <label asp-for="GameGameGenre.GameGenreId" class="control-label"></label> <select asp-for="GameGameGenre.GameGenreId" class="form-select" asp-items="@(new SelectList(Model.GameGenres,"GenreId","GenreName"))"></select> </div> <hr /> <div class="form-group"> <input type="submit" value="Cadastrar" class="btn btn-primary" /> | <a asp-action="Index" class="btn btn-secondary">Retornar a Lista</a> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
GameFormViewModel
namespace ProjectC01.Models.ViewModels { public class GameFormViewModel { public Game Game { get; set; } public GameGenre GameGenre { get; set; } public GameGameGenre GameGameGenre { get; set; } public IFormFile ImageUpload { get; set; } public ICollection<VideoGameConsole> Consoles { get; set; } public ICollection<GameGenre> GameGenres { get; set; } } }
DbContext配置
namespace ProjectC01.Data { public class ProjectC01Context : DbContext { public ProjectC01Context(DbContextOptions<ProjectC01Context> options) : base(options) { } public DbSet<VideoGameConsole>? VideoGameConsole { get; set; } public DbSet<GameGenre>? GameGenre { get; set; } public DbSet<Game>? Game { get; set; } public DbSet<GameGameGenre>? GameGameGenre { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Game>() .HasMany(e => e.GameGenres) .WithMany(e => e.Games) .UsingEntity<GameGameGenre>( r => r.HasOne<GameGenre>(e => e.GameGenre).WithMany(e => e.GameGameGenres), l => l.HasOne<Game>(e => e.Game).WithMany(e => e.GameGameGenres)); modelBuilder.Entity<GameGenre>() .HasMany(e => e.Games) .WithMany(e => e.GameGenres) .UsingEntity<GameGameGenre>( r => r.HasOne<Game>(e => e.Game).WithMany(e => e.GameGameGenres), l => l.HasOne<GameGenre>(e => e.GameGenre).WithMany(e => e.GameGameGenres)); //不确定是否需要对两个类都做配置 } } }
我怀疑问题出在Create视图或Controller的Post方法中,但未找到类似问题的解决方案,希望得到帮助。
解决方案
1. 修复Controller关联数据保存逻辑
错误根源是创建GameGameGenre时,GameId被设置为model.GameGameGenre.GameId——新创建的Game还未保存到数据库,EF Core尚未生成自动递增的GameId,导致外键值为空。
修改Post方法的关联代码,有两种可选方式:
- 方式一:先保存Game获取生成的Id
// 先添加并保存Game,获取自动生成的GameId _context.Add(game); await _context.SaveChangesAsync(); // 使用刚生成的GameId创建关联对象 var gameGameGenre = new GameGameGenre { GameId = game.GameId, GameGenreId = model.GameGameGenre.GameGenreId, }; _context.Add(gameGameGenre); await _context.SaveChangesAsync();
- 方式二:直接关联Game对象,EF自动处理外键
// 直接关联已添加到上下文的Game对象 var gameGameGenre = new GameGameGenre { Game = game, GameGenreId = model.GameGameGenre.GameGenreId, }; _context.Add(game); _context.Add(gameGameGenre); await _context.SaveChangesAsync(); // 一次保存即可,EF会自动处理实体依赖顺序
2. 简化DbContext多对多配置
不需要重复配置Game和GameGenre的多对多关系,保留一份即可避免冲突:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Game>() .HasMany(e => e.GameGenres) .WithMany(e => e.Games) .UsingEntity<GameGameGenre>( join => join.HasOne<GameGenre>().WithMany(g => g.GameGameGenres).HasForeignKey(gg => gg.GameGenreId), join => join.HasOne<Game>().WithMany(g => g.GameGameGenres).HasForeignKey(gg => gg.GameId)); }
3. 优化ViewModel(可选)
视图仅需传递选中的游戏类型ID,可简化GameFormViewModel,去掉冗余的GameGameGenre对象:
public class GameFormViewModel { public Game Game { get; set; } public int SelectedGenreId { get; set; } // 替代GameGameGenre public IFormFile ImageUpload { get; set; } public ICollection<VideoGameConsole> Consoles { get; set; } public ICollection<GameGenre> GameGenres { get; set; } }
对应视图修改:
<div class="form-group"> <label for="SelectedGenreId" class="control-label">游戏类型</label> <select asp-for="SelectedGenreId" class="form-select" asp-items="@(new SelectList(Model.GameGenres,"GenreId","GenreName"))"></select> </div>
Controller中直接使用model.SelectedGenreId即可,代码更简洁清晰。
内容的提问来源于stack exchange,提问作者Mateus Fernandes Vailate

