You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mchsamples-.net-core/p08_BDD_EntityFramework/ex_042_013_OneToOne_FluentAPI
Marc CHEVALDONNE a773557095
continuous-integration/drone/push Build is failing Details
update to .NET6
2 years ago
..
CarnetDeSante.cs begun ex_042_013 5 years ago
Nounours.cs begun ex_042_013 5 years ago
NounoursDBEntities.cs ended ex_042_013 5 years ago
Program.cs ended ex_042_013 5 years ago
ReadMe.md ended ex_042_013 5 years ago
StubbedContext.cs begun ex_042_013 5 years ago
ex_042_013_OneToOne_FluentAPI.csproj update to .NET6 2 years ago

ReadMe.md

ex_042_013_OneToOne_FluentAPI

20/01/2020 ⋅ Marc Chevaldonné


Cet exemple montre comment réaliser une relation One To One entre deux entités avec Entity Framework Core et la Fluent API.
Une version équivalente réalisée avec les annotations de données est disponible dans l'exemple ex_042_012 : One To One with data annotations


Comment est construit cet exemple ?

  • Le projet est de type .NET Core
  • Il contient quatre classes :
    • Nounours
    • CarnetDeSante
    • NounoursDBEntities
    • StubbedContext

Les classes entités : Nounours et CarnetDeSante

Un Nounours contient différentes propriétés (Nom, DateDeNaissance, NbPoils) dont Carnet de type CarnetDeSante.
Un CarnetDeSante possède une propriété de type DateTime (LastModified), et une propriété Owner de type Nounours.
On a donc bien une relation One To One puisqu'un Nounours possède un CarnetDeSante et qu'un CarnetDeSante possède un Nounours.

Ce qu'il faut noter :

  • Nounours possède une association vers CarnetDeSante
public CarnetDeSante Carnet { get; set; }
  • CarnetDeSante possède une association vers Nounours
public Nounours Owner { get; set; }
  • Nounours possède un identifiant unique UniqueId.
  • CarnetDeSante possède un identifiant unique UniqueId.

La classe NounoursDBEntities

  • Comme dans les exemples précédents, NounoursDBEntities dérive de DbContext.
  • NounoursDBEntities déclare deux DbSet : un de Nounours et l'autre de CarnetDeSante.
public DbSet<Nounours> NounoursSet { get; set; }
public DbSet<CarnetDeSante> Carnets { get; set; }
  • La classe réécrit ensuite la méthode OnModelCreating :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //création de la table TableNounours
    modelBuilder.Entity<Nounours>().ToTable("TableNounours"); //nom de la table
    modelBuilder.Entity<Nounours>().HasKey(n => n.UniqueId); //définition de la clé primaire
    modelBuilder.Entity<Nounours>().Property(n => n.UniqueId)
                                    .ValueGeneratedOnAdd(); //définition du mode de génération de la clé : génération à l'insertion
    modelBuilder.Entity<Nounours>().Property(n => n.Nom).IsRequired()
                                                        .HasMaxLength(256); //définition de la colonne Nom
    modelBuilder.Entity<Nounours>().Property(n => n.DateDeNaissance).HasColumnName("Naissance").HasColumnType("date"); //changement du nom de la colonne Naissance

    //création de la table "Carnets"
    modelBuilder.Entity<CarnetDeSante>().ToTable("Carnets"); // nom de la table
    modelBuilder.Entity<CarnetDeSante>().HasKey(c => c.UniqueId); //définition de la clé primaire
    modelBuilder.Entity<CarnetDeSante>().Property(c => c.UniqueId)
                                        .ValueGeneratedNever(); // définition du mode de génération de la clé : pas de génération automatique
    //note : la colonne LastModified n'est pas touchée : utilisation des conventions EF


    //on précise qu'il y a une relation entre CarnetDeSante et Nounours
    modelBuilder.Entity<Nounours>() //l'entité Nounours...
                .HasOne(n => n.Carnet) //a une propriété obligatoire Carnet...
                .WithOne(c => c.Owner) //reliée à la propriété Owner du Carnet...
                .HasForeignKey<CarnetDeSante>(c => c.UniqueId);//dont la propriété UniqueId est une Foreign Key
    //remplace la ForeignKey

    base.OnModelCreating(modelBuilder);
}

Voyons cette méthode plus en détails. Tout d'abord, il y a la définition des détails de la table de Nounours :

  • le changement de nom de la table :
modelBuilder.Entity<Nounours>().ToTable("TableNounours");
  • la définition de la clé primaire :
modelBuilder.Entity<Nounours>().HasKey(n => n.UniqueId);
modelBuilder.Entity<Nounours>().Property(n => n.UniqueId)
                               .ValueGeneratedOnAdd();
  • définition de contraintes sur les colonnes de Nounours :
modelBuilder.Entity<Nounours>().Property(n => n.Nom)
                               .IsRequired()
                               .HasMaxLength(256);
modelBuilder.Entity<Nounours>().Property(n => n.DateDeNaissance)
                               .HasColumnName("Naissance")
                               .HasColumnType("date");

On continue avec la définition des détails de la table de CarnetDeSante :

  • le changement de nom de la table :
modelBuilder.Entity<CarnetDeSante>().ToTable("Carnets");
  • la définition de la clé primaire (notez qu'on ne demande pas ici à la base de générer la clé, puisqu'on va utiliser une clé étrangère) :
modelBuilder.Entity<CarnetDeSante>().HasKey(c => c.UniqueId);
modelBuilder.Entity<CarnetDeSante>().Property(c => c.UniqueId)
                                    .ValueGeneratedNever();

On s'intéresse enfin à la relation One To One, où l'on précise qu'une entité Nounours possède une association vers l'entité de type CarnetDeSante grâce à la propriété Carnet (.HasOne(n => n.Carnet)). Cette entité CarnetDeSante possède elle-même une association vers une entité de type Nounours grâce à sa propriété Owner (.WithOne(c => c.Owner)), et on précise que cette entité CarnetDeSante voit sa propriété UniqueId utilisée comme clé étrangère (.HasForeignKey<CarnetDeSante>(c => c.UniqueId)). Elle automatiquement reliée à la clé primaire de Nounours.

modelBuilder.Entity<Nounours>() //l'entité Nounours...
             .HasOne(n => n.Carnet) //a une propriété obligatoire Carnet...
             .WithOne(c => c.Owner) //reliée à la propriété Owner du Carnet...
             .HasForeignKey<CarnetDeSante>(c => c.UniqueId);//dont la propriété UniqueId est une Foreign Key

Pour pouvoir relier une entité Nounours à une entité CarnetDeSante de manière bidirectionnelle, les propriétés Nounours.Carnet et CarnetDeSante.Owner sont suffisantes, à condition qu'une clé étrangère soit déclarée et utilisée. C'est le cas grâce au code précédent qui s'occupe de définir de quelle manière les deux entités sont reliées.

La classe StubbedContext

  • StubbedContext est une classe fille de NounoursDBEntities. Son rôle est de proposer un Stub en plus de ce que sait déjà faire sa classe mère. Elle ne sera donc utilisée que pour des tests unitaires ou fonctionnels. En conséquence, elle reprend tout ce que fait sa classe mère et ne change que la méthode OnModelCreating qui appelle la méthode de la classe mère puis ajoute des instances d'entités, grâce à la méthode d'extension HasData.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<CarnetDeSante>().HasData(
            new CarnetDeSante { UniqueId=1, LastModified = DateTime.Today },
            new CarnetDeSante { UniqueId=2, LastModified = new DateTime(1980, 5, 21) },
            new CarnetDeSante { UniqueId=3, LastModified = new DateTime(1983, 5, 25) }
        );

    modelBuilder.Entity<Nounours>().HasData(
        new Nounours { UniqueId=1, Nom = "Chewbacca", DateDeNaissance = new DateTime(1977, 5, 27), NbPoils = 1234567 },
        new Nounours { UniqueId=2, Nom = "Yoda", DateDeNaissance = new DateTime(1980, 5, 21), NbPoils = 3 },
        new Nounours { UniqueId=3, Nom = "Ewok", DateDeNaissance = new DateTime(1983, 5, 25), NbPoils = 3456789 }
        );
}
  • Remarquez que À AUCUN MOMENT nous ne précisons les valeurs des propriétés Owner de CarnetDeSante et Carnet de Nounours. Le simple fait d'utiliser la même clé (propriétés UniqueId) est suffisant puisque celle de CarnetDeSante est une clé étrangère.
  • Notez que ce ne sera pas le cas lors d'une utilisation classique de nos classes (ajout, modification...). Nous ne donnerons plus les identifiants directement mais les références des propriétés Carnet et Owner.

La classe Program

  • La classe StubbedContext est ensuite indirectement utilisée dans Program pour remplir la base de manière tout à fait classique et ne nécessite aucun appel supplémentaire : ceci est fait lors de la migration et la création de la base.
  • On affiche tout d'abord le contenu de la base (c'est-à-dire rien ou d'anciennes données si la migration est faite à partir de NounoursDBEntites) ou le stub (si la migration est faite à partir de StubbedContext). Notez l'utilisation d'Include dans db.NounoursSet.Include(n => n.Carnet) sinon, les CarnetDeSante ne sont pas chargés. Include n'est pas utilisé ensuite dans db.Carnets car les Nounours ont déjà été chargés depuis la connexion. Mais on aurait pu faire les accès dans l'autre sens et dans ce cas d'abord db.Carnets.Include(c => c.Owner) puis simplement db.NounoursSet.
using (NounoursDBEntities db = new NounoursDBEntities())
{
    WriteLine("Contenu de la base (nounours) : ");
    foreach (var n in db.NounoursSet.Include(n => n.Carnet))
    {
        WriteLine($"\t{n}, LastModified: {n.Carnet.LastModified.ToString("d")}");
    }

    WriteLine("Contenu de la base (carnets de santé) : ");
    foreach (var c in db.Carnets)
    {
        WriteLine($"\t{c}");
    }

    //...
}
  • La suite de l'exemple ajoute un nouveau Nounours et son CarnetDeSante puis affiche le contenu de la base de données.
using (NounoursDBEntities db = new NounoursDBEntities())
{
    //...

    WriteLine("\nAjout d'un nounours et de son carnet de santé\n");
                    
    Nounours porg = new Nounours { Nom = "Porg", DateDeNaissance = new DateTime(2017, 7, 19), NbPoils = 123 };
    CarnetDeSante carnetPorg = new CarnetDeSante { LastModified = DateTime.Now, Owner = porg };
    porg.Carnet = carnetPorg;

    db.AddRange(porg, carnetPorg);

    db.SaveChanges();
}
using (NounoursDBEntities db = new NounoursDBEntities())
{
    WriteLine("Contenu de la base (nounours) : ");
    foreach (var n in db.NounoursSet.Include(n => n.Carnet))
    {
        WriteLine($"\t{n}, LastModified: {n.Carnet.LastModified.ToString("d")}");
    }

    WriteLine("Contenu de la base (carnets de santé) : ");
    foreach (var c in db.Carnets)
    {
        WriteLine($"\t{c}");
    }
}

Comment exécuter cet exemple ?

Pour tester cette application, n'oubliez pas les commandes comme présentées dans l'exemple ex_041_001 : pour générer l'exemple, il vous faut d'abord préparer les migrations et les tables.

  • Ouvrez la Console du Gestionnaire de package sous Windows ou le Terminal sous MacOSX, pour cela, dirigez-vous dans le menu Outils, puis Gestionnaire de package NuGet, puis Console du Gestionnaire de package.
  • Dans la console que vous venez d'ouvrir, déplacez-vous dans le dossier du projet .NET Core, ici :
cd .\p08_BDD_EntityFramework\ex_042_013_OneToOne_FluentAPI

Note: si vous n'avez pas installé correctement EntityFrameworkCore, il vous faudra peut-être utiliser également :

  • dotnet tool install --global dotnet-ef si vous utilisez la dernière version de .NET Core (3.1 aujourd'hui),

  • dotnet tool install --global dotnet-ef --version 3.0.0 si vous vous utiliser spécifiquement .NET Core 3.0.

    • Migration :
dotnet ef migrations add ex_042_013 --context StubbedContext
  • Création de la table :
dotnet ef database update --context StubbedContext
  • Génération et exécution Vous pouvez maintenant générer et exécuter l'exemple ex_042_013_OneToOne_FluentAPI.

  • Le résultat de l'exécution va ressembler à :

Contenu de la base (nounours) :
        1: Chewbacca (27/05/1977, 1234567 poils), LastModified: 20/01/2020
        2: Yoda (21/05/1980, 3 poils), LastModified: 21/05/1980
        3: Ewok (25/05/1983, 3456789 poils), LastModified: 25/05/1983
Contenu de la base (carnets de santé) :
        1 : carnet de Chewbacca, modifié la dernière fois le 20/01/2020
        2 : carnet de Yoda, modifié la dernière fois le 21/05/1980
        3 : carnet de Ewok, modifié la dernière fois le 25/05/1983

Ajout d'un nounours et de son carnet de santé

Contenu de la base (nounours) :
        1: Chewbacca (27/05/1977, 1234567 poils), LastModified: 20/01/2020
        2: Yoda (21/05/1980, 3 poils), LastModified: 21/05/1980
        3: Ewok (25/05/1983, 3456789 poils), LastModified: 25/05/1983
        4: Porg (19/07/2017, 123 poils), LastModified: 20/01/2020
Contenu de la base (carnets de santé) :
        1 : carnet de Chewbacca, modifié la dernière fois le 20/01/2020
        2 : carnet de Yoda, modifié la dernière fois le 21/05/1980
        3 : carnet de Ewok, modifié la dernière fois le 25/05/1983
        4 : carnet de Porg, modifié la dernière fois le 20/01/2020

Note : l'identifiant du dernier Nounours sera vraisemblablement différent puisqu'il est créé par la base lors de l'insertion.

Comment exécuter cet exemple sans le stub ?

Il suffit de faire exactement comme dans le paragraphe précédent, mais en choisissant le contexte NounoursDBEntities à la place de StubbedContext :

dotnet ef migrations add ex_042_013 --context NounoursDBEntities
dotnet ef database update --context NounoursDBEntities

Lors de l'exécution, le résultat sera évidemment différent puisqu'il n'y aura pas les 3 nounours du stub.

Comment vérifier quelles base et tables ont été créées et leur contenu ?

Pour vérifier le contenu de votre base SQLite, vous pouvez utiliser le programme DB Browser :

  • Rendez-vous sur la page : https://sqlitebrowser.org/dl/ et téléchargez le programme DB Browser.
  • Lancez DB Browser for SQLite
  • Glissez-déposez au milieu de la fenêtre de DB Browser for SQLite le fichier ex_042_013_OneToOne_FluentAPI.Nounours.db qui a été généré par l'exécution du programme et qui se trouve près de ex_042_012_OneToOne_conventions.csproj. DB Browser for SQLite
  • Choisissez ensuite l'onglet Parcourir les données
  • Observez les résultats obtenus des deux tables DB Browser for SQLite DB Browser for SQLite