1+ using System . Data ;
2+
13using BenchmarkDotNet . Attributes ;
24
35using DotNet . Testcontainers . Containers ;
46
57using EFCore . BulkExtensions ;
68
9+ using Microsoft . Data . SqlClient ;
10+ using Microsoft . Data . Sqlite ;
11+ using Microsoft . EntityFrameworkCore ;
12+
13+ using Npgsql ;
14+
715using PhenX . EntityFrameworkCore . BulkInsert . Extensions ;
816
917namespace PhenX . EntityFrameworkCore . BulkInsert . Benchmark ;
1018
1119public abstract class LibComparator
1220{
13- [ Params ( 100_000 /*, 1_000_000/*, 10_000_000*/ ) ]
21+ [ Params ( 500_000 /*, 1_000_000/*, 10_000_000*/ ) ]
1422 public int N ;
1523
1624 private IList < TestEntity > data = [ ] ;
@@ -54,37 +62,45 @@ public async Task PhenX_EntityFrameworkCore_BulkInsert()
5462 {
5563 await DbContext . ExecuteInsertAsync ( data ) ;
5664 }
57-
58- [ Benchmark ]
59- public void PhenX_EntityFrameworkCore_BulkInsert_Sync ( )
60- {
61- DbContext . ExecuteInsert ( data ) ;
62- }
6365 //
6466 // [Benchmark]
65- // public async Task PhenX_EntityFrameworkCore_BulkInsertWithIdentity ()
67+ // public void PhenX_EntityFrameworkCore_BulkInsert_Sync ()
6668 // {
67- // await DbContext.ExecuteInsertWithIdentityAsync(data);
68- // }
69- //
70- // [Benchmark]
71- // public async Task PhenX_EntityFrameworkCore_BulkInsertWithIdentityMoveRows()
72- // {
73- // await DbContext.ExecuteInsertWithIdentityAsync(data, options => options.MoveRows = true);
69+ // DbContext.ExecuteInsert(data);
7470 // }
7571
7672 [ Benchmark ]
77- public async Task Z_EntityFramework_Extensions_EFCore ( )
73+ public void RawInsert ( )
7874 {
79- await DbContext . BulkInsertOptimizedAsync ( data , options => options . IncludeGraph = false ) ;
75+ if ( DbContext . Database . ProviderName ! . Contains ( "SqlServer" , StringComparison . InvariantCultureIgnoreCase ) )
76+ {
77+ // Use SqlBulkCopy for SQL Server
78+ RawInsertSqlServer ( ) ;
79+ }
80+ else if ( DbContext . Database . ProviderName ! . Contains ( "Sqlite" , StringComparison . InvariantCultureIgnoreCase ) )
81+ {
82+ // Use raw sql insert statements for SQLite
83+ RawInsertSqlite ( ) ;
84+ }
85+ else if ( DbContext . Database . ProviderName ! . Contains ( "Npgsql" , StringComparison . InvariantCultureIgnoreCase ) )
86+ {
87+ // Use BeginBinaryImport for PostgreSQL
88+ RawInsertPostgreSql ( ) ;
89+ }
8090 }
8191
8292 [ Benchmark ]
83- public void Z_EntityFramework_Extensions_EFCore_Sync ( )
93+ public async Task Z_EntityFramework_Extensions_EFCore ( )
8494 {
85- DbContext . BulkInsertOptimized ( data , options => options . IncludeGraph = false ) ;
95+ await DbContext . BulkInsertOptimizedAsync ( data , options => options . IncludeGraph = false ) ;
8696 }
8797
98+ // [Benchmark]
99+ // public void Z_EntityFramework_Extensions_EFCore_Sync()
100+ // {
101+ // DbContext.BulkInsertOptimized(data, options => options.IncludeGraph = false);
102+ // }
103+
88104 [ Benchmark ]
89105 public async Task EFCore_BulkExtensions ( )
90106 {
@@ -95,20 +111,157 @@ await DbContext.BulkInsertAsync(data, options =>
95111 } ) ;
96112 }
97113
114+ // [Benchmark]
115+ // public void EFCore_BulkExtensions_Sync()
116+ // {
117+ // DbContext.BulkInsert(data, options =>
118+ // {
119+ // options.IncludeGraph = false;
120+ // options.PreserveInsertOrder = false;
121+ // });
122+ // }
123+
98124 [ Benchmark ]
99- public void EFCore_BulkExtensions_Sync ( )
125+ public async Task EFCore_SaveChanges ( )
100126 {
101- DbContext . BulkInsert ( data , options =>
127+ DbContext . AddRange ( data ) ;
128+ await DbContext . SaveChangesAsync ( ) ;
129+ }
130+
131+ private void RawInsertPostgreSql ( )
132+ {
133+ using var connection = ( NpgsqlConnection ) DbContext . Database . GetDbConnection ( ) ;
134+ if ( connection . State != ConnectionState . Open )
102135 {
103- options . IncludeGraph = false ;
104- options . PreserveInsertOrder = false ;
105- } ) ;
136+ connection . Open ( ) ;
137+ }
138+
139+ const string copyCommand = $ """
140+ COPY "{ nameof ( TestEntity ) } " (
141+ "Name",
142+ "Price",
143+ "Identifier",
144+ "CreatedAt",
145+ "UpdatedAt",
146+ "StringEnumValue",
147+ "NumericEnumValue"
148+ ) FROM STDIN (FORMAT BINARY)
149+ """ ;
150+
151+ using var writer = connection . BeginBinaryImport ( copyCommand ) ;
152+ foreach ( var entity in data )
153+ {
154+ writer . StartRow ( ) ;
155+ writer . Write ( entity . Name ) ;
156+ writer . Write ( entity . Price ) ;
157+ writer . Write ( entity . Identifier ) ;
158+ writer . Write ( entity . CreatedAt ) ;
159+ writer . Write ( entity . UpdatedAt ) ;
160+ writer . Write ( entity . StringEnumValue . ToString ( ) ) ;
161+ writer . Write ( ( int ) entity . NumericEnumValue ) ;
162+ }
163+
164+ writer . Complete ( ) ;
106165 }
107166
108- // [Benchmark]
109- // public async Task EFCore_SaveChanges()
110- // {
111- // DbContext.AddRange(data);
112- // await DbContext.SaveChangesAsync();
113- // }
167+ private void RawInsertSqlite ( )
168+ {
169+ var connection = ( SqliteConnection ) DbContext . Database . GetDbConnection ( ) ;
170+ if ( connection . State != ConnectionState . Open )
171+ {
172+ connection . Open ( ) ;
173+ }
174+ using var transaction = connection . BeginTransaction ( ) ;
175+ using var command = connection . CreateCommand ( ) ;
176+ command . CommandText = $ """
177+ INSERT INTO "{ nameof ( TestEntity ) } " (
178+ "Name",
179+ "Price",
180+ "Identifier",
181+ "CreatedAt",
182+ "UpdatedAt",
183+ "StringEnumValue",
184+ "NumericEnumValue"
185+ ) VALUES (@Name, @Price, @Identifier, @CreatedAt, @UpdatedAt, @StringEnumValue, @NumericEnumValue)
186+ """ ;
187+
188+ command . Parameters . Add ( new SqliteParameter ( "@Name" , DbType . String ) ) ;
189+ command . Parameters . Add ( new SqliteParameter ( "@Price" , DbType . Decimal ) ) ;
190+ command . Parameters . Add ( new SqliteParameter ( "@Identifier" , DbType . Guid ) ) ;
191+ command . Parameters . Add ( new SqliteParameter ( "@CreatedAt" , DbType . DateTime ) ) ;
192+ command . Parameters . Add ( new SqliteParameter ( "@UpdatedAt" , DbType . DateTime2 ) ) ;
193+ command . Parameters . Add ( new SqliteParameter ( "@StringEnumValue" , DbType . String ) ) ;
194+ command . Parameters . Add ( new SqliteParameter ( "@NumericEnumValue" , DbType . Int32 ) ) ;
195+
196+ foreach ( var entity in data )
197+ {
198+ command . Parameters [ "@Name" ] . Value = entity . Name ;
199+ command . Parameters [ "@Price" ] . Value = entity . Price ;
200+ command . Parameters [ "@Identifier" ] . Value = entity . Identifier ;
201+ command . Parameters [ "@CreatedAt" ] . Value = entity . CreatedAt ;
202+ command . Parameters [ "@UpdatedAt" ] . Value = entity . UpdatedAt ;
203+ command . Parameters [ "@StringEnumValue" ] . Value = entity . StringEnumValue . ToString ( ) ;
204+ command . Parameters [ "@NumericEnumValue" ] . Value = ( int ) entity . NumericEnumValue ;
205+
206+ command . ExecuteNonQuery ( ) ;
207+ }
208+
209+ transaction . Commit ( ) ;
210+ }
211+
212+ private void RawInsertSqlServer ( )
213+ {
214+ var connection = ( SqlConnection ) DbContext . Database . GetDbConnection ( ) ;
215+ if ( connection . State != ConnectionState . Open )
216+ {
217+ connection . Open ( ) ;
218+ }
219+
220+ using var bulkCopy = new SqlBulkCopy ( connection ) ;
221+
222+ bulkCopy . DestinationTableName = nameof ( TestEntity ) ;
223+ bulkCopy . BatchSize = 50_000 ;
224+ bulkCopy . BulkCopyTimeout = 60 ;
225+
226+ bulkCopy . ColumnMappings . Add ( "Name" , "Name" ) ;
227+ bulkCopy . ColumnMappings . Add ( "Price" , "Price" ) ;
228+ bulkCopy . ColumnMappings . Add ( "Identifier" , "Identifier" ) ;
229+ bulkCopy . ColumnMappings . Add ( "CreatedAt" , "CreatedAt" ) ;
230+ bulkCopy . ColumnMappings . Add ( "UpdatedAt" , "UpdatedAt" ) ;
231+ bulkCopy . ColumnMappings . Add ( "StringEnumValue" , "StringEnumValue" ) ;
232+ bulkCopy . ColumnMappings . Add ( "NumericEnumValue" , "NumericEnumValue" ) ;
233+
234+ var dataTable = new DataTable ( ) ;
235+ dataTable . Columns . Add ( "Name" , typeof ( string ) ) ;
236+ dataTable . Columns . Add ( "Price" , typeof ( decimal ) ) ;
237+ dataTable . Columns . Add ( "Identifier" , typeof ( Guid ) ) ;
238+ dataTable . Columns . Add ( "CreatedAt" , typeof ( DateTime ) ) ;
239+ dataTable . Columns . Add ( "UpdatedAt" , typeof ( DateTimeOffset ) ) ;
240+ dataTable . Columns . Add ( "StringEnumValue" , typeof ( string ) ) ;
241+ dataTable . Columns . Add ( "NumericEnumValue" , typeof ( int ) ) ;
242+
243+ foreach ( var entity in data )
244+ {
245+ var row = dataTable . NewRow ( ) ;
246+ row [ "Name" ] = entity . Name ;
247+ row [ "Price" ] = entity . Price ;
248+ row [ "Identifier" ] = entity . Identifier ;
249+ row [ "CreatedAt" ] = entity . CreatedAt ;
250+ row [ "UpdatedAt" ] = entity . UpdatedAt ;
251+ row [ "StringEnumValue" ] = entity . StringEnumValue . ToString ( ) ;
252+ row [ "NumericEnumValue" ] = ( int ) entity . NumericEnumValue ;
253+ dataTable . Rows . Add ( row ) ;
254+
255+ if ( dataTable . Rows . Count >= 50_000 )
256+ {
257+ bulkCopy . WriteToServer ( dataTable ) ;
258+ dataTable . Clear ( ) ;
259+ }
260+ }
261+
262+ if ( dataTable . Rows . Count > 0 )
263+ {
264+ bulkCopy . WriteToServer ( dataTable ) ;
265+ }
266+ }
114267}
0 commit comments