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 TypeHandler
a, Dapper wie, jak mapować dane między
EmailAddressa
string`.
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 🙂
