diff --git a/readme.md b/readme.md index ee55c2cc..2251ea9d 100644 --- a/readme.md +++ b/readme.md @@ -81,7 +81,7 @@ Recording allows all commands executed by EF to be captured and then (optionally ### Enable -Call `EfRecording.EnableRecording()` on `DbContextOptionsBuilder`. +Call `EnableRecording()` on `DbContextOptionsBuilder`. @@ -99,7 +99,7 @@ var data = new SampleDbContext(builder.Options); ### Usage -To start recording call `EfRecording.StartRecording()`. The results will be automatically included in verified file. +To start recording call `Recording.Start()`. The results will be automatically included in verified file. @@ -144,7 +144,7 @@ where c.Name = N'Title' -Sql entries can be explicitly read using `EfRecording.FinishRecording`, optionally filtered, and passed to Verify: +Sql entries can be explicitly read using `Recording.Stop()`, optionally filtered, and passed to Verify: @@ -178,7 +178,7 @@ await Verify( ### DbContext spanning -`StartRecording` can be called on different DbContext instances (built from the same options) and the results will be aggregated. +`Recording.Start()` can be called on different DbContext instances (built from the same options) and the results will be aggregated. @@ -291,6 +291,26 @@ where c.Name = N'Title' +### Disabling Recording globally + +Recording is attached by `EnableRecording()`. Pass `recordCommands: false` to `Initialize` to leave that interceptor unattached, which makes every subsequent `EnableRecording()` call a no-op: + + + +```cs +VerifyEntityFramework.Initialize(data, recordCommands: false); +``` +snippet source | anchor + + +Only recording is disabled. The converters, and the queryable to SQL file converter, are still registered. + +This is useful when another package records the same commands. [Verify.SqlServer](https://github.com/VerifyTests/Verify.SqlServer) subscribes to the `Microsoft.Data.SqlClient` diagnostic listener and records under the name `sql`. Since EF Core executes its commands through `SqlCommand`, with both packages recording every command EF executes is captured twice: once as `ef` and once as `sql`. Disable one of the two: + + * `VerifyEntityFramework.Initialize(model, recordCommands: false)` keeps the `sql` entries. + * `VerifySqlServer.Initialize(recordCommands: false)` keeps the `ef` entries, which also carry the command `Type` and transaction state. It has to be called before `VerifierSettings.InitializePlugins()`, otherwise plugin discovery initializes Verify.SqlServer first with recording enabled, and the explicit call throws `Already Initialized`. + + ## ChangeTracking Added, deleted, and Modified entities can be verified by performing changes on a DbContext and then verifying the instance of ChangeTracking. This approach leverages the [EntityFramework ChangeTracker](https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.changetracker). diff --git a/src/Verify.EntityFramework.RecordingDisabledTests/GlobalUsings.cs b/src/Verify.EntityFramework.RecordingDisabledTests/GlobalUsings.cs new file mode 100644 index 00000000..bdc5b06d --- /dev/null +++ b/src/Verify.EntityFramework.RecordingDisabledTests/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using System.ComponentModel.DataAnnotations.Schema; +global using Argon; +global using Microsoft.EntityFrameworkCore; diff --git a/src/Verify.EntityFramework.RecordingDisabledTests/ModuleInitializer.cs b/src/Verify.EntityFramework.RecordingDisabledTests/ModuleInitializer.cs new file mode 100644 index 00000000..1021b09e --- /dev/null +++ b/src/Verify.EntityFramework.RecordingDisabledTests/ModuleInitializer.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Metadata; + +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Init() + { + using var data = GetDbContext(); + + #region DisableRecording + + VerifyEntityFramework.Initialize(data, recordCommands: false); + + #endregion + } + + static SampleDbContext GetDbContext() + { + var options = new DbContextOptionsBuilder(); + options.UseSqlServer("fake"); + return new(options.Options); + } +} diff --git a/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.ConvertersAreStillRegistered.verified.sql b/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.ConvertersAreStillRegistered.verified.sql new file mode 100644 index 00000000..3241b00e --- /dev/null +++ b/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.ConvertersAreStillRegistered.verified.sql @@ -0,0 +1,4 @@ +select c.Id, + c.Name +from Companies as c +where c.Name = N'Title' \ No newline at end of file diff --git a/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.ConvertersAreStillRegistered.verified.txt b/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.ConvertersAreStillRegistered.verified.txt new file mode 100644 index 00000000..ad47dbb9 --- /dev/null +++ b/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.ConvertersAreStillRegistered.verified.txt @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.cs b/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.cs new file mode 100644 index 00000000..20579f51 --- /dev/null +++ b/src/Verify.EntityFramework.RecordingDisabledTests/RecordingDisabledTests.cs @@ -0,0 +1,67 @@ +[TestFixture] +public class RecordingDisabledTests +{ + static SqlInstance sqlInstance = new( + buildTemplate: data => data.Database.EnsureCreatedAsync(), + storage: Storage.FromSuffix("RecordingDisabled"), + constructInstance: builder => + { + builder.EnableRecording(); + return new(builder.Options); + }); + + static SqlInstance identifierInstance = new( + buildTemplate: data => data.Database.EnsureCreatedAsync(), + storage: Storage.FromSuffix("RecordingDisabledIdentifier"), + constructInstance: builder => + { + builder.EnableRecording("theIdentifier"); + return new(builder.Options); + }); + + [Test] + public async Task ExecutedCommandIsNotRecorded() + { + await using var database = await sqlInstance.Build(); + var data = database.Context; + + Recording.Start(); + + await data + .Companies + .Where(_ => _.Name == "Title") + .ToListAsync(); + + Assert.That(Recording.Stop(), Is.Empty); + } + + [Test] + public async Task ExecutedCommandIsNotRecordedForIdentifier() + { + await using var database = await identifierInstance.Build(); + var data = database.Context; + + Recording.Start("theIdentifier"); + + await data + .Companies + .Where(_ => _.Name == "Title") + .ToListAsync(); + + Assert.That(Recording.Stop("theIdentifier"), Is.Empty); + } + + // recordCommands only disables the recording interceptor, not the converters + [Test] + public async Task ConvertersAreStillRegistered() + { + await using var database = await sqlInstance.Build(); + var data = database.Context; + + var queryable = data + .Companies + .Where(_ => _.Name == "Title"); + + await Verify(queryable); + } +} diff --git a/src/Verify.EntityFramework.RecordingDisabledTests/Verify.EntityFramework.RecordingDisabledTests.csproj b/src/Verify.EntityFramework.RecordingDisabledTests/Verify.EntityFramework.RecordingDisabledTests.csproj new file mode 100644 index 00000000..65dad1a6 --- /dev/null +++ b/src/Verify.EntityFramework.RecordingDisabledTests/Verify.EntityFramework.RecordingDisabledTests.csproj @@ -0,0 +1,21 @@ + + + net10.0 + + + + + + + + + + + + + + + + + + diff --git a/src/Verify.EntityFramework.slnx b/src/Verify.EntityFramework.slnx index 52aab3f2..0dd27520 100644 --- a/src/Verify.EntityFramework.slnx +++ b/src/Verify.EntityFramework.slnx @@ -11,6 +11,7 @@ + diff --git a/src/Verify.EntityFramework/VerifyEntityFramework.cs b/src/Verify.EntityFramework/VerifyEntityFramework.cs index 8f7d8d83..65b44586 100644 --- a/src/Verify.EntityFramework/VerifyEntityFramework.cs +++ b/src/Verify.EntityFramework/VerifyEntityFramework.cs @@ -105,9 +105,23 @@ public static void IgnoreNavigationProperties(IModel? model = null) public static void Initialize(DbContext context) => Initialize(context.Model); + public static void Initialize(DbContext context, bool recordCommands) => + Initialize(context.Model, recordCommands); + public static bool Initialized { get; private set; } - public static void Initialize(IModel? model = null) + static bool recordCommands = true; + + public static void Initialize(IModel? model = null) => + Initialize(model, recordCommands: true); + + /// The used to cache navigation property information. Can be null. + /// + /// Allow to add the interceptor that + /// adds executed commands to under the name `ef`. Disable when another package, for + /// example Verify.SqlServer, already records the same commands. + /// + public static void Initialize(IModel? model, bool recordCommands) { if (Initialized) { @@ -115,6 +129,7 @@ public static void Initialize(IModel? model = null) } Initialized = true; + VerifyEntityFramework.recordCommands = recordCommands; InnerVerifier.ThrowIfVerifyHasBeenRun(); if (model != null) @@ -191,8 +206,15 @@ public static DbContextOptionsBuilder EnableRecording(this D => builder.EnableRecording(null); public static DbContextOptionsBuilder EnableRecording(this DbContextOptionsBuilder builder, string? identifier) - where TContext : DbContext => - builder.AddInterceptors(new LogCommandInterceptor(identifier)); + where TContext : DbContext + { + if (!recordCommands) + { + return builder; + } + + return builder.AddInterceptors(new LogCommandInterceptor(identifier)); + } static ConcurrentBag recordingDisabledContextIds = [];