Entity Framework 6

De Banane Atomic
Aller à la navigationAller à la recherche

Liens

Présentation

EF est un ORM (Object Relational Mapper)

  • stockage et la récupération d'objet .NET dans une bdd
  • automatise les taches de connexions et requêtes, rend le code plus concis qu'avec ADO.NET
  • requêtes formulées avec LINQ to Entities

Architecture

Pour éviter les références circulaires: isoler les classes du domaine dans un projet à part, ainsi le projet data model EF pourra le référencer.

  • 1 projet pour les classes du domaine
  • 1 projet pour le data model EF, celui-ci référencera le projet des classes du domaine
  • les autres projets référenceront au besoin l'un ou l'autre ou les 2.

Setting up

  • optinally create a new Class Library project name *.DataAccess
  • add the EntityFramework nuget package
  • create the entities
Entities/Item.cs
public class Item
{
    public int Id { get; set; }
    public string Name { get; set; }
}
  • create the DbContext
MyAppDbContext.cs
public class MyAppDbContext : DbContext
{
    public DbSet<Item> Items { get; set; }
}
  • create the repository
Repositories/ItemRepository.cs
public class ItemRepository : IItemRepository
{
    private readonly MyAppDbContext dbContext;

    public ItemRepository(MyAppDbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    public async Task<IEnumerable<Item>> GetAsync(CancellationToken cancellationToken)
    {
        var items = await this.dbContext.Items.ToListAsync(cancellationToken);
        return items;
    }
}
  • connection string. Use the same name as the DbContext class to load automatically the connection string.
Web.config
<configuration>
  <configSections></configSections>
  <connectionStrings>
    <add name="MyAppDbContext"
         providerName="System.Data.SqlClient"
         connectionString="Server={server-name};Database={db-name};Trusted_Connection=True;MultipleActiveResultSets=true;Connection Timeout=3" />
  </connectionStrings>
  • dependency injection of the DbContext in a web api project
App_Start/DryIocConfig.cs
public static class DryIocConfig
{
    public static void Register(HttpConfiguration config)
    {
        var container = new Container().WithWebApi(config, throwIfUnresolved: t => t.IsController());

        container.Register<IItemRepository, ItemRepository>();
        container.Register<MyAppDbContext>(Reuse.InWebRequest);
    }
}

Code first

  • création du code SQL à partir des classes .NET permettant la création d'une bdd
  • au run-time, les classes .NET permettent la génération du Model
  • si les classes .NET changent, il est possible de mettre à jour la bdd

Ajout d'une classe *Context

  1. Ajouter un project *DataModel de type Class Library
  2. Nuget → Ajouter EF
  3. Créer une classe *Context
ContactContext.cs
public class ContactContext : DbContext  // hérite de DbContext
{
    // DbSet de Contact permet de manipuler une collection de Contact
    public DbSet<Contact> Contacts { get; set; }
}

Vérifier le Data Model

Installer l'extension Entity Framework 6 Power Tools
Permet de générer le Data Model de la même manière qu'à l’exécution afin de vérifier que le Data Model correspond bien à nos attentes.
  1. Solution Explorer → clique-droit sur la classe *Context → Entity Framework → View Entity Data Model
  2. Corriger le Data Model si besoin
Le projet contenant la classe *Context doit être le Startup Project
Csharp.svg
public class Contact
{
    // relation 0..1 - * entre Address et Contact
    // un Contact peut avoir 0,1 ou plusieurs addresses. Une adresse appartient à 0 ou 1 Contact.
    public List<Address> Addresses { get; set; }
}

public class Address
{
    // Contact étant nullable, une adresse appartient à 0 ou 1 Contact.
    [Required]  // pour spécifier qu'une adresse appartient forcément à 1 Contact
    public Contact Contact { get; set; }
}

Data Annotations

Contact.cs
// force le nom de la table
[Table("Persons")]
public class Contact
{
    // définit ContactId comme clé primaire
    [Key]
    public string ContactId { get; set; }

    // ajoute un index. Nom par défaut: IX_<property name>
    [Index("AgeIndex")]
    public int Age { get; set; }

     // ajoute un index unique. Nom par défaut: IX_<property name>
    [Index(IsUnique = true)]
     // longueur max 200
    [StringLength(200)]
    public string Username { get; set; }

    // Address étant nullable, rend la propriété obligatoire (not null)
    [Required]
    public Address Address { get; set; }

    // propriété à ne pas prendre en compte
    [NotMapped]
    public int MyNumber { get; } = 45;

    // force le nom de la colonne et son type de donnée
    [Column("ContactDescription", TypeName="ntext")]
    public string Description { get; set; }

Fluent API with configuration files

ContactConfiguration.cs
internal sealed class ContactConfiguration : EntityTypeConfiguration<Contact>
{
    public ContactConfiguration()
    {
        this.HasKey(x => x.ContactId);
        this.HasIndex(x => x.Age).HasName("AgeIndex");
        this.HasIndex(x => x.Username).IsUnique(true);
        this.Property(x => x.Username).HasMaxLength(200);
        this.HasRequired(x => x.Address);
        this.Ignore(x => x.MyNumber);
        this.Property(x => x.Description).HasColumnName("ContactDescription").HasColumnType("ntype");
    }
}
MyAppContext.cs
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    // use all types that inherit from EntityTypeConfiguration and ComplexTypeConfiguration in the current assembly
    modelBuilder.Configurations.AddFromAssembly(typeof(MyAppContext).Assembly);

Astuces

Ignorer une propriété

ContactContext.cs
public class ContactContext : DbContext
{
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // ignore la propriété MyProperty dans tous les types
        modelBuilder.Types().Configure(c => c.Ignore("MyProperty"));
        base.OnModelCreating(modelBuilder);

Modifier les données avant de les sauvegarder dans la bdd

ContactContext.cs
public class ContactContext : DbContext
{
    public override int SaveChanges()
    {
        foreach (var contact in ChangeTracker.Entries()
            .Where(e => e.Entity is Contact && (e.State == EntityState.Added || e.State == EntityState.Modified))
            .Select(e => e.Entity as Contact))
        {
            contact.DateModified = DateTime.Now;
        }

        return base.SaveChanges();

DB first

  • génération d'un fichier *.edmx à partir d'une bdd existante
  • au run-time, le fichier *.edmx permet la génération du Model
  • si la bdd change, il est possible de mettre à jour le fichier *.edmx

Ajouter un nouveau Entity Data Model

Clique-droit dans le projet → Add → New Item → Data → ADO.NET Entity Data Model → EF Designer from database

Connection String dynamique

Csharp.svg
var entityBuilder = new EntityConnectionStringBuilder();
entityBuilder.Provider = "System.Data.SqlClient";
// Ma Connection String dynamique
entityBuilder.ProviderConnectionString = "Data Source=MonServeur;Initial Catalog=MaBdd;Integrated Security=True";
entityBuilder.Metadata = "res://*/MonModel.csdl|res://*/MonModel.ssdl|res://*/MonModel.msl";

Entities entities = new Entities(entityBuilder.ToString());

Mettre à jour le model edmx après un changement dans la bdd

Ouvrir le model edmx → clique-droit → Update Model from Database → Refresh

Utilisation

Csharp.svg
using (var context = new ContactContext())
{
    // récupérer des données
    List<Contact> allContacts = context.Contacts.ToList();
    Contact contact = context.Contacts.FirstOrDefault(c => c.Name == "Nicolas");
    // avec la primary key
    Contact contact = context.Contacts.Find(contactId);
    // équivalent à 
    Contact contact = context.Contacts.SingleOrDefault(c => c.Id == contactId);
    
    List<SqlParameter> parameters = this.BuildSqlParameters(...);
    string parameterNamesList = string.Join(", ", parameters.Select(p => p.ParameterName));
    // exécuter un procédure stockée qui renvoie des contacts
    var contacts = context.Contacts.SqlQuery<List<Contact>>(
        $"GetContacts {parameterNamesList}",
        parameters.Cast<object>().ToArray()).ToListAsync(cancellationToken);
    // exécuter un procédure stockée qui ne renvoie pas de résultat
    await this.context.Database.ExecuteSqlCommandAsync(
        $"InsertContacts {parameterNamesList}",
        cancellationToken,
        parameters.Cast<object>().ToArray());

    // récupérer des contacts ainsi que les numéros de téléphone associés
    List<Contact> allContacts = context.Contacts.Include(c => c.PhoneNumbers).ToList();
    // récupérer des données en mémoire
    ObservableCollection<Contact> allContacts = context.Contacts.Local;

    // insérer des données
    context.Contacts.Add(new Contact() { Name = "Nicolas" });
    context.SaveChanges();

    // mettre à jour des données
    Contact contact = context.Contacts.FirstOrDefault(c => c.Name == "Nicolas");
    contact.Address = "New Address";
    context.SaveChanges();

    // insérer ou mettre à jour des données
    // using System.Data.Entity.Migrations;
    context.Contacts.AddOrUpdate(contact);
    context.SaveChanges();

    // supprimer un contact
    context.Contacts.Remove(contact);
    // supprimer tous les contacts un par un
    context.Contacts.RemoveRange(context.Contacts);

    // exécuter une commande sql qui ne retourne pas de contacts
    context.Database.ExecuteSqlCommand("truncate table Contacts");
    context.Database.ExecuteSqlCommand($"exec DeleteContactById {Id}");
}
Csharp.svg
// context.Contacts.Contains(contact) or context.Contacts.Any(c => c == contact) raises the following exception
// Unable to create a constant value of type '...'. Only primitive types or enumeration types are supported in this context.
if (context.Contacts.Any(c => c.Id == contact.Id))
{
    // if the contact is not already added in the database the following exception is raised
    // InvalidOperationException : The element at index 0 in the collection of objects to refresh is in the added state.
    // Objects in this state cannot be refreshed.
    context.Contacts.Refresh(RefreshMode.StoreWins, contact);
}

Initialisation de la bdd

Titre colonne 1 Titre colonne 2
CreateDatabaseIfNotExists<TContext> créé la bdd si elle n'existe pas.
Option par défaut.
MigrateDatabaseToLatestVersion<TContext, Configuration> migre la db si besoin mais ne la créé pas si elle n'existe pas.
DropCreateDatabaseAlways<TContext> recréé la bdd à chaque fois.
DropCreateDatabaseIfModelChanges<TContext> recréé la bdd si le data model a changé.
NullDatabaseInitializer<TContext> ne rien faire.
À placer avant le premier appel au context: Global.asax.cs ou App.xaml.cs
Csharp.svg
// ne rien faire à l'initialisation
Database.SetInitializer(new NullDatabaseInitializer<ContactContext>());
// équivalent
Database.SetInitializer<ContactContext>(null);
DataHelpers.cs
public static class DataHelpers
{
    // recréé la bdd et la rempli
    public static void NewDbWithSeed()
    {
        Database.SetInitializer(new DropCreateDatabaseAlways<ComptesContext>());
        using (var context = new ComptesContext())
        {
            context.Comptes.AddRange(new List<Compte>()
            {
                new Compte() { Name = "Stan" },
                new Compte() { Name = "Kenny" }
            });
            context.SaveChanges();

Relations

Csharp.svg
class Entreprise
{
    public int Id { get; set; }
    public string Name { get; set; }

    // liaison Entreprise → Employe. Optionnel si Employe contient déjà un lien vers Entreprise.
    // pratique pour connaître directement la liste des employés
    public List<Employe> Employes { get; set; }
}

class Employe
{
    public int Id { get; set; }
    public string Name { get; set; }

    // liaison Employe → Entreprise. Optionnel si Entreprise contient déjà un lien vers Employe.
    // pratique pour connaître directement l'entreprise d'un employé
    // permet aussi de rendre obligatoire la propriété Entreprise avec Required. Relation 0..1 devient 1
    [Required]
    public Entreprise Entreprise { get; set; }

    // déclaration de la foreign key. Optionnel mais très utile dans les applications web car déconnectées.
    // permet de rendre obligatoire la propriété Entreprise sans Required. Relation 0..1 devient 1
    public int EntrepriseId { get; set; }
}

class AppContext : DbContext
{
    public DbSet<Entreprise> Entreprises { get; set; }
    public DbSet<Employe> Employes { get; set; }
}
Csharp.svg
// ajout de données dans la bdd
Database.SetInitializer(new DropCreateDatabaseAlways<AppContext>());
using (var context = new AppContext())
{
    // création des entreprise sans initialiser Entreprise.Employes ni renseigner les employés
    var wc = context.Entreprises.Add(new Entreprise() { Name = "World Companie" });
    var sb = context.Entreprises.Add(new Entreprise() { Name = "Super Bank" });

    context.Employes.AddRange(new List<Employe>()
    {
        // la liaison entre Entreprise et Employe se fait grace à la propriété Employe.Entreprise
        // lors de la récupération des entreprises, Entreprise.Employes sera renseignée 
        new Employe() { Name = "Mike", Entreprise = wc },
        new Employe() { Name = "Paul", Entreprise = wc },
        new Employe() { Name = "Julie", Entreprise = sb },
        new Employe() { Name = "Rachelle", Entreprise = sb }
    });

    context.SaveChanges();
Csharp.svg
using (var context = new AppContext())
{
    // sans Include entreprise.Employes serait null
    // une autre solution moins performante: mettre entreprise.Employes en virtual
    foreach (var entreprise in context.Entreprises.Include(e => e.Employes))
    {
        Console.WriteLine(entreprise.Name);
        foreach (var employe in entreprise.Employes)
        {
            Console.WriteLine(employe.Name);
        }
    }

Log the SQL queries

Csharp.svg
// in VS Output
context.Database.Log = s => Debug.WriteLine(s);

// in Console app
context.Database.Log = Console.WriteLine;

Configuration

SQL Server Express ou LocalDb

Sans configuration, SQL Server Express est utilisé s'il est installé, sinon LocalDb.
Web.config
<!-- SQL Server -->
<entityFramework>
  <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />

<!-- LocalDB -->
<entityFramework>
  <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
    <parameters>
      <parameter value="mssqllocaldb" />
    </parameters>
  </defaultConnectionFactory>

Connection String

Si le connection string name correspond à la classe *Context (avec ou sans namespace), la connexion sera chargée automatiquement
Default database name: Namespace.ClasseContext
Web.config
<configuration>
  <connectionStrings>
    <add name="ClasseContext"
         providerName="System.Data.SqlClient"
         connectionString="data source=PC-NAME\SQLEXPRESS;initial catalog=Namespace.ClasseContext;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" />

Migration

  • Dans Package Manager Console, sélectionner le projet qui contient les référence EF.
  • C'est le fichier de config du startup project qui est lu pour obtenir la connection string.

Initialisation

  • Activer la migration: View → Other Windows → Package Manager Console
Powershell.svg
Enable-Migrations
# s'il y a plusieurs classe *Context: ajouter -ContextTypeName ClasseContext

# un dossier Migration avec un fichier Configuration.cs est créé
  • Si la bdd existe a déjà été créé, un fichier YYYYMMDDHHMMSSS_IntialCreate.cs est ajouté au dossier Migration. Il représente le code C# qui permet la création de la structure de la bdd.
  • Si la bdd n'existe pas encore:
Powershell.svg
# Créer le fichier de migration avec la commande suivante
Add-Migration IntialCreate
# un fichier YYYYMMDDHHMMSSS_IntialCreate.cs est créé dans le dossier Migration

# génération et exécution du script T-SQL pour créer la bdd
Update-Database -Verbose

Migration

Powershell.svg
# Après avoir modifié le modèle objet.
Add-Migration Migration-Name
# créé un fichier YYYYMMDDHHMMSSS_Migration-Name.cs dans le dossier Migration
YYYYMMDDHHMMSSS_Migration-Name.cs
// Vérifier le contenu du fichier et apporter des modifications si besoin
public partial class Migration-Name : DbMigration
{
    public override void Up()
    {
        // Exécuter une commande SQL 
        Sql("UPDATE dbo.MyTable SET Column1 = 'xxx' WHERE Column1 IS NULL");
Powershell.svg
# génération et exécution du script T-SQL
Update-Database -Verbose

Autres

Powershell.svg
# list the applied migrations
get-migrations

# il est possible de retourner à n'importe quelle étape de migration
Update-Database -TargetMigration: Migration-Name

# générer un script de la création complète de la bdd jusqu'à Migration-Name
Update-Database -Script -SourceMigration: $InitialDatabase -TargetMigration: Migration-Name
# par défaut, SourceMigration: état actuel, et TargetMigration: dernière migration

Configuration.cs

Configuration.cs
public Configuration()
{
    // si true, permet d'appeler Update-Database pour mettre à jour la bdd sans créer de point de migration avec Add-Migration
    AutomaticMigrationsEnabled = false;
    ContextKey = "Namespace.ClasseContext";
}

//  This method will be called after migrating to the latest version.
protected override void Seed(ConsoleTestEF.AppContext context)
{
    var e1 = new Entreprise() { Nom = "World Companie" };
    var e2 = new Entreprise() { Nom = "Super Bank" };

    // ajoute les entreprises précédentes si elles n'existent pas
    // le test d’existence se fait sur la propriété Nom
    context.Entreprises.AddOrUpdate(e => e.Nom, e1, e2);

    // si on a des éléments référençant les entreprises précédemment créées, il faut appeler SaveChanges
    context.SaveChanges();

    // ajoute 1000 employés si leur nom n'existe pas déjà
    var rnd = new Random();
    for (int i = 0; i < 1000; i++)
    {
        context.Employes.AddOrUpdate(e => e.Nom,
            new Employe()
            {
                Nom = $"Mr {i}",
                // utiliser la ForeignKey EntrepriseId, plutôt que la propriété Entreprise
                EntrepriseId = rnd.Next(2) == 0 ? e1.Id : e2.Id
            });
    }

Unit tests with EFFORT

Entity Framework Fake ObjectContext Realization Tool is the official In Memory provider for Entity Framework Classic.
It creates a fake or mock database that allows you to test the Business Logic Layer (BLL) without worrying about your Data Access Layer (DAL).

Cs.svg
using Effort;

// create the fake context
var connection = DbConnectionFactory.CreateTransient();
var context = new MyAppContext(connection, true);

// fill the database
context.Database.CreateIfNotExists(); // create the db once for all the tests
using (this.Context.Database.Connection.BeginTransaction()) // then use transactions
{
    var item = new Item();
    context.Set<Item>().Add(item);
    await context.SaveChangesAsync(CancellationToken.None);
}

// create the repository and call the method to test
var repository = new ItemRepository(context);
var query = new ItemQuery();
var result = await repository.GetAsync(query);
You can use the ids to link objects between themselves, but when SaveChangesAsync is called all the ids are replaced by the values they have now in the database.

Error: Incorrect number of arguments for constructor

Cs.svg
DbConnectionFactory.LargePropertyCount = 16; // set from 8 to 16
var connection = DbConnectionFactory.CreateTransient();
var context = new MyAppContext(connection, true);

Erreurs

The ObjectContext instance has been disposed and can no longer be used for operations that require a connection

L'objet a été disposé, via un using par exemple, et ne peut plus faire de requêtes.

EntityType '...' has no key defined. Define the key for this EntityType.

La classe ne contient pas de clé.

  • Ajouter une propriété Id de type int.
  • Décorer une propriété avec l'attribut [Key].

Cannot drop database "..." because it is currently in use.

Fermer SSMS.

EF6 vs EF7

EF6 EF7
Visual Designer Oui Non
Backward Compatible Oui Non
Core CLR Support Non Oui
Non-Relational Data Non Oui

EF et WPF

ConnectedRepository.cs
// instancié par la MainWindow, une seule instance pour toute la durée de vie de l'application
public class ConnectedRepository
{
    // utilisation d'un seul context
    private readonly ContactContext _context = new ContactContext();

    // méthode pour récupérer les données
    public List<Contact> GetAllContacts()
    {
        return _context.Contacts.ToList();

EF et ASP.NET MVC

DisconnectedRepository.cs
public class DisconnectedRepository
{
    public IEnumerable<Ecriture> GetEcritures()
    {
        using (var context = new ComptesContext())
        {
            return context.Ecritures.AsNoTracking().ToList();
        }
    }

    public Ecriture GetEcriture(int id)
    {
        using (var context = new ComptesContext())
        {
            return context.Ecritures.Find(id);
            // plus performant mais moins lisible
            return context.Ecritures.AsNoTracking().SingleOrDefault(e => e.Id == id);
        }
    }

    public void Add(Ecriture ecriture)
    {
        using (var context = new ComptesContext())
        {
            context.Ecritures.Add(ecriture);
            context.SaveChanges();
        }
    }

    public void Edit(Ecriture ecriture)
    {
        using (var context = new ComptesContext())
        {
            context.Entry(ecriture).State = EntityState.Modified;
            context.SaveChanges();
        }
    }

    public void Delete(int id)
    {
        Delete(GetEcriture(id));
    }

    public void Delete(Ecriture ecriture)
    {
        using (var context = new ComptesContext())
        {
            context.Entry(ecriture).State = EntityState.Deleted;
            context.SaveChanges();
        }
    }

Console application

  1. add EntityFramework nuget package

Data

Data/Item.cs
class Item
{
    public int Id { get; set; }
    public string Name { get; set; }
}

DBContext

Data/ItemDbContext.cs
class ItemDbContext : DbContext
{
    public ItemDbContext() : base("name=ItemContext")
    { }

    public DbSet<Item> Items { get; set; }
}
App.config
<configuration>
    <connectionStrings>
        <add name="ItemContext" connectionString="Server=localhost;Database=Item;Integrated Security=True" providerName="System.Data.SqlClient" />
    </connectionStrings>
</configuration>

Entities configuration

Data/ItemDbContext.cs
class ItemDbContext : DbContext
{
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Configurations.AddFromAssembly(typeof(ItemDbContext).Assembly);
    }
}
EntitiesConfiguration/ItemConfiguration.cs
class ItemConfiguration : EntityTypeConfiguration<Item>
{
    public ItemConfiguration()
    {
        this.Property(x => x.Name).IsRequired().HasMaxLength(50);
    }
}

Migration

  1. Visual Studio → Package Manager Console
Ps.svg
# create a Configuration class
Enable-Migrations
Migrations/Configuration.cs
internal sealed class Configuration : DbMigrationsConfiguration<ItemDbContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false;
    }

    protected override void Seed(ItemDbContext context)
    {
        context.Items.AddOrUpdate(new Item { Id = 1, Name = "Item1" });
        context.Items.AddOrUpdate(new Item { Id = 2, Name = "Item2" });
    }
}
Ps.svg
# add a migration
Add-Migration IntialCreate

# generate the sql script representing the migration
Update-Database -script

# if it is not what you expected, update the entities and the configuration then run again the add migration
Add-Migration IntialCreate

# apply the migration by updating the database
Update-Database

Program

Program.cs
static void Main(string[] args)
{
    using (var db = new ItemDbContext())
    {
        var items = db.Items.ToList();
        foreach (var item in items)
        {
            Console.WriteLine($@"{item.Id} - {item.Name}");
        }
    }

    Console.ReadKey();
}