|
|
Ligne 30 : |
Ligne 30 : |
|
| |
|
| * create the {{boxx|DbContext}} | | * create the {{boxx|DbContext}} |
| <filebox fn='ItemDbContext.cs'> | | <filebox fn='MyAppDbContext.cs'> |
| public class MyAppDbContext : DbContext | | public class MyAppDbContext : DbContext |
| { | | { |
Version du 18 juin 2021 à 15:07
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.
- 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; }
}
|
MyAppDbContext.cs
|
public class MyAppDbContext : DbContext
{
public DbSet<Item> Items { get; set; }
}
|
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>
|
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
- Ajouter un project *DataModel de type Class Library
- Nuget → Ajouter EF
- 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. |
- Solution Explorer → clique-droit sur la classe *Context → Entity Framework → View Entity Data Model
- Corriger le Data Model si besoin
|
Le projet contenant la classe *Context doit être le Startup Project |
|
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; }
}
|
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
|
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();
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
|
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
|
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);
// exécuter un procédure stockée qui renvoie des contacts
List<Contact> contacts = context.Contacts.SqlQuery("exec GetContacts").ToList();
// 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}");
}
|
|
// 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);
}
|
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 |
|
// 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
|
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; }
}
|
|
// 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();
|
|
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
|
// in VS Output
context.Database.Log = s => Debug.WriteLine(s);
// in Console app
context.Database.Log = Console.WriteLine;
|
|
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" />
|
|
- 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
|
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:
|
# 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
|
# 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");
|
|
# génération et exécution du script T-SQL
Update-Database -Verbose
|
Autres
|
# 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
});
}
|
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).
|
using Effort;
// create the fake context
var connection = DbConnectionFactory.CreateTransient();
var context = new MyAppContext(connection, true);
// fill the database
context.Database.CreateIfNotExists();
var item = new Item();
context.Set<Item>().Add(item);
context.SaveChanges();
// create the repository and call the method to test
var repository = new ItemRepository(context);
var query = new ItemQuery();
var result = await repository.GetAsync(query);
|
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
- 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
- Visual Studio → Package Manager Console
|
# 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" });
}
}
|
|
# 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();
}
|