If you are changing the schema of your database's schema outside of EF Core' migrations, say by using SQL change scripts, then this library can quickly tell you if the a specific database schema and EF Core's Model
of the database are in step. Versions below 8 this library
The first number in the version number of this library defines what version of NET framework it works for. e.g.
- EfCore.SchemaCompare version 8.?.? works with to NET 9.
- EfCore.SchemaCompare version 7.?.? works with to NET 8 ... an so
WARNING: Version 8 contains a BREAKING CHANGE. You need to add the Microsoft.EntityFrameworkCore.Design NuGet to your application when using this library. This is needed as the EfCore.SchemaCompare library now supports multiple database providers does - see What database providers does it support.
The EfCore.SchemaCompare library (shortened to EfSchemaCompare in the documentations) is available on NuGet as EfCore.SchemaCompare and is an open-source library under the MIT licence. See ReleaseNotes for details of changes and information on versions from EF Core 5 onwards.
TABLE OF CONTENT
- What database providers does it support
- What does EfSchemaCompare check?
- List of limitations
- Introduction to how EfSchemaCompare works
- How to use EfSchemaCompare
- Different parameters to the
CompareEfWithDb
method - Understanding the error messages
- How to suppress certain error messages
- Other configuration options
NOTE: I use the term entity class for classes mapped to the database by EF Core.
- Version 8 now compares all EF Core database providers that can be be scaffolded. However some database providers may show incorrect match errors, because some database providers don't follow the SqlServer style, e.g. CosmosDB
Thanks to GitHub @bgrauer-atacom and @lweberprb for suggesting that this library could support extra database providers. See the issue #26 to see the code that these two people provided to add this feature.
- Versions below 8 support:
- SqlServer
- Sqlite
- Npgsql.EntityFrameworkCore.PostgreSQL
- Table/View exists: That a table or view that an entity class is mapped exists. This checks both table/view name and schema name
- Property/Column: exists, database type (including size and precision), nullability, computed column (including persistence), column default value, when updated (e.g. column is updated
OnAdd
for a int primary key which is provided by the database) - Primary key: SQL constraint name, properties
- Foreign keys: SQL constraint name, Delete behavior, properties
- Indexes: SQL constraint name, Unique/not unique, properties
- Unused tables or views
- Unused columns
- Unused Indexes
- Normal classes/properties
- Keyless classes
- Backing fields
- Shadow properties
- Value Converters
- Owned Types
- Table-per-Hierarchy
- Table splitting
- Concurrency tokens
- Json Mapping added in V8.1.0 and above - see Json Mapping checking limitation below
- The EF Core's scaffolder doesn't read in any index on the foreign key (the scaffolder assumes EF Core will do that by default). That means I can't check that there is an index on a foreign key.
- Cannot correctly check Table-per-Type or Table-per-Class classes because EF Core doesn't currently hold that data. This is tracked by Ef Core #19811.
- Cannot compare database tables/columns using InvariantCultureIgnoreCase. That is a EF Core 5+ limitation.
- EfCore.SchemaCompare versions below 8 only support SQL Server, Sqlite and PostgresSql, but version 8 supports all EF Core database providers that can be be scaffolded. However some database providers may show incorrect match errors, because some database providers don't follow the SqlServer style, e.g. CosmosDB
- The EF Core 8 new features aren't compared in this release. I may add some later.
- The Json Mapping checking assumes you are using the default name for the column, which is the name of the property.
The following are things I haven't bothered to check.
- Checking of Alternative keys
- Checking of collations
- Checking of sequences
EfSchemaCompare uses two EF Core features to get EF Core's internal schema and the database's schema. They are
- EF Core's
Model
property in your application's DbContext. This provides the internal version of the database schema that EF Core builds from looking at the entity classes and any EF Core configuration attributes/methods. - EF Core's Reverse Engineering service, which allows me to access an actual database schema.
The fun part is comparing these two sources, especially with all the different types of configurations that EF Core can handle. The diagram shown below shows using EfSchemaCompare to check a test database that you updated with some SQL migration scripts against the current EF Core's Model
.
The EfSchemaCompare uses two stages: Stage 1 checks your EF Core DbContext matches your database. Stage 2 checks your database for extra tables, columns, etc. that your EF Core DbContext doesn't use.
I usually run the EfSchemaCompare code in my unit tests, but that is up to you.
Here is a example of using the EfSchemaCompare feature
[Fact]
public void CompareViaContext()
{
//SETUP
var options = //... with connection to database to check
using (var context = new BookContext(options))
{
var comparer = new CompareEfSql();
//ATTEMPT
//This will compare EF Core model of the database with the database that the context's connection points to
var hasErrors = comparer.CompareEfWithDb(context);
//VERIFY
//The CompareEfWithDb method returns true if there were errors.
//The comparer.GetAllErrors property returns a string, with each error on a separate line
hasErrors.ShouldBeFalse(comparer.GetAllErrors);
}
}
- The
CompareEfWithDb
method can take multiple DbContexts, known as bounded contexts (see chapter 13, section 13.4.8 in my book Entity Framework Core in Action, second edition). You can add as many contexts and they are compared to one database. - You can also provide a string that points to the database as the first parameter. It can have two forms:
- It will use the string as a connection string name in the test's
appsetting.json
file. - If no connection string is found in the
appsetting.json
file, or there is noappsetting.json
, then it assumes the string is a connection string.
- It will use the string as a connection string name in the test's
See below for an example of both of of these options:
[Fact]
public void CompareBookThenOrderAgainstBookOrderDatabaseViaAppSettings()
{
//SETUP
//... I have left out how the options are created
//This is the name of a connection string in the appsetting.json file in your test project
const string connectionStringName = "BookOrderConnection";
using (var context1 = new BookContext(options1))
using (var context2 = new OrderContext(options2))
{
var comparer = new CompareEfSql();
//ATTEMPT
//Its starts with the connection string/name and then you can have as many contexts as you like
var hasErrors = comparer.CompareEfWithDb(connectionStringName, context1, context2);
//VERIFY
hasErrors.ShouldBeFalse(comparer.GetAllErrors);
}
}
The comparer.GetAllErrors
property will return a string with each error separated by the Environment.NewLine
string. Below is an example of an error
"DIFFERENT: MyEntity->Property 'MyString'. Expected = varchar(max), found = nvarchar(max)"
The error above says
DIFFERENT:
There is a difference between EF Core and the database (other settings areNotInDatabase
,ExtraInDatabase
)MyEntity->Property 'MyString', column type
gives a description of what was checkedExpected = varchar(max)
says what EF Core thought it should befound = nvarchar(max)
says what the database setting was
Here is another error coming from stage 2 where it checks the database side, i.e., Unused Tables, Columns and Indexes
EXTRA IN DATABASE: Table 'HeadEntries', column name. Found = DifferentColumnName
This says that there is a column called MyEntityId
in the table MyEntites
that hasn't got a property in the entity class mapped to the MyEntites
table.
NOTE: When errors contain the word Table
it can be a SQL Table or View.
In a few cases you will get errors that aren't correct (see limitations) or not relevant. In these cases you might want to suppress those errors. There are two way to do this, with the first being the easiest. Both use the CompareEfSqlConfig
class.
In this approach you capture the error strings you want to ignore and return them as a string, with each error separated by the newline, '\n'
, character. You feed the errors via the configuration's IgnoreTheseErrors
method. See an example below
public void CompareTptContextSuppressViaIgnoreTheseErrors()
{
//SETUP
var options = this.CreateUniqueClassOptions<TptDbContext>();
using var context = new TptDbContext(options);
context.Database.EnsureClean();
var config = new CompareEfSqlConfig();
//This converts the error strings back CompareLog classes (see next example) which suppresses these errors
config.IgnoreTheseErrors(@"DIFFERENT: TptVer1->PrimaryKey 'PK_TptBases', constraint name. Expected = PK_TptBases, found = PK_TptVer1
DIFFERENT: TptVer1->Property 'Id', value generated. Expected = OnAdd, found = Never
DIFFERENT: TptVer1->Property 'MyVer1Int', nullability. Expected = NULL, found = NOT NULL
DIFFERENT: TptVer1->ForeignKey 'FK_TptVer1_TptBases_Id', delete behavior. Expected = ClientCascade, found = NoAction
DIFFERENT: Entity 'TptVer1', constraint name. Expected = PK_TptBases, found = PK_TptVer1
DIFFERENT: TptVer2->PrimaryKey 'PK_TptBases', constraint name. Expected = PK_TptBases, found = PK_TptVer2
DIFFERENT: TptVer2->Property 'Id', value generated. Expected = OnAdd, found = Never
DIFFERENT: TptVer2->Property 'MyVer2Int', nullability. Expected = NULL, found = NOT NULL
DIFFERENT: TptVer2->ForeignKey 'FK_TptVer2_TptBases_Id', delete behavior. Expected = ClientCascade, found = NoAction
DIFFERENT: Entity 'TptVer2', constraint name. Expected = PK_TptBases, found = PK_TptVer2");
var comparer = new CompareEfSql(config);
//ATTEMPT
var hasErrors = comparer.CompareEfWithDb(context);
//VERIFY
hasErrors.ShouldBeFalse(comparer.GetAllErrors);
}
}
The other approach is useful when you want to suppress a general set of errors, but it is a bit complicated. Here is an example where it suppresses any errors found on the default value set on a column.
[Fact]
public void CompareSuppressViaViaAddIgnoreCompareLog()
{
//SETUP
var options = this.CreateUniqueClassOptions<BookContext>();
using var context = new BookContext(options);
context.Database.EnsureClean();
var config = new CompareEfSqlConfig
config.AddIgnoreCompareLog(new CompareLog(CompareType.Property, CompareState.Different, null, CompareAttributes.DefaultValueSql));
var comparer = new CompareEfSql(config);
//ATTEMPT
var hasErrors = comparer.CompareEfWithDb(context);
//VERIFY
hasErrors.ShouldBeFalse(comparer.GetAllErrors);
}
You have already seen the class called CompareEfSqlConfig
for suppressing errors, but there are two other configrations.
You have already seen the class called CompareEfSqlConfig
for suppressing errors. There is one other configuration property called TablesToIgnoreCommaDelimited
, which allows you to control what table/views in the database are considered.
By default (i.e. when TablesToIgnoreCommaDelimited
is null) then CompareEfSql
will only look at the tables/views in the database that your EF Core entity classes are mapped to. This provides an simple starting point. The other options are:
- Set
TablesToIgnoreCommaDelimited
to "" (i.e. empty string)
This will check all the tables/Views in the database. - Set
TablesToIgnoreCommaDelimited
to a list of tables to ignore.
If there are tables/views in your database that EF Core doesn't access then you need to tellCompareEfSql
about them, otherwise it will output a message saying there are extra tables you are not accessing from EF Core. You do this by providing a comma delimited list of table names, with an optional schema name if needed. Here are two examples of a table nameMyTable
- this has no schema, so the default schema of the database will be useddbo.MyTable
- this defines the schema to bedbo
, - a full stop separates the schema name from the table name.
NOTE: The comparison is case insensitive.
Here is an example of configuring the comparer to not look at the tables Orders
and LineItem
var config = new CompareEfSqlConfig
{
TablesToIgnoreCommaDelimited = "Orders,LineItem"
};
var comparer = new CompareEfSql(config);
Getting all the errors in one go can be useful, for instance when you are creating a EF Core DbContext to match a given database. But by default, Stage 2 isn't run if Stage 1 found errors that haven't been register in config.IgnoreTheseErrors(... your error strings ...)
.
In version 8.2.0 a new boolean property called AlwaysRunStage2
in CompareEfSqlConfig
and if you you set this to true
then Stage 2 will always run, even if there are non-ignored errors. See issue #38 which made me add this new feature.