Mert Özen Her Satırda Daha İleri
Veritabanı Dapper — #01

Dapper Nedir? ADO.NET'in Üzerindeki İnce Katman ve İlk Sorgu

Mert Özen 6 Kas 2026 11 dk 3 görüntülenme
Dapper Nedir? ADO.NET'in Üzerindeki İnce Katman ve İlk Sorgu

Dapper'ı ADO.NET üzerine eklenen extension method'lar olarak tanıyoruz: micro-ORM ne demek, EF Core ile ne zaman hangisi, .NET 10 + Dapper 2.1.89 + SQLite kurulumu, ilk Query ve 1.x'ten Dapper.AOT'ye kısa tarihçe.

Sürüm notu: Bu yazıdaki kodlar Dapper 2.1 ve .NET 10 içindir.

Önce Tahmin Et

Aşağıdaki kodda bir connection oluşturuyoruz ama hiçbir yerde Open() çağırmıyoruz. Hemen ardından aynı connection üzerinden bir sorgu çalıştırıyoruz:

using var connection = new SqliteConnection(BookstoreDb.ConnectionString);

Console.WriteLine($"Before: {connection.State}");
var books = connection.Query<Book>("SELECT id, title, price FROM books ORDER BY price");
Console.WriteLine($"After: {connection.State}");

Üç ihtimal var. Birincisi, Query satırı "connection kapalı" diye bir exception fırlatır. İkincisi, sorgu çalışır ve son satır Open yazar. Üçüncüsü, sorgu çalışır ve son satır yine Closed yazar. Hangisi olduğunu birazdan ilk gerçek çıktımızda göreceğiz.

ADO.NET ile Bir Sorgu: Elle Yapılan İş

.NET'te bir veritabanıyla konuşmanın en alt katmanı ADO.NET'tir. Karşında SQL Server, PostgreSQL ya da SQLite olması fark etmez; her provider aynı soyutlamaları uygular: connection (DbConnection), command (DbCommand), parameter (DbParameter) ve sonuçları satır satır okuyan reader (DbDataReader). Fiyatı belli bir değerin altındaki kitapları düz ADO.NET ile okumak şöyle görünür:

static List<Book> GetCheapBooksAdoNet(decimal maxPrice)
{
    using var connection = new SqliteConnection(BookstoreDb.ConnectionString);
    connection.Open();

    using var command = connection.CreateCommand();
    command.CommandText = "SELECT id, title, price FROM books WHERE price < @MaxPrice";
    command.Parameters.AddWithValue("@MaxPrice", maxPrice);

    var books = new List<Book>();
    using var reader = command.ExecuteReader();
    while (reader.Read())
    {
        // Kolonları sırasıyla okuyup doğru tipe elle çeviriyoruz
        books.Add(new Book
        {
            Id = reader.GetInt32(0),
            Title = reader.GetString(1),
            Price = reader.GetDecimal(2)
        });
    }
    return books;
}

Yaptığımız işe bir bak: connection'ı aç, command oluştur, SQL'i ve parameter'ı ver, reader'ı al, her satırda kolonları sırayla oku, doğru tipe çevir, bir object'e yerleştir, listeye ekle. Bu satırların hepsi gerekli ama hiçbiri iş kuralı değil. SELECT'teki kolon sırasını değiştirdiğin anda GetString(1) başka bir kolonu okur; tabloya yeni bir kolon eklediğinde bu döngüyü elle güncellersin.

Dapper Ne Yapar?

Aynı sorgunun Dapper hâli:

static List<Book> GetCheapBooksDapper(decimal maxPrice)
{
    using var connection = new SqliteConnection(BookstoreDb.ConnectionString);
    return connection.Query<Book>(
        "SELECT id, title, price FROM books WHERE price < @MaxPrice",
        new { MaxPrice = maxPrice }).ToList();
}

İki method'u aynı programda 14m ile çağırdığımızda sonuç birebir aynı:

ADO.NET: A Wizard of Earthsea, Solaris, The Hitchhiker's Guide to the Galaxy, Exhalation
Dapper:  A Wizard of Earthsea, Solaris, The Hitchhiker's Guide to the Galaxy, Exhalation

Dapper, ADO.NET'in yerine geçen bir şey değil; onun üzerine eklenen bir avuç extension method. Query, Execute, QuerySingle gibi method'lar IDbConnection interface'ine eklenir, bu yüzden elindeki connection hangi provider'dan gelirse gelsin çalışır. Arka planda yine bir command oluşturulur, parameter'lar eklenir, reader açılır ve satırlar okunur. Tek fark, bu tekrar eden işi artık senin değil Dapper'ın yapması.

Satırları Book object'lerine nasıl çevirdiğine gelince: Dapper bir sorguyu ilk kez çalıştırdığında result set'in kolonlarına bakar, onları Book class'ının property'leriyle adlarına göre eşleştirir ve bu mapping için çalışma anında IL kodu üretir. Ürettiği küçük method'u SQL metni ve tiplerle birlikte önbelleğe koyar; aynı sorgunun sonraki çalışmaları doğrudan bu hazır kodu kullanır. Elle yazdığın while (reader.Read()) döngüsüne çok yakın bir hızın sırrı bu.

Dapper'a bu yüzden ORM değil, micro-ORM denir. Bir ORM'den beklediğin şeylerin çoğu burada yok. Change tracking yok; bir object'in property'sini değiştirmen veritabanında hiçbir şeyi değiştirmez. SaveChanges yok, LINQ'i SQL'e çeviren bir katman yok, migration yok, ilişkili veriyi kendiliğinden yükleyen navigation property'ler yok. Dapper iki işi iyi yapar: senin yazdığın SQL'i parameter'larıyla birlikte güvenli şekilde çalıştırır ve sonucu object'lere eşler (mapping).

Bir benzetmeyle anlatayım. ADO.NET bir alet çantasıdır, her vidayı kendin sıkarsın. Dapper o çantaya bir şarjlı tornavida ekler; vidanın nereye gireceğine, yani SQL'e yine sen karar verirsin. EF Core gibi bir ORM ise binayı senin yerine projeden kuran bir müteahhittir: ne istediğini anlatırsın, SQL'i o üretir.

Kurulum: .NET 10, Dapper 2.1.89 ve SQLite

Seri boyunca kurulum gerektirmeyen bir veritabanı kullanacağız: SQLite. Ayrı bir sunucu yok, veritabanı proje klasöründeki tek bir dosya. Provider olarak Microsoft'un Microsoft.Data.Sqlite paketini kullanıyoruz. Terminalde:

dotnet new console -n Bookstore -f net10.0
cd Bookstore
dotnet add package Dapper --version 2.1.89
dotnet add package Microsoft.Data.Sqlite --version 10.0.14

csproj dosyan şuna benzemeli:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Dapper" Version="2.1.89" />
    <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.14" />
  </ItemGroup>

</Project>

Dapper paketi hiçbir veritabanı provider'ı getirmez; hangi veritabanıyla konuşacaksan onun ADO.NET paketini ayrıca eklersin. SQL Server için Microsoft.Data.SqlClient, PostgreSQL için Npgsql, bizim örneğimizde de Microsoft.Data.Sqlite. Dapper 2.1.89'un NuGet paketinde net10.0 için ayrı bir hedef var, yani .NET 10 projesinde doğrudan o derleme kullanılıyor.

Örnek alanımız küçük bir kitapçı: yazarlar (authors), kitaplar (books), siparişler (orders) ve sipariş kalemleri (order_items). Şemayı ve örnek veriyi kuran yardımcı class'ı projeye BookstoreDb.cs adıyla ekle:

using Dapper;
using Microsoft.Data.Sqlite;

public static class BookstoreDb
{
    public const string ConnectionString = "Data Source=bookstore.db";

    // Her çalıştırmada tabloları silip aynı örnek veriyle baştan kurar
    public static void Reset()
    {
        using var connection = new SqliteConnection(ConnectionString);
        connection.Execute("""
            DROP TABLE IF EXISTS order_items;
            DROP TABLE IF EXISTS orders;
            DROP TABLE IF EXISTS books;
            DROP TABLE IF EXISTS authors;

            CREATE TABLE authors (
                id      INTEGER PRIMARY KEY,
                name    TEXT NOT NULL,
                country TEXT
            );
            CREATE TABLE books (
                id             INTEGER PRIMARY KEY,
                author_id      INTEGER NOT NULL REFERENCES authors(id),
                title          TEXT NOT NULL,
                price          REAL NOT NULL,
                published_year INTEGER,
                stock          INTEGER NOT NULL DEFAULT 0
            );
            CREATE TABLE orders (
                id             INTEGER PRIMARY KEY,
                customer_email TEXT NOT NULL,
                created_at     TEXT NOT NULL
            );
            CREATE TABLE order_items (
                id         INTEGER PRIMARY KEY,
                order_id   INTEGER NOT NULL REFERENCES orders(id),
                book_id    INTEGER NOT NULL REFERENCES books(id),
                quantity   INTEGER NOT NULL,
                unit_price REAL NOT NULL
            );

            INSERT INTO authors (id, name, country) VALUES
                (1, 'Ursula K. Le Guin', 'USA'),
                (2, 'Frank Herbert', 'USA'),
                (3, 'Stanislaw Lem', 'Poland'),
                (4, 'Douglas Adams', 'UK'),
                (5, 'Ted Chiang', 'USA');

            INSERT INTO books (id, author_id, title, price, published_year, stock) VALUES
                (1, 1, 'A Wizard of Earthsea', 12.50, 1968, 8),
                (2, 1, 'The Left Hand of Darkness', 14.00, 1969, 3),
                (3, 1, 'The Dispossessed', 15.25, 1974, 0),
                (4, 2, 'Dune', 18.90, 1965, 12),
                (5, 3, 'Solaris', 11.40, 1961, 5),
                (6, 4, 'The Hitchhiker''s Guide to the Galaxy', 9.99, 1979, 20),
                (7, 5, 'Exhalation', 13.20, NULL, 4);
            """);
    }
}

Veritabanını kurarken bile Dapper'ın Execute method'unu kullandık; SQLite provider'ı noktalı virgülle ayrılmış birden fazla ifadeyi tek command içinde çalıştırabiliyor. Kolon adlarının snake_case olduğuna dikkat et: author_id, published_year. Bu bilinçli bir tercih. C# tarafındaki PascalCase property'lerle nasıl buluşacaklarını bir sonraki yazıda konuşacağız.

İlk Query

Sonuçları taşıyacak class, Book.cs:

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public decimal Price { get; set; }
}

Ve Program.cs:

using Dapper;
using Microsoft.Data.Sqlite;

BookstoreDb.Reset();

using var connection = new SqliteConnection(BookstoreDb.ConnectionString);

Console.WriteLine($"Before: {connection.State}");
var books = connection.Query<Book>("SELECT id, title, price FROM books ORDER BY price");
Console.WriteLine($"After: {connection.State}");

foreach (var book in books)
{
    Console.WriteLine($"{book.Id}  {book.Title,-38} {book.Price,6:0.00}");
}

dotnet run ile çalıştırdığında:

Before: Closed
After: Closed
6  The Hitchhiker's Guide to the Galaxy     9.99
5  Solaris                                 11.40
1  A Wizard of Earthsea                    12.50
7  Exhalation                              13.20
2  The Left Hand of Darkness               14.00
3  The Dispossessed                        15.25
4  Dune                                    18.90

Baştaki sorunun cevabı burada: exception yok ve connection sorgudan önce de sonra da Closed. Dapper kapalı bir connection aldığını görürse onu açar, sorguyu çalıştırır ve iş bitince kapatır. Connection'ı sen açtıysan ona dokunmaz, açık bırakır. Buradaki kapatma da fiziksel bir kopuş değil; ADO.NET'in connection pooling mekanizması fiziksel connection'ı havuzda tutar ve bir sonraki açılış ucuza gelir. Tek tek sorgular için bu çok rahat bir davranış. Ama birden fazla command'ı aynı oturumda çalıştırman gerektiğinde, örneğin bir transaction içinde, connection'ı kendin açarsın. Connection yönetimini 06'da, transaction'ları 07'de ayrıntılı göreceğiz.

Query<Book> bir IEnumerable<Book> döndürür, ama varsayılan olarak bütün satırlar method dönmeden okunup bir listeye konur. Buna buffered sorgu denir ve connection kapandığında verinin çoktan bellekte olmasının sebebi budur. Büyük result set'lerde bunun alternatifini serinin ileri bölümünde konuşacağız.

Bir de eşlemeye bak. Sorgudaki id, title, price kolonları, Book'taki Id, Title, Price property'lerine harf büyüklüğüne bakılmadan eşlendi. SQLite id'yi 64 bitlik tamsayı, price'ı da double olarak döndürse de Dapper bunları int ve decimal property'lere çevirdi.

Dapper mı, EF Core mu?

Bu soru çoğu zaman yanlış kurulur, çünkü cevap sıklıkla "ikisi birden" olur. EF Core serisinde gördüğümüz gibi EF Core; change tracking, LINQ, migration'lar ve ilişki yönetimi konusunda çok güçlü. Yazma ağırlıklı, domain modeli zengin ve şeması koddan yönetilen bir uygulamada sana ciddi zaman kazandırır.

Dapper ise SQL'in tamamen senin kontrolünde olmasını istediğin yerlerde öne çıkar: raporlar, dashboard sorguları, karmaşık JOIN'ler ve CTE'ler, window function'lar gibi veritabanına özgü özellikler, veritabanı ekibinin yazıp optimize ettiği hazır SQL'ler ve her milisaniyenin sayıldığı okuma yolları.

Bedeli de var. SQL string'lerini derleyici denetlemez; bir kolon adını yanlış yazarsan bunu çalışma anında öğrenirsin. Şema değiştiğinde SQL'leri sen güncellersin, insert ve update kodunu da sen yazarsın. Pratikte birçok ekip yazma tarafında EF Core, ağır okuma sorgularında Dapper kullanır. İkisi aynı projede rahatça yaşar; EF Core'un context.Database.GetDbConnection() ile verdiği connection üzerinde Dapper çalıştırabilirsin.

Kısa Bir Tarihçe: Stack Overflow'dan Dapper.AOT'ye

Dapper, Stack Overflow için ve Stack Overflow ekibi tarafından geliştirildi; 2011'den beri açık kaynak ve bugün GitHub'da DapperLib organizasyonu altında yaşıyor. 2016'daki 1.50 sürümü .NET Core desteğini getirdi; bugün kullandığın çekirdek API'nin neredeyse tamamı o dönemde zaten vardı.

Ağustos 2019'daki 2.0.4 daha çok bir paketleme değişikliğiydi: Dapper'ın System.Data.SqlClient bağımlılığı kaldırıldı. Artık provider paketini sen ekliyorsun. 2.0 serisi içinde IAsyncEnumerable döndüren QueryUnbufferedAsync (2.0.138) gibi eklemeler geldi.

2023'te başlayan 2.1 serisi bütün API'ye nullable reference type annotation'larını ekledi ve $ parameter önekini destekledi. 2.1.11 ile boşluk içermeyen command metinlerinin stored procedure sayılması gibi küçük ama dikkat isteyen bir davranış değişikliği geldi. 2025'teki 2.1.66 hedef framework'leri sadeleştirdi, 2026'daki 2.1.72 ayrı bir net10.0 hedefi ekledi, 2.1.86 da daha önce sorun çıkardığı için geri çekilen DateOnly/TimeOnly desteğini düzeltilmiş hâliyle geri getirdi. Bu yazının dayandığı 2.1.89 Eylül 2026'da yayımlandı.

Hikâyenin gerçekten yeni olan kısmı Dapper.AOT. Klasik Dapper mapping kodunu çalışma anında üretirken Dapper.AOT bunu derleme zamanında, bir source generator ve interceptor'larla yapıyor; böylece Native AOT ile yayımlanan uygulamalarda da kullanılabiliyor. Serinin son yazısı tamamen ona ayrıldı.

Kısacası Dapper'ın API'si 2.0.4'ten beri kırıcı bir değişiklik yaşamadı. Bu yüzden seriyi sürüm sürüm değil, tek bir güncel taban üzerinden anlatıyoruz; bir özellik belirli bir sürümle geldiyse ilgili yazıda not düşeceğiz.

Sık Düşülen Tuzaklar

İlk tuzak neredeyse herkesin başına gelir: using Dapper; satırını unutmak. Dapper'ın method'ları extension method olduğu için namespace'i eklemezsen connection'da Query diye bir şey görünmez ve derleyici şunu söyler:

Program.cs(4,24): error CS1061: 'SqliteConnection' does not contain a definition for 'Query' and no accessible extension method 'Query' accepting a first argument of type 'SqliteConnection' could be found (are you missing a using directive or an assembly reference?)

.NET 10'un implicit using'leri Dapper namespace'ini kapsamaz.

İkincisi provider seçimi. Eski blog yazılarında ve forum cevaplarında System.Data.SqlClient görürsün. Bu paket NuGet'te deprecated olarak işaretli ve Microsoft.Data.SqlClient'ı öneriyor. Yeni bir projede SQL Server için her zaman Microsoft.Data.SqlClient kullan.

Üçüncüsü Dapper'dan ORM davranışı beklemek. Query ile okuduğun bir kitabın fiyatını değiştirip bunun bir yere kaydedilmesini beklersen hiçbir şey olmaz; o object artık veritabanıyla bağı olmayan sıradan bir C# object'i. Değişikliği yazmak için kendi UPDATE ifadeni Execute ile çalıştırırsın, bunu 05'te göreceğiz.

Dördüncüsü SELECT * alışkanlığı. Dapper'la çalışırken neye ihtiyacın varsa onu seç. Hangi kolonun hangi property'ye gittiği de kodu okuyan herkes için açık olur. Eşleşmeyen bir kolon olduğunda ne olduğunu bir sonraki yazıda göreceğiz; spoiler vereyim, çoğu zaman hiçbir şey olmuyor, sessizce.

Bu Seri Nasıl İlerleyecek?

Seri on altı yazıdan oluşuyor ve hepsi Dapper 2.1 ile .NET 10 üzerine kurulu. İlk dokuz yazı çekirdek: Query<T> ve mapping, tek satır sorguları, parameter'lar ve SQL injection, Execute ile veri değiştirmek, async kullanım ve connection yönetimi, transaction'lar, multi-mapping ve QueryMultiple. Sonraki yedi yazı ileri seviye: buffered ve unbuffered sorgular, type handler'lar, stored procedure'lar, SQL Server'a özgü konular, dinamik SQL ve ekosistem, performans ve son olarak Dapper.AOT.

Örneklerin neredeyse tamamı bu SQLite veritabanında çalışıyor; stored procedure gibi SQLite'ın desteklemediği konularda SQL Server ve PostgreSQL'i docker ile ayağa kaldıracağız. Yazılardaki bütün çıktılar kodların gerçek çalıştırmalarından alındı.

Küçük Bir Deneme

★☆☆ Projeyi kur, BookstoreDb.cs, Book.cs ve Program.cs dosyalarını ekle ve çıktıyı kendi makinende al. Sonra ORDER BY price DESC ile sırayı ters çevir.

★★☆ Program.cs'te sorgudan önce connection.Open(); çağır. "Before" ve "After" satırları bu kez ne yazıyor? Dapper'ın açık bir connection'ı neden kapatmadığını kendi cümlelerinle açıkla.

★★★ GetCheapBooksAdoNet ve GetCheapBooksDapper method'larının ikisinde de SELECT'teki kolon sırasını title, id, price olarak değiştir ve başka hiçbir şeye dokunmadan çalıştır. Hangi versiyonun sonucu bozuldu? Exception aldın mı, yoksa yanlış veri mi? Neden?

Bir sonraki yazıda Query<T>'nin içine gireceğiz: kolonlar property'lere tam olarak nasıl eşleniyor, snake_case kolonlar ne olacak, dynamic sonuçlar ne zaman işe yarar ve record'lar neden bazen "constructor bulunamadı" diye itiraz eder. Seri burada da bitmiyor; Dapper'ın yeni sürümleri çıktıkça yeni yazılarla devam edeceğiz.