From 5b9bdf0be1d059a2cf7a2408bc146cff5cb1bae1 Mon Sep 17 00:00:00 2001 From: Jay Harris Date: Wed, 8 Apr 2020 16:22:33 -0700 Subject: [PATCH] feat(efcore3): add support for efcore v3 --- .../EntityFrameworkCore.sln | 30 +++++ ...web.EntityFrameworkCore3.Validation.csproj | 26 ++++ .../SchemaComparison.cs | 107 +++++++++++++++++ .../SchemaValidator.cs | 112 ++++++++++++++++++ ...tityFrameworkCore3.Validation.Tests.csproj | 79 ++++++++++++ 5 files changed, 354 insertions(+) create mode 100644 src/entityframeworkcore/src/entityframeworkcore3.validation/Aranasoft.Cobweb.EntityFrameworkCore3.Validation.csproj create mode 100644 src/entityframeworkcore/src/entityframeworkcore3.validation/SchemaComparison.cs create mode 100644 src/entityframeworkcore/src/entityframeworkcore3.validation/SchemaValidator.cs create mode 100644 src/entityframeworkcore/test/entityframeworkcore3.validation.tests/Aranasoft.Cobweb.EntityFrameworkCore3.Validation.Tests.csproj diff --git a/src/entityframeworkcore/EntityFrameworkCore.sln b/src/entityframeworkcore/EntityFrameworkCore.sln index 293a9a4..a27be28 100644 --- a/src/entityframeworkcore/EntityFrameworkCore.sln +++ b/src/entityframeworkcore/EntityFrameworkCore.sln @@ -22,6 +22,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_deps", "_deps", "{6CB010A9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aranasoft.Cobweb.FluentMigrator3.Extensions", "..\fluentmigrator\src\fluentmigrator3.extensions\Aranasoft.Cobweb.FluentMigrator3.Extensions.csproj", "{F743C645-C632-4692-B87E-60FCC9557C20}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aranasoft.Cobweb.EntityFrameworkCore3.Validation", "src\entityframeworkcore3.validation\Aranasoft.Cobweb.EntityFrameworkCore3.Validation.csproj", "{B5EF01F1-5FB0-43F7-ABE4-040CC7833953}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aranasoft.Cobweb.EntityFrameworkCore3.Validation.Tests", "test\entityframeworkcore3.validation.tests\Aranasoft.Cobweb.EntityFrameworkCore3.Validation.Tests.csproj", "{6B754961-97C2-43FE-8132-2B7FADEC03FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +72,30 @@ Global {F743C645-C632-4692-B87E-60FCC9557C20}.Release|x64.Build.0 = Release|Any CPU {F743C645-C632-4692-B87E-60FCC9557C20}.Release|x86.ActiveCfg = Release|Any CPU {F743C645-C632-4692-B87E-60FCC9557C20}.Release|x86.Build.0 = Release|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Debug|x64.Build.0 = Debug|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Debug|x86.Build.0 = Debug|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Release|Any CPU.Build.0 = Release|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Release|x64.ActiveCfg = Release|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Release|x64.Build.0 = Release|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Release|x86.ActiveCfg = Release|Any CPU + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953}.Release|x86.Build.0 = Release|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Debug|x64.Build.0 = Debug|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Debug|x86.Build.0 = Debug|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Release|Any CPU.Build.0 = Release|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Release|x64.ActiveCfg = Release|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Release|x64.Build.0 = Release|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Release|x86.ActiveCfg = Release|Any CPU + {6B754961-97C2-43FE-8132-2B7FADEC03FA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -76,6 +104,8 @@ Global {63AE368C-C059-4CF5-A75F-C96F53FE2030} = {091B6554-596C-4EEC-B37E-271DB9B5E10A} {826678B4-32FC-4DFF-A83D-8A89CC9F3AFA} = {381B9DD8-06CA-42CA-9A5A-69577E25B584} {F743C645-C632-4692-B87E-60FCC9557C20} = {6CB010A9-4ADC-4F5B-843C-0490204A9110} + {B5EF01F1-5FB0-43F7-ABE4-040CC7833953} = {091B6554-596C-4EEC-B37E-271DB9B5E10A} + {6B754961-97C2-43FE-8132-2B7FADEC03FA} = {381B9DD8-06CA-42CA-9A5A-69577E25B584} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {77E1A84A-D7A1-4FAE-90E7-2D5C1912349A} diff --git a/src/entityframeworkcore/src/entityframeworkcore3.validation/Aranasoft.Cobweb.EntityFrameworkCore3.Validation.csproj b/src/entityframeworkcore/src/entityframeworkcore3.validation/Aranasoft.Cobweb.EntityFrameworkCore3.Validation.csproj new file mode 100644 index 0000000..9fdcf57 --- /dev/null +++ b/src/entityframeworkcore/src/entityframeworkcore3.validation/Aranasoft.Cobweb.EntityFrameworkCore3.Validation.csproj @@ -0,0 +1,26 @@ + + + + + netstandard2.1 + Aranasoft.Cobweb.EntityFrameworkCore.Validation + Aranasoft.Cobweb.EntityFrameworkCore.Validation + 1.30.0 + + + + Schema validation and testing components for Entity Framework Core. + entityframeworkcore;efcore;validation;$(PackageTags) + + + + + + + + + + + + + diff --git a/src/entityframeworkcore/src/entityframeworkcore3.validation/SchemaComparison.cs b/src/entityframeworkcore/src/entityframeworkcore3.validation/SchemaComparison.cs new file mode 100644 index 0000000..24e5576 --- /dev/null +++ b/src/entityframeworkcore/src/entityframeworkcore3.validation/SchemaComparison.cs @@ -0,0 +1,107 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; + +namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation { + internal static class SchemaComparison { + public static bool TableExists(this DatabaseModel model, IEntityType type) { + return TableExists(model, type.GetSchema() ?? model.DefaultSchema, type.GetTableName()); + } + + public static bool TableExists(this DatabaseModel model, string schema, string tableName) { + var checkedSchema = schema ?? model.DefaultSchema; + return model.Tables.Any(table => table.Schema == checkedSchema && table.Name == tableName); + } + + public static DatabaseTable GetTable(this DatabaseModel model, IEntityType type) { + return GetTable(model, type.GetSchema() ?? model.DefaultSchema, type.GetTableName()); + } + + public static DatabaseTable GetTable(DatabaseModel model, string schema, string tableName) { + var checkedSchema = schema ?? model.DefaultSchema; + return model.Tables.FirstOrDefault(table => table.Schema == checkedSchema && table.Name == tableName); + } + + public static bool ColumnExists(this DatabaseModel model, IProperty property) { + return ColumnExists(model, property.DeclaringEntityType, property); + } + + public static bool ColumnExists(this DatabaseModel model, + IEntityType type, + IProperty property) { + return ColumnExists(model, type.GetSchema(), type.GetTableName(), property.GetColumnName()); + } + + public static bool ColumnExists(this DatabaseModel model, string schema, string tableName, string columnName) { + var tableModel = GetTable(model, schema, tableName); + return tableModel != null && tableModel.Columns.Any(column => column.Name == columnName); + } + + public static DatabaseColumn GetColumn(this DatabaseModel model, IProperty property) { + return GetColumn(model, property.DeclaringEntityType, property); + } + + public static DatabaseColumn GetColumn(this DatabaseModel model, IEntityType type, IProperty property) { + return GetColumn(model, type.GetSchema(), type.GetTableName(), property.GetColumnName()); + } + + public static DatabaseColumn GetColumn(this DatabaseModel model, + string schema, + string tableName, + string columnName) { + var tableModel = GetTable(model, schema, tableName); + return tableModel?.Columns.FirstOrDefault(column => column.Name == columnName); + } + + public static bool IndexExists(this DatabaseModel model, IIndex index) { + var entityType = index.DeclaringEntityType; + return IndexExists(model, entityType.GetSchema(), entityType.GetTableName(), index.GetName()); + } + + public static bool IndexExists(this DatabaseModel model, string schema, string tableName, string indexName) { + var tableModel = GetTable(model, schema, tableName); + return tableModel != null && tableModel.Indexes.Any(column => column.Name == indexName); + } + + public static DatabaseIndex GetIndex(this DatabaseModel model, IIndex index) { + var entityType = index.DeclaringEntityType; + return GetIndex(model, entityType.GetSchema(), entityType.GetTableName(), index.GetName()); + } + + public static DatabaseIndex GetIndex(this DatabaseModel model, + string schema, + string tableName, + string indexName) { + var tableModel = GetTable(model, schema, tableName); + return tableModel?.Indexes.FirstOrDefault(index => index.Name == indexName); + } + + public static bool ForeignKeyExists(this DatabaseModel model, + IForeignKey foreignKey) { + var entityType = foreignKey.DeclaringEntityType; + return ForeignKeyExists(model, entityType.GetSchema(), entityType.GetTableName(), foreignKey.GetConstraintName()); + } + + public static bool ForeignKeyExists(this DatabaseModel model, + string schema, + string tableName, + string foreignKeyName) { + var tableModel = GetTable(model, schema, tableName); + return tableModel != null && tableModel.ForeignKeys.Any(column => column.Name == foreignKeyName); + } + + public static DatabaseForeignKey GetForeignKey(this DatabaseModel model, IForeignKey foreignKey) { + var entityType = foreignKey.DeclaringEntityType; + return GetForeignKey(model, entityType.GetSchema(), entityType.GetTableName(), foreignKey.GetConstraintName()); + } + + public static DatabaseForeignKey GetForeignKey(this DatabaseModel model, + string schema, + string tableName, + string foreignKeyName) { + var tableModel = GetTable(model, schema, tableName); + return tableModel?.ForeignKeys.FirstOrDefault(index => index.Name == foreignKeyName); + } + } +} diff --git a/src/entityframeworkcore/src/entityframeworkcore3.validation/SchemaValidator.cs b/src/entityframeworkcore/src/entityframeworkcore3.validation/SchemaValidator.cs new file mode 100644 index 0000000..1ab8a11 --- /dev/null +++ b/src/entityframeworkcore/src/entityframeworkcore3.validation/SchemaValidator.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Scaffolding; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; + +namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation { + public class SchemaValidator { + protected DbContext Context { get; } + + public SchemaValidator(DbContext context) { + Context = context; + } + + public void ValidateSchema(SchemaValidationOptions validationOptions = null) { + if (validationOptions == null) { + validationOptions = new SchemaValidationOptions(); + } + + var databaseModel = GetDatabaseModel(); + + var entityModel = Context.Model; + var validationErrors = new List(); + var persistedTypes = entityModel.GetEntityTypes().Where(entityType => entityType.FindPrimaryKey() != null); + + foreach (var persistedType in persistedTypes) { + var dbTable = databaseModel.GetTable(persistedType); + + if (dbTable == null) { + validationErrors.Add($"Missing table: {persistedType.GetTableName()}"); + continue; + } + + validationErrors.AddRange(ValidateColumns(databaseModel, persistedType)); + + if (validationOptions.ValidateIndexes) { + validationErrors.AddRange(ValidateIndexes(databaseModel, persistedType)); + } + + if (validationOptions.ValidateForeignKeys) { + validationErrors.AddRange(ValidateForeignKeys(databaseModel, persistedType)); + } + } + + if (validationErrors.Count > 0) { + throw new SchemaValidationException("Schema validation failed", validationErrors); + } + } + + private List ValidateColumns(DatabaseModel databaseModel, IEntityType persistedType) { + var valErrors = new List(); + foreach (var persistedColumn in persistedType.GetProperties()) { + var dbColumn = databaseModel.GetColumn(persistedColumn); + if (dbColumn == null) { + valErrors.Add($"Missing column: {persistedColumn.GetColumnName()} in {persistedType.GetTableName()}"); + continue; + } + + var columnTypesMatch = + dbColumn.StoreType.Equals(persistedColumn.GetColumnType(), StringComparison.OrdinalIgnoreCase); + if (!columnTypesMatch) { + valErrors.Add( + $"Column type mismatch in {persistedType.GetTableName()} for column {persistedColumn.GetColumnName()}. Found: {dbColumn.StoreType.ToLowerInvariant()}, Expected {persistedColumn.GetColumnType().ToLowerInvariant()}"); + } + + if (persistedColumn.IsNullable != dbColumn.IsNullable) { + valErrors.Add( + $"Column nullability mismatch in {persistedType.GetTableName()} for column {persistedColumn.GetColumnName()}. Found: {(dbColumn.IsNullable ? "Nullable" : "NotNullable")}, Expected {(persistedColumn.IsNullable ? "Nullable" : "NotNullable")}"); + } + } + + return valErrors; + } + + private IEnumerable ValidateIndexes(DatabaseModel databaseModel, IEntityType persistedType) { + var validationErrors = new List(); + + foreach (var index in persistedType.GetIndexes()) { + var dbIndex = databaseModel.GetIndex(index); + if (dbIndex == null) { + validationErrors.Add( + $"Missing index: {index.GetName()} on {persistedType.GetTableName()}"); + } + } + + return validationErrors; + } + + private IEnumerable ValidateForeignKeys(DatabaseModel databaseModel, IEntityType persistedType) { + var validationErrors = new List(); + + foreach (var foreignKey in persistedType.GetForeignKeys()) { + var databaseForeignKey = databaseModel.GetForeignKey(foreignKey); + if (databaseForeignKey == null) { + validationErrors.Add( + $"Missing Foreign Key: {foreignKey.GetConstraintName()} on {persistedType.GetTableName()}"); + } + } + + return validationErrors; + } + + private DatabaseModel GetDatabaseModel() { + var factory = Context.GetService(); + var databaseModel = factory.Create(Context.Database.GetDbConnection(), new DatabaseModelFactoryOptions()); + return databaseModel; + } + } +} diff --git a/src/entityframeworkcore/test/entityframeworkcore3.validation.tests/Aranasoft.Cobweb.EntityFrameworkCore3.Validation.Tests.csproj b/src/entityframeworkcore/test/entityframeworkcore3.validation.tests/Aranasoft.Cobweb.EntityFrameworkCore3.Validation.Tests.csproj new file mode 100644 index 0000000..da057f0 --- /dev/null +++ b/src/entityframeworkcore/test/entityframeworkcore3.validation.tests/Aranasoft.Cobweb.EntityFrameworkCore3.Validation.Tests.csproj @@ -0,0 +1,79 @@ + + + + + netcoreapp3.0 + Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests + Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + +