Verify.EntityFramework
April 9, 2026 ยท View on GitHub
Extends Verify to allow snapshot testing with EntityFramework.
See Milestones for release notes.
Sponsors
Entity Framework Extensions
Entity Framework Extensions is a major sponsor and is proud to contribute to the development this project.
Developed using JetBrains IDEs
NuGet
- https://nuget.org/packages/Verify.EntityFramework/
- https://nuget.org/packages/Verify.EntityFrameworkClassic/
Enable
Enable VerifyEntityFramework once at assembly load time:
EF Core
static IModel GetDbModel()
{
var options = new DbContextOptionsBuilder<SampleDbContext>();
options.UseSqlServer("fake");
using var data = new SampleDbContext(options.Options);
return data.Model;
}
[ModuleInitializer]
public static void Init()
{
var model = GetDbModel();
VerifyEntityFramework.Initialize(model);
}
The GetDbModel pattern allows an instance of the IModel to be stored for use when IgnoreNavigationProperties is called inside tests. This is optional, and instead can be passed explicitly to IgnoreNavigationProperties.
EF Classic
[ModuleInitializer]
public static void Init() =>
VerifyEntityFrameworkClassic.Initialize();
Recording
Recording allows all commands executed by EF to be captured and then (optionally) verified.
Enable
Call EfRecording.EnableRecording() on DbContextOptionsBuilder.
var builder = new DbContextOptionsBuilder<SampleDbContext>();
builder.UseSqlServer(connection);
builder.EnableRecording();
var data = new SampleDbContext(builder.Options);
EnableRecording should only be called in the test context.
Usage
To start recording call EfRecording.StartRecording(). The results will be automatically included in verified file.
var company = new Company
{
Name = "Title"
};
data.Add(company);
await data.SaveChangesAsync();
Recording.Start();
await data
.Companies
.Where(_ => _.Name == "Title")
.ToListAsync();
await Verify();
Will result in the following verified file:
{
ef: {
Type: ReaderExecutedAsync,
HasTransaction: false,
Text:
select c.Id,
c.Name
from Companies as c
where c.Name = N'Title'
}
}
Sql entries can be explicitly read using EfRecording.FinishRecording, optionally filtered, and passed to Verify:
var company = new Company
{
Name = "Title"
};
data.Add(company);
await data.SaveChangesAsync();
Recording.Start();
await data
.Companies
.Where(_ => _.Name == "Title")
.ToListAsync();
var entries = Recording.Stop();
//TODO: optionally filter the results
await Verify(
new
{
target = data.Companies.Count(),
entries
});
DbContext spanning
StartRecording can be called on different DbContext instances (built from the same options) and the results will be aggregated.
var builder = new DbContextOptionsBuilder<SampleDbContext>();
builder.UseSqlServer(connectionString);
builder.EnableRecording();
await using var data1 = new SampleDbContext(builder.Options);
Recording.Start();
var company = new Company
{
Name = "Title"
};
data1.Add(company);
await data1.SaveChangesAsync();
await using var data2 = new SampleDbContext(builder.Options);
await data2
.Companies
.Where(_ => _.Name == "Title")
.ToListAsync();
await Verify();
{
ef: [
{
Type: ReaderExecutedAsync,
HasTransaction: false,
Parameters: {
@p0 (Int32): 0,
@p1 (String): Title
},
Text:
set implicit_transactions off;
set nocount on;
insert into Companies (Id, Name)
values (@p0, @p1)
},
{
Type: ReaderExecutedAsync,
HasTransaction: false,
Text:
select c.Id,
c.Name
from Companies as c
where c.Name = N'Title'
}
]
}
Disabling Recording for an instance
var company = new Company
{
Name = "Title"
};
data.Add(company);
await data.SaveChangesAsync();
Recording.Start();
await data
.Companies
.Where(_ => _.Name == "Title")
.ToListAsync();
data.DisableRecording();
await data
.Companies
.Where(_ => _.Name == "Disabled")
.ToListAsync();
await Verify();
{
ef: {
Type: ReaderExecutedAsync,
HasTransaction: false,
Text:
select c.Id,
c.Name
from Companies as c
where c.Name = N'Title'
}
}
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.
Added entity
This test:
[Test]
public async Task Added()
{
var options = DbContextOptions();
await using var data = new SampleDbContext(options);
var company = new Company
{
Name = "company name"
};
data.Add(company);
await Verify(data.ChangeTracker);
}
Will result in the following verified file:
{
Added: {
Company: {
Id: 0,
Name: company name
}
}
}
Deleted entity
This test:
[Test]
public async Task Deleted()
{
var options = DbContextOptions();
await using var data = new SampleDbContext(options);
data.Add(new Company
{
Name = "company name"
});
await data.SaveChangesAsync();
var company = data.Companies.Single();
data.Companies.Remove(company);
await Verify(data.ChangeTracker);
}
Will result in the following verified file:
{
Deleted: {
Company: {
Id: 0
}
}
}
Modified entity
This test:
[Test]
public async Task Modified()
{
var options = DbContextOptions();
await using var data = new SampleDbContext(options);
var company = new Company
{
Name = "old name"
};
data.Add(company);
await data.SaveChangesAsync();
data.Companies.Single()
.Name = "new name";
await Verify(data.ChangeTracker);
}
Will result in the following verified file:
{
Modified: {
Company: {
Id: 0,
Name: {
Original: old name,
Current: new name
}
}
}
}
Queryable
This test:
var queryable = data.Companies
.Where(_ => _.Name == "company name");
await Verify(queryable);
Will result in the following verified files:
EF Core
CoreTests.Queryable.verified.txt
[
{
Name: company name
}
]
CoreTests.Queryable.verified.sql
select c.Id,
c.Name
from Companies as c
where c.Name = N'company name'
EF Classic
ClassicTests.Queryable.verified.txt
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Content] AS [Content]
FROM [dbo].[Companies] AS [Extent1]
WHERE N'value' = [Extent1].[Content]
AllData
This test:
await Verify(data.AllData())
.AddExtraSettings(
serializer =>
serializer.TypeNameHandling = TypeNameHandling.Objects);
Will result in the following verified file with all data in the database:
[
{
$type: Company,
Id: 1,
Name: Company1
},
{
$type: Company,
Id: 4,
Name: Company2
},
{
$type: Company,
Id: 6,
Name: Company3
},
{
$type: Company,
Id: 7,
Name: Company4
},
{
$type: Employee,
Id: 2,
CompanyId: 1,
Name: Employee1,
Age: 25
},
{
$type: Employee,
Id: 3,
CompanyId: 1,
Name: Employee2,
Age: 31
},
{
$type: Employee,
Id: 5,
CompanyId: 4,
Name: Employee4,
Age: 34
}
]
IgnoreNavigationProperties
IgnoreNavigationProperties extends SerializationSettings to exclude all navigation properties from serialization:
[Test]
public async Task IgnoreNavigationProperties()
{
var options = DbContextOptions();
await using var data = new SampleDbContext(options);
var company = new Company
{
Name = "company"
};
var employee = new Employee
{
Name = "employee",
Company = company
};
await Verify(employee)
.IgnoreNavigationProperties();
}
Ignore globally
var options = DbContextOptions();
using var data = new SampleDbContext(options);
VerifyEntityFramework.IgnoreNavigationProperties();
WebApplicationFactory
To be able to use WebApplicationFactory for integration testing an identifier must be used to be able to retrieve the recorded commands. Start by enable recording with a unique identifier, for example the test name or a GUID:
protected override void ConfigureWebHost(IWebHostBuilder webBuilder)
{
var dataBuilder = new DbContextOptionsBuilder<SampleDbContext>()
.EnableRecording(name)
.UseSqlite($"Data Source={name};Mode=Memory;Cache=Shared");
webBuilder.ConfigureTestServices(
_ => _.AddScoped(
_ => dataBuilder.Options));
}
Then use the same identifier for recording:
var httpClient = factory.CreateClient();
Recording.Start(testName);
var companies = await httpClient.GetFromJsonAsync<Company[]>("/companies");
var entries = Recording.Stop(testName);
The results will not be automatically included in verified file so it will have to be verified manually:
await Verify(
new
{
target = companies!.Length,
sql = entries
});
Descriptive Table Aliases
By default EF generates single character table aliases in SQL (eg c for Companies, e for Employees). UseDescriptiveTableAliases replaces these with the full table name, making recorded and verified SQL easier to read.
Enable
Call UseDescriptiveTableAliases() on DbContextOptionsBuilder.
var builder = new DbContextOptionsBuilder<SampleDbContext>();
builder.UseSqlServer(connection);
builder.UseDescriptiveTableAliases();
Result
With descriptive aliases enabled, the generated SQL:
select companies.Id,
companies.Name,
employees.Id,
employees.Age,
employees.CompanyId,
employees.Name
from Companies as companies
left outer join
Employees as employees
on companies.Id = employees.CompanyId
order by companies.Name,
companies.Id
Instead of the default:
select c.Id,
c.Name,
e.Id,
e.Age,
e.CompanyId,
e.Name
from Companies as c
left outer join
Employees as e
on c.Id = e.CompanyId
order by c.Name,
c.Id
Descriptive Parameter Names
By default EF generates generic parameter names in SQL (eg @p0, @p1). UseDescriptiveParameterNames replaces these with the column name, making recorded and verified SQL easier to read. When the same column name appears across multiple tables in a batch, subsequent occurrences are prefixed with the entity type name (eg @Id for the first table, @EmployeeId for the second).
Enable
Call UseDescriptiveParameterNames() on DbContextOptionsBuilder.
var builder = new DbContextOptionsBuilder<SampleDbContext>();
builder.UseSqlServer(connection);
builder.UseDescriptiveParameterNames();
Result
With descriptive parameter names enabled, an insert:
{
ef: {
Type: ReaderExecutedAsync,
HasTransaction: false,
Parameters: {
@Id (Int32): 0,
@Name (String): Title
},
Text:
set implicit_transactions off;
set nocount on;
insert into Companies (Id, Name)
values (@Id, @Name)
}
}
Instead of the default:
Parameters: {
@p0 (Int32): 0,
@p1 (String): Title
},
Text:
insert into Companies (Id, Name)
values (@p0, @p1)
Duplicate column names
When multiple tables in the same batch have columns with the same name, subsequent occurrences are prefixed with the entity type name:
{
ef: {
Type: ReaderExecutedAsync,
HasTransaction: true,
Parameters: {
@Age (Int32): 25,
@CompanyId (Int32): 100,
@EmployeeId (Int32): 200,
@EmployeeName (String): EmployeeName,
@Id (Int32): 100,
@Name (String): CompanyName
},
Text:
set nocount on;
insert into Companies (Id, Name)
values (@Id, @Name);
insert into Employees (Id, Age, CompanyId, Name)
values (@EmployeeId, @Age, @CompanyId, @EmployeeName)
}
}
If the entity-prefixed name itself collides with an existing column name (eg Company + Id = CompanyId which is already a column on Employee), a counter suffix is used as a fallback.
Missing OrderBy
To detect and correct missing OrderBy clauses in EF queries, use EntityFramework.OrderBy.
ScrubInlineEfDateTimes
In some scenarios EntityFrmaeowrk does not parameterise DateTimes. For example when querying temporal tables.
ScrubInlineEfDateTimes() is a convenience method that calls .ScrubInlineDateTimes("yyyy-MM-ddTHH:mm:ss.fffffffZ").
Static usage
VerifyEntityFramework.ScrubInlineEfDateTimes();
Instance usage
var settings = new VerifySettings();
settings.ScrubInlineEfDateTimes();
await Verify(target, settings);
Fluent usage
await Verify(target)
.ScrubInlineEfDateTimes();
Icon
Database designed by Creative Stall from The Noun Project.

