Search code examples
c#asp.net-mvcasp.net-coremany-to-many.net-6.0

"InvalidOperationException:" While trying to save a many to many relationship to database


First of all, i just wanna say i'm a total newbie with asp .net core mvc, and entity framework, soo i don't really know if i'm doing this right or in the best way.

I'm trying to do a simple program where you can register games, every game got some information about them, a console associated with them, and the thing that i can't solve, a association with one or more genres.

But to start simple, i'm only trying to register a game with only one genre (and i don't know how i gonna display multiple genres for selection yet).

The game, and console information have no problem registering in the db, but when i try to register a game with a genre, i get this error:

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()

These are the relevant MODELS for this problem (i think at least)

This is the Game class:

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;
        }
    }
}

This is the Game Genre Class:

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;
        }
    }
}

And this is the class that does the join between the games, and the genres

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;
        }
    }
}

This is the the CONTROLLER for the Games

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(); //The error occurs after this line is executed
            }

            return RedirectToAction("Index");
        }
    }
}

This is the CREATE VIEW for the Games

@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");
    }
}

This is the VIEWMODEL used on the CREATE VIEW above

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; }
    }
}

And this is the CONTEXT for the application

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)); 

            //I don't really know if it was necessary to do this for both classes
        }
    }
}

And i think that's all the things connected in some way to the problem, probably i did something wrong in the CREATE view or in the CREATE POST action on the CONTROLLER, at least that's the conclusion i got to.

I searched a lot for some post that maybe had the same problem, but i really couldn't find any, sorry if maybe i'm not clear enough, but as i said, i'm just getting started on this asp .net MVC/EF thing.


Solution

  • Thanks to this issue on GitHub i found out the solution for this problem.

    Turns out the problem was because the code never sets a id value for the GameId, since its a auto increment PK on the database, because of this when the controller tried to add a GameId to the GameGameGenre table, it was aways 0, causing the error above.

    I solved it by first including the game on the database (as i was already doing), and them doing a LINQ search on the db for the game that just got registered, them getting its id, and using that for the GameGameGenre.GameId insertion.

    public async Task CreateNewGGS(GameFormViewModel model)
    {
        if (model != null)
        {
            var gameId = _context.Game.SingleOrDefault(m =>
    
                m.GameName == model.Game.GameName &&
                m.GameDeveloper == model.Game.GameDeveloper &&
                m.GamePublisher == model.Game.GamePublisher &&
                m.GameRelease == model.Game.GameRelease &&
                m.GameCover == model.Game.GameCover &&
                m.VideoGameConsoleId == model.Game.VideoGameConsoleId
            ).GameId;
    
            var ggs = new GameGameGenre
            {
                GameId = gameId,
                GameGenreId = model.GameGameGenre.GameGenreId,
            };
    
            await InsertAsync(ggs);
        }
    }