├── .gitignore ├── LICENSE.md ├── README.md ├── example ├── EfCoreAutoMigrator.db ├── Example.csproj ├── Logger.cs ├── Model.cs └── Program.cs └── src ├── AutoMigratorTable.cs ├── EFCoreAutoMigrator.cs ├── EFCoreAutoMigrator.csproj ├── IAutoMigratorTableMetatdata.cs ├── IMigrationScriptExecutor.cs ├── MigrationProviders ├── IMigrationProviderFactory.cs ├── MigrationProviderFactory.cs └── Providers │ ├── MSSQLMigrations.cs │ ├── MigrationsProvider.cs │ ├── MySQLMigration.cs │ ├── PostgresMigrations.cs │ └── SQLiteMigrations.cs ├── MigrationScriptExecutor.cs └── Utilities ├── Consts.cs ├── Extension.cs └── Utilities.cs /.gitignore: -------------------------------------------------------------------------------- 1 | **/obj/ 2 | **/bin/ 3 | launch.json 4 | tasks.json -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Centrid Software Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EFCoreAutoMigrator 2 | #### Code driven auto-migrations for Entity Framework Core 3 | 4 | If you are using Entity Framework Core (EFCore) and want to auto-migrate your database you might know that this is a bit challenging (as noted in this thread https://github.com/dotnet/efcore/issues/6214). 5 | This library builds upon suggested comments from the above thread as to how to implement this. 6 | **Notes:** 7 | 8 | * This library was created as part of a bigger project to meet our particular need. Extensive testing and optimization has not been done on it. Please use this library with caution. 9 | * This libray does not run manually created migration scripts for you (created via `dotnet ef migrations add ...`). For those you will still need to run `dotnet ef database update`. You can then switch over to migrating through EFCoreAutoMigrator once you run those migrations. 10 | 11 | 12 | ## Getting Started 13 | 14 | ### 1. Installation 15 | 16 | You can either clone this project and add it to your project directly or install it via nuget (See https://www.nuget.org/packages/CentridNet.EFCoreAutoMigrator) 17 | 18 | 19 | ### 2. Integrating 20 | 21 | Once you have installed the package you can intergrate it by passing in your DbContext to the `EFCoreAutoMigrator` class (along with a logger). From there you can call `PrepareMigration()` which returns an instance of `MigrationScriptExecutor`. This instance is what you use to get the migration script to be executed (using `GetMigrationScript()`) and to execute it when ready (using `MigrateDB()`). 22 | 23 | Below is a simple example of this based on the EFCore getting started tutorial found at https://docs.microsoft.com/en-us/ef/core/get-started 24 | 25 | ```c# 26 | using System; 27 | using System.IO; 28 | using System.Threading.Tasks; 29 | using CentridNet.EFCoreAutoMigrator; 30 | using Microsoft.EntityFrameworkCore; 31 | 32 | namespace EFCoreAutoMigratorExample 33 | { 34 | class Program 35 | { 36 | static void Main() 37 | { 38 | using (var db = new BloggingContext()) 39 | { 40 | AutoMigrateMyDB(db).Wait(); 41 | } 42 | } 43 | 44 | public static async Task AutoMigrateMyDB(DbContext db){ 45 | EFCoreAutoMigrator dbMigrator = new EFCoreAutoMigrator(db, new Logger()); 46 | MigrationScriptExecutor migrationScriptExcutor = await dbMigrator.PrepareMigration(); 47 | await migrationScriptExcutor.MigrateDB(); 48 | Console.WriteLine("Migration Complete"); 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | Below is a more complex example that allows for a more user driven/conditional migration. 55 | 56 | ```c# 57 | using System; 58 | using System.IO; 59 | using System.Threading.Tasks; 60 | using CentridNet.EFCoreAutoMigrator; 61 | using Microsoft.EntityFrameworkCore; 62 | 63 | namespace EFCoreAutoMigratorExample 64 | { 65 | class Program 66 | { 67 | static void Main() 68 | { 69 | using (var db = new BloggingContext()) 70 | { 71 | AutoMigrateMyDB(db).Wait(); 72 | } 73 | } 74 | 75 | public static async Task AutoMigrateMyDB(DbContext db){ 76 | EFCoreAutoMigrator dbMigrator = new EFCoreAutoMigrator(db, new Logger()); 77 | MigrationScriptExecutor migrationScriptExcutor = await dbMigrator.PrepareMigration(); 78 | 79 | // Checking if there are migrations 80 | if (migrationScriptExcutor.HasMigrations()){ 81 | Console.WriteLine("The program `Example` wants to run the following script on your database: "); 82 | Console.WriteLine("------"); 83 | 84 | // Printing out the script to be run if they are 85 | Console.WriteLine(migrationScriptExcutor.GetMigrationScript()); 86 | Console.WriteLine("------"); 87 | 88 | Console.WriteLine("Do you want (R)un it, (S)ave the script or (C)ancel. ?"); 89 | string userInput = Console.ReadLine(); 90 | if (userInput.Length == 0){ 91 | Console.WriteLine("No value entered. Exiting..."); 92 | Environment.Exit(0); 93 | } 94 | if (userInput[0] == 'R'){ 95 | // Migrating 96 | MigrationResult result = await migrationScriptExcutor.MigrateDB(); 97 | if (result == MigrationResult.Migrated){ 98 | Console.WriteLine("Completed succesfully."); 99 | } 100 | else if (result == MigrationResult.Noop){ 101 | Console.WriteLine("Completed. There was nothing to migrate."); 102 | } 103 | else if (result == MigrationResult.ErrorMigrating){ 104 | Console.WriteLine("Error occurred whilst migrating."); 105 | } 106 | } 107 | else if (userInput[0] == 'S'){ 108 | using (StreamWriter writer = new StreamWriter(Path.Join(Environment.CurrentDirectory,"ERCoreAutoMigratorGenetaedScript.sql"))) 109 | { 110 | writer.WriteLine(migrationScriptExcutor.GetMigrationScript()); 111 | Console.WriteLine("Migration script saved succefully."); 112 | } 113 | } 114 | } 115 | else{ 116 | Console.WriteLine("Completed. There was nothing to migrate."); 117 | } 118 | 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ## EFCoreAutoMigrator Configuration 125 | 126 | ### ShouldAllowDestructive 127 | 128 | This is used to state whether desctructive migrations are allowed or not. If not, and exception will occur when you run the `PrepareMigration()` method. Default is false. 129 | 130 | Usage example: 131 | ```c# 132 | ... 133 | EFCoreAutoMigrator dbMigrator = new EFCoreAutoMigrator(db, new Logger()) 134 | .ShouldAllowDestructive(true); 135 | ... 136 | ``` 137 | ### SetSnapshotHistoryLimit 138 | 139 | For every migration EFCoreAutoMigrator runs it saves a snapshot (and other corresponding metadata...See _SetMigrationTableMetadataClass_ below). Use this method to limt the number of snapshot saved. Default is unlimited 140 | 141 | Usage example: 142 | ```c# 143 | ... 144 | EFCoreAutoMigrator dbMigrator = new EFCoreAutoMigrator(db, new Logger()) 145 | .SetSnapshotHistoryLimit(1); 146 | ... 147 | ``` 148 | 149 | ### SetMigrationTableMetadataClass 150 | 151 | This method takes in a class that implements the `IAutoMigratorTableMetatdata` interface. This class is then used to set the metadata field in the auto-migration database table and add a comment to the top of the generated sql script. This is useful if you want to track additional information, for instance, your application version associated with the migration. A default class is used, if not set. 152 | 153 | Usage example: 154 | ```c# 155 | class MyMigrationMetadata : IAutoMigratorTableMetatdata { 156 | public string GetDBMetadata() 157 | { 158 | return "MyAppVersion: 1.0.0"; 159 | } 160 | 161 | public string GetGeneratedScriptMetatdata() 162 | { 163 | return $"This script was auto-generated by MyApp (Version 1.0.0)"; 164 | } 165 | } 166 | ... 167 | EFCoreAutoMigrator dbMigrator = new EFCoreAutoMigrator(db, new Logger()) 168 | .SetMigrationTableMetadataClass(new MyMigrationMetadata()); 169 | ... 170 | ``` 171 | 172 | ### SetMigrationProviderFactory 173 | 174 | EFCoreAutoMigrator selects the appropiate migration provider (see below) for your connected database using the MigrationProviderFactory. If you want to replace this, use this method to pass in a class that implements the `IMigrationProviderFactory` interface. This is useful if you know exactly what database you will be using and want to remove the overhead associated with figuring out the database when migrating. 175 | 176 | Usage example: 177 | ```c# 178 | public class MyMigrationProviderFactory : IMigrationProviderFactory{ 179 | public MigrationsProvider Build(DBMigratorProps dBMigratorProps, MigrationScriptExecutor migrationScriptExecutor){ 180 | 181 | if (dBMigratorProps.dbContext.Database.IsNpgsql()){ 182 | return new PostgresMigrations(dBMigratorProps, migrationScriptExecutor); 183 | } 184 | return null; 185 | } 186 | } 187 | ... 188 | EFCoreAutoMigrator dbMigrator = new EFCoreAutoMigrator(db, new Logger()) 189 | .SetMigrationProviderFactory(new MyMigrationProviderFactory()); 190 | ... 191 | ``` 192 | 193 | ## Migration Providers 194 | 195 | In the EFCoreAutoMigrator library, MigrationProviders are responsible for creating and interacting with the auto-migration tables for the various databases. EFCoreAutoMigrator comes with providers for Postgres, MSSQL, MySQL, and SQLLite baked in. 196 | 197 | If you want to create your own provider class (i.e for a particular database) you can do this by creating a class that inherits the `MigrationProvider` class. An example of this is below (For method code examples, look at the MigrationProviders already written. See [src/MigrationProviders/Providers](src/MigrationProviders/Providers)): 198 | 199 | ```c# 200 | class MSSQLMigrations : MigrationsProvider 201 | { 202 | //TODO use CosmoDB 203 | public MSSQLMigrations(DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor) : base(dbMigratorProps, migrationScriptExecutor){} 204 | 205 | protected override void EnsureMigrateTablesExist(){...}; 206 | 207 | protected override void EnsureSnapshotLimitNotReached(){...}; 208 | 209 | protected override AutoMigratorTable GetLastMigrationRecord(){...}; 210 | 211 | protected override void UpdateMigrationTables(byte[] snapshotData){...}; 212 | } 213 | ``` 214 | 215 | Once you have created a MigrationProvider you will need to let EFCoreAutoMigrator know it exists. You can either do this by creating your own MigrationProviderFactory (as described above) or write a `DBContext` extension method with the format `[ProviderName]DBMigrations` for the method name. Using the example above this will be: 216 | 217 | ```c# 218 | public static MigrationsProvider CosmoDBDBMigrations(this DbContext dbContext, DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor){ 219 | return new CosmoDBMigrations(dbMigratorProps, migrationScriptExecutor); 220 | } 221 | ``` 222 | 223 | ## Known Issues 224 | 225 | * There is currently a migration issue that occurs when you change the namespace(s) associated with your DBSets (Error messasge `Failed to compile previous snapshot`). Will look into this and should provide a fix soon. 226 | 227 | ---- 228 | 229 | ## Contributing 230 | 231 | EFCoreAutoMigrator is an opensource project and contributions are valued. If there is a bug fix please create a pull request explain what the bug is, how you fixed and tested it. 232 | 233 | If it's a new feature, please add it as a issue with the label enhancement, detailing the new feature and why you think it's needed. Will discuss it there and once it's agreed upon you can create a pull request with the details highlighted above. 234 | 235 | ## Credit 236 | 237 | Thank you to our following contributors 238 | 239 | @stavroskasidis - worked on the .net 6.0 version 240 | 241 | ## Authors 242 | 243 | * **Chido Warambwa** - *Initial Work* - [chidow@centridsol.tech](mailto://chidow@centridsol.tech) 244 | 245 | ## License 246 | 247 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 248 | 249 | ## Thanks 250 | 251 | Thanks to all those that have been contributing to the dicussion on this (see [(https://github.com/dotnet/efcore/issues/6214)](https://github.com/dotnet/efcore/issues/6214)) and in particular, a huge thanks Jeremy Lakeman's [code snippets](https://gist.github.com/lakeman/1509f790ead00a884961865b5c79b630) that became the starting of point of this libray. 252 | 253 | -------------------------------------------------------------------------------- /example/EfCoreAutoMigrator.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/centridsol/EFCoreAutoMigrator/77673509054d3f8d0e9cd257a1c01e67aca456fd/example/EfCoreAutoMigrator.db -------------------------------------------------------------------------------- /example/Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace EFCoreAutoMigratorExample{ 5 | class Logger : ILogger 6 | { 7 | public IDisposable BeginScope(TState state) 8 | { 9 | return null; 10 | } 11 | 12 | public bool IsEnabled(LogLevel logLevel) 13 | { 14 | return true; 15 | } 16 | 17 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 18 | { 19 | if ( (int)logLevel > 1) 20 | { 21 | Console.WriteLine($" {formatter(state, exception)}"); 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /example/Model.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | 5 | namespace EFCoreAutoMigratorExample 6 | { 7 | // From https://docs.microsoft.com/en-us/ef/core/get-started 8 | public class BloggingContext : DbContext 9 | { 10 | public DbSet Blogs { get; set; } 11 | public DbSet Posts { get; set; } 12 | 13 | protected override void OnConfiguring(DbContextOptionsBuilder options) 14 | => options.UseSqlite("Data Source=EfCoreAutoMigrator.db"); 15 | } 16 | 17 | public class Blog 18 | { 19 | public int BlogId { get; set; } 20 | public string Url { get; set; } 21 | 22 | public List Posts { get; } = new List(); 23 | } 24 | 25 | public class Post 26 | { 27 | public int PostId { get; set; } 28 | public string Title { get; set; } 29 | public string Content { get; set; } 30 | 31 | public int BlogId { get; set; } 32 | public Blog Blog { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/Program.cs: -------------------------------------------------------------------------------- 1 |  using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using CentridNet.EFCoreAutoMigrator; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace EFCoreAutoMigratorExample 8 | { 9 | class Program 10 | { 11 | static void Main() 12 | { 13 | using (var db = new BloggingContext()) 14 | { 15 | AutoMigrateMyDB(db).Wait(); 16 | } 17 | } 18 | 19 | public static async Task AutoMigrateMyDB(DbContext db){ 20 | EFCoreAutoMigrator dbMigrator = new EFCoreAutoMigrator(db, new Logger()); 21 | MigrationScriptExecutor migrationScriptExcutor = await dbMigrator.PrepareMigration(); 22 | 23 | // Checking if there are migrations 24 | if (migrationScriptExcutor.HasMigrations()){ 25 | Console.WriteLine("The program `Example` wants to run the following script on your database: "); 26 | Console.WriteLine("------"); 27 | 28 | // Printing out the script to be run if they are 29 | Console.WriteLine(migrationScriptExcutor.GetMigrationScript()); 30 | Console.WriteLine("------"); 31 | 32 | Console.WriteLine("Do you want (R)un it, (S)ave the script or (C)ancel. ?"); 33 | string userInput = Console.ReadLine(); 34 | if (userInput.Length == 0){ 35 | Console.WriteLine("No value entered. Exiting..."); 36 | Environment.Exit(0); 37 | } 38 | if (userInput[0] == 'R'){ 39 | // Migrating 40 | MigrationResult result = await migrationScriptExcutor.MigrateDB(); 41 | if (result == MigrationResult.Migrated){ 42 | Console.WriteLine("Completed succesfully."); 43 | } 44 | else if (result == MigrationResult.Noop){ 45 | Console.WriteLine("Completed. There was nothing to migrate."); 46 | } 47 | else if (result == MigrationResult.ErrorMigrating){ 48 | Console.WriteLine("Error occurred whilst migrating."); 49 | } 50 | } 51 | else if (userInput[0] == 'S'){ 52 | using (StreamWriter writer = new StreamWriter(Path.Join(Environment.CurrentDirectory,"ERCoreAutoMigratorGenetaedScript.sql"))) 53 | { 54 | writer.WriteLine(migrationScriptExcutor.GetMigrationScript()); 55 | Console.WriteLine("Migration script saved succefully."); 56 | } 57 | } 58 | } 59 | else{ 60 | Console.WriteLine("Completed. There was nothing to migrate."); 61 | } 62 | 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/AutoMigratorTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace CentridNet.EFCoreAutoMigrator{ 7 | 8 | public class AutoMigratorTable{ 9 | 10 | 11 | public int runId = -1; 12 | public DateTime? runDate = null; 13 | public string efcoreVersion = ""; 14 | public string metadata = ""; 15 | public byte [] snapshot; 16 | 17 | public AutoMigratorTable(IAutoMigratorTableMetatdata migratorTableMetatdata){ 18 | metadata = migratorTableMetatdata.GetDBMetadata(); 19 | } 20 | 21 | public AutoMigratorTable(){} 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /src/EFCoreAutoMigrator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Design.Internal; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Migrations.Internal; 8 | using Microsoft.EntityFrameworkCore.Migrations.Operations; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | using System; 12 | using System.Linq; 13 | using System.Reflection; 14 | using System.Threading.Tasks; 15 | using Microsoft.EntityFrameworkCore.Internal; 16 | using CentridNet.EFCoreAutoMigrator.MigrationContexts; 17 | using CentridNet.EFCoreAutoMigrator.Utilities; 18 | using Microsoft.EntityFrameworkCore.Migrations.Design; 19 | 20 | //Base on code from lakeman. See https://gist.github.com/lakeman/1509f790ead00a884961865b5c79b630/ for reference. 21 | namespace CentridNet.EFCoreAutoMigrator 22 | { 23 | public class EFCoreAutoMigrator : IOperationReporter 24 | { 25 | private DBMigratorProps dbMigratorProps; 26 | public MigrationScriptExecutor migrationScriptExecutor; 27 | 28 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Allowing for code driven migrations")] 29 | public EFCoreAutoMigrator(DbContext _dbContext, ILogger _logger) 30 | { 31 | dbMigratorProps = new DBMigratorProps(){ 32 | dbContext = _dbContext, 33 | logger = _logger, 34 | dbMigratorTableMetatdata = new DefaultMigrationMetadata(), 35 | migrationProviderFactory = new MigrationProviderFactory(), 36 | allowDestructive = false, 37 | snapshotHistoryLimit = -1 38 | }; 39 | var migrationAssembly = _dbContext.GetService(); 40 | DesignTimeServicesBuilder builder = new DesignTimeServicesBuilder(migrationAssembly.Assembly, Assembly.GetEntryAssembly(), this, null); 41 | var dbServices = builder.Build(_dbContext); 42 | 43 | var dependencies = dbServices.GetRequiredService(); 44 | var migrationName = dependencies.MigrationsIdGenerator.GenerateId(Utilities.DalConsts.MIGRATION_NAME_PREFIX); 45 | 46 | dbMigratorProps.dbServices = dbServices; 47 | dbMigratorProps.migrationName = migrationName; 48 | 49 | } 50 | 51 | public EFCoreAutoMigrator ShouldAllowDestructive(bool shouldAllowDestruction){ 52 | dbMigratorProps.allowDestructive = shouldAllowDestruction; 53 | return this; 54 | } 55 | 56 | public EFCoreAutoMigrator SetSnapshotHistoryLimit(int limit){ 57 | dbMigratorProps.snapshotHistoryLimit = limit; 58 | return this; 59 | } 60 | public EFCoreAutoMigrator SetMigrationTableMetadataClass(IAutoMigratorTableMetatdata updateddbMigratorTableMetatdata){ 61 | dbMigratorProps.dbMigratorTableMetatdata = updateddbMigratorTableMetatdata; 62 | return this; 63 | } 64 | public EFCoreAutoMigrator SetMigrationProviderFactory(IMigrationProviderFactory updateddbMigratorProviderFactory){ 65 | dbMigratorProps.migrationProviderFactory = updateddbMigratorProviderFactory; 66 | return this; 67 | } 68 | 69 | 70 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Allowing for code driven migrations")] 71 | public async Task PrepareMigration() 72 | { 73 | migrationScriptExecutor = new MigrationScriptExecutor(dbMigratorProps); 74 | 75 | var migrationAssembly = dbMigratorProps.dbContext.GetService(); 76 | var designTimeModel = dbMigratorProps.dbContext.GetService(); 77 | var declaredMigrations = dbMigratorProps.dbContext.Database.GetMigrations().ToList(); 78 | var dependencies = dbMigratorProps.dbServices.GetRequiredService(); 79 | var appliedMigrations = (await dbMigratorProps.dbContext.Database.GetAppliedMigrationsAsync()).ToList(); 80 | 81 | if (declaredMigrations.Except(appliedMigrations).Any()){ 82 | throw new InvalidOperationException("Pending migration scripts have been found. Please run those migrations first () before trying to use the DBMigrator."); 83 | } 84 | 85 | string dbMigratorRunMigrations = null; 86 | var lastRunMigration = appliedMigrations.LastOrDefault(); 87 | if (lastRunMigration != null && declaredMigrations.Find(s=> string.Compare(s, lastRunMigration) == 0) == null){ 88 | dbMigratorRunMigrations = lastRunMigration; 89 | } 90 | 91 | migrationScriptExecutor.EnsureMigrateTablesExist(); 92 | 93 | ModelSnapshot modelSnapshot = null; 94 | 95 | if (dbMigratorRunMigrations != null) 96 | { 97 | AutoMigratorTable migrationRecord = migrationScriptExecutor.GetLastMigrationRecord(); 98 | var source = await Utilities.Utilities.DecompressSource(migrationRecord.snapshot); 99 | 100 | if (source == null || !source.Contains(dbMigratorRunMigrations)) 101 | throw new InvalidOperationException($"Expected to find the source code of the {dbMigratorRunMigrations} ModelSnapshot stored in the database"); 102 | 103 | try{ 104 | modelSnapshot = Utilities.Utilities.CompileSnapshot(migrationAssembly.Assembly, dbMigratorProps.dbContext, source); 105 | } 106 | catch(Exception ex){ 107 | throw new InvalidOperationException("Failed to compile previous snapshot. This usually occurs when you have changed the namespace(s) associates with your DBSets. To fix you will have to delete the table causing the problem in your database (see below).", ex); 108 | } 109 | 110 | } 111 | else 112 | { 113 | modelSnapshot = migrationAssembly.ModelSnapshot; 114 | } 115 | 116 | var snapshotModel = modelSnapshot?.Model; 117 | if (snapshotModel is IMutableModel mutableModel) 118 | { 119 | snapshotModel = mutableModel.FinalizeModel(); 120 | } 121 | 122 | if (snapshotModel != null) 123 | { 124 | snapshotModel = dbMigratorProps.dbContext.GetService().Initialize(snapshotModel); 125 | 126 | // apply fixes for upgrading between major / minor versions 127 | snapshotModel = dependencies.SnapshotModelProcessor.Process(snapshotModel); 128 | } 129 | 130 | if (SetMigrationCommands(migrationAssembly.Assembly, snapshotModel?.GetRelationalModel(), designTimeModel.Model.GetRelationalModel())){ 131 | await UpdateMigrationTables(); 132 | } 133 | 134 | return migrationScriptExecutor; 135 | } 136 | 137 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Allowing for code driven migrations")] 138 | private bool SetMigrationCommands(Assembly migrationAssembly, IRelationalModel oldModel , IRelationalModel newModel) 139 | { 140 | bool hasMigrations = false; 141 | var dependencies = dbMigratorProps.dbServices.GetRequiredService(); 142 | 143 | if (oldModel == null) 144 | { 145 | migrationScriptExecutor.AddSQLCommand(dbMigratorProps.dbContext.Database.GenerateCreateScript()); 146 | hasMigrations = true; 147 | } 148 | else 149 | { 150 | 151 | var operations = dependencies.MigrationsModelDiffer 152 | .GetDifferences(oldModel, newModel) 153 | // Ignore all seed updates. Workaround for (https://github.com/aspnet/EntityFrameworkCore/issues/18943) 154 | .Where(o => !(o is UpdateDataOperation)) 155 | .ToList(); 156 | 157 | if (operations.Any()) 158 | { 159 | if (!dbMigratorProps.allowDestructive && operations.Any(o => o.IsDestructiveChange)) 160 | throw new InvalidOperationException( 161 | "Automatic migration was not applied because it could result in data loss."); 162 | 163 | var sqlGenerator = dbMigratorProps.dbContext.GetService(); 164 | var commands = sqlGenerator.Generate(operations, dbMigratorProps.dbContext.Model); 165 | 166 | foreach (MigrationCommand migrateCommand in commands) 167 | { 168 | migrationScriptExecutor.AddSQLCommand(migrateCommand.CommandText); 169 | } 170 | 171 | hasMigrations = true; 172 | } 173 | } 174 | return hasMigrations; 175 | } 176 | 177 | private async Task UpdateMigrationTables(){ 178 | var codeGen = dbMigratorProps.dbServices.GetRequiredService().MigrationsCodeGeneratorSelector.Select(null); 179 | var designTimeModel = dbMigratorProps.dbContext.GetService(); 180 | string modelSource = codeGen.GenerateSnapshot("AutoMigrations", dbMigratorProps.dbContext.GetType(), $"Migration_{dbMigratorProps.migrationName}", 181 | designTimeModel.Model); 182 | byte[] newSnapshotBinary = await Utilities.Utilities.CompressSource(modelSource); 183 | migrationScriptExecutor.UpdateMigrationTables(newSnapshotBinary); 184 | } 185 | 186 | void IOperationReporter.WriteError(string message) => dbMigratorProps.logger.LogError(message); 187 | void IOperationReporter.WriteInformation(string message) => dbMigratorProps.logger.LogInformation(message); 188 | void IOperationReporter.WriteVerbose(string message) => dbMigratorProps.logger.LogTrace(message); 189 | void IOperationReporter.WriteWarning(string message) => dbMigratorProps.logger.LogWarning(message); 190 | 191 | private class DefaultMigrationMetadata : IAutoMigratorTableMetatdata { 192 | 193 | private string name; 194 | private string version; 195 | 196 | public DefaultMigrationMetadata(){ 197 | AssemblyName assebmlyDetail = typeof(DefaultMigrationMetadata).Assembly.GetName(); 198 | name = assebmlyDetail.Name; 199 | version = assebmlyDetail.Version.ToString(); 200 | } 201 | public string GetDBMetadata() 202 | { 203 | return $"{name} (Version {version})"; 204 | } 205 | 206 | public string GetGeneratedScriptMetatdata() 207 | { 208 | return $"This script was auto-generated by {name} (Version {version})"; 209 | } 210 | } 211 | } 212 | 213 | public struct DBMigratorProps{ 214 | public bool allowDestructive; 215 | public int snapshotHistoryLimit; 216 | public IAutoMigratorTableMetatdata dbMigratorTableMetatdata; 217 | public IMigrationProviderFactory migrationProviderFactory; 218 | public DbContext dbContext; 219 | public ILogger logger; 220 | public IServiceProvider dbServices; 221 | public string migrationName; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/EFCoreAutoMigrator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | CentridNet.EFCoreAutoMigrator 5 | 6.0.0.1 6 | Code driven auto-migrations for Entity Framework Core 7 | LICENSE.md 8 | https://github.com/centridsol/EFCoreAutoMigrator 9 | efcore;auto-migrations;migrations;database 10 | net6.0 11 | 6.0.0.1 12 | EFCoreAutoMigrator 13 | Chido W 14 | Centrid Software Solutions 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/IAutoMigratorTableMetatdata.cs: -------------------------------------------------------------------------------- 1 | namespace CentridNet.EFCoreAutoMigrator{ 2 | public interface IAutoMigratorTableMetatdata{ 3 | string GetDBMetadata(); 4 | string GetGeneratedScriptMetatdata(); 5 | } 6 | } -------------------------------------------------------------------------------- /src/IMigrationScriptExecutor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CentridNet.EFCoreAutoMigrator.MigrationContexts; 3 | 4 | namespace CentridNet.EFCoreAutoMigrator{ 5 | 6 | interface IMigrationScriptExecutor{ 7 | void AddSQLCommand(string command, params object[] parameters); 8 | void AddText(string sqltext); 9 | bool HasMigrations(); 10 | Task MigrateDB(); 11 | string GetMigrationScript(); 12 | void EnsureMigrateTablesExist(); 13 | AutoMigratorTable GetLastMigrationRecord(); 14 | void UpdateMigrationTables(byte[] updatedSnapShot); 15 | 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /src/MigrationProviders/IMigrationProviderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace CentridNet.EFCoreAutoMigrator.MigrationContexts{ 6 | 7 | public interface IMigrationProviderFactory { 8 | MigrationsProvider Build(DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor); 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /src/MigrationProviders/MigrationProviderFactory.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.Logging; 5 | using System.Collections.Generic; 6 | using System.Reflection; 7 | using System.Linq; 8 | 9 | //TODO: Change namesapces 10 | namespace CentridNet.EFCoreAutoMigrator.MigrationContexts{ 11 | 12 | public class MigrationProviderFactory : IMigrationProviderFactory{ 13 | 14 | 15 | public MigrationsProvider Build(DBMigratorProps dBMigratorProps, MigrationScriptExecutor migrationScriptExecutor){ 16 | 17 | string extensionMethod = $"{dBMigratorProps.dbContext.Database.ProviderName.Split('.').Last()}DBMigrations"; 18 | 19 | List contextMigrationMethods = Utilities.Utilities.GetExtensionMethods(extensionMethod , typeof(DbContext)).ToList(); 20 | 21 | if (contextMigrationMethods.Count() > 0){ 22 | return (MigrationsProvider)contextMigrationMethods[0].Invoke(null, new object[] {dBMigratorProps.dbContext, dBMigratorProps, migrationScriptExecutor}); 23 | } 24 | throw new InvalidOperationException($"The extension method '{extensionMethod}' for type {typeof(DbContext)} was not found"); 25 | } 26 | 27 | 28 | } 29 | } -------------------------------------------------------------------------------- /src/MigrationProviders/Providers/MSSQLMigrations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using CentridNet.EFCoreAutoMigrator.Utilities; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace CentridNet.EFCoreAutoMigrator.MigrationContexts{ 9 | public static class SqlServerMigrationsExtensions{ 10 | public static MigrationsProvider SqlServerDBMigrations(this DbContext dbContext, DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor){ 11 | return new MSSQLMigrations(dbMigratorProps, migrationScriptExecutor); 12 | } 13 | } 14 | public class MSSQLMigrations : MigrationsProvider 15 | { 16 | public MSSQLMigrations(DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor) : base(dbMigratorProps, migrationScriptExecutor){} 17 | 18 | protected override void EnsureMigrateTablesExist() 19 | { 20 | DataTable resultDataTable = dbContext.ExecuteSqlRawWithoutModel($"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'{DalConsts.MIGRATION_TABLE_NAME}';"); 21 | 22 | if (resultDataTable.Rows.Count == 0 || !Convert.ToBoolean(resultDataTable.Rows[0][0])){ 23 | migrationScriptExecutor.AddSQLCommand($@"CREATE TABLE {DalConsts.MIGRATION_TABLE_NAME} ( 24 | runId INT NOT NULL IDENTITY(1,1) PRIMARY KEY, 25 | runDate DATETIME, 26 | efcoreVersion VARCHAR (355) NOT NULL, 27 | metadata TEXT, 28 | snapshot VARBINARY(MAX) NOT NULL 29 | );"); 30 | } 31 | } 32 | 33 | protected override void EnsureSnapshotLimitNotReached() 34 | { 35 | if (snapshotHistoryLimit > 0){ 36 | migrationScriptExecutor.AddSQLCommand($"DELETE FROM {DalConsts.MIGRATION_TABLE_NAME} WHERE runId NOT IN (SELECT TOP {snapshotHistoryLimit-1} runId FROM {DalConsts.MIGRATION_TABLE_NAME} ORDER BY runId DESC);"); 37 | } 38 | } 39 | 40 | protected override AutoMigratorTable GetLastMigrationRecord() 41 | { 42 | IList migrationMetadata = dbContext.ExecuteSqlRawWithoutModel($"SELECT TOP 1 * FROM {DalConsts.MIGRATION_TABLE_NAME} ORDER BY runId DESC;", (dbDataReader) => { 43 | return new AutoMigratorTable(){ 44 | runId = (int)dbDataReader[0], 45 | runDate = (DateTime)dbDataReader[1], 46 | efcoreVersion = (string)dbDataReader[2], 47 | metadata = (string)dbDataReader[3], 48 | snapshot = (byte[])dbDataReader[4] 49 | }; 50 | }); 51 | 52 | if (migrationMetadata.Count >0){ 53 | return migrationMetadata[0]; 54 | } 55 | return null; 56 | } 57 | 58 | protected override void UpdateMigrationTables(byte[] snapshotData) 59 | { 60 | migrationScriptExecutor.AddSQLCommand($@"INSERT INTO {DalConsts.MIGRATION_TABLE_NAME} ( 61 | runDate, 62 | efcoreVersion, 63 | metadata, 64 | snapshot 65 | ) 66 | VALUES 67 | (getdate(), 68 | '{typeof(DbContext).Assembly.GetName().Version.ToString()}', 69 | '{migrationMetadata.metadata}', 70 | {"0x"+BitConverter.ToString(snapshotData).Replace("-", "")});"); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/MigrationProviders/Providers/MigrationsProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | using Microsoft.EntityFrameworkCore.Migrations.Design; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace CentridNet.EFCoreAutoMigrator.MigrationContexts{ 9 | 10 | public abstract class MigrationsProvider { 11 | protected DbContext dbContext; 12 | protected AutoMigratorTable migrationMetadata; 13 | protected IServiceProvider dbServices; 14 | protected string migrationName; 15 | protected dynamic migrationScriptExecutor; 16 | protected dynamic dbMigrateDependencies; 17 | protected int snapshotHistoryLimit; 18 | 19 | 20 | public MigrationsProvider(DBMigratorProps dbMigratorProps, MigrationScriptExecutor _migrationScriptExecutor){ 21 | dbContext = dbMigratorProps.dbContext; 22 | dbServices = dbMigratorProps.dbServices; 23 | migrationName = dbMigratorProps.migrationName; 24 | snapshotHistoryLimit = dbMigratorProps.snapshotHistoryLimit; 25 | migrationScriptExecutor = _migrationScriptExecutor; 26 | dbMigrateDependencies = dbServices.GetRequiredService(); 27 | migrationMetadata = new AutoMigratorTable(dbMigratorProps.dbMigratorTableMetatdata); 28 | } 29 | protected void CreateEFHistoryTable(){ 30 | if (!dbMigrateDependencies.HistoryRepository.Exists()){ 31 | migrationScriptExecutor.AddText("Creating migration history tables"); 32 | migrationScriptExecutor.AddSQLCommand(dbMigrateDependencies.HistoryRepository.GetCreateScript()); 33 | } 34 | 35 | } 36 | protected void AddEFHistoryRecord(){ 37 | var insert = dbMigrateDependencies.HistoryRepository.GetInsertScript( 38 | new HistoryRow( 39 | migrationName, 40 | typeof(DbContext).Assembly.GetName().Version.ToString() 41 | )); 42 | migrationScriptExecutor.AddSQLCommand(insert); 43 | } 44 | protected abstract void EnsureMigrateTablesExist(); 45 | protected abstract void EnsureSnapshotLimitNotReached(); 46 | protected abstract void UpdateMigrationTables(byte [] snapshotData); 47 | protected abstract AutoMigratorTable GetLastMigrationRecord(); 48 | 49 | public void _EnsureMigrateTablesExist(){ 50 | CreateEFHistoryTable(); 51 | EnsureMigrateTablesExist(); 52 | migrationScriptExecutor.AddText("DB Context Migrations"); 53 | } 54 | public void _UpdateMigrationTables(byte [] snapshotData){ 55 | migrationScriptExecutor.AddText("Updating migration history tables"); 56 | EnsureSnapshotLimitNotReached(); 57 | UpdateMigrationTables(snapshotData); 58 | AddEFHistoryRecord(); 59 | } 60 | public AutoMigratorTable _GetLastMigrationRecord(){ 61 | return GetLastMigrationRecord(); 62 | } 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/MigrationProviders/Providers/MySQLMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using CentridNet.EFCoreAutoMigrator.Utilities; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace CentridNet.EFCoreAutoMigrator.MigrationContexts{ 9 | 10 | public static class MySqlMigrationsExtensions{ 11 | public static MigrationsProvider MySqlDBMigrations(this DbContext dbContext, DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor){ 12 | return new MySQLMigrations(dbMigratorProps, migrationScriptExecutor); 13 | } 14 | } 15 | public class MySQLMigrations : MigrationsProvider 16 | { 17 | public MySQLMigrations(DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor) : base(dbMigratorProps, migrationScriptExecutor){} 18 | protected override void EnsureMigrateTablesExist() 19 | { 20 | DataTable resultDataTable = dbContext.ExecuteSqlRawWithoutModel($"SELECT COUNT(*) FROM information_schema.tables WHERE table_name = '{DalConsts.MIGRATION_TABLE_NAME}';"); 21 | 22 | if (resultDataTable.Rows.Count == 0 || !Convert.ToBoolean(resultDataTable.Rows[0][0])){ 23 | migrationScriptExecutor.AddSQLCommand($@"CREATE TABLE {DalConsts.MIGRATION_TABLE_NAME} ( 24 | runId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 25 | runDate TIMESTAMP, 26 | efcoreVersion VARCHAR (355) NOT NULL, 27 | metadata TEXT, 28 | snapshot LONGBLOB NOT NULL 29 | );"); 30 | } 31 | 32 | } 33 | protected override void EnsureSnapshotLimitNotReached() 34 | { 35 | if (snapshotHistoryLimit > 0){ 36 | migrationScriptExecutor.AddSQLCommand($"DELETE FROM {DalConsts.MIGRATION_TABLE_NAME} WHERE runId NOT IN (SELECT * FROM (SELECT runId FROM {DalConsts.MIGRATION_TABLE_NAME} ORDER BY runId DESC LIMIT {snapshotHistoryLimit-1}) as t) ORDER BY runId ASC;"); 37 | } 38 | } 39 | protected override AutoMigratorTable GetLastMigrationRecord() 40 | { 41 | IList migrationMetadata = dbContext.ExecuteSqlRawWithoutModel($"SELECT * FROM {DalConsts.MIGRATION_TABLE_NAME} ORDER BY runId DESC LIMIT 1;", (dbDataReader) => { 42 | return new AutoMigratorTable(){ 43 | runId = (int)dbDataReader[0], 44 | runDate = (DateTime)dbDataReader[1], 45 | efcoreVersion = (string)dbDataReader[2], 46 | metadata = (string)dbDataReader[3], 47 | snapshot = (byte[])dbDataReader[4] 48 | }; 49 | }); 50 | 51 | if (migrationMetadata.Count >0){ 52 | return migrationMetadata[0]; 53 | } 54 | return null; 55 | } 56 | 57 | protected override void UpdateMigrationTables(byte[] snapshotData) 58 | { 59 | migrationScriptExecutor.AddSQLCommand($@"INSERT INTO {DalConsts.MIGRATION_TABLE_NAME} ( 60 | runDate, 61 | efcoreVersion, 62 | metadata, 63 | snapshot 64 | ) 65 | VALUES 66 | (NOW(), 67 | '{typeof(DbContext).Assembly.GetName().Version.ToString()}', 68 | '{migrationMetadata.metadata}', 69 | X'{BitConverter.ToString(snapshotData).Replace("-", "")}');"); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/MigrationProviders/Providers/PostgresMigrations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.IO; 5 | using System.Linq; 6 | using CentridNet.EFCoreAutoMigrator.Utilities; 7 | using Microsoft.Data.SqlClient; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace CentridNet.EFCoreAutoMigrator.MigrationContexts{ 12 | 13 | public static class PostgresMigrationsExtensions{ 14 | public static MigrationsProvider PostgreSQLDBMigrations(this DbContext dbContext, DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor){ 15 | return new PostgresMigrations(dbMigratorProps, migrationScriptExecutor); 16 | } 17 | } 18 | public class PostgresMigrations : MigrationsProvider 19 | { 20 | public PostgresMigrations(DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor) : base(dbMigratorProps, migrationScriptExecutor){} 21 | protected override void EnsureMigrateTablesExist() 22 | { 23 | 24 | DataTable resultDataTable = dbContext.ExecuteSqlRawWithoutModel($"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{DalConsts.MIGRATION_TABLE_NAME}');"); 25 | 26 | if (resultDataTable.Rows.Count == 0 || !(bool)resultDataTable.Rows[0][0]){ 27 | migrationScriptExecutor.AddSQLCommand($@"CREATE TABLE {DalConsts.MIGRATION_TABLE_NAME} ( 28 | runId SERIAL PRIMARY KEY, 29 | runDate TIMESTAMP, 30 | efcoreVersion VARCHAR (355) NOT NULL, 31 | metadata TEXT, 32 | snapshot BYTEA NOT NULL 33 | );"); 34 | } 35 | } 36 | 37 | protected override void EnsureSnapshotLimitNotReached(){ 38 | if (snapshotHistoryLimit > 0){ 39 | migrationScriptExecutor.AddSQLCommand($"DELETE FROM {DalConsts.MIGRATION_TABLE_NAME} WHERE runId NOT IN (SELECT runId FROM {DalConsts.MIGRATION_TABLE_NAME} ORDER BY runId DESC LIMIT {snapshotHistoryLimit-1});"); 40 | } 41 | } 42 | 43 | protected override AutoMigratorTable GetLastMigrationRecord() 44 | { 45 | IList migrationMetadata = dbContext.ExecuteSqlRawWithoutModel($"SELECT * FROM {DalConsts.MIGRATION_TABLE_NAME} ORDER BY runId DESC LIMIT 1;", (dbDataReader) => { 46 | return new AutoMigratorTable(){ 47 | runId = (int)dbDataReader[0], 48 | runDate = (DateTime)dbDataReader[1], 49 | efcoreVersion = (string)dbDataReader[2], 50 | metadata = (string)dbDataReader[3], 51 | snapshot = (byte[])dbDataReader[4] 52 | }; 53 | }); 54 | 55 | if (migrationMetadata.Count >0){ 56 | return migrationMetadata[0]; 57 | } 58 | return null; 59 | } 60 | 61 | protected override void UpdateMigrationTables(byte[] snapshotData) 62 | { 63 | 64 | migrationScriptExecutor.AddSQLCommand($@"INSERT INTO {DalConsts.MIGRATION_TABLE_NAME} ( 65 | runDate, 66 | efcoreVersion, 67 | metadata, 68 | snapshot 69 | ) 70 | VALUES 71 | (NOW(), 72 | '{typeof(DbContext).Assembly.GetName().Version.ToString()}', 73 | '{migrationMetadata.metadata}', 74 | decode('{BitConverter.ToString(snapshotData).Replace("-", "")}', 'hex'));"); 75 | 76 | 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/MigrationProviders/Providers/SQLiteMigrations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using CentridNet.EFCoreAutoMigrator.Utilities; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace CentridNet.EFCoreAutoMigrator.MigrationContexts{ 9 | 10 | public static class SqliteMigrationsExtensions{ 11 | public static MigrationsProvider SqliteDBMigrations(this DbContext dbContext, DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor){ 12 | return new SQLiteMigrations(dbMigratorProps, migrationScriptExecutor); 13 | } 14 | } 15 | public class SQLiteMigrations : MigrationsProvider 16 | { 17 | public SQLiteMigrations(DBMigratorProps dbMigratorProps, MigrationScriptExecutor migrationScriptExecutor) : base(dbMigratorProps, migrationScriptExecutor){} 18 | 19 | protected override void EnsureMigrateTablesExist() 20 | { 21 | DataTable resultDataTable = dbContext.ExecuteSqlRawWithoutModel($"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name= '{DalConsts.MIGRATION_TABLE_NAME}';"); 22 | 23 | if (resultDataTable.Rows.Count == 0 || !Convert.ToBoolean(resultDataTable.Rows[0][0])){ 24 | migrationScriptExecutor.AddSQLCommand($@"CREATE TABLE {DalConsts.MIGRATION_TABLE_NAME} ( 25 | runId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 26 | runDate TEXT NOT NULL, 27 | efcoreVersion TEXT NOT NULL, 28 | metadata TEXT, 29 | snapshot BLOB NOT NULL 30 | );"); 31 | } 32 | } 33 | 34 | protected override void EnsureSnapshotLimitNotReached() 35 | { 36 | if (snapshotHistoryLimit > 0){ 37 | migrationScriptExecutor.AddSQLCommand($"DELETE FROM {DalConsts.MIGRATION_TABLE_NAME} WHERE runId NOT IN (SELECT runId FROM {DalConsts.MIGRATION_TABLE_NAME} ORDER BY runId DESC LIMIT {snapshotHistoryLimit-1});"); 38 | } 39 | } 40 | 41 | protected override AutoMigratorTable GetLastMigrationRecord() 42 | { 43 | IList migrationMetadata = dbContext.ExecuteSqlRawWithoutModel($"SELECT * FROM {DalConsts.MIGRATION_TABLE_NAME} ORDER BY runId DESC LIMIT 1;", (dbDataReader) => { 44 | return new AutoMigratorTable(){ 45 | runId = Convert.ToInt32(dbDataReader[0]), 46 | runDate = DateTime.Parse((string)dbDataReader[1]), 47 | efcoreVersion = (string)dbDataReader[2], 48 | metadata = (string)dbDataReader[3], 49 | snapshot = (byte[])dbDataReader[4] 50 | }; 51 | }); 52 | 53 | if (migrationMetadata.Count >0){ 54 | return migrationMetadata[0]; 55 | } 56 | return null; 57 | } 58 | 59 | protected override void UpdateMigrationTables(byte[] snapshotData) 60 | { 61 | migrationScriptExecutor.AddSQLCommand($@"INSERT INTO {DalConsts.MIGRATION_TABLE_NAME} ( 62 | runDate, 63 | efcoreVersion, 64 | metadata, 65 | snapshot 66 | ) 67 | VALUES 68 | (strftime('%Y-%m-%d %H:%M','now'), 69 | '{typeof(DbContext).Assembly.GetName().Version.ToString()}', 70 | '{migrationMetadata.metadata}', 71 | x'{BitConverter.ToString(snapshotData).Replace("-", "")}');"); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/MigrationScriptExecutor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using CentridNet.EFCoreAutoMigrator.MigrationContexts; 6 | using CentridNet.EFCoreAutoMigrator.Utilities; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Storage; 9 | using Microsoft.Extensions.Logging; 10 | using System.Linq; 11 | using System.Text.RegularExpressions; 12 | 13 | namespace CentridNet.EFCoreAutoMigrator 14 | { 15 | //TODO: Check 16 | public enum MigrationResult 17 | { 18 | Noop, 19 | Migrated, 20 | ErrorMigrating 21 | } 22 | 23 | public class MigrationScriptExecutor : IMigrationScriptExecutor{ 24 | 25 | private MigrationsProvider contextMigrator; 26 | DBMigratorProps dBMigratorProps; 27 | private string migrationScript = null; 28 | private IList commandsList = new List(); 29 | public MigrationResult executionResult = MigrationResult.Noop; 30 | 31 | public MigrationScriptExecutor(DBMigratorProps _dBMigratorProps){ 32 | dBMigratorProps = _dBMigratorProps; 33 | contextMigrator = dBMigratorProps.migrationProviderFactory.Build(_dBMigratorProps, this); 34 | } 35 | 36 | //TODO: Consider making these methods internal 37 | public void AddSQLCommand(string _command, params object[] _parameters){ 38 | commandsList.Add(new MigrationSQLCommand(){ 39 | sqlCommand = _command, 40 | parameters = _parameters, 41 | commandType = MigrationSQLCommandType.command 42 | }); 43 | } 44 | 45 | public void AddText(string sqltext){ 46 | commandsList.Add(new MigrationSQLCommand(){ 47 | sqlCommand = sqltext, 48 | commandType = MigrationSQLCommandType.text 49 | }); 50 | } 51 | 52 | public bool HasMigrations(){ 53 | return commandsList.Where(x => x.commandType != MigrationSQLCommandType.text).Count() > 0; 54 | } 55 | 56 | public async Task MigrateDB(){ 57 | if (HasMigrations()){ 58 | 59 | using (IDbContextTransaction transaction = dBMigratorProps.dbContext.Database.BeginTransaction()){ 60 | try{ 61 | if (migrationScript == null){ 62 | migrationScript = GetMigrationScript(); 63 | } 64 | await dBMigratorProps.dbContext.Database.ExecuteSqlRawAsync(migrationScript); 65 | transaction.Commit(); 66 | return MigrationResult.Migrated; 67 | } 68 | catch (Exception ex) 69 | { 70 | transaction.Rollback(); 71 | dBMigratorProps.logger.LogError($"Error occured will migrating database. Error message {ex.Message}. StackTrace {ex.StackTrace}"); 72 | return MigrationResult.ErrorMigrating; 73 | } 74 | } 75 | 76 | } 77 | else { 78 | return MigrationResult.Noop; 79 | } 80 | 81 | } 82 | 83 | private string RemoveSQLServerGoCommand(string sqlcommand){ 84 | return String.Join("; ", Regex.Split(sqlcommand, ";.*\n*\t*\r*GO", RegexOptions.Multiline)); 85 | } 86 | public string GetMigrationScript() 87 | { 88 | bool IsMSSSQL = dBMigratorProps.dbContext.Database.ProviderName.Contains("SqlServer"); 89 | var builder = new StringBuilder(); 90 | builder.Append($"/* {dBMigratorProps.dbMigratorTableMetatdata.GetGeneratedScriptMetatdata()} */").AppendLine(); 91 | foreach (var command in commandsList) 92 | { 93 | if (command.commandType == MigrationSQLCommandType.text){ 94 | builder.Append($"/* {command.sqlCommand} */").AppendLine(); 95 | } 96 | else if(command.commandType == MigrationSQLCommandType.command){ 97 | builder.Append(IsMSSSQL ? RemoveSQLServerGoCommand(command.sqlCommand) : command.sqlCommand) 98 | .AppendLine(); 99 | } 100 | 101 | 102 | } 103 | 104 | migrationScript = builder.ToString(); 105 | return migrationScript; 106 | } 107 | 108 | public void EnsureMigrateTablesExist() 109 | { 110 | contextMigrator._EnsureMigrateTablesExist(); 111 | } 112 | 113 | public AutoMigratorTable GetLastMigrationRecord() 114 | { 115 | return contextMigrator._GetLastMigrationRecord(); 116 | } 117 | 118 | public void UpdateMigrationTables(byte[] updatedSnapShot) 119 | { 120 | contextMigrator._UpdateMigrationTables(updatedSnapShot); 121 | } 122 | 123 | public enum MigrationSQLCommandType 124 | { 125 | command, 126 | text 127 | } 128 | public struct MigrationSQLCommand{ 129 | public string sqlCommand; 130 | public object[] parameters; 131 | public MigrationSQLCommandType commandType; 132 | }; 133 | } 134 | } -------------------------------------------------------------------------------- /src/Utilities/Consts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | namespace CentridNet.EFCoreAutoMigrator.Utilities{ 6 | 7 | //TODO: Change for public 8 | struct DalConsts{ 9 | public static string MIGRATION_TABLE_NAME = "__cnf_db_migrations"; 10 | public static string MIGRATION_NAME_PREFIX = "CNF_DAL_Migrator"; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/Utilities/Extension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Infrastructure; 8 | using Microsoft.EntityFrameworkCore.Migrations; 9 | using Microsoft.EntityFrameworkCore.Utilities; 10 | 11 | //TODO: Conisder removing these as we are not using them 12 | namespace CentridNet.EFCoreAutoMigrator.Utilities{ 13 | 14 | public static class MigrationExtensions{ 15 | public static DataTable ExecuteSqlRawWithoutModel(this DbContext dbContext, string query){ 16 | 17 | DataTable dataTable = new DataTable(); 18 | using (var command = dbContext.Database.GetDbConnection().CreateCommand()) 19 | { 20 | command.CommandText = query; 21 | command.CommandType = CommandType.Text; 22 | 23 | dbContext.Database.OpenConnection(); 24 | 25 | using (var result = command.ExecuteReader()) 26 | { 27 | dataTable.Load(result); 28 | } 29 | 30 | dbContext.Database.CloseConnection(); 31 | } 32 | return dataTable; 33 | 34 | } 35 | public static IList ExecuteSqlRawWithoutModel(this DbContext dbContext, string query, Func map){ 36 | using (var command = dbContext.Database.GetDbConnection().CreateCommand()) 37 | { 38 | command.CommandText = query; 39 | command.CommandType = CommandType.Text; 40 | 41 | dbContext.Database.OpenConnection(); 42 | 43 | using (var result = command.ExecuteReader()) 44 | { 45 | DataTable schemaTable = result.GetSchemaTable(); 46 | 47 | var entities = new List(); 48 | 49 | while (result.Read()) 50 | { 51 | entities.Add(map(result)); 52 | } 53 | return entities; 54 | } 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Utilities/Utilities.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.IO.Compression; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Runtime; 14 | using System.Runtime.CompilerServices; 15 | using System.Runtime.Loader; 16 | using System.Text; 17 | using System.Threading.Tasks; 18 | 19 | 20 | namespace CentridNet.EFCoreAutoMigrator.Utilities{ 21 | 22 | class Utilities{ 23 | 24 | public static ModelSnapshot CompileSnapshot(Assembly migrationAssembly, DbContext dbContext, string source){ 25 | return Compile(source, new HashSet() { 26 | typeof(object).Assembly, 27 | typeof(DbContext).Assembly, 28 | migrationAssembly, 29 | dbContext.GetType().Assembly, 30 | typeof(DbContextAttribute).Assembly, 31 | typeof(ModelSnapshot).Assembly, 32 | typeof(SqlServerValueGenerationStrategy).Assembly, 33 | typeof(AssemblyTargetedPatchBandAttribute).Assembly, 34 | Assembly.Load("System.Runtime") 35 | }.Concat(AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name.Contains(dbContext.Database.ProviderName) || a.GetName().Name == "netstandard" )).ToHashSet()); 36 | } 37 | 38 | private static T Compile(string source, IEnumerable references) 39 | { 40 | var options = CSharpParseOptions.Default 41 | .WithLanguageVersion(LanguageVersion.Latest); 42 | 43 | var compileOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) 44 | .WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default); 45 | 46 | var compilation = CSharpCompilation.Create("Dynamic", 47 | new[] { SyntaxFactory.ParseSyntaxTree(source, options) }, 48 | references.Select(a => MetadataReference.CreateFromFile(a.Location)), 49 | compileOptions 50 | ); 51 | 52 | using var ms = new MemoryStream(); 53 | var e = compilation.Emit(ms); 54 | if (!e.Success) 55 | throw new Exception("Compilation failed"); 56 | ms.Seek(0, SeekOrigin.Begin); 57 | 58 | var context = new AssemblyLoadContext(null, true); 59 | var assembly = context.LoadFromStream(ms); 60 | 61 | var modelType = assembly.DefinedTypes.Where(t => typeof(T).IsAssignableFrom(t)).Single(); 62 | 63 | return (T)Activator.CreateInstance(modelType); 64 | } 65 | 66 | public static async Task CompressSource(string source) 67 | { 68 | using var dbStream = new MemoryStream(); 69 | using (var blobStream = new GZipStream(dbStream, CompressionLevel.Fastest, true)) 70 | { 71 | await blobStream.WriteAsync(Encoding.UTF8.GetBytes(source)); 72 | } 73 | dbStream.Seek(0, SeekOrigin.Begin); 74 | 75 | return dbStream.ToArray(); 76 | } 77 | 78 | public static async Task DecompressSource(byte[] source){ 79 | if (source != null){ 80 | using var stream = new GZipStream(new MemoryStream(source), CompressionMode.Decompress); 81 | return await new StreamReader(stream).ReadToEndAsync(); 82 | } 83 | return null; 84 | } 85 | 86 | public static IEnumerable GetExtensionMethods(string extensionMethod, Type extendedType) 87 | { 88 | return from asm in AppDomain.CurrentDomain.GetAssemblies() 89 | from type in asm.GetTypes() 90 | where type.IsSealed && !type.IsGenericType && !type.IsNested 91 | from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) 92 | where method.Name == extensionMethod 93 | where method.IsDefined(typeof(ExtensionAttribute), false) 94 | where method.GetParameters()[0].ParameterType == extendedType 95 | select method; 96 | 97 | } 98 | } 99 | } --------------------------------------------------------------------------------