/EFCore.BulkExtensions.Tests/EFCoreBulkTestAsync.cs

https://github.com/borisdj/EFCore.BulkExtensions · C# · 343 lines · 295 code · 36 blank · 12 comment · 19 complexity · ebadf3a39a86848cb5aaff160d897c28 MD5 · raw file

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. using Microsoft.Data.Sqlite;
  6. using Microsoft.EntityFrameworkCore;
  7. using Microsoft.EntityFrameworkCore.Infrastructure;
  8. using Microsoft.EntityFrameworkCore.Storage;
  9. using Xunit;
  10. namespace EFCore.BulkExtensions.Tests
  11. {
  12. public class EFCoreBulkTestAsync
  13. {
  14. protected int EntitiesNumber => 100000;
  15. private static Func<TestContext, int> ItemsCountQuery = EF.CompileQuery<TestContext, int>(ctx => ctx.Items.Count());
  16. private static Func<TestContext, Item> LastItemQuery = EF.CompileQuery<TestContext, Item>(ctx => ctx.Items.LastOrDefault());
  17. private static Func<TestContext, IEnumerable<Item>> AllItemsQuery = EF.CompileQuery<TestContext, IEnumerable<Item>>(ctx => ctx.Items.AsNoTracking());
  18. [Theory]
  19. [InlineData(DbServer.SqlServer, true)]
  20. [InlineData(DbServer.Sqlite, true)]
  21. //[InlineData(DatabaseType.SqlServer, false)] // for speed comparison with Regular EF CUD operations
  22. public async Task OperationsTestAsync(DbServer databaseType, bool isBulkOperation)
  23. {
  24. ContextUtil.DbServer = databaseType;
  25. await new EFCoreBatchTestAsync().RunDeleteAllAsync(databaseType); // TODO
  26. // Test can be run individually by commenting others and running each separately in order one after another
  27. await RunInsertAsync(isBulkOperation);
  28. await RunInsertOrUpdateAsync(isBulkOperation);
  29. await RunUpdateAsync(isBulkOperation);
  30. if (databaseType == DbServer.SqlServer)
  31. {
  32. await RunReadAsync(isBulkOperation); // Not Yet supported for Sqlite
  33. }
  34. await RunDeleteAsync(isBulkOperation, databaseType);
  35. }
  36. [Theory]
  37. [InlineData(DbServer.SqlServer)]
  38. [InlineData(DbServer.Sqlite)]
  39. public async Task SideEffectsTestAsync(DbServer databaseType)
  40. {
  41. await BulkOperationShouldNotCloseOpenConnectionAsync(databaseType, context => context.BulkInsertAsync(new[] { new Item() }));
  42. await BulkOperationShouldNotCloseOpenConnectionAsync(databaseType, context => context.BulkUpdateAsync(new[] { new Item() }));
  43. }
  44. private static async Task BulkOperationShouldNotCloseOpenConnectionAsync(
  45. DbServer databaseType,
  46. Func<TestContext, Task> bulkOperation)
  47. {
  48. ContextUtil.DbServer = databaseType;
  49. using (var context = new TestContext(ContextUtil.GetOptions()))
  50. {
  51. var sqlHelper = context.GetService<ISqlGenerationHelper>();
  52. await context.Database.OpenConnectionAsync();
  53. try
  54. {
  55. // we use a temp table to verify whether the connection has been closed (and re-opened) inside BulkUpdate(Async)
  56. var columnName = sqlHelper.DelimitIdentifier("Id");
  57. var tableName = sqlHelper.DelimitIdentifier("#MyTempTable");
  58. var createTableSql = $" TABLE {tableName} ({columnName} INTEGER);";
  59. switch (databaseType)
  60. {
  61. case DbServer.Sqlite:
  62. createTableSql = $"CREATE TEMPORARY {createTableSql}";
  63. break;
  64. case DbServer.SqlServer:
  65. createTableSql = $"CREATE {createTableSql}";
  66. break;
  67. default:
  68. throw new ArgumentException($"Unknown database type: '{databaseType}'.", nameof(databaseType));
  69. }
  70. await context.Database.ExecuteSqlRawAsync(createTableSql);
  71. await bulkOperation(context);
  72. await context.Database.ExecuteSqlRawAsync($"SELECT {columnName} FROM {tableName}");
  73. }
  74. finally
  75. {
  76. await context.Database.CloseConnectionAsync();
  77. }
  78. }
  79. }
  80. private async Task RunInsertAsync(bool isBulkOperation)
  81. {
  82. using (var context = new TestContext(ContextUtil.GetOptions()))
  83. {
  84. var entities = new List<Item>();
  85. var subEntities = new List<ItemHistory>();
  86. for (int i = 1; i < EntitiesNumber; i++)
  87. {
  88. var entity = new Item
  89. {
  90. ItemId = isBulkOperation ? i : 0,
  91. Name = "name " + i,
  92. Description = "info " + Guid.NewGuid().ToString().Substring(0, 3),
  93. Quantity = i % 10,
  94. Price = i / (i % 5 + 1),
  95. TimeUpdated = DateTime.Now,
  96. ItemHistories = new List<ItemHistory>()
  97. };
  98. var subEntity1 = new ItemHistory
  99. {
  100. ItemHistoryId = SeqGuid.Create(),
  101. Remark = $"some more info {i}.1"
  102. };
  103. var subEntity2 = new ItemHistory
  104. {
  105. ItemHistoryId = SeqGuid.Create(),
  106. Remark = $"some more info {i}.2"
  107. };
  108. entity.ItemHistories.Add(subEntity1);
  109. entity.ItemHistories.Add(subEntity2);
  110. entities.Add(entity);
  111. }
  112. if (isBulkOperation)
  113. {
  114. if (ContextUtil.DbServer == DbServer.SqlServer)
  115. {
  116. using (var transaction = await context.Database.BeginTransactionAsync())
  117. {
  118. await context.BulkInsertAsync(entities, new BulkConfig { PreserveInsertOrder = true, SetOutputIdentity = true, BatchSize = 4000 });
  119. foreach (var entity in entities)
  120. {
  121. foreach (var subEntity in entity.ItemHistories)
  122. {
  123. subEntity.ItemId = entity.ItemId; // setting FK to match its linked PK that was generated in DB
  124. }
  125. subEntities.AddRange(entity.ItemHistories);
  126. }
  127. await context.BulkInsertAsync(subEntities);
  128. transaction.Commit();
  129. }
  130. }
  131. else if (ContextUtil.DbServer == DbServer.Sqlite)
  132. {
  133. using (var transaction = context.Database.BeginTransaction())
  134. {
  135. var bulkConfig = new BulkConfig()
  136. {
  137. SetOutputIdentity = true,
  138. };
  139. await context.BulkInsertAsync(entities, bulkConfig);
  140. foreach (var entity in entities)
  141. {
  142. foreach (var subEntity in entity.ItemHistories)
  143. {
  144. subEntity.ItemId = entity.ItemId; // setting FK to match its linked PK that was generated in DB
  145. }
  146. subEntities.AddRange(entity.ItemHistories);
  147. }
  148. await context.BulkInsertAsync(subEntities, bulkConfig);
  149. transaction.Commit();
  150. }
  151. }
  152. }
  153. else
  154. {
  155. await context.Items.AddRangeAsync(entities);
  156. await context.SaveChangesAsync();
  157. }
  158. }
  159. using (var context = new TestContext(ContextUtil.GetOptions()))
  160. {
  161. //int entitiesCount = ItemsCountQuery(context);
  162. int entitiesCount = await context.Items.CountAsync();
  163. //Item lastEntity = LastItemQuery(context);
  164. Item lastEntity = context.Items.OrderByDescending(a => a.ItemId).FirstOrDefault();
  165. Assert.Equal(EntitiesNumber - 1, entitiesCount);
  166. Assert.NotNull(lastEntity);
  167. Assert.Equal("name " + (EntitiesNumber - 1), lastEntity.Name);
  168. }
  169. }
  170. private async Task RunInsertOrUpdateAsync(bool isBulkOperation)
  171. {
  172. using (var context = new TestContext(ContextUtil.GetOptions()))
  173. {
  174. var entities = new List<Item>();
  175. var dateTimeNow = DateTime.Now;
  176. for (int i = 2; i <= EntitiesNumber; i += 2)
  177. {
  178. entities.Add(new Item
  179. {
  180. ItemId = i,
  181. Name = "name InsertOrUpdate " + i,
  182. Description = "info",
  183. Quantity = i,
  184. Price = i / (i % 5 + 1),
  185. TimeUpdated = dateTimeNow
  186. });
  187. }
  188. if (isBulkOperation)
  189. {
  190. var bulkConfig = new BulkConfig() { SetOutputIdentity = true, CalculateStats = true };
  191. await context.BulkInsertOrUpdateAsync(entities, bulkConfig);
  192. }
  193. else
  194. {
  195. await context.Items.AddRangeAsync(entities);
  196. await context.SaveChangesAsync();
  197. }
  198. }
  199. using (var context = new TestContext(ContextUtil.GetOptions()))
  200. {
  201. //int entitiesCount = ItemsCountQuery(context);
  202. int entitiesCount = await context.Items.CountAsync();
  203. //Item lastEntity = LastItemQuery(context);
  204. Item lastEntity = context.Items.OrderByDescending(a => a.ItemId).FirstOrDefault();
  205. Assert.Equal(EntitiesNumber, entitiesCount);
  206. Assert.NotNull(lastEntity);
  207. Assert.Equal("name InsertOrUpdate " + EntitiesNumber, lastEntity.Name);
  208. }
  209. }
  210. private async Task RunUpdateAsync(bool isBulkOperation)
  211. {
  212. using (var context = new TestContext(ContextUtil.GetOptions()))
  213. {
  214. int counter = 1;
  215. var entities = AllItemsQuery(context).ToList();
  216. foreach (var entity in entities)
  217. {
  218. entity.Description = "Desc Update " + counter++;
  219. entity.TimeUpdated = DateTime.Now;
  220. }
  221. if (isBulkOperation)
  222. {
  223. var bulkConfig = new BulkConfig() { SetOutputIdentity = true, CalculateStats = true };
  224. await context.BulkUpdateAsync(entities, bulkConfig);
  225. }
  226. else
  227. {
  228. context.Items.UpdateRange(entities);
  229. await context.SaveChangesAsync();
  230. }
  231. }
  232. using (var context = new TestContext(ContextUtil.GetOptions()))
  233. {
  234. //int entitiesCount = ItemsCountQuery(context);
  235. int entitiesCount = await context.Items.CountAsync();
  236. //Item lastEntity = LastItemQuery(context);
  237. Item lastEntity = context.Items.OrderByDescending(a => a.ItemId).FirstOrDefault();
  238. Assert.Equal(EntitiesNumber, entitiesCount);
  239. Assert.NotNull(lastEntity);
  240. Assert.Equal("Desc Update " + EntitiesNumber, lastEntity.Description);
  241. }
  242. }
  243. private async Task RunReadAsync(bool isBulkOperation)
  244. {
  245. using (var context = new TestContext(ContextUtil.GetOptions()))
  246. {
  247. var entities = new List<Item>();
  248. for (int i = 1; i < EntitiesNumber; i++)
  249. {
  250. var entity = new Item
  251. {
  252. Name = "name " + i,
  253. };
  254. entities.Add(entity);
  255. }
  256. await context.BulkReadAsync(
  257. entities,
  258. new BulkConfig
  259. {
  260. UpdateByProperties = new List<string> { nameof(Item.Name) }
  261. }
  262. );
  263. Assert.Equal(1, entities[0].ItemId);
  264. Assert.Equal(0, entities[1].ItemId);
  265. Assert.Equal(3, entities[2].ItemId);
  266. Assert.Equal(0, entities[3].ItemId);
  267. }
  268. }
  269. private async Task RunDeleteAsync(bool isBulkOperation, DbServer databaseType)
  270. {
  271. using (var context = new TestContext(ContextUtil.GetOptions()))
  272. {
  273. var entities = AllItemsQuery(context).ToList();
  274. // ItemHistories will also be deleted because of Relationship - ItemId (Delete Rule: Cascade)
  275. if (isBulkOperation)
  276. {
  277. await context.BulkDeleteAsync(entities);
  278. }
  279. else
  280. {
  281. context.Items.RemoveRange(entities);
  282. await context.SaveChangesAsync();
  283. }
  284. }
  285. using (var context = new TestContext(ContextUtil.GetOptions()))
  286. {
  287. //int entitiesCount = ItemsCountQuery(context);
  288. int entitiesCount = await context.Items.CountAsync();
  289. //Item lastEntity = LastItemQuery(context);
  290. Item lastEntity = context.Items.OrderByDescending(a => a.ItemId).FirstOrDefault();
  291. Assert.Equal(0, entitiesCount);
  292. Assert.Null(lastEntity);
  293. }
  294. using (var context = new TestContext(ContextUtil.GetOptions()))
  295. {
  296. if (databaseType == DbServer.SqlServer)
  297. {
  298. await context.Database.ExecuteSqlRawAsync("DBCC CHECKIDENT('[dbo].[Item]', RESEED, 0);").ConfigureAwait(false);
  299. }
  300. if (databaseType == DbServer.Sqlite)
  301. {
  302. await context.Database.ExecuteSqlRawAsync("DELETE FROM sqlite_sequence WHERE name = 'Item';").ConfigureAwait(false);
  303. }
  304. }
  305. }
  306. }
  307. }