Dapper w .NET 9 – Kompletny przewodnik z 21 przykładami aplikacji konsolowej w C#

Dapper w .NET 9 – Kompletny przewodnik z 21 przykładami aplikacji konsolowej w C#
26 maja 2025 Brak komentarzy .NET, Console, Poradnik, Wzorce Projektowe Tajko

Wprowadzenie

Dapper to lekkie i szybkie narzędzie ORM (Object-Relational Mapping) dla platformy .NET, które pozwala na łatwe wykonywanie zapytań SQL oraz mapowanie wyników do obiektów C#. W porównaniu do Entity Framework, Dapper nie śledzi zmian w obiektach i oferuje znacznie wyższą wydajność, co czyni go idealnym wyborem dla aplikacji wymagających maksymalnej kontroli nad zapytaniami SQL i szybkości działania.

W tym artykule pokażę Ci, jak wykorzystać Dapper w aplikacji konsolowej w .NET 9, zaczynając od najprostszych przykładów, aż po zaawansowane scenariusze, z wykorzystaniem klas, metod synchronicznych i asynchronicznych oraz opisu XML. Skorzystam z bazy danych SQL Server oraz w jednym przykładzie będą trzy bazy danych SQL Server, MySQL, SQLite.

Komunikaty początkowe na zrzutach ekranu możesz pominąć/zignorować.

Solucja z projektami do pobrania: DapperExamples

Spis treści

  1. Poziom: Podstawy
    1. Instalacja Dapper
    2. Połączenie z bazą danych
    3. Pobieranie pojedynczego rekordu
    4. Pobieranie wielu rekordów
    5. Wstawianie danych
    6. Aktualizacja danych
    7. Usuwanie danych
  2. Poziom: Średniozaawansowany
    1. Użycie parametrów
    2. Mapowanie do klas
    3. Wiele wyników (QueryMultiple)
    4. Transakcje SQL
    5. Metody asynchroniczne (QueryAsync, ExecuteAsync)
    6. Wywołanie procedur składowanych
  3. Poziom: Zaawansowany
    1. Mapowanie relacji 1:1 i 1:N (Multi-mapping)
    2. Obsługa wielu zapytań w jednym wywołaniu
    3. Buforowanie zapytań
    4. Integracja z warstwą repozytorium
    5. Praca z strukturami (np. ValueObject)
    6. Dynamiczne zapytania
    7. Własna implementacja SqlConnectionFactory
    8. Utworzenie serwisu z metodami CRUD i zaawansowanymi operacjami
  4. Podsumowanie

Poziom: Podstawowy

Przykład 1: Instalacja Dapper

Krok 1: Tworzymy nową aplikację konsolową:

mkdir DapperDemo
cd DapperDemo
dotnet new console -n DapperDemo
cd DapperDemo

Krok 2: Instalacja pakietu Dapper:

dotnet add package Dapper

Przykład 2: Połączenie z bazą danych

W tym przykładzie utworzymy prostą klasę, która zarządza połączeniem z bazą danych SQL Server.

using System.Data;
using Microsoft.Data.SqlClient;

/// <summary>
/// Klasa pomocnicza do uzyskiwania połączenia z bazą danych.
/// </summary>
public static class DbConnectionFactory
{
    /// <summary>
    /// Łańcuch połączenia do lokalnej bazy danych.
    /// </summary>
    private const string ConnectionString = "Server=localhost;Database=DapperTestDb;Trusted_Connection=True;TrustServerCertificate=True;";

    /// <summary>
    /// Zwraca nowe połączenie z bazą danych SQL Server.
    /// </summary>
    /// <returns>Obiekt połączenia IDbConnection</returns>
    public static IDbConnection CreateConnection()
    {
        // Tworzymy nowe połączenie z SQL Server
        return new SqlConnection(ConnectionString);
    }
}

Uwaga: Upewnij się, że masz lokalnie zainstalowany SQL Server oraz utworzoną bazę danych DapperTestDb.

W Program.cs możemy teraz przetestować połączenie:

class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        Console.WriteLine($"Stan połączenia: {connection.State}");
        connection.Open();
        Console.WriteLine($"Po otwarciu: {connection.State}");
    }
}

Wynik:

Dapper - Przykład 2 - Połączenie z bazą danych

Przykład 3: Pobieranie pojedynczego rekordu

Utwórzmy prostą tabelę i klasę modelu:

CREATE TABLE Users (
    Id INT PRIMARY KEY IDENTITY,
    Name NVARCHAR(100),
    Email NVARCHAR(100)
);

INSERT INTO Users (Name, Email) VALUES ('Jan Kowalski', 'jan.kowalski@example.com'), ('Riasso Gremurro', 'riasso.gremurro@hothell.com')
/// <summary>
/// Klasa reprezentująca użytkownika.
/// </summary>
public class User
{
    /// <summary>Identyfikator użytkownika.</summary>
    public int Id { get; set; }

    /// <summary>Imię i nazwisko użytkownika.</summary>
    public string Name { get; set; }

    /// <summary>Adres e-mail użytkownika.</summary>
    public string Email { get; set; }
}

W Program.cs dodajmy kod do pobrania użytkownika po ID:

using Dapper;

class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        // Pobranie użytkownika o ID = 1
        string sql = "SELECT * FROM Users WHERE Id = @Id";
        var user = connection.QuerySingleOrDefault<User>(sql, new { Id = 1 });

        if (user != null)
        {
            Console.WriteLine($"Użytkownik: {user.Name}, Email: {user.Email}");
        }
        else
        {
            Console.WriteLine("Nie znaleziono użytkownika.");
        }
    }
}

Wynik:

Dapper - Przykład 3 - Pobieranie pojedynczego rekordu

Przykład 4: Pobieranie wielu rekordów

W tym przykładzie pokażemy, jak pobrać wielu użytkowników z bazy danych i wyświetlić ich dane. Jest to typowy przypadek przy listowaniu danych w aplikacjach CRUD.

using Dapper;

class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        // Zapytanie SQL pobierające wszystkich użytkowników
        string sql = "SELECT * FROM Users";
        var users = connection.Query<User>(sql).ToList();

        Console.WriteLine("Lista użytkowników:");
        foreach (var user in users)
        {
            Console.WriteLine($"{user.Id}: {user.Name} - {user.Email}");
        }
    }
}

Opis: Wykorzystujemy metodę Query<T>, która zwraca kolekcję obiektów typu User. Dzięki LINQ możemy ją przekształcić na listę i wypisać jej zawartość. To bardzo wydajna i prosta metoda do listowania rekordów.

Wynik:

Dapper - Przykład 4 - Pobieranie wielu rekordów

Przykład 5: Wstawianie danych

Poniżej pokażemy, jak dodać nowego użytkownika do bazy danych z użyciem Dappera.

using Dapper;

class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        // Dodanie nowego użytkownika do tabeli
        string sql = "INSERT INTO Users (Name, Email) VALUES (@Name, @Email)";
        var affectedRows = connection.Execute(sql, new { Name = "Anna Nowak", Email = "anna.nowak@example.com" });

        Console.WriteLine($"Liczba dodanych wierszy: {affectedRows}");
    }
}

Opis: Metoda Execute służy do wykonywania zapytań, które nie zwracają danych (np. INSERT, UPDATE, DELETE). Przekazujemy parametry za pomocą obiektu anonimowego. affectedRows wskazuje, ile wierszy zostało zmodyfikowanych.

Wynik:

Dapper - Przykład 5 - Wstawianie danych

Przykład 6: Aktualizacja danych

Zaktualizujmy adres e-mail użytkownika na podstawie jego ID.

using Dapper;

class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        string sql = "UPDATE Users SET Email = @Email WHERE Id = @Id";
        var result = connection.Execute(sql, new { Id = 1, Email = "nowy.email@example.com" });

        Console.WriteLine($"Zaktualizowano wierszy: {result}");
    }
}

Opis: Podobnie jak wcześniej, używamy Execute, ale tym razem do aktualizacji danych w istniejącym rekordzie. Parametry są mapowane na zapytanie SQL przez nazwę.

Wynik:

Dapper - Przykład 6 - Aktualizacja danych

Przykład 7: Usuwanie danych

Na koniec tej sekcji pokażemy, jak usunąć użytkownika z tabeli.

using Dapper;

class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        string sql = "DELETE FROM Users WHERE Id = @Id";
        var deleted = connection.Execute(sql, new { Id = 1 });

        Console.WriteLine($"Usunięto wierszy: {deleted}");
    }
}

Opis: Dapper pozwala także łatwo wykonywać zapytania DELETE. Jak zawsze, parametry są wstrzykiwane bezpiecznie, co minimalizuje ryzyko SQL Injection.

Wynik:

Dapper - Przykład 7 - Usuwanie danych

Poziom: Średniozaawansowany

Przykład 8: Użycie parametrów

Dapper automatycznie mapuje wartości parametrów, ale warto pokazać, jak działa to w praktyce — np. pobieranie użytkowników po fragmencie imienia.

-- Załóżmy, że tabela Users już istnieje z Przykładu 3
INSERT INTO Users (Name, Email) VALUES ('Anna Kowalska', 'anna@example.com'), ('Andrzej Kowalski', 'andrzej@example.com');
using Dapper;

/// <summary>
/// Program pokazujący użycie parametrów w zapytaniu SQL.
/// </summary>
class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        // Zapytanie z parametrem LIKE
        string sql = "SELECT * FROM Users WHERE Name LIKE @Pattern";

        var users = connection.Query<User>(sql, new { Pattern = "%Anna%" }).ToList();

        Console.WriteLine("Znalezieni użytkownicy:");
        foreach (var user in users)
            Console.WriteLine($"{user.Id}: {user.Name} - {user.Email}");
    }
}

Opis: Parametry są przekazywane bezpiecznie jako obiekty anonimowe, a Dapper podmienia je pod spodem w zapytaniu, eliminując ryzyko SQL Injection.

Wynik:

Dapper - Przykład 8 - Użycie parametrów

Przykład 9: Mapowanie do klas

Dapper pozwala na automatyczne mapowanie wyników zapytania do klas. W tym przykładzie pokażemy, że można odczytywać tylko wybrane kolumny.

/// <summary>
/// Podgląd użytkownika z ograniczonym zestawem danych.
/// </summary>
public class UserPreview
{
    /// <summary>ID użytkownika</summary>
    public int Id { get; set; }

    /// <summary>Imię i nazwisko użytkownika</summary>
    public string Name { get; set; }
}

class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        string sql = "SELECT Id, Name FROM Users";
        var previews = connection.Query<UserPreview>(sql).ToList();

        Console.WriteLine("Podgląd użytkowników:");
        foreach (var preview in previews)
            Console.WriteLine($"{preview.Id}: {preview.Name}");
    }
}

Opis: Klasa UserPreview nie zawiera pola Email, więc Dapper go po prostu ignoruje — to świetna metoda na tworzenie lekkich DTO.

Wynik:

Dapper - Przykład 9 - Mapowanie do klas

Przykład 10: Wiele wyników (QueryMultiple)

Czasem musimy wykonać kilka zapytań jednocześnie, np. pobrać listę użytkowników i ich liczbę.

using Dapper;

/// <summary>
/// Program demonstrujący pobieranie wielu zestawów danych jednocześnie.
/// </summary>
class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        string sql = @"
            SELECT * FROM Users;
            SELECT COUNT(*) FROM Users;";

        using var multi = connection.QueryMultiple(sql);
        var users = multi.Read<User>().ToList();
        var count = multi.ReadSingle<int>();

        Console.WriteLine($"Użytkowników: {count}");
        foreach (var user in users)
            Console.WriteLine($"{user.Id}: {user.Name} - {user.Email}");
    }
}

Opis: QueryMultiple pozwala wykonać wiele zapytań w jednym wywołaniu i odczytać każde osobno przy pomocy Read<T>().

Wynik:

Dapper - Przykład 10 - Wiele wyników (QueryMultiple)

Przykład 11: Transakcje SQL

W niektórych operacjach (np. aktualizacja kilku tabel) warto użyć transakcji. Dapper działa z nimi bez problemu.

using Dapper;

/// <summary>
/// Program pokazujący użycie transakcji SQL z Dapperem.
/// </summary>
class Program
{
    static void Main(string[] args)
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        using var transaction = connection.BeginTransaction();

        try
        {
            int result = connection.Execute("UPDATE Users SET Email = @Email WHERE Id = @Id",
                new { Id = 2, Email = "rollback@example.com" }, transaction);

            Console.WriteLine($"Użytkownik z Id = 2 zaktualizowany: {(result > 0 ? "Tak" : "Nie")}");

            result = connection.Execute("DELETE FROM Users WHERE Id = @Id",
                new { Id = 999 }, transaction); // Zakładamy, że ID nie istnieje

            Console.WriteLine($"Użytkownik z Id = 999 usunięty: {(result > 0 ? "Tak" : "Nie")}");
            transaction.Commit();
            Console.WriteLine($"Transakcja zakończona powodzeniem.");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Błąd transakcji: {ex.Message}");
            transaction.Rollback();
        }
    }
}

Opis: Transakcje działają klasycznie — rozpoczynamy, wykonujemy kilka operacji, a potem Commit() lub Rollback() w razie błędu.

Wynik:

Dapper - Przykład 11 - Transakcje SQL

Przykład 12: Metody asynchroniczne (QueryAsync, ExecuteAsync)

W nowoczesnych aplikacjach warto używać operacji asynchronicznych, aby nie blokować wątku głównego.

using Dapper;
using System.Threading.Tasks;

/// <summary>
/// Przykład pokazujący pobieranie danych w sposób asynchroniczny.
/// </summary>
class Program
{
    static async Task Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        await connection.OpenAsync();

        var users = (await connection.QueryAsync<User>("SELECT * FROM Users")).ToList();

        Console.WriteLine("Asynchroniczne pobieranie użytkowników:");
        foreach (var user in users)
            Console.WriteLine($"{user.Id}: {user.Name} - {user.Email}");
    }
}

Opis: Asynchroniczne odpowiedniki metod (QueryAsync, ExecuteAsync) umożliwiają bezpieczne i wydajne operacje we współczesnych aplikacjach.

Wynik:

Dapper - Przykład 12 - Metody asynchroniczne (QueryAsync, ExecuteAsync)

Przykład 13: Wywołanie procedur składowanych

Jeśli Twoja aplikacja korzysta z procedur SQL, Dapper bez problemu je obsługuje.

CREATE PROCEDURE GetUsers
AS
BEGIN
    SELECT * FROM Users;
END
using Dapper;
using System.Data;

/// <summary>
/// Program pokazujący wywołanie procedury składowanej przy użyciu Dappera.
/// </summary>
class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        var users = connection.Query<User>("GetUsers", commandType: CommandType.StoredProcedure).ToList();

        Console.WriteLine("Dane z procedury GetUsers:");
        foreach (var user in users)
            Console.WriteLine($"{user.Id}: {user.Name} - {user.Email}");
    }
}

Opis: commandType: CommandType.StoredProcedure informuje Dappera, że wykonujemy procedurę. Parametry przekazujemy jak zwykle.

Wynik:

Dapper - Przykład 13 - Wywołanie procedur składowanych

Poziom: Zaawansowany

Przykład 14: Mapowanie relacji 1:1 i 1:N (Multi-mapping)

W tym przykładzie wykorzystujemy wcześniej utworzoną tabelę Users z przykładu 3 oraz dodajemy nową tabelę Orders. Dapper obsługuje relacje między tabelami poprzez funkcję Query z mapowaniem wielu typów. Zobaczmy relację 1:N (np. użytkownik i jego zamówienia). między tabelami poprzez funkcję Query z mapowaniem wielu typów. Zobaczmy relację 1:N (np. użytkownik i jego zamówienia).

Załóżmy, że mamy tabelę Orders (poniższa) i Users (utworzona wcześniej w Przykład 3):

CREATE TABLE Orders (
    Id INT PRIMARY KEY IDENTITY,
    UserId INT,
    Product NVARCHAR(100),
    FOREIGN KEY (UserId) REFERENCES Users(Id)
);

INSERT INTO Orders (UserId, Product) VALUES (1, 'Produkt A'), (1, 'Produkt B');

Model danych:

/// <summary>
/// Model zamówienia.
/// </summary>
public class Order
{
    public int Id { get; set; }
    public string Product { get; set; }
}

/// <summary>
/// Użytkownik z listą zamówień.
/// </summary>
public class UserWithOrders : User
{
    public List<Order> Orders { get; set; } = new();
}

Zapytanie i mapowanie:

using Dapper;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        var userDict = new Dictionary<int, UserWithOrders>();

        string sql = @"
            SELECT u.Id, u.Name, u.Email, o.Id, o.Product
            FROM Users u
            LEFT JOIN Orders o ON u.Id = o.UserId";

        var users = connection.Query<UserWithOrders, Order, UserWithOrders>(
            sql,
            (user, order) =>
            {
                if (!userDict.TryGetValue(user.Id, out var userEntry))
                {
                    userEntry = user;
                    userEntry.Orders = new List<Order>();
                    userDict.Add(userEntry.Id, userEntry);
                }
                if (order != null)
                    userEntry.Orders.Add(order);
                return userEntry;
            },
            splitOn: "Id")
            .Distinct()
            .ToList();

        foreach (var u in users)
        {
            Console.WriteLine($"{u.Name} - {u.Email} ({u.Orders.Count} zamówień)");
            foreach (var o in u.Orders)
                Console.WriteLine($"  - {o.Product}");
        }
    }
}

Opis: Kluczowe jest użycie splitOn: "Id", by Dapper wiedział, gdzie rozpoczyna się drugi obiekt. Warto też użyć słownika do agregacji zamówień.

Wynik:

Dapper - Przykład 14 - Mapowanie relacji 1:1 i 1:N (Multi-mapping)

Przykład 15: Obsługa wielu zapytań w jednym wywołaniu

Czasem chcemy dynamicznie budować zapytania i wykonać kilka w jednym połączeniu – np. do logowania lub dashboardów.

using Dapper;

/// <summary>
/// Przykład pokazujący wykonywanie wielu zapytań SQL w jednym wywołaniu.
/// </summary>
class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        string sql = @"
            SELECT COUNT(*) FROM Users;
            SELECT COUNT(*) FROM Orders;
            SELECT TOP 1 * FROM Users ORDER BY Id DESC;";

        using var multi = connection.QueryMultiple(sql);
        var userCount = multi.ReadSingle<int>();
        var orderCount = multi.ReadSingle<int>();
        var lastUser = multi.ReadSingle<User>();

        Console.WriteLine($"Użytkowników: {userCount}, Zamówień: {orderCount}");
        Console.WriteLine($"Ostatni użytkownik: {lastUser.Name}, Email: {lastUser.Email}");
    }
}

Opis: QueryMultiple przydaje się w panelach administracyjnych, gdzie trzeba pobrać wiele danych statystycznych jednym zapytaniem.

Wynik:

Dapper - Przykład 15 - Obsługa wielu zapytań w jednym wywołaniu

Przykład 16: Buforowanie zapytań

Dapper sam w sobie nie posiada wbudowanego mechanizmu cache, ale można to łatwo dodać ręcznie.

/// <summary>
/// Bufor wyników zapytania o użytkowników.
/// </summary>
public static class UserCache
{
    private static List<User>? _cachedUsers;

    /// <summary>
    /// Zwraca listę użytkowników z pamięci podręcznej lub bazy danych.
    /// </summary>
    public static List<User> GetUsers(IDbConnection connection)
    {
        if (_cachedUsers is not null)
            return _cachedUsers;

             // Równoważne ToList() – tworzy nową List<User> z wyników zapytania (C# 12 operator spread)
             _cachedUsers = [.. connection.Query<User>("SELECT * FROM Users")];

        return _cachedUsers;
    }

    /// <summary>
    /// Czyści pamięć podręczną użytkowników.
    /// </summary>
    public static void Clear() => _cachedUsers = null;
}
/// <summary>
/// Program testujący działanie buforowania zapytań.
/// </summary>
class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        var users = UserCache.GetUsers(connection);
        Console.WriteLine("Pobrano użytkowników z cache:");
        foreach (var user in users)
            Console.WriteLine($"{user.Name} - {user.Email}");

        // UserCache.Clear(); // W razie potrzeby można wyczyścić cache
    }
}

Opis: Buforowanie poprawia wydajność, jeśli dane rzadko się zmieniają. Można dodać TTL lub integrację z IMemoryCache.

Wynik:

Dapper - Przykład 16 - Buforowanie zapytań

Przykład 17: Integracja z warstwą repozytorium

Aby zachować czystość architektury, warto oddzielić logikę zapytań od warstwy prezentacji. Poniżej prosty przykład repozytorium.

/// <summary>
/// Interfejs repozytorium użytkowników.
/// </summary>
public interface IUserRepository
{
    /// <summary>
    /// Zwraca listę wszystkich użytkowników.
    /// </summary>
    List<User> GetAll();
}

/// <summary>
/// Implementacja repozytorium użytkowników z użyciem Dappera.
/// </summary>
public class UserRepository : IUserRepository
{
    private readonly IDbConnection _connection;

    /// <summary>
    /// Tworzy instancję repozytorium z przekazanym połączeniem do bazy.
    /// </summary>
    public UserRepository(IDbConnection connection)
    {
        _connection = connection;
    }

    /// <summary>
    /// Pobiera wszystkich użytkowników z tabeli Users.
    /// </summary>
    public List<User> GetAll()
    {           
         // Równoważne ToList() – tworzy nową List<User> z wyników zapytania (C# 12 operator spread)
         return [.. _connection.Query<User>("SELECT * FROM Users")];
    }
}

Użycie w aplikacji:

/// <summary>
/// Program wykorzystujący repozytorium do pobierania danych.
/// </summary>
class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        var repository = new UserRepository(connection);
        var users = repository.GetAll();

        Console.WriteLine("Użytkownicy z repozytorium:");
        foreach (var user in users)
            Console.WriteLine($"{user.Id}: {user.Name} - {user.Email}");
    }
}

Opis: Dzięki repozytorium nasz kod w Main nie zawiera zapytań SQL, co ułatwia testowanie i utrzymanie aplikacji.

Wynik:

Dapper - Przykład 17 - Integracja z warstwą repozytorium

Przykład 18: Praca z strukturami (np. ValueObject)

Można użyć struktur jako typów danych, pod warunkiem, że właściwości są publiczne. Poniżej przykład klasy EmailAddress jako value object:

/// <summary>
/// Obiekt wartości reprezentujący adres e-mail z walidacją.
/// </summary>
public readonly struct EmailAddress
{
    /// <summary>Wartość adresu e-mail jako tekst.</summary>
    public string Value { get; }

    /// <summary>
    /// Tworzy instancję adresu e-mail i sprawdza jego poprawność.
    /// </summary>
    /// <param name="value">Tekstowy adres e-mail</param>
    /// <exception cref="ArgumentException">Rzucany, gdy adres jest nieprawidłowy</exception>
    public EmailAddress(string value)
    {
        if (!value.Contains('@'))
            throw new ArgumentException("Nieprawidłowy email.");
        Value = value;
    }

    /// <summary>
    /// Zwraca adres e-mail jako tekst.
    /// </summary>
    public override string ToString() => Value;
}

/// <summary>
/// Model użytkownika wykorzystujący typ EmailAddress jako ValueObject.
/// </summary>
public class UserWithValueObject
{
    /// <summary>Unikalny identyfikator użytkownika.</summary>
    public int Id { get; set; }
    /// <summary>Imię i nazwisko użytkownika.</summary>
    public string Name { get; set; }
    /// <summary>Adres e-mail użytkownika jako typ EmailAddress.</summary>
    public EmailAddress Email { get; set; }
}

Aby Dapper mógł mapować typ EmailAddress, potrzebna będzie konwersja za pomocą własnego TypeHandlera. Należy go zarejestrować raz na początku działania aplikacji:

SqlMapper.AddTypeHandler(new EmailAddressTypeHandler());

/// <summary>
/// TypeHandler Dappera umożliwiający mapowanie struktury EmailAddress.
/// </summary>
public class EmailAddressTypeHandler : SqlMapper.TypeHandler<EmailAddress>
{
    /// <summary>
    /// Ustawia wartość parametru SQL na podstawie struktury EmailAddress.
    /// </summary>
    /// <param name="parameter">Parametr SQL</param>
    /// <param name="value">Wartość EmailAddress</param>
    public override void SetValue(IDbDataParameter parameter, EmailAddress value)
        => parameter.Value = value.Value;

    /// <summary>
    /// Parsuje wartość z bazy danych na typ EmailAddress.
    /// </summary>
    /// <param name="value">Wartość z bazy</param>
    /// <returns>Instancja EmailAddress</returns>
    public override EmailAddress Parse(object value)
        => new((string)value);
}

Przykład użycia w aplikacji konsolowej:

using Dapper;
using System.Data.SqlClient;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(Globals.TitleMessage("Przykład 18 - Praca z strukturami (np. ValueObject)"));

        // Rejestrujemy handlera raz na początku aplikacji
        SqlMapper.AddTypeHandler(new EmailAddressTypeHandler());

        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        var users = connection.Query<UserWithValueObject>("SELECT * FROM Users").ToList();

        foreach (var user in users)
        {
            Console.WriteLine($"{user.Id}: {user.Name} - {user.Email}");
        }
    }
}

Opis: Warto używać ValueObject dla pól typu Email, NumerTelefonu itp., aby wymusić poprawność już na poziomie modelu. Dzięki rejestracji TypeHandlera, Dapper wie, jak mapować dane między EmailAddressastring`.


Przykład 19: Dynamiczne zapytania

Jeśli nie znamy z góry struktury odpowiedzi (np. API proxy), można użyć dynamicznych obiektów.

using Dapper;

/// <summary>
/// Przykład pokazujący użycie dynamicznego typu do mapowania danych z zapytania.
/// </summary>
class Program
{
    static void Main()
    {
        using var connection = DbConnectionFactory.CreateConnection();
        connection.Open();

        string sql = "SELECT Id, Name, Email FROM Users";
        // var results = connection.Query(sql).ToList(); // Zwraca List<dynamic>

        IEnumerable<dynamic> results = [.. connection.Query(sql)]; // Zwraca IEnumerable<dynamic>

        Console.WriteLine("Wyniki zapytania dynamicznego:");
        foreach (var row in results)
        {
            Console.WriteLine($"{row.Id}: {row.Name} - {row.Email}");
        }
    }
}

Opis: Czasem nie potrzebujemy konkretnego modelu – możemy użyć dynamic, np. w logach, tabelach lub narzędziach administracyjnych.

Wynik:

Dapper - Przykład 19 - Dynamiczne zapytania

Przykład 20: Własna implementacja SqlConnectionFactory

W tym przykładzie stworzymy elastyczną strukturę, która pozwoli aplikacji .NET 9 łączyć się z różnymi typami baz danych (SQL Server, MySQL, SQLite) bez zmieniania logiki aplikacyjnej.

Jest to szczególnie przydatne w aplikacjach:

  • testowanych lokalnie (SQLite),
  • wdrażanych na produkcję (SQL Server, MySQL),
  • wymagających wymienialności silnika bazy danych (np. przez konfigurację).

📦 Interfejs wspólny: ISqlConnectionFactory

using System.Data;

/// <summary>
/// Interfejs definiujący fabrykę połączeń do bazy danych.
/// </summary>
public interface ISqlConnectionFactory
{
    /// <summary>
    /// Tworzy i zwraca nowe połączenie do bazy danych.
    /// </summary>
    /// <returns>Obiekt połączenia typu IDbConnection.</returns>
    IDbConnection Create();
}

🖥 SQL Server – SqlServerConnectionFactory

Wymagane dodanie: NuGet: Microsoft.Data.SqlClient

using Microsoft.Data.SqlClient;
using System.Data;

/// <summary>
/// Fabryka połączeń dla SQL Server.
/// </summary>
public class SqlServerConnectionFactory : ISqlConnectionFactory
{
    private readonly string _connectionString;

    /// <summary>
    /// Tworzy nową instancję fabryki z łańcuchem połączenia.
    /// </summary>
    /// <param name="connectionString">Łańcuch połączenia do SQL Server.</param>
    public SqlServerConnectionFactory(string connectionString)
    {
        _connectionString = connectionString;
    }

    /// <summary>
    /// Zwraca nowe połączenie do SQL Server.
    /// </summary>
    public IDbConnection Create()
    {
        // Można tutaj logować lub opakowywać połączenie
        return new SqlConnection(_connectionString);
    }
}

🐬 MySQL – MySqlConnectionFactory

Wymagane dodanie: NuGet: MySql.Data

using MySql.Data.MySqlClient;
using System.Data;

/// <summary>
/// Fabryka połączeń dla MySQL.
/// </summary>
public class MySqlConnectionFactory : ISqlConnectionFactory
{
    private readonly string _connectionString;

    /// <summary>
    /// Inicjalizuje fabrykę z connection stringiem dla MySQL.
    /// </summary>
    /// <param name="connectionString">Łańcuch połączenia do MySQL.</param>
    public MySqlConnectionFactory(string connectionString)
    {
        _connectionString = connectionString;
    }

    /// <summary>
    /// Zwraca nowe połączenie do MySQL.
    /// </summary>
    public IDbConnection Create()
    {
        return new MySqlConnection(_connectionString);
    }
}

💾 SQLite – SqliteConnectionFactory

Wymagane dodanie: NuGet: Microsoft.Data.Sqlite

using Microsoft.Data.Sqlite;
using System.Data;

/// <summary>
/// Fabryka połączeń dla lokalnej bazy SQLite.
/// </summary>
public class SqliteConnectionFactory : ISqlConnectionFactory
{
    private readonly string _connectionString;

    /// <summary>
    /// Inicjalizuje fabrykę z connection stringiem dla SQLite.
    /// </summary>
    /// <param name="connectionString">Ścieżka do pliku bazy SQLite.</param>
    public SqliteConnectionFactory(string connectionString)
    {
        _connectionString = connectionString;
    }

    /// <summary>
    /// Zwraca nowe połączenie do SQLite.
    /// </summary>
    public IDbConnection Create()
    {
        return new SqliteConnection(_connectionString);
    }
}

🚀 Użycie fabryki w aplikacji

using Dapper;

/// <summary>
/// Program testujący dynamiczne wybieranie źródła bazy danych.
/// </summary>
class Program
{
    static void Main()
    {
        // Wybór typu bazy – można dynamicznie ustawiać z pliku konfiguracyjnego lub argumentu
        string dbType = "sqlserver"; // lub: "sqlite", "mysql"

        // Przykładowe connection stringi – dostosuj do środowiska
        ISqlConnectionFactory factory = dbType switch
        {
            "sqlserver" => new SqlServerConnectionFactory("Server=localhost;Database=DapperTestDb;Trusted_Connection=True;TrustServerCertificate=True;"),
            "mysql" => new MySqlConnectionFactory("Server=localhost;Database=DapperTestDb;Uid=root;Pwd=yourpassword;"),
            "sqlite" => new SqliteConnectionFactory("Data Source=DapperTestDb.db"),
            _ => throw new NotSupportedException("Nieobsługiwany typ bazy danych.")
        };

        using var connection = factory.Create();
        connection.Open();

        // Wypisanie użytkowników
        var users = connection.Query<User>("SELECT * FROM Users").ToList();

        Console.WriteLine($"Lista użytkowników dla bazy danych '{dbType}':");
        foreach (var user in users)
            Console.WriteLine($"{user.Id}: {user.Name} - {user.Email}");
    }
}

🧠 Co daje taka struktura?

Testowalność: Możesz w testach podmienić fabrykę na np. SQLite in-memory.
Elastyczność: Wystarczy zmienić ISqlConnectionFactory, a nie całą logikę zapytań.
Czytelność: Odpowiedzialność za połączenie przeniesiona do klasy.
Skalowalność: Można później dodać logger, retry policy, dekoratory itp.

Wynik:

Dapper - Przykład 20 - Własna implementacja SqlConnectionFactory

Przykład 21: Serwis z metodami CRUD i operacjami zaawansowanymi

Po przeanalizowaniu wszystkich przykładów czas pokazać bardziej praktyczne podejście: utworzymy klasę UserService, która łączy operacje CRUD i dodatkowe funkcje w jednej klasie serwisowej gotowej do użycia w aplikacjach.

Interfejs serwisu IUserService:

/// <summary>
/// Interfejs serwisu użytkowników definiujący operacje CRUD oraz metody pomocnicze.
/// </summary>
public interface IUserService
{
    /// <summary>
    /// Dodaje nowego użytkownika do bazy danych.
    /// </summary>
    /// <param name="name">Imię i nazwisko użytkownika.</param>
    /// <param name="email">Adres e-mail użytkownika.</param>
    /// <returns>ID nowo dodanego użytkownika.</returns>
    int Add(string name, string email);

    /// <summary>
    /// Zwraca liczbę wszystkich użytkowników w bazie.
    /// </summary>
    /// <returns>Liczba użytkowników.</returns>
    int Count();

    /// <summary>
    /// Usuwa użytkownika na podstawie jego identyfikatora.
    /// </summary>
    /// <param name="id">Identyfikator użytkownika.</param>
    void Delete(int id);

    /// <summary>
    /// Zwraca listę wszystkich użytkowników.
    /// </summary>
    /// <returns>Lista obiektów typu <see cref="User"/>.</returns>
    List<User> GetAll();

    /// <summary>
    /// Pobiera użytkownika na podstawie identyfikatora.
    /// </summary>
    /// <param name="id">Identyfikator użytkownika.</param>
    /// <returns>Obiekt <see cref="User"/> lub null, jeśli nie znaleziono.</returns>
    User? GetById(int id);

    /// <summary>
    /// Zwraca statystyki: liczbę użytkowników i ostatniego użytkownika.
    /// </summary>
    /// <returns>Krotka: liczba użytkowników i ostatni użytkownik.</returns>
    (int Count, User? LastUser) GetUserStats();

    /// <summary>
    /// Aktualizuje wszystkie dane użytkownika.
    /// </summary>
    /// <param name="user">Obiekt użytkownika z uzupełnionym ID.</param>
    void Update(User user);

    /// <summary>
    /// Aktualizuje wyłącznie adres e-mail danego użytkownika.
    /// </summary>
    /// <param name="id">Identyfikator użytkownika.</param>
    /// <param name="newEmail">Nowy adres e-mail.</param>
    /// <returns>Liczba zmodyfikowanych rekordów (0 lub 1).</returns>
    int UpdateUserEmail(int id, string newEmail);
}

Klasa serwisu UserService:

/// <summary>
/// Serwis użytkowników obsługujący operacje CRUD i inne operacje za pomocą Dappera.
/// </summary>
public class UserService : IUserService
{
    private readonly IDbConnection _connection;

    /// <summary>
    /// Tworzy instancję serwisu z przekazanym połączeniem do bazy.
    /// </summary>
    /// <param name="connection">Połączenie do bazy danych</param>
    public UserService(IDbConnection connection)
    {
        _connection = connection;
    }

    /// <inheritdoc/>
    public List<User> GetAll()
        => _connection.Query<User>("SELECT * FROM Users").ToList();

    /// <inheritdoc/>
    public User? GetById(int id)
        => _connection.QuerySingleOrDefault<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = id });

    /// <inheritdoc/>
    public int Add(string name, string email)
    {
        var sql = "INSERT INTO Users (Name, Email) VALUES (@Name, @Email)";
        return _connection.Execute(sql, new { Name = name, Email = email });
    }

    /// <inheritdoc/>
    public int UpdateUserEmail(int id, string newEmail)
    {
        var sql = "UPDATE Users SET Email = @Email WHERE Id = @Id";
        return _connection.Execute(sql, new { Id = id, Email = newEmail });
    }

    /// <inheritdoc/>
    public void Update(User user)
    {
        string sql = "UPDATE Users SET Name = @Name, Email = @Email WHERE Id = @Id";
        _connection.Execute(sql, user);
    }

    /// <inheritdoc/>
    public void Delete(int id)
    {
        string sql = "DELETE FROM Users WHERE Id = @Id";
        _connection.Execute(sql, new { Id = id });
    }

    /// <inheritdoc/>
    public int Count()
    {
        string sql = "SELECT COUNT(*) FROM Users";
        int count = _connection.ExecuteScalar<int>(sql);
        return count;
    }

    /// <inheritdoc/>
    public (int Count, User? LastUser) GetUserStats()
    {
        string sql = @"
        SELECT COUNT(*) FROM Users;
        SELECT TOP 1 * FROM Users ORDER BY Id DESC;";

        using var multi = _connection.QueryMultiple(sql);
        int count = multi.ReadSingle<int>();
        User? last = multi.ReadSingleOrDefault<User>();
        return (count, last);
    }
}

Przykładowe użycie serwisu:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(Globals.TitleMessage("Przykład 21 - Utworzenie serwisu z metodami CRUD i zaawansowanymi operacjami"));
        using var connection = DbConnectionFactory.CreateConnection();
        var userService = new UserService(connection);

        // Dodanie użytkownika
        userService.Add("Marek Marecki", "marek@example.com");

        // Wyświetlenie wszystkich
        var all = userService.GetAll();
        Console.WriteLine($"Liczba użytkowników: {all.Count}\n");

        Console.WriteLine("Aktualni Użytkownicy:");
        foreach (var u in all)
            Console.WriteLine($"{u.Id}: {u.Name} - {u.Email}");

        var (Count, LastUser) = userService.GetUserStats();
        Console.WriteLine($"Liczba użytkowników: {Count}, Ostatni: {LastUser?.Name}");

        // Edycja
        var user = userService.GetById(7);
        if (user != null)
        {
            user.Email = "zmieniony@example.com";
            userService.Update(user);
        }

        // Usunięcie
        userService.Delete(7);

        Console.WriteLine($"Usunięto użytkownika z Id = 7\n");
        Console.WriteLine("Aktualni Użytkownicy:");
        foreach (var u in all)
            Console.WriteLine($"{u.Id}: {u.Name} - {u.Email}");
    }
}

Opis: Serwis oddziela logikę danych od UI, ułatwia testowanie, refaktoryzację i stosowanie wzorców projektowych. Można go łatwo rozszerzyć o metody asynchroniczne, procedury, transakcje itp.

Wynik:


Podsumowanie

W tym artykule zapoznałeś się z pełnym spektrum możliwości, jakie daje Dapper w połączeniu z .NET 9. Rozpoczęliśmy od podstaw, takich jak wykonywanie prostych zapytań i mapowanie wyników do klas C#, poprzez bardziej zaawansowane mechanizmy, takie jak multi-mapping, transakcje, procedury składowane, aż po dynamiczne zapytania i tworzenie własnej implementacji SqlConnectionFactory.

Na zakończenie zbudowaliśmy kompletny serwis UserService, który łączy wszystkie kluczowe operacje w jednej klasie, gotowej do użycia w realnych aplikacjach. To pokazuje, jak praktycznie zastosować Dappera w dobrze zaprojektowanym rozwiązaniu.

Dapper to narzędzie niezwykle elastyczne – jego prostota, wydajność i pełna kontrola nad zapytaniami SQL czynią go świetnym wyborem dla developerów ceniących sobie szybkość działania i przejrzystość kodu.

Powodzenia z Dapperem w Twoich projektach! 💡

Jeśli Ci się spodobało to udostępnij dalej 🙂

Tagi
O Autorze
Tajko
Tajko Tajko z tej strony:) Obecnie pracuję we Wrocławiu przy projektach desktopowych. Skoro sam się czegoś nauczyłem to i inni mogliby nauczyć się tego co ja w łopatologiczny sposób:)

ZOSTAW ODPOWIEDŹ