You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core MVC中EF多对多关系保存报InvalidOperationException求助

ASP.NET Core MVC + EF Core 游戏注册系统多对多关联保存报错

我是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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 10:57:01