diff --git a/readme.md b/readme.md
index c8054e4d..cf383fce 100644
--- a/readme.md
+++ b/readme.md
@@ -26,7 +26,7 @@ Enable VerifyEntityFramework once at assembly load time:
```cs
VerifyEntityFramework.Enable();
```
-snippet source | anchor
+snippet source | anchor
@@ -58,7 +58,7 @@ builder.UseSqlServer(connection);
builder.EnableRecording();
var data = new SampleDbContext(builder.Options);
```
-snippet source | anchor
+snippet source | anchor
`EnableRecording` should only be called in the test context.
@@ -86,7 +86,7 @@ await data.Companies
await Verify(data.Companies.Count());
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -143,7 +143,7 @@ await Verify(new
sql = entries
});
```
-snippet source | anchor
+snippet source | anchor
@@ -174,7 +174,7 @@ await data2.Companies
await Verify(data2.Companies.Count());
```
-snippet source | anchor
+snippet source | anchor
@@ -241,7 +241,7 @@ public async Task Added()
await Verify(data.ChangeTracker);
}
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -275,7 +275,7 @@ public async Task Deleted()
var options = DbContextOptions();
await using var data = new SampleDbContext(options);
- data.Add(new Company {Content = "before"});
+ data.Add(new Company { Content = "before" });
await data.SaveChangesAsync();
var company = data.Companies.Single();
@@ -283,7 +283,7 @@ public async Task Deleted()
await Verify(data.ChangeTracker);
}
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -327,7 +327,7 @@ public async Task Modified()
await Verify(data.ChangeTracker);
}
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -362,7 +362,7 @@ var queryable = data.Companies
.Where(x => x.Content == "value");
await Verify(queryable);
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -410,7 +410,7 @@ await Verify(data.AllData())
serializer =>
serializer.TypeNameHandling = TypeNameHandling.Objects));
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file with all data in the database:
@@ -494,9 +494,60 @@ public async Task IgnoreNavigationProperties()
x => x.IgnoreNavigationProperties(data));
}
```
-snippet source | anchor
+snippet source | anchor
+## WebApplicationFactory
+
+To be able to use [WebApplicationFactory](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) for [integration testing](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests)
+an identifier must be used to be able to retrive the recorded commands. Start by enable recording with a unique identifier, for example the test name or a GUID:
+
+
+
+```cs
+.ConfigureTestServices(services =>
+{
+ services.AddScoped>(_ =>
+ {
+ return new DbContextOptionsBuilder()
+ .EnableRecording(testName)
+ .UseSqlite($"Data Source={testName};Mode=Memory;Cache=Shared")
+ .Options;
+ });
+});
+```
+snippet source | anchor
+
+
+Then use the same identifer for recording:
+
+
+
+```cs
+var httpClient = factory.CreateClient();
+
+EfRecording.StartRecording(testName);
+
+var companies = await httpClient.GetFromJsonAsync("/companies");
+
+var entries = EfRecording.FinishRecording(testName);
+```
+snippet source | anchor
+
+
+The results will not be automatically included in verified file so it will have to be verified manually:
+
+
+
+```cs
+await Verify(new
+{
+ target = companies!.Length,
+ sql = entries
+});
+```
+snippet source | anchor
+
## Icon
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory.verified.txt
new file mode 100644
index 00000000..4404b68f
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory.verified.txt
@@ -0,0 +1,11 @@
+{
+ target: 1,
+ sql: [
+ {
+ Type: ReaderExecutedAsync,
+ Text:
+SELECT "c"."Id", "c"."Content"
+FROM "Companies" AS "c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory9.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory9.verified.txt
new file mode 100644
index 00000000..4404b68f
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory9.verified.txt
@@ -0,0 +1,11 @@
+{
+ target: 1,
+ sql: [
+ {
+ Type: ReaderExecutedAsync,
+ Text:
+SELECT "c"."Id", "c"."Content"
+FROM "Companies" AS "c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=0.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=0.verified.txt
new file mode 100644
index 00000000..4404b68f
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=0.verified.txt
@@ -0,0 +1,11 @@
+{
+ target: 1,
+ sql: [
+ {
+ Type: ReaderExecutedAsync,
+ Text:
+SELECT "c"."Id", "c"."Content"
+FROM "Companies" AS "c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=1.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=1.verified.txt
new file mode 100644
index 00000000..4404b68f
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=1.verified.txt
@@ -0,0 +1,11 @@
+{
+ target: 1,
+ sql: [
+ {
+ Type: ReaderExecutedAsync,
+ Text:
+SELECT "c"."Id", "c"."Content"
+FROM "Companies" AS "c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=2.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=2.verified.txt
new file mode 100644
index 00000000..4404b68f
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=2.verified.txt
@@ -0,0 +1,11 @@
+{
+ target: 1,
+ sql: [
+ {
+ Type: ReaderExecutedAsync,
+ Text:
+SELECT "c"."Id", "c"."Content"
+FROM "Companies" AS "c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=3.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=3.verified.txt
new file mode 100644
index 00000000..4404b68f
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=3.verified.txt
@@ -0,0 +1,11 @@
+{
+ target: 1,
+ sql: [
+ {
+ Type: ReaderExecutedAsync,
+ Text:
+SELECT "c"."Id", "c"."Content"
+FROM "Companies" AS "c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=4.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=4.verified.txt
new file mode 100644
index 00000000..4404b68f
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.RecordingWebApplicationFactory_run=4.verified.txt
@@ -0,0 +1,11 @@
+{
+ target: 1,
+ sql: [
+ {
+ Type: ReaderExecutedAsync,
+ Text:
+SELECT "c"."Id", "c"."Content"
+FROM "Companies" AS "c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.cs b/src/Verify.EntityFramework.Tests/CoreTests.cs
index 296d355d..7b3d3ccb 100644
--- a/src/Verify.EntityFramework.Tests/CoreTests.cs
+++ b/src/Verify.EntityFramework.Tests/CoreTests.cs
@@ -1,8 +1,17 @@
-using Microsoft.EntityFrameworkCore;
+using System.Net.Http.Json;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using VerifyTests.EntityFramework;
[TestFixture]
+[Parallelizable(ParallelScope.All)]
public class CoreTests
{
#region Added
@@ -31,7 +40,7 @@ public async Task Deleted()
var options = DbContextOptions();
await using var data = new SampleDbContext(options);
- data.Add(new Company {Content = "before"});
+ data.Add(new Company { Content = "before" });
await data.SaveChangesAsync();
var company = data.Companies.Single();
@@ -158,7 +167,7 @@ public Task ShouldIgnoreDbContext() =>
Factory = new SampleDbContext(new DbContextOptions())
});
- class MyDbContextFactory: IDbContextFactory
+ class MyDbContextFactory : IDbContextFactory
{
public SampleDbContext CreateDbContext() =>
throw new NotImplementedException();
@@ -222,7 +231,7 @@ public async Task NestedQueryable()
var data = database.Context;
var queryable = data.Companies
.Where(x => x.Content == "value");
- await Verify(new {queryable});
+ await Verify(new { queryable });
}
void Build(string connection)
@@ -327,6 +336,99 @@ await data.Companies
#endregion
}
+ [DatapointSource]
+ public IEnumerable runs = Enumerable.Range(0, 5);
+
+ [Theory]
+ public async Task RecordingWebApplicationFactory(int run)
+ {
+ // Not actually the test name, the variable name is for README.md to make sense
+ var testName = nameof(RecordingWebApplicationFactory) + run;
+
+ using var connection = new SqliteConnection($"Data Source={testName};Mode=Memory;Cache=Shared");
+ await connection.OpenAsync();
+
+ var factory = new CustomWebApplicationFactory(testName);
+
+ using (var scope = factory.Services.CreateScope())
+ {
+ var context = scope.ServiceProvider.GetRequiredService();
+
+ await context.Database.EnsureCreatedAsync();
+
+ context.Add(new Company { Id = 1, Content = "Foo" });
+
+ await context.SaveChangesAsync();
+ }
+
+ #region RecordWithIdentifier
+ var httpClient = factory.CreateClient();
+
+ EfRecording.StartRecording(testName);
+
+ var companies = await httpClient.GetFromJsonAsync("/companies");
+
+ var entries = EfRecording.FinishRecording(testName);
+ #endregion
+
+ #region VerifyRecordedCommandsWithIdentifier
+ await Verify(new
+ {
+ target = companies!.Length,
+ sql = entries
+ });
+ #endregion
+ }
+
+ class CustomWebApplicationFactory : WebApplicationFactory
+ {
+ readonly string testName;
+
+ public CustomWebApplicationFactory(string testName)
+ {
+ this.testName = testName;
+ }
+
+ protected override void ConfigureWebHost(IWebHostBuilder builder) =>
+ builder
+ #region EnableRecordingWithIdentifier
+ .ConfigureTestServices(services =>
+ {
+ services.AddScoped>(_ =>
+ {
+ return new DbContextOptionsBuilder()
+ .EnableRecording(testName)
+ .UseSqlite($"Data Source={testName};Mode=Memory;Cache=Shared")
+ .Options;
+ });
+ });
+ #endregion
+
+ protected override IHostBuilder CreateHostBuilder() =>
+ Host.CreateDefaultBuilder()
+ .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup());
+ }
+
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services
+ .AddDbContext(builder =>
+ {
+ builder.UseInMemoryDatabase("");
+ });
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseRouting();
+
+ app.UseEndpoints(endpoints
+ => endpoints.MapGet("/companies", async (SampleDbContext data) => await data.Companies.ToListAsync()));
+ }
+ }
+
[Test]
public async Task RecordingSpecific()
{
diff --git a/src/Verify.EntityFramework.Tests/Verify.EntityFramework.Tests.csproj b/src/Verify.EntityFramework.Tests/Verify.EntityFramework.Tests.csproj
index da37736e..e63caffe 100644
--- a/src/Verify.EntityFramework.Tests/Verify.EntityFramework.Tests.csproj
+++ b/src/Verify.EntityFramework.Tests/Verify.EntityFramework.Tests.csproj
@@ -4,7 +4,9 @@
+
+
diff --git a/src/Verify.EntityFramework/LogCommandInterceptor.cs b/src/Verify.EntityFramework/LogCommandInterceptor.cs
index ef5eb886..14422e98 100644
--- a/src/Verify.EntityFramework/LogCommandInterceptor.cs
+++ b/src/Verify.EntityFramework/LogCommandInterceptor.cs
@@ -1,4 +1,4 @@
-using System.Data.Common;
+using System.Data.Common;
using Microsoft.EntityFrameworkCore.Diagnostics;
using VerifyTests.EntityFramework;
@@ -6,8 +6,11 @@ class LogCommandInterceptor :
DbCommandInterceptor
{
static AsyncLocal asyncLocal = new();
+ static ConcurrentDictionary> namedEvents = new(StringComparer.OrdinalIgnoreCase);
+ readonly string? identifier;
public static void Start() => asyncLocal.Value = new();
+ public static void Start(string identifier) => namedEvents.GetOrAdd(identifier, _ => new());
public static IEnumerable? Stop()
{
@@ -16,6 +19,18 @@ class LogCommandInterceptor :
return state?.Events.OrderBy(x => x.StartTime);
}
+ public static IEnumerable? Stop(string identifier)
+ {
+ namedEvents.TryRemove(identifier, out var state);
+
+ return state?.OrderBy(x => x.StartTime);
+ }
+
+ public LogCommandInterceptor(string? identifier)
+ {
+ this.identifier = identifier;
+ }
+
public override void CommandFailed(DbCommand command, CommandErrorEventData data)
=> Add("CommandFailed", command, data, data.Exception);
@@ -61,8 +76,17 @@ public override ValueTask NonQueryExecutedAsync(DbCommand command, CommandE
return new(result);
}
- static void Add(string type, DbCommand command, CommandEndEventData data, Exception? exception = null)
- => asyncLocal.Value?.WriteLine(new(type, command, data, exception));
+ void Add(string type, DbCommand command, CommandEndEventData data, Exception? exception = null)
+ {
+ if (identifier is null)
+ {
+ asyncLocal.Value?.WriteLine(new(type, command, data, exception));
+ }
+ else if (namedEvents.ContainsKey(identifier))
+ {
+ namedEvents[identifier].Add(new(type, command, data, exception));
+ }
+ }
class State
{
diff --git a/src/Verify.EntityFramework/SqlRecording.cs b/src/Verify.EntityFramework/SqlRecording.cs
index dedbf4e5..5a42c644 100644
--- a/src/Verify.EntityFramework/SqlRecording.cs
+++ b/src/Verify.EntityFramework/SqlRecording.cs
@@ -1,15 +1,23 @@
-using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
namespace VerifyTests.EntityFramework;
public static class EfRecording
{
- public static void EnableRecording(this DbContextOptionsBuilder builder)
- => builder.AddInterceptors(new LogCommandInterceptor());
+ public static DbContextOptionsBuilder EnableRecording(this DbContextOptionsBuilder builder)
+ where TContext : DbContext
+ => builder.EnableRecording(null);
+
+ public static DbContextOptionsBuilder EnableRecording(this DbContextOptionsBuilder builder, string? identifier)
+ where TContext : DbContext
+ => builder.AddInterceptors(new LogCommandInterceptor(identifier));
public static void StartRecording()
=> LogCommandInterceptor.Start();
+ public static void StartRecording(string identifier)
+ => LogCommandInterceptor.Start(identifier);
+
public static IEnumerable FinishRecording()
{
var entries = LogCommandInterceptor.Stop();
@@ -19,4 +27,14 @@ public static IEnumerable FinishRecording()
}
throw new("No recorded state. It is possible `VerifyEntityFramework.StartRecording()` has not been called on the DbContext.");
}
+
+ public static IEnumerable FinishRecording(string identifier)
+ {
+ var entries = LogCommandInterceptor.Stop(identifier);
+ if (entries is not null)
+ {
+ return entries;
+ }
+ throw new("No recorded state. It is possible `VerifyEntityFramework.StartRecording(string identifier)` has not been called on the DbContext.");
+ }
}
\ No newline at end of file