Terletak di https://github.com/dapperlib/dapper/releases
MyGet Pre-Release Feed: https://www.myget.org/gallery/dapper
Kemasan | Nuget Stable | Pra-rilis Nuget | Unduhan | Myget |
---|---|---|---|---|
Rapi | ||||
Dapper.entityframework | ||||
Dapper.entityframework.strongname | ||||
Dapper.Rainbow | ||||
Dapper.sqlbuilder | ||||
Dapper.strongname |
Tujuan Paket:
Dapper awalnya dikembangkan untuk dan dengan stack overflow, tetapi f/oss. Sponsorship dipersilakan dan diundang - lihat tautan sponsor di bagian atas halaman. Terima kasih banyak untuk semua orang (individu atau organisasi) yang telah mensponsori necis, tetapi terima kasih banyak untuk:
Dapper adalah perpustakaan Nuget yang dapat Anda tambahkan ke proyek Anda yang akan meningkatkan koneksi ADO.NET Anda melalui metode ekstensi pada instance DbConnection
Anda. Ini memberikan API yang sederhana dan efisien untuk memohon SQL, dengan dukungan untuk akses data sinkron dan asinkron, dan memungkinkan kueri buffered dan non-buffered.
Ini memberikan banyak pembantu, tetapi API utamanya adalah:
// insert/update/delete etc
var count = connection . Execute ( sql [ , args ] ) ;
// multi-row query
IEnumerable < T > rows = connection . Query < T > ( sql [ , args ] ) ;
// single-row query ({Single|First}[OrDefault])
T row = connection . QuerySingle < T > ( sql [ , args ] ) ;
di mana args
bisa (antara lain):
Dictionary<string,object>
DynamicParameters
public class Dog
{
public int ? Age { get ; set ; }
public Guid Id { get ; set ; }
public string Name { get ; set ; }
public float ? Weight { get ; set ; }
public int IgnoredProperty { get { return 1 ; } }
}
var guid = Guid . NewGuid ( ) ;
var dog = connection . Query < Dog > ( " select Age = @Age, Id = @Id " , new { Age = ( int ? ) null , Id = guid } ) ;
Assert . Equal ( 1 , dog . Count ( ) ) ;
Assert . Null ( dog . First ( ) . Age ) ;
Assert . Equal ( guid , dog . First ( ) . Id ) ;
Metode ini akan menjalankan SQL dan mengembalikan daftar dinamis.
Contoh Penggunaan:
var rows = connection . Query ( " select 1 A, 2 B union all select 3, 4 " ) . AsList ( ) ;
Assert . Equal ( 1 , ( int ) rows [ 0 ] . A ) ;
Assert . Equal ( 2 , ( int ) rows [ 0 ] . B ) ;
Assert . Equal ( 3 , ( int ) rows [ 1 ] . A ) ;
Assert . Equal ( 4 , ( int ) rows [ 1 ] . B ) ;
Contoh Penggunaan:
var count = connection . Execute ( @"
set nocount on
create table #t(i int)
set nocount off
insert #t
select @a a union all select @b
set nocount on
drop table #t" , new { a = 1 , b = 2 } ) ;
Assert . Equal ( 2 , count ) ;
Tanda tangan yang sama juga memungkinkan Anda untuk menjalankan perintah dengan mudah dan efisien beberapa kali (misalnya untuk data beban curah)
Contoh Penggunaan:
var count = connection . Execute ( @"insert MyTable(colA, colB) values (@a, @b)" ,
new [ ] { new { a = 1 , b = 1 } , new { a = 2 , b = 2 } , new { a = 3 , b = 3 } }
) ;
Assert . Equal ( 3 , count ) ; // 3 rows inserted: "1,1", "2,2" and "3,3"
Contoh penggunaan lain saat Anda sudah memiliki koleksi yang ada:
var foos = new List < Foo >
{
{ new Foo { A = 1 , B = 1 } }
{ new Foo { A = 2 , B = 2 } }
{ new Foo { A = 3 , B = 3 } }
} ;
var count = connection . Execute ( @"insert MyTable(colA, colB) values (@a, @b)" , foos ) ;
Assert . Equal ( foos . Count , count ) ;
Ini berfungsi untuk parameter apa pun yang mengimplementasikan IEnumerable<T>
untuk beberapa T.
Fitur utama Dapper adalah kinerja. Metrik berikut menunjukkan berapa lama waktu yang dibutuhkan untuk menjalankan pernyataan SELECT
terhadap DB (dalam berbagai konfigurasi, masing -masing berlabel) dan memetakan data yang dikembalikan ke objek.
Tolok ukur dapat ditemukan di dapper.tests.performance (kontribusi selamat datang!) Dan dapat dijalankan melalui:
dotnet run --project . b enchmarks D apper.Tests.Performance -c Release -f net8.0 -- -f * --join
Output dari menjalankan terbaru adalah:
BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3693/22H2/2022Update)
Intel Core i7-3630QM CPU 2.40GHz (Ivy Bridge), 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.100
[Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX
ShortRun : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX
Orm | Metode | Kembali | Berarti | Stddev | Kesalahan | Gen0 | Gen1 | Gen2 | Dialokasikan |
---|---|---|---|---|---|---|---|---|---|
Dampak cache necis | ExecutEparameters_cache | Ruang kosong | 96.75 AS | 0.668 AS | 1.010 AS | 0.6250 | - | - | 2184 b |
Dampak cache necis | QueryFirstParameters_cache | Ruang kosong | 96.86 AS | 0.493 AS | 0.746 AS | 0.8750 | - | - | 2824 b |
Kode tangan | SQLCommand | Pos | 119.70 AS | 0.706 AS | 1.067 AS | 1.3750 | 1.0000 | 0.1250 | 7584 b |
Kode tangan | DataTable | dinamis | 126.64 AS | 1.239 AS | 1.873 AS | 3.0000 | - | - | 9576 b |
Sqlmarshal | SQLCommand | Pos | 132.36 AS | 1.008 AS | 1.523 AS | 2.0000 | 1.0000 | 0.2500 | 11529 b |
Rapi | Queryfirstordefault | Pos | 133.73 AS | 1.301 AS | 2.186 AS | 1.7500 | 1.5000 | - | 11608 b |
Kuat | Pertanyaan | dinamis | 133.92 AS | 1.075 AS | 1.806 AS | 2.0000 | 1.7500 | - | 12710 b |
Linq ke db | Pertanyaan | Pos | 134.24 AS | 1.068 AS | 1.614 AS | 1.7500 | 1.2500 | - | 10904 b |
Repodb | Executequery | Pos | 135.83 AS | 1.839 AS | 3.091 AS | 1.7500 | 1.5000 | - | 11649 b |
Rapi | 'Permintaan (buffered)' | Pos | 136.14 AS | 1.755 AS | 2.653 AS | 2.0000 | 1.5000 | - | 11888 b |
Kuat | Pertanyaan | Pos | 137.96 AS | 1.485 AS | 2.244 AS | 2.2500 | 1.2500 | - | 12201 b |
Rapi | Queryfirstordefault | dinamis | 139.04 AS | 1.507 AS | 2.279 AS | 3.5000 | - | - | 11648 b |
Kuat | SingleFromQuery | dinamis | 139.74 AS | 2.521 AS | 3.811 AS | 2.0000 | 1.7500 | - | 12710 b |
Rapi | 'Permintaan (buffered)' | dinamis | 140.13 AS | 1.382 AS | 2.090 AS | 2.0000 | 1.5000 | - | 11968 b |
Servicestack | SingleByid | Pos | 140.76 AS | 1.147 AS | 2.192 AS | 2.5000 | 1.2500 | 0.2500 | 15248 b |
Rapi | 'Contrib Get' | Pos | 141.09 AS | 1.394 AS | 2.108 AS | 2.0000 | 1.5000 | - | 12440 b |
Kuat | SingleFromQuery | Pos | 141.17 AS | 1.941 AS | 2.935 AS | 1.7500 | 1.5000 | - | 12201 b |
Besar sekali | 'Kueri (dinamis)' | dinamis | 142.01 AS | 4.957 AS | 7.494 AS | 2.0000 | 1.5000 | - | 12342 b |
Linq ke db | 'Pertama (dikompilasi)' | Pos | 144.59 AS | 1.295 AS | 1.958 AS | 1.7500 | 1.5000 | - | 12128 b |
Repodb | Queryfield | Pos | 148.31 AS | 1.742 AS | 2.633 AS | 2.0000 | 1.5000 | 0,5000 | 13938 b |
Norma | 'Baca <> (Tuples)' | ValueTuple`8 | 148.58 AS | 2.172 AS | 3.283 AS | 2.0000 | 1.7500 | - | 12745 b |
Norma | 'Baca <()> (bernama Tuples)' | ValueTuple`8 | 150.60 AS | 0.658 AS | 1.106 AS | 2.2500 | 2.0000 | 1.2500 | 14562 b |
Repodb | Pertanyaan | Pos | 152.34 AS | 2.164 AS | 3.271 AS | 2.2500 | 1.5000 | 0.2500 | 14106 b |
Repodb | Querydynamic | Pos | 154.15 AS | 4.108 AS | 6.210 AS | 2.2500 | 1.7500 | 0,5000 | 13930 b |
Repodb | Kueri di mana | Pos | 155.90 AS | 1.953 AS | 3.282 AS | 2.5000 | 0,5000 | - | 14858 b |
Dampak cache necis | Executenoparameters_nocache | Ruang kosong | 162.35 AS | 1.584 AS | 2.394 AS | - | - | - | 760 b |
Dampak cache necis | Executenoparameters_cache | Ruang kosong | 162.42 AS | 2.740 AS | 4.142 US | - | - | - | 760 b |
Dampak cache necis | Queryfirstnoparameters_cache | Ruang kosong | 164.35 AS | 1.206 AS | 1.824 AS | 0.2500 | - | - | 1520 b |
Devexpress.xpo | FindObject | Pos | 165.87 AS | 1.012 AS | 1.934 AS | 8.5000 | - | - | 28099 b |
Dampak cache necis | QueryFirstNoparameters_nocache | Ruang kosong | 173.87 AS | 1.178 AS | 1.781 AS | 0,5000 | - | - | 1576 b |
Linq ke db | Pertama | Pos | 175.21 AS | 2.292 AS | 3.851 AS | 2.0000 | 0,5000 | - | 14041 b |
EF 6 | SQLQUERY | Pos | 175.36 AS | 2.259 AS | 3.415 AS | 4.0000 | 0.7500 | - | 24209 b |
Norma | 'Baca <> (kelas)' | Pos | 186.37 AS | 1.305 AS | 2.496 AS | 3.0000 | 0,5000 | - | 17579 b |
Devexpress.xpo | GetObjectBykey | Pos | 186.78 AS | 3.407 AS | 5.151 AS | 4.5000 | 1.0000 | - | 30114 b |
Rapi | 'Kueri (tidak terikat)' | dinamis | 194.62 AS | 1.335 AS | 2.019 AS | 1.7500 | 1.5000 | - | 12048 b |
Rapi | 'Kueri (tidak terikat)' | Pos | 195.01 AS | 0.888 AS | 1.343 AS | 2.0000 | 1.5000 | - | 12008 b |
Devexpress.xpo | Pertanyaan | Pos | 199.46 AS | 5.500 AS | 9.243 AS | 10.0000 | - | - | 32083 b |
Beograd | Firstordefault | Tugas`1 | 228.70 AS | 2.181 AS | 3.665 AS | 4.5000 | 0,5000 | - | 20555 b |
EF Core | 'Pertama (dikompilasi)' | Pos | 265.45 AS | 17.745 AS | 26.828 AS | 2.0000 | - | - | 7521 b |
Nhibernate | Mendapatkan | Pos | 276.02 AS | 8.029 AS | 12.139 AS | 6.5000 | 1.0000 | - | 29885 b |
Nhibernate | HQL | Pos | 277.74 AS | 13.032 AS | 19.703 AS | 8.0000 | 1.0000 | - | 31886 b |
Nhibernate | Kriteria | Pos | 300.22 AS | 14.908 AS | 28.504 AS | 13.0000 | 1.0000 | - | 57562 b |
EF 6 | Pertama | Pos | 310.55 AS | 27.254 AS | 45.799 AS | 13.0000 | - | - | 43309 b |
EF Core | Pertama | Pos | 317.12 AS | 1.354 AS | 2.046 AS | 3.5000 | - | - | 11306 b |
EF Core | SQLQUERY | Pos | 322.34 AS | 23.990 AS | 40.314 AS | 5.0000 | - | - | 18195 b |
Nhibernate | SQL | Pos | 325.54 AS | 3.937 AS | 7.527 US | 22.0000 | 1.0000 | - | 80007 b |
EF 6 | 'Pertama (tidak ada pelacakan)' | Pos | 331.14 AS | 27.760 AS | 46.649 AS | 12.0000 | 1.0000 | - | 50237 b |
EF Core | 'Pertama (tidak ada pelacakan)' | Pos | 337.82 AS | 27.814 AS | 46.740 AS | 3.0000 | 1.0000 | - | 17986 b |
Nhibernate | Linq | Pos | 604.74 AS | 5.549 AS | 10.610 AS | 10.0000 | - | - | 46061 b |
Dampak cache necis | ExecutEparameters_nocache | Ruang kosong | 623.42 AS | 3.978 AS | 6.684 AS | 3.0000 | 2.0000 | - | 10001 b |
Dampak cache necis | QueryFirstParameters_nocache | Ruang kosong | 630.77 AS | 3.027 AS | 4.576 AS | 3.0000 | 2.0000 | - | 10640 b |
Jangan ragu untuk mengirimkan tambalan yang mencakup ORM lain - saat menjalankan tolok ukur, pastikan untuk dikompilasi dalam rilis dan tidak melampirkan debugger ( CTRL + F5 ).
Atau, Anda mungkin lebih suka rangkaian tes RawDataAccessBencher atau Ormbenchmark.
Parameter biasanya diteruskan sebagai kelas anonim. Ini memungkinkan Anda untuk memberi nama parameter Anda dengan mudah dan memberi Anda kemampuan untuk hanya memotong dan menempel cuplikan SQL dan menjalankannya di penganalisa kueri platform DB Anda.
new { A = 1 , B = " b " } // A will be mapped to the param @A, B to the param @B
Parameter juga dapat dibangun secara dinamis menggunakan kelas DynamicParameters. Ini memungkinkan untuk membangun pernyataan SQL yang dinamis sambil tetap menggunakan parameter untuk keselamatan dan kinerja.
var sqlPredicates = new List < string > ( ) ;
var queryParams = new DynamicParameters ( ) ;
if ( boolExpression )
{
sqlPredicates . Add ( " column1 = @param1 " ) ;
queryParams . Add ( " param1 " , dynamicValue1 , System . Data . DbType . Guid ) ;
} else {
sqlPredicates . Add ( " column2 = @param2 " ) ;
queryParams . Add ( " param2 " , dynamicValue2 , System . Data . DbType . String ) ;
}
DynamicParameters juga mendukung menyalin banyak parameter dari objek yang ada dari berbagai jenis.
var queryParams = new DynamicParameters ( objectOfType1 ) ;
queryParams . AddDynamicParams ( objectOfType2 ) ;
Ketika suatu objek yang mengimplementasikan antarmuka IDynamicParameters
yang diteruskan ke fungsi Execute
atau Query
, nilai parameter akan diekstraksi melalui antarmuka ini. Jelas, kelas objek yang paling mungkin digunakan untuk tujuan ini adalah kelas DynamicParameters
bawaan.
Dapper memungkinkan Anda untuk lulus di IEnumerable<int>
dan akan secara otomatis parameterisasi kueri Anda.
Misalnya:
connection . Query < int > ( " select * from (select 1 as Id union all select 2 union all select 3) as X where Id in @Ids " , new { Ids = new int [ ] { 1 , 2 , 3 } } ) ;
Akan diterjemahkan ke:
select * from ( select 1 as Id union all select 2 union all select 3 ) as X where Id in ( @Ids1 , @Ids2 , @Ids3 ) " // @Ids1 = 1 , @Ids2 = 2 , @Ids2 = 3
Dapper mendukung penggantian literal untuk jenis bool dan numerik.
connection . Query ( " select * from User where UserTypeId = {=Admin} " , new { UserTypeId . Admin } ) ;
Penggantian literal tidak dikirim sebagai parameter; Ini memungkinkan rencana yang lebih baik dan penggunaan indeks yang difilter tetapi biasanya harus digunakan dengan hemat dan setelah pengujian. Fitur ini sangat berguna ketika nilai yang disuntikkan sebenarnya adalah nilai tetap (misalnya, "ID kategori" tetap "," kode status "atau" wilayah "yang spesifik untuk kueri). Untuk data langsung di mana Anda mempertimbangkan literal, Anda mungkin juga ingin mempertimbangkan dan menguji petunjuk kueri khusus penyedia seperti OPTIMIZE FOR UNKNOWN
dengan parameter reguler.
Perilaku default Dapper adalah untuk menjalankan SQL Anda dan buffer seluruh pembaca saat kembali. Ini sangat ideal dalam banyak kasus karena meminimalkan kunci bersama di DB dan mengurangi waktu jaringan DB.
Namun ketika menjalankan kueri besar, Anda mungkin perlu meminimalkan jejak memori dan hanya memuat objek sesuai kebutuhan. Untuk melakukannya, buffered: false
ke dalam metode Query
.
Dapper memungkinkan Anda untuk memetakan satu baris ke beberapa objek. Ini adalah fitur utama jika Anda ingin menghindari kueri asing dan asosiasi beban yang bersemangat.
Contoh:
Pertimbangkan 2 Kelas: Post
dan User
class Post
{
public int Id { get ; set ; }
public string Title { get ; set ; }
public string Content { get ; set ; }
public User Owner { get ; set ; }
}
class User
{
public int Id { get ; set ; }
public string Name { get ; set ; }
}
Sekarang mari kita katakan bahwa kami ingin memetakan kueri yang bergabung dengan kedua posting dan tabel pengguna. Sampai sekarang jika kami perlu menggabungkan hasil dari 2 kueri, kami membutuhkan objek baru untuk mengekspresikannya tetapi lebih masuk akal dalam hal ini untuk menempatkan objek User
di dalam objek Post
.
Ini adalah kasus penggunaan untuk pemetaan multi. Anda memberi tahu Dapper bahwa kueri mengembalikan Post
dan objek User
dan kemudian memberikan fungsi yang menggambarkan apa yang ingin Anda lakukan dengan masing -masing baris yang berisi Post
dan objek User
. Dalam kasus kami, kami ingin mengambil objek pengguna dan memasukkannya ke dalam objek POST. Jadi kami menulis fungsinya:
( post , user ) => { post . Owner = user ; return post ; }
Argumen tipe 3 ke metode Query
menentukan objek apa yang harus digunakan Dapper untuk deserialisasi baris dan apa yang akan dikembalikan. Kami akan menafsirkan kedua baris sebagai kombinasi Post
dan User
dan kami mengembalikan objek Post
. Karenanya jenis deklarasi menjadi
< Post , User , Post >
Semuanya disatukan, terlihat seperti ini:
var sql =
@"select * from #Posts p
left join #Users u on u.Id = p.OwnerId
Order by p.Id" ;
var data = connection . Query < Post , User , Post > ( sql , ( post , user ) => { post . Owner = user ; return post ; } ) ;
var post = data . First ( ) ;
Assert . Equal ( " Sams Post1 " , post . Content ) ;
Assert . Equal ( 1 , post . Id ) ;
Assert . Equal ( " Sam " , post . Owner . Name ) ;
Assert . Equal ( 99 , post . Owner . Id ) ;
Dapper dapat membagi baris yang dikembalikan dengan membuat asumsi bahwa kolom ID Anda bernama Id
atau id
. Jika kunci utama Anda berbeda atau Anda ingin membagi baris pada titik selain Id
, gunakan parameter splitOn
opsional.
Dapper memungkinkan Anda untuk memproses beberapa kisi hasil dalam satu kueri.
Contoh:
var sql =
@"
select * from Customers where CustomerId = @id
select * from Orders where CustomerId = @id
select * from Returns where CustomerId = @id" ;
using ( var multi = connection . QueryMultiple ( sql , new { id = selectedId } ) )
{
var customer = multi . Read < Customer > ( ) . Single ( ) ;
var orders = multi . Read < Order > ( ) . ToList ( ) ;
var returns = multi . Read < Return > ( ) . ToList ( ) ;
.. .
}
Dapper sepenuhnya mendukung procs tersimpan:
var user = cnn . Query < User > ( " spGetUser " , new { Id = 1 } ,
commandType : CommandType . StoredProcedure ) . SingleOrDefault ( ) ;
Jika Anda menginginkan sesuatu yang lebih mewah, Anda dapat melakukannya:
var p = new DynamicParameters ( ) ;
p . Add ( " @a " , 11 ) ;
p . Add ( " @b " , dbType : DbType . Int32 , direction : ParameterDirection . Output ) ;
p . Add ( " @c " , dbType : DbType . Int32 , direction : ParameterDirection . ReturnValue ) ;
cnn . Execute ( " spMagicProc " , p , commandType : CommandType . StoredProcedure ) ;
int b = p . Get < int > ( " @b " ) ;
int c = p . Get < int > ( " @c " ) ;
Dapper mendukung varchar params, jika Anda menjalankan klausa di mana pada kolom varchar menggunakan param pastikan untuk meneruskannya dengan cara ini:
Query < Thing > ( " select * from Thing where Name = @Name " , new { Name = new DbString { Value = " abcde " , IsFixedLength = true , Length = 10 , IsAnsi = true } } ) ;
Di SQL Server, sangat penting untuk menggunakan Unicode saat menanyakan Unicode dan ANSI saat menanyakan Non Unicode.
Biasanya Anda ingin memperlakukan semua baris dari tabel yang diberikan sebagai tipe data yang sama. Namun, ada beberapa keadaan di mana berguna untuk dapat menguraikan baris yang berbeda sebagai tipe data yang berbeda. Di sinilah IDataReader.GetRowParser
berguna.
Bayangkan Anda memiliki tabel database bernama "bentuk" dengan kolom: Id
, Type
, dan Data
, dan Anda ingin menguraikan barisnya menjadi objek Circle
, Square
, atau Triangle
berdasarkan nilai kolom tipe.
var shapes = new List < IShape > ( ) ;
using ( var reader = connection . ExecuteReader ( " select * from Shapes " ) )
{
// Generate a row parser for each type you expect.
// The generic type <IShape> is what the parser will return.
// The argument (typeof(*)) is the concrete type to parse.
var circleParser = reader . GetRowParser < IShape > ( typeof ( Circle ) ) ;
var squareParser = reader . GetRowParser < IShape > ( typeof ( Square ) ) ;
var triangleParser = reader . GetRowParser < IShape > ( typeof ( Triangle ) ) ;
var typeColumnIndex = reader . GetOrdinal ( " Type " ) ;
while ( reader . Read ( ) )
{
IShape shape ;
var type = ( ShapeType ) reader . GetInt32 ( typeColumnIndex ) ;
switch ( type )
{
case ShapeType . Circle :
shape = circleParser ( reader ) ;
break ;
case ShapeType . Square :
shape = squareParser ( reader ) ;
break ;
case ShapeType . Triangle :
shape = triangleParser ( reader ) ;
break ;
default :
throw new NotImplementedException ( ) ;
}
shapes . Add ( shape ) ;
}
}
Untuk menggunakan variabel SQL non-parameter dengan konektor MySQL, Anda harus menambahkan opsi berikut ke string koneksi Anda:
Allow User Variables=True
Pastikan Anda tidak menyediakan properti untuk memetakan.
Dapper menyimpan informasi tentang setiap kueri yang dijalankannya, ini memungkinkannya untuk mewujudkan objek dengan cepat dan memproses parameter dengan cepat. Implementasi saat ini menyimpan informasi ini dalam objek ConcurrentDictionary
. Pernyataan yang hanya digunakan sekali secara rutin memerah dari cache ini. Namun, jika Anda menghasilkan string SQL dengan cepat tanpa menggunakan parameter, ada kemungkinan Anda dapat mencapai masalah memori.
Kesederhanaan Dapper berarti bahwa banyak fitur yang dikirim oleh ORMS dilucuti. Ini mengkhawatirkan skenario 95%, dan memberi Anda alat yang Anda butuhkan sebagian besar waktu. Itu tidak berusaha menyelesaikan setiap masalah.
Dapper tidak memiliki detail implementasi spesifik DB, ia bekerja di semua penyedia .NET ADO termasuk SQLite, SQL CE, Firebird, Oracle, Mariadb, MySQL, PostgreSQL dan SQL Server.
Dapper memiliki rangkaian uji komprehensif dalam proyek pengujian.
Dapper digunakan di produksi di Stack Overflow.