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>
|
- 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
- Ajouter un project *DataModel de type Class Library
- Nuget → Ajouter EF
- Créer une classe *Context
ContactContext.cs
|
public class ContactContext : DbContext
{
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
{
public List<Address> Addresses { get; set; }
}
public class Address
{
[Required]
public Contact Contact { get; set; }
}
|
Contact.cs
|
[Table("Persons")]
public class Contact
{
[Key]
public string ContactId { get; set; }
[Index("AgeIndex")]
public int Age { get; set; }
[Index(IsUnique = true)]
[StringLength(200)]
public string Username { get; set; }
[Required]
public Address Address { get; set; }
[NotMapped]
public int MyNumber { get; } = 45;
[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)
{
modelBuilder.Configurations.AddFromAssembly(typeof(MyAppContext).Assembly);
|
Astuces
Ignorer une propriété
ContactContext.cs
|
public class ContactContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
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";
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())
{
List<Contact> allContacts = context.Contacts.ToList();
Contact contact = context.Contacts.FirstOrDefault(c => c.Name == "Nicolas");
Contact contact = context.Contacts.Find(contactId);
Contact contact = context.Contacts.SingleOrDefault(c => c.Id == contactId);
List<SqlParameter> parameters = this.BuildSqlParameters(...);
string parameterNamesList = string.Join(", ", parameters.Select(p => p.ParameterName));
var contacts = context.Contacts.SqlQuery<List<Contact>>(
$"GetContacts {parameterNamesList}",
parameters.Cast<object>().ToArray()).ToListAsync(cancellationToken);
await this.context.Database.ExecuteSqlCommandAsync(
$"InsertContacts {parameterNamesList}",
cancellationToken,
parameters.Cast<object>().ToArray());
List<Contact> allContacts = context.Contacts.Include(c => c.PhoneNumbers).ToList();
ObservableCollection<Contact> allContacts = context.Contacts.Local;
context.Contacts.Add(new Contact() { Name = "Nicolas" });
context.SaveChanges();
Contact contact = context.Contacts.FirstOrDefault(c => c.Name == "Nicolas");
contact.Address = "New Address";
context.SaveChanges();
context.Contacts.AddOrUpdate(contact);
context.SaveChanges();
context.Contacts.Remove(contact);
context.Contacts.RemoveRange(context.Contacts);
context.Database.ExecuteSqlCommand("truncate table Contacts");
context.Database.ExecuteSqlCommand($"exec DeleteContactById {Id}");
}
|
|
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 |
|
Database.SetInitializer(new NullDatabaseInitializer<ContactContext>());
Database.SetInitializer<ContactContext>(null);
|
DataHelpers.cs
|
public static class DataHelpers
{
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; }
public List<Employe> Employes { get; set; }
}
class Employe
{
public int Id { get; set; }
public string Name { get; set; }
[Required]
public Entreprise Entreprise { get; set; }
public int EntrepriseId { get; set; }
}
class AppContext : DbContext
{
public DbSet<Entreprise> Entreprises { get; set; }
public DbSet<Employe> Employes { get; set; }
}
|
|
Database.SetInitializer(new DropCreateDatabaseAlways<AppContext>());
using (var context = new AppContext())
{
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())
{
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
|
context.Database.Log = s => Debug.WriteLine(s);
context.Database.Log = Console.WriteLine;
|
 |
Sans configuration, SQL Server Express est utilisé s'il est installé, sinon LocalDb. |
Web.config
|
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
<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
|
- 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:
|
Add-Migration IntialCreate
Update-Database -Verbose
|
Migration
|
Add-Migration Migration-Name
|
YYYYMMDDHHMMSSS_Migration-Name.cs
|
public partial class Migration-Name : DbMigration
{
public override void Up()
{
Sql("UPDATE dbo.MyTable SET Column1 = 'xxx' WHERE Column1 IS NULL");
|
|
Update-Database -Verbose
|
Autres
|
get-migrations
Update-Database -TargetMigration: Migration-Name
Update-Database -Script -SourceMigration: $InitialDatabase -TargetMigration: Migration-Name
|
Configuration.cs
Configuration.cs
|
public Configuration()
{
AutomaticMigrationsEnabled = false;
ContextKey = "Namespace.ClasseContext";
}
protected override void Seed(ConsoleTestEF.AppContext context)
{
var e1 = new Entreprise() { Nom = "World Companie" };
var e2 = new Entreprise() { Nom = "Super Bank" };
context.Entreprises.AddOrUpdate(e => e.Nom, e1, e2);
context.SaveChanges();
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;
var connection = DbConnectionFactory.CreateTransient();
var context = new MyAppContext(connection, true);
context.Database.CreateIfNotExists();
using (this.Context.Database.Connection.BeginTransaction()) // then use transactions
{
var item = new Item();
context.Set<Item>().Add(item);
await context.SaveChangesAsync(CancellationToken.None);
}
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. |
|
DbConnectionFactory.LargePropertyCount = 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
|
public class ConnectedRepository
{
private readonly ContactContext _context = new ContactContext();
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);
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
|
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-Migration IntialCreate
Update-Database -script
Add-Migration IntialCreate
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();
}
|