Skip to content

Commit

Permalink
feat(efcore): add support for not-stored columns, view columns
Browse files Browse the repository at this point in the history
 - handle not-stored columns such as NotMapped and HasComputedSql columns
 - validate column names and types on views
  • Loading branch information
jayharris committed Jun 18, 2022
1 parent b50574a commit 9235464
Show file tree
Hide file tree
Showing 37 changed files with 3,108 additions and 108 deletions.
Expand Up @@ -5,7 +5,7 @@
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>Aranasoft.Cobweb.EntityFrameworkCore.Validation</AssemblyName>
<RootNamespace>Aranasoft.Cobweb.EntityFrameworkCore.Validation</RootNamespace>
<VersionPrefix>1.20.5</VersionPrefix>
<VersionPrefix>1.20.6</VersionPrefix>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>

Expand Down
Expand Up @@ -56,7 +56,9 @@ public class SchemaValidator {
SchemaValidationOptions validationOptions) {
var entityTable = persistedType.Relational();
var valErrors = new List<string>();
foreach (var entityProperty in persistedType.GetProperties()) {
foreach (var entityProperty in persistedType.GetProperties()
.Where(theProperty => theProperty.BeforeSaveBehavior != PropertySaveBehavior.Ignore &&
theProperty.AfterSaveBehavior != PropertySaveBehavior.Ignore)) {
var entityColumn = entityProperty.Relational();

var dbColumn = databaseModel.GetColumn(entityProperty);
Expand Down
Expand Up @@ -5,7 +5,7 @@
<TargetFrameworks>netstandard2.1</TargetFrameworks>
<AssemblyName>Aranasoft.Cobweb.EntityFrameworkCore.Validation</AssemblyName>
<RootNamespace>Aranasoft.Cobweb.EntityFrameworkCore.Validation</RootNamespace>
<VersionPrefix>1.30.5</VersionPrefix>
<VersionPrefix>1.30.6</VersionPrefix>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>

Expand Down
Expand Up @@ -31,7 +31,6 @@ public class SchemaValidator {
validationErrors.Add(persistedType.FindAnnotation(RelationalAnnotationNames.ViewDefinition) != null
? $"Missing view: {persistedType.GetTableName()}"
: $"Missing table: {persistedType.GetTableName()}");

continue;
}

Expand All @@ -57,7 +56,9 @@ public class SchemaValidator {
IEntityType persistedType,
SchemaValidationOptions validationOptions) {
var valErrors = new List<string>();
foreach (var persistedColumn in persistedType.GetProperties()) {
foreach (var persistedColumn in persistedType.GetProperties()
.Where(theProperty => theProperty.GetBeforeSaveBehavior() != PropertySaveBehavior.Ignore &&
theProperty.GetAfterSaveBehavior() != PropertySaveBehavior.Ignore)) {
var dbColumn = databaseModel.GetColumn(persistedColumn);
if (dbColumn == null) {
valErrors.Add(
Expand Down
Expand Up @@ -5,7 +5,7 @@
<TargetFrameworks>netstandard2.1</TargetFrameworks>
<AssemblyName>Aranasoft.Cobweb.EntityFrameworkCore.Validation</AssemblyName>
<RootNamespace>Aranasoft.Cobweb.EntityFrameworkCore.Validation</RootNamespace>
<VersionPrefix>1.50.3</VersionPrefix>
<VersionPrefix>1.50.4</VersionPrefix>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>

Expand Down
Expand Up @@ -4,6 +4,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding;
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;

Expand All @@ -25,28 +26,35 @@ public class SchemaValidator {
var persistedTypes = entityModel.GetEntityTypes();

foreach (var persistedType in persistedTypes) {
if (databaseModel.GetView(persistedType) == null &&
persistedType.FindAnnotation(RelationalAnnotationNames.ViewName)?.Value != null) {
if (persistedType.GetViewName() != null &&
databaseModel.GetView(persistedType) == null) {
validationErrors.Add($"Missing view: {persistedType.GetViewName()}");
}

if (persistedType.FindAnnotation(RelationalAnnotationNames.TableName)?.Value == null) {
continue;
if (persistedType.GetTableName() == null) {
continue;
}
}

if (databaseModel.GetTable(persistedType) == null) {
if (persistedType.GetTableName() != null &&
databaseModel.GetTable(persistedType) == null) {
validationErrors.Add($"Missing table: {persistedType.GetTableName()}");
continue;

if (persistedType.GetViewName() == null) {
continue;
}
}

validationErrors.AddRange(ValidateColumns(databaseModel, persistedType, validationOptions));

if (validationOptions.ValidateIndexes) {
validationErrors.AddRange(ValidateIndexes(databaseModel, persistedType));
}
// For now, only validate Indexes and FKs on Tables
if (persistedType.GetTableName() != null) {
if (validationOptions.ValidateIndexes) {
validationErrors.AddRange(ValidateIndexes(databaseModel, persistedType));
}

if (validationOptions.ValidateForeignKeys) {
validationErrors.AddRange(ValidateForeignKeys(databaseModel, persistedType));
if (validationOptions.ValidateForeignKeys) {
validationErrors.AddRange(ValidateForeignKeys(databaseModel, persistedType));
}
}
}

Expand All @@ -59,10 +67,13 @@ public class SchemaValidator {
IEntityType persistedType,
SchemaValidationOptions validationOptions) {
var valErrors = new List<string>();
foreach (var persistedColumn in persistedType.GetProperties()) {
var dbColumn = databaseModel.GetTableColumn(persistedColumn);

if (persistedType.FindAnnotation(RelationalAnnotationNames.TableName)?.Value != null) {
foreach (var persistedColumn in persistedType.GetProperties()
.Where(theProperty => theProperty.GetBeforeSaveBehavior() != PropertySaveBehavior.Ignore ||
theProperty.GetAfterSaveBehavior() != PropertySaveBehavior.Ignore ||
theProperty.FindAnnotation(RelationalAnnotationNames.IsStored)?.Value is true)
) {
if (persistedType.GetTableName() != null) {
var dbColumn = databaseModel.GetTableColumn(persistedColumn);
if (dbColumn == null) {
valErrors.Add(
$"Missing column: {persistedColumn.GetColumnName(StoreObjectIdentifier.Table(persistedType.GetTableName(), null))} in {persistedType.GetTableName()}");
Expand All @@ -82,7 +93,8 @@ public class SchemaValidator {
}
}

if (persistedType.FindAnnotation(RelationalAnnotationNames.ViewName)?.Value != null) {
if (persistedType.GetViewName() != null) {
var dbColumn = databaseModel.GetViewColumn(persistedColumn);
if (dbColumn == null) {
valErrors.Add(
$"Missing column: {persistedColumn.GetColumnName(StoreObjectIdentifier.View(persistedType.GetViewName(), null))} in {persistedType.GetViewName()}");
Expand Down
Expand Up @@ -5,7 +5,7 @@
<TargetFrameworks>net6.0</TargetFrameworks>
<AssemblyName>Aranasoft.Cobweb.EntityFrameworkCore.Validation</AssemblyName>
<RootNamespace>Aranasoft.Cobweb.EntityFrameworkCore.Validation</RootNamespace>
<VersionPrefix>1.60.0</VersionPrefix>
<VersionPrefix>1.60.1</VersionPrefix>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>

Expand Down
Expand Up @@ -28,25 +28,32 @@ public class SchemaValidator {
if (databaseModel.GetView(persistedType) == null &&
persistedType.FindAnnotation(RelationalAnnotationNames.ViewName)?.Value != null) {
validationErrors.Add($"Missing view: {persistedType.GetViewName()}");
}

if (persistedType.FindAnnotation(RelationalAnnotationNames.TableName)?.Value == null) {
continue;
if (persistedType.FindAnnotation(RelationalAnnotationNames.TableName)?.Value == null) {
continue;
}
}

if (databaseModel.GetTable(persistedType) == null) {
if (databaseModel.GetTable(persistedType) == null &&
persistedType.FindAnnotation(RelationalAnnotationNames.TableName)?.Value != null) {
validationErrors.Add($"Missing table: {persistedType.GetTableName()}");
continue;

if (persistedType.FindAnnotation(RelationalAnnotationNames.ViewName)?.Value == null) {
continue;
}
}

validationErrors.AddRange(ValidateColumns(databaseModel, persistedType, validationOptions));

if (validationOptions.ValidateIndexes) {
validationErrors.AddRange(ValidateIndexes(databaseModel, persistedType));
}
// For now, only validate Indexes and FKs on Tables
if (persistedType.GetTableName() != null) {
if (validationOptions.ValidateIndexes) {
validationErrors.AddRange(ValidateIndexes(databaseModel, persistedType));
}

if (validationOptions.ValidateForeignKeys) {
validationErrors.AddRange(ValidateForeignKeys(databaseModel, persistedType));
if (validationOptions.ValidateForeignKeys) {
validationErrors.AddRange(ValidateForeignKeys(databaseModel, persistedType));
}
}
}

Expand All @@ -59,10 +66,13 @@ public class SchemaValidator {
IEntityType persistedType,
SchemaValidationOptions validationOptions) {
var valErrors = new List<string>();
foreach (var persistedColumn in persistedType.GetProperties()) {
var dbColumn = databaseModel.GetTableColumn(persistedColumn);

foreach (var persistedColumn in persistedType.GetProperties()
.Where(theProperty => theProperty.GetBeforeSaveBehavior() != PropertySaveBehavior.Ignore ||
theProperty.GetAfterSaveBehavior() != PropertySaveBehavior.Ignore ||
theProperty.FindAnnotation(RelationalAnnotationNames.IsStored)?.Value is true)
) {
if (persistedType.FindAnnotation(RelationalAnnotationNames.TableName)?.Value != null) {
var dbColumn = databaseModel.GetTableColumn(persistedColumn);
if (dbColumn == null) {
valErrors.Add(
$"Missing column: {persistedColumn.GetColumnName(StoreObjectIdentifier.Table(persistedType.GetTableName(), null))} in {persistedType.GetTableName()}");
Expand All @@ -83,6 +93,7 @@ public class SchemaValidator {
}

if (persistedType.FindAnnotation(RelationalAnnotationNames.ViewName)?.Value != null) {
var dbColumn = databaseModel.GetViewColumn(persistedColumn);
if (dbColumn == null) {
valErrors.Add(
$"Missing column: {persistedColumn.GetColumnName(StoreObjectIdentifier.View(persistedType.GetViewName(), null))} in {persistedType.GetViewName()}");
Expand Down
@@ -0,0 +1,99 @@
using System;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.Migrations;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.SqlServer;
using Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.XUnit;
using FluentAssertions;
using Xunit;

namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.SqlServer {
[OperatingSystemRequirement(OperatingSystems.Windows)]
public class
WhenValidatingSchemaGivenMissingColumns : IClassFixture<SqlServerMigrationsFixture<MigrationsMissingColumns>> {
private readonly SqlServerMigrationsFixture<MigrationsMissingColumns> _fixture;

public WhenValidatingSchemaGivenMissingColumns(SqlServerMigrationsFixture<MigrationsMissingColumns> fixture) {
_fixture = fixture;
}

[ConditionalFact]
public void ItShouldThrowValidationException() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should().ThrowExactly<SchemaValidationException>();
}

[ConditionalFact]
public void ItShouldHaveValidationErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should()
.Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should()
.NotBeEmpty();
}

[ConditionalFact]
public void ItShouldNotHaveMissingTableErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should()
.Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should()
.NotContain(
error => error.StartsWith("Missing Table",
StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveMissingViewErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should()
.Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should()
.NotContain(
error => error.StartsWith("Missing View", StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldOnlyHaveMissingColumnErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should()
.Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should()
.OnlyContain(
error => error.StartsWith("Missing Column",
StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveMissingIndexErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should()
.Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should()
.NotContain(
error => error.StartsWith("Missing Index",
StringComparison.InvariantCultureIgnoreCase));
}

[ConditionalFact]
public void ItShouldNotHaveMissingForeignKeyErrors() {
var applicationDbContext = _fixture.GetContext();
Action validatingSchema = () => applicationDbContext.ValidateSchema();
validatingSchema.Should()
.Throw<SchemaValidationException>()
.Which.ValidationErrors
.Should()
.NotContain(error => error.StartsWith("Missing Foreign Key",
StringComparison.InvariantCultureIgnoreCase));
}
}
}

0 comments on commit 9235464

Please sign in to comment.