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#
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
- Poziom: Podstawy
- Poziom: Średniozaawansowany
- Poziom: Zaawansowany
- Mapowanie relacji 1:1 i 1:N (Multi-mapping)
- Obsługa wielu zapytań w jednym wywołaniu
- Buforowanie zapytań
- Integracja z warstwą repozytorium
- Praca z strukturami (np. ValueObject)
- Dynamiczne zapytania
- Własna implementacja
SqlConnectionFactory - Utworzenie serwisu z metodami CRUD i zaawansowanymi operacjami
- 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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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 🙂
