LocalDb
repository·main·Indexed 41 days ago
https://github.com/simoncropp/localdbA wrapper around Microsoft's SqlLocalDB that facilitates isolated, high-performance SQL Server database testing for Entity Framework (Core and Classic) and raw ADO.NET applications. It provides dedicated NuGet packages for raw SqlConnection, EF Core, and EF6, with integrations for NUnit, xUnit v3, MSTest, and TUnit. Supported exclusively on Windows.
What's inside LocalDb
- LocalDb is a wrapper around SqlLocalDB designed to simplify running tests against Entity Framework or a raw SQL Database.
Overview of LocalDb
mainLocalDb is a wrapper around Microsoft's SqlLocalDB designed to simplify running unit tests against Entity Framework or a raw SQL Database.
Key Goals:
- Provide an isolated SQL Server Database for each unit test method.
- Maintain high performance.
- Ensure the resulting database can be accessed via SQL Server Management Studio (SSMS) or other tools for debugging failed tests.
Important Constraint:
- SqlLocalDB is only supported on Windows.
Use EfLocalDb.TUnit for Entity Framework Core testing
mainThe
EfLocalDb.TUnitpackage integratesEfLocalDb,TUnit,Verify.TUnit, andVerify.EntityFrameworkinto a unified testing experience. It provides a test base class that ensures each test runs against an isolated database and enforces the Arrange-Act-Assert pattern.dotnet add package EfLocalDb.TUnitUse EfLocalDb.NUnit for Entity Framework Core testing
mainThe
EfLocalDb.NUnitpackage provides a test base class that integratesEfLocalDb,NUnit,Verify.NUnit, andVerify.EntityFramework. It is designed to provide an isolated database per test and enforces the Arrange-Act-Assert pattern.https://nuget.org/packages/EfLocalDb.NUnit/How RowVersions.Read works
mainThe
RowVersions.Readmethod automates the discovery and retrieval of row versions through the following process:- Dynamic Discovery: It scans the database to find all base tables containing both an
Idcolumn (UNIQUEIDENTIFIER) and aRowVersioncolumn (ROWVERSION). - Query Construction: It builds a dynamic SQL query using
STRING_AGGtoUNIONall matching tables into a single result set. - Type Conversion: It converts the SQL Server 8-byte
ROWVERSION(also known asTIMESTAMP) values into C#ulongtypes to facilitate easier comparisons.
Row Version Behavior
SQL Server's
ROWVERSIONis a monotonically increasing value that is unique within the database and changes automatically whenever a row is modified. This makes it ideal for detecting data changes.- Dynamic Discovery: It scans the database to find all base tables containing both an
How phase ordering works in LocalDbTestBase
mainLocalDbTestBase<T>enforces a strict lifecycle for database tests to prevent accidental state leakage or incorrect test logic. The lifecycle follows these transitions:ArrangeData$\rightarrow$ActData$\rightarrow$AssertDataRules and Exceptions
- Transitioning: Simply accessing the property (e.g.,
var x = ActData;) triggers the transition to that phase. - Illegal Access:
- Accessing
ArrangeDataafterActDatahas been accessed throws an exception: `"Phase has already moved to Act. Check for a ActData usage in the preceding code."$ - Accessing
ActDataafterAssertDatahas been accessed throws an exception: `"Phase has already moved to Assert. Check for a AssertData usage in the preceding code."$
- Accessing
- Queryable Constraints: Using a
IQueryablecreated in a previous phase (e.g., a queryable fromArrangeDataused inside anAssertDataverification) after the phase has transitioned will throw an exception.
- Transitioning: Simply accessing the property (e.g.,
How phase ordering works in LocalDb
mainLocalDb uses a state machine to enforce the Arrange-Act-Assert pattern. This prevents common testing mistakes like modifying data in the 'Arrange' phase after the 'Act' phase has already begun.
- Transitions:
- Accessing
ActData$\rightarrow$ moves state from Arrange to Act. - Accessing
AssertData$\rightarrow$ moves state from Act to Assert.
- Accessing
- Errors:
- Accessing
ArrangeDataafterActDatahas been accessed $\rightarrow$ Throws exception:"Phase has already moved to Act. Check for a ActData usage in the preceding code." - Accessing
ActDataafterAssertDatahas been accessed $\rightarrow$ Throws exception:"Phase has already moved to Assert. Check for a AssertData usage in the preceding code." - Accessing
ArrangeDataafterAssertDatahas been accessed $\rightarrow$ Throws exception:"Phase has already moved to Assert. Check for a AssertData usage in the preceding code."
- Accessing
- Transitions:
Control template re-generation using the timestamp parameter
mainTo prevent expensive re-generation on every run, LocalDb uses a timestamp convention. It compares the current timestamp to the file creation time of the template. If they match, re-generation is skipped.
You can control this behavior using the
timestampparameter in theSqlInstanceconstructor.If the
timestampparameter is not defined, the following default conventions apply:- Raw SQL: Uses the last modified time of the calling Assembly (via
Assembly.GetCallingAssembly). This typically corresponds to the unit test assembly, meaning re-generation only occurs if a solution build has happened. - EntityFramework: Uses the last modified time of the Assembly containing the
DataContext.
- Raw SQL: Uses the last modified time of the calling Assembly (via
How template databases work in LocalDb
mainWhen a
SqlInstanceis defined, LocalDb creates a template database. All subsequentSqlDatabaseobjects created from thatSqlInstanceare cloned from this template.Benefits
- Performance: Instead of re-creating schemas or running migrations every time a database is needed, LocalDb uses the pre-configured template.
- Consistency: Allows you to define schema and seed data once.
Template Database Settings
The following database-level settings are applied to the template and inherited by all databases attached from it:
auto_update_statistics off: Prevents background statistics updates from causing nondeterministic test timing.read_committed_snapshot on: Uses row versioning forREAD COMMITTEDisolation. This is required to prevent S/X-lock deadlocks when running parallel transactional tests.
Updating Template Settings
If the template files already exist on disk, changes to settings will not be applied automatically. To regenerate the template with new settings, you must either:
- Delete the existing template files from the file system.
- Change the timestamp passed to the
SqlInstanceconstructor.
How RowVersion behavior works in LocalDb
mainLocalDb utilizes SQL Server's
ROWVERSION(formerly known asTIMESTAMP) mechanism. This is a monotonically increasing value that provides several key properties:- Automatic Updates: The value changes automatically every time a row is modified.
- Uniqueness: The value is unique within the database.
- Change Detection: It can be used to detect if data has changed since it was last read by comparing the current version against a previously stored version.
The
RowVersions.Readmethod converts the underlying 8-byte SQLROWVERSIONinto a C#ulongto facilitate easier comparisons in your application logic.Caveats and requirements for temporal helpers
mainWhen using
SetCurrentPeriodStartorSetHistoryColumn, keep the following in mind:- Strictly increasing periods: The
periodStartprovided must be strictly greater than the row's previousPeriodStart. Otherwise, re-enabling system versioning will fail with a period-overlap error. - Temporal Configuration: The entity must be configured as temporal in
OnModelCreatingusing.IsTemporal(). - Temporal Joins: When using
TemporalAsOf, ensure youranchortime is after any related entities were created. SinceTemporalAsOfuses inner joins for navigation properties, a missing related row at the anchor time will cause the query to return no results. - Inheritance: For TPH (Table-Per-Hierarchy) inheritance, the helper automatically walks to the root entity type.
- Limitations: Composite primary keys are not supported.
- Concurrency: The
UPDATEoperations bump theRowVersion. Use the entity overload ofSetCurrentPeriodStartor callContext.Entry(entity).ReloadAsync()after using the ID-based overload to avoid optimistic concurrency failures.
- Strictly increasing periods: The
Performance considerations for SetCurrentPeriodStart
mainEach call to
SetCurrentPeriodStartperforms three round trips to SQL Server and involves severalALTER TABLEstatements.Key Performance Factors
- Locking: Each call takes a
Sch-M(Schema Modification) lock, which invalidates the plan cache for that table. - Validation Cost: When re-enabling system versioning, SQL Server validates that periods do not overlap. This cost scales with the total number of history rows, not just the rows being touched.
- Complexity: Typical cost is 5–15 ms on small databases.
Scaling Pitfalls
- Avoid loops over many rows: Calling the helper in a loop for $N$ inserts results in $O(N^2)$ complexity because each call re-validates all existing history rows. For bulk seeding, use a dedicated path that disables versioning, performs a bulk insert into the history table, and then re-enables versioning.
- Plan Cache: Frequent calls will cause subsequent EF queries to pay a one-time recompile cost.
- Locking: Each call takes a