diff --git a/src/entityframeworkcore/EntityFrameworkCore.sln b/src/entityframeworkcore/EntityFrameworkCore.sln
index fd6df04..fcc0e02 100644
--- a/src/entityframeworkcore/EntityFrameworkCore.sln
+++ b/src/entityframeworkcore/EntityFrameworkCore.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29519.87
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{091B6554-596C-4EEC-B37E-271DB9B5E10A}"
EndProject
@@ -30,6 +30,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aranasoft.Cobweb.EntityFram
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aranasoft.Cobweb.EntityFrameworkCore5.Validation.Tests", "test\entityframeworkcore5.validation.tests\Aranasoft.Cobweb.EntityFrameworkCore5.Validation.Tests.csproj", "{5477CEB7-5A6E-4212-BC86-65509034E371}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aranasoft.Cobweb.EntityFrameworkCore6.Validation", "src\entityframeworkcore6.validation\Aranasoft.Cobweb.EntityFrameworkCore6.Validation.csproj", "{5885A8A3-453C-4CE1-A2BB-1B2686B3728D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aranasoft.Cobweb.EntityFrameworkCore6.Validation.Tests", "test\entityframeworkcore6.validation.tests\Aranasoft.Cobweb.EntityFrameworkCore6.Validation.Tests.csproj", "{689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -94,6 +98,22 @@ Global
{5477CEB7-5A6E-4212-BC86-65509034E371}.Release|Any CPU.Build.0 = Release|Any CPU
{5477CEB7-5A6E-4212-BC86-65509034E371}.Release|x64.ActiveCfg = Release|x64
{5477CEB7-5A6E-4212-BC86-65509034E371}.Release|x64.Build.0 = Release|x64
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D}.Debug|x64.ActiveCfg = Debug|x64
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D}.Debug|x64.Build.0 = Debug|x64
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D}.Release|x64.ActiveCfg = Release|x64
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D}.Release|x64.Build.0 = Release|x64
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}.Debug|x64.ActiveCfg = Debug|x64
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}.Debug|x64.Build.0 = Debug|x64
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}.Release|x64.ActiveCfg = Release|x64
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -106,6 +126,8 @@ Global
{6B754961-97C2-43FE-8132-2B7FADEC03FA} = {381B9DD8-06CA-42CA-9A5A-69577E25B584}
{A25246BD-873C-460C-A65B-0C90740EE4F9} = {091B6554-596C-4EEC-B37E-271DB9B5E10A}
{5477CEB7-5A6E-4212-BC86-65509034E371} = {381B9DD8-06CA-42CA-9A5A-69577E25B584}
+ {5885A8A3-453C-4CE1-A2BB-1B2686B3728D} = {091B6554-596C-4EEC-B37E-271DB9B5E10A}
+ {689B40E5-6A6F-4D49-AF2D-CD9567B2DE9C} = {381B9DD8-06CA-42CA-9A5A-69577E25B584}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {77E1A84A-D7A1-4FAE-90E7-2D5C1912349A}
diff --git a/src/entityframeworkcore/src/entityframeworkcore6.validation/Aranasoft.Cobweb.EntityFrameworkCore6.Validation.csproj b/src/entityframeworkcore/src/entityframeworkcore6.validation/Aranasoft.Cobweb.EntityFrameworkCore6.Validation.csproj
new file mode 100644
index 0000000..412c120
--- /dev/null
+++ b/src/entityframeworkcore/src/entityframeworkcore6.validation/Aranasoft.Cobweb.EntityFrameworkCore6.Validation.csproj
@@ -0,0 +1,27 @@
+
+
+
+
+ net6.0
+ Aranasoft.Cobweb.EntityFrameworkCore.Validation
+ Aranasoft.Cobweb.EntityFrameworkCore.Validation
+ 1.60.0
+ AnyCPU;x64
+
+
+
+ Schema validation and testing components for Entity Framework Core.
+ entityframeworkcore;efcore;validation;$(PackageTags)
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/entityframeworkcore/src/entityframeworkcore6.validation/SchemaComparison.cs b/src/entityframeworkcore/src/entityframeworkcore6.validation/SchemaComparison.cs
new file mode 100644
index 0000000..eb6f647
--- /dev/null
+++ b/src/entityframeworkcore/src/entityframeworkcore6.validation/SchemaComparison.cs
@@ -0,0 +1,176 @@
+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 ViewExists(this DatabaseModel model, string schema, string viewName) {
+ var checkedSchema = schema ?? model.DefaultSchema;
+ return model.Tables.Any(table => table.Schema == checkedSchema && table.Name == viewName);
+ }
+
+ public static DatabaseTable GetView(this DatabaseModel model, IEntityType type) {
+ return GetTable(model, type.GetViewSchema() ?? model.DefaultSchema, type.GetViewName());
+ }
+
+ public static DatabaseTable GetView(DatabaseModel model, string schema, string viewName) {
+ var checkedSchema = schema ?? model.DefaultSchema;
+ return model.Tables.FirstOrDefault(table => table.Schema == checkedSchema && table.Name == viewName);
+ }
+
+ public static bool ViewColumnExists(this DatabaseModel model, IProperty property) {
+ return ViewColumnExists(model, property.DeclaringEntityType, property);
+ }
+
+ public static bool ViewColumnExists(this DatabaseModel model,
+ IEntityType type,
+ IProperty property) {
+ return ViewColumnExists(model,
+ type.GetViewSchema(),
+ type.GetViewName(),
+ property.GetColumnName(StoreObjectIdentifier.View(type.GetViewName(), type.GetSchema())));
+ }
+
+ public static bool ViewColumnExists(this DatabaseModel model,
+ string schema,
+ string viewName,
+ string columnName) {
+ var viewModel = GetView(model, schema, viewName);
+ return viewModel != null && viewModel.Columns.Any(column => column.Name == columnName);
+ }
+
+ public static DatabaseColumn GetViewColumn(this DatabaseModel model, IProperty property) {
+ return GetViewColumn(model, property.DeclaringEntityType, property);
+ }
+
+ public static DatabaseColumn GetViewColumn(this DatabaseModel model, IEntityType type, IProperty property) {
+ return GetViewColumn(model,
+ type.GetViewSchema(),
+ type.GetViewName(),
+ property.GetColumnName(StoreObjectIdentifier.View(type.GetViewName(), type.GetSchema())));
+ }
+
+ public static DatabaseColumn GetViewColumn(this DatabaseModel model,
+ string schema,
+ string viewName,
+ string columnName) {
+ var viewModel = GetView(model, schema, viewName);
+ return viewModel?.Columns.FirstOrDefault(column => column.Name == columnName);
+ }
+
+ 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 TableColumnExists(this DatabaseModel model, IProperty property) {
+ return TableColumnExists(model, property.DeclaringEntityType, property);
+ }
+
+ public static bool TableColumnExists(this DatabaseModel model,
+ IEntityType type,
+ IProperty property) {
+ return TableColumnExists(model,
+ type.GetSchema(),
+ type.GetTableName(),
+ property.GetColumnName(StoreObjectIdentifier.Table(type.GetTableName(), type.GetSchema())));
+ }
+
+ public static bool TableColumnExists(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 GetTableColumn(this DatabaseModel model, IProperty property) {
+ return GetTableColumn(model, property.DeclaringEntityType, property);
+ }
+
+ public static DatabaseColumn GetTableColumn(this DatabaseModel model, IEntityType type, IProperty property) {
+ return GetTableColumn(model,
+ type.GetSchema(),
+ type.GetTableName(),
+ property.GetColumnName(StoreObjectIdentifier.Table(type.GetTableName(), type.GetSchema())));
+ }
+
+ public static DatabaseColumn GetTableColumn(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.GetDatabaseName());
+ }
+
+ 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.GetDatabaseName());
+ }
+
+ 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/entityframeworkcore6.validation/SchemaValidator.cs b/src/entityframeworkcore/src/entityframeworkcore6.validation/SchemaValidator.cs
new file mode 100644
index 0000000..9f58408
--- /dev/null
+++ b/src/entityframeworkcore/src/entityframeworkcore6.validation/SchemaValidator.cs
@@ -0,0 +1,146 @@
+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) {
+ validationOptions ??= new SchemaValidationOptions();
+
+ var databaseModel = GetDatabaseModel();
+
+ var entityModel = Context.Model;
+ var validationErrors = new List();
+ var persistedTypes = entityModel.GetEntityTypes();
+
+ foreach (var persistedType in persistedTypes) {
+ 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 (databaseModel.GetTable(persistedType) == null) {
+ validationErrors.Add($"Missing table: {persistedType.GetTableName()}");
+ continue;
+ }
+
+ validationErrors.AddRange(ValidateColumns(databaseModel, persistedType, validationOptions));
+
+ 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,
+ SchemaValidationOptions validationOptions) {
+ var valErrors = new List();
+ foreach (var persistedColumn in persistedType.GetProperties()) {
+ var dbColumn = databaseModel.GetTableColumn(persistedColumn);
+
+ if (persistedType.FindAnnotation(RelationalAnnotationNames.TableName)?.Value != null) {
+ if (dbColumn == null) {
+ valErrors.Add(
+ $"Missing column: {persistedColumn.GetColumnName(StoreObjectIdentifier.Table(persistedType.GetTableName(), null))} in {persistedType.GetTableName()}");
+ continue;
+ }
+
+ var columnTypesMatch =
+ dbColumn.StoreType.Replace(", ",",").Equals(persistedColumn.GetColumnType().Replace(", ",","), StringComparison.OrdinalIgnoreCase);
+ if (!columnTypesMatch) {
+ valErrors.Add(
+ $"Column type mismatch in {persistedType.GetTableName()} for column {persistedColumn.GetColumnName(StoreObjectIdentifier.Table(persistedType.GetTableName(), null))}. Found: {dbColumn.StoreType.ToLowerInvariant()}, Expected {persistedColumn.GetColumnType().ToLowerInvariant()}");
+ }
+
+ if (validationOptions.ValidateNullabilityForTables && persistedColumn.IsNullable != dbColumn.IsNullable) {
+ valErrors.Add(
+ $"Column nullability mismatch in {persistedType.GetTableName()} for column {persistedColumn.GetColumnName(StoreObjectIdentifier.Table(persistedType.GetTableName(), null))}. Found: {(dbColumn.IsNullable ? "Nullable" : "NotNullable")}, Expected {(persistedColumn.IsNullable ? "Nullable" : "NotNullable")}");
+ }
+ }
+
+ if (persistedType.FindAnnotation(RelationalAnnotationNames.ViewName)?.Value != null) {
+ if (dbColumn == null) {
+ valErrors.Add(
+ $"Missing column: {persistedColumn.GetColumnName(StoreObjectIdentifier.View(persistedType.GetViewName(), null))} in {persistedType.GetViewName()}");
+ continue;
+ }
+
+ var columnTypesMatch =
+ dbColumn.StoreType.Replace(", ",",").Equals(persistedColumn.GetColumnType().Replace(", ",","), StringComparison.OrdinalIgnoreCase);
+ if (!columnTypesMatch) {
+ valErrors.Add(
+ $"Column type mismatch in {persistedType.GetViewName()} for column {persistedColumn.GetColumnName(StoreObjectIdentifier.View(persistedType.GetViewName(), null))}. Found: {dbColumn.StoreType.ToLowerInvariant()}, Expected {persistedColumn.GetColumnType().ToLowerInvariant()}");
+ }
+
+ if (validationOptions.ValidateNullabilityForViews && persistedColumn.IsNullable != dbColumn.IsNullable) {
+ valErrors.Add(
+ $"Column nullability mismatch in {persistedType.GetViewName()} for column {persistedColumn.GetColumnName(StoreObjectIdentifier.View(persistedType.GetViewName(), null))}. 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.GetDatabaseName()} on {persistedType.GetTableName()}");
+ }
+ }
+
+ return validationErrors;
+ }
+
+ private IEnumerable ValidateForeignKeys(DatabaseModel databaseModel, IEntityType persistedType) {
+ var validationErrors = new List();
+
+ foreach (var foreignKey in persistedType.GetForeignKeys()
+ .Where(key => key.PrincipalEntityType.FindAnnotation(
+ RelationalAnnotationNames.ViewDefinitionSql) ==
+ null)) {
+ 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/entityframeworkcore6.validation.tests/Aranasoft.Cobweb.EntityFrameworkCore6.Validation.Tests.csproj b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Aranasoft.Cobweb.EntityFrameworkCore6.Validation.Tests.csproj
new file mode 100644
index 0000000..edc029f
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Aranasoft.Cobweb.EntityFrameworkCore6.Validation.Tests.csproj
@@ -0,0 +1,84 @@
+
+
+
+
+ net6.0
+ Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests
+ Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests
+ AnyCPU;x64
+ EF1001
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/LocalDbTestingDatabase.cs b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/LocalDbTestingDatabase.cs
new file mode 100644
index 0000000..2f581fa
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/LocalDbTestingDatabase.cs
@@ -0,0 +1,104 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Data.SqlClient;
+
+namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.SqlServer {
+ class LocalDbTestingDatabase : IDisposable {
+ public string LocalDbConnectionString { get; set; }
+ public string DatabaseName { get; }
+
+ public LocalDbTestingDatabase() : this($"Test{new Random().Next()}") {}
+
+ public LocalDbTestingDatabase(string databaseName,
+ string localDbConnectionString = @"Data Source=(LocalDB)\MSSQLLocalDB") {
+ DatabaseName = databaseName;
+ LocalDbConnectionString = localDbConnectionString;
+ }
+
+ public async Task EnsureDatabaseAsync(CancellationToken cancellationToken = default) {
+ using (var connection = new SqlConnection(LocalDbConnectionString)) {
+ await connection.OpenAsync(cancellationToken);
+ var cmd = connection.CreateCommand();
+ cmd.CommandText =
+ $"CREATE DATABASE {DatabaseName} ON PRIMARY ( NAME={DatabaseName}_Data, FILENAME = '{GetDataFilePath()}' ) LOG ON ( NAME={DatabaseName}_Log, FILENAME = '{GetLogFilePath()}' )";
+ await cmd.ExecuteNonQueryAsync(cancellationToken);
+ connection.Close();
+ }
+
+ if (!File.Exists(GetDataFilePath()))
+ throw new Exception($"Failed to create database file: {GetDataFilePath()}");
+ }
+
+ public void EnsureDatabase() {
+ using (var connection = new SqlConnection(LocalDbConnectionString)) {
+ connection.Open();
+ var cmd = connection.CreateCommand();
+ cmd.CommandText =
+ $"CREATE DATABASE {DatabaseName} ON PRIMARY ( NAME={DatabaseName}_Data, FILENAME = '{GetDataFilePath()}' ) LOG ON ( NAME={DatabaseName}_Log, FILENAME = '{GetLogFilePath()}' )";
+ cmd.ExecuteNonQuery();
+ connection.Close();
+ }
+
+ if (!File.Exists(GetDataFilePath()))
+ throw new Exception($"Failed to create database file: {GetDataFilePath()}");
+ }
+
+ public string ConnectionString {
+ get {
+ return
+ $"{LocalDbConnectionString};Initial Catalog={DatabaseName};Integrated Security=True; MultipleActiveResultSets=True;AttachDBFilename={GetDataFilePath()}";
+ }
+ }
+
+ private void DeleteIfExists(string path) {
+ if (File.Exists(path)) File.Delete(path);
+ }
+
+ public void DeleteDatabase() {
+ DeleteIfExists(GetDataFilePath());
+ DeleteIfExists(GetLogFilePath());
+ }
+
+ public async Task DetachDatabaseAsync(CancellationToken cancellationToken = default) {
+ using (var connection = new SqlConnection(LocalDbConnectionString)) {
+ await connection.OpenAsync(cancellationToken);
+ var cmd = connection.CreateCommand();
+ cmd.CommandText =
+ $"ALTER DATABASE [{DatabaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; exec sp_detach_db N'{DatabaseName}'";
+ await cmd.ExecuteNonQueryAsync(cancellationToken);
+ connection.Close();
+ }
+ }
+
+ public void DetachDatabase() {
+ using (var connection = new SqlConnection(LocalDbConnectionString)) {
+ connection.Open();
+ var cmd = connection.CreateCommand();
+ cmd.CommandText =
+ $"ALTER DATABASE [{DatabaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; exec sp_detach_db N'{DatabaseName}'";
+ cmd.ExecuteNonQuery();
+ connection.Close();
+ }
+ }
+
+ private string GetDataFilePath() {
+ return Path.Combine(
+ Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
+ $"{DatabaseName}.mdf");
+ }
+
+ private string GetLogFilePath() {
+ return Path.Combine(
+ Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
+ $"{DatabaseName}_Log.ldf");
+ }
+
+ public void Dispose() {
+ DetachDatabase();
+ DeleteDatabase();
+ }
+ }
+}
diff --git a/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/SqlServerLocalDbFixture.cs b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/SqlServerLocalDbFixture.cs
new file mode 100644
index 0000000..b030067
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/SqlServerLocalDbFixture.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Data;
+using System.Data.Common;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.SqlServer {
+ public class SqlServerLocalDbFixture : IDisposable {
+ public ApplicationDbContext GetContext() {
+ return new ApplicationDbContext(_builder.Options);
+ }
+
+ private readonly LocalDbTestingDatabase _testingDatabase;
+ private readonly DbContextOptionsBuilder _builder;
+ private readonly DbConnection _dbConnection;
+
+ public SqlServerLocalDbFixture() {
+ var serviceCollection = new ServiceCollection().AddEntityFrameworkDesignTimeServices();
+ new SqlServerDesignTimeServices().ConfigureDesignTimeServices(serviceCollection);
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ _testingDatabase = new LocalDbTestingDatabase();
+ _testingDatabase.EnsureDatabase();
+
+ var connectionString = _testingDatabase.ConnectionString;
+ _dbConnection = new SqlConnection(connectionString);
+ _dbConnection.Open();
+
+ _builder = new DbContextOptionsBuilder();
+ _builder.UseSqlServer(_dbConnection);
+ _builder.UseApplicationServiceProvider(serviceProvider);
+ }
+
+ public virtual void Dispose() {
+ if (_dbConnection.State == ConnectionState.Open) _dbConnection.Close();
+ _dbConnection.Dispose();
+
+ _testingDatabase.Dispose();
+ }
+ }
+}
diff --git a/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/SqlServerTestingProcessor.cs b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/SqlServerTestingProcessor.cs
new file mode 100644
index 0000000..327b048
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/SqlServer/SqlServerTestingProcessor.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Data;
+using FluentMigrator.Runner.Generators.SqlServer;
+using FluentMigrator.Runner.Initialization;
+using FluentMigrator.Runner.Processors;
+using FluentMigrator.Runner.Processors.SqlServer;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.SqlServer {
+ public class SqlServerTestingProcessor : SqlServer2016Processor {
+ public SqlServerTestingProcessor(IDbConnection connection,
+ SqlServer2016Generator generator,
+ SqlServer2008Quoter quoter,
+ ILogger logger,
+ IOptionsSnapshot options,
+ IConnectionStringAccessor connectionStringAccessor,
+ IServiceProvider serviceProvider) : base(
+ logger,
+ quoter,
+ generator,
+ options,
+ connectionStringAccessor,
+ serviceProvider) {
+ Connection = connection;
+ }
+
+ public override string DatabaseType => "SqlServer-Test";
+
+ protected override void EnsureConnectionIsClosed() {}
+ }
+}
diff --git a/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/Sqlite/SqliteTestingProcessor.cs b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/Sqlite/SqliteTestingProcessor.cs
new file mode 100644
index 0000000..93539f6
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/Sqlite/SqliteTestingProcessor.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Data;
+using FluentMigrator.Runner.Generators.SQLite;
+using FluentMigrator.Runner.Initialization;
+using FluentMigrator.Runner.Processors;
+using FluentMigrator.Runner.Processors.SQLite;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support.Sqlite {
+ public class SqliteTestingProcessor : SQLiteProcessor {
+ public SqliteTestingProcessor(IDbConnection connection,
+ SQLiteDbFactory factory,
+ SQLiteGenerator generator,
+ ILogger logger,
+ IOptionsSnapshot options,
+ IConnectionStringAccessor connectionStringAccessor,
+ IServiceProvider serviceProvider,
+ SQLiteQuoter quoter) : base(
+ factory,
+ generator,
+ logger,
+ options,
+ connectionStringAccessor,
+ serviceProvider,
+ quoter) {
+ Connection = connection;
+ }
+
+ public override string DatabaseType => "SQLite-Test";
+
+ protected override void EnsureConnectionIsClosed() {}
+ }
+}
diff --git a/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/TableBasedChildEntity.cs b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/TableBasedChildEntity.cs
new file mode 100644
index 0000000..e2d94af
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/TableBasedChildEntity.cs
@@ -0,0 +1,15 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support {
+ public class TableBasedChildEntity {
+ [Key]
+ public int Id { get; set; }
+
+ [MaxLength(10000)]
+ public string? Name { get; set; }
+
+ [Required]
+ public ViewBasedEntity ViewEntity { get; set; }
+ }
+}
diff --git a/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/TableBasedEntity.cs b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/TableBasedEntity.cs
new file mode 100644
index 0000000..efb7a96
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/TableBasedEntity.cs
@@ -0,0 +1,20 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.AspNetCore.Identity;
+
+namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support {
+ public class TableBasedEntity {
+ [Key]
+ public int Id { get; set; }
+
+ [MaxLength(256)]
+ public string? Field { get; set; }
+
+ [Column("NumberValue")]
+ public decimal Number { get; set; }
+
+ [Required]
+ public IdentityRole Role { get; set; }
+ }
+}
diff --git a/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/ViewBasedEntity.cs b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/ViewBasedEntity.cs
new file mode 100644
index 0000000..2ae9096
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/ViewBasedEntity.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Identity;
+
+namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support {
+ public class ViewBasedEntity {
+ public int Id { get; set; }
+
+ [MaxLength(256)]
+ public string? Field { get; set; }
+
+ public IdentityRole Role { get; set; }
+ public TableBasedEntity TableEntity { get; set; }
+ }
+}
diff --git a/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/ViewBasedEntityMapping.cs b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/ViewBasedEntityMapping.cs
new file mode 100644
index 0000000..8125dc5
--- /dev/null
+++ b/src/entityframeworkcore/test/entityframeworkcore6.validation.tests/Support/ViewBasedEntityMapping.cs
@@ -0,0 +1,12 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Aranasoft.Cobweb.EntityFrameworkCore.Validation.Tests.Support {
+ public class ViewBasedEntityMapping : IEntityTypeConfiguration {
+ public void Configure(EntityTypeBuilder builder) {
+ builder.ToView("ViewBasedEntities").ToTable((string?) null);
+ builder.HasOne(entity => entity.TableEntity).WithMany().HasForeignKey(view => view.Id);
+ builder.HasOne(entity => entity.Role).WithMany();
+ }
+ }
+}