diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs index 5b07f7f364..7925b82aa6 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs @@ -30,9 +30,13 @@ namespace Volo.Abp.Cli.Commands throw new CliUsageException("Module name is missing!" + Environment.NewLine + Environment.NewLine + GetUsageInfo()); } + var skipDbMigrations = Convert.ToBoolean( + commandLineArgs.Options.GetOrNull(Options.DbMigrations.Skip) ?? "false"); + await SolutionModuleAdder.AddAsync( GetSolutionFile(commandLineArgs), - commandLineArgs.Target + commandLineArgs.Target, + skipDbMigrations ); } @@ -89,10 +93,12 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(""); sb.AppendLine("Options:"); sb.AppendLine(" -s|--solution Specify the solution file explicitly."); + sb.AppendLine(" --skip-db-migrations Specify if a new migration will be added or not."); sb.AppendLine(""); sb.AppendLine("Examples:"); sb.AppendLine(" abp add-module Volo.Blogging Adds the module to the current soluton."); sb.AppendLine(" abp add-module Volo.Blogging -s Acme.BookStore Adds the module to the given soluton."); + sb.AppendLine(" abp add-module Volo.Blogging -s Acme.BookStore --skip-db-migrations false Adds the module to the given soluton but doesn't add-migration."); sb.AppendLine(""); return sb.ToString(); @@ -105,6 +111,11 @@ namespace Volo.Abp.Cli.Commands public const string Short = "s"; public const string Long = "solution"; } + + public static class DbMigrations + { + public const string Skip = "skip-db-migrations"; + } } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DbContextFileBuilderConfigureAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DbContextFileBuilderConfigureAdder.cs new file mode 100644 index 0000000000..0211a42243 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DbContextFileBuilderConfigureAdder.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class DbContextFileBuilderConfigureAdder : ITransientDependency + { + public ILogger Logger { get; set; } + + protected UsingStatementAdder UsingStatementAdder { get; } + + public DbContextFileBuilderConfigureAdder(UsingStatementAdder usingStatementAdder) + { + UsingStatementAdder = usingStatementAdder; + Logger = NullLogger.Instance; + } + + public void Add(string path, string moduleConfiguration) + { + var file = File.ReadAllText(path); + + file = UsingStatementAdder.Add(file, GetNamespace(moduleConfiguration)); + + var stringToAdd = GetLineToAdd(moduleConfiguration); + if (!file.Contains(stringToAdd)) + { + var indexToInsert = FindIndexToInsert(file); + + if (indexToInsert <= 0 || indexToInsert >= file.Length) + { + Logger.LogWarning($"\"OnModelCreating(ModelBuilder builder)\" method couldn't be found in {path}"); + return; + } + file = file.Insert(indexToInsert, " " + stringToAdd + Environment.NewLine + " "); + } + + + File.WriteAllText(path, file); + } + + protected int FindIndexToInsert(string file) + { + var indexOfMethodDeclaration = file.IndexOf("OnModelCreating(", StringComparison.Ordinal); + var indexOfOpeningBracket = indexOfMethodDeclaration + file.Substring(indexOfMethodDeclaration).IndexOf('{'); + + var stack = 1; + var index = indexOfOpeningBracket; + + while (stack > 0) + { + index++; + + if (index >= file.Length) + { + break; + } + + if (file[index] == '{') + { + stack++; + } + else if (file[index] == '}') + { + stack--; + } + } + + return index; + } + + protected string GetLineToAdd(string moduleConfiguration) + { + return "builder." + moduleConfiguration.Split(':')[1] + "();"; + } + + protected string GetNamespace(string moduleConfiguration) + { + return string.Join(".", moduleConfiguration.Split(':')[0]); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs index bf11e0af65..db39a660ce 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs @@ -1,42 +1,83 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Cli.ProjectModification { public class DerivedClassFinder : ITransientDependency { + public ILogger Logger { get; set; } + + public DerivedClassFinder() + { + Logger = NullLogger.Instance; + } + public virtual List Find(string csprojFilePath, string baseClass) { var moduleFilePaths = new List(); + var csprojFileDirectory = Path.GetDirectoryName(csprojFilePath); + var binFile = Path.Combine(csprojFileDirectory, "bin"); + var objFile = Path.Combine(csprojFileDirectory, "obj"); + - var csFiles = new DirectoryInfo(Path.GetDirectoryName(csprojFilePath)) + var csFiles = new DirectoryInfo(csprojFileDirectory) .GetFiles("*.cs", SearchOption.AllDirectories) + .Where(f => !f.DirectoryName.StartsWith(binFile)) + .Where(f => !f.DirectoryName.StartsWith(objFile)) .Select(f => f.FullName) .ToList(); foreach (var csFile in csFiles) { - if (IsDerivedFromAbpModule(csFile, baseClass)) + try { - moduleFilePaths.Add(csFile); + if (IsDerived(csFile, baseClass)) + { + moduleFilePaths.Add(csFile); + } + } + catch (Exception) + { + Logger.LogDebug($"Couldn't parse {csFile}."); } } return moduleFilePaths; } - protected bool IsDerivedFromAbpModule(string csFile, string baseClass) + protected bool IsDerived(string csFile, string baseClass) { + Logger.LogDebug(csFile); var root = CSharpSyntaxTree.ParseText(File.ReadAllText(csFile)).GetRoot(); var namespaceSyntax = root.DescendantNodes().OfType().First(); var classDeclaration = (namespaceSyntax.DescendantNodes().OfType()).First(); + var baseTypeList = classDeclaration.BaseList?.Types.Select(t => t.ToString()).ToList(); + + if (baseTypeList == null) + { + return false; + } + + foreach (var baseType in baseTypeList) + { + if (baseType.Contains('<') && baseType.Substring(0, baseType.IndexOf('<')) == baseClass) + { + return true; + } + if (baseType == baseClass) + { + return true; + } + } - return classDeclaration.BaseList?.Types - .Any(t => t.ToString().Equals(baseClass)) ?? false; + return false; } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationAdder.cs new file mode 100644 index 0000000000..d3178adcb3 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationAdder.cs @@ -0,0 +1,47 @@ +using System; +using System.Diagnostics; +using System.IO; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class EfCoreMigrationAdder : ITransientDependency + { + public void AddMigration(string csprojFile, string module, bool updateDatabase = true) + { + var moduleName = ParseModuleName(module); + var migrationName = "Added_" + moduleName + "_Module" + GetUniquePostFix(); + + var process = Process.Start("CMD.exe", "/C cd \"" + Path.GetDirectoryName(csprojFile) + "\" & dotnet ef migrations add " + migrationName); + process.WaitForExit(); + + if (updateDatabase) + { + UpdateDatabase(csprojFile); + } + } + + protected void UpdateDatabase(string csprojFile) + { + var process = Process.Start("CMD.exe", "/C cd \"" + Path.GetDirectoryName(csprojFile) + "\" & dotnet ef database update"); + process.WaitForExit(); + } + + protected virtual string ParseModuleName(string fullModuleName) + { + var words = fullModuleName?.Split('.'); + + if (words == null || words.Length <= 1) + { + return ""; + } + + return words[words.Length - 1]; + } + + protected virtual string GetUniquePostFix() + { + return "_" + new Random().Next(1,99999); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleClassDependcyAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleClassDependcyAdder.cs index 594c77e5fa..9388ce42fe 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleClassDependcyAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleClassDependcyAdder.cs @@ -7,16 +7,20 @@ namespace Volo.Abp.Cli.ProjectModification { public class ModuleClassDependcyAdder : ITransientDependency { + protected UsingStatementAdder UsingStatementAdder { get; } + + public ModuleClassDependcyAdder(UsingStatementAdder usingStatementAdder) + { + UsingStatementAdder = usingStatementAdder; + } + public virtual void Add(string path, string module) { ParseModuleNameAndNameSpace(module, out var nameSpace, out var moduleName); var file = File.ReadAllText(path); - if (!file.Contains(GetUsingStatement(nameSpace)) ) - { - file = InsertUsingStatement(file, nameSpace); - } + file = UsingStatementAdder.Add(file, nameSpace); if (!file.Contains(moduleName) ) { @@ -34,38 +38,6 @@ namespace Volo.Abp.Cli.ProjectModification return file.Insert(indexOfPublicClassDeclaration, dependsOnAttribute); } - protected virtual string InsertUsingStatement(string file, string nameSpace) - { - var indexOfTheEndOfTheLastUsingStatement = GetIndexOfTheEndOfTheLastUsingStatement(file); - - return file.Insert(indexOfTheEndOfTheLastUsingStatement, Environment.NewLine + GetUsingStatement(nameSpace)); - } - - protected virtual int GetIndexOfTheEndOfTheLastUsingStatement(string file) - { - var indexOfPublicClassDeclaration = GetIndexOfWhereDependsOnWillBeAdded(file); - file = file.Substring(0, indexOfPublicClassDeclaration); - - var indexOfTheStartOfLastUsingStatement = - file.LastIndexOf("using ", StringComparison.Ordinal); - - if (indexOfTheStartOfLastUsingStatement < 0) - { - return 0; - } - - var indexOfFirstSemiColonAfterLastUsingStatement = - file.Substring(indexOfTheStartOfLastUsingStatement).IndexOf(';'); - - if (indexOfFirstSemiColonAfterLastUsingStatement < 0) - { - return 0; - } - - return indexOfTheStartOfLastUsingStatement - + indexOfFirstSemiColonAfterLastUsingStatement + 1; - } - protected virtual int GetIndexOfWhereDependsOnWillBeAdded(string file) { var indexOfPublicClassDeclaration = file.IndexOf("public class", StringComparison.Ordinal); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleInfo.cs index 08c70eeede..d5e7a4e15a 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleInfo.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleInfo.cs @@ -8,6 +8,8 @@ namespace Volo.Abp.Cli.ProjectModification public string DisplayName { get; set; } + public string EfCoreConfigureMethodName { get; set; } + public List NugetPackages { get; set; } public List NpmPackages { get; set; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs index 917a7bf1a0..c62beeacc1 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs @@ -19,22 +19,32 @@ namespace Volo.Abp.Cli.ProjectModification protected IJsonSerializer JsonSerializer { get; } protected ProjectNugetPackageAdder ProjectNugetPackageAdder { get; } + protected DbContextFileBuilderConfigureAdder DbContextFileBuilderConfigureAdder { get; } + protected EfCoreMigrationAdder EfCoreMigrationAdder { get; } protected ProjectNpmPackageAdder ProjectNpmPackageAdder { get; } + protected DerivedClassFinder DerivedClassFinder { get; } public SolutionModuleAdder( IJsonSerializer jsonSerializer, ProjectNugetPackageAdder projectNugetPackageAdder, + DbContextFileBuilderConfigureAdder dbContextFileBuilderConfigureAdder, + EfCoreMigrationAdder efCoreMigrationAdder, + DerivedClassFinder derivedClassFinder, ProjectNpmPackageAdder projectNpmPackageAdder) { + EfCoreMigrationAdder = efCoreMigrationAdder; + DerivedClassFinder = derivedClassFinder; JsonSerializer = jsonSerializer; ProjectNugetPackageAdder = projectNugetPackageAdder; + DbContextFileBuilderConfigureAdder = dbContextFileBuilderConfigureAdder; ProjectNpmPackageAdder = projectNpmPackageAdder; Logger = NullLogger.Instance; } public virtual async Task AddAsync( [NotNull] string solutionFile, - [NotNull] string moduleName) + [NotNull] string moduleName, + bool skipDbMigrations = false) { Check.NotNull(solutionFile, nameof(solutionFile)); Check.NotNull(moduleName, nameof(moduleName)); @@ -75,8 +85,42 @@ namespace Volo.Abp.Cli.ProjectModification Logger.LogDebug("Target project is not available for NPM packages."); } } + + ModifyDbContext(projectFiles, module, skipDbMigrations); } - + + protected void ModifyDbContext(string[] projectFiles, ModuleInfo module, bool skipDbMigrations = false) + { + if (string.IsNullOrWhiteSpace(module.EfCoreConfigureMethodName)) + { + return; + } + + var dbMigrationsProject = projectFiles.FirstOrDefault(p => p.EndsWith(".DbMigrations.csproj")); + + if (dbMigrationsProject == null) + { + Logger.LogDebug("Solution doesn't have a \".DbMigrations\" project."); + return; + } + + var dbContextFile = DerivedClassFinder.Find(dbMigrationsProject, "AbpDbContext").FirstOrDefault(); + + if (dbContextFile == null) + { + Logger.LogDebug($"{dbMigrationsProject} project doesn't have a class that is derived from \"AbpDbContext\"."); + return; + } + + DbContextFileBuilderConfigureAdder.Add(dbContextFile, module.EfCoreConfigureMethodName); + + + if (!skipDbMigrations) + { + EfCoreMigrationAdder.AddMigration(dbMigrationsProject, module.Name); + } + } + protected virtual async Task FindModuleInfoAsync(string moduleName) { using (var client = new HttpClient()) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/UsingStatementAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/UsingStatementAdder.cs new file mode 100644 index 0000000000..aed44cc512 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/UsingStatementAdder.cs @@ -0,0 +1,63 @@ +using System; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class UsingStatementAdder : ITransientDependency + { + public string Add(string fileContent, string nameSpace) + { + if (fileContent.Contains(nameSpace)) + { + return fileContent; + } + + var index = GetIndexOfTheEndOfTheLastUsingStatement(fileContent); + + if (index < 0 || index >= fileContent.Length) + { + index = 0; + } + + var usingStatement = GetUsingStatement(nameSpace); + + return fileContent.Insert(index, usingStatement); + } + + protected string GetUsingStatement(string nameSpace) + { + return Environment.NewLine + "using " + nameSpace + ";"; + } + + protected virtual int GetIndexOfTheEndOfTheLastUsingStatement(string fileContent) + { + var indexOfNamespaceDeclaration = fileContent.IndexOf("namespace", StringComparison.Ordinal); + + if (indexOfNamespaceDeclaration < 0) + { + return 0; + } + + fileContent = fileContent.Substring(0, indexOfNamespaceDeclaration); + + var indexOfTheStartOfLastUsingStatement = + fileContent.LastIndexOf("using ", StringComparison.Ordinal); + + if (indexOfTheStartOfLastUsingStatement < 0) + { + return 0; + } + + var indexOfFirstSemiColonAfterLastUsingStatement = + fileContent.Substring(indexOfTheStartOfLastUsingStatement).IndexOf(';'); + + if (indexOfFirstSemiColonAfterLastUsingStatement < 0) + { + return 0; + } + + return indexOfTheStartOfLastUsingStatement + + indexOfFirstSemiColonAfterLastUsingStatement + 1; + } + } +}