1+ using System . Collections . Concurrent ;
12using System . Diagnostics . CodeAnalysis ;
3+ using System . Linq . Expressions ;
4+ using System . Reflection ;
25using Microsoft . Data . SqlClient ;
36using Microsoft . EntityFrameworkCore . Infrastructure ;
7+ using Microsoft . EntityFrameworkCore . Metadata ;
48using Microsoft . EntityFrameworkCore . Metadata . Internal ;
59using Microsoft . EntityFrameworkCore . Migrations ;
610using Microsoft . EntityFrameworkCore . Storage ;
11+ using Thinktecture . EntityFrameworkCore . Parameters ;
12+ using Thinktecture . EntityFrameworkCore . TempTables ;
713
814namespace Thinktecture . EntityFrameworkCore . Testing ;
915
@@ -36,11 +42,30 @@ public interface ITestIsolationOptions
3642 public static readonly ITestIsolationOptions CleanupOnly = new CleanupDatabase ( ) ;
3743
3844 /// <summary>
39- /// Deletes all records from the tables.
45+ /// Deletes all records from the tables using "TRUNCATE" .
4046 /// No ambient transaction; no unique schema.
4147 /// </summary>
4248 public static readonly ITestIsolationOptions TruncateTables = new TruncateAllTables ( ) ;
4349
50+ /// <summary>
51+ /// Deletes all records from the tables using "DELETE".
52+ /// No ambient transaction; no unique schema.
53+ /// </summary>
54+ public static ITestIsolationOptions DeleteData ( Predicate < IEntityType > ? filter = null )
55+ {
56+ return new DeleteAllData ( filter is null
57+ ? SkipTempTablesAndCollectionParameters
58+ : entityType => filter ( entityType ) && SkipTempTablesAndCollectionParameters ( entityType ) ) ;
59+ }
60+
61+ private static bool SkipTempTablesAndCollectionParameters ( IEntityType entityType )
62+ {
63+ return ! entityType . ClrType . IsGenericType
64+ || ( entityType . ClrType . GetGenericTypeDefinition ( ) != typeof ( TempTable < > )
65+ && entityType . ClrType . GetGenericTypeDefinition ( ) != typeof ( TempTable < , > )
66+ && entityType . ClrType . GetGenericTypeDefinition ( ) != typeof ( ScalarCollectionParameter < > ) ) ;
67+ }
68+
4469 /// <summary>
4570 /// Performs custom cleanup.
4671 /// </summary>
@@ -144,13 +169,13 @@ public async ValueTask CleanupAsync(DbContext dbContext, string? schema, Cancell
144169 }
145170 }
146171
147- [ SuppressMessage ( "Usage" , "EF1001:Internal EF Core API usage." ) ]
148172 private class TruncateAllTables : ITestIsolationOptions
149173 {
150174 public bool NeedsAmbientTransaction => false ;
151175 public bool NeedsUniqueSchema => false ;
152176 public bool NeedsCleanup => true ;
153177
178+ [ SuppressMessage ( "Usage" , "EF1001:Internal EF Core API usage." ) ]
154179 public async ValueTask CleanupAsync ( DbContext dbContext , string ? schema , CancellationToken cancellationToken )
155180 {
156181 foreach ( var entityType in dbContext . Model . GetEntityTypesInHierarchicalOrder ( ) . Reverse ( ) )
@@ -161,6 +186,57 @@ public async ValueTask CleanupAsync(DbContext dbContext, string? schema, Cancell
161186 }
162187 }
163188
189+ private class DeleteAllData : ITestIsolationOptions
190+ {
191+ private readonly Predicate < IEntityType > _filter ;
192+
193+ private static readonly MethodInfo _deleteData = typeof ( DeleteAllData ) . GetMethod ( nameof ( DeleteDataAsync ) , BindingFlags . Static | BindingFlags . NonPublic )
194+ ?? throw new Exception ( $ "Method '{ nameof ( DeleteDataAsync ) } ' not found.") ;
195+
196+ private readonly ConcurrentDictionary < Type , Func < DbContext , CancellationToken , Task > > _deleteDelegatesLookup ;
197+
198+ public bool NeedsAmbientTransaction => false ;
199+ public bool NeedsUniqueSchema => false ;
200+ public bool NeedsCleanup => true ;
201+
202+ public DeleteAllData ( Predicate < IEntityType > filter )
203+ {
204+ _filter = filter ;
205+ _deleteDelegatesLookup = new ConcurrentDictionary < Type , Func < DbContext , CancellationToken , Task > > ( ) ;
206+ }
207+
208+ [ SuppressMessage ( "Usage" , "EF1001:Internal EF Core API usage." ) ]
209+ public async ValueTask CleanupAsync ( DbContext dbContext , string ? schema , CancellationToken cancellationToken )
210+ {
211+ foreach ( var entityType in dbContext . Model . GetEntityTypesInHierarchicalOrder ( ) . Reverse ( ) )
212+ {
213+ if ( entityType . GetTableName ( ) is not null && _filter ( entityType ) )
214+ {
215+ var delete = _deleteDelegatesLookup . GetOrAdd ( entityType . ClrType , CreateDelegate ) ;
216+
217+ await delete ( dbContext , cancellationToken ) ;
218+ }
219+ }
220+ }
221+
222+ private Func < DbContext , CancellationToken , Task > CreateDelegate ( Type type )
223+ {
224+ var ctxParam = Expression . Parameter ( typeof ( DbContext ) ) ;
225+ var cancellationTokenParam = Expression . Parameter ( typeof ( CancellationToken ) ) ;
226+ var method = _deleteData . MakeGenericMethod ( type ) ;
227+
228+ var call = Expression . Call ( method , ctxParam , cancellationTokenParam ) ;
229+
230+ return Expression . Lambda < Func < DbContext , CancellationToken , Task > > ( call , ctxParam , cancellationTokenParam ) . Compile ( ) ;
231+ }
232+
233+ private static async Task DeleteDataAsync < T > ( DbContext dbContext , CancellationToken cancellationToken )
234+ where T : class
235+ {
236+ await dbContext . Set < T > ( ) . ExecuteDeleteAsync ( cancellationToken ) ;
237+ }
238+ }
239+
164240 /// <summary>
165241 /// Rollbacks all migrations.
166242 /// </summary>
0 commit comments