Dapper – cheat sheet

Micro-ORM: SQL selv, objekter automatisk · SQLite / C# · async hele vejen

1. Før Dapper — manuelt med reader

var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT DrinkId, Name, Price FROM Drink;";

var drinks = new List<Drink>();
await using (var reader = await cmd.ExecuteReaderAsync())
{
    while (await reader.ReadAsync())
    {
        drinks.Add(new Drink {
            DrinkId = reader.GetInt32(0),
            Name = reader.GetString(1),
            Price = reader.IsDBNull(2)
                ? null : reader.GetDouble(2)
        });
    }
}

Sådan så læsning ud før Dapper: hver kolonne hentes i hånden — indeks, datatype og NULL skal selv håndteres. Sammenlign med afsnit 5: Dapper gør præcis dette arbejde for dig, automatisk.

2. Hvad er en ORM?

ORM = Object-Relational Mapping: et værktøj, der oversætter mellem to verdener — databasens tabeller/rækker og programmets objekter. Uden en ORM står du selv for oversættelsen, som i afsnit 1.

SQL → objrække i tabel → objekt med properties obj → SQLobjekt → parametre i INSERT/UPDATE Full ORMf.eks. EF Core — skriver næsten al SQL for dig Micro-ORMf.eks. Dapper — du skriver SQL, den mapper objekter

3. Hvad er Dapper?

Dapper er et micro-ORM: du skriver selv SQL'en — Dapper oversætter resultat-rækkerne til C#-objekter (og parametre den anden vej). Du slipper altså for al den manuelle SqliteDataReader-læsning.

CreateExecuteAsync — INSERT ReadQueryAsync / QuerySingleAsync — SELECT UpdateExecuteAsync — UPDATE DeleteExecuteAsync — DELETE

4. Installation og forbindelse

dotnet add package Dapper
dotnet add package Microsoft.Data.Sqlite

To pakker, hver sin rolle: Dapper er selve micro-ORM'et — det tilføjer Query/Execute-metoderne. Microsoft.Data.Sqlite er driveren: ADO.NET-adgangen til SQLite-filen. Dapper kender selv ingen database — den bygger oven på en driver som denne.

var cs = new SqliteConnectionStringBuilder {
    // connection-streng = "adressen" på databasen: 
    // navn=værdi;navn=værdi: 
    DataSource = "db.sqlite",
    // "db.sqlite" er databasens FIL — oprettes,
    // hvis den ikke findes
    // PRAGMA foreign_keys = ON (pr. forbindelse!): 
    ForeignKeys = true
}.ToString();

// SqliteConnection definerer  forbindelsen, 
// leveret af driver-pakken: 
await using var conn = new SqliteConnection(cs);
// await using sørger for at forbindelsen lukkes automatisk
// (også ved fejl)

// Nu åbnes forbindelsen, og derefter kan der køres SQL: 
await conn.OpenAsync();

Connection-strengen fortæller hvor databasen er (her: en fil på disken) og hvordan der logges ind — på en server-database står host, bruger og adgangskode også dér.

5. Read — flere rækker

public class Drink {
    public int DrinkId { get; set; }
    public string Name { get; set; } = "";
    public double? Price { get; set; }   // NULL tilladt
}

var drinks = (await conn.QueryAsync<Drink>(
    "SELECT * FROM Drink ORDER BY Name;")).ToList();

QueryAsync giver 0–n rækker — en tom liste, hvis ingen matcher.
Brug .ToList(), hvis samlingen skal ændres efterfølgende.

6. Read — én række

var d = await conn.QuerySingleOrDefaultAsync<Drink>(
    "SELECT * FROM Drink WHERE DrinkId = @Id;",
    new { Id = 5 });

QuerySingleAsync kræver præcis én række og kaster ellers en exception; QuerySingleOrDefaultAsync giver null (for reference-typer), hvis ingen række matcher.

7. Execute — INSERT / UPDATE / DELETE

int n = await conn.ExecuteAsync(
    "UPDATE Drink SET Price = @Price WHERE DrinkId = @Id;",
    new { Price = 42.5, Id = 5 });

Returnerer antal berørte rækker — tjek n for at vide, om noget faktisk blev ændret (n == 0 → ingen match).

8. ExecuteScalar — én enkelt værdi

long antal = await conn.ExecuteScalarAsync<long>(
    "SELECT COUNT(*) FROM Drink;");

Bruges når resultatet er én skalarværdi: COUNT, MAX, SUM osv.

9. Parametre — altid @Param

// RIGTIGT — parametre er sikre mod SQL-injection
new { Id = id, Name = navn }

// FORKERT — aldrig interpolation/concat i SQL!
$"SELECT * FROM Drink WHERE DrinkId = {id}";
Vigtigst: brug altid @Navn-parametre + anonymt objekt — aldrig string-interpolation i SQL. Det er din beskyttelse mod SQL-injection. Bemærk: @ kan kun bruges til værdier, ikke kolonne-/tabelnavne.

10. Insert + hent auto-PK

const string sql = @"
INSERT INTO Drink (Name, Price) VALUES (@Name, @Price);
SELECT last_insert_rowid();";

long id = await conn.QuerySingleAsync<long>(
    sql, new { Name = "Mojito", Price = 55.0 });

11. Bulk insert — en hel liste

await conn.ExecuteAsync(
    "INSERT INTO Drink (Name) VALUES (@Name);",
    drinks.Select(d => new { d.Name }));

ExecuteAsync kører SQL'en én gang pr. element i listen.

12. Transaktion — alt eller intet

await using var tx = await conn.BeginTransactionAsync();
try {
    await conn.ExecuteAsync(sql1, args1, tx);
    await conn.ExecuteAsync(sql2, args2, tx);
    await tx.CommitAsync();      // gennemfør alt
} catch {
    await tx.RollbackAsync();    // fortryd det hele
    throw;
}

13. Mapping-regler

14. Metode-oversigt

Query<T>0–n rækker QuerySingle<T>præcis 1 — ellers kastes exception QuerySingleOrDefault<T>0 eller 1 (null) ExecuteINSERT/UPDATE/DELETE → antal rækker ExecuteScalarén skalarværdi (COUNT, MAX, …) QueryMultipleflere resultatsæt i ét kald

Alle findes i en Async-udgave (QueryAsync, …) — bland ikke sync og async i samme flow.

15. Faldgruber

  • Aldrig string-interpolation i SQL → SQL-injection (se afsnit 9).
  • SQLite: husk ForeignKeys = true pr. forbindelse.
  • await using på connection og transaktion.
  • Bland ikke sync og async i samme flow (Query vs. QueryAsync).
  • n == 0 fra ExecuteAsync betyder "intet skete" — tjek det, før du rapporterer succes.