Skip to content

Commit

Permalink
feat(efcore3): add support for efcore v3
Browse files Browse the repository at this point in the history
  • Loading branch information
jayharris committed Apr 8, 2020
1 parent c612b78 commit 5b9bdf0
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 0 deletions.
30 changes: 30 additions & 0 deletions src/entityframeworkcore/EntityFrameworkCore.sln
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\netfx.props" />

<PropertyGroup>
<TargetFrameworks>netstandard2.1</TargetFrameworks>
<AssemblyName>Aranasoft.Cobweb.EntityFrameworkCore.Validation</AssemblyName>
<RootNamespace>Aranasoft.Cobweb.EntityFrameworkCore.Validation</RootNamespace>
<VersionPrefix>1.30.0</VersionPrefix>
</PropertyGroup>

<PropertyGroup>
<Description>Schema validation and testing components for Entity Framework Core.</Description>
<PackageTags>entityframeworkcore;efcore;validation;$(PackageTags)</PackageTags>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\entityframeworkcore2.validation\SchemaValidationException.cs" Link="SchemaValidationException.cs" />
<Compile Include="..\entityframeworkcore2.validation\SchemaValidationOptions.cs" Link="SchemaValidationOptions.cs" />
<Compile Include="..\entityframeworkcore2.validation\SchemaValidatorExtensions.cs" Link="SchemaValidatorExtensions.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[3,4)" />
</ItemGroup>

</Project>
@@ -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);
}
}
}
@@ -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<string>();
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<string> ValidateColumns(DatabaseModel databaseModel, IEntityType persistedType) {
var valErrors = new List<string>();
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<string> ValidateIndexes(DatabaseModel databaseModel, IEntityType persistedType) {
var validationErrors = new List<string>();

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<string> ValidateForeignKeys(DatabaseModel databaseModel, IEntityType persistedType) {
var validationErrors = new List<string>();

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<IDatabaseModelFactory>();
var databaseModel = factory.Create(Context.Database.GetDbConnection(), new DatabaseModelFactoryOptions());
return databaseModel;
}
}
}

0 comments on commit 5b9bdf0

Please sign in to comment.