« Entity Framework Core » : différence entre les versions
Ligne 183 : | Ligne 183 : | ||
= Connection string = | = Connection string = | ||
* [[Asp.net_core#Configuration|Configuration file and secrets]] | * [[Asp.net_core#Configuration|Configuration file and secrets]] | ||
== OnConfiguring == | |||
<filebox fn='DataAccess/MyAppContext.cs'> | |||
public class MyAppContext : DbContext | |||
{ | |||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | |||
{ | |||
optionsBuilder.UseSqlServer("server=localhost;database=test;user=test;password=****"); | |||
optionsBuilder.UseSqlServer(Configuration.GetConnectionString("SqlServer")); // Web / Console | |||
optionsBuilder.UseSqlServer(ConfigurationManager.ConnectionStrings["SqlServerConnectionString"].ConnectionString); // WPF | |||
} | |||
} | |||
</filebox> | |||
== WebHost == | == WebHost == | ||
Ligne 218 : | Ligne 231 : | ||
options.UseInMemoryDatabase("WebApplicationCoreMemoryDb"); | options.UseInMemoryDatabase("WebApplicationCoreMemoryDb"); | ||
); | ); | ||
</filebox> | </filebox> | ||
Version du 22 novembre 2020 à 18:23
Liens
- Getting Started with EF Core on .NET Core Console App with a New database
- Getting started with ASP.NET Core and Entity Framework Core using Visual Studio
- ASP.NET Core 2.0 with SQLite and Entity Framework Core
- Utilisation
- Migration
Description
- Réécriture complète d'EF
- Plus de nécessite d'utilisé des BdD relationnelles uniquement
Ajouter les packages au projet
cd MyProject dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design |
Data Providers
Provider | Package NuGet | Connection String |
---|---|---|
SQL Server | Microsoft.EntityFrameworkCore.SqlServer | Server=(localdb)\\MSSQLLocalDB;Database=MyDb;Integrated Security=True;MultipleActiveResultSets=True; Server=localhost;Database=MyDb;User=sa;Password=pwd; |
Sqlite | Microsoft.EntityFrameworkCore.Sqlite | Data Source=file.db |
MySQL / MariaDB | Pomelo.EntityFrameworkCore.MySql | server=localhost;database=MyDb;user=root;password=pwd |
Entity Framework Core Tools
# tester si Entity Framework Core Tools est bien installé et fonctionnel dotnet ef # bien se mettre dans le dossier du projet # faire un dotnet restore au cas où la restoration n'aurait pas fonctionnée |
EF Core 3.x
# dotnet ef must be installed as a global or local tool dotnet tool install --global dotnet-ef # installed in ~/.dotnet/tools # Add ~/.dotnet/tools to PATH # màj de dotnet-ef dotnet tool update --global dotnet-ef # ajouter le paquet nuget suivant dotnet add package Microsoft.EntityFrameworkCore.Design |
ASP.NET Core 2.1+
Plus besoin depuis dotnet core 2.1 (lien) |
Si le fichier de projet ne contient pas Microsoft.EntityFrameworkCore.Tools.DotNet, l'ajouter. (version)
À la sauvegarde NuGet va restaurer les paquets nécessaires.
MyProject.csproj |
<ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" /> </ItemGroup> |
Create DbContext
DataAccess/MyAppContext.cs |
public class MyAppContext : DbContext { public DbSet<Item> Items { get; set; } } |
Configure entity types
Target | Convention |
---|---|
Primary key | property named Id or <type_name>Id will be configured as the primary key of an entity with the name PK_<type_name>. |
Foreign key | a relationship will be created when there is a navigation property discovered on a type. |
required | property whose .NET type can contain null will be configured as optional, whereas properties whose .NET type cannot contain null will be configured as required. |
Table name | same name as the DbSet property that exposes the entity or same as class name. |
Links:
EntityTypeConfiguration with Fluent API
DataAccess/DbModelConfiguration/ItemConfiguration.cs |
internal sealed class ItemConfiguration : IEntityTypeConfiguration<Item> { public void Configure(EntityTypeBuilder<Item> builder) { builder.ToTable("item"); // bc same name as the DbSet property builder.Property(e => e.Id) .HasColumnName("id") // bc same name as property name .HasColumnType("smallint"); builder.HasKey(e => e.Id) // bc a primary key is automatically generated on property named \w*Id .HasName("pk_item"); // bc PK_item builder.Property(e => e.Name) .HasColumnName("name") .HasMaxLength(50) // varchar(50) CHARACTER SET utf8mb4 .HasColumnType("nvarchar(50)") .HasCharSet("utf8mb4") // if not specified the database character set and collation are used .HasCollation("utf8mb4_unicode_ci"); .IsRequired(); builder.Property(e => e.CreationDate) .HasColumnName("creation_date") .HasColumnType("date"); builder.Property(e => e.Price) .HasColumnName("price") .HasColumnType("decimal(5,2)"); builder.HasIndex(e => e.Name) .HasName("ix_item_name") .IsUnique(); // foreign key builder.Property(e => e.GroupId) .HasColumnName("group_id") .HasColumnType("tinyint"); builder.HasIndex(e => e.GroupId) .HasName("ix_items_group_id"); builder.HasOne(e => e.Group) // navigation property: Item.Group .WithMany(e => e.Items) // with navigation property: Group.Items .WithMany() // without navigation property: Group.Items .HasForeignKey(e => e.GroupId) // foreign key property: Item.GroupId .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("fk_items_groups_group_id"); |
Unsigned types are not supported. |
DataAccess/ItemContext.cs |
protected override void OnModelCreating(ModelBuilder modelBuilder) { // specify the configuration modelBuilder.ApplyConfiguration(new ItemConfiguration()); // look for EntityTypeConfiguration in the current assembly modelBuilder.ApplyConfigurationsFromAssembly(typeof(ItemContext).Assembly); } |
Data Annotations Attributes
Data/Entities/Item.cs |
// définit le nom de la table, par défaut le nom de la classe est utilisées [Table("Items")] public class Item { public int Id { get; set; } [Column("My_Name")] // mapping public string Name { get; set; } // définit le type SQL qui sera utilisé pour le stockage de la donnée, ici un décimal de 7 chiffres dont 2 après la virgule [Column(TypeName = "decimal(7, 2)")] public decimal Price { get; set; } [Column(TypeName = "date")] public DateTime Date { get; set; } } |
Nécessite le package nuget System.ComponentModel.DataAnnotations
Vue/view et QueryType
Data/MyAppContext.cs |
public class MyAppContext : DbContext { protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Query<MyQueryType>().ToView("MyView"); |
Connection string
OnConfiguring
DataAccess/MyAppContext.cs |
public class MyAppContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("server=localhost;database=test;user=test;password=****"); optionsBuilder.UseSqlServer(Configuration.GetConnectionString("SqlServer")); // Web / Console optionsBuilder.UseSqlServer(ConfigurationManager.ConnectionStrings["SqlServerConnectionString"].ConnectionString); // WPF } } |
WebHost
DataAccess/MyAppContext.cs |
public class MyAppContext : DbContext { // permet de passer des options à la construction du DbContext public MyAppContext(DbContextOptions<MyAppContext> options) : base(options) {} } |
Startup.cs |
using Microsoft.EntityFrameworkCore; public void ConfigureServices(IServiceCollection services) { // SQL Server services.AddDbContext<AppContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SqlServer")) ); // MySql services.AddDbContext<AppDbContext>(options => options.UseMySql( Configuration.GetConnectionString("MySql"), mySqlOptions => mySqlOptions.ServerVersion( new ServerVersion(new Version(10, 5, 8), ServerType.MariaDb))); // mySqlOptions => mySqlOptions.ServerVersion("10.5.8-mariadb"); ); // In Memory services.AddDbContext<AppContext>(options => options.UseInMemoryDatabase("WebApplicationCoreMemoryDb"); ); |
IDesignTimeDbContextFactory
Explain how to create a Context which doesn't have a parameterless ctor.
DataAccess/MyAppContextFactory |
public class MyAppContextFactory : IDesignTimeDbContextFactory<MyAppContext> { private string connectionString; public MyAppContextFactory() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddUserSecrets<MyAppContextFactory>(); // read stored secrets var configuration = builder.Build(); connectionString = configuration.GetConnectionString("SqlServer"); } public MyAppContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder<MyAppContext>(); builder.UseSqlServer(connectionString); var context = new MyAppContext(builder.Options); return context; } } |
DataAccess/MyAppContext.cs |
public class MyAppContext : DbContext { // permet de passer des options à la construction du DbContext public MyAppContext(DbContextOptions<MyAppContext> options) : base(options) {} } |
Migrations
# création du fichier de migration dotnet ef migrations add InitialCreate # un fichier YYYYMMDDHHMMSSS_IntialCreate.cs est créé dans le dossier Migration # supprimer la dernière migration dotnet ef migrations remove # lister les migration dotnet ef migrations list # il faut créer une migration avant de pouvoir passer à la génération # génération d'un script SQL pour voir ce que va faire la migration dotnet ef migrations script <FROM> <TO> # From: 0 par défaut # To: dernière migration par défaut # génération et exécution du script SQL dotnet ef database update |
Add data update during migration
After having added a migration, you can edit the Migrations/YYYYMMDDHHmmss_MigrationName.cs file:
Migrations/YYYYMMDDHHmmss_MigrationName.cs |
public partial class MigrationName : Migration { protected override void Up(MigrationBuilder migrationBuilder) { /* ... */ // update data migrationBuilder.UpdateData( table: "MyTable", keyColumn: "Id", keyValue: 1, column: "Column1", value: 1); // execute SQL command migrationBuilder.Sql("UPDATE `MyTable` SET `Column1` = `Column2`;"); } |
Apply migrations at runtime
ASP.Net web API
Startup.cs |
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyDbContext context) { // ensures your existing migrations are applied to the database, create it if needed context.Database.Migrate(); // create the db if it doesn't exist context.Database.EnsureCreated(); } |
EnsureCreated totally bypasses migrations and just creates the schema for you, you can't mix this with migrations. EnsureCreated is designed for testing or rapid prototyping where you are ok with dropping and re-creating the database each time. |
Data Seeding
Permet de remplir la bdd avec un jeu initial de données.
Data/MyAppContext.cs |
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Item>().HasData( new Item() { Name = "Item1" }, new Item() { Name = "Item2" }); } |
OLD
Data/MyAppSeeder.cs |
public class MyAppSeeder { private readonly MyAppContext _context; public MyAppSeeder(MyAppContext context) { _context = context; } public void Seed() { _context.Database.EnsureCreated(); if (!_context.Items.Any()) { _context.Items.Add(new Item() { Name = "Item 1" }); // ne pas définir l'Id _context.Items.Add(new Item() { Name = "Item 2" }); _context.SaveChanges(); } |
Sartup.cs |
public void ConfigureServices(IServiceCollection services) { services.AddTransient<MyAppSeeder>(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { using (var scope = app.ApplicationServices.CreateScope()) { var seeder = scope.ServiceProvider.GetService<MyAppSeeder>(); seeder.Seed(); } } |
Database first: scaffold database to model entities
# generate entity classes and context class dotnet ef dbcontext scaffold "Server=localhost;Database=MyDb;User=sa;Password=pwd;" Microsoft.EntityFrameworkCore.SqlServer --output-dir Model # output folder for entities --context-dir DataAccess # output folder for DbContext --context "MyDbContext" # default context name: DbNameContext --force # overwrite all the class files --table table1 --table table2 # scaffold only table1 and table2 |
Utilisation dans le controller
MyController.cs |
private readonly MyAppContext _context; public MyController(MyAppContext context) { _context = context; } public IActionResult Index() { var model = _context.Items.ToList(); return View(model); } [HttpGet] [ProducesResponseType(200, Type = typeof(IEnumerable<Item>))] public IActionResult Get() { return Ok(_context.Items); } |
Repository Pattern
- 1 Repository par classe: ItemsRepository
- 1 DbContext pour toute l'application
Data/ItemsRepository.cs |
public class ItemsRepository : IItemsRepository { private readonly MyAppContext context; public ItemsRepository(MyAppContext context) { this.context = context; } public async Task<IReadOnlyList<Item>> GetAllAsync() { return await context.Set<Item>() .Include(x => x.SubClass) .ToListAsync(); } public async Task<Item> GetByIdAsync(int id) { var item = await context.FindAsync<Item>(id); // use Include with Find this.context.Entry(item).Reference(x => x.User).Load(); return item; } public async Task<Item> CreateAsync(Item item) { var createdItemEntry = await context.AddAsync(item); await context.SaveChangesAsync(); return createdItemEntry.Entity; } public async Task UpdateAsync(Item itemToUpdate, Item item) { itemToUpdate.Name = item.Name; await context.SaveChangesAsync(); } public async Task DeleteAsync(Item item) { context.Remove(item); await context.SaveChangesAsync(); } } |
La création d'une interface permettra d'utiliser d'autres sources de données pour faire des tests.
Data\IItemsRepository.cs |
public interface IItemsRepository { Task<IReadOnlyList<Item>> GetAllAsync(); Task<Item> GetByIdAsync(int id); Task<Item> CreateAsync(Item item); Task UpdateAsync(Item itemToUpdate, Item item); Task DeleteAsync(Item item); } |
Startup.cs |
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IItemsRepository, ItemsRepository>(); |
N+1 selects problem
EF fait ses requêtes en lazy loading, ce qui veut dire que les requêtes SQL ne sont exécutées que lorsqu'on a besoin des données.
// 1 requête pour récupérer tous les contacts IEnumerable<Contact> allContacts = context.Contacts; foreach (var contact in allContacts) { // pour récupérer l'age de chaque contact il faut faire une nouvelle requête pour chaque contact // ce qui donne de mauvaises performances if (contact.age > 30) { /* ... */ } } // inclure Age lors de la récupération des contacts // ainsi tous se fait en une seule requête IEnumerable<Contact> allContacts = context.Contacts.Include(c => c.Age); |
Executing Raw SQL Queries
using System.Data.SqlClient; // pour SqlParameter var books = context.Books.FromSql("SELECT Id, Title FROM Books").ToList(); // passage de paramètre avec l'interpolation de string var book = context.Books.FromSql($"SELECT Id, Title FROM Books WHERE Title = {title}").FirstOrDefault(); // passage de paramètre avec DbParameter var p1 = new SqlParameter("@Id", id); var book = db.Books.FromSql("SELECT * FROM Books WHERE Id = @Id", p1).FirstOrDefault(); // opérations INSERT, UPDATE, DELETE var commandText = "INSERT Authors (AuthorName) VALUES (@AuthorName)"; var name = new SqlParameter("@AuthorName", "Jean-Christophe Grangé"); context.Database.ExecuteSqlCommand(commandText, name); // procédure stockée var authorId = new SqlParameter("@AuthorId", 1); var books = context.Books.FromSql("EXEC GetBooksByAuthor @AuthorId" , authorId).ToList(); // procédure stockée avec plusieurs paramètres var parameters = new List<SqlParameter>(); parameters.Add(new SqlParameter(name, SqlDbType.Int) { Value = (object)value?.ToInt32(CultureInfo.InvariantCulture) ?? DBNull.Value }); var parameterNamesList = string.Join(", ", parameters.Select(p => p.ParameterName)); var storedProcedureResultRows = await context.Database.SqlQuery<StoredProcedureResultRow>( $"{StoredProcedureName} {parameterNamesList}", parameters.Cast<object>().ToArray()).ToListAsync(cancellationToken); |
Examples
One to one relationship
One Address per User.
public class Address { public int Id { get; set; } [Required, StringLength(40)] public string City { get; set; } public User User { get; set; } } public class User { public int Id { get; set; } [Required, StringLength(40)] public string Name { get; set; } public Address Address { get; set; } public int AddressId { get; set; } } public class MyAppContext : DbContext { protected override void OnModelCreating(ModelBuilder modelBuilder) { // allow to have both User.Address and Address.User modelBuilder.Entity<Address>() .HasOne(x => x.User) .WithOne(user => user.Address) .HasForeignKey<User>(x => x.AddressId); |
CREATE TABLE [Addresses] ( [Id] int NOT NULL IDENTITY, [City] nvarchar(40) NOT NULL, CONSTRAINT [PK_Addresses] PRIMARY KEY ([Id]) ); CREATE TABLE [Users] ( [Id] int NOT NULL IDENTITY, [Name] nvarchar(40) NOT NULL, [AddressId] int NOT NULL, CONSTRAINT [PK_Users] PRIMARY KEY ([Id]), CONSTRAINT [FK_Users_Addresses_AddressId] FOREIGN KEY ([AddressId]) REFERENCES [Addresses] ([Id]) ON DELETE CASCADE ); CREATE UNIQUE INDEX [IX_Users_AddressId] ON [Users] ([AddressId]); |
One to many relationship
Many Task per User.
public class User { public int Id { get; set; } [Required, StringLength(40)] public string Name { get; set; } public Task[] Tasks { get; set; } // can be remove without changing the generated sql if Task.User exists } public class Task { public int Id { get; set; } [Required, StringLength(40)] public string Name { get; set; } [Required] public User User { get; set; } // can be remove without changing the generated sql if User.Tasks exists } public class MyAppContext : DbContext { protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>() .HasMany(x => x.Tasks) .WithOne(task => task.User); |
CREATE TABLE [Users] ( [Id] int NOT NULL IDENTITY, [Name] nvarchar(40) NOT NULL, CONSTRAINT [PK_Users] PRIMARY KEY ([Id]) ); CREATE TABLE [Task] ( [Id] int NOT NULL IDENTITY, [Name] nvarchar(40) NOT NULL, [UserId] int NOT NULL, CONSTRAINT [PK_Task] PRIMARY KEY ([Id]), CONSTRAINT [FK_Task_Users_UserId] FOREIGN KEY ([UserId]) REFERENCES [Users] ([Id]) ON DELETE CASCADE ); CREATE INDEX [IX_Task_UserId] ON [Task] ([UserId]); |
Many to many relationship
Many User per Group and many Group per User.
public class Group { public int Id { get; set; } [Required, StringLength(40)] public string Name { get; set; } public UserGroup[] UserGroups { get; set; } } public class User { public int Id { get; set; } [Required, StringLength(40)] public string Name { get; set; } [Required] public Address Address { get; set; } public Task[] Tasks { get; set; } public UserGroup[] UserGroups { get; set; } } public class UserGroup { public int UserId { get; set; } public User User { get; set; } public int GroupId { get; set; } public Group Group { get; set; } } public class MyAppContext : DbContext { protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<UserGroup>() .HasKey(x => new { x.UserId, x.GroupId }); modelBuilder.Entity<UserGroup>() .HasOne(x => x.User) .WithMany(user => user.UserGroups) .HasForeignKey(x => x.UserId); modelBuilder.Entity<UserGroup>() .HasOne(x => x.Group) .WithMany(group => group.UserGroups) .HasForeignKey(x => x.GroupId); } |
Erreurs
Cannot authenticate using Kerberos
Erreur sur Linux durant l'exécution de dotnet ef database update. Impossible de se connecter au serveur SQL.
Passer Trusted_Connection à False et ajouter le user et password à la connection string corrige le problème.
appsettings.json |
"ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=MyDb;Trusted_Connection=False;MultipleActiveResultSets=true;User=sa;Password=xxx" }, |
Specified key was too long; max key length is 767 bytes
Les index InnoDB (MySQL) ont une taille limitée. Il faut donc limité la taille des index à la création de la bdd.
Data/MyAppContext.cs |
protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Shorten key length for Identity builder.Entity<IdentityRole>(entity => { entity.Property(m => m.NormalizedName).HasMaxLength(127); }); builder.Entity<IdentityUser>(entity => { entity.Property(m => m.NormalizedUserName).HasMaxLength(127); }); } |
The host localhost does not support SSL connections
Ajouter SslMode=none à la ConnectionString MySQL.
OLD - No executable found matching command "dotnet-ef"
Installer les packages Nuget:
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.EntityFrameworkCore.Tools.DotNet
MyProject.csproj |
<Project Sdk="Microsoft.NET.Sdk"> <!-- Ajouter le groupe suivant --> <ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" /> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> </ItemGroup> |
dotnet restore |