Entity Framework Core

De Banane Atomic
Aller à la navigationAller à la recherche

Liens

Description

  • Réécriture complète d'EF
  • Plus de nécessite d'utilisé des BdD relationnelles uniquement

Ajouter les packages au projet

Bash.svg
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=myserver;Database=mydatabase;User Id=sa;Password=xxx;
Sqlite Microsoft.EntityFrameworkCore.Sqlite Data Source=file.db
MySQL Pomelo.EntityFrameworkCore.MySql
MySql.Data.EntityFrameworkCore
server=localhost;database=MyDb;user=user;password=password

Définition du Context et création des classes Entity

Data/MyAppContext.cs
public class MyAppContext : DbContext
{
    // permet de passer des options à la construction du DbCDbContextontext
    public MyAppContext(DbContextOptions<MyAppContext> options) : base(options)
    {}

    public DbSet<Item> Items { get; set; }

    // pour ASP.NET Core, préférer la configuration dans Startup.ConfigureServices
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(ConfigurationManager.ConnectionStrings["SqlServerConnectionString"].ConnectionString);
    }
}

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; }
}

Index

Data/MyContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Item>()
                .HasIndex(i => i.Name)
                .HasName("Index_Name");

Vue/view et QueryType

Data/MyAppContext.cs
public class MyAppContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<MyQueryType>().ToView("MyView");

Entity Framework Core Tools

Powershell.svg
# 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

Bash.svg
# 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

# 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>

CLI

Powershell.svg
# 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

# génération et exécution du script SQL
dotnet ef database update

# génération d'un script SQL
dotnet ef migrations script <FROM> <TO>
# From: 0 par défaut
# To: dernière migration par défaut

Configuration

Startup.cs
using Microsoft.EntityFrameworkCore;

public void ConfigureServices(IServiceCollection services)
{
    // SQL Server
    services.AddDbContext<AppContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("SqlServerConnectionString2"));
    });

    // In Memory
    services.AddDbContext<AppContext>(options =>
    {
        options.UseInMemoryDatabase("WebApplicationCoreMemoryDb");
    });
appsettings.json
{
  "ConnectionStrings": {
    "SqlServerConnectionString1": "Server=(localdb)\\MSSQLLocalDB;Database=MyDb;Integrated Security=True;MultipleActiveResultSets=True",
    "SqlServerConnectionString2": "Server=localhost;Database=Comptes;User Id=sa;Password=ppp;",
    "MySqlConnectionString": "Server=localhost;database=testdb;uid=uuu;pwd=ppp;",
    "SqliteConnectionString": "Data Source=testdb.db"
  }
}

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);
}

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();
    }
}

Repository Pattern

1 Repository par classe (Item), 1 DbContext pour toute l'application.
Repository peut aussi être nommé Service.

Data/ItemRepository.cs
public class ItemRepository : IItemRepository
{
    private readonly MyAppContext _context;

    public ItemRepository(MyAppContext context)
    {
        _context = context;
    }

    public IEnumerable<Item> GetAllItems()
    {
        return _context.Items;
    }

    public Item Get(int id)
    {
        return _context.Find<Item>(id);
    }

    public void Add(Item item)
    {
        _context.Add(item);
        _context.SaveChanges();
    }

    public void Remove(int id)
    {
        _context.Remove(GetItem(id));
        _context.SaveChanges();
    }

    public void Update(int id, Item item)
    {
        var itemToUpdate = GetItem(id);
        itemToUpdate.Name = item.Name;
        _context.SaveChanges();
    }

    public bool SaveAll()
    {
        return _context.SaveChanges() > 0;
    }
}

La création d'une interface permettra d'utiliser d'autres sources de données pour faire des tests.

Data\IItemRepository.cs
public interface IItemRepository
{
    IList<Item> GetAllItems();
    bool SaveAll();
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // Add database services.
    services.AddDbContext<MyAppContext>(options =>
        options.UseSqlServer("MyConnectionString")
    );

    // Add framework services.
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddScoped<IItemRepository, ItemRepository>();
MyController.cs
private readonly IItemRepository _itemRepository;

public MyController(IItemRepository itemRepository)
{
    _itemRepository = itemRepository;
}

public IActionResult Index()
{
    var model = _itemRepository.GetAllItems();
    return View(model);
}

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.

Cs.svg
// 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

Cs.svg
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);

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>
Bash.svg
dotnet restore