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(); + } + } +}