From 9908ba4dca53f8364184c37dd50dc6d8467ebb9b Mon Sep 17 00:00:00 2001 From: Sascha Date: Sun, 2 Jul 2023 12:06:09 +0200 Subject: [PATCH] Added runtime migration for ef core (#58) * Added runtime migration for ef core * mostly copied from warfabrik clone, but also with some fixed and new features, so that the default ids for migrations work and the database context can be configured better * added name of configurators so that one can have seperated logic for the different configurators * made it possible to load multiple configurators, so that one could have multiple databases for different use cases in one program --- .../InMemoryConfigurator.cs | 28 +++++ ...amework.Extension.Database.InMemory.csproj | 21 ++++ ...Extension.Database.InMemory.props.template | 23 ++++ .../MSSQLConfigurator.cs | 29 +++++ ....Framework.Extension.Database.MSSQL.csproj | 21 ++++ ...rk.Extension.Database.MSSQL.props.template | 23 ++++ .../MySQLConfigurator.cs | 30 +++++ ....Framework.Extension.Database.MySql.csproj | 22 ++++ ...rk.Extension.Database.MySql.props.template | 23 ++++ ...tension.Database.PostgreSQL.props.template | 23 ++++ ...ework.Extension.Database.PostrgeSQL.csproj | 23 ++++ .../PostgresConfigurator.cs | 28 +++++ ...Framework.Extension.Database.Sqlite.csproj | 21 ++++ ...k.Extension.Database.Sqlite.props.template | 23 ++++ .../SqLiteConfigurator.cs | 30 +++++ .../DatabaseContext.cs | 115 ++++++++++++++++++ .../DatabaseFactory.cs | 52 ++++++++ .../IDatabaseConfigurator.cs | 13 ++ .../IEntity.cs | 7 ++ .../MigrationDatabaseContext.cs | 41 +++++++ .../Migrations/IAutoMigrationContext.cs | 20 +++ .../Migrations/IAutoMigrationTypeProvider.cs | 6 + .../Migrations/ModelMigration.cs | 85 +++++++++++++ ...ework.Extension.EntityFrameworkCore.csproj | 24 ++++ ...tension.EntityFrameworkCore.props.template | 23 ++++ NonSucking.Framework.Extension.sln | 45 +++++++ 26 files changed, 799 insertions(+) create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/InMemoryConfigurator.cs create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/NonSucking.Framework.Extension.Database.InMemory.csproj create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/NonSucking.Framework.Extension.Database.InMemory.props.template create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/MSSQLConfigurator.cs create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/NonSucking.Framework.Extension.Database.MSSQL.csproj create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/NonSucking.Framework.Extension.Database.MSSQL.props.template create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/MySQLConfigurator.cs create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/NonSucking.Framework.Extension.Database.MySql.csproj create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/NonSucking.Framework.Extension.Database.MySql.props.template create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/NonSucking.Framework.Extension.Database.PostgreSQL.props.template create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/NonSucking.Framework.Extension.Database.PostrgeSQL.csproj create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/PostgresConfigurator.cs create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/NonSucking.Framework.Extension.Database.Sqlite.csproj create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/NonSucking.Framework.Extension.Database.Sqlite.props.template create mode 100644 EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/SqLiteConfigurator.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/DatabaseContext.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/DatabaseFactory.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/IDatabaseConfigurator.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/IEntity.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/MigrationDatabaseContext.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/IAutoMigrationContext.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/IAutoMigrationTypeProvider.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/ModelMigration.cs create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/NonSucking.Framework.Extension.EntityFrameworkCore.csproj create mode 100644 NonSucking.Framework.Extension.EntityFrameworkCore/NonSucking.Framework.Extension.EntityFrameworkCore.props.template diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/InMemoryConfigurator.cs b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/InMemoryConfigurator.cs new file mode 100644 index 0000000..0661311 --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/InMemoryConfigurator.cs @@ -0,0 +1,28 @@ + +using NonSucking.Framework.Extension.EntityFrameworkCore; +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore; + +namespace NonSucking.Framework.Extension.Database.InMemory; +public class InMemoryConfigurator : IDatabaseConfigurator +{ + public string Name => "InMemory"; + public class MigrationContext : MigrationDatabaseContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + _ = optionsBuilder.UseInMemoryDatabase("Empty"); + base.OnConfiguring(optionsBuilder); + } + } + + public IAutoMigrationContextBuilder GetEmptyForMigration() + { + return new MigrationContext(); + } + + public DbContextOptionsBuilder OnConfiguring(DbContextOptionsBuilder optionsBuilder, string connectionString) + { + return optionsBuilder.UseInMemoryDatabase(connectionString); + } +} diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/NonSucking.Framework.Extension.Database.InMemory.csproj b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/NonSucking.Framework.Extension.Database.InMemory.csproj new file mode 100644 index 0000000..596520c --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/NonSucking.Framework.Extension.Database.InMemory.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + True + + + + diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/NonSucking.Framework.Extension.Database.InMemory.props.template b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/NonSucking.Framework.Extension.Database.InMemory.props.template new file mode 100644 index 0000000..3d98332 --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.InMemory/NonSucking.Framework.Extension.Database.InMemory.props.template @@ -0,0 +1,23 @@ + + + ${VERSION_FULL} + ${VERSION_LONG} + ${AUTHORS} + ${COPYRIGHT} + ${COMPANY} + ${TITLE_INMEMORY} + ${DESCRIPTION_INMEMORY} + + This is a first preview version and not intended for productive use. Not everything has been tested or commented yet. + + + ${TAGS_INMEMORY} + + + ${REPO_URL} + $(RepositoryUrl) + true + LICENSE + en-US + + \ No newline at end of file diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/MSSQLConfigurator.cs b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/MSSQLConfigurator.cs new file mode 100644 index 0000000..fbbfc1b --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/MSSQLConfigurator.cs @@ -0,0 +1,29 @@ + +using NonSucking.Framework.Extension.EntityFrameworkCore; +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore; + +namespace NonSucking.Framework.Extension.Database.MSSQL; +public class MSSQLConfigurator : IDatabaseConfigurator +{ + public string Name => "MSSQL"; + public class MigrationContext : MigrationDatabaseContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + _ = optionsBuilder.UseSqlServer(); + base.OnConfiguring(optionsBuilder); + } + + } + + public IAutoMigrationContextBuilder GetEmptyForMigration() + { + return new MigrationContext(); + } + + public DbContextOptionsBuilder OnConfiguring(DbContextOptionsBuilder optionsBuilder, string connectionString) + { + return optionsBuilder.UseSqlServer(connectionString); + } +} diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/NonSucking.Framework.Extension.Database.MSSQL.csproj b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/NonSucking.Framework.Extension.Database.MSSQL.csproj new file mode 100644 index 0000000..3286925 --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/NonSucking.Framework.Extension.Database.MSSQL.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + True + + + + diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/NonSucking.Framework.Extension.Database.MSSQL.props.template b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/NonSucking.Framework.Extension.Database.MSSQL.props.template new file mode 100644 index 0000000..adfcbf7 --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MSSQL/NonSucking.Framework.Extension.Database.MSSQL.props.template @@ -0,0 +1,23 @@ + + + ${VERSION_FULL} + ${VERSION_LONG} + ${AUTHORS} + ${COPYRIGHT} + ${COMPANY} + ${TITLE_MSSQL} + ${DESCRIPTION_MSSQL} + + This is a first preview version and not intended for productive use. Not everything has been tested or commented yet. + + + ${TAGS_MSSQL} + + + ${REPO_URL} + $(RepositoryUrl) + true + LICENSE + en-US + + \ No newline at end of file diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/MySQLConfigurator.cs b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/MySQLConfigurator.cs new file mode 100644 index 0000000..7f2d705 --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/MySQLConfigurator.cs @@ -0,0 +1,30 @@ + +using NonSucking.Framework.Extension.EntityFrameworkCore; +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore; + +namespace NonSucking.Framework.Extension.Database.MySql; +public class MySQLConfigurator : IDatabaseConfigurator +{ + public string Name => "MySQL"; + public class MigrationContext : MigrationDatabaseContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + _ = optionsBuilder.UseMySql("server=none;userid=none;password=none;database=none", ServerVersion.Create(10, 9, 3, Pomelo.EntityFrameworkCore.MySql.Infrastructure.ServerType.MariaDb)); + base.OnConfiguring(optionsBuilder); + } + + } + + public IAutoMigrationContextBuilder GetEmptyForMigration() + { + return new MigrationContext(); + } + + public DbContextOptionsBuilder OnConfiguring(DbContextOptionsBuilder optionsBuilder, string connectionString) + { + return optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); + } +} + diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/NonSucking.Framework.Extension.Database.MySql.csproj b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/NonSucking.Framework.Extension.Database.MySql.csproj new file mode 100644 index 0000000..703a48f --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/NonSucking.Framework.Extension.Database.MySql.csproj @@ -0,0 +1,22 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + True + + + + diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/NonSucking.Framework.Extension.Database.MySql.props.template b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/NonSucking.Framework.Extension.Database.MySql.props.template new file mode 100644 index 0000000..4441b0f --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.MySql/NonSucking.Framework.Extension.Database.MySql.props.template @@ -0,0 +1,23 @@ + + + ${VERSION_FULL} + ${VERSION_LONG} + ${AUTHORS} + ${COPYRIGHT} + ${COMPANY} + ${TITLE_MYSQL} + ${DESCRIPTION_MYSQL} + + This is a first preview version and not intended for productive use. Not everything has been tested or commented yet. + + + ${TAGS_MYSQL} + + + ${REPO_URL} + $(RepositoryUrl) + true + LICENSE + en-US + + \ No newline at end of file diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/NonSucking.Framework.Extension.Database.PostgreSQL.props.template b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/NonSucking.Framework.Extension.Database.PostgreSQL.props.template new file mode 100644 index 0000000..ec4acf7 --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/NonSucking.Framework.Extension.Database.PostgreSQL.props.template @@ -0,0 +1,23 @@ + + + ${VERSION_FULL} + ${VERSION_LONG} + ${AUTHORS} + ${COPYRIGHT} + ${COMPANY} + ${TITLE_POSTGRESQL} + ${DESCRIPTION_POSTGRESQL} + + This is a first preview version and not intended for productive use. Not everything has been tested or commented yet. + + + ${TAGS_POSTGRESQL} + + + ${REPO_URL} + $(RepositoryUrl) + true + LICENSE + en-US + + \ No newline at end of file diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/NonSucking.Framework.Extension.Database.PostrgeSQL.csproj b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/NonSucking.Framework.Extension.Database.PostrgeSQL.csproj new file mode 100644 index 0000000..c1866ab --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/NonSucking.Framework.Extension.Database.PostrgeSQL.csproj @@ -0,0 +1,23 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + True + + + + diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/PostgresConfigurator.cs b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/PostgresConfigurator.cs new file mode 100644 index 0000000..7158acd --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.PostrgeSQL/PostgresConfigurator.cs @@ -0,0 +1,28 @@ + +using NonSucking.Framework.Extension.EntityFrameworkCore; +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore; + +namespace NonSucking.Framework.Extension.Database.PostrgeSQL; +public class PostgresConfigurator : IDatabaseConfigurator +{ + public string Name => "PostrgeSQL"; + public class MigrationContext : MigrationDatabaseContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + _ = optionsBuilder.UseNpgsql(); + base.OnConfiguring(optionsBuilder); + } + + } + + public IAutoMigrationContextBuilder GetEmptyForMigration() + { + return new MigrationContext(); + } + public DbContextOptionsBuilder OnConfiguring(DbContextOptionsBuilder optionsBuilder, string connectionString) + { + return optionsBuilder.UseNpgsql(connectionString); + } +} diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/NonSucking.Framework.Extension.Database.Sqlite.csproj b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/NonSucking.Framework.Extension.Database.Sqlite.csproj new file mode 100644 index 0000000..7fb5cd3 --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/NonSucking.Framework.Extension.Database.Sqlite.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + True + + + + diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/NonSucking.Framework.Extension.Database.Sqlite.props.template b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/NonSucking.Framework.Extension.Database.Sqlite.props.template new file mode 100644 index 0000000..c283e3c --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/NonSucking.Framework.Extension.Database.Sqlite.props.template @@ -0,0 +1,23 @@ + + + ${VERSION_FULL} + ${VERSION_LONG} + ${AUTHORS} + ${COPYRIGHT} + ${COMPANY} + ${TITLE_SQLITE} + ${DESCRIPTION_SQLITE} + + This is a first preview version and not intended for productive use. Not everything has been tested or commented yet. + + + ${TAGS_SQLITE} + + + ${REPO_URL} + $(RepositoryUrl) + true + LICENSE + en-US + + \ No newline at end of file diff --git a/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/SqLiteConfigurator.cs b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/SqLiteConfigurator.cs new file mode 100644 index 0000000..22846e1 --- /dev/null +++ b/EntityFrameworkExtension/NonSucking.Framework.Extension.Database.Sqlite/SqLiteConfigurator.cs @@ -0,0 +1,30 @@ + +using Microsoft.EntityFrameworkCore; + +using NonSucking.Framework.Extension.EntityFrameworkCore; +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + +namespace NonSucking.Framework.Extension.Database.Sqlite; +public class SqLiteConfigurator : IDatabaseConfigurator +{ + public string Name => "SQLite"; + + public class MigrationContext : MigrationDatabaseContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + _ = optionsBuilder.UseSqlite(); + base.OnConfiguring(optionsBuilder); + } + + } + + public IAutoMigrationContextBuilder GetEmptyForMigration() + { + return new MigrationContext(); + } + public DbContextOptionsBuilder OnConfiguring(DbContextOptionsBuilder optionsBuilder, string connectionString) + { + return optionsBuilder.UseSqlite(connectionString); + } +} diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/DatabaseContext.cs b/NonSucking.Framework.Extension.EntityFrameworkCore/DatabaseContext.cs new file mode 100644 index 0000000..717ea0c --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/DatabaseContext.cs @@ -0,0 +1,115 @@ + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.Loader; + +namespace NonSucking.Framework.Extension.EntityFrameworkCore +{ + + public abstract class DatabaseContext : DbContext, IAutoMigrationContext + { + public bool EnableUseLazyLoading { get; set; } = true; + public string AssemblyRootName { get; set; } + public bool AddAllEntities { get; set; } + + protected DatabaseContext() + { + } + + protected DatabaseContext(DbContextOptions options) : base(options) + { + } + + protected DatabaseContext(bool enableUseLazyLoading, string assemblyRootName, bool addAllEntities) + { + EnableUseLazyLoading = enableUseLazyLoading; + AssemblyRootName = assemblyRootName; + AddAllEntities = addAllEntities; + } + + public bool FindLastMigration(Type contextAttributeType, [MaybeNullWhen(false)] out Migration migration, [MaybeNullWhen(false)] out string id) + { + migration = null; + id = ""; + TypeInfo? migrationType = null; + var assembly = Database.GetService(); + var migrationsInDb = Database.GetAppliedMigrations().OrderByDescending(id => id); + foreach (var item in migrationsInDb) + { + if (assembly.Migrations.TryGetValue(item, out migrationType)) + { + id = item; + break; + } + } + + if (string.IsNullOrWhiteSpace(id) || migrationType is null) + return false; + + migration = (Migration)Activator.CreateInstance(migrationType)!; + + return true; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseLazyLoadingProxies(EnableUseLazyLoading); + base.OnConfiguring(optionsBuilder); + } + + static Type[] onModelCreatingMethodTypes = new[] { typeof(ModelBuilder) }; + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + if (AddAllEntities) + { + + foreach (var type in AssemblyLoadContext + .Default + .Assemblies + .Where(x => string.IsNullOrWhiteSpace(AssemblyRootName) + || x.FullName.Contains(AssemblyRootName)) + .SelectMany(x => x.GetTypes()) + .Where(type => !type.IsAbstract + && !type.IsInterface + && typeof(IEntity).IsAssignableFrom(type)) + .Where(x => x.Namespace is not null && !x.Namespace.Contains("Migration"))) + { + if (modelBuilder.Model.FindEntityType(type) is null) + { + _ = modelBuilder.Model.AddEntityType(type); + } + var method = type.GetMethod(nameof(OnModelCreating), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, onModelCreatingMethodTypes); + if (method is not null) + method.Invoke(null, new[] { modelBuilder }); + } + } + + base.OnModelCreating(modelBuilder); + } + /// + /// Migrates with a transaction + /// + /// Applies any pending migrations for the context to the database. Will create the database + /// if it does not already exist. + /// + /// + /// Note that this API is mutually exclusive with . EnsureCreated does not use migrations + /// to create the database and therefore the database that is created cannot be later updated using migrations. + /// + /// + /// + /// See Database migrations for more information. + /// + /// The for the context. + public void Migrate() + { + Database.Migrate(); + } + } +} diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/DatabaseFactory.cs b/NonSucking.Framework.Extension.EntityFrameworkCore/DatabaseFactory.cs new file mode 100644 index 0000000..fde33a8 --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/DatabaseFactory.cs @@ -0,0 +1,52 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace NonSucking.Framework.Extension.EntityFrameworkCore +{ + + public static class DatabaseFactory + { + private static AssemblyLoadContext? assemblyLoadContext; + + public static List DatabaseConfigurators { get; } = new(); + + /// + /// Needs to be called, so that the are filled + /// + /// Path to the dll which contains the implementation(s) + public static void Initialize(string source) + { + if (DatabaseConfigurators.Any(x => source.Contains(x.Name, StringComparison.OrdinalIgnoreCase))) + return; + + if (assemblyLoadContext is null) + { + assemblyLoadContext = AssemblyLoadContext.GetLoadContext(typeof(IDatabaseConfigurator).Assembly); + assemblyLoadContext.Resolving += Default_Resolving; + } + var fullName = new FileInfo(source).FullName; + var databasePlugin = assemblyLoadContext.LoadFromAssemblyPath(fullName); + + //Get All IDatabaseConfigurator + DatabaseConfigurators + .AddRange(databasePlugin + .GetTypes() + .Where(x => x.IsAssignableTo(typeof(IDatabaseConfigurator)) && x.GetConstructor(Array.Empty()) != null) + .Select(x => (IDatabaseConfigurator)Activator.CreateInstance(x))); + } + + private static Assembly? Default_Resolving(AssemblyLoadContext context, AssemblyName name) + { + if (name.Name?.EndsWith("resources") ?? false) + return null; + + var existing = context.Assemblies.FirstOrDefault(x => x.FullName == name.FullName); + if (existing is not null) + return existing; + string assemblyPath = new FileInfo($"{name.Name}.dll").FullName; + if (assemblyPath is not null) + return context.LoadFromAssemblyPath(assemblyPath); + return null; + } + } +} diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/IDatabaseConfigurator.cs b/NonSucking.Framework.Extension.EntityFrameworkCore/IDatabaseConfigurator.cs new file mode 100644 index 0000000..0c49054 --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/IDatabaseConfigurator.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; + +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + +namespace NonSucking.Framework.Extension.EntityFrameworkCore +{ + public interface IDatabaseConfigurator + { + string Name { get; } + DbContextOptionsBuilder OnConfiguring(DbContextOptionsBuilder optionsBuilder, string connectionString); + IAutoMigrationContextBuilder GetEmptyForMigration(); + } +} diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/IEntity.cs b/NonSucking.Framework.Extension.EntityFrameworkCore/IEntity.cs new file mode 100644 index 0000000..181d264 --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/IEntity.cs @@ -0,0 +1,7 @@ +namespace NonSucking.Framework.Extension.EntityFrameworkCore +{ + public interface IEntity + { + + } +} diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/MigrationDatabaseContext.cs b/NonSucking.Framework.Extension.EntityFrameworkCore/MigrationDatabaseContext.cs new file mode 100644 index 0000000..46985c4 --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/MigrationDatabaseContext.cs @@ -0,0 +1,41 @@ + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; + +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + +namespace NonSucking.Framework.Extension.EntityFrameworkCore +{ + public abstract class MigrationDatabaseContext : DbContext, IAutoMigrationContextBuilder + { + public ModelBuilder CreateBuilder() + { + var dependencies = Database.GetService(); + var setBuilder = Database.GetService(); + var serviceProvider = Database.GetService(); + var modelConfigurationBuilder = + new ModelConfigurationBuilder(setBuilder.CreateConventionSet(), serviceProvider); + + return modelConfigurationBuilder.CreateModelBuilder(dependencies); + } + + public IModel FinalizeModel(IModel model) + { + var initializer = Database.GetService(); + return initializer.Initialize(model); + } + + public IReadOnlyList GenerateDiff(IModel? source, IModel? target) + { + var sourceModel = source?.GetRelationalModel(); + var targetModel = target?.GetRelationalModel(); + + var differ = Database.GetService(); + return differ.GetDifferences(sourceModel, targetModel); + } + } +} diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/IAutoMigrationContext.cs b/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/IAutoMigrationContext.cs new file mode 100644 index 0000000..4cfc194 --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/IAutoMigrationContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; + +using System.Diagnostics.CodeAnalysis; + +namespace NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + +public interface IAutoMigrationContextBuilder +{ + IModel FinalizeModel(IModel model); + IReadOnlyList GenerateDiff(IModel? source, IModel? target); + ModelBuilder CreateBuilder(); +} + +public interface IAutoMigrationContext +{ + bool FindLastMigration(Type contextAttributeType, [MaybeNullWhen(false)] out Migration migration, [MaybeNullWhen(false)] out string id); +} \ No newline at end of file diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/IAutoMigrationTypeProvider.cs b/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/IAutoMigrationTypeProvider.cs new file mode 100644 index 0000000..6fca5f3 --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/IAutoMigrationTypeProvider.cs @@ -0,0 +1,6 @@ +namespace NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + +public interface IAutoMigrationTypeProvider +{ + IReadOnlyList GetEntityTypes(); +} \ No newline at end of file diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/ModelMigration.cs b/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/ModelMigration.cs new file mode 100644 index 0000000..28b211b --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/Migrations/ModelMigration.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +using System.Reflection; + +namespace NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + +public static class ModelMigration +{ + public static void BuildVersion(this ModelBuilder modelBuilder, string version) + { + + } + + public static void BuildVersion(this ModelBuilder modelBuilder, IAutoMigrationTypeProvider typeProvider) + { + //TODO: Solution required our table names are property names from the context + foreach (var type in typeProvider.GetEntityTypes()) + { + if (modelBuilder.Model.FindEntityType(type) is null) + _ = modelBuilder.Model.AddEntityType(type); + } + } + + public static void SetUpgradeOperations(this MigrationBuilder migrationBuilder, Migration migration) + { + IAutoMigrationContextBuilder providerContextBuilder; + IModel target, source; + GetMigrationClasses(migration, out providerContextBuilder, out target, out source); + + var diff = providerContextBuilder.GenerateDiff(source, target); + migrationBuilder.Operations.AddRange(diff); + } + public static void SetDowngradeOperations(this MigrationBuilder migrationBuilder, Migration migration) + { + IAutoMigrationContextBuilder providerContextBuilder; + IModel target, source; + GetMigrationClasses(migration, out providerContextBuilder, out target, out source); + + var diff = providerContextBuilder.GenerateDiff(target, source); + migrationBuilder.Operations.AddRange(diff); + } + + private static void GetMigrationClasses(Migration migration, out IAutoMigrationContextBuilder providerContextBuilder, out IModel target, out IModel? source) + { + providerContextBuilder = DatabaseFactory.DatabaseConfigurators.First().GetEmptyForMigration(); + var migrationType = migration.GetType(); + var contextAttribute = migrationType.GetCustomAttribute() ?? throw new ArgumentNullException(); + var currentContext = (IAutoMigrationContext)Activator.CreateInstance(contextAttribute.ContextType)!; + + var targetBuilder = providerContextBuilder.CreateBuilder(); + + if (migration is IAutoMigrationTypeProvider autoTypeProvider) + { + targetBuilder.BuildVersion(autoTypeProvider); + } + else + { + var idAttribute = migrationType.GetCustomAttribute() ?? + throw new ArgumentNullException(); + + targetBuilder.BuildVersion(idAttribute.Id); + } + + target = providerContextBuilder.FinalizeModel((IModel)targetBuilder.Model); + source = null; + if (currentContext.FindLastMigration(contextAttribute.ContextType, out var lastMigration, out var lastMigrationId)) + { + var sourceBuilder = providerContextBuilder.CreateBuilder(); + + if (lastMigration is IAutoMigrationTypeProvider lastTypeProvider) + { + sourceBuilder.BuildVersion(lastTypeProvider); + } + else + { + sourceBuilder.BuildVersion(lastMigrationId); + } + + source = providerContextBuilder.FinalizeModel((IModel)sourceBuilder.Model); + } + } +} diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/NonSucking.Framework.Extension.EntityFrameworkCore.csproj b/NonSucking.Framework.Extension.EntityFrameworkCore/NonSucking.Framework.Extension.EntityFrameworkCore.csproj new file mode 100644 index 0000000..b87f850 --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/NonSucking.Framework.Extension.EntityFrameworkCore.csproj @@ -0,0 +1,24 @@ + + + + + false + net6.0 + enable + enable + en + LICENSE + + + + + True + + + + + + + + + diff --git a/NonSucking.Framework.Extension.EntityFrameworkCore/NonSucking.Framework.Extension.EntityFrameworkCore.props.template b/NonSucking.Framework.Extension.EntityFrameworkCore/NonSucking.Framework.Extension.EntityFrameworkCore.props.template new file mode 100644 index 0000000..6f6434d --- /dev/null +++ b/NonSucking.Framework.Extension.EntityFrameworkCore/NonSucking.Framework.Extension.EntityFrameworkCore.props.template @@ -0,0 +1,23 @@ + + + ${VERSION_FULL} + ${VERSION_LONG} + ${AUTHORS} + ${COPYRIGHT} + ${COMPANY} + ${TITLE_ENTITYFRAMEWORKCORE} + ${DESCRIPTION_ENTITYFRAMEWORKCORE} + + This is a first preview version and not intended for productive use. Not everything has been tested or commented yet. + + + ${TAGS_ENTITYFRAMEWORKCORE} + + + ${REPO_URL} + $(RepositoryUrl) + true + LICENSE + en-US + + \ No newline at end of file diff --git a/NonSucking.Framework.Extension.sln b/NonSucking.Framework.Extension.sln index bf7d0a5..07f9793 100644 --- a/NonSucking.Framework.Extension.sln +++ b/NonSucking.Framework.Extension.sln @@ -21,6 +21,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonSucking.Framework.Serial EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DEMO.Base", "DEMO.Base\DEMO.Base.csproj", "{B1A1B2D7-A54A-4FDE-BED9-48DCB702F418}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NonSucking.Framework.Extension.EntityFrameworkCore", "NonSucking.Framework.Extension.EntityFrameworkCore\NonSucking.Framework.Extension.EntityFrameworkCore.csproj", "{9531439E-BA92-4457-A30C-6C707DEE4EF6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EntityFrameworkExtension", "EntityFrameworkExtension", "{88C91CBB-4FFD-4A3E-BD44-FFDC1C202BE6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonSucking.Framework.Extension.Database.InMemory", "EntityFrameworkExtension\NonSucking.Framework.Extension.Database.InMemory\NonSucking.Framework.Extension.Database.InMemory.csproj", "{E1DEA45A-A4BB-4A88-8FD9-20061CD3B014}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonSucking.Framework.Extension.Database.MSSQL", "EntityFrameworkExtension\NonSucking.Framework.Extension.Database.MSSQL\NonSucking.Framework.Extension.Database.MSSQL.csproj", "{ED5663F3-A355-4586-A30A-68E6CE3F2FC7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonSucking.Framework.Extension.Database.MySql", "EntityFrameworkExtension\NonSucking.Framework.Extension.Database.MySql\NonSucking.Framework.Extension.Database.MySql.csproj", "{56EBC6DE-7D58-4FB9-B696-28C4C7335A97}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonSucking.Framework.Extension.Database.PostrgeSQL", "EntityFrameworkExtension\NonSucking.Framework.Extension.Database.PostrgeSQL\NonSucking.Framework.Extension.Database.PostrgeSQL.csproj", "{0DD295C6-648C-4D8F-8FC8-942959D5A007}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonSucking.Framework.Extension.Database.Sqlite", "EntityFrameworkExtension\NonSucking.Framework.Extension.Database.Sqlite\NonSucking.Framework.Extension.Database.Sqlite.csproj", "{65FA58CE-BD18-45F3-9A70-B500D419C45D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,10 +77,41 @@ Global {B1A1B2D7-A54A-4FDE-BED9-48DCB702F418}.Debug|Any CPU.Build.0 = Debug|Any CPU {B1A1B2D7-A54A-4FDE-BED9-48DCB702F418}.Release|Any CPU.ActiveCfg = Release|Any CPU {B1A1B2D7-A54A-4FDE-BED9-48DCB702F418}.Release|Any CPU.Build.0 = Release|Any CPU + {9531439E-BA92-4457-A30C-6C707DEE4EF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9531439E-BA92-4457-A30C-6C707DEE4EF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9531439E-BA92-4457-A30C-6C707DEE4EF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9531439E-BA92-4457-A30C-6C707DEE4EF6}.Release|Any CPU.Build.0 = Release|Any CPU + {E1DEA45A-A4BB-4A88-8FD9-20061CD3B014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1DEA45A-A4BB-4A88-8FD9-20061CD3B014}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1DEA45A-A4BB-4A88-8FD9-20061CD3B014}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1DEA45A-A4BB-4A88-8FD9-20061CD3B014}.Release|Any CPU.Build.0 = Release|Any CPU + {ED5663F3-A355-4586-A30A-68E6CE3F2FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED5663F3-A355-4586-A30A-68E6CE3F2FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED5663F3-A355-4586-A30A-68E6CE3F2FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED5663F3-A355-4586-A30A-68E6CE3F2FC7}.Release|Any CPU.Build.0 = Release|Any CPU + {56EBC6DE-7D58-4FB9-B696-28C4C7335A97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56EBC6DE-7D58-4FB9-B696-28C4C7335A97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56EBC6DE-7D58-4FB9-B696-28C4C7335A97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56EBC6DE-7D58-4FB9-B696-28C4C7335A97}.Release|Any CPU.Build.0 = Release|Any CPU + {0DD295C6-648C-4D8F-8FC8-942959D5A007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DD295C6-648C-4D8F-8FC8-942959D5A007}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DD295C6-648C-4D8F-8FC8-942959D5A007}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DD295C6-648C-4D8F-8FC8-942959D5A007}.Release|Any CPU.Build.0 = Release|Any CPU + {65FA58CE-BD18-45F3-9A70-B500D419C45D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65FA58CE-BD18-45F3-9A70-B500D419C45D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65FA58CE-BD18-45F3-9A70-B500D419C45D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65FA58CE-BD18-45F3-9A70-B500D419C45D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E1DEA45A-A4BB-4A88-8FD9-20061CD3B014} = {88C91CBB-4FFD-4A3E-BD44-FFDC1C202BE6} + {ED5663F3-A355-4586-A30A-68E6CE3F2FC7} = {88C91CBB-4FFD-4A3E-BD44-FFDC1C202BE6} + {56EBC6DE-7D58-4FB9-B696-28C4C7335A97} = {88C91CBB-4FFD-4A3E-BD44-FFDC1C202BE6} + {0DD295C6-648C-4D8F-8FC8-942959D5A007} = {88C91CBB-4FFD-4A3E-BD44-FFDC1C202BE6} + {65FA58CE-BD18-45F3-9A70-B500D419C45D} = {88C91CBB-4FFD-4A3E-BD44-FFDC1C202BE6} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2938EA08-9C94-425B-81F3-434AF51FFBCE} EndGlobalSection