Gå til indholdet

Intro til EF Core

Entity Framework Core (EF Core) er en open source og platformuafhængig version af Entity Framework, et populært Object-Relational Mapping (ORM) framework udviklet af Microsoft.

Entity Framework blev først introduceret i 2008 som en del af .NET Framework 3.5 Service Pack 1. Det blev designet til at gøre det nemmere for .NET udviklere at arbejde med relationelle databaser ved at abstrahere databaselaget og tillade udviklere at arbejde med .NET objekter i stedet for SQL queries. Over tid blev Entity Framework videreudviklet og forbedret, hvilket førte til flere versioner med forskellige nye funktioner og forbedringer.

Information til undervisere

Kursisterne bør kende til EF Core, da det er et populært og udbredt ORM framework til .NET. Det er vigtigt at de har en forståelse for hvad et ORM framework er, og en grundlæggende forståelse for, hvordan de fungerer. Det vil dog være en stor fordel at kende til databaser og SQL, da EF Core er designet til at arbejde med relationelle databaser, og det er vigtigt at forstå, hvordan EF Core oversætter .NET objekter til SQL queries og omvendt. Brug gerne eksemplet (person.db) til at vise EF Core i praksis, og lad kursisterne prøve at skrive deres egne queries og opdateringer. Lidt mere omfattende vil være opgaven omkring Northwind databasen - se nederst.

Det blev dog hurtigt klart, at det originale Entity Framework ikke ville kunne understøtte mange nye ønskede features. Derfor blev Entity Framework Core udviklet som et nyt ORM framework, der er designet fra bunden med henblik på at være lettere, mere fleksibelt og at kunne understøtte nye funktioner, såsom ikke-relationelle databaser, in-memory databaser til test, og mere.

EF Core har bevaret mange af de populære funktioner fra det originale Entity Framework, men med mange forbedringer og nye funktioner. Det understøtter LINQ queries, ændringssporing, opdateringer, migrationer og meget mere. Men det er vigtigt at bemærke, at på grund af dets redesign er EF Core ikke bare en version af Entity Framework, der er “portet” til .NET Core. Det er et helt nyt framework, og mens det har mange ligheder med det originale Entity Framework, er der også mange forskelle.

Overordnet om EF Core

Entity Framework Core (EF Core) er en moderne, alsidig Object-Relational Mapper (ORM) til .NET. Det gør det muligt for udviklere at arbejde med databaser ved hjælp af .NET-objekter, hvilket gør det nemmere at skrive databasedrevet software.

Når du arbejder med EF Core, starter du typisk med at definere dine dataklasser, også kendt som dine “entiteter”. Disse klasser repræsenterer tabellerne i din database og indeholder egenskaber, der repræsenterer kolonnerne i disse tabeller.

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Header { get; set; }
    public string Text { get; set; }
    public DateTime Date { get; set; }
}

Efter at have defineret dine entiteter, skal du oprette en DbContext klasse, som repræsenterer en session med databasen, hvilket giver dig mulighed for at udføre CRUD-operationer (Create, Read, Update, Delete).

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

For at konfigurere din forbindelse til databasen (her SQL server), skal du overskrive OnConfiguring metoden i din DbContext klasse og angive forbindelsesstrengen.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlServer(
        @"Server=(localdb)\mssqllocaldb;Database=Blogging;Integrated Security=True");
}

Når du har din DbContext opsat, kan du begynde at bruge EF Core til at interagere med din database. Du kan oprette nye instanser af dine entiteter, tilføje dem til din DbContext og kalde SaveChanges metoden for at persistere disse ændringer til databasen.

Uderstøttelse af LINQ

EF Core understøtter også LINQ (Language Integrated Query), hvilket gør det nemt at skrive komplekse data queries i C#. Du kan bruge LINQ til at hente, filtrere, sortere og manipulere dine data på forskellige måder.

BloggingContext c = new BloggingContext();
var res = c.Posts.Where(i=> i.Date < DateTime.Now.AddDays(-14)).OrderBy(i => i.Date).ToList();

Transaktioner

Transaktioner er vigtige i EF Core, fordi de sikrer, at flere databaseoperationer enten gennemføres som en samlet enhed eller slet ikke gennemføres. Dette garanterer dataintegriteten, især ved fejl eller uventede afbrydelser.

Et klassisk eksempel er en pengeoverførsel mellem to konti: Enten opdateres begge konti korrekt, eller også skal ingen af dem opdateres, så der ikke opstår ubalance i regnskabet.

I EF Core håndteres transaktioner typisk således:

Simpelt eksempel:

using var transaction = context.Database.BeginTransaction();
try
{
    // Operation 1
    kontoA.Saldo -= 500;
    context.SaveChanges();

    // Operation 2
    kontoB.Saldo += 500;
    context.SaveChanges();

    transaction.Commit(); // Begge operationer lykkedes
}
catch
{
    transaction.Rollback(); // Der opstod en fejl – alle ændringer annulleres
}

Her sikrer transaktionen, at hvis én af operationerne fejler, vil alle ændringer blive rullet tilbage, og databasen forbliver konsistent. Uden transaktioner risikerer man, at data kommer i en inkonsistent eller fejlbehæftet tilstand.

Asynkron EF Core

EF Core understøtter asynkrone operationer, som hjælper med at forbedre applikationens skalerbarhed og responsivitet. Ved at bruge asynkron programmering undgår applikationen at vente (“blokere”) på databasekald, hvilket frigør ressourcer til andre opgaver.

I praksis tilføjer du blot await og bruger EF Cores asynkrone metoder med suffixet Async.

Eksempel på asynkron forespørgsel:

// Asynkron hentning af data
var blogs = await context.Blogs
    .Where(b => b.Posts.Count > 10)
    .ToListAsync();

Eksempel på asynkron gemning af data:

// Asynkron gemning af ændringer
await context.SaveChangesAsync();

Brug af asynkrone operationer er især vigtigt i webapplikationer og API’er, hvor det forbedrer ydeevnen ved at tillade flere samtidige anmodninger uden at øge antallet af nødvendige tråde markant.

Fejlhåndtering

Fejlhåndtering er afgørende i EF Core for at sikre stabil og pålidelig dataadgang. Når der udføres databaseoperationer, kan der opstå undtagelser som DbUpdateConcurrencyException ved samtidighedsproblemer eller DbUpdateException ved databasebegrænsninger. Effektiv fejlhåndtering sikrer, at applikationen reagerer hensigtsmæssigt på disse situationer og fortsat er robust.

Transaktioner

Ved at anvende transaktioner kan flere databaseoperationer grupperes og behandles som én samlet enhed. Transaktioner sikrer, at enten gennemføres alle operationer, eller også gennemføres ingen, hvilket opretholder dataintegritet og beskytter mod inkonsistente data.

Samtidighedskontrol

EF Core benytter optimistisk samtidighedskontrol til håndtering af konflikter ved samtidige dataændringer. Gennem et samtidighedstoken kan EF Core opdage eventuelle konflikter og reagere med en DbUpdateConcurrencyException, som skal håndteres passende i applikationen.

Validering af data

EF Core validerer ikke automatisk data, før de gemmes. Derfor bør du implementere manuel validering af dine entiteter for at sikre, at dataene opfylder nødvendige forretningsregler og begrænsninger. Typisk sker dette ved hjælp af dataannotations eller ved at implementere interfacet IValidatableObject.

Brug af hjælpebiblioteker

Der findes bibliotafeker som f.eks. EntityFrameworkCore.Exceptions, der gør fejlhåndteringen enklere og mere effektiv. Disse biblioteker oversætter generelle databasefejl til mere præcise og specifikke undtagelser, såsom fejl ved unikhedskrav, ugyldige fremmednøgler eller NULL-værdier. Ved at bruge sådanne biblioteker bliver det nemmere at identificere årsagen til fejl og give brugeren mere informative og meningsfulde fejlmeddelelser.

En anden fordel ved hjælpebibliotekerne er, at de gør fejlhåndteringen konsistent på tværs af databaser, hvilket betyder, at applikationen bliver lettere at vedligeholde, især hvis den understøtter flere forskellige databasetyper.

Ved at implementere disse strategier kan du øge robustheden og pålideligheden af dine EF Core-baserede applikationer væsentligt.

Migreringer

EF Core understøtter migrationer, som er en vigtig funktion til at administrere og vedligeholde ændringer i databasen over tid. Når din applikation udvikler sig, ændres datamodellerne ofte, og migrationer sikrer, at databasen følger med disse ændringer.

Migrationer har typisk to fremgangsmåder (modes):

  • Code-first:
    Her definerer du først dine C#-modeller (entiteter) i koden og bruger EF Core til automatisk at generere database-skemaet. Ændringer i dine klasser udløser nye migrationer, som opdaterer databasen.

  • Database-first (Reverse Engineering):
    Hvis databasen allerede eksisterer, kan EF Core oprette modellerne ud fra det eksisterende databaseskema. Dette gøres med værktøjer, som genererer kode ud fra tabellerne i databasen.

Hvor bruges migreringer?

  • Udviklingsmiljø:
    Hurtigt oprette og ændre databaser i takt med udviklingen.

  • Testmiljø:
    Automatiseret opsætning af databasen med korrekt skema og seed-data til testformål.

  • Produktionsmiljø:
    Kontrolleret udrulning af ændringer til databasen, så risikoen for fejl minimeres.

Eksempel på migreringsproces (code-first):

Når du ændrer en entitet (tilføjer eller fjerner en egenskaf), genererer du en migration:

dotnet ef migrations add AddBlogRating
dotnet ef database update

Eksempel på Reverse Engineering (database-first):

Hvis du har en eksisterende database og ønsker at generere kode:

dotnet ef dbcontext scaffold "Server=.;Database=MyDB;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer

Hvorfor er migreringer vigtige?

Migreringer sikrer, at databaseændringer håndteres systematisk, hvilket gør databasen robust og pålidelig gennem applikationens livscyklus. Det giver også tydelig sporbarhed, nem fejlfinding, og hjælper teams med at undgå fejl ved manuelle ændringer.

I en database-first tilgang (reverse engineering) opretter du modeller fra en eksisterende database, men du kan ikke bruge migrationer til efterfølgende at ændre denne database. Ændringer skal i så fald foretages direkte i databasen eller ved manuelt at tilpasse modellerne.

Info

Migrationer understøttes kun ved brug af code-first-tilgangen. Ved brug af database-first (reverse engineering) kan EF Core generere modeller ud fra en eksisterende database, men efterfølgende databaseændringer skal foretages direkte i databasen eller modellerne skal genskabes manuelt med ny scaffolding.

Prøv det selv

Den bedste måde at bliver introduceret til EF er ved at prøve det. Følgende applikation er en simpel konsol applikation, der bruger EF Core til at interagere med en SQLite database. Du kan bruge denne applikation som en skabelon til at eksperimentere med EF Core og udforske dets funktioner.

  • Skab en tom konsol applikation med top-level statements
  • Tilføj NuGet pakken Microsoft.EntityFrameworkCore.Sqlite og EntityFrameworkCore.Exceptions.Sqlite
  • Tilføj NuGet pakken Dumpify og Spectre.Console (ikke relateret til EF)
    • Bruges til at skrive “nemt” ud på konsol samt skabe menu mv.
  • Download min eksempel Sqlite database og gem den i c:\temp
  • Læs om eksempel databasen eller kig selv med SQLite Browser

Her er et kort script (Poweshell) til at oprette projektet og tilføje de nødvendige NuGet pakker mv:

dotnet new console -n EFCoreDemo
cd EFCoreDemo
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package EntityFrameworkCore.Exceptions.Sqlite
dotnet add package Dumpify
dotnet add package Spectre.Console
Invoke-WebRequest -Uri "https://mcronberg.github.io/bogenomcsharp/downloads/people.db" -OutFile "C:\temp\people.db"
  • Tilføj følgende kode til Program.cs og prøv så at køre programmet.
using Dumpify;
using Spectre.Console;
using Microsoft.EntityFrameworkCore;
using EntityFramework.Exceptions.Common;
using Microsoft.Extensions.Logging;

SQLitePCL.Batteries.Init(); // Initialiser SQLite

string? menu = "";
int menuIndex = -1;

while (menu != "Afslut")
{
    Console.Clear();

    var choices = new[] {
        "Hent personer",
        "Find alle over 180 sorteret efter efternavn",
        "Find person (med land) udfra id",
        "Ret person med id = 1",
        "Opret ny person",
        "Slet person",
        "Fejlhåndtering", 
        "Benyt transaktion",
        "Hent personer asynkront",
        "Find land med personer",
        "Afslut"
    };

    menu = AnsiConsole.Prompt(
        new SelectionPrompt<string>()
            .Title("EF DEMO")
            .PageSize(20)
            .AddChoices(choices)
    );

    menuIndex = Array.IndexOf(choices, menu);
    switch (menuIndex)
    {
        case 0:
            using (PeopleContext context = new PeopleContext())
            {
                int antal = AnsiConsole.Ask<int>("Hvor mange personer skal hentes: ", 5);
                var persons = context.People.Take(antal).ToList();
                persons.Dump();
                break;
            }
        case 1:
            using (PeopleContext context = new PeopleContext())
            {
                var persons = context.People.Where(i => i.Height > 180).OrderBy(i => i.LastName).ToList();
                persons.Dump();
                break;
            }
        case 2:
            using (PeopleContext context = new PeopleContext())
            {
                int id = AnsiConsole.Ask<int>("Indtast id: ");
                var person = context.People.Include(i => i.Country).FirstOrDefault(i => i.PersonId == id);
                person.Dump();
                break;
            }
        case 3:
            using (PeopleContext context = new PeopleContext())
            {
                var person = context.People.FirstOrDefault(i => i.PersonId == 1);
                if (person != null)
                {
                    person.FirstName = AnsiConsole.Ask<string>("Fornavn: ");
                    person.LastName = AnsiConsole.Ask<string>("Efternavn: ");
                    context.SaveChanges();
                }
                AnsiConsole.WriteLine("Person rettet");
                break;
            }
        case 4:
            using (PeopleContext context = new PeopleContext())
            {
                var countries = context.Countries.ToList();
                var selectedCountry = AnsiConsole.Prompt(
                    new SelectionPrompt<dynamic>()
                        .Title("Vælg et land")
                        .PageSize(20)
                        .AddChoices(countries)
                        .UseConverter(p => p.Name)
                );
                var person = new Person
                {
                    FirstName = AnsiConsole.Ask<string>("Fornavn: ", "Anders"),
                    LastName = AnsiConsole.Ask<string>("Efternavn: ", "And"),
                    DateOfBirth = new DateTime(2000, 1, 1),
                    Gender = GenderType.Male,
                    IsHealthy = true,
                    CountryId = selectedCountry.CountryId,
                };
                context.People.Add(person);
                context.SaveChanges();
                AnsiConsole.WriteLine("Person oprettet med id " + person.PersonId);
                break;
            }
        case 5:
            using (PeopleContext context = new PeopleContext())
            {
                var id = AnsiConsole.Ask<int>("Indtast id: ");
                var person = context.People.FirstOrDefault(i => i.PersonId == id);
                if (person != null)
                {
                    context.People.Remove(person);
                    context.SaveChanges();
                    AnsiConsole.WriteLine("Person slettet");
                }
                else
                {
                    AnsiConsole.WriteLine("Person ikke fundet");
                }
                break;
            }
        case 6: // Nyt menupunkt: Exception bibliotek eksempel
            using (PeopleContext context = new PeopleContext())
            {
                AnsiConsole.WriteLine("Fejlhåndtering - forsøger at tilføje en person med et eksisterende ID");
                try
                {
                    // Forsøg at tilføje en person med et fast ID, som antages allerede at eksistere (f.eks. ID = 1)
                    var duplicatePerson = new Person
                    {
                        PersonId = 1,  // Forudsætning: Der findes allerede en person med ID = 1
                        FirstName = "Test",
                        LastName = "test",
                        DateOfBirth = DateTime.Now,
                        Gender = GenderType.Male,
                        IsHealthy = true,
                        CountryId = 1
                    };
                    context.People.Add(duplicatePerson);
                    context.SaveChanges();
                }
                catch (UniqueConstraintException ex)
                {
                    AnsiConsole.WriteLine("UniqueConstraintException fanget: " + ex.Message);
                }
                catch (DbUpdateException ex)
                {
                    AnsiConsole.WriteLine("DbUpdateException fanget: " + ex.Message);
                }
                break;
            }
        case 7:
            using (PeopleContext context = new PeopleContext())
            {
                Console.WriteLine("Benyt transaktion");
                using (var transaction = context.Database.BeginTransaction())
                {
                    try
                    {
                        var p = new Person
                        {
                            LastName = AnsiConsole.Ask<string>("Efternavn: "),
                            CountryId = 1,
                        };
                        context.People.Add(p);
                        context.SaveChanges();
                        p.FirstName = AnsiConsole.Ask<string>("Fornavn: ");
                        context.SaveChanges();
                        bool fejl = AnsiConsole.Ask<bool>("Smid en fejl?: ", true);
                        if (!fejl)
                            transaction.Commit();
                        else
                            throw new Exception("Fejl");
                    }
                    catch (Exception ex)
                    {
                        transaction.Rollback();
                        AnsiConsole.WriteLine("Fejl - transaktion rullet tilbage " + ex.Message);
                    }
                }
                break;
            }
        case 8:
            await using (PeopleContext context = new PeopleContext())
            {
                int antal = AnsiConsole.Ask<int>("Hvor mange personer skal hentes: ", 5);
                var persons = await context.People.Take(antal).ToListAsync();
                persons.Dump();
                break;
            }
        case 9:
            using (PeopleContext context = new PeopleContext())
            {
                var countries = context.Countries.ToList();
                var selectedCountry = AnsiConsole.Prompt(
                    new SelectionPrompt<dynamic>()
                        .Title("Vælg et land")
                        .PageSize(20)
                        .AddChoices(countries)
                        .UseConverter(p => p.Name)
                );
                int id = selectedCountry.CountryId;
                var countriesPeople = context.Countries.Include(i => i.People).Where(i => i.CountryId == id).ToList();
                countriesPeople.Dump();
                break;
            }
    }
    if (menu != "Afslut" && !AnsiConsole.Confirm("\r\nFortsæt?", true))
        break;
}

public enum GenderType
{
    Male,
    Female
}

public class Person
{
    public int PersonId { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public bool IsHealthy { get; set; }
    public GenderType Gender { get; set; }
    public int Height { get; set; }
    public int CountryId { get; set; }
    public Country? Country { get; set; }
}

public class Country
{
    public int CountryId { get; set; }
    public string? Name { get; set; }
    public List<Person>? People { get; set; }
}

public class PeopleContext : DbContext
{
    private readonly string pathToDb;

    public DbSet<Person> People { get; set; }
    public DbSet<Country> Countries { get; set; }

    public PeopleContext(string pathToDb = @"c:\temp\people.db")
    {
        this.pathToDb = pathToDb;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=" + pathToDb);
        optionsBuilder.LogTo(
            log => System.IO.File.AppendAllText(@"c:\temp\efdemo.log", log),
            new[] { DbLoggerCategory.Database.Command.Name },
            LogLevel.Information
        );
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>(e =>
        {
            e.ToTable("Person");
            // Binder enum til en int i databasen
            e.Property(i => i.Gender).HasConversion<int>();
            // Hvis man ønsker at have en liste af personer fra Country
            e.HasOne(p => p.Country).WithMany(b => b.People).HasForeignKey(p => p.CountryId);
        });

        modelBuilder.Entity<Country>(e =>
        {
            e.ToTable("Country");
        });

        base.OnModelCreating(modelBuilder);
    }
}

Migrations

Hvis du har lyst til at prøve migrations skal du først installere EF Core tools:

dotnet tool install --global dotnet-ef

Og derefter tilføje migrations pakken:

dotnet add package Microsoft.EntityFrameworkCore.Tools

Herefter kan du skabe grundlaget til migrations:

dotnet ef migrations add InitialCreate

Det skaber en migrations fil i Migrations mappen, men på CLI bliver du (i nuværende version) nødt til at slette koden i Up og Down metoderne så filen ser sådan ud:

using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EFCoreDemo.Migrations
{
    /// <inheritdoc />
    public partial class InitialBaseline : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {

        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {

        }
    }
}

Herefter kan du køre en migration:

dotnet ef database update

Den vil så skabe et __EFMigrationsHistory tabel i databasen, som holder styr på hvilke migrationer der er kørt.

Nu kan du så ændre i din Person klasse og eksempelvis tilføje en EMail property:

public string? EMail { get; set; }

Og så køre en ny migration:

dotnet ef migrations add AddEmail

Og så opdatere databasen:

dotnet ef database update

Check databasen og se at der er tilføjet en EMail kolonne, og bemærk filerne i Migrations mappen.

Opgaver