Révision MVC et Relations
Objectif
L'objectif de cet exercice est de réviser les types de relations et de mieux comprendre comment elles fonctionnent. Les relations révisées sont:
- 1 à N
- N à N (sans doublon)
- N à N (avec doublon)
Fork
Pour commencer l'exercice, il y a un projet de base que vous pouvez forker :
- Le projet GitHub
- En créant un fork, vous allez pouvoir créer votre propre copie du projet

- Vous allez pouvoir choisir pour quels utilisateur ou organisation vous créez la copie (fork).
- C'est préférable de copier toutes les branches dans ce cas-ci. Mais ce n'est pas très grave si vous avez uniquement copié seulement la branche main.

- Une fois que vous avez fait votre fork, vous allez voir le nouveau repo à l'endroit où vous l'avez créé (probablement dans votre profil personnel)
- Vous pouvez simplement faire un clone et commencer à travailler!
Les forks sont très utiles. Un repo créé avec un fork contient une option pour mettre le projet à jour à partir du fork.
On peut également utiliser un fork pour faire une demande d'intégration vers le repo originale. C'est la façon de travailler pour de nombreux projets collaboratifs.
Comprendre les migrations
Première migration
- Générer une première migration :
add-migration initiale
- Regardez le contenu du fichier de migration. Il n'y a pas encore de relations entre les 2 classes
GameetGenre, le fichier de migration est donc assez simple.
- Code de la migration
- Diagrammes
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Game",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Image = table.Column<string>(type: "nvarchar(max)", nullable: false),
ReleaseYear = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Game", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Genre",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Genre", x => x.Id);
});
}
Relation 1 à N entre Game et Genre
- Ajouter une relation optionnelle entre
GameetGenre. Pour l'instant, ne faites que modifier la classeGame. ChaqueGamen'a qu'un seulGenre.
public class Game
{
public int Id { get; set; }
public string Name { get; set; }
public string Image { get; set; }
public int ReleaseYear { get; set; }
public int? GenreId { get; set; }
[ValidateNever]
public Genre? Genre { get; set; }
}
- Générer une migration
add-migration relationGameGenre
- Regarder la migration. Si on prend le temps de lire, c'est relativement simple. On a une nouvelle colonne qui contient un Index pour améliorer la performance et qui est une clé étrangère vers la table Genre. Nullable: true nous indique que la relation est optionnelle.
- Code de la migration
- Diagrammes
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "GenreId",
table: "Game",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Game_GenreId",
table: "Game",
column: "GenreId");
migrationBuilder.AddForeignKey(
name: "FK_Game_Genre_GenreId",
table: "Game",
column: "GenreId",
principalTable: "Genre",
principalColumn: "Id");
}
- Ajouter la liste de Games au Genre
public class Genre
{
public int Id { get; set; }
public string Name { get; set; }
[ValidateNever]
public List<Game> Games { get; set; }
}
- Générer une nouvelle migration (peu importe le nom)
- Regarder le contenu de la migration. Elle devrait être vide! Par défaut, ce genre de relation est de type 1 à N, alors le fait d'ajouter la liste au Genre ne change RIEN au modèle de données!
protected override void Up(MigrationBuilder migrationBuilder)
{
}
- On peut simplement retirer la migration vide
remove-migration
- Et maintenant mettre notre BD à jour
update-database
- Générer un contrôleur avec une vue pour Genre en utilisant cette commande
//dotnet aspnet-codegenerator controller -m NOM_DU_MODEL -dc NOM_DU_CONTEXT -scripts -udl -outDir Controllers -name NOM_DU_CONTROLEUR -async -actions
dotnet aspnet-codegenerator controller -m Genre -dc RelationsNaNContext -scripts -udl -outDir Controllers -name GenresController -async -actions
Le générateur de scaffolding de VisualStudio 2026 n'est vaiment pas fameux, alors on utilise l'ancienne version!
Il y a des problèmes avec l'image présentement, on va donc devoir faire les actions suivantes si vous avez une erreur qui vous indique qu'il ne trouve pas aspnet-codegenerator
Régler le problème de path pour trouver la commande
- Ouvrir le prompt de commande (PAS powershell) avec touche windows + r, écrire cmd et faire enter
- Taper la commande suivante: echo %USERPROFILE%.dotnet\tools
![]() |
|---|
- Copier le réultat (avec votre numéro d'étudiant)
- Ouvrir l'outil suivant:
![]() |
|---|
- Cliquer sur Variables d'environnement...
![]() |
|---|
- Choisir Path et cliquer sur Modifier...
![]() |
|---|
- Cliquer sur Nouveau et copier le path que vous avez obtenu avec echo
![]() |
|---|
- Redémarrer Visual Studio et rouler la commande à nouveau, vous devriez réussir à générer le contrôleur!
- Ajouter un lien dans la barre de navigation
- Vérifier que ça fonctionne correctement en ajoutant les genres suivants:
- shooter
- fighting
- Générer un contrôleur avec une vue pour Game
Modifier la commande pour générer un contrôleur pour Game maintenant (Attention, il faut faire 2 changements dans la commande)
- Ajouter un lien dans la barre de navigation
- La vue générée a probablement un problème avec le champ optionnel Genre. On peut simplement modifier pour afficher le nom lorsque le genre n'est pas null:
<td>
@if(item.Genre != null)
{
@Html.DisplayFor(modelItem => item.Genre.Name)
}
</td>
- Vous devriez avoir quelque chose qui ressemble à ça maintenant:
![]() |
|---|
- Afficher les images avec un height de 200
- Corriger le Contrôleur GamesController et ses vues pour que le SelectList affiche le champ "Name" et pas "Id". Il y a 4 endroits où un SelectList est créé dans chaque contrôleur (Edit et Create)!
Si vous avez oublié comment faire, vous pouvez aller voir comment vous aviez fait dans le cours de 3W6 - Programmation Web Transactionnelle.
- Corriger les vues Edit et Create pour bien supporter la possibilité de n'avoir aucun genre.
Votre code risque d'être un peu différent, mais ça vous donne une façon de faire pour ajouter une option "null"
<select asp-for="GenreId" class="form-control" asp-items="ViewBag.GenreId">
<option selected value="">--- NONE ---</option>
</select>
-
Vérifier qu'il est possible de choisir un nouveau genre pour un jeu existant
-
Une fois que tout est fonctionnel:

Relation N à N sans doublon entre Game et Platform
Un même jeu peut avoir plusieurs Platform différentes!
- Ajouter une classe Platform avec une relation N à N avec Game
public class Platform
{
public int Id { get; set; }
public string Name { get; set; }
[ValidateNever]
public List<Game> Games { get; set; }
}
public class Game
{
public int Id { get; set; }
public string Name { get; set; }
public string Image { get; set; }
public int ReleaseYear { get; set; }
public int? GenreId { get; set; }
[ValidateNever]
public Genre? Genre { get; set; }
[ValidateNever]
public List<Platform> Platforms { get; set; }
}
- Faites une migration.
- Cette fois-ci c'est un peu plus complexe. Il y a une table en plus qui est créée pour stocker l'information des relations multiples entre
GameetPlatform. - Cette classe contient 2 champs pour l'Id de
Gameet dePlatform.
- Code de la migration
- Diagrammes
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Platform",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Platform", x => x.Id);
});
migrationBuilder.CreateTable(
name: "GamePlatform",
columns: table => new
{
GamesId = table.Column<int>(type: "int", nullable: false),
PlatformsId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GamePlatform", x => new { x.GamesId, x.PlatformsId });
table.ForeignKey(
name: "FK_GamePlatform_Game_GamesId",
column: x => x.GamesId,
principalTable: "Game",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_GamePlatform_Platform_PlatformsId",
column: x => x.PlatformsId,
principalTable: "Platform",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_GamePlatform_PlatformsId",
table: "GamePlatform",
column: "PlatformsId");
}
- Ajouter des plateformes avec votre
DbInitializer.
if (_context.Platform.Count() == 0)
{
Platform ps5 = new Platform()
{
Name = "PS4"
};
await _context.Platform.AddAsync(ps5);
Platform xbox360 = new Platform()
{
Name = "Xbox One"
};
await _context.Platform.AddAsync(xbox360);
Platform n64 = new Platform()
{
Name = "Switch"
};
await _context.Platform.AddAsync(n64);
Platform pc = new Platform()
{
Name = "PC"
};
await _context.Platform.AddAsync(pc);
await _context.SaveChangesAsync();
}
- Utiliser le code suivant pour afficher les
Plateformd'unGameen utilisant des badges
<td>
@foreach (var platform in item.Platforms)
{
<span class="badge bg-primary">@platform.Name</span>
}
</td>
- Pour l'ajout et la suppression de Platforms, vous pouvez ajouter ceci à la vue Edit (Au dessus du formulaire existant):
@foreach (var platform in Model.Platforms)
{
<form asp-action="RemovePlatform" method="post">
<input type="hidden" name="id" value="@Model.Id"/>
<input type="hidden" name="platformId" value="@platform.Id" />
<button type="submit" class="btn btn-danger m-1">
@platform.Name
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash3" viewBox="0 0 16 16">
<path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5" />
</svg>
</button>
</form>
}
<form asp-action="AddPlatform" method="post">
<input type="hidden" name="id" value="@Model.Id" />
<div class="form-group">
<label asp-for="Platforms" class="control-label"></label>
<select name="platformId" class="form-control" asp-items="ViewBag.Platforms"></select>
</div>
<div class="form-group m-2">
<input type="submit" value="Add Platform" class="btn btn-primary" />
</div>
</form>
- Vous allez devoir ajouter les actions
AddPlatformetRemovePlatformqui vont recevoir les Ids et modifier leGame - Essayer d'ajouter 2 fois la même Platform. Essayer d'ajouter une autre Platform en double. Comme vu en classe, c'est problématique!
- Comme on ne veut pas vraiment avoir des doublons pour les plateformes, on va améliorer notre logique pour que la liste affiche uniquement les plateformes qui ne sont pas déjà ajoutées à ce Game.
- Modifiez le contrôleur pour y arriver.
Faites attention, il y a plusieurs endroits où l'on génère une SelectList pour les plateformes. Pour l'exercice, ce n'est pas nécessaire de s'occuper des actions Create, seulement Edit.
Une fois terminé, vous devriez avoir un affichage comme ceci:
![]() |
|---|
Relation N à N avec doublon entre Game et Purchase
En créant une relation qui utilise une classe de relation, on a plus de contrôle sur notre façon de gérer la relation. On peut entre autres avoir plusieurs copies de la même relation.
- Ajouter une classe Purchase très simple
public class Purchase
{
public int Id { get; set; }
[ValidateNever]
public List<GamePurchase> GamePurchases { get; set; }
}
- Ajouter une classe
GamePurchase. Comme elle contient son propre Id, on va pouvoir ajouter plusieurs fois la même relationGame/Purchase
public class GamePurchase
{
public int Id { get; set; }
[ValidateNever]
public Game Game { get; set; }
[ValidateNever]
public Purchase Purchase { get; set; }
}
- Ajouter également une liste de
GamePurchasesà la classeGame
public class Game
{
public int Id { get; set; }
public string Name { get; set; }
public string Image { get; set; }
public int ReleaseYear { get; set; }
public int? GenreId { get; set; }
[ValidateNever]
public Genre? Genre { get; set; }
[ValidateNever]
public List<Platform> Platforms { get; set; }
[ValidateNever]
public List<GamePurchase> GamePurchases { get; set; }
}
- Faire une migration et regarder son contenu. On peut remarquer qu'il y a des colonnes
GameIdetPurchasedId, c'est le nom par défaut d'une clé étrangère (Nom de la propriété + Id)
- Code de la migration
- Diagrammes
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Purchase",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1")
},
constraints: table =>
{
table.PrimaryKey("PK_Purchase", x => x.Id);
});
migrationBuilder.CreateTable(
name: "GamePurchase",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
GameId = table.Column<int>(type: "int", nullable: false),
PurchaseId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GamePurchase", x => x.Id);
table.ForeignKey(
name: "FK_GamePurchase_Game_GameId",
column: x => x.GameId,
principalTable: "Game",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_GamePurchase_Purchase_PurchaseId",
column: x => x.PurchaseId,
principalTable: "Purchase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_GamePurchase_GameId",
table: "GamePurchase",
column: "GameId");
migrationBuilder.CreateIndex(
name: "IX_GamePurchase_PurchaseId",
table: "GamePurchase",
column: "PurchaseId");
}
- Ajouter 1 vente dans le
DbInitializer.
if (_context.Purchase.Count() == 0)
{
Purchase purchase = new Purchase()
{
};
await _context.Purchase.AddAsync(purchase);
// On va chercher les 2 premiers jeux
Game game1 = _context.Game.First(x => x.Id == 1);
Game game2 = _context.Game.First(x => x.Id == 2);
// Création d'une 1ere relation avec le premier jeu
GamePurchase gamePurchase1 = new GamePurchase()
{
Game = game1,
Purchase = purchase
};
await _context.AddAsync(gamePurchase1);
// Création d'une 2e relation avec encore le premier jeu
GamePurchase gamePurchase2 = new GamePurchase()
{
Game = game1,
Purchase = purchase
};
await _context.AddAsync(gamePurchase2);
// Création d'une 3e relation avec le deuxième jeu
GamePurchase gamePurchase3 = new GamePurchase()
{
Game = game2,
Purchase = purchase
};
await _context.AddAsync(gamePurchase3);
// Création d'une 4e relation avec ENCORE le premier jeu
GamePurchase gamePurchase4 = new GamePurchase()
{
Game = game1,
Purchase = purchase
};
await _context.AddAsync(gamePurchase4);
await _context.SaveChangesAsync();
}
- Ajouter ce contrôleur pour visualiser les données de
Purchase.
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RelationsNaN.Data;
namespace RelationsNaN.Controllers
{
public class PurchasesController : Controller
{
private readonly RelationsNaNContext _context;
public PurchasesController(RelationsNaNContext context)
{
_context = context;
}
// GET: Games
public async Task<IActionResult> Index()
{
var relationsNaNContext = _context.Purchase.Include(p => p.GamePurchases).ThenInclude(gp => gp.Game);
return View(await relationsNaNContext.ToListAsync());
}
}
}
- Ajouter une vue pour visualiser les données de
Purchase.
@model IEnumerable<RelationsNaN.Models.Purchase>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Id)
</th>
<th>
@Html.DisplayNameFor(model => model.GamePurchases)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Id)
</td>
<td>
@foreach (var gamePurchase in item.GamePurchases)
{
<span class="badge bg-primary">@gamePurchase.Game.Name</span>
}
</td>
</tr>
}
</tbody>
</table>
- Vérifier le résultat en allant sur /purchases
Autres possibilités
Une fois que l'on a une classe de relation, on a la flexibilité de gérer notre problème comme on le veut. On peut par exemple:
- Ajouter une propriété
Countà la relation au lieu d'avoir un Id, comme ceci:
public class GamePurchase
{
public int Id { get; set; }
[ValidateNever]
public Game Game { get; set; }
[ValidateNever]
public Purchase Purchase { get; set; }
public int Count { get; set; }
}
- Ou encore en ayant un "Owner" pour pouvoir faire des cadeaux.
public class GamePurchase
{
public int Id { get; set; }
[ValidateNever]
public Game Game { get; set; }
[ValidateNever]
public Purchase Purchase { get; set; }
public Owner Owner { get; set; }
}






