diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingLogicMembers.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingLogicMembersCommand.cs similarity index 97% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingLogicMembers.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingLogicMembersCommand.cs index b9c6ce3..ca3c407 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingLogicMembers.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingLogicMembersCommand.cs @@ -19,7 +19,7 @@ namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile /// /// Code factory command for automation of a C# document when selected from a project in solution explorer. /// - public class AddMissingLogicMembers : CSharpSourceCommandBase + public class AddMissingLogicMembersCommand : CSharpSourceCommandBase { private static readonly string commandTitle = "Add Missing Logic Members"; private static readonly string commandDescription = "Adds missing contract interface members to the Logic implementation."; @@ -27,7 +27,7 @@ public class AddMissingLogicMembers : CSharpSourceCommandBase #pragma warning disable CS1998 /// - public AddMissingLogicMembers(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public AddMissingLogicMembersCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -37,7 +37,7 @@ public AddMissingLogicMembers(ILogger logger, IVsActions vsActions) : base(logge /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(AddMissingLogicMembers).FullName; + public static string Type = typeof(AddMissingLogicMembersCommand).FullName; /// diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingRepositoryMembers.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingRepositoryMembersCommand.cs similarity index 96% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingRepositoryMembers.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingRepositoryMembersCommand.cs index 9db81f5..3f4df55 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingRepositoryMembers.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/AddMissingRepositoryMembersCommand.cs @@ -12,16 +12,15 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile { - /// - /// Code factory command for automation of a C# document when selected from a project in solution explorer. - /// - public class AddMissingRepositoryMembers : CSharpSourceCommandBase + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class AddMissingRepositoryMembersCommand : CSharpSourceCommandBase { private static readonly string commandTitle = "Add Missing Repository Members"; private static readonly string commandDescription = "Adds missing contract interface members to the repository implementation."; @@ -29,7 +28,7 @@ public class AddMissingRepositoryMembers : CSharpSourceCommandBase #pragma warning disable CS1998 /// - public AddMissingRepositoryMembers(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public AddMissingRepositoryMembersCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -39,7 +38,7 @@ public AddMissingRepositoryMembers(ILogger logger, IVsActions vsActions) : base( /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(AddMissingRepositoryMembers).FullName; + public static string Type = typeof(AddMissingRepositoryMembersCommand).FullName; /// diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepository.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepository.cs deleted file mode 100644 index 96b5d8e..0000000 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepository.cs +++ /dev/null @@ -1,529 +0,0 @@ -using CodeFactory.Automation.NDF.Logic.Data.Sql.EF; -using CodeFactory.Automation.NDF.Logic; -using CodeFactory.Automation.Standard.Logic.FluentValidation; -using CodeFactory.Automation.Standard.Logic; -using CodeFactory.WinVs; -using CodeFactory.WinVs.Commands; -using CodeFactory.WinVs.Commands.SolutionExplorer; -using CodeFactory.WinVs.Logging; -using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; -using CodeFactory.WinVs.Models.ProjectSystem; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using CodeFactory.Automation.NDF.Logic.Testing.MSTest; -using CodeFactory.Automation.NDF.Logic.General; - -namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile -{ - /// - /// Code factory command for automation of a C# document when selected from a project in solution explorer. - /// - public class RefreshEFRepository : CSharpSourceCommandBase - { - private static readonly string commandTitle = "Refresh EF Repository"; - private static readonly string commandDescription = "Refreshes the EF repository and models implementation."; - - -#pragma warning disable CS1998 - - /// - public RefreshEFRepository(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) - { - //Intentionally blank - } - - #region External Configuration - - /// - /// The fully qualified name of the command to be used with configuration. - /// - public static string Type = typeof(RefreshEFRepository).FullName; - - /// - /// The execution project that contains the definition of the entity framework entity models. - /// - public static string ExecutionProject = "ExecutionProject"; - - /// - /// The execution project folder the entity framework models are stored in. This is optional and only used when entities are stored in a target folder. - /// - public static string ExecutionModelFolder = "ExecutionModelFolder"; - - /// - /// The project where application level entities will be created and stored. - /// - public static string EntityProject = "EntityProject"; - - /// - /// The target folder where application entities will be created and stored. This is optional and only used when entities are stored in a target folder. - /// - public static string EntityFolder = "EntityFolder"; - - /// - /// The repository project where repositories will be created or updated in. - /// - public static string RepoProject = "RepoProject"; - - /// - /// The target folder where repositories will be created and stored. This is optional and only used when repositories are stored in a target folder. - /// - public static string RepoFolder = "RepoFolder"; - - /// - /// The target project that will store the interface contract definition for the created and updated repositories. - /// - public static string RepoContractProject = "RepoContractProject"; - - /// - /// The target folder where interface contract definitions for repositories is stored. This is optional and only used with interface contracts for repositories stored in the target folder. - /// - public static string RepoContractFolder = "RepoContractFolder"; - - /// - /// The target project to generate and update integration tests that support the repository. - /// - public static string IntegrationTestProject = "IntegrationTestProject"; - - /// - /// The target folder where integration tests will be stored. This is optional and only used with integration tests that are to be stored in the target folder. - /// - public static string IntegrationTestFolder = "IntegrationTestFolder"; - - /// - /// The name of the entity framework context class. - /// - public static string EFContextClassName = "EFContextClassName"; - - /// - /// Optional, list of prefixes seperated by comma to remove from the name of the entity framework entity. - /// - public static string EFEntityRemovePrefixes = "EFEntityRemovePrefixes"; - - /// - /// Optional, list of prefixes seperated by comma to remove from the name of the entity framework entity. - /// - public static string EFEntityRemoveSuffixes = "EFEntityRemoveSuffixes"; - - /// - /// Optional, prefix to assign to a repository when creating. - /// - public static string RepositoryPrefix = "RepositoryPrefix"; - - /// - /// Optional, suffix to assign to a repository when creating. - /// - public static string RepositorySuffix = "RepositorySuffix"; - - /// - /// Optional, prefix to assign to a application model when creating. - /// - public static string AppModelPrefix ="AppModelPrefix"; - - /// - /// Optional, suffix to assign to a application model when creating. - /// - public static string AppModelSuffix ="AppModelSuffix"; - - /// - /// Optional, prefix to assign to a integration test when creating. - /// - public static string TestPrefix ="TestPrefix"; - - /// - /// Optional, suffix to assign to a integration test when creating. - /// - public static string TestSuffix ="TestSuffix"; - - /// - /// The prefix to assign to the name of a application models validation class. - /// - public static string AppModelValidatorPrefix = "AppModelValidatorPrefix"; - - /// - /// The suffix to assign to the name of a application models validation class. - /// - public static string AppModelValidatorSuffix = "AppModelValidatorSuffix"; - - /// - /// Loads the external configuration definition for this command. - /// - /// Will return the command configuration or null if this command does not support external configurations. - public override ConfigCommand LoadExternalConfigDefinition() - { - var command = new ConfigCommand { Category = "RefreshEFRepository", Name = "EFRepositoryRefresh", CommandType = Type } - .UpdateExecutionProject - ( - new ConfigProject - { - Name = ExecutionProject, - Guidance = "Enter the fully project name for the project that hosts the EF models." - } - .AddFolder - ( - new ConfigFolder - { - Name = ExecutionModelFolder, - Required = false, - Guidance = - "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = EFContextClassName, - Guidance = "Enter the class name of the database context used by entity framework." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = EFEntityRemovePrefixes, - Guidance = "Comma seperated value list of the prefixes in case sensitive format to be removed from the entity framework entity name when creating new objects." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = EFEntityRemoveSuffixes, - Guidance = "Comma seperated value list of the suffixes in case sensitive format to be removed from the entity framework entity name when creating new objects." - } - ) - ) - .AddProject - ( - new ConfigProject - { - Name = EntityProject, - Guidance = - "Enter the full project name for the project that hosts the generated application POCO models that represent the EF Models." - } - .AddFolder - ( - new ConfigFolder - { - Name = EntityFolder, - Required = false, - Guidance = - "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = AppModelPrefix, - Guidance = "Optional, prefix to assign to the application model entity when it is created." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = AppModelSuffix, - Guidance = "Optional, suffix to assign to the application model entity when it is created.", - Value = "AppModel" - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = AppModelValidatorPrefix, - Guidance = "Optional, prefix to assign to the application model entity validator when it is created." - - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = AppModelValidatorSuffix, - Guidance = "Optional, suffix to assign to the application model entity validator when it is created.", - Value = "Validator" - } - ) - ) - .AddProject - ( - new ConfigProject - { - Name = RepoProject, - Guidance = - "Enter the full project name for the project that hosts the generated repositories that represent the EF Models." - } - .AddFolder - ( - new ConfigFolder() - { - Name = RepoFolder, - Required = false, - Guidance = - "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = RepositoryPrefix, - Guidance = "Optional, prefix to assign to the repository when it is created." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = RepositorySuffix, - Guidance = "Optional, suffix to assign to the repository when it is created.", - Value = "Repository" - } - ) - ) - .AddProject - ( - new ConfigProject - { - Name = RepoContractProject, - Guidance = - "Enter the full project name for the project that hosts the interface definition for the repository contract." - } - .AddFolder - ( - new ConfigFolder - { - Name = RepoContractFolder, - Required = false, - Guidance = - "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." - } - ) - ) - .AddProject - ( - new ConfigProject - { - Name = IntegrationTestProject, - Guidance = - "Enter the full project name for the project that hosts the generated integration test for the repository." - } - .AddFolder - ( - new ConfigFolder() - { - Name = IntegrationTestFolder, - Required = false, - Guidance = - "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = TestPrefix, - Guidance = "Optional, prefix to assign to the integration test when it is created." - } - ) - .AddParameter - ( - new ConfigParameter - { - Name = TestSuffix, - Guidance = "Optional, suffix to assign to the integration test when it is created.", - Value = "Test" - } - ) - ); - - return command; - } - #endregion - - #region Overrides of VsCommandBase - - /// - /// Validation logic that will determine if this command should be enabled for execution. - /// - /// The target model data that will be used to determine if this command should be enabled. - /// Boolean flag that will tell code factory to enable this command or disable it. - public override async Task EnableCommandAsync(VsCSharpSource result) - { - //Result that determines if the command is enabled and visible in the context menu for execution. - bool isEnabled = false; - - try - { - isEnabled = (await ConfigManager.LoadCommandByFolderAsync(Type, ExecutionModelFolder, result, FolderLoadType.TargetFolderOnly)) != null; - - if (!isEnabled) isEnabled = (await ConfigManager.LoadCommandByProjectAsync(Type, result)) != null; - } - catch (Exception unhandledError) - { - _logger.Error($"The following unhandled error occurred while checking if the solution explorer C# document command {commandTitle} is enabled. ", - unhandledError); - isEnabled = false; - } - - return isEnabled; - } - - /// - /// Code factory framework calls this method when the command has been executed. - /// - /// The code factory model that has generated and provided to the command to process. - public override async Task ExecuteCommandAsync(VsCSharpSource result) - { - try - { - var command = (await ConfigManager.LoadCommandByFolderAsync(Type, ExecutionModelFolder, result) - ?? await ConfigManager.LoadCommandByProjectAsync(Type, result)) - ?? throw new CodeFactoryException("Could not load the automation configuration cannot refresh the EF repository."); - - VsProject efModelProject = await VisualStudioActions.GetProjectFromConfigAsync(command.ExecutionProject) - ?? throw new CodeFactoryException("Could load the EF hosting project, cannot refresh the EF repository."); - - VsProjectFolder efModelFolder = - await VisualStudioActions.GetProjectFolderFromConfigAsync(command.ExecutionProject, ExecutionModelFolder); - - var contextClassName = command.ExecutionProject.ParameterValue(EFContextClassName); - var efEntityRemovePrefixs = command.ExecutionProject.ParameterValue(EFEntityRemovePrefixes); - var efEntityRemoveSuffixes = command.ExecutionProject.ParameterValue(EFEntityRemoveSuffixes); - - VsProject appModelProject = await VisualStudioActions.GetProjectFromConfigAsync(command.Project(EntityProject)) - ?? throw new CodeFactoryException("Could not load the entity model project, cannot refresh the EF repository."); - - VsProjectFolder appModelFolder = - await VisualStudioActions.GetProjectFolderFromConfigAsync(command.Project(EntityProject), EntityFolder); - - var appModelPrefix = command.Project(EntityProject).ParameterValue(AppModelPrefix); - var appModelSuffix = command.Project(EntityProject).ParameterValue(AppModelSuffix); - var appModelValidatorPrefix = command.Project(EntityProject).ParameterValue(AppModelValidatorPrefix); - var appModelValidatorSuffix = command.Project(EntityProject).ParameterValue(AppModelValidatorSuffix); - - VsProject repoProject = await VisualStudioActions.GetProjectFromConfigAsync(command.Project(RepoProject)) - ?? throw new CodeFactoryException("Could not load the repository project, cannot refresh the EF repository."); - - VsProjectFolder repoFolder = - await VisualStudioActions.GetProjectFolderFromConfigAsync(command.Project(RepoProject), RepoFolder); - - var repoPrefix = command.Project(RepoProject).ParameterValue(RepositoryPrefix); - - var repoSuffix = command.Project(RepoProject).ParameterValue(RepositorySuffix); - - VsProject contractProject = - await VisualStudioActions.GetProjectFromConfigAsync(command.Project(RepoContractProject)) ?? - throw new CodeFactoryException( - "Could not load the repository contract project, cannot refresh the EF repository."); - - - VsProjectFolder contractFolder = await VisualStudioActions.GetProjectFolderFromConfigAsync(command.Project(RepoContractProject), RepoFolder); - - VsProject testProject = - await VisualStudioActions.GetProjectFromConfigAsync(command.Project(IntegrationTestProject)); - - VsProjectFolder testFolder = - await VisualStudioActions.GetProjectFolderFromConfigAsync(command.Project(IntegrationTestProject), - IntegrationTestFolder); - - var testPrefix = testProject != null ? command.Project(IntegrationTestProject).ParameterValue(TestPrefix) : null; - - var testSuffix = testProject != null ? command.Project(IntegrationTestProject).ParameterValue(TestSuffix) : null; - - if (string.IsNullOrWhiteSpace(contextClassName)) - throw new CodeFactoryException( - "The entity framework context class name was not provided, cannot refresh the EF repository."); - - var contextClass = (await efModelProject.FindCSharpSourceByClassNameAsync(contextClassName)) - ?.SourceCode?.Classes?.FirstOrDefault() - ?? throw new CodeFactoryException($"The entity framework context class '{contextClassName}' could not be loaded, cannot refresh the EF repository,"); - - await VisualStudioActions.RefreshDbContextAsync(contextClassName, efModelProject, efModelFolder); - - bool supportsLogging = await repoProject.SupportsLogging(); - - bool supportsNDF = await repoProject.SupportsNDF(); - - var efModel = result.SourceCode?.Classes?.FirstOrDefault() - ?? throw new CodeFactoryException("The EF entity class could not be loaded, cannot refresh the EF repository."); - - var nameManagement = NameManagement.Init(efEntityRemovePrefixs,efEntityRemoveSuffixes,appModelPrefix,appModelSuffix); - - var appModel = (await VisualStudioActions.RefreshModelAsync(efModel, appModelProject, - EntityModelNamespaces(),nameManagement, appModelFolder, $"Application data model that supports '{efModel.Name}'", true,useSourceProperty: RepositoryBuilder.UseSourceProperty)) - ?? throw new CodeFactoryException($"Could not load the entity that supports the ef model '{efModel.Name}', cannot refresh the EF repository."); - - string noReplacePrefix = null; - string noReplaceSuffix = null; - - var modelValidatorNameManagement = NameManagement.Init(noReplacePrefix,noReplaceSuffix,appModelValidatorPrefix,appModelValidatorSuffix); - - var validation = (await VisualStudioActions.RefreshValidationClassAsync(appModel,appModelProject,appModelFolder,modelValidatorNameManagement)) - ?? throw new CodeFactoryException($"Could not refresh the validation for the app model '{appModel.Name}', cannot refresh the EF repository."); - - await VisualStudioActions.RefreshFluentValidationAsync(efModel,appModel,validation); - - await VisualStudioActions.RefreshEntityFrameworkEntityTransform(appModel, efModel, efModelProject, - efModelFolder); - - var repositoryName = NameManagement.Init(efEntityRemovePrefixs,efEntityRemoveSuffixes,repoPrefix,repoSuffix).FormatName(efModel.Name); - - var repoClass = await VisualStudioActions.RefreshEFRepositoryAsync(repositoryName,efModel, repoProject, contractProject, appModel, - contextClass, supportsNDF, supportsLogging, repoFolder, contractFolder); - - - if(repoClass != null & testProject != null) - { - var contractName = $"I{repositoryName}"; - - CsInterface contractInterface = contractFolder != null ? (await contractFolder.FindCSharpSourceByInterfaceNameAsync(contractName))?.SourceCode?.Interfaces?.FirstOrDefault() - : (await contractProject.FindCSharpSourceByInterfaceNameAsync(contractName))?.SourceCode?.Interfaces?.FirstOrDefault(); - - if(contractInterface != null) - { - var testName = NameManagement.Init(noReplacePrefix,noReplaceSuffix,testPrefix,testSuffix).FormatName(repositoryName); - await VisualStudioActions.RefreshMSTestIntegrationTestAsync(testName, contractInterface,testProject); - } - } - - } - catch (CodeFactoryException codeFactoryError) - { - MessageBox.Show(codeFactoryError.Message, "Automation Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - catch (Exception unhandledError) - { - _logger.Error($"The following unhandled error occurred while executing the solution explorer C# document command {commandTitle}. ", - unhandledError); - - } - - } - - #endregion - - /// - /// Helper method that creates the default namespaces to add to a new entity class that is crearted. - /// - /// List of using statements to use. - private List EntityModelNamespaces() - { - return new List - { - new ManualUsingStatementNamespace("System"), - new ManualUsingStatementNamespace("System.Collections.Generic"), - new ManualUsingStatementNamespace("System.Linq"), - new ManualUsingStatementNamespace("System.Text") - }; - } - } - -} diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepositoryCommand.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepositoryCommand.cs new file mode 100644 index 0000000..85311b8 --- /dev/null +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepositoryCommand.cs @@ -0,0 +1,558 @@ +using CodeFactory.Automation.NDF.Logic; +using CodeFactory.Automation.NDF.Logic.Data.Sql.EF; +using CodeFactory.Automation.NDF.Logic.General; +using CodeFactory.Automation.NDF.Logic.Testing.MSTest; +using CodeFactory.Automation.NDF.Logic.Testing.XUnit; +using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Commands; +using CodeFactory.WinVs.Commands.SolutionExplorer; +using CodeFactory.WinVs.Logging; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.ProjectSystem; +using Drives.Automation.NDF.Logic.Data.Sql.EF; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile +{ + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class RefreshEFRepositoryCommand : CSharpSourceCommandBase + { + private static readonly string commandTitle = "Refresh EF Repository"; + private static readonly string commandDescription = "Refreshes the EF repository and models implementation."; + + +#pragma warning disable CS1998 + + /// + public RefreshEFRepositoryCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + { + //Intentionally blank + } + + #region External Configuration + + /// + /// The fully qualified name of the command to be used with configuration. + /// + public static string Type = typeof(RefreshEFRepositoryCommand).FullName; + + /// + /// The execution project that contains the definition of the entity framework entity models. + /// + public static string ExecutionProject = "ExecutionProject"; + + /// + /// The execution project folder the entity framework models are stored in. This is optional and only used when entities are stored in a target folder. + /// + public static string ExecutionModelFolder = "ExecutionModelFolder"; + + /// + /// The project where application level entities will be created and stored. + /// + public static string EntityProject = "EntityProject"; + + /// + /// The target folder where application entities will be created and stored. This is optional and only used when entities are stored in a target folder. + /// + public static string EntityFolder = "EntityFolder"; + + /// + /// The repository project where repositories will be created or updated in. + /// + public static string RepoProject = "RepoProject"; + + /// + /// The target folder where repositories will be created and stored. This is optional and only used when repositories are stored in a target folder. + /// + public static string RepoFolder = "RepoFolder"; + + /// + /// The target project that will store the interface contract definition for the created and updated repositories. + /// + public static string RepoContractProject = "RepoContractProject"; + + /// + /// The target folder where interface contract definitions for repositories is stored. This is optional and only used with interface contracts for repositories stored in the target folder. + /// + public static string RepoContractFolder = "RepoContractFolder"; + + /// + /// The target project to generate and update integration tests that support the repository. + /// + public static string IntegrationTestProject = "IntegrationTestProject"; + + /// + /// The target folder where integration tests will be stored. This is optional and only used with integration tests that are to be stored in the target folder. + /// + public static string IntegrationTestFolder = "IntegrationTestFolder"; + + /// + /// The name of the entity framework context class. + /// + public static string EFContextClassName = "EFContextClassName"; + + /// + /// Optional, list of prefixes seperated by comma to remove from the name of the entity framework entity. + /// + public static string EFEntityRemovePrefixes = "EFEntityRemovePrefixes"; + + /// + /// Optional, list of prefixes seperated by comma to remove from the name of the entity framework entity. + /// + public static string EFEntityRemoveSuffixes = "EFEntityRemoveSuffixes"; + + /// + /// Optional, prefix to assign to a repository when creating. + /// + public static string RepositoryPrefix = "RepositoryPrefix"; + + /// + /// Optional, suffix to assign to a repository when creating. + /// + public static string RepositorySuffix = "RepositorySuffix"; + + /// + /// Optional, prefix to assign to a application model when creating. + /// + public static string AppModelPrefix = "AppModelPrefix"; + + /// + /// Optional, suffix to assign to a application model when creating. + /// + public static string AppModelSuffix = "AppModelSuffix"; + + /// + /// Optional, prefix to assign to a integration test when creating. + /// + public static string TestPrefix = "TestPrefix"; + + /// + /// Optional, suffix to assign to a integration test when creating. + /// + public static string TestSuffix = "TestSuffix"; + + /// + /// The prefix to assign to the name of a application models validation class. + /// + public static string AppModelValidatorPrefix = "AppModelValidatorPrefix"; + + /// + /// The suffix to assign to the name of a application models validation class. + /// + public static string AppModelValidatorSuffix = "AppModelValidatorSuffix"; + + /// + /// Optional, flag to generate default Add, Update, and Delete operations. + /// + public static string GenerateCrudOperations = "GenerateCrudOperations"; + + /// + /// Loads the external configuration definition for this command. + /// + /// Will return the command configuration or null if this command does not support external configurations. + public override ConfigCommand LoadExternalConfigDefinition() + { + var command = new ConfigCommand { Category = "RefreshEFRepository", Name = "EFRepositoryRefresh", CommandType = Type } + .UpdateExecutionProject + ( + new ConfigProject + { + Name = ExecutionProject, + Guidance = "Enter the fully project name for the project that hosts the EF models." + } + .AddFolder + ( + new ConfigFolder + { + Name = ExecutionModelFolder, + Required = false, + Guidance = + "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = EFContextClassName, + Guidance = "Enter the class name of the database context used by entity framework." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = EFEntityRemovePrefixes, + Guidance = "Comma seperated value list of the prefixes in case sensitive format to be removed from the entity framework entity name when creating new objects." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = EFEntityRemoveSuffixes, + Guidance = "Comma seperated value list of the suffixes in case sensitive format to be removed from the entity framework entity name when creating new objects." + } + ) + ) + .AddProject + ( + new ConfigProject + { + Name = EntityProject, + Guidance = + "Enter the full project name for the project that hosts the generated application POCO models that represent the EF Models." + } + .AddFolder + ( + new ConfigFolder + { + Name = EntityFolder, + Required = false, + Guidance = + "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = AppModelPrefix, + Guidance = "Optional, prefix to assign to the application model entity when it is created." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = AppModelSuffix, + Guidance = "Optional, suffix to assign to the application model entity when it is created.", + Value = "AppModel" + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = AppModelValidatorPrefix, + Guidance = "Optional, prefix to assign to the application model entity validator when it is created." + + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = AppModelValidatorSuffix, + Guidance = "Optional, suffix to assign to the application model entity validator when it is created.", + Value = "Validator" + } + ) + ) + .AddProject + ( + new ConfigProject + { + Name = RepoProject, + Guidance = + "Enter the full project name for the project that hosts the generated repositories that represent the EF Models." + } + .AddFolder + ( + new ConfigFolder() + { + Name = RepoFolder, + Required = false, + Guidance = + "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = RepositoryPrefix, + Guidance = "Optional, prefix to assign to the repository when it is created." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = RepositorySuffix, + Guidance = "Optional, suffix to assign to the repository when it is created.", + Value = "Repository" + } + ) + ) + .AddProject + ( + new ConfigProject + { + Name = RepoContractProject, + Guidance = + "Enter the full project name for the project that hosts the interface definition for the repository contract." + } + .AddFolder + ( + new ConfigFolder + { + Name = RepoContractFolder, + Required = false, + Guidance = + "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = GenerateCrudOperations, + Guidance = "True by default. Defines whether the initial contracts include methods for Create, Update, and Delete.", + Value = "true" + } + ) + ) + .AddProject + ( + new ConfigProject + { + Name = IntegrationTestProject, + Guidance = + "Enter the full project name for the project that hosts the generated integration test for the repository." + } + .AddFolder + ( + new ConfigFolder() + { + Name = IntegrationTestFolder, + Required = false, + Guidance = + "Optional, set the relative path from the root of the project. If it is more then one directory deep then use '/' instead of back slashes." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = TestPrefix, + Guidance = "Optional, prefix to assign to the integration test when it is created." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = TestSuffix, + Guidance = "Optional, suffix to assign to the integration test when it is created.", + Value = "Test" + } + ) + ); + + return command; + } + #endregion + + #region Overrides of VsCommandBase + + /// + /// Validation logic that will determine if this command should be enabled for execution. + /// + /// The target model data that will be used to determine if this command should be enabled. + /// Boolean flag that will tell code factory to enable this command or disable it. + public override async Task EnableCommandAsync(VsCSharpSource result) + { + //Result that determines if the command is enabled and visible in the context menu for execution. + bool isEnabled = false; + + try + { + isEnabled = (await ConfigManager.LoadCommandByFolderAsync(Type, ExecutionModelFolder, result, FolderLoadType.TargetFolderOnly)) != null; + + if (!isEnabled) isEnabled = (await ConfigManager.LoadCommandByProjectAsync(Type, result)) != null; + } + catch (Exception unhandledError) + { + _logger.Error($"The following unhandled error occurred while checking if the solution explorer C# document command {commandTitle} is enabled. ", + unhandledError); + isEnabled = false; + } + + return isEnabled; + } + + /// + /// Code factory framework calls this method when the command has been executed. + /// + /// The code factory model that has generated and provided to the command to process. + public override async Task ExecuteCommandAsync(VsCSharpSource result) + { + try + { + var command = (await ConfigManager.LoadCommandByFolderAsync(Type, ExecutionModelFolder, result) + ?? await ConfigManager.LoadCommandByProjectAsync(Type, result)) + ?? throw new CodeFactoryException("Could not load the automation configuration cannot refresh the EF repository."); + + VsProject efModelProject = await VisualStudioActions.GetProjectFromConfigAsync(command.ExecutionProject) + ?? throw new CodeFactoryException("Could load the EF hosting project, cannot refresh the EF repository."); + + VsProjectFolder efModelFolder = + await VisualStudioActions.GetProjectFolderFromConfigAsync(command.ExecutionProject, ExecutionModelFolder); + + var contextClassName = command.ExecutionProject.ParameterValue(EFContextClassName); + var efEntityRemovePrefixs = command.ExecutionProject.ParameterValue(EFEntityRemovePrefixes); + var efEntityRemoveSuffixes = command.ExecutionProject.ParameterValue(EFEntityRemoveSuffixes); + + VsProject appModelProject = await VisualStudioActions.GetProjectFromConfigAsync(command.Project(EntityProject)) + ?? throw new CodeFactoryException("Could not load the entity model project, cannot refresh the EF repository."); + + VsProjectFolder appModelFolder = + await VisualStudioActions.GetProjectFolderFromConfigAsync(command.Project(EntityProject), EntityFolder); + + var appModelPrefix = command.Project(EntityProject).ParameterValue(AppModelPrefix); + var appModelSuffix = command.Project(EntityProject).ParameterValue(AppModelSuffix); + var appModelValidatorPrefix = command.Project(EntityProject).ParameterValue(AppModelValidatorPrefix); + var appModelValidatorSuffix = command.Project(EntityProject).ParameterValue(AppModelValidatorSuffix); + + VsProject repoProject = await VisualStudioActions.GetProjectFromConfigAsync(command.Project(RepoProject)) + ?? throw new CodeFactoryException("Could not load the repository project, cannot refresh the EF repository."); + + VsProjectFolder repoFolder = + await VisualStudioActions.GetProjectFolderFromConfigAsync(command.Project(RepoProject), RepoFolder); + + var repoPrefix = command.Project(RepoProject).ParameterValue(RepositoryPrefix); + + var repoSuffix = command.Project(RepoProject).ParameterValue(RepositorySuffix); + + VsProject contractProject = + await VisualStudioActions.GetProjectFromConfigAsync(command.Project(RepoContractProject)) ?? + throw new CodeFactoryException( + "Could not load the repository contract project, cannot refresh the EF repository."); + + + VsProjectFolder contractFolder = await VisualStudioActions.GetProjectFolderFromConfigAsync(command.Project(RepoContractProject), RepoFolder); + + VsProject testProject = + await VisualStudioActions.GetProjectFromConfigAsync(command.Project(IntegrationTestProject)); + + VsProjectFolder testFolder = + await VisualStudioActions.GetProjectFolderFromConfigAsync(command.Project(IntegrationTestProject), + IntegrationTestFolder); + + var testPrefix = testProject != null ? command.Project(IntegrationTestProject).ParameterValue(TestPrefix) : null; + + var testSuffix = testProject != null ? command.Project(IntegrationTestProject).ParameterValue(TestSuffix) : null; + + if (string.IsNullOrWhiteSpace(contextClassName)) + throw new CodeFactoryException( + "The entity framework context class name was not provided, cannot refresh the EF repository."); + + var contextClass = (await efModelProject.FindCSharpSourceByClassNameAsync(contextClassName)) + ?.SourceCode?.Classes?.FirstOrDefault() + ?? throw new CodeFactoryException($"The entity framework context class '{contextClassName}' could not be loaded, cannot refresh the EF repository,"); + + await VisualStudioActions.RefreshDbContextAsync(contextClassName, efModelProject, efModelFolder); + + bool supportsLogging = await repoProject.SupportsLogging(); + + bool supportsNDF = await repoProject.SupportsNDF(); + + var efModel = result.SourceCode?.Classes?.FirstOrDefault() + ?? throw new CodeFactoryException("The EF entity class could not be loaded, cannot refresh the EF repository."); + + var nameManagement = NameManagement.Init(efEntityRemovePrefixs, efEntityRemoveSuffixes, appModelPrefix, appModelSuffix); + + var appModel = (await VisualStudioActions.RefreshModelWithoutDependenciesAsync(efModel, appModelProject, + EntityModelNamespaces(), nameManagement, appModelFolder, $"Application data model that supports '{efModel.Name}'", true, useSourceProperty: RepositoryBuilder.UseSourceProperty)) + ?? throw new CodeFactoryException($"Could not load the entity that supports the ef model '{efModel.Name}', cannot refresh the EF repository."); + + string noReplacePrefix = null; + string noReplaceSuffix = null; + + var modelValidatorNameManagement = NameManagement.Init(noReplacePrefix, noReplaceSuffix, appModelValidatorPrefix, appModelValidatorSuffix); + + var validation = (await VisualStudioActions.RefreshValidationClassAsync(appModel, appModelProject, appModelFolder, modelValidatorNameManagement)) + ?? throw new CodeFactoryException($"Could not refresh the validation for the app model '{appModel.Name}', cannot refresh the EF repository."); + + await VisualStudioActions.RefreshFluentValidationAsync(efModel, appModel, validation); + + await VisualStudioActions.RefreshEntityFrameworkEntityTransform(appModel, efModel, efModelProject, + efModelFolder); + + var repositoryName = NameManagement.Init(efEntityRemovePrefixs, efEntityRemoveSuffixes, repoPrefix, repoSuffix).FormatName(efModel.Name); + + // Get boolean parameters to know what methods we are going to generate by default. + bool generateCrudOperations = bool.Parse(command.Project(RepoContractProject).ParameterValue(GenerateCrudOperations)); + + var repoClass = await VisualStudioActions.RefreshEFRepositoryWithCRUDAsync(repositoryName, efModel, repoProject, contractProject, appModel, + contextClass, supportsNDF, supportsLogging, repoFolder, contractFolder, generateCrudOperations); + + if (repoClass != null & testProject != null) + { + var contractName = $"I{repositoryName}"; + + CsInterface contractInterface = contractFolder != null ? (await contractFolder.FindCSharpSourceByInterfaceNameAsync(contractName))?.SourceCode?.Interfaces?.FirstOrDefault() + : (await contractProject.FindCSharpSourceByInterfaceNameAsync(contractName))?.SourceCode?.Interfaces?.FirstOrDefault(); + + if (contractInterface != null) + { + var testName = NameManagement.Init(noReplacePrefix, noReplaceSuffix, testPrefix, testSuffix).FormatName(repositoryName); + + + if (await testProject.TestProjectIsConfiguredMSTestAsync()) + { + var RefreshMSTestCommand = new RefreshMSTestCommand(null, null); + await VisualStudioActions.RefreshMSTestIntegrationTestAsync(testName, contractInterface, testProject); + } + + if (await testProject.TestProjectIsConfiguredXUnitAsync()) + { + var RefreshXUnitTestCommand = new RefreshXUnitTestCommand(null, null); + await VisualStudioActions.RefreshIntegrationTestAsync(testName, contractInterface, testProject); + + } + } + } + + } + catch (CodeFactoryException codeFactoryError) + { + MessageBox.Show(codeFactoryError.Message, "Automation Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + catch (Exception unhandledError) + { + _logger.Error($"The following unhandled error occurred while executing the solution explorer C# document command {commandTitle}. ", + unhandledError); + + } + + } + + #endregion + + /// + /// Helper method that creates the default namespaces to add to a new entity class that is crearted. + /// + /// List of using statements to use. + private List EntityModelNamespaces() + { + return new List + { + new ManualUsingStatementNamespace("System"), + new ManualUsingStatementNamespace("System.Collections.Generic"), + new ManualUsingStatementNamespace("System.Linq"), + new ManualUsingStatementNamespace("System.Text") + }; + } + } + +} diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshFluentValidation.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshFluentValidationCommand.cs similarity index 92% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshFluentValidation.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshFluentValidationCommand.cs index a84831c..2215d9e 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshFluentValidation.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshFluentValidationCommand.cs @@ -1,25 +1,21 @@ -using CodeFactory.Automation.Standard.Logic.FluentValidation; -using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.FluentValidation; using CodeFactory.WinVs; using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.SolutionExplorer; using CodeFactory.WinVs.Logging; -using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile { - /// - /// Code factory command for automation of a C# document when selected from a project in solution explorer. - /// - public class RefreshFluentValidation : CSharpSourceCommandBase + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class RefreshFluentValidationCommand : CSharpSourceCommandBase { private static readonly string commandTitle = "Refresh Validation"; private static readonly string commandDescription = "Refreshes the fluent validation class that supports the class implemented in this source file."; @@ -27,7 +23,7 @@ public class RefreshFluentValidation : CSharpSourceCommandBase #pragma warning disable CS1998 /// - public RefreshFluentValidation(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public RefreshFluentValidationCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -37,7 +33,7 @@ public RefreshFluentValidation(ILogger logger, IVsActions vsActions) : base(logg /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(RefreshFluentValidation).FullName; + public static string Type = typeof(RefreshFluentValidationCommand).FullName; /// /// The execution project that contains the definition of the model to refresh validation in. @@ -67,7 +63,7 @@ public RefreshFluentValidation(ILogger logger, IVsActions vsActions) : base(logg public override ConfigCommand LoadExternalConfigDefinition() { return new ConfigCommand - { Category = "ModelValidation", Name = nameof(RefreshFluentValidation), CommandType = Type } + { Category = "ModelValidation", Name = nameof(RefreshFluentValidationCommand), CommandType = Type } .UpdateExecutionProject ( new ConfigProject diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshLogic.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshLogicCommand.cs similarity index 96% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshLogic.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshLogicCommand.cs index 347ac4f..6439ed8 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshLogic.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshLogicCommand.cs @@ -1,25 +1,23 @@ using CodeFactory.Automation.NDF.Logic.General; using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.Extensions; using CodeFactory.WinVs; using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.SolutionExplorer; using CodeFactory.WinVs.Logging; using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile { - /// - /// Code factory command for automation of a C# document when selected from a project in solution explorer. - /// - public class RefreshLogic : CSharpSourceCommandBase + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class RefreshLogicCommand : CSharpSourceCommandBase { private static readonly string commandTitle = "Refresh Logic"; private static readonly string commandDescription = "Refreshes the logic implementation for the target logic interface."; @@ -27,7 +25,7 @@ public class RefreshLogic : CSharpSourceCommandBase #pragma warning disable CS1998 /// - public RefreshLogic(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public RefreshLogicCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -37,7 +35,7 @@ public RefreshLogic(ILogger logger, IVsActions vsActions) : base(logger, vsActio /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(RefreshLogic).FullName; + public static string Type = typeof(RefreshLogicCommand).FullName; /// /// Exection project for the command. diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshTest.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshMSTestCommand.cs similarity index 89% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshTest.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshMSTestCommand.cs index 81513b7..edb7b14 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshTest.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshMSTestCommand.cs @@ -1,33 +1,31 @@ using CodeFactory.Automation.NDF.Logic.Testing.MSTest; using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.Extensions; using CodeFactory.WinVs; using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.SolutionExplorer; using CodeFactory.WinVs.Logging; using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile { - /// - /// Code factory command for automation of a C# document when selected from a project in solution explorer. - /// - public class RefreshTest : CSharpSourceCommandBase + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class RefreshMSTestCommand : CSharpSourceCommandBase { - private static readonly string commandTitle = "Refresh Test"; - private static readonly string commandDescription = "Refreshes an integration test from the target interface."; + private static readonly string commandTitle = "Refresh MSTest Test"; + private static readonly string commandDescription = "Refreshes an MSTest integration test from the target interface."; #pragma warning disable CS1998 /// - public RefreshTest(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public RefreshMSTestCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -37,7 +35,7 @@ public RefreshTest(ILogger logger, IVsActions vsActions) : base(logger, vsAction /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(RefreshTest).FullName; + public static string Type = typeof(RefreshMSTestCommand).FullName; /// /// Project executing the command @@ -59,7 +57,6 @@ public RefreshTest(ILogger logger, IVsActions vsActions) : base(logger, vsAction /// public static string TestSuffix = "TestSuffix"; - /// /// Loads the external configuration definition for this command. /// @@ -69,7 +66,7 @@ public override ConfigCommand LoadExternalConfigDefinition() var config = new ConfigCommand { CommandType = Type, Category="Testing", - Name=nameof(RefreshTest), + Name=nameof(RefreshMSTestCommand), Guidance="Automation command that generates integration tests from a provided interface." } .UpdateExecutionProject @@ -96,7 +93,6 @@ public override ConfigCommand LoadExternalConfigDefinition() Name = TestSuffix, Guidance = "Optional, Suffix to append to the name of the integration test when being created.", Value = "Test" - } ); @@ -132,7 +128,7 @@ public override async Task EnableCommandAsync(VsCSharpSource result) isEnabled = testProject != null; - if (isEnabled) isEnabled = await testProject.TestProjectIsConfiguredAsync(); + if (isEnabled) isEnabled = await testProject.TestProjectIsConfiguredMSTestAsync(); } } } @@ -158,9 +154,7 @@ public override async Task ExecuteCommandAsync(VsCSharpSource result) ?? throw new CodeFactoryException("Could not load the commands configuration."); var testProject = (await VisualStudioActions.GetProjectFromConfigAsync(config.Project(TestProject))) - ?? throw new CodeFactoryException("Could not locate the test project cannot refresh the test."); - - + ?? throw new CodeFactoryException("Could not locate the test project cannot refresh the test."); var targetInterface = result.SourceCode?.Interfaces?.FirstOrDefault() ?? throw new CodeFactoryException("Could not locate the interface to have tests created from."); @@ -172,10 +166,7 @@ public override async Task ExecuteCommandAsync(VsCSharpSource result) var testName = NameManagement.Init(noRemove,noRemove,testPrefix,testSuffix).FormatName(targetInterface.Name.GenerateCSharpFormattedClassName()); - var test = VisualStudioActions.RefreshMSTestIntegrationTestAsync(testName,targetInterface, testProject); - - } catch (CodeFactoryException codeFactoryError) { @@ -185,12 +176,8 @@ public override async Task ExecuteCommandAsync(VsCSharpSource result) { _logger.Error($"The following unhandled error occurred while executing the solution explorer C# document command {commandTitle}. ", unhandledError); - } - } - #endregion } - } diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshRestService.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshRestServiceCommand.cs similarity index 95% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshRestService.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshRestServiceCommand.cs index 4bd6c02..d3dd285 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshRestService.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshRestServiceCommand.cs @@ -1,25 +1,23 @@ using CodeFactory.Automation.NDF.Logic.AspNetCore.Service.Rest.Json; using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.Extensions; using CodeFactory.WinVs; using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.SolutionExplorer; using CodeFactory.WinVs.Logging; using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile { - /// - /// Code factory command for automation of a C# document when selected from a project in solution explorer. - /// - public class RefreshRestService : CSharpSourceCommandBase + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class RefreshRestServiceCommand : CSharpSourceCommandBase { private static readonly string commandTitle = "Refresh Rest Service"; private static readonly string commandDescription = "Refreshes the implementation of a rest service from a contract definition."; @@ -27,7 +25,7 @@ public class RefreshRestService : CSharpSourceCommandBase #pragma warning disable CS1998 /// - public RefreshRestService(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public RefreshRestServiceCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -37,7 +35,7 @@ public RefreshRestService(ILogger logger, IVsActions vsActions) : base(logger, v /// /// The name of the command the configuration is tied to. /// - public static string Type = typeof(RefreshRestService).FullName; + public static string Type = typeof(RefreshRestServiceCommand).FullName; /// /// The execution project that contains the definition of the logic contract to implement as a service. @@ -101,7 +99,7 @@ public RefreshRestService(ILogger logger, IVsActions vsActions) : base(logger, v public override ConfigCommand LoadExternalConfigDefinition() { var command = new ConfigCommand - { Category = "JsonRestService", Name = nameof(RefreshRestService), CommandType = Type } + { Category = "JsonRestService", Name = nameof(RefreshRestServiceCommand), CommandType = Type } .UpdateExecutionProject ( new ConfigProject diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshXUnitTestCommand .cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshXUnitTestCommand .cs new file mode 100644 index 0000000..97cd38a --- /dev/null +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshXUnitTestCommand .cs @@ -0,0 +1,179 @@ +using CodeFactory.Automation.NDF.Logic.Testing.XUnit; +using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Commands; +using CodeFactory.WinVs.Commands.SolutionExplorer; +using CodeFactory.WinVs.Logging; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.ProjectSystem; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile +{ + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class RefreshXUnitTestCommand : CSharpSourceCommandBase + { + private static readonly string commandTitle = "Refresh xUnit Test"; + private static readonly string commandDescription = "Refreshes an integration test from the target interface."; +#pragma warning disable CS1998 + + /// + public RefreshXUnitTestCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + { + //Intentionally blank + } + + #region External Configuration + + /// + /// The fully qualified name of the command to be used with configuration. + /// + public static string Type = typeof(RefreshXUnitTestCommand).FullName; + + /// + /// Project executing the command + /// + public static string ExecutionProject = "ExecutionProject"; + + /// + /// Project that hosts the intergration tests + /// + public static string TestProject = "TestProject"; + + /// + /// Prefix to append to the name of the integration test being created. + /// + public static string TestPrefix = "TestPrefix"; + + /// + /// Suffix to append to the name of the integration test being created. + /// + public static string TestSuffix = "TestSuffix"; + + /// + /// Loads the external configuration definition for this command. + /// + /// Will return the command configuration or null if this command does not support external configurations. + public override ConfigCommand LoadExternalConfigDefinition() + { + var config = new ConfigCommand + { + CommandType = Type, + Category = "Testing", + Name = nameof(RefreshXUnitTestCommand), + Guidance = "Automation command that generates integration tests from a provided interface." + } + .UpdateExecutionProject + ( + new ConfigProject { Name = ExecutionProject, Guidance = "Enter the name of the project the command will trigger from." } + ) + + .AddProject + ( + new ConfigProject { Name = TestProject, Guidance = "Enter the name of the project that hosts the xUnit integration testings." } + ) + .AddParameter + ( + new ConfigParameter + { + Name = TestPrefix, + Guidance = "Optional, prefix to append to the name of the integration test when being created." + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = TestSuffix, + Guidance = "Optional, Suffix to append to the name of the integration test when being created.", + Value = "Test" + + } + ); + + return config; + } + #endregion + + #region Overrides of VsCommandBase + + /// + /// Validation logic that will determine if this command should be enabled for execution. + /// + /// The target model data that will be used to determine if this command should be enabled. + /// Boolean flag that will tell code factory to enable this command or disable it. + public override async Task EnableCommandAsync(VsCSharpSource result) + { + //Result that determines if the command is enabled and visible in the context menu for execution. + bool isEnabled = false; + + try + { + isEnabled = result.SourceCode?.Interfaces?.FirstOrDefault() != null; + + if (isEnabled) + { + var config = await ConfigManager.LoadCommandByProjectAsync(Type, result); + + isEnabled = config != null; + + if (isEnabled) + { + var testProject = await VisualStudioActions.GetProjectFromConfigAsync(config.Project(TestProject)); + + isEnabled = testProject != null; + + if (isEnabled) isEnabled = await testProject.TestProjectIsConfiguredXUnitAsync(); + } + } + } + catch (Exception unhandledError) + { + _logger.Error($"The following unhandled error occurred while checking if the solution explorer C# document command {commandTitle} is enabled. ", + unhandledError); + isEnabled = false; + } + + return isEnabled; + } + + /// + /// Code factory framework calls this method when the command has been executed. + /// + /// The code factory model that has generated and provided to the command to process. + public override async Task ExecuteCommandAsync(VsCSharpSource result) + { + try + { + var config = (await ConfigManager.LoadCommandByProjectAsync(Type, result)) + ?? throw new CodeFactoryException("Could not load the commands configuration."); + + var testProject = (await VisualStudioActions.GetProjectFromConfigAsync(config.Project(TestProject))) + ?? throw new CodeFactoryException("Could not locate the test project cannot refresh the test."); + + var targetInterface = result.SourceCode?.Interfaces?.FirstOrDefault() + ?? throw new CodeFactoryException("Could not locate the interface to have tests created from."); + + var testPrefix = config.Project(TestProject).ParameterValue(TestPrefix); + var testSuffix = config.Project(TestProject).ParameterValue(TestSuffix); + + string noRemove = null; + + var testName = NameManagement.Init(noRemove, noRemove, testPrefix, testSuffix).FormatName(targetInterface.Name.GenerateCSharpFormattedClassName()); + + var test = VisualStudioActions.RefreshIntegrationTestAsync(testName, targetInterface, testProject); + } + catch (Exception unhandledError) + { + _logger.Error($"The following unhandled error occurred while executing the solution explorer C# document command {commandTitle}. ", + unhandledError); + } + } + #endregion + } +} \ No newline at end of file diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/UpdateLogicImplementation.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/UpdateLogicImplementationCommand.cs similarity index 96% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/UpdateLogicImplementation.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/UpdateLogicImplementationCommand.cs index e52b270..d7744e8 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/UpdateLogicImplementation.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/UpdateLogicImplementationCommand.cs @@ -1,25 +1,23 @@ using CodeFactory.Automation.NDF.Logic.General; using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.Extensions; using CodeFactory.WinVs; using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.SolutionExplorer; using CodeFactory.WinVs.Logging; using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile { - /// - /// Code factory command for automation of a C# document when selected from a project in solution explorer. - /// - public class UpdateLogicImplementation : CSharpSourceCommandBase + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class UpdateLogicImplementationCommand : CSharpSourceCommandBase { private static readonly string commandTitle = "Update Logic Implementation"; private static readonly string commandDescription = "Clones changes from the respository contract to the logic contract and refreshes the logic implementation."; @@ -27,7 +25,7 @@ public class UpdateLogicImplementation : CSharpSourceCommandBase #pragma warning disable CS1998 /// - public UpdateLogicImplementation(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public UpdateLogicImplementationCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -37,7 +35,7 @@ public UpdateLogicImplementation(ILogger logger, IVsActions vsActions) : base(lo /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(UpdateLogicImplementation).FullName; + public static string Type = typeof(UpdateLogicImplementationCommand).FullName; /// /// Exection project for the command. diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CodeFactory.Architecture.AspNetCore.Service.Rest.csproj b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CodeFactory.Architecture.AspNetCore.Service.Rest.csproj index 1e4830a..01df8ab 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CodeFactory.Architecture.AspNetCore.Service.Rest.csproj +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CodeFactory.Architecture.AspNetCore.Service.Rest.csproj @@ -48,19 +48,21 @@ - - - - - - - - - - + + + + + + + + + + + - - + + + diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/IDE/LoadExternalConfiguration.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/IDE/LoadExternalConfigurationCommand.cs similarity index 61% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/IDE/LoadExternalConfiguration.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/IDE/LoadExternalConfigurationCommand.cs index dc58566..6e34816 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/IDE/LoadExternalConfiguration.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/IDE/LoadExternalConfigurationCommand.cs @@ -1,30 +1,27 @@ using CodeFactory.Architecture.AspNetCore.Service.Rest.CSharpFile; +using CodeFactory.Automation.NDF.Logic.Testing.MSTest; +using CodeFactory.Automation.NDF.Logic.Testing.XUnit; using CodeFactory.WinVs; using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.IDE; using CodeFactory.WinVs.Logging; -using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.IDE { - /// - /// Code factory command that is executed when the solution is loaded. This command only gets called one time on load of the solution. - /// - public class LoadExternalConfiguration : SolutionLoadCommandBase + /// + /// Code factory command that is executed when the solution is loaded. This command only gets called one time on load of the solution. + /// + public class LoadExternalConfigurationCommand : SolutionLoadCommandBase { private static readonly string commandTitle = "Load External Configuration"; private static readonly string commandDescription = "Loads the external configuration for automation."; #pragma warning disable CS1998 /// - public LoadExternalConfiguration(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public LoadExternalConfigurationCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -38,28 +35,42 @@ public override async Task ExecuteCommandAsync(VsSolution result) try { - var refreshEFRepository = new RefreshEFRepository(null, null); + var refreshEFRepository = new RefreshEFRepositoryCommand(null, null); refreshEFRepository.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); - var refreshRestService = new RefreshRestService(null, null); + var refreshRestService = new RefreshRestServiceCommand(null, null); refreshRestService.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); - var refreshTest = new RefreshTest(null, null); - refreshTest.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); + // Load configuration based on what type of test project you have loaded + var projects = await result.GetProjectsAsync(false); + foreach (var project in projects) + { + if (await project.TestProjectIsConfiguredMSTestAsync()) + { + var RefreshMSTestCommand = new RefreshMSTestCommand(null, null); + RefreshMSTestCommand.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); + } - var refreshFluentValidation = new RefreshFluentValidation(null, null); + if (await project.TestProjectIsConfiguredXUnitAsync()) + { + var RefreshXUnitTestCommand = new RefreshXUnitTestCommand(null, null); + RefreshXUnitTestCommand.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); + } + } + + var refreshFluentValidation = new RefreshFluentValidationCommand(null, null); refreshFluentValidation.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); - var addMissingRepositoryMembers = new AddMissingRepositoryMembers(null, null); + var addMissingRepositoryMembers = new AddMissingRepositoryMembersCommand(null, null); addMissingRepositoryMembers.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); - var updateLogicImplementation = new UpdateLogicImplementation(null, null); + var updateLogicImplementation = new UpdateLogicImplementationCommand(null, null); updateLogicImplementation.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); - var addMissingLogicMembers = new AddMissingLogicMembers(null, null); + var addMissingLogicMembers = new AddMissingLogicMembersCommand(null, null); addMissingLogicMembers.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); - var refreshLogic = new RefreshLogic(null, null); + var refreshLogic = new RefreshLogicCommand(null, null); refreshLogic.LoadExternalConfigDefinition().RegisterCommandWithDefaultConfiguration(); ConfigManager.LoadConfiguration(result, "Automation", VisualStudioActions); diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Project/RegisterTransientServices.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Project/RegisterTransientServicesCommand.cs similarity index 87% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Project/RegisterTransientServices.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Project/RegisterTransientServicesCommand.cs index e1d8dbf..6650ece 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Project/RegisterTransientServices.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Project/RegisterTransientServicesCommand.cs @@ -3,22 +3,17 @@ using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.SolutionExplorer; using CodeFactory.WinVs.Logging; -using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.Project { -/// - /// Code factory command for automation of a project when selected from solution explorer. - /// - public class RegisterTransientServices : ProjectCommandBase + /// + /// Code factory command for automation of a project when selected from solution explorer. + /// + public class RegisterTransientServicesCommand : ProjectCommandBase { private static readonly string commandTitle = "Register Transient Services"; private static readonly string commandDescription = "Registers Transient classes with a NDF dependency injection loader for a target project."; @@ -26,7 +21,7 @@ public class RegisterTransientServices : ProjectCommandBase #pragma warning disable CS1998 /// - public RegisterTransientServices(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public RegisterTransientServicesCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -37,7 +32,7 @@ public RegisterTransientServices(ILogger logger, IVsActions vsActions) : base(lo /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(RegisterTransientServices).FullName; + public static string Type = typeof(RegisterTransientServicesCommand).FullName; /// /// Loads the external configuration definition for this command. diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CountLinesOfCodeCommand.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CountLinesOfCodeCommand.cs new file mode 100644 index 0000000..d3f82e8 --- /dev/null +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CountLinesOfCodeCommand.cs @@ -0,0 +1,111 @@ +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Commands; +using CodeFactory.WinVs.Commands.SolutionExplorer; +using CodeFactory.WinVs.Logging; +using CodeFactory.WinVs.Models.ProjectSystem; +using Drives.Automation.Standards.Logic.Extensions; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace Drives.Architecture.Commands.Solution +{ + /// + /// Code factory command for automation of a C# document when selected from a project in solution explorer. + /// + public class CountLinesOfCodeCommand : SolutionCommandBase + { + private static readonly string commandTitle = "Count Lines of Code"; + private static readonly string commandDescription = "Counds the lines of code within in this Project."; + public IVsActions _vsActions { get; set; } +#pragma warning disable CS1998 + + /// + public CountLinesOfCodeCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + { + //Intentionally blank + _vsActions = vsActions; + } + + #region Overrides of VsCommandBase + + #region External Configuration + + /// + /// The fully qualified name of the command to be used with configuration. + /// + public static string Type = typeof(CountLinesOfCodeCommand).FullName; + + /// + /// Loads the external configuration definition for this command. + /// + /// Will return the command configuration or null if this command does not support external configurations. + public override ConfigCommand LoadExternalConfigDefinition() + { + var config = new ConfigCommand + { + CommandType = Type, + Category = "Code Analysis", + Name = nameof(CountLinesOfCodeCommand), + Guidance = "Automation command that calculates a total number of lines included in this Sollution." + }; + + return config; + } + #endregion + + /// + /// Validation logic that will determine if this command should be enabled for execution. + /// + /// The target model data that will be used to determine if this command should be enabled. + /// Boolean flag that will tell code factory to enable this command or disable it. + public override async Task EnableCommandAsync(VsSolution result) + { + //Result that determines if the command is enabled and visible in the context menu for execution. + bool isEnabled = false; + + try + { + var projects = await result.GetProjectsAsync(true); + foreach (var project in projects) + { + var children = await project.GetChildrenAsync(true); + if (children.Any(p => p.ModelType.Equals(VisualStudioModelType.Document) && (p.IsMarkupCode() || p.IsSourceCode()))) + { + isEnabled = true; + break; + } + } + } + catch (Exception unhandledError) + { + _logger.Error($"The following unhandled error occurred while checking if the solution explorer C# document command {commandTitle} is enabled. ", + unhandledError); + isEnabled = false; + } + + return isEnabled; + } + + /// + /// Code factory framework calls this method when the command has been executed. + /// + /// The code factory model that has generated and provided to the command to process. + public override async Task ExecuteCommandAsync(VsSolution result) + { + try + { + // Prompt Dialog for user input. + MessageBox.Show($"There are {await result.CountLinesOfCodeAsync()} lines of code in the {result.Name} solution."); + } + catch (Exception unhandledError) + { + _logger.Error($"The following unhandled error occurred while executing the solution explorer C# document command {commandTitle}. ", + unhandledError); + } + } + #endregion + } +} \ No newline at end of file diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CreateAutomationConfiguration.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CreateAutomationConfigurationCommand.cs similarity index 86% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CreateAutomationConfiguration.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CreateAutomationConfigurationCommand.cs index bcb078d..608fdbf 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CreateAutomationConfiguration.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CreateAutomationConfigurationCommand.cs @@ -2,29 +2,24 @@ using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.SolutionExplorer; using CodeFactory.WinVs.Logging; -using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.Solution { - /// - /// Code factory command for automation of the solution when selected from solution explorer. - /// - public class CreateAutomationConfiguration : SolutionCommandBase + /// + /// Code factory command for automation of the solution when selected from solution explorer. + /// + public class CreateAutomationConfigurationCommand : SolutionCommandBase { private static readonly string commandTitle = "Create Automation Configuration"; private static readonly string commandDescription = "Creates an empty automation configuration."; #pragma warning disable CS1998 /// - public CreateAutomationConfiguration(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public CreateAutomationConfigurationCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -34,7 +29,7 @@ public CreateAutomationConfiguration(ILogger logger, IVsActions vsActions) : bas /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(CreateAutomationConfiguration).FullName; + public static string Type = typeof(CreateAutomationConfigurationCommand).FullName; /// /// Loads the external configuration definition for this command. diff --git a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/ReloadAutomationConfiguration.cs b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/ReloadAutomationConfigurationCommand.cs similarity index 86% rename from src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/ReloadAutomationConfiguration.cs rename to src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/ReloadAutomationConfigurationCommand.cs index c2d9d6d..0db356f 100644 --- a/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/ReloadAutomationConfiguration.cs +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/ReloadAutomationConfigurationCommand.cs @@ -2,29 +2,24 @@ using CodeFactory.WinVs.Commands; using CodeFactory.WinVs.Commands.SolutionExplorer; using CodeFactory.WinVs.Logging; -using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows; namespace CodeFactory.Architecture.AspNetCore.Service.Rest.Solution { -/// - /// Code factory command for automation of the solution when selected from solution explorer. - /// - public class ReloadAutomationConfiguration : SolutionCommandBase + /// + /// Code factory command for automation of the solution when selected from solution explorer. + /// + public class ReloadAutomationConfigurationCommand : SolutionCommandBase { private static readonly string commandTitle = "Reload Automation Configuration"; private static readonly string commandDescription = "Reloads the automation configuration."; #pragma warning disable CS1998 /// - public ReloadAutomationConfiguration(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) + public ReloadAutomationConfigurationCommand(ILogger logger, IVsActions vsActions) : base(logger, vsActions, commandTitle, commandDescription) { //Intentionally blank } @@ -34,7 +29,7 @@ public ReloadAutomationConfiguration(ILogger logger, IVsActions vsActions) : bas /// /// The fully qualified name of the command to be used with configuration. /// - public static string Type = typeof(ReloadAutomationConfiguration).FullName; + public static string Type = typeof(ReloadAutomationConfigurationCommand).FullName; /// /// Loads the external configuration definition for this command. diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/MethodExtensions.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/MethodExtensions.cs index cca3a0d..9076ba8 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/MethodExtensions.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/MethodExtensions.cs @@ -1,4 +1,6 @@ -using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; using System; using System.Collections.Generic; using System.Linq; @@ -74,5 +76,324 @@ public static bool IsPostCall(this CsMethod source) return source.HasParameters; } - } + + + // + // Summary: + // Generates a string representation of the await statement. + // + // Parameters: + // source: + // CsMethod to generate parameters for. + // + // Returns: + // True if has a return type. + // + // Exceptions: + // T:System.ArgumentNullException: + // Instance of the method was not provided. + public static SourceFormatter GenerateNewTestObjectsFromParameters(this CsMethod source, SourceFormatter sourceFormatter) + { + StringBuilder parameterBuilder = new StringBuilder(); + + if (source == null) + { + throw new ArgumentNullException("source"); + } + + // Initialize parameter variables + if (source.HasParameters) + { + bool firstParameter = true; + + foreach (var testParameter in source.Parameters) + { + if (firstParameter) + { + parameterBuilder.Append($"{testParameter.Name}"); + firstParameter = false; + } + else parameterBuilder.Append($", {testParameter.Name}"); + + string defaultValue = testParameter.ParameterType.GenerateCSharpDefaultTestValue(testParameter.Name); + sourceFormatter.AppendCodeLine(3, + defaultValue != null + ? $"{testParameter.ParameterType.GenerateCSharpTypeName()} {testParameter.Name} = {defaultValue};" + : $"{testParameter.ParameterType.GenerateCSharpTypeName()} {testParameter.Name};"); + + // Create default properties. + if (!testParameter.ParameterType.IsWellKnownType) + { + foreach (var property in testParameter.ParameterType.GetClassModel().Properties) + { + // Ignore the first property, as this is the primary key. + if (property.Name != testParameter.ParameterType.GetClassModel().Properties[0].Name) + sourceFormatter.AppendCodeLine(3, $"{testParameter.Name}.{property.Name} = {property.PropertyType.GenerateCSharpDefaultTestValue(property.Name)};"); + } + } + + sourceFormatter.AppendCodeLine(0); + } + } + + return sourceFormatter; + } + + /// + /// Generates the full method signature for a REST Controller. + /// + /// Method to generate type name from. + /// Fully formatted list of attribute parameters. + public static string GenerateRESTControllerMethodSignature(this CsMethod source, SourceClassManager serviceManager, bool isOverload) + { + var returnSyntax = (string)null; + if (source == null || !source.IsLoaded) return returnSyntax; + + string serviceCallName = source.GetRestName(isOverload); + string serviceMethodName = $"{serviceCallName}Async"; + CsType returnType = source.ReturnType.TaskReturnType(); + + bool returnsData = returnType != null; + + var returnTypeSyntax = returnType == null ? "NoDataResult" : $"ServiceResult<{returnType.GenerateCSharpTypeName(serviceManager.NamespaceManager, serviceManager.MappedNamespaces)}>"; + + returnSyntax = $"public async Task> {serviceMethodName}({source.BuildServiceMethodParameters(serviceManager)})"; + + return returnSyntax; + } + + /// + /// Generates + /// + /// Method to generate type name from. + /// Fully formatted list of attribute parameters. + public static SourceFormatter GenerateHttpClientCaller(this CsMethod source, SourceFormatter contentFormatter, SourceClassManager classManager, string serviceName, string returnTypeSyntax) + { + var returnSyntax = (string)null; + if (source == null || !source.IsLoaded) return contentFormatter; + + CsType returnType = source.ReturnType.TaskReturnType(); + var serviceUrlParameter = $"_serviceUrl"; + var collectionPath = serviceName; + if (!string.IsNullOrEmpty(collectionPath)) + collectionPath = $"{collectionPath.ToLower()}/{collectionPath.ToLower().Pluralize()}/"; + + var url = $"$\"{{{serviceUrlParameter}.Url}}/api/v1/{collectionPath}\""; + + if (source.Name != "GetAsync") + { + // build api caller with query parameters. + if (source.HasParameters && source.Parameters.All(p => p.ParameterType.IsWellKnownType)) + { + var queryParameters = new StringBuilder(); + bool isFirst = true; + + foreach (var parameter in source.Parameters) + { + if (isFirst) + { + queryParameters.Append($"?{parameter.Name.ToSnakeCase()}={{{parameter.Name}}}"); + isFirst = false; + } + else + { + queryParameters.Append($"&{parameter.Name.ToSnakeCase()}={{{parameter.Name}}}"); + } + } + + url = $"$\"{{{serviceUrlParameter}.Url}}/api/v1/{collectionPath}{queryParameters}\""; + contentFormatter.AppendCodeLine(5, $"var request = new HttpRequestMessage({source.GenerateHttpRequestMethodType()}, {url});"); + contentFormatter.AppendCodeLine(5, $"request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(\"application/json\"));"); + } + // Build api caller with serialized body content. + else + { + var requestName = source.GetRestServiceRequestModelName(serviceName); + + contentFormatter.AppendCodeLine(5, $"var request = new HttpRequestMessage({source.GenerateHttpRequestMethodType()}, {url});"); + contentFormatter.AppendCodeLine(5, $"request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(\"application/json\"));"); + + if (source.HasParameters) + { + contentFormatter.AppendCodeLine(5, $"request.Content = new StringContent(JsonSerializer.Serialize({source.Parameters[0].Name}), Encoding.UTF8);"); + contentFormatter.AppendCodeLine(5, $"request.Content.Headers.ContentType = new MediaTypeHeaderValue(\"application/json\");"); + } + } + } + else + { + url = $"$\"{{{serviceUrlParameter}.Url}}/api/v1/{collectionPath}{{{source.Parameters[0].Name}}}\""; + contentFormatter.AppendCodeLine(5, $"var request = new HttpRequestMessage({source.GenerateHttpRequestMethodType()}, {url});"); + contentFormatter.AppendCodeLine(5, $"request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(\"application/json\"));"); + } + + contentFormatter.AppendCodeLine(5, $"var serviceData = await httpClient.SendAsync(request);"); + contentFormatter.AppendCodeLine(5); + + contentFormatter.AppendCodeLine(5, "await RaiseUnhandledExceptionsAsync(serviceData);"); + contentFormatter.AppendCodeLine(5); + contentFormatter.AppendCodeLine(5, $"var serviceResult = await serviceData.Content.ReadFromJsonAsync<{returnTypeSyntax}>();"); + contentFormatter.AppendCodeLine(5); + return contentFormatter; + } + + /// + /// Generates the full method signature for a BFF Controller. + /// + /// Method to generate type name from. + /// Fully formatted list of attribute parameters. + public static string GenerateBFFControllerMethodSignature(this CsMethod source, SourceClassManager serviceManager, bool isOverload) + { + var returnSyntax = (string)null; + if (source == null || !source.IsLoaded) return returnSyntax; + + string serviceCallName = source.GetRestName(isOverload); + string serviceMethodName = $"{serviceCallName}Async"; + CsType returnType = source.ReturnType.TaskReturnType(); + + if (returnType != null) + returnSyntax = $"public async Task<{returnType.GenerateCSharpTypeName(serviceManager.NamespaceManager, serviceManager.MappedNamespaces)}> {serviceMethodName}({source.BuildServiceMethodParameters(serviceManager)})"; + else + returnSyntax = $"public async Task {serviceMethodName}({source.BuildServiceMethodParameters(serviceManager)})"; + return returnSyntax; + } + + /// + /// Builds a service attribute based on method properties. + /// + /// Method to generate type name from. + /// Fully formatted list of attributes. + public static string GenerateHttpAttribute(this CsMethod source, SourceClassManager serviceManager) + { + var returnSyntax = (string)null; + if (source == null || !source.IsLoaded) return returnSyntax; + + if (source.HasParameters && source.Parameters.Count == 1 && source.Parameters[0].ParameterType.IsWellKnownType) + { + if (source.Parameters[0].ParameterType.WellKnownType == CsKnownLanguageType.String) + returnSyntax = "[" + source.GenerateHttpAttributeType() + "(\"{" + source.Parameters[0].Name.ToSnakeCase() + "}\")]"; + else + returnSyntax = "[" + source.GenerateHttpAttributeType() + "(\"{" + source.Parameters[0].Name.ToSnakeCase() + ":" + source.Parameters[0].ParameterType.GenerateCSharpTypeName(serviceManager.NamespaceManager, serviceManager.MappedNamespaces) + "}\")]"; + } + else + { + // If there are more than 1 parameter, exclude them from here since they will need to be added as method parameters. + returnSyntax = $"[{source.GenerateHttpAttributeType()}]"; + } + + return returnSyntax; + } + + /// + /// Builds a string list of attribute parameters. + /// + /// Method to generate type name from. + /// Fully formatted list of attribute parameters. + private static string BuildServiceMethodParameters(this CsMethod source, SourceClassManager serviceManager) + { + var returnSyntax = (string)null; + if (source == null || !source.IsLoaded) return returnSyntax; + + var parameterAttribute = ""; + if (source.Name.ToLower() == "getasync") + parameterAttribute = ""; + else if (source.Parameters.All(p => p.ParameterType.IsWellKnownType)) + parameterAttribute = "[FromQuery] "; + else + parameterAttribute = "[FromBody] "; + + if (source.HasParameters) + { + int count = 0; + foreach (var parameter in source.Parameters) + { + if (count > 0) + returnSyntax += ", "; + + returnSyntax += ($"{parameterAttribute}{parameter.ParameterType.GenerateCSharpTypeName(serviceManager.NamespaceManager, serviceManager.MappedNamespaces)} {parameter.Name.ToSnakeCase()}"); + count++; + } + } + + return returnSyntax; + } + + /// + /// Returns an Http Attribute Type. + /// + /// Method to generate type name from. + /// An Http Attribute Type. + private static string GenerateHttpAttributeType(this CsMethod source) + { + var returnSyntax = (string)null; + if (source == null || !source.IsLoaded) return returnSyntax; + + if (source.Name == "GetAsync" || source.Name == "QueryAsync") + returnSyntax = "HttpGet"; + else if (source.Name == "AddAsync") + returnSyntax = "HttpPost"; + else if (source.Name == "UpdateAsync") + returnSyntax = "HttpPut"; + else if (source.Name == "DeleteAsync") + returnSyntax = "HttpDelete"; + else if (source.IsPostCall()) + returnSyntax = "HttpPost"; + else + returnSyntax = "HttpGet"; + + return returnSyntax; + } + + /// + /// Returns an Http Attribute Type. + /// + /// Method to generate type name from. + /// An Http Attribute Type. + private static string GenerateHttpMethodType(this CsMethod source) + { + var returnSyntax = (string)null; + if (source == null || !source.IsLoaded) return returnSyntax; + + if (source.Name == "GetAsync" || source.Name == "QueryAsync") + returnSyntax = "GetFromJsonAsync"; + else if (source.Name == "AddAsync") + returnSyntax = "PostAsJsonAsync"; + else if (source.Name == "UpdateAsync") + returnSyntax = "PutAsJsonAsync"; + else if (source.Name == "DeleteAsync") + returnSyntax = "DeleteAsync"; + else if (source.IsPostCall()) + returnSyntax = "PostAsJsonAsync"; + else + returnSyntax = "GetFromJsonAsync"; + + return returnSyntax; + } + + /// + /// Returns an Http Attribute Type. + /// + /// Method to generate type name from. + /// An Http Attribute Type. + private static string GenerateHttpRequestMethodType(this CsMethod source) + { + var returnSyntax = (string)null; + if (source == null || !source.IsLoaded) return returnSyntax; + + if (source.Name == "GetAsync" || source.Name == "QueryAsync") + returnSyntax = "HttpMethod.Get"; + else if (source.Name == "AddAsync") + returnSyntax = "HttpMethod.Post"; + else if (source.Name == "UpdateAsync") + returnSyntax = "HttpMethod.Put"; + else if (source.Name == "DeleteAsync") + returnSyntax = "HttpMethod.Delete"; + else if (source.IsPostCall()) + returnSyntax = "HttpMethod.Post"; + else + returnSyntax = "HttpMethod.Get"; + + return returnSyntax; + } + } } diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/RestJsonCSharpAbstractionBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/RestJsonCSharpAbstractionBuilder.cs index 33d967e..c0edc9a 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/RestJsonCSharpAbstractionBuilder.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/RestJsonCSharpAbstractionBuilder.cs @@ -1,20 +1,19 @@ using CodeFactory.Automation.Standard.Logic; -using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; -using CodeFactory.WinVs; -using System; -using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeFactory.Automation.NDF.Logic.AspNetCore.Service.Rest.Json { -/// - /// Automation class that generates C# abstraction contracts. - /// - public static class RestJsonCSharpAbstractionBuilder + /// + /// Automation class that generates C# abstraction contracts. + /// + public static class RestJsonCSharpAbstractionBuilder { /// /// Refreshes the interface definition of an service abstraction client. @@ -96,8 +95,8 @@ private static async Task CreateCSharpAbstractionContractAsync(this IV contractFormatter.AppendCodeLine(1, "}"); contractFormatter.AppendCodeLine(0, "}"); - var doc = contractFolder != null ? await contractFolder.AddDocumentAsync($"{contractName}.cs", contractFormatter.ReturnSource()) - : await contractProject.AddDocumentAsync($"{contractName}.cs", contractFormatter.ReturnSource()); + var doc = contractFolder != null ? await contractFolder.AddDocumentAsync($"{contractName}.cs", contractFormatter.ReturnSource().TrimStartEndLines()) + : await contractProject.AddDocumentAsync($"{contractName}.cs", contractFormatter.ReturnSource().TrimStartEndLines()); return doc == null ? throw new CodeFactoryException($"Failed to create the abstraction contract '{contractName}'.") @@ -481,7 +480,7 @@ private static async Task UpdateAbstractionClassAsync(this IVsActions s { contentFormatter.AppendCodeLine(3, logicReturnType.IsClass - ? $"{logicReturnType.GenerateCSharpTypeName(abstractManager.NamespaceManager,abstractManager.MappedNamespaces)} result = null; " + ? $"{logicReturnType.GenerateCSharpTypeName(abstractManager.NamespaceManager,abstractManager.MappedNamespaces)}? result = null; " : $"{logicReturnType.GenerateCSharpTypeName(abstractManager.NamespaceManager, abstractManager.MappedNamespaces)} result;"); contentFormatter.AppendCodeLine(3); } @@ -575,12 +574,12 @@ private static async Task UpdateAbstractionClassAsync(this IVsActions s contentFormatter.AppendCodeLine(3, "_logger.InformationExitLog();"); contentFormatter.AppendCodeLine(3); - if (returnsData) contentFormatter.AppendCodeLine(3, "return result;"); + if (returnsData) contentFormatter.AppendCodeLine(3, "return result!;"); contentFormatter.AppendCodeLine(2, "}"); contentFormatter.AppendCodeLine(2); - await abstractManager.ConstructorsAddAfterAsync(contentFormatter.ReturnSource()); + await abstractManager.ConstructorsAddAfterAsync(contentFormatter.ReturnSource().TrimEndLines()); contentFormatter.ResetFormatter(); } @@ -711,7 +710,7 @@ private static async Task AddRestAbstractionBaseClassAsync(this IVsActions sourc sourceFormatter.AppendCodeLine(1, "}"); sourceFormatter.AppendCodeLine(0, "}"); - await abstractionProject.AddDocumentAsync("RestAbstraction.cs", sourceFormatter.ReturnSource()); + await abstractionProject.AddDocumentAsync("RestAbstraction.cs", sourceFormatter.ReturnSource().TrimStartEndLines()); } private static async Task AddServiceUrlBaseClassAsync(this IVsActions source, VsProject abstractionProject) @@ -810,8 +809,8 @@ private static async Task AddServiceUrlClassAsync(this IVsActions source, VsProj sourceFormatter.AppendCodeLine(1, "}"); sourceFormatter.AppendCodeLine(0, "}"); - if (abstractionFolder != null) await abstractionFolder.AddDocumentAsync($"{className}.cs", sourceFormatter.ReturnSource()); - else await abstractionProject.AddDocumentAsync($"{className}.cs", sourceFormatter.ReturnSource()); + if (abstractionFolder != null) await abstractionFolder.AddDocumentAsync($"{className}.cs", sourceFormatter.ReturnSource().TrimEndLines()); + else await abstractionProject.AddDocumentAsync($"{className}.cs", sourceFormatter.ReturnSource().TrimEndLines()); } } } diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/RestJsonServiceBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/RestJsonServiceBuilder.cs index fae61a8..f803d20 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/RestJsonServiceBuilder.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/AspNetCore/Service/Rest/Json/RestJsonServiceBuilder.cs @@ -1,20 +1,18 @@ -using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; -using CodeFactory.WinVs; -using System; -using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using CodeFactory.Automation.Standard.Logic; namespace CodeFactory.Automation.NDF.Logic.AspNetCore.Service.Rest.Json { - /// - /// Automation logic for creating and updated web api rest based services. - /// - public static class RestJsonServiceBUilder + /// + /// Automation logic for creating and updated web api rest based services. + /// + public static class RestJsonServiceBUilder { /// /// Refreshes a rest json service implementation. @@ -316,7 +314,7 @@ private static async Task UpdateJsonRestServiceAsync(this IVsActions so { logicFormatter.AppendCodeLine(3, logicReturnType.IsValueType ? $"{logicReturnType.GenerateCSharpTypeName(serviceManager.NamespaceManager, serviceManager.MappedNamespaces)} result;" - : $"{logicReturnType.GenerateCSharpTypeName(serviceManager.NamespaceManager, serviceManager.MappedNamespaces)} result = null;"); + : $"{logicReturnType.GenerateCSharpTypeName(serviceManager.NamespaceManager, serviceManager.MappedNamespaces)}? result = null;"); } logicFormatter.AppendCodeLine(3, "try"); diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/CodeFactory.Automation.NDF.Logic.csproj b/src/Automation/CodeFactory.Automation.NDF.Logic/CodeFactory.Automation.NDF.Logic.csproj index baf0570..2f669ac 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/CodeFactory.Automation.NDF.Logic.csproj +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/CodeFactory.Automation.NDF.Logic.csproj @@ -73,9 +73,15 @@ + + + + + + diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/Data/Sql/EF/RepositoryBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/Data/Sql/EF/RepositoryBuilder.cs index dad5432..7f899cb 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/Data/Sql/EF/RepositoryBuilder.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/Data/Sql/EF/RepositoryBuilder.cs @@ -1,464 +1,468 @@ -using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory; +using CodeFactory.Automation.NDF.Logic; +using CodeFactory.Automation.NDF.Logic.Data.Sql; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; -using CodeFactory.WinVs; using Microsoft.Extensions.Logging; -using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; -using CodeFactory.Automation.Standard.Logic; -namespace CodeFactory.Automation.NDF.Logic.Data.Sql.EF +namespace Drives.Automation.NDF.Logic.Data.Sql.EF { -/// - /// Automation logic to create or update a data repository that supports a target entity hosted in Entity Framework. - /// - public static class RepositoryBuilder - { - /// - /// Creates or updates a repository that uses entity framework for management of data. - /// - /// CodeFactory automation for Visual Studio for Windows. - /// The name of the repository to refresh. - /// Entity framework entity. - /// POCO model that supports the repository. - /// Project the contract will be added to. - /// Optional, project folder contracts will be stored in, default is null. - /// Optional, additional namespaces to add to the contract definition, default is null. - /// Optional, the name of the logger field if logging is supported, default value is '_logger' - /// Optional, the target logging level for logging messages, default value is Information. - /// Repository project. - /// EF context class for accessing entity framework. - /// Optional, flag that determines if the NDF libraries are used, default true. - /// Optional, flag that determines if logging is supported, default true. - /// Optional, project folder the repositories are stored in, default is null. - /// Optional, list of additional namespaces to update the repository with. - /// Created or updated repository. - /// Raised if required data to create or update the repository is missing. - public static async Task RefreshEFRepositoryAsync(this IVsActions source, string repositoryName, CsClass efEntity, VsProject repoProject, - VsProject contractProject, CsClass poco, CsClass contextClass, bool useNDF = true,bool supportLogging = true, VsProjectFolder repoFolder = null, VsProjectFolder contractFolder = null, - List additionRepositoryNamespaces = null, - List additionalContractNamespaces = null,string loggerFieldName = "_logger", LogLevel logLevel = LogLevel.Information) - { - - if (source == null) - throw new CodeFactoryException("CodeFactory automation was not provided, cannot refresh the EF repository."); - - if(string.IsNullOrEmpty(repositoryName)) - throw new CodeFactoryException("The repository name was not provided, cannot create the repository."); - - if (efEntity == null) - throw new CodeFactoryException("The entity framework model was not provided, cannot refresh the EF repository."); - - if (repoProject == null) throw new CodeFactoryException("The repository project was not provided, cannot refresh the EF repository."); - - if (contractProject == null) - throw new CodeFactoryException("The contract project for the repository was not provided, cannot refresh the EF repository."); - - if (poco == null) - throw new CodeFactoryException("The POCO model was not provided, cannot refresh the EF repository."); - - if (contextClass == null) - throw new CodeFactoryException("The entity framework data context class was not provided, cannot refresh the EF repository."); - - var contractName = $"I{repositoryName}"; - - CsInterface contractInterface = contractFolder != null ? (await contractFolder.FindCSharpSourceByInterfaceNameAsync(contractName))?.SourceCode?.Interfaces?.FirstOrDefault() - : (await contractProject.FindCSharpSourceByInterfaceNameAsync(contractName))?.SourceCode?.Interfaces?.FirstOrDefault(); - - if (contractInterface == null) - contractInterface = (await source.CreateRepositoryContractAsync(contractName, efEntity, contractProject, poco, - contractFolder, additionalContractNamespaces)) ?? throw new CodeFactoryException("Could not create a repos"); - - var repoName = repositoryName; - - CsSource repoSource = repoFolder != null - ? (await repoFolder.FindCSharpSourceByClassNameAsync(repoName))?.SourceCode - : (await repoProject.FindCSharpSourceByClassNameAsync(repoName))?.SourceCode; - - bool newRepo = false; - - if(repoSource == null) - { - newRepo = true; - repoSource = await source.CreateEFRepositoryAsync(repositoryName, efEntity, repoProject, contractInterface, poco, - contextClass, useNDF, supportLogging, repoFolder,additionRepositoryNamespaces) - ?? throw new CodeFactoryException($"Could not create the repository '{repoName}'."); - } - - - var repoClass = await source.UpdateEFRepositoryAsync(efEntity, repoProject, repoSource, contractInterface, poco, - contextClass, useNDF, supportLogging, repoFolder, additionRepositoryNamespaces); - - if(newRepo) await source.RegisterTransientClassesAsync(repoProject,false); - - return repoClass; - } - - /// - /// Create the repositories interface contract. - /// - /// CodeFactory automation for Visual Studio for Windows. - /// The name of the contract to be created. - /// Entity framework entity. - /// POCO model that supports the repository. - /// Project the contract will be added to. - /// Optional, project folder contracts will be stored in, default is null. - /// Optional, additional namespaces to add to the contract definition, default is null. - /// Contract interface definition - /// Raised if required data is missing to create the interface. - private static async Task CreateRepositoryContractAsync(this IVsActions source, string contractName, CsClass efEntity, VsProject contractProject, - CsClass poco, VsProjectFolder contractFolder = null,List additionalContractNamespaces = null) - { - if (source == null) - throw new CodeFactoryException("CodeFactory automation was not provided, cannot create the repository contract."); - - if (efEntity == null) - throw new CodeFactoryException("The entity framework model was not provided, cannot create the repository contract."); - - if (contractProject == null) - throw new CodeFactoryException("The contract project for the repository was not provided, cannot create the repository contract."); - - if (poco == null) - throw new CodeFactoryException("The POCO model was not provided, cannot create the repository contract."); - - - string defaultNamespace = contractFolder != null - ? await contractFolder.GetCSharpNamespaceAsync() - : contractProject.DefaultNamespace; - - SourceFormatter contractFormatter = new SourceFormatter(); - - contractFormatter.AppendCodeLine(0, "using System;"); - contractFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); - contractFormatter.AppendCodeLine(0, "using System.Text;"); - contractFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); - - if (additionalContractNamespaces != null) - { - foreach (var additionalContractNamespace in additionalContractNamespaces) - { - contractFormatter.AppendCodeLine(0, additionalContractNamespace.HasAlias - ? $"using {additionalContractNamespace.Alias} = {additionalContractNamespace.ReferenceNamespace};" - : $"using {additionalContractNamespace.ReferenceNamespace};"); - } - } - - contractFormatter.AppendCodeLine(0, $"using {poco?.Namespace};"); - contractFormatter.AppendCodeLine(0, $"namespace {defaultNamespace}"); - contractFormatter.AppendCodeLine(0, "{"); - contractFormatter.AppendCodeLine(1, "/// "); - contractFormatter.AppendCodeLine(1, $"/// Contract for implementing a repository that supports the model "); - contractFormatter.AppendCodeLine(1, "/// "); - contractFormatter.AppendCodeLine(1, $"public interface {contractName}"); - contractFormatter.AppendCodeLine(1, "{"); - contractFormatter.AppendCodeLine(1); - contractFormatter.AppendCodeLine(2, "/// "); - contractFormatter.AppendCodeLine(2, $"/// Adds a new instance of the model."); - contractFormatter.AppendCodeLine(2, "/// "); - contractFormatter.AppendCodeLine(2, $"Task<{poco?.Name}> AddAsync({poco?.Name} {poco?.Name.GenerateCSharpCamelCase()});"); - contractFormatter.AppendCodeLine(2); - contractFormatter.AppendCodeLine(2, "/// "); - contractFormatter.AppendCodeLine(2, $"/// Updates a instance of the model."); - contractFormatter.AppendCodeLine(2, "/// "); - contractFormatter.AppendCodeLine(2, $"Task<{poco?.Name}> UpdateAsync({poco?.Name} {poco?.Name.GenerateCSharpCamelCase()});"); - contractFormatter.AppendCodeLine(2); - contractFormatter.AppendCodeLine(2, "/// "); - contractFormatter.AppendCodeLine(2, $"/// Deletes the instance of the model."); - contractFormatter.AppendCodeLine(2, "/// "); - contractFormatter.AppendCodeLine(2, $"Task DeleteAsync({poco?.Name} {poco?.Name.GenerateCSharpCamelCase()});"); - contractFormatter.AppendCodeLine(2); - - contractFormatter.AppendCodeLine(1, "}"); - contractFormatter.AppendCodeLine(0, "}"); - - var doc = contractFolder != null ? await contractFolder.AddDocumentAsync($"{contractName}.cs", contractFormatter.ReturnSource()) - : await contractProject.AddDocumentAsync($"{contractName}.cs", contractFormatter.ReturnSource()); - - return doc == null - ? throw new CodeFactoryException($"Failed to create the repository contract '{contractName}' cannot upgrade the repository.") - : ((await doc.GetCSharpSourceModelAsync())?.Interfaces.FirstOrDefault()); - } - - /// - /// Creates a new repository that support entity framework. - /// - /// CodeFactory automation for Visual Studio for Windows. - /// Entity framework entity. - /// Repository project. - /// Repository contract. - /// POCO model that supports the repository. - /// EF context class for accessing entity framework. - /// Optional, flag that determines if the NDF libraries are used, default true. - /// Optional, flag that determines if logging is supported, default true. - /// Optional, project folder the repositories are stored in, default is null. - /// Optional, list of additional namespaces to update the repository with. - /// Optional, name of the logger field if logging is supported, default is '_logger' - /// Optional, prefix to assign to the name of the repository, default is null. - /// Optional, suffix to assign to the name of the repository, default is null. - /// Source for the created repository. - /// Raised if required data is missing to create the repository. - private static async Task CreateEFRepositoryAsync(this IVsActions source, string repositoryName, CsClass efEntity, VsProject repoProject, - CsInterface repoContract, CsClass poco, CsClass contextClass, bool useNDF = true, bool supportLogging = true, VsProjectFolder repoFolder = null, - List additionRepositoryNamespaces = null, string loggerFieldName = "_logger") - { - if (source == null) - throw new CodeFactoryException("CodeFactory automation was not provided, cannot create the repository."); - - if(string.IsNullOrEmpty(repositoryName)) - throw new CodeFactoryException("The repository name was not provided, cannot create the repository."); - - if (efEntity == null) - throw new CodeFactoryException("The entity framework model was not provided, cannot create the repository."); - - if (repoContract == null) - throw new CodeFactoryException("The repository interface was not provided, cannot create the repository."); - - if (repoProject == null) - throw new CodeFactoryException("The repository project for the repository was not provided, cannot create the repository."); - - if (poco == null) - throw new CodeFactoryException("The POCO model was not provided, cannot create the repository."); - - if (contextClass == null) - throw new CodeFactoryException("No db contact class was provided, cannot create the repository."); - - string defaultNamespace = repoFolder != null - ? await repoFolder.GetCSharpNamespaceAsync() - : repoProject.DefaultNamespace; - - string repoName = repositoryName; - - SourceFormatter repoFormatter = new SourceFormatter(); - - repoFormatter.AppendCodeLine(0, "using System;"); - repoFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); - repoFormatter.AppendCodeLine(0, "using System.Text;"); - repoFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); - repoFormatter.AppendCodeLine(0, "using Microsoft.EntityFrameworkCore;"); - repoFormatter.AppendCodeLine(0, "using Microsoft.Data.SqlClient;"); - - if (supportLogging) - repoFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Logging;"); - if (useNDF) - { - repoFormatter.AppendCodeLine(0, "using CodeFactory.NDF;"); - repoFormatter.AppendCodeLine(0, "using CodeFactory.NDF.SQL;"); - } - - if (additionRepositoryNamespaces != null) - { - foreach (var repoNamespace in additionRepositoryNamespaces) - { - repoFormatter.AppendCodeLine(0, repoNamespace.HasAlias - ? $"using {repoNamespace.Alias} = {repoNamespace.ReferenceNamespace};" - : $"using {repoNamespace.ReferenceNamespace};"); - } - } - - repoFormatter.AppendCodeLine(0, $"using {efEntity.Namespace};"); - repoFormatter.AppendCodeLine(0, $"using {poco.Namespace};"); - repoFormatter.AppendCodeLine(0, $"using {repoContract.Namespace};"); - repoFormatter.AppendCodeLine(0); - repoFormatter.AppendCodeLine(0, $"namespace {defaultNamespace}"); - repoFormatter.AppendCodeLine(0, "{"); - repoFormatter.AppendCodeLine(1, "/// "); - repoFormatter.AppendCodeLine(1, $"/// Repository implementation that supports the model "); - repoFormatter.AppendCodeLine(1, "/// "); - repoFormatter.AppendCodeLine(1, $"public class {repoName}:{repoContract.Name}"); - repoFormatter.AppendCodeLine(1, "{"); - repoFormatter.AppendCodeLine(2, "/// "); - repoFormatter.AppendCodeLine(2, "/// Connection string of the repository"); - repoFormatter.AppendCodeLine(2, "/// "); - repoFormatter.AppendCodeLine(2, "private readonly string _connectionString;"); - repoFormatter.AppendCodeLine(2); - repoFormatter.AppendCodeLine(2, "/// "); - repoFormatter.AppendCodeLine(2, "/// Logger used by the repository."); - repoFormatter.AppendCodeLine(2, "/// "); - repoFormatter.AppendCodeLine(2, $"private readonly ILogger {loggerFieldName};"); - repoFormatter.AppendCodeLine(2); - repoFormatter.AppendCodeLine(2, "/// "); - repoFormatter.AppendCodeLine(2, "/// Creates a new instance of the repository."); - repoFormatter.AppendCodeLine(2, "/// "); - repoFormatter.AppendCodeLine(2, "/// Logger used with the repository."); - repoFormatter.AppendCodeLine(2, "/// The connection information for the repository."); - repoFormatter.AppendCodeLine(2, $"public {repoName}(ILogger<{repoName}> logger, IDBContextConnection<{contextClass.Name}> connection)"); - repoFormatter.AppendCodeLine(2, "{"); - repoFormatter.AppendCodeLine(3, $"{loggerFieldName} = logger;"); - repoFormatter.AppendCodeLine(3, "_connectionString = connection.ConnectionString;"); - repoFormatter.AppendCodeLine(2, "}"); - repoFormatter.AppendCodeLine(2); - repoFormatter.AppendCodeLine(1, "}"); - repoFormatter.AppendCodeLine(0, "}"); - - var doc = repoFolder != null ? await repoFolder.AddDocumentAsync($"{repoName}.cs", repoFormatter.ReturnSource()) - : await repoProject.AddDocumentAsync($"{repoName}.cs", repoFormatter.ReturnSource()); - - return doc == null - ? throw new CodeFactoryException($"Failed to create the repository '{repoName}' cannot upgrade the repository.") - : await doc.GetCSharpSourceModelAsync(); - } - - /// - /// Updates a repository with contract methods that are missing. - /// - /// CodeFactory automation for Visual Studio for Windows. - /// Entity framework entity. - /// Repository project. - /// Repository source - /// Repository contract. - /// POCO model that supports the repository. - /// EF context class for accessing entity framework. - /// Optional, flag that determines if the NDF libraries are used, default true. - /// Optional, flag that determines if logging is supported, default true. - /// Optional, project folder the repositories are stored in, default is null. - /// Optional, list of additional namespaces to update the repository with. - /// Optional, the name of the logger field if logging is supported, default value is '_logger' - /// Optional, the target logging level for logging messages, default value is Information. - /// - /// - private static async Task UpdateEFRepositoryAsync(this IVsActions source, CsClass efEntity, VsProject repoProject, - CsSource repoSource,CsInterface repoContract, CsClass poco, CsClass contextClass, bool useNDF = true, bool supportLogging = true, VsProjectFolder repoFolder = null, - List additionRepositoryNamespaces = null,string loggerFieldName = "_logger", LogLevel logLevel = LogLevel.Information) - { - if (source == null) - throw new CodeFactoryException("CodeFactory automation was not provided, cannot update the repository."); - - if (efEntity == null) - throw new CodeFactoryException("The entity framework model was not provided, cannot update the repository."); - - if (repoContract == null) - throw new CodeFactoryException("The repository interface was not provided, cannot update the repository."); - - if (repoProject == null) - throw new CodeFactoryException("The repository project for the repository was not provided, cannot update the repository."); - - if (poco == null) - throw new CodeFactoryException("The POCO model was not provided, cannot update the repository."); - - if (contextClass == null) - throw new CodeFactoryException("No db contact class was provided, cannot update the repository."); - - var repoClass = repoSource?.Classes?.FirstOrDefault(); - - if (repoClass == null) - throw new CodeFactoryException("Could not load the class model for the existing repository, cannot update the repository."); - - var currentSource = repoSource; - - var repoMethods = repoClass.Methods; - - var contractMethods = repoContract.GetAllInterfaceMethods(); - - var missingMethods = contractMethods.Where(m => - { - var contractHash = m.GetComparisonHashCode(); - return repoMethods.All(c => c.GetComparisonHashCode() != contractHash); - }).ToList(); - - if (!missingMethods.Any()) return repoClass; - - var repoManager = new SourceClassManager(currentSource, repoClass, source); - - repoManager.LoadNamespaceManager(); - - ILoggerBlock logFormatter = null; - - if (supportLogging) - { - if (useNDF) logFormatter = new LoggerBlockNDFLogger(loggerFieldName); - else logFormatter = new LoggerBlockMicrosoft(loggerFieldName); - } - - var boundCheckBlocks = new List(); - var catchBlocks = new List(); - - if (useNDF) - { - boundCheckBlocks.Add(new BoundsCheckBlockStringNDFException(true,logFormatter)); - boundCheckBlocks.Add(new BoundsCheckBlockNullNDFException(true,logFormatter)); - - catchBlocks.Add(new CatchBlockManagedExceptionNDFException(logFormatter)); - catchBlocks.Add(new CatchBlockDBUpdateExceptionNDFException(logFormatter)); - catchBlocks.Add(new CatchBlockSqlExceptionNDFException(logFormatter)); - catchBlocks.Add(new CatchBlockExceptionNDFException(logFormatter)); - } - else - { - boundCheckBlocks.Add(new BoundsCheckBlockString(true,logFormatter)); - boundCheckBlocks.Add(new BoundsCheckBlockNull(true,logFormatter)); - - catchBlocks.Add(new CatchBlockStandard(logFormatter)); - } - - var methodBuilder = new MethodBuilderStandard(logFormatter, boundCheckBlocks,new TryBlockStandard(logFormatter,catchBlocks)); - - SourceFormatter injectFormatter = new SourceFormatter(); - - foreach (var missingMethod in missingMethods) - { - injectFormatter.AppendCodeLine(0, $"using (var context = new {contextClass.Name}(_connectionString))"); - injectFormatter.AppendCodeLine(0, "{"); - - switch (missingMethod.Name) - { - case "AddAsync": - injectFormatter.AppendCodeLine(1, $"var model = {efEntity.Name}.CreateDataModel({poco.Name.GenerateCSharpCamelCase()});"); - injectFormatter.AppendCodeLine(1, "await context.AddAsync(model);"); - injectFormatter.AppendCodeLine(1, "await context.SaveChangesAsync();"); - injectFormatter.AppendCodeLine(1, "result = model.CreatePocoModel();"); - break; - - case "UpdateAsync": - injectFormatter.AppendCodeLine(1, $"var model = {efEntity.Name}.CreateDataModel({poco.Name.GenerateCSharpCamelCase()});"); - injectFormatter.AppendCodeLine(1, "context.Update(model);"); - injectFormatter.AppendCodeLine(1, "await context.SaveChangesAsync();"); - injectFormatter.AppendCodeLine(1, "result = model.CreatePocoModel();"); - break; - - case "DeleteAsync": - injectFormatter.AppendCodeLine(1, $"var model = {efEntity.Name}.CreateDataModel({poco.Name.GenerateCSharpCamelCase()});"); - injectFormatter.AppendCodeLine(1, "context.Remove(model);"); - injectFormatter.AppendCodeLine(1, "await context.SaveChangesAsync();"); - break; + /// + /// Automation logic to create or update a data repository that supports a target entity hosted in Entity Framework. + /// + public static class RepositoryBuilder + { + /// + /// Creates or updates a repository that uses entity framework for management of data. + /// + /// CodeFactory automation for Visual Studio for Windows. + /// The name of the repository to refresh. + /// Entity framework entity. + /// POCO model that supports the repository. + /// Project the contract will be added to. + /// Optional, project folder contracts will be stored in, default is null. + /// Optional, additional namespaces to add to the contract definition, default is null. + /// Optional, the name of the logger field if logging is supported, default value is '_logger' + /// Optional, the target logging level for logging messages, default value is Information. + /// Repository project. + /// EF context class for accessing entity framework. + /// Optional, flag that determines if the NDF libraries are used, default true. + /// Optional, flag that determines if logging is supported, default true. + /// Optional, project folder the repositories are stored in, default is null. + /// Optional, list of additional namespaces to update the repository with. + /// Created or updated repository. + /// Raised if required data to create or update the repository is missing. + public static async Task RefreshEFRepositoryWithCRUDAsync(this IVsActions source, string repositoryName, CsClass efEntity, VsProject repoProject, + VsProject contractProject, CsClass poco, CsClass contextClass, bool useNDF = true, bool supportLogging = true, VsProjectFolder repoFolder = null, VsProjectFolder contractFolder = null, + bool generateCrudOperations = true, List additionRepositoryNamespaces = null, + List additionalContractNamespaces = null, string loggerFieldName = "_logger", LogLevel logLevel = LogLevel.Information) + { + + if (source == null) + throw new CodeFactoryException("CodeFactory automation was not provided, cannot refresh the EF repository."); + + if (string.IsNullOrEmpty(repositoryName)) + throw new CodeFactoryException("The repository name was not provided, cannot create the repository."); + + if (efEntity == null) + throw new CodeFactoryException("The entity framework model was not provided, cannot refresh the EF repository."); + + if (repoProject == null) throw new CodeFactoryException("The repository project was not provided, cannot refresh the EF repository."); + + if (contractProject == null) + throw new CodeFactoryException("The contract project for the repository was not provided, cannot refresh the EF repository."); + + if (poco == null) + throw new CodeFactoryException("The POCO model was not provided, cannot refresh the EF repository."); + + if (contextClass == null) + throw new CodeFactoryException("The entity framework data context class was not provided, cannot refresh the EF repository."); + + var contractName = $"I{repositoryName}"; + + CsInterface contractInterface = contractFolder != null ? (await contractFolder.FindCSharpSourceByInterfaceNameAsync(contractName))?.SourceCode?.Interfaces?.FirstOrDefault() + : (await contractProject.FindCSharpSourceByInterfaceNameAsync(contractName))?.SourceCode?.Interfaces?.FirstOrDefault(); + + if (contractInterface == null) + contractInterface = (await source.CreateRepositoryContractAsync(contractName, efEntity, contractProject, poco, + contractFolder, additionalContractNamespaces, generateCrudOperations)) ?? throw new CodeFactoryException("Could not create a repos"); + + var repoName = repositoryName; + + CsSource repoSource = repoFolder != null + ? (await repoFolder.FindCSharpSourceByClassNameAsync(repoName))?.SourceCode + : (await repoProject.FindCSharpSourceByClassNameAsync(repoName))?.SourceCode; + + bool newRepo = false; + + if (repoSource == null) + { + newRepo = true; + repoSource = await source.CreateEFRepositoryAsync(repositoryName, efEntity, repoProject, contractInterface, poco, + contextClass, useNDF, supportLogging, repoFolder, additionRepositoryNamespaces) + ?? throw new CodeFactoryException($"Could not create the repository '{repoName}'."); + } + + + var repoClass = await source.UpdateEFRepositoryAsync(efEntity, repoProject, repoSource, contractInterface, poco, + contextClass, useNDF, supportLogging, repoFolder, additionRepositoryNamespaces); + + if (newRepo) await source.RegisterTransientClassesAsync(repoProject, false); + + return repoClass; + } + + /// + /// Create the repositories interface contract. + /// + /// CodeFactory automation for Visual Studio for Windows. + /// The name of the contract to be created. + /// Entity framework entity. + /// POCO model that supports the repository. + /// Project the contract will be added to. + /// Optional, project folder contracts will be stored in, default is null. + /// Optional, additional namespaces to add to the contract definition, default is null. + /// Contract interface definition + /// Raised if required data is missing to create the interface. + private static async Task CreateRepositoryContractAsync(this IVsActions source, string contractName, CsClass efEntity, VsProject contractProject, + CsClass poco, VsProjectFolder contractFolder = null, List additionalContractNamespaces = null, bool generateCrudOperations = true) + { + if (source == null) + throw new CodeFactoryException("CodeFactory automation was not provided, cannot create the repository contract."); + + if (efEntity == null) + throw new CodeFactoryException("The entity framework model was not provided, cannot create the repository contract."); + + if (contractProject == null) + throw new CodeFactoryException("The contract project for the repository was not provided, cannot create the repository contract."); + + if (poco == null) + throw new CodeFactoryException("The POCO model was not provided, cannot create the repository contract."); + + + string defaultNamespace = contractFolder != null + ? await contractFolder.GetCSharpNamespaceAsync() + : contractProject.DefaultNamespace; + + SourceFormatter contractFormatter = new SourceFormatter(); + + contractFormatter.AppendCodeLine(0, "using System;"); + contractFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); + contractFormatter.AppendCodeLine(0, "using System.Text;"); + contractFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); + + if (additionalContractNamespaces != null) + { + foreach (var additionalContractNamespace in additionalContractNamespaces) + { + contractFormatter.AppendCodeLine(0, additionalContractNamespace.HasAlias + ? $"using {additionalContractNamespace.Alias} = {additionalContractNamespace.ReferenceNamespace};" + : $"using {additionalContractNamespace.ReferenceNamespace};"); + } + } + + contractFormatter.AppendCodeLine(0, $"using {poco?.Namespace};"); + contractFormatter.AppendCodeLine(0, $"namespace {defaultNamespace}"); + contractFormatter.AppendCodeLine(0, "{"); + contractFormatter.AppendCodeLine(1, "/// "); + contractFormatter.AppendCodeLine(1, $"/// Contract for implementing a repository that supports the model "); + contractFormatter.AppendCodeLine(1, "/// "); + contractFormatter.AppendCodeLine(1, $"public interface {contractName}"); + contractFormatter.AppendCodeLine(1, "{"); + + if (generateCrudOperations) + { + contractFormatter.AppendCodeLine(1); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"/// Adds a new instance of the model."); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"Task<{poco?.Name}> AddAsync({poco?.Name} {poco?.Name.GenerateCSharpCamelCase()});"); + contractFormatter.AppendCodeLine(2); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"/// Updates a instance of the model."); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"Task<{poco?.Name}> UpdateAsync({poco?.Name} {poco?.Name.GenerateCSharpCamelCase()});"); + contractFormatter.AppendCodeLine(2); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"/// Deletes the instance of the model."); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"Task DeleteAsync({poco?.Properties[0].PropertyType.Name}? {poco?.Properties[0].Name.GenerateCSharpCamelCase()});"); + } + + contractFormatter.AppendCodeLine(2); + contractFormatter.AppendCodeLine(1, "}"); + contractFormatter.AppendCodeLine(0, "}"); + + var doc = contractFolder != null ? await contractFolder.AddDocumentAsync($"{contractName}.cs", contractFormatter.ReturnSource().TrimStartEndLines()) + : await contractProject.AddDocumentAsync($"{contractName}.cs", contractFormatter.ReturnSource().TrimStartEndLines()); + + return doc == null + ? throw new CodeFactoryException($"Failed to create the repository contract '{contractName}' cannot upgrade the repository.") + : ((await doc.GetCSharpSourceModelAsync())?.Interfaces.FirstOrDefault()); + } + + /// + /// Creates a new repository that support entity framework. + /// + /// CodeFactory automation for Visual Studio for Windows. + /// Entity framework entity. + /// Repository project. + /// Repository contract. + /// POCO model that supports the repository. + /// EF context class for accessing entity framework. + /// Optional, flag that determines if the NDF libraries are used, default true. + /// Optional, flag that determines if logging is supported, default true. + /// Optional, project folder the repositories are stored in, default is null. + /// Optional, list of additional namespaces to update the repository with. + /// Optional, name of the logger field if logging is supported, default is '_logger' + /// Optional, prefix to assign to the name of the repository, default is null. + /// Optional, suffix to assign to the name of the repository, default is null. + /// Source for the created repository. + /// Raised if required data is missing to create the repository. + private static async Task CreateEFRepositoryAsync(this IVsActions source, string repositoryName, CsClass efEntity, VsProject repoProject, + CsInterface repoContract, CsClass poco, CsClass contextClass, bool useNDF = true, bool supportLogging = true, VsProjectFolder repoFolder = null, + List additionRepositoryNamespaces = null, string loggerFieldName = "_logger") + { + if (source == null) + throw new CodeFactoryException("CodeFactory automation was not provided, cannot create the repository."); + + if (string.IsNullOrEmpty(repositoryName)) + throw new CodeFactoryException("The repository name was not provided, cannot create the repository."); + + if (efEntity == null) + throw new CodeFactoryException("The entity framework model was not provided, cannot create the repository."); + + if (repoContract == null) + throw new CodeFactoryException("The repository interface was not provided, cannot create the repository."); + + if (repoProject == null) + throw new CodeFactoryException("The repository project for the repository was not provided, cannot create the repository."); + + if (poco == null) + throw new CodeFactoryException("The POCO model was not provided, cannot create the repository."); + + if (contextClass == null) + throw new CodeFactoryException("No db contact class was provided, cannot create the repository."); + + string defaultNamespace = repoFolder != null + ? await repoFolder.GetCSharpNamespaceAsync() + : repoProject.DefaultNamespace; + + string repoName = repositoryName; + + SourceFormatter repoFormatter = new SourceFormatter(); + + repoFormatter.AppendCodeLine(0, "using Microsoft.EntityFrameworkCore;"); + repoFormatter.AppendCodeLine(0, "using Microsoft.Data.SqlClient;"); + + if (supportLogging) + repoFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Logging;"); + if (useNDF) + { + repoFormatter.AppendCodeLine(0, "using CodeFactory.NDF;"); + repoFormatter.AppendCodeLine(0, "using CodeFactory.NDF.SQL;"); + } + + if (additionRepositoryNamespaces != null) + { + foreach (var repoNamespace in additionRepositoryNamespaces) + { + repoFormatter.AppendCodeLine(0, repoNamespace.HasAlias + ? $"using {repoNamespace.Alias} = {repoNamespace.ReferenceNamespace};" + : $"using {repoNamespace.ReferenceNamespace};"); + } + } + + repoFormatter.AppendCodeLine(0, $"using {efEntity.Namespace};"); + repoFormatter.AppendCodeLine(0, $"using {poco.Namespace};"); + repoFormatter.AppendCodeLine(0, $"using {repoContract.Namespace};"); + repoFormatter.AppendCodeLine(0); + repoFormatter.AppendCodeLine(0, $"namespace {defaultNamespace}"); + repoFormatter.AppendCodeLine(0, "{"); + repoFormatter.AppendCodeLine(1, "/// "); + repoFormatter.AppendCodeLine(1, $"/// Repository implementation that supports the model "); + repoFormatter.AppendCodeLine(1, "/// "); + repoFormatter.AppendCodeLine(1, $"public class {repoName}:{repoContract.Name}"); + repoFormatter.AppendCodeLine(1, "{"); + repoFormatter.AppendCodeLine(2, "/// "); + repoFormatter.AppendCodeLine(2, "/// Connection string of the repository"); + repoFormatter.AppendCodeLine(2, "/// "); + repoFormatter.AppendCodeLine(2, "private readonly string _connectionString;"); + repoFormatter.AppendCodeLine(2); + repoFormatter.AppendCodeLine(2, "/// "); + repoFormatter.AppendCodeLine(2, "/// Logger used by the repository."); + repoFormatter.AppendCodeLine(2, "/// "); + repoFormatter.AppendCodeLine(2, $"private readonly ILogger {loggerFieldName};"); + repoFormatter.AppendCodeLine(2); + repoFormatter.AppendCodeLine(2, "/// "); + repoFormatter.AppendCodeLine(2, "/// Creates a new instance of the repository."); + repoFormatter.AppendCodeLine(2, "/// "); + repoFormatter.AppendCodeLine(2, "/// Logger used with the repository."); + repoFormatter.AppendCodeLine(2, "/// The connection information for the repository."); + repoFormatter.AppendCodeLine(2, $"public {repoName}(ILogger<{repoName}> logger, IDBContextConnection<{contextClass.Name}> connection)"); + repoFormatter.AppendCodeLine(2, "{"); + repoFormatter.AppendCodeLine(3, $"{loggerFieldName} = logger;"); + repoFormatter.AppendCodeLine(3, "_connectionString = connection.ConnectionString!;"); + repoFormatter.AppendCodeLine(2, "}"); + repoFormatter.AppendCodeLine(2); + repoFormatter.AppendCodeLine(1, "}"); + repoFormatter.AppendCodeLine(0, "}"); + + var doc = repoFolder != null ? await repoFolder.AddDocumentAsync($"{repoName}.cs", repoFormatter.ReturnSource().TrimStartEndLines()) + : await repoProject.AddDocumentAsync($"{repoName}.cs", repoFormatter.ReturnSource().TrimStartEndLines()); + + return doc == null + ? throw new CodeFactoryException($"Failed to create the repository '{repoName}' cannot upgrade the repository.") + : await doc.GetCSharpSourceModelAsync(); + } + + /// + /// Updates a repository with contract methods that are missing. + /// + /// CodeFactory automation for Visual Studio for Windows. + /// Entity framework entity. + /// Repository project. + /// Repository source + /// Repository contract. + /// POCO model that supports the repository. + /// EF context class for accessing entity framework. + /// Optional, flag that determines if the NDF libraries are used, default true. + /// Optional, flag that determines if logging is supported, default true. + /// Optional, project folder the repositories are stored in, default is null. + /// Optional, list of additional namespaces to update the repository with. + /// Optional, the name of the logger field if logging is supported, default value is '_logger' + /// Optional, the target logging level for logging messages, default value is Information. + /// + /// + private static async Task UpdateEFRepositoryAsync(this IVsActions source, CsClass efEntity, VsProject repoProject, + CsSource repoSource, CsInterface repoContract, CsClass poco, CsClass contextClass, bool useNDF = true, bool supportLogging = true, VsProjectFolder repoFolder = null, + List additionRepositoryNamespaces = null, string loggerFieldName = "_logger", LogLevel logLevel = LogLevel.Information) + { + if (source == null) + throw new CodeFactoryException("CodeFactory automation was not provided, cannot update the repository."); + + if (efEntity == null) + throw new CodeFactoryException("The entity framework model was not provided, cannot update the repository."); + + if (repoContract == null) + throw new CodeFactoryException("The repository interface was not provided, cannot update the repository."); + + if (repoProject == null) + throw new CodeFactoryException("The repository project for the repository was not provided, cannot update the repository."); + + if (poco == null) + throw new CodeFactoryException("The POCO model was not provided, cannot update the repository."); + + if (contextClass == null) + throw new CodeFactoryException("No db contact class was provided, cannot update the repository."); + + var repoClass = repoSource?.Classes?.FirstOrDefault(); + + if (repoClass == null) + throw new CodeFactoryException("Could not load the class model for the existing repository, cannot update the repository."); + + var currentSource = repoSource; + + var repoMethods = repoClass.Methods; + + var contractMethods = repoContract.GetAllInterfaceMethods(); + + var missingMethods = contractMethods.Where(m => + { + var contractHash = m.GetComparisonHashCode(); + return repoMethods.All(c => c.GetComparisonHashCode() != contractHash); + }).ToList(); + + if (!missingMethods.Any()) return repoClass; + + var repoManager = new SourceClassManager(currentSource, repoClass, source); + + repoManager.LoadNamespaceManager(); + + ILoggerBlock logFormatter = null; + + if (supportLogging) + { + if (useNDF) logFormatter = new LoggerBlockNDFLogger(loggerFieldName); + else logFormatter = new LoggerBlockMicrosoft(loggerFieldName); + } + + var boundCheckBlocks = new List(); + var catchBlocks = new List(); + + if (useNDF) + { + boundCheckBlocks.Add(new BoundsCheckBlockStringNDFException(true, logFormatter)); + boundCheckBlocks.Add(new BoundsCheckBlockNullNDFException(true, logFormatter)); + + catchBlocks.Add(new CatchBlockManagedExceptionNDFException(logFormatter)); + catchBlocks.Add(new CatchBlockDBUpdateExceptionNDFException(logFormatter)); + catchBlocks.Add(new CatchBlockSqlExceptionNDFException(logFormatter)); + catchBlocks.Add(new CatchBlockExceptionNDFException(logFormatter)); + } + else + { + boundCheckBlocks.Add(new BoundsCheckBlockString(true, logFormatter)); + boundCheckBlocks.Add(new BoundsCheckBlockNull(true, logFormatter)); + + catchBlocks.Add(new CatchBlockStandard(logFormatter)); + } + + var methodBuilder = new MethodBuilderStandard(logFormatter, boundCheckBlocks, new TryBlockStandard(logFormatter, catchBlocks)); + + SourceFormatter injectFormatter = new SourceFormatter(); + + foreach (var missingMethod in missingMethods) + { + injectFormatter.AppendCodeLine(0, $"using (var context = new {contextClass.Name}(_connectionString))"); + injectFormatter.AppendCodeLine(0, "{"); + + switch (missingMethod.Name) + { + case "AddAsync": + injectFormatter.AppendCodeLine(1, $"var model = {efEntity.Name}.CreateDataModel({poco.Name.GenerateCSharpCamelCase()});"); + injectFormatter.AppendCodeLine(1, "context.Entry(model).State = EntityState.Modified;"); + injectFormatter.AppendCodeLine(1, "await context.AddAsync(model);"); + injectFormatter.AppendCodeLine(1, "await context.SaveChangesAsync();"); + injectFormatter.AppendCodeLine(1, "result = model!.CreatePocoModel();"); + break; + + case "UpdateAsync": + injectFormatter.AppendCodeLine(1, $"var model = {efEntity.Name}.CreateDataModel({poco.Name.GenerateCSharpCamelCase()});"); + injectFormatter.AppendCodeLine(1, "context.Entry(model).State = EntityState.Modified;"); + injectFormatter.AppendCodeLine(1, "context.Update(model);"); + injectFormatter.AppendCodeLine(1, "await context.SaveChangesAsync();"); + injectFormatter.AppendCodeLine(1, "result = model!.CreatePocoModel();"); + break; + + case "DeleteAsync": + injectFormatter.AppendCodeLine(1, $"var model = context!.{efEntity.Name.Pluralize()}.FirstOrDefault(m => m.{poco.Properties[0].Name} == {poco.Properties[0].Name.GenerateCSharpCamelCase()})!;"); + injectFormatter.AppendCodeLine(1, "context.Entry(model).State = EntityState.Deleted;"); + injectFormatter.AppendCodeLine(1, "context.Remove(model);"); + injectFormatter.AppendCodeLine(1, "await context.SaveChangesAsync();"); + break; - default: - injectFormatter.AppendCodeLine(0); - break; - } + default: + injectFormatter.AppendCodeLine(0); + break; + } - injectFormatter.AppendCodeLine(0, "}"); + injectFormatter.AppendCodeLine(0, "}"); - string syntax = injectFormatter.ReturnSource(); - await methodBuilder.InjectMethodAsync(missingMethod, repoManager, 2, syntax: syntax,defaultLogLevel:logLevel); + string syntax = injectFormatter.ReturnSource().TrimStartEndLines(); + await methodBuilder.InjectMethodAsync(missingMethod, repoManager, 2, syntax: syntax, defaultLogLevel: logLevel); - injectFormatter.ResetFormatter(); - } + injectFormatter.ResetFormatter(); + } - return repoManager.Container; - } + return repoManager.Container; + } - /// - /// Checks the ef model property and determines if it should be included in a poco model implementation. - /// - /// Property to evaluate. - /// True if the property should be included, false if not. - public static bool UseSourceProperty(CsProperty source) - { - if (source == null) return false; - - bool useSource = (source.HasGet & source.HasSet & source.Security == CsSecurity.Public & !source.IsStatic); - - if (source.HasAttributes & useSource) - { - useSource = !source.Attributes.Any(a => a.Type.Namespace == "System.ComponentModel.DataAnnotations.Schema" & a.Type.Name == "ForeignKeyAttribute"); - if (useSource) useSource = !source.Attributes.Any(a => a.Type.Namespace == "System.ComponentModel.DataAnnotations.Schema" & a.Type.Name == "InversePropertyAttribute"); - } - - return useSource; - - } - } + /// + /// Checks the ef model property and determines if it should be included in a poco model implementation. + /// + /// Property to evaluate. + /// True if the property should be included, false if not. + public static bool UseSourceProperty(CsProperty source) + { + if (source == null) return false; + + bool useSource = (source.HasGet & source.HasSet & source.Security == CsSecurity.Public & !source.IsStatic); + + if (source.HasAttributes & useSource) + { + useSource = !source.Attributes.Any(a => a.Type.Namespace == "System.ComponentModel.DataAnnotations.Schema" & a.Type.Name == "ForeignKeyAttribute"); + if (useSource) useSource = !source.Attributes.Any(a => a.Type.Namespace == "System.ComponentModel.DataAnnotations.Schema" & a.Type.Name == "InversePropertyAttribute"); + } + + return useSource; + + } + } } diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/DependencyInjectionBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/DependencyInjectionBuilder.cs index a99b625..7eb15e9 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/DependencyInjectionBuilder.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/DependencyInjectionBuilder.cs @@ -1,22 +1,22 @@ -using CodeFactory.WinVs.Logging; -using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Logging; using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; -using CodeFactory.WinVs; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; using System.Threading.Tasks; -using CodeFactory.Automation.Standard.Logic; namespace CodeFactory.Automation.NDF.Logic { - /// - /// Automation class that will manage creation of dependency injection transient code in libraries that support NDF. - /// - public static class DependencyInjectionBuilder + /// + /// Automation class that will manage creation of dependency injection transient code in libraries that support NDF. + /// + public static class DependencyInjectionBuilder { //Logger used for code factory logging // ReSharper disable once InconsistentNaming diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/General/FluentValidationBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/General/FluentValidationBuilder.cs index e733c53..43e2206 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/General/FluentValidationBuilder.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/General/FluentValidationBuilder.cs @@ -1,19 +1,17 @@ -using CodeFactory.WinVs.Models.CSharp; -using CodeFactory.WinVs.Models.ProjectSystem; +using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.Extensions; using CodeFactory.WinVs; -using System; -using System.Collections.Generic; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.ProjectSystem; using System.Linq; -using System.Text; using System.Threading.Tasks; -using CodeFactory.Automation.Standard.Logic; namespace CodeFactory.Automation.NDF.Logic.General { - /// - /// Automation logic for the creation and update of fluent validation classes. - /// - public static class FluentValidationBuilder + /// + /// Automation logic for the creation and update of fluent validation classes. + /// + public static class FluentValidationBuilder { /// /// Refreshes the definition of a fluent validation class. diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/General/ModelBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/General/ModelBuilder.cs index f08fa17..3eadf4f 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/General/ModelBuilder.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/General/ModelBuilder.cs @@ -1,352 +1,312 @@ -using CodeFactory.WinVs.Logging; -using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory.Automation.Standard.Logic; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Logging; using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; -using CodeFactory.WinVs; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; -using CodeFactory.Automation.Standard.Logic; namespace CodeFactory.Automation.NDF.Logic.General { - /// - /// Automation library that creates a model class that supports standard property implementation. - /// - public static class ModelBuilder - { - private static readonly ILogger _logger = LogManager.GetLogger(typeof(ModelBuilder)); - - /// - /// Refreshes the implementation of a plain old CLR object based on the definition of a provided class. This will also create the model if it does not exist. - /// - /// CodeFactory automation framework used to trigger the refresh. - /// The source class model the model will be generated or updated from. - /// The target project the model will be created in. - /// The default namespaces to be added as using statements if they do not exist in the model class. - /// Optional parameter that provides the details for how to format the name of the model class. - /// Optional parameter that determines the target folder the model is to be located in if not on the root of the project. - /// Optional parameter that sets the XML summary for the model class. - /// Optional parameter that determines if properties should converted to non nullable types, default value is true. - /// Optional parameter that maps source and target namespaces when creating the model, default value is null. - /// Optional parameter that calls a delegate to confirm the property should be included with the model class. - /// The loaded class model for the created model class. - /// Raised if required data is missing or a processing error has occurred. - public static async Task RefreshModelAsync(this IVsActions source, CsClass sourceClass, - VsProject modelProject, List defaultNamespaces, NameManagement nameManagement = null, VsProjectFolder modelFolder = null, - string modelSummary = null, bool convertNullableTypes = true, List mappedNamespaces = null, UseSourcePropertyDelegate useSourceProperty = null) - { - //Bounds checking - if (source == null) - throw new CodeFactoryException( - "The CodeFactory automation was not provided cannot refresh the model class."); - if (sourceClass == null) - throw new CodeFactoryException("The source class was not provided cannot refresh the model class."); - if (modelProject == null) - throw new CodeFactoryException( - "The target model project was not provided cannot refresh the model class."); - - CsSource modelSource = null; - - string modelName = null; - - modelName = nameManagement != null ? nameManagement.FormatName(sourceClass.Name) : sourceClass.Name; - - if(string.IsNullOrEmpty(modelName)) - throw new CodeFactoryException("Could not format the target model class name, cannot create the model class."); - - bool wasCreated = false; - - if (modelFolder == null) - { - modelSource = (await modelProject.FindCSharpSourceByClassNameAsync(modelName))?.SourceCode; - } - else - { - modelSource = (await modelFolder.FindCSharpSourceByClassNameAsync(modelName))?.SourceCode; - } - - if (modelSource == null) - { - modelSource = await source.CreateModelAsync(sourceClass, modelProject,modelName, defaultNamespaces, modelFolder, - modelSummary, convertNullableTypes) - ?? throw new CodeFactoryException($"Could not create the model '{modelName}' cannot refresh the model."); - } - - - - var modelClass = await source.UpdateModelAsync(sourceClass, modelProject, modelSource, defaultNamespaces,nameManagement, modelFolder, - modelSummary, convertNullableTypes, mappedNamespaces); - - - //if(wasCreated) await source.RegisterTransientClassesAsync(modelProject,false); - - return modelClass; - - } - - /// - /// Creates the shell implementation of a plain old CLR object. - /// - /// CodeFactory automation framework used to trigger the refresh. - /// The source class model the model will be generated or updated from. - /// The target project the model will be created in. - /// The default namespaces to be added as using statements if they do not exist in the model class. - /// Optional parameter that determines the target folder the model is to be located in if not on the root of the project. - /// Optional parameter that sets the XML summary for the model class. - /// Optional parameter that determines if properties should converted to non nullable types, default value is true. - /// The loaded class model for the created model class. - /// Raised if required data is missing or a processing error has occurred. - private static async Task CreateModelAsync(this IVsActions source, CsClass sourceClass, - VsProject modelProject, string targetClassName, List defaultNamespaces, VsProjectFolder modelFolder = null, - string modelSummary = null, bool convertNullableTypes = true) - { - if (source == null) - throw new CodeFactoryException( - "The CodeFactory automation was not provided cannot create the model class."); - if (sourceClass == null) - throw new CodeFactoryException("The source class was not provided cannot create the model class."); - if (modelProject == null) - throw new CodeFactoryException( - "The target model project was not provided cannot create the model class."); - - CsSource result = null; - - try - { - SourceFormatter modelFormatter = new SourceFormatter(); - - if (defaultNamespaces != null) - { - if (defaultNamespaces.Any()) - { - foreach (var usingStatement in defaultNamespaces) - { - modelFormatter.AppendCodeLine(0, - usingStatement.HasAlias - ? $"using {usingStatement.Alias} = {usingStatement.ReferenceNamespace};" - : $"using {usingStatement.ReferenceNamespace};"); - } - } - } - - modelFormatter.AppendCodeLine(0, - modelFolder == null - ? $"namespace {modelProject.DefaultNamespace}" - : $"namespace {await modelFolder.GetCSharpNamespaceAsync()}"); - modelFormatter.AppendCodeLine(0, "{"); - - modelFormatter.AppendCodeLine(1, "/// "); - - if (modelSummary == null) - modelFormatter.AppendCodeLine(1, "/// Plain old CLR object (model) data class implementation."); - else - { - var summaryLines = modelSummary.Split(Environment.NewLine.ToCharArray()); - foreach (var summaryLine in summaryLines) modelFormatter.AppendCodeLine(1, $"/// {summaryLine}"); - } - - modelFormatter.AppendCodeLine(1, "/// "); - modelFormatter.AppendCodeLine(1, $"public class {targetClassName}"); - modelFormatter.AppendCodeLine(1, "{"); - modelFormatter.AppendCodeLine(1); - modelFormatter.AppendCodeLine(1, "}"); - - modelFormatter.AppendCodeLine(0, "}"); - - - VsDocument modelDoc = modelFolder == null - ? await modelProject.AddDocumentAsync($"{targetClassName}.cs", modelFormatter.ReturnSource()) - : await modelFolder.AddDocumentAsync($"{targetClassName}.cs", modelFormatter.ReturnSource()); - - if (modelDoc == null) throw new CodeFactoryException($"Error occurred saving the model to the project."); - - result = await modelDoc.GetCSharpSourceModelAsync(); - } - catch (CodeFactoryException) - { - throw; - } - catch (Exception unhandledError) - { - _logger.Error("The following unhandled error occurred creating a model.", unhandledError); - throw new CodeFactoryException( - $"An unhandled error occurred while creating the model '{targetClassName}'. Check the code factory logs for details of what happened."); - } - - return result; - } - - /// - /// Updates the implementation of a plain old CLR object from the definition of a source class. - /// - /// CodeFactory automation framework used to trigger the refresh. - /// The source class model the model will be generated or updated from. - /// The target project the model will be created in. - /// The source model that holds the existing model implementation. - /// The default namespaces to be added as using statements if they do not exist in the model class. - /// Optional parameter that provides the details for how to format the name of the model class. - /// Optional parameter that determines the target folder the model is to be located in if not on the root of the project. - /// Optional parameter that sets the XML summary for the model class. - /// Optional parameter that determines if properties should converted to non nullable types, default value is true. - /// Optional parameter that maps source and target namespaces when creating the model, default value is null. - /// Optional parameter that calls a delegate to confirm the property should be included with the model class. - /// The loaded class model for the created model class. - /// Raised if required data is missing or a processing error has occurred. - private static async Task UpdateModelAsync(this IVsActions source, CsClass sourceClass, - VsProject modelProject, CsSource modelSource, List defaultNamespaces,NameManagement nameManagement = null, - VsProjectFolder modelFolder = null, string modelSummary = null, bool convertNullableTypes = true, List mappedNamespaces = null,UseSourcePropertyDelegate useSourceProperty = null) - { - - if (source == null) - throw new CodeFactoryException( - "The CodeFactory automation was not provided cannot update the model class."); - if (sourceClass == null) - throw new CodeFactoryException("The source class was not provided cannot update the model class."); - if (modelSource == null) - throw new CodeFactoryException("The target model source was not provided cannot update the model class."); - - var sourcePoco = modelSource; - var modelClass = sourcePoco.Classes.FirstOrDefault(); - - if (modelClass == null) - throw new CodeFactoryException( - $"The call information for the model could not be loaded, cannot update the model class from source class '{sourceClass.Name}'."); - - var sourceProperties = useSourceProperty != null - ? sourceClass.Properties.Where( p=> useSourceProperty(p)).ToList() - : sourceClass.Properties; - - if (!sourceProperties.Any()) return modelClass; - - var modelProperties = modelClass.Properties - .Where(p => p.HasGet & p.HasSet & p.Security == CsSecurity.Public & !p.IsStatic).ToList(); - - var addList = sourceProperties.Where(s => modelProperties.All(p => p.Name != s.Name)).ToList(); - var checkList = sourceProperties.Where(s => modelProperties.Any(p => p.Name == s.Name)).ToList(); - var removeList = new List(); - - bool hasChanges = false; - - var modelMappedNamespaces = mappedNamespaces ?? new List(); - modelMappedNamespaces.Add(new MapNamespace { Destination = modelClass.Namespace, Source = sourceClass.Namespace }); - - foreach (var sourceProperty in checkList) - { - var modelProperty = modelProperties.FirstOrDefault(p => p.Name == sourceProperty.Name); - - if (modelProperty == null) - { - addList.Add(sourceProperty); - continue; - } - - if (sourceProperty.PropertyType.GenerateCSharpTypeName(mappedNamespaces: modelMappedNamespaces) - != modelProperty.PropertyType.GenerateCSharpTypeName(mappedNamespaces: modelMappedNamespaces)) - { - addList.Add(sourceProperty); - removeList.Add(modelProperty); - } - } - - hasChanges = removeList.Any(); - if (!hasChanges) hasChanges = addList.Any(); - - if (!hasChanges) return modelClass; - - if (defaultNamespaces != null) - { - foreach (var defaultNamespace in defaultNamespaces) - { - modelSource = await modelSource.AddUsingStatementAsync(defaultNamespace.ReferenceNamespace, - defaultNamespace.Alias); - } - } - - var propBuilder = new PropertyBuilderTransformNullableTypes(convertNullableTypes); - - var modelUpdateManager = new SourceClassManager(modelSource, modelClass, source, mappedNamespaces: modelMappedNamespaces); - - modelUpdateManager.LoadNamespaceManager(); - - foreach (var removeProperty in removeList) - { - if (removeProperty == null) continue; - - var propertyToRemove = modelUpdateManager.Container.GetModel(removeProperty.LookupPath); - - if (propertyToRemove != null) await modelUpdateManager.MemberCommentOut(propertyToRemove); - } - - foreach (var propertyToAdd in addList) - { - if (propertyToAdd == null) continue; - - //Checking to make sure other source class objects used by the model also have model's created. - if (propertyToAdd.PropertyType.TypeInTargetNamespace(sourceClass.Namespace)) - { - if (propertyToAdd.PropertyType.Name != sourceClass.Name) - { - var targetPocoModel = - await modelProject.FindCSharpSourceByClassNameAsync(propertyToAdd.PropertyType.Name); - - if (targetPocoModel == null) - { - var sourceModelClass = propertyToAdd.PropertyType.GetClassModel() ?? throw new CodeFactoryException( - $"Could not load the data model '{propertyToAdd.PropertyType.Name}' could not refresh the model model, cannot update the model class '{modelClass.Name}'."); - - if (sourceModelClass.IsLoaded == false) throw new CodeFactoryException($"Could not load the data model '{propertyToAdd.PropertyType.Name}' could not refresh the model model, cannot update the model class '{modelClass.Name}'."); - - await source.RefreshModelAsync(sourceModelClass, modelProject, defaultNamespaces,nameManagement, - modelFolder,modelSummary, convertNullableTypes,useSourceProperty:useSourceProperty); - } - } - } - - if (propertyToAdd.PropertyType.IsGeneric) - { - //Checking to make sure other source class objects used by the model also have model's created. - foreach (var propGenericType in propertyToAdd.PropertyType.GenericTypes) - { - //Checking to make sure other source class objects used by the model also have model's created. - if (propGenericType.TypeInTargetNamespace(sourceClass.Namespace)) - { - if (propGenericType.Name != sourceClass.Name) - { - var targetPocoModel = - await modelProject.FindCSharpSourceByClassNameAsync(propGenericType.Name); - - if (targetPocoModel == null) - { - var sourceModelClass = propGenericType.GetClassModel() ?? throw new CodeFactoryException( - $"Could not load the data model '{propertyToAdd.PropertyType.Name}' could not refresh the model model, cannot update the model class '{modelClass.Name}'."); - - if (sourceModelClass.IsLoaded == false) throw new CodeFactoryException($"Could not load the data model '{propertyToAdd.PropertyType.Name}' could not refresh the model model, cannot update the model class '{modelClass.Name}'."); - - await source.RefreshModelAsync(sourceModelClass, modelProject, defaultNamespaces,nameManagement, - modelFolder,modelSummary,convertNullableTypes,useSourceProperty:useSourceProperty); - } - } - } - } - } - - var propertySyntax = await propBuilder.BuildPropertyAsync(propertyToAdd, modelUpdateManager, 2); - - if (propertySyntax != null) await modelUpdateManager.PropertiesAddAfterAsync(propertySyntax); - - } - - return modelUpdateManager.Container; - } - - /// - /// Logic to check if a property should be included in a model class implementation. - /// - /// The property model to be evaluated. - /// True if the property should be used, false if not. - public delegate bool UseSourcePropertyDelegate(CsProperty source); - } + /// + /// Automation library that creates a model class that supports standard property implementation. + /// + public static class ModelBuilder + { + private static readonly ILogger _logger = LogManager.GetLogger(typeof(ModelBuilder)); + + /// + /// Refreshes the implementation of a plain old CLR object based on the definition of a provided class. This will also create the model if it does not exist. + /// + /// CodeFactory automation framework used to trigger the refresh. + /// The source class model the model will be generated or updated from. + /// The target project the model will be created in. + /// The default namespaces to be added as using statements if they do not exist in the model class. + /// Optional parameter that provides the details for how to format the name of the model class. + /// Optional parameter that determines the target folder the model is to be located in if not on the root of the project. + /// Optional parameter that sets the XML summary for the model class. + /// Optional parameter that determines if properties should converted to non nullable types, default value is true. + /// Optional parameter that maps source and target namespaces when creating the model, default value is null. + /// Optional parameter that calls a delegate to confirm the property should be included with the model class. + /// The loaded class model for the created model class. + /// Raised if required data is missing or a processing error has occurred. + public static async Task RefreshModelWithoutDependenciesAsync(this IVsActions source, CsClass sourceClass, + VsProject modelProject, List defaultNamespaces, NameManagement nameManagement = null, VsProjectFolder modelFolder = null, + string modelSummary = null, bool convertNullableTypes = true, List mappedNamespaces = null, UseSourcePropertyDelegate useSourceProperty = null) + { + //Bounds checking + if (source == null) + throw new CodeFactoryException( + "The CodeFactory automation was not provided cannot refresh the model class."); + if (sourceClass == null) + throw new CodeFactoryException("The source class was not provided cannot refresh the model class."); + if (modelProject == null) + throw new CodeFactoryException( + "The target model project was not provided cannot refresh the model class."); + + CsSource modelSource = null; + + string modelName = null; + + modelName = nameManagement != null ? nameManagement.FormatName(sourceClass.Name) : sourceClass.Name; + + if (string.IsNullOrEmpty(modelName)) + throw new CodeFactoryException("Could not format the target model class name, cannot create the model class."); + + bool wasCreated = false; + + if (modelFolder == null) + { + modelSource = (await modelProject.FindCSharpSourceByClassNameAsync(modelName))?.SourceCode; + } + else + { + modelSource = (await modelFolder.FindCSharpSourceByClassNameAsync(modelName))?.SourceCode; + } + + if (modelSource == null) + { + modelSource = await source.CreateModelAsync(sourceClass, modelProject, modelName, defaultNamespaces, modelFolder, + modelSummary, convertNullableTypes) + ?? throw new CodeFactoryException($"Could not create the model '{modelName}' cannot refresh the model."); + } + + + + var modelClass = await source.UpdateModelAsync(sourceClass, modelProject, modelSource, defaultNamespaces, nameManagement, modelFolder, + modelSummary, convertNullableTypes, mappedNamespaces); + + + //if(wasCreated) await source.RegisterTransientClassesAsync(modelProject,false); + + return modelClass; + + } + + /// + /// Creates the shell implementation of a plain old CLR object. + /// + /// CodeFactory automation framework used to trigger the refresh. + /// The source class model the model will be generated or updated from. + /// The target project the model will be created in. + /// The default namespaces to be added as using statements if they do not exist in the model class. + /// Optional parameter that determines the target folder the model is to be located in if not on the root of the project. + /// Optional parameter that sets the XML summary for the model class. + /// Optional parameter that determines if properties should converted to non nullable types, default value is true. + /// The loaded class model for the created model class. + /// Raised if required data is missing or a processing error has occurred. + private static async Task CreateModelAsync(this IVsActions source, CsClass sourceClass, + VsProject modelProject, string targetClassName, List defaultNamespaces, VsProjectFolder modelFolder = null, + string modelSummary = null, bool convertNullableTypes = true) + { + if (source == null) + throw new CodeFactoryException( + "The CodeFactory automation was not provided cannot create the model class."); + if (sourceClass == null) + throw new CodeFactoryException("The source class was not provided cannot create the model class."); + if (modelProject == null) + throw new CodeFactoryException( + "The target model project was not provided cannot create the model class."); + + CsSource result = null; + + try + { + SourceFormatter modelFormatter = new SourceFormatter(); + + if (defaultNamespaces != null) + { + if (defaultNamespaces.Any()) + { + foreach (var usingStatement in defaultNamespaces) + { + modelFormatter.AppendCodeLine(0, + usingStatement.HasAlias + ? $"using {usingStatement.Alias} = {usingStatement.ReferenceNamespace};" + : $"using {usingStatement.ReferenceNamespace};"); + } + } + } + + modelFormatter.AppendCodeLine(0, + modelFolder == null + ? $"namespace {modelProject.DefaultNamespace}" + : $"namespace {await modelFolder.GetCSharpNamespaceAsync()}"); + modelFormatter.AppendCodeLine(0, "{"); + + modelFormatter.AppendCodeLine(1, "/// "); + + if (modelSummary == null) + modelFormatter.AppendCodeLine(1, "/// Plain old CLR object (model) data class implementation."); + else + { + var summaryLines = modelSummary.Split(Environment.NewLine.ToCharArray()); + foreach (var summaryLine in summaryLines) modelFormatter.AppendCodeLine(1, $"/// {summaryLine}"); + } + + modelFormatter.AppendCodeLine(1, "/// "); + modelFormatter.AppendCodeLine(1, $"public class {targetClassName}"); + modelFormatter.AppendCodeLine(1, "{"); + modelFormatter.AppendCodeLine(1); + modelFormatter.AppendCodeLine(1, "}"); + + modelFormatter.AppendCodeLine(0, "}"); + + + VsDocument modelDoc = modelFolder == null + ? await modelProject.AddDocumentAsync($"{targetClassName}.cs", modelFormatter.ReturnSource().TrimStartEndLines()) + : await modelFolder.AddDocumentAsync($"{targetClassName}.cs", modelFormatter.ReturnSource().TrimStartEndLines()); + + if (modelDoc == null) throw new CodeFactoryException($"Error occurred saving the model to the project."); + + result = await modelDoc.GetCSharpSourceModelAsync(); + } + catch (CodeFactoryException) + { + throw; + } + catch (Exception unhandledError) + { + _logger.Error("The following unhandled error occurred creating a model.", unhandledError); + throw new CodeFactoryException( + $"An unhandled error occurred while creating the model '{targetClassName}'. Check the code factory logs for details of what happened."); + } + + return result; + } + + /// + /// Updates the implementation of a plain old CLR object from the definition of a source class. + /// + /// CodeFactory automation framework used to trigger the refresh. + /// The source class model the model will be generated or updated from. + /// The target project the model will be created in. + /// The source model that holds the existing model implementation. + /// The default namespaces to be added as using statements if they do not exist in the model class. + /// Optional parameter that provides the details for how to format the name of the model class. + /// Optional parameter that determines the target folder the model is to be located in if not on the root of the project. + /// Optional parameter that sets the XML summary for the model class. + /// Optional parameter that determines if properties should converted to non nullable types, default value is true. + /// Optional parameter that maps source and target namespaces when creating the model, default value is null. + /// Optional parameter that calls a delegate to confirm the property should be included with the model class. + /// The loaded class model for the created model class. + /// Raised if required data is missing or a processing error has occurred. + private static async Task UpdateModelAsync(this IVsActions source, CsClass sourceClass, + VsProject modelProject, CsSource modelSource, List defaultNamespaces, NameManagement nameManagement = null, + VsProjectFolder modelFolder = null, string modelSummary = null, bool convertNullableTypes = true, List mappedNamespaces = null, UseSourcePropertyDelegate useSourceProperty = null) + { + + if (source == null) + throw new CodeFactoryException( + "The CodeFactory automation was not provided cannot update the model class."); + if (sourceClass == null) + throw new CodeFactoryException("The source class was not provided cannot update the model class."); + if (modelSource == null) + throw new CodeFactoryException("The target model source was not provided cannot update the model class."); + + var sourcePoco = modelSource; + var modelClass = sourcePoco.Classes.FirstOrDefault(); + + if (modelClass == null) + throw new CodeFactoryException( + $"The call information for the model could not be loaded, cannot update the model class from source class '{sourceClass.Name}'."); + + var sourceProperties = useSourceProperty != null + ? sourceClass.Properties.Where(p => useSourceProperty(p)).ToList() + : sourceClass.Properties; + + if (!sourceProperties.Any()) return modelClass; + + var modelProperties = modelClass.Properties + .Where(p => p.HasGet & p.HasSet & p.Security == CsSecurity.Public & !p.IsStatic).ToList(); + + var addList = sourceProperties.Where(s => modelProperties.All(p => p.Name != s.Name)).ToList(); + var checkList = sourceProperties.Where(s => modelProperties.Any(p => p.Name == s.Name)).ToList(); + var removeList = new List(); + + bool hasChanges = false; + + var modelMappedNamespaces = mappedNamespaces ?? new List(); + modelMappedNamespaces.Add(new MapNamespace { Destination = modelClass.Namespace, Source = sourceClass.Namespace }); + + foreach (var sourceProperty in checkList) + { + var modelProperty = modelProperties.FirstOrDefault(p => p.Name == sourceProperty.Name); + + if (modelProperty == null) + { + addList.Add(sourceProperty); + continue; + } + + if (sourceProperty.PropertyType.GenerateCSharpTypeName(mappedNamespaces: modelMappedNamespaces) + != modelProperty.PropertyType.GenerateCSharpTypeName(mappedNamespaces: modelMappedNamespaces)) + { + addList.Add(sourceProperty); + removeList.Add(modelProperty); + } + } + + hasChanges = removeList.Any(); + if (!hasChanges) hasChanges = addList.Any(); + + if (!hasChanges) return modelClass; + + if (defaultNamespaces != null) + { + foreach (var defaultNamespace in defaultNamespaces) + { + modelSource = await modelSource.AddUsingStatementAsync(defaultNamespace.ReferenceNamespace, + defaultNamespace.Alias); + } + } + + var propBuilder = new PropertyBuilderTransformNullableTypes(convertNullableTypes); + + var modelUpdateManager = new SourceClassManager(modelSource, modelClass, source, mappedNamespaces: modelMappedNamespaces); + + modelUpdateManager.LoadNamespaceManager(); + + //instead of removing, we need to exclude the remove from the add list. + addList = addList.Except(removeList).ToList(); + + foreach (var propertyToAdd in addList) + { + if (propertyToAdd == null) continue; + + // Exclude properties with inverse attributes. + if (propertyToAdd.HasAttributes && propertyToAdd.Attributes.Any(a => a.Type.Name.ToLower() == "inversepropertyattribute")) continue; + + var propertySyntax = propBuilder.BuildPropertyAsync(propertyToAdd, modelUpdateManager, 2).Result.TrimEndLines(); + if (propertyToAdd.PropertyType.IsGeneric) + { + // Add suffix + prefix + var genericType = propertyToAdd.PropertyType.GenericTypes.FirstOrDefault().Name; + if (propertyToAdd.PropertyType.Name == "ICollection") + propertySyntax = propertySyntax.Replace($"<{genericType}>", $"<{nameManagement.AddPrefix}{genericType}{nameManagement.AddSuffix}>?"); + } + + if (propertyToAdd.Name == propertyToAdd.PropertyType.Name) + { + propertySyntax = propertySyntax.Replace($"{propertyToAdd.Name} {propertyToAdd.Name}", $"{nameManagement.AddPrefix}{propertyToAdd.Name}{nameManagement.AddSuffix}? {propertyToAdd.Name}"); + } + + if (propertySyntax != null && (!removeList.Any(x => x.Name == propertyToAdd.Name)) && !modelUpdateManager.Source.ToString().Contains($" {propertyToAdd.Name} ")) + await modelUpdateManager.PropertiesAddAfterAsync(propertySyntax.TrimEndLines()).ConfigureAwait(false); + } + modelUpdateManager.Source.Classes.FirstOrDefault().ToString().TrimEndLines(); + return modelUpdateManager.Container; + } + + /// + /// Logic to check if a property should be included in a model class implementation. + /// + /// The property model to be evaluated. + /// True if the property should be used, false if not. + public delegate bool UseSourcePropertyDelegate(CsProperty source); + } } diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/LocalModelBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/LocalModelBuilder.cs new file mode 100644 index 0000000..78106b0 --- /dev/null +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/LocalModelBuilder.cs @@ -0,0 +1,53 @@ +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.ProjectSystem; +using System.Threading.Tasks; + +namespace CodeFactory.Automation.NDF.Logic.Node.Angular +{ + /// + /// Automation logic for creating and updated a local service within an angular project. + /// + public static class LocalModelBuilder + { + public static async Task RefreshAngularModel(this IVsActions source, CsClass controllerClass, + VsProject angularWebProject, string targetFileName, string targetClassName, VsProjectFolder modulesFolder = null, VsProjectFolder modelsFolder = null) + { + if (source == null) + throw new CodeFactoryException("CodeFactory automation was not provided cannot refresh the angular service."); + + if (controllerClass == null) + throw new CodeFactoryException("Cannot load the controller class, cannot refresh the angular service."); + + if (angularWebProject == null) + throw new CodeFactoryException("Cannot load the angular web project, cannot refresh the angular service."); + + VsDocument modelsDocument = (await modelsFolder.FindTypscriptSourceByClassNameAsync(targetClassName)) + ?? await source.CreateAngularModelAsync(controllerClass, angularWebProject, targetFileName, targetClassName, modulesFolder, modelsFolder); + return modelsDocument; + } + + private static async Task CreateAngularModelAsync(this IVsActions source, CsClass modelClass, + VsProject angularWebProject, string targetFileName, string targetClassName, VsProjectFolder modulesFolder = null, VsProjectFolder modelsFolder = null) + { + if (source == null) + throw new CodeFactoryException("CodeFactory automation was not provided cannot refresh the angular service."); + + if (modelClass == null) + throw new CodeFactoryException("Cannot load the model class, cannot refresh the angular service."); + + if (angularWebProject == null) + throw new CodeFactoryException("Cannot load the angular web project, cannot refresh the angular service."); + + // Convert the existing CSharp model class to Typescript + var typescriptModel = TypescriptConverter.ConvertCsModeltoTs(modelClass, targetClassName); + + // Add the new Typescript document + var sourceDocument = (await modelsFolder.AddDocumentAsync($"{targetFileName}.ts", typescriptModel.TrimStartEndLines())) + ?? throw new CodeFactoryException($"Was not able to load the created angular service implementation for '{targetFileName}.ts'"); + + return sourceDocument; + } + } +} \ No newline at end of file diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/LocalServiceBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/LocalServiceBuilder.cs new file mode 100644 index 0000000..a4cb549 --- /dev/null +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/LocalServiceBuilder.cs @@ -0,0 +1,171 @@ +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.ProjectSystem; +using System.Threading.Tasks; + +namespace CodeFactory.Automation.NDF.Logic.Node.Angular +{ + /// + /// Automation logic for creating and updated a local service within an angular project. + /// + public static class LocalServiceBuilder + { + public static async Task RefreshAngularService(this IVsActions source, CsClass controllerClass, + VsProject angularWebProject, VsProjectFolder modulesFolder = null, VsProjectFolder modelsFolder = null, VsProjectFolder servicesFolder = null) + { + if (source == null) + throw new CodeFactoryException("CodeFactory automation was not provided cannot refresh the angular service."); + + if (controllerClass == null) + throw new CodeFactoryException("Cannot load the controller class, cannot refresh the angular service."); + + if (angularWebProject == null) + throw new CodeFactoryException("Cannot load the angular web project, cannot refresh the angular service."); + + // Build controller name + var controllerName = $"{controllerClass.Name.GenerateCSharpFormattedClassName().ToLower()}"; + + // Strip the suffix + var strippedName = controllerName.Replace("controller", "").GenerateCSharpProperCase(); + + // Get or create the service + var serviceFileName = $"{modulesFolder.Name}-{strippedName.ToLower()}-api.service"; + var serviceClassName = $"{modulesFolder.Name}{strippedName}ApiService"; + VsDocument serviceDocument = (await servicesFolder.FindTypscriptSourceByClassNameAsync(serviceClassName)) + ?? await source.CreateAngularServiceAsync(controllerClass, angularWebProject, serviceFileName, serviceClassName, modulesFolder, modelsFolder, servicesFolder); + + return serviceDocument; + } + + private static async Task CreateAngularServiceAsync(this IVsActions source, CsClass controllerClass, + VsProject angularWebProject, string serviceFileName, string serviceClassName, VsProjectFolder modulesFolder = null, VsProjectFolder modelsFolder = null, VsProjectFolder servicesFolder = null) + { + if (source == null) + throw new CodeFactoryException("CodeFactory automation was not provided cannot refresh the angular service."); + + if (controllerClass == null) + throw new CodeFactoryException("Cannot load the controller class, cannot refresh the angular service."); + + if (angularWebProject == null) + throw new CodeFactoryException("Cannot load the angular web project, cannot refresh the angular service."); + + var test = controllerClass.Methods[0].MethodType; + var sourceFormatter = new SourceFormatter(); + sourceFormatter.AppendCodeLine(0, $"/* Autogenerated Angular Service */"); + sourceFormatter.AppendCodeLine(0, "import { Injectable } from \"@angular/core\";"); + sourceFormatter.AppendCodeLine(0, "import { HttpClient } from '@angular/common/http';"); + sourceFormatter.AppendCodeLine(0, "import { Observable, map } from \"rxjs\";"); + + foreach (var method in controllerClass.Methods) + { + if (method.HasParameters) + { + foreach (var parameter in method.Parameters) + { + if (parameter.Name.ToLower().EndsWith("model") && !parameter.ParameterType.IsWellKnownType) + { + // Resolve the cs class model + var csClassModel = parameter.ParameterType.GetClassModel(); + + // Get the stripped Name + var strippedClassName = csClassModel.Name.Replace("Model", "").GenerateCSharpProperCase(); + + // Get or create the model + var modelClassName = $"{strippedClassName.ToKebabCase()}"; + var modelFileName = $"{modulesFolder.Name}-{modelClassName}.model"; + var importStatement = "import { " + strippedClassName + " }"; + + // Import the model reference if it hasn't already been imported + if (!sourceFormatter.ReturnSource().Contains(importStatement)) + sourceFormatter.AppendCodeLine(0, importStatement + " from '../" + modelsFolder.Name + "/" + modelFileName + "';"); + } + } + } + } + + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(0, "@Injectable({ providedIn: 'root' })"); + sourceFormatter.AppendCodeLine(0, "export class " + serviceClassName + " {"); + + // Add Constructor + sourceFormatter.AppendCodeLine(1, $"/* Constructor */"); + sourceFormatter.AppendCodeLine(1, "constructor(private http: HttpClient) {"); + sourceFormatter.AppendCodeLine(1, "}"); + sourceFormatter.AppendCodeLine(0); + + // Add Methods based-on the Controller in the BFF + foreach (var method in controllerClass.Methods) + { + // Skip if the method is not public + if (method.Security == CsSecurity.Public) + { + sourceFormatter.AppendCodeLine(0, $"/* Method to {method.Name.Prettify()} */"); + + // Get the action type + var action = "get"; + // TODO Uncomment when ready to support posts + //if (method.HasAttributes && method.Attributes.Any(a => a.Type.Name == "HttpPostAttribute")) + //{ + // action = "post"; + //} + + // Get the Return Type + var returnType = ""; + if (method.ReturnType.HasStrongTypesInGenerics) + { + returnType = method.ReturnType.GenericTypes[0].Name; + if (returnType.EndsWith("Model")) + returnType = returnType.Replace("Model", ""); + } + else + { + returnType = method.ReturnType.Name; + } + + // Build Signature + var signatureParameters = ""; + if (method.HasParameters) + { + // Build parameters + for (int i = 0; i < method.Parameters.Count; i++) + { + if(i > 0) + signatureParameters += ", "; + + signatureParameters += $"{method.Parameters[i].Name}: {TypescriptConverter.ConvertToTypeScriptType(method.Parameters[i].ParameterType.Name)}"; + } + } + sourceFormatter.AppendCodeLine(0, $"{method.Name.GenerateCSharpCamelCase()}({signatureParameters}): Observable<{TypescriptConverter.ConvertToTypeScriptType(returnType)}>"); + + // Add thie method body + sourceFormatter.AppendCodeLine(0, "{"); + var urlParameters = ""; + if (method.HasParameters) + { + // Build parameters + for (int i = 0; i < method.Parameters.Count; i++) + { + if (i > 0) + urlParameters += "&"; + else + urlParameters += "?"; + + urlParameters += "{" + method.Parameters[i].Name + "}"; + } + } + sourceFormatter.AppendCodeLine(1, "return this.http."+ action +"(`/api/v1/t1/"+ method.Name.ToLower() + "/$"+ urlParameters + "`).pipe(map(r => r as "+ returnType +"));"); + sourceFormatter.AppendCodeLine(0, "}"); + } + } + + sourceFormatter.AppendCodeLine(0, "}"); + + // Add the document + var sourceDocument = (await servicesFolder.AddDocumentAsync($"{serviceFileName}.ts", sourceFormatter.ReturnSource().TrimStartEndLines())) + ?? throw new CodeFactoryException($"Was not able to load the created angular service implementation for '{serviceFileName}.ts'"); + + return sourceDocument; + } + } +} \ No newline at end of file diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/TypescriptConverter.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/TypescriptConverter.cs new file mode 100644 index 0000000..93d4905 --- /dev/null +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/TypescriptConverter.cs @@ -0,0 +1,128 @@ +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs.Models.CSharp; + +namespace CodeFactory.Automation.NDF.Logic.Node.Angular +{ + /// + /// Automation library that creates a model class that supports standard node angular implementation. + /// + public static class TypescriptConverter + { + // + // Converts a C# class model to an Angular model class in TypeScript. + // + // Parameters: + // - csharpModel: The C# class model to be converted. + // + // Returns: + // - A string representing the TypeScript model class. + // + public static string ConvertCsModeltoTs(CsClass csharpModel, string targetModelName) + { + // Create a StringBuilder to store the TypeScript model class. + var sourceFormatter = new SourceFormatter(); + sourceFormatter.AppendCodeLine(0, $"/* Autogenerated model file */"); + sourceFormatter.AppendCodeLine(0, $"export class {targetModelName}"); + sourceFormatter.AppendCodeLine(0, "{"); + + // Iterate through each property of the C# model. + foreach (var property in csharpModel.Properties) + { + // Convert the C# property type to TypeScript. + string tsType = ConvertToTypeScriptType(property.PropertyType.Name); + + // Append the TypeScript property to the model class. + sourceFormatter.AppendCodeLine(2, $"{property.Name}?: {tsType};"); + } + + // Add the default model. + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(1, "static default() {"); + sourceFormatter.AppendCodeLine(2, $"const def = new {targetModelName}();"); + foreach (var property in csharpModel.Properties) + { + sourceFormatter.AppendCodeLine(2, $"def.{property.Name} = {GetDefaultValueFromTypeScriptType(property.PropertyType.Name)};"); + } + + sourceFormatter.AppendCodeLine(1, "}"); + sourceFormatter.AppendCodeLine(0, "}"); + + // Return the TypeScript model class. + return sourceFormatter.ReturnSource().TrimStartEndLines(); + } + + // + // Converts a C# property type to the equivalent TypeScript type. + // + // Parameters: + // - csharpType: The C# property type to be converted. + // + // Returns: + // - A string representing the equivalent TypeScript type. + // + public static string ConvertToTypeScriptType(string csharpType) + { + switch (csharpType.ToLower()) + { + case "int": + case "int32": + case "int64": + case "long": + case "float": + case "decimal": + case "double": + return "number"; + case "task": + case "list": + case "collection": + case "ienumeration": + return "any"; + case "boolean": + return "boolean"; + case "string": + return "string"; + case "datetime": + return "Date"; + default: + return csharpType; + } + } + + // + // Gets a C# property type, Typescript equivelant, default value. + // + // Parameters: + // - csharpType: The C# property type. + // + // Returns: + // - A string representing the equivalent TypeScript type default value. + // + public static string GetDefaultValueFromTypeScriptType(string csharpType) + { + switch (csharpType.ToLower()) + { + case "int": + case "int32": + case "int64": + case "long": + case "float": + case "decimal": + case "double": + return "0"; + case "task": + case "list": + case "collection": + case "ienumerable": + return "{}"; + case "boolean": + return "false"; + case "string": + return "\"\""; + case "datetime": + return "new Date()"; + default: + return "null"; + } + } + } +} \ No newline at end of file diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/ProjectExtensions.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/ProjectExtensions.cs index 2e1285f..2699424 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/ProjectExtensions.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/ProjectExtensions.cs @@ -1,16 +1,13 @@ -using CodeFactory.WinVs.Models.ProjectSystem; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs.Models.ProjectSystem; using System.Threading.Tasks; -using CodeFactory.Automation.Standard.Logic; + namespace CodeFactory.Automation.NDF.Logic { - /// - /// Extension methods that support the - /// - public static class ProjectExtensions + /// + /// Extension methods that support the + /// + public static class ProjectExtensions { /// diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/BUnit/IntegrationTestBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/BUnit/IntegrationTestBuilder.cs new file mode 100644 index 0000000..dcb8342 --- /dev/null +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/BUnit/IntegrationTestBuilder.cs @@ -0,0 +1,462 @@ +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory.WinVs.Models.ProjectSystem; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeFactory.Automation.NDF.Logic.Testing.BUnit +{ + /// + /// Automation logic that supports integration testing using the BUnit unit test framework. + /// + public static class IntegrationTestBuilder + { + /// + /// Automation to refresh the integration test implementation. + /// + /// CodeFactory automation access to Visual Studio. + /// The name of the test class to be refreshed. + /// The target contract to implement testing for. + /// The target project the target logic is implemented in. + /// + public static async Task RefreshIntegrationTestAsync(this IVsActions source, string testName, CsInterface contract, VsProject testProject) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the integration test."); + + if (string.IsNullOrEmpty(testName)) throw new CodeFactoryException("No test name was provided cannot update the integration test."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot update the integration test."); + + if (testProject == null) throw new CodeFactoryException($"No test project was provided cannot refresh the integration tests that support the contract '{contract.Name}'"); + + var isTestProject = await testProject.TestProjectIsConfiguredBUnitAsync(true); + + await testProject.CreateTestLoaderAsync(); + + var testClassName = testName; + + if (string.IsNullOrEmpty(testClassName)) throw new CodeFactoryException("Could not load the test class name. Cannot refresh the integration tests."); + + var testSource = await testProject.FindCSharpSourceByClassNameAsync(testClassName, false); + + if (testSource == null) await source.CreateTestAsync(contract, testProject, testClassName); + else await source.UpdateTestAsync(contract, testSource.SourceCode); + + } + + /// + /// Creates a new integration test; + /// + /// CodeFactory automation for Visual Studio. + /// The target interface to be tested. + /// The target project the test should be created in. + /// The name of the target test class. + /// Raised if required data is missing. + public static async Task CreateTestAsync(this IVsActions source, CsInterface contract, VsProject testProject, string testClassName) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the tests."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot create the tests."); + + if (testProject == null) throw new CodeFactoryException($"No test project was provided cannot create the tests that support the contract '{contract.Name}'"); + + if (string.IsNullOrEmpty(testClassName)) throw new CodeFactoryException($"The test class name was not provided cannot create the tests that support the contract '{contract.Name}'"); + + SourceFormatter testFormatter = new SourceFormatter(); + + testFormatter.AppendCodeLine(0, "using System;"); + testFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); + testFormatter.AppendCodeLine(0, "using System.Linq;"); + testFormatter.AppendCodeLine(0, "using System.Linq.Expressions;"); + testFormatter.AppendCodeLine(0, "using System.Text;"); + testFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); + testFormatter.AppendCodeLine(0, "using Xunit;"); + testFormatter.AppendCodeLine(0, $"using {contract.Namespace};"); + testFormatter.AppendCodeLine(0); + + testFormatter.AppendCodeLine(0, $"namespace {testProject.DefaultNamespace}"); + testFormatter.AppendCodeLine(0, "{"); + + testFormatter.AppendCodeLine(1, "/// "); + testFormatter.AppendCodeLine(1, $"/// Integration test class that tests the contract "); + testFormatter.AppendCodeLine(1, "/// "); + testFormatter.AppendCodeLine(1, $"public class {testClassName}"); + testFormatter.AppendCodeLine(1, "{"); + + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"/// The contract being tested."); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"private readonly {contract.Name} _contract;"); + testFormatter.AppendCodeLine(0); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"/// Creates a new instances of the intergration test class for testing."); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"public {testClassName}()"); + testFormatter.AppendCodeLine(2, "{"); + testFormatter.AppendCodeLine(3, $"_contract = TestLoader.GetRequiredService<{contract.Name}>();"); + testFormatter.AppendCodeLine(2, "}"); + testFormatter.AppendCodeLine(0); + + testFormatter.AppendCodeLine(1, "}"); + + testFormatter.AppendCodeLine(0, "}"); + + var doc = await testProject.AddDocumentAsync($"{testClassName}.cs", testFormatter.ReturnSource().TrimStartEndLines()); + + var testSourceCode = await doc.GetCSharpSourceModelAsync(); + + await source.UpdateTestAsync(contract, testSourceCode); + } + + /// + /// Updates a test class that performs intergration tests on the target contract. + /// + /// CodeFactory automation for visual studio. + /// The target contract that is being tested. + /// The target source code that is to be updated. + /// Throw if required data is missing. + public static async Task UpdateTestAsync(this IVsActions source, CsInterface contract, CsSource testClassSource) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the integration tests."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot update the integration tests."); + + if (testClassSource == null) throw new CodeFactoryException($"The test class source was not provided cannot update the integration tests that support the contract '{contract.Name}'"); + + var currentSource = testClassSource; + + var testClass = currentSource.Classes.FirstOrDefault(); + + if (testClass == null) throw new CodeFactoryException($"The test class could not be loaded from the provided source. cannot update the integrationn tests that support the contract '{contract.Name}'"); + + var contractMethods = contract.GetAllInterfaceMethods(); + + if (!contractMethods.Any()) return; + + var sourceMethods = testClass.Methods ?? new List(); + + var AddTests = new List(); + + foreach (var contractMethod in contractMethods) + { + var testMethodName = contractMethod.FormatTestMethodName(); + + if (string.IsNullOrEmpty(testMethodName)) continue; + + if (!sourceMethods.Any(m => m.Name == testMethodName)) AddTests.Add(contractMethod); + } + + if (!AddTests.Any()) return; + + var sourceManager = new SourceClassManager(currentSource, testClass, source); + + foreach (var addMethod in AddTests) + { + bool hasReturnType = !addMethod.IsVoid; + bool isAsync = false; + bool hasParameters = addMethod.HasParameters; + StringBuilder parameterBuilder = new StringBuilder(); + var testMethodFormatter = new SourceFormatter(); + + if (hasReturnType) + { + isAsync = addMethod.ReturnType.IsTaskType(); + + if (isAsync) hasReturnType = !addMethod.ReturnType.IsTaskOnlyType(); + } + + testMethodFormatter.AppendCodeLine(0); + testMethodFormatter.AppendCodeLine(2, "/// "); + testMethodFormatter.AppendCodeLine(2, $"/// Integration test that tests the contract method \"{addMethod.Name}\""); + testMethodFormatter.AppendCodeLine(2, "/// "); + testMethodFormatter.AppendCodeLine(2, "[Fact]"); + + if (isAsync) testMethodFormatter.AppendCodeLine(2, $"public async Task {addMethod.FormatTestMethodName()}()"); + else testMethodFormatter.AppendCodeLine(2, $"public void {addMethod.FormatTestMethodName()}()"); + testMethodFormatter.AppendCodeLine(2, "{"); + testMethodFormatter.AppendCodeLine(3, "//Arrange"); + + if (hasParameters) + { + bool firstParameter = true; + + foreach (var testParameter in addMethod.Parameters) + { + if (firstParameter) + { + parameterBuilder.Append($"{testParameter.Name}"); + firstParameter = false; + } + else parameterBuilder.Append($", {testParameter.Name}"); + + string defaultValue = testParameter.ParameterType.GenerateCSharpDefaultValue(); + testMethodFormatter.AppendCodeLine(3, + defaultValue != null + ? $"{testParameter.ParameterType.GenerateCSharpTypeName()} {testParameter.Name} = new {testParameter.ParameterType.GenerateCSharpTypeName()}();" + : $"{testParameter.ParameterType.GenerateCSharpTypeName()} {testParameter.Name};"); + testMethodFormatter.AppendCodeLine(0); + } + } + testMethodFormatter.AppendCodeLine(3, "using var ctx = new TestContext();"); + testMethodFormatter.AppendCodeLine(0); + + testMethodFormatter.AppendCodeLine(3, "try"); + testMethodFormatter.AppendCodeLine(3, "{"); + + if (hasReturnType) + { + testMethodFormatter.AppendCodeLine(4, "//Act"); + var returnType = isAsync ? addMethod.ReturnType.GenericParameters.First().Type : addMethod.ReturnType; + + var defaultValue = returnType.GenerateCSharpDefaultValue(); + + testMethodFormatter.AppendCodeLine(4, defaultValue != null + ? $"{returnType.GenerateCSharpTypeName()}? result = new {returnType.GenerateCSharpTypeName()}();" + : $"{returnType.GenerateCSharpTypeName()} result;"); + testMethodFormatter.AppendCodeLine(4); + + } + + string awaitStatement = isAsync ? " await " : " "; + string asyncStatement = isAsync ? "async" : ""; + + var methodParameters = hasParameters ? parameterBuilder.ToString() : ""; + + testMethodFormatter.AppendCodeLine(4, hasReturnType + ? $"result ={awaitStatement}_contract.{addMethod.Name}({methodParameters});" + : $"{awaitStatement}_contract.{addMethod.Name}({methodParameters});"); + testMethodFormatter.AppendCodeLine(4); + + if (hasReturnType) + { + testMethodFormatter.AppendCodeLine(4, "//Assert"); + testMethodFormatter.AppendCodeLine(4, "Assert.NotNull(result);"); + } + + else + { + testMethodFormatter.AppendCodeLine(4, "//Act and Assert"); + testMethodFormatter.AppendCodeLine(4, $"var ex = Assert.Throws(()=> {awaitStatement} _contract.{addMethod.Name}({methodParameters}));"); + testMethodFormatter.AppendCodeLine(4, $"Assert.NotNull(ex);"); + } + + testMethodFormatter.AppendCodeLine(4); + testMethodFormatter.AppendCodeLine(3, "}"); + testMethodFormatter.AppendCodeLine(3, "catch (Exception unhandled)"); + testMethodFormatter.AppendCodeLine(3, "{"); + testMethodFormatter.AppendCodeLine(4, "Assert.Fail($\"The following unhandled exception occurred '{unhandled.Message}' \");"); + testMethodFormatter.AppendCodeLine(3, "}"); + + testMethodFormatter.AppendCodeLine(3, "finally"); + testMethodFormatter.AppendCodeLine(3, "{"); + testMethodFormatter.AppendCodeLine(4, "//Cleanup"); + testMethodFormatter.AppendCodeLine(4, "// Add any cleanup code if necessary"); + testMethodFormatter.AppendCodeLine(3, "}"); + testMethodFormatter.AppendCodeLine(0); + + testMethodFormatter.AppendCodeLine(2, "}"); + + await sourceManager.ConstructorsAddAfterAsync(testMethodFormatter.ReturnSource()); + + testMethodFormatter.ResetFormatter(); + } + + return; + } + + /// + /// Formats the repositories test class name. + /// + /// Source interface to convert to the test class name. + /// Formatted test class name or null if it was not found. + private static string FormatTestClassName(this CsInterface source) + { + if (source == null) return null; + + return $"{source.Name.GenerateCSharpFormattedClassName()}Test"; + } + + /// + /// Formats the name of the test method for the contract. + /// + /// Method model to generate the test method name from. + /// Formatted method name or null if it is not found. + private static string FormatTestMethodName(this CsMethod source) + { + if (source == null) return null; + + if (!source.HasParameters) return source.Name; + + StringBuilder testMethodBuilder = new StringBuilder(); + + testMethodBuilder.Append($"{source.Name}By"); + + foreach (var parameter in source.Parameters) + testMethodBuilder.Append(parameter.Name.GenerateCSharpProperCase()); + + return testMethodBuilder.ToString(); + } + + /// + /// Creates a new instance of the 'TestLoader' class. + /// + /// Target project to add the test loader to. + public static async Task CreateTestLoaderAsync(this VsProject testProject) + { + var sourceFile = await testProject.FindCSharpSourceByClassNameAsync("TestLoader", false); + + if (sourceFile != null) return; + + var sourceFormatter = new SourceFormatter(); + + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Configuration;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.DependencyInjection;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Logging;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Logging.Abstractions;"); + sourceFormatter.AppendCodeLine(0, "using System;"); + sourceFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); + sourceFormatter.AppendCodeLine(0, "using System.Linq;"); + sourceFormatter.AppendCodeLine(0, "using System.Text;"); + sourceFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(0, $"namespace {testProject.DefaultNamespace}"); + sourceFormatter.AppendCodeLine(0, "{"); + sourceFormatter.AppendCodeLine(1, "/// "); + sourceFormatter.AppendCodeLine(1, "/// Loader class that is used by testing to load required services."); + sourceFormatter.AppendCodeLine(1, "/// "); + sourceFormatter.AppendCodeLine(1, "public static class TestLoader"); + sourceFormatter.AppendCodeLine(1, "{"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Backing field for the property."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly IConfiguration _configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Backing field for the property."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly IServiceProvider _serviceProvider;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Logging factory used for testing"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly ILoggerFactory _loggerFactory;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Constructor that gets called when the class is accessed for the first time."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "static TestLoader()"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "//Loading the configuration"); + sourceFormatter.AppendCodeLine(3, "var configuration = new ConfigurationBuilder()"); + sourceFormatter.AppendCodeLine(3, " .AddJsonFile(\"appsettings.local.json\", true)"); + sourceFormatter.AppendCodeLine(3, " .AddEnvironmentVariables().Build();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Setting the config property."); + sourceFormatter.AppendCodeLine(3, "_configuration = configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Creating the service container"); + sourceFormatter.AppendCodeLine(3, "var services = new ServiceCollection();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Adding access to configuration from the service container"); + sourceFormatter.AppendCodeLine(3, "services.AddSingleton(Configuration);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Loading the libraries into the service collection."); + sourceFormatter.AppendCodeLine(3, "LoadLibraries(services, _configuration);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "_loggerFactory = new NullLoggerFactory();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "services.AddSingleton(_loggerFactory);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "services.AddLogging();"); + sourceFormatter.AppendCodeLine(3, "//Building the service provider."); + sourceFormatter.AppendCodeLine(3, "_serviceProvider = services.BuildServiceProvider();"); + sourceFormatter.AppendCodeLine(0); + + sourceFormatter.AppendCodeLine(2, "}"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// The loaded configuration to be used with testing."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "public static IConfiguration Configuration => _configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Service provider for the loaded dependency configuration."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "public static IServiceProvider ServiceProvider => _serviceProvider;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Gets the required service to use with testing."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Type of the service to be loaded."); + sourceFormatter.AppendCodeLine(2, "/// Instance of the service"); + sourceFormatter.AppendCodeLine(2, "public static T GetRequiredService() where T : notnull"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "return _serviceProvider.GetRequiredService();"); + + sourceFormatter.AppendCodeLine(2, "}"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Loads the libraries into service collection"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Service collection to load."); + sourceFormatter.AppendCodeLine(2, "/// The configuration to be provided to services."); + sourceFormatter.AppendCodeLine(2, "public static void LoadLibraries(IServiceCollection services, IConfiguration configuration)"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "// Load Default Library Loader."); + sourceFormatter.AppendCodeLine(3, "var libraryLoader = new LibraryLoader();"); + sourceFormatter.AppendCodeLine(3, "libraryLoader.Load(services, configuration);"); + sourceFormatter.AppendCodeLine(2, "}"); + + + sourceFormatter.AppendCodeLine(1, "}"); + sourceFormatter.AppendCodeLine(0, "}"); + + await testProject.AddDocumentAsync("TestLoader.cs", sourceFormatter.ReturnSource().TrimStartEndLines()); + } + + /// + /// Helper method that checks a project to make sure all required project references exist before building a test. + /// + /// Project to check. + /// Optional flag that determines if an exception should be thrown if the project is not configred, default is false. + /// True if configured with BUnit Tests, false if not. + /// Thrown if required data is missing. + public static async Task TestProjectIsConfiguredBUnitAsync(this VsProject project, bool throwError = false) + { + if (project == null) throw new CodeFactoryException("No test project was provided cannot build integration tests."); + + var projectRefs = await project.GetProjectReferencesAsync(); + + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.Logging.Abstractions")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.Logging.Abstractions'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.Configuration")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.Configuration'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.DependencyInjection")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.DependencyInjection'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.VisualStudio.TestPlatform.ObjectModel")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.VisualStudio.TestPlatform.ObjectModel'"); + return false; + } + if (!projectRefs.Any(r => r.Name.ToLower().Contains("bunit."))) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'bunit'"); + return false; + } + return true; + } + } +} diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/MSTest/IntegrationTestBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/MSTest/IntegrationTestBuilder.cs index 325bb37..3ebf1c0 100644 --- a/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/MSTest/IntegrationTestBuilder.cs +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/MSTest/IntegrationTestBuilder.cs @@ -1,19 +1,19 @@ -using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; using CodeFactory.WinVs.Models.ProjectSystem; -using CodeFactory.WinVs; -using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using CodeFactory.Automation.Standard.Logic; + namespace CodeFactory.Automation.NDF.Logic.Testing.MSTest { - /// - /// Automation logic that supports integration testing using the MSTest unit test framework. - /// - public static class IntegrationTestBuilder + /// + /// Automation logic that supports integration testing using the MSTest unit test framework. + /// + public static class IntegrationTestBuilder { /// /// Automation to refresh the integration test implementation. @@ -33,7 +33,7 @@ public static async Task RefreshMSTestIntegrationTestAsync(this IVsActions sourc if (testProject == null) throw new CodeFactoryException($"No test project was provided cannot refresh the integration tests that support the contract '{contract.Name}'"); - var isTestProject = await testProject.TestProjectIsConfiguredAsync(true); + var isTestProject = await testProject.TestProjectIsConfiguredMSTestAsync(true); await testProject.CreateTestLoaderAsync(); @@ -398,7 +398,7 @@ public static async Task CreateTestLoaderAsync(this VsProject testProject) /// Optional flag that determines if an exception should be thrown if the project is not configred, default is false. /// True if configured false if not. /// Thrown if required data is missing. - public static async Task TestProjectIsConfiguredAsync(this VsProject project, bool throwError = false) + public static async Task TestProjectIsConfiguredMSTestAsync(this VsProject project, bool throwError = false) { if (project == null) throw new CodeFactoryException("No test project was provided cannot build integration tests."); diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/NUnit/IntegrationTestBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/NUnit/IntegrationTestBuilder.cs new file mode 100644 index 0000000..4e3a0fe --- /dev/null +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/NUnit/IntegrationTestBuilder.cs @@ -0,0 +1,461 @@ +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory.WinVs.Models.ProjectSystem; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeFactory.Automation.NDF.Logic.Testing.NUnit +{ + /// + /// Automation logic that supports integration testing using the NUnit unit test framework. + /// + public static class IntegrationTestBuilder + { + /// + /// Automation to refresh the integration test implementation. + /// + /// CodeFactory automation access to Visual Studio. + /// The name of the test class to be refreshed. + /// The target contract to implement testing for. + /// The target project the target logic is implemented in. + /// + public static async Task RefreshIntegrationTestAsync(this IVsActions source, string testName, CsInterface contract, VsProject testProject) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the integration test."); + + if (string.IsNullOrEmpty(testName)) throw new CodeFactoryException("No test name was provided cannot update the integration test."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot update the integration test."); + + if (testProject == null) throw new CodeFactoryException($"No test project was provided cannot refresh the integration tests that support the contract '{contract.Name}'"); + + var isTestProject = await testProject.TestProjectIsConfiguredNUnitAsync(true); + + await testProject.CreateTestLoaderAsync(); + + var testClassName = testName; + + if (string.IsNullOrEmpty(testClassName)) throw new CodeFactoryException("Could not load the test class name. Cannot refresh the integration tests."); + + var testSource = await testProject.FindCSharpSourceByClassNameAsync(testClassName, false); + + if (testSource == null) await source.CreateTestAsync(contract, testProject, testClassName); + else await source.UpdateTestAsync(contract, testSource.SourceCode); + + } + + /// + /// Creates a new integration test; + /// + /// CodeFactory automation for Visual Studio. + /// The target interface to be tested. + /// The target project the test should be created in. + /// The name of the target test class. + /// Raised if required data is missing. + public static async Task CreateTestAsync(this IVsActions source, CsInterface contract, VsProject testProject, string testClassName) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the tests."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot create the tests."); + + if (testProject == null) throw new CodeFactoryException($"No test project was provided cannot create the tests that support the contract '{contract.Name}'"); + + if (string.IsNullOrEmpty(testClassName)) throw new CodeFactoryException($"The test class name was not provided cannot create the tests that support the contract '{contract.Name}'"); + + SourceFormatter testFormatter = new SourceFormatter(); + + testFormatter.AppendCodeLine(0, "using System;"); + testFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); + testFormatter.AppendCodeLine(0, "using System.Linq;"); + testFormatter.AppendCodeLine(0, "using System.Linq.Expressions;"); + testFormatter.AppendCodeLine(0, "using System.Text;"); + testFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); + testFormatter.AppendCodeLine(0, "using Microsoft.VisualStudio.TestTools.UnitTesting;"); + testFormatter.AppendCodeLine(0, $"using {contract.Namespace};"); + testFormatter.AppendCodeLine(0); + + testFormatter.AppendCodeLine(0, $"namespace {testProject.DefaultNamespace}"); + testFormatter.AppendCodeLine(0, "{"); + + testFormatter.AppendCodeLine(1, "/// "); + testFormatter.AppendCodeLine(1, $"/// Integration test class that tests the contract "); + testFormatter.AppendCodeLine(1, "/// "); + testFormatter.AppendCodeLine(1, "[TestFixture]"); + testFormatter.AppendCodeLine(1, $"public class {testClassName}"); + testFormatter.AppendCodeLine(1, "{"); + + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"/// The contract being tested."); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"private readonly {contract.Name} _contract;"); + testFormatter.AppendCodeLine(0); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"/// Creates a new instances of the intergration test class for testing."); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"public {testClassName}()"); + testFormatter.AppendCodeLine(2, "{"); + testFormatter.AppendCodeLine(3, $"_contract = TestLoader.GetRequiredService<{contract.Name}>();"); + testFormatter.AppendCodeLine(2, "}"); + testFormatter.AppendCodeLine(0); + + testFormatter.AppendCodeLine(1, "}"); + + testFormatter.AppendCodeLine(0, "}"); + + var doc = await testProject.AddDocumentAsync($"{testClassName}.cs", testFormatter.ReturnSource().TrimStartEndLines()); + + var testSourceCode = await doc.GetCSharpSourceModelAsync(); + + await source.UpdateTestAsync(contract, testSourceCode); + } + + /// + /// Updates a test class that performs intergration tests on the target contract. + /// + /// CodeFactory automation for visual studio. + /// The target contract that is being tested. + /// The target source code that is to be updated. + /// Throw if required data is missing. + public static async Task UpdateTestAsync(this IVsActions source, CsInterface contract, CsSource testClassSource) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the integration tests."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot update the integration tests."); + + if (testClassSource == null) throw new CodeFactoryException($"The test class source was not provided cannot update the integration tests that support the contract '{contract.Name}'"); + + var currentSource = testClassSource; + + var testClass = currentSource.Classes.FirstOrDefault(); + + if (testClass == null) throw new CodeFactoryException($"The test class could not be loaded from the provided source. cannot update the integrationn tests that support the contract '{contract.Name}'"); + + var contractMethods = contract.GetAllInterfaceMethods(); + + if (!contractMethods.Any()) return; + + var sourceMethods = testClass.Methods ?? new List(); + + var AddTests = new List(); + + foreach (var contractMethod in contractMethods) + { + var testMethodName = contractMethod.FormatTestMethodName(); + + if (string.IsNullOrEmpty(testMethodName)) continue; + + if (!sourceMethods.Any(m => m.Name == testMethodName)) AddTests.Add(contractMethod); + } + + if (!AddTests.Any()) return; + + var sourceManager = new SourceClassManager(currentSource, testClass, source); + + foreach (var addMethod in AddTests) + { + bool hasReturnType = !addMethod.IsVoid; + bool isAsync = false; + bool hasParameters = addMethod.HasParameters; + StringBuilder parameterBuilder = new StringBuilder(); + var testMethodFormatter = new SourceFormatter(); + + if (hasReturnType) + { + isAsync = addMethod.ReturnType.IsTaskType(); + + if (isAsync) hasReturnType = !addMethod.ReturnType.IsTaskOnlyType(); + } + + testMethodFormatter.AppendCodeLine(0); + testMethodFormatter.AppendCodeLine(2, "/// "); + testMethodFormatter.AppendCodeLine(2, $"/// Integration test that tests the contract method \"{addMethod.Name}\""); + testMethodFormatter.AppendCodeLine(2, "/// "); + testMethodFormatter.AppendCodeLine(2, "[Test]"); + + if (isAsync) testMethodFormatter.AppendCodeLine(2, $"public async Task {addMethod.FormatTestMethodName()}()"); + else testMethodFormatter.AppendCodeLine(2, $"public void {addMethod.FormatTestMethodName()}()"); + testMethodFormatter.AppendCodeLine(2, "{"); + testMethodFormatter.AppendCodeLine(3, "//Arrange"); + + if (hasParameters) + { + bool firstParameter = true; + + foreach (var testParameter in addMethod.Parameters) + { + if (firstParameter) + { + parameterBuilder.Append($"{testParameter.Name}"); + firstParameter = false; + } + else parameterBuilder.Append($", {testParameter.Name}"); + + string defaultValue = testParameter.ParameterType.GenerateCSharpDefaultValue(); + testMethodFormatter.AppendCodeLine(3, + defaultValue != null + ? $"{testParameter.ParameterType.GenerateCSharpTypeName()} {testParameter.Name} = new {testParameter.ParameterType.GenerateCSharpTypeName()}();" + : $"{testParameter.ParameterType.GenerateCSharpTypeName()} {testParameter.Name};"); + testMethodFormatter.AppendCodeLine(0); + } + } + + testMethodFormatter.AppendCodeLine(3, "try"); + testMethodFormatter.AppendCodeLine(3, "{"); + + if (hasReturnType) + { + testMethodFormatter.AppendCodeLine(4, "//Act"); + var returnType = isAsync ? addMethod.ReturnType.GenericParameters.First().Type : addMethod.ReturnType; + + var defaultValue = returnType.GenerateCSharpDefaultValue(); + + testMethodFormatter.AppendCodeLine(4, defaultValue != null + ? $"{returnType.GenerateCSharpTypeName()}? result = new {returnType.GenerateCSharpTypeName()}();" + : $"{returnType.GenerateCSharpTypeName()} result;"); + testMethodFormatter.AppendCodeLine(4); + + } + + string awaitStatement = isAsync ? " await " : " "; + string asyncStatement = isAsync ? "async" : ""; + + var methodParameters = hasParameters ? parameterBuilder.ToString() : ""; + + testMethodFormatter.AppendCodeLine(4, hasReturnType + ? $"result ={awaitStatement}_contract.{addMethod.Name}({methodParameters});" + : $"{awaitStatement}_contract.{addMethod.Name}({methodParameters});"); + testMethodFormatter.AppendCodeLine(4); + + if (hasReturnType) + { + testMethodFormatter.AppendCodeLine(4, "//Assert"); + testMethodFormatter.AppendCodeLine(4, "Assert.IsNotNull(result);"); + } + + else + { + testMethodFormatter.AppendCodeLine(4, "//Act and Assert"); + testMethodFormatter.AppendCodeLine(4, $"await Assert.ThrowsAsync({asyncStatement} ()=> {awaitStatement} _contract.{addMethod.Name}({methodParameters}));"); + } + + testMethodFormatter.AppendCodeLine(4); + testMethodFormatter.AppendCodeLine(3, "}"); + + testMethodFormatter.AppendCodeLine(3, "catch (Exception unhandled)"); + testMethodFormatter.AppendCodeLine(3, "{"); + testMethodFormatter.AppendCodeLine(4, "Assert.Fail($\"The following unhandled exception occurred '{unhandled.Message}' \");"); + testMethodFormatter.AppendCodeLine(3, "}"); + + testMethodFormatter.AppendCodeLine(3, "finally"); + testMethodFormatter.AppendCodeLine(3, "{"); + testMethodFormatter.AppendCodeLine(4, "//Cleanup"); + testMethodFormatter.AppendCodeLine(4, "// Add any cleanup code if necessary"); + testMethodFormatter.AppendCodeLine(3, "}"); + testMethodFormatter.AppendCodeLine(0); + + testMethodFormatter.AppendCodeLine(2, "}"); + + await sourceManager.ConstructorsAddAfterAsync(testMethodFormatter.ReturnSource()); + + testMethodFormatter.ResetFormatter(); + } + + return; + } + + /// + /// Formats the repositories test class name. + /// + /// Source interface to convert to the test class name. + /// Formatted test class name or null if it was not found. + private static string FormatTestClassName(this CsInterface source) + { + if (source == null) return null; + + return $"{source.Name.GenerateCSharpFormattedClassName()}Test"; + } + + /// + /// Formats the name of the test method for the contract. + /// + /// Method model to generate the test method name from. + /// Formatted method name or null if it is not found. + private static string FormatTestMethodName(this CsMethod source) + { + if (source == null) return null; + + if (!source.HasParameters) return source.Name; + + StringBuilder testMethodBuilder = new StringBuilder(); + + testMethodBuilder.Append($"{source.Name}By"); + + foreach (var parameter in source.Parameters) + testMethodBuilder.Append(parameter.Name.GenerateCSharpProperCase()); + + return testMethodBuilder.ToString(); + } + + /// + /// Creates a new instance of the 'TestLoader' class. + /// + /// Target project to add the test loader to. + public static async Task CreateTestLoaderAsync(this VsProject testProject) + { + var sourceFile = await testProject.FindCSharpSourceByClassNameAsync("TestLoader", false); + + if (sourceFile != null) return; + + var sourceFormatter = new SourceFormatter(); + + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Configuration;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.DependencyInjection;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Logging;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Logging.Abstractions;"); + sourceFormatter.AppendCodeLine(0, "using System;"); + sourceFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); + sourceFormatter.AppendCodeLine(0, "using System.Linq;"); + sourceFormatter.AppendCodeLine(0, "using System.Text;"); + sourceFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(0, $"namespace {testProject.DefaultNamespace}"); + sourceFormatter.AppendCodeLine(0, "{"); + sourceFormatter.AppendCodeLine(1, "/// "); + sourceFormatter.AppendCodeLine(1, "/// Loader class that is used by testing to load required services."); + sourceFormatter.AppendCodeLine(1, "/// "); + sourceFormatter.AppendCodeLine(1, "public static class TestLoader"); + sourceFormatter.AppendCodeLine(1, "{"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Backing field for the property."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly IConfiguration _configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Backing field for the property."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly IServiceProvider _serviceProvider;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Logging factory used for testing"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly ILoggerFactory _loggerFactory;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Constructor that gets called when the class is accessed for the first time."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "static TestLoader()"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "//Loading the configuration"); + sourceFormatter.AppendCodeLine(3, "var configuration = new ConfigurationBuilder()"); + sourceFormatter.AppendCodeLine(3, " .AddJsonFile(\"appsettings.local.json\", true)"); + sourceFormatter.AppendCodeLine(3, " .AddEnvironmentVariables().Build();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Setting the config property."); + sourceFormatter.AppendCodeLine(3, "_configuration = configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Creating the service container"); + sourceFormatter.AppendCodeLine(3, "var services = new ServiceCollection();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Adding access to configuration from the service container"); + sourceFormatter.AppendCodeLine(3, "services.AddSingleton(Configuration);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Loading the libraries into the service collection."); + sourceFormatter.AppendCodeLine(3, "LoadLibraries(services, _configuration);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "_loggerFactory = new NullLoggerFactory();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "services.AddSingleton(_loggerFactory);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "services.AddLogging();"); + sourceFormatter.AppendCodeLine(3, "//Building the service provider."); + sourceFormatter.AppendCodeLine(3, "_serviceProvider = services.BuildServiceProvider();"); + sourceFormatter.AppendCodeLine(0); + + sourceFormatter.AppendCodeLine(2, "}"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// The loaded configuration to be used with testing."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "public static IConfiguration Configuration => _configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Service provider for the loaded dependency configuration."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "public static IServiceProvider ServiceProvider => _serviceProvider;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Gets the required service to use with testing."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Type of the service to be loaded."); + sourceFormatter.AppendCodeLine(2, "/// Instance of the service"); + sourceFormatter.AppendCodeLine(2, "public static T GetRequiredService() where T : notnull"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "return _serviceProvider.GetRequiredService();"); + + sourceFormatter.AppendCodeLine(2, "}"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Loads the libraries into service collection"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Service collection to load."); + sourceFormatter.AppendCodeLine(2, "/// The configuration to be provided to services."); + sourceFormatter.AppendCodeLine(2, "public static void LoadLibraries(IServiceCollection services, IConfiguration configuration)"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "// Load Default Library Loader."); + sourceFormatter.AppendCodeLine(3, "var libraryLoader = new LibraryLoader();"); + sourceFormatter.AppendCodeLine(3, "libraryLoader.Load(services, configuration);"); + sourceFormatter.AppendCodeLine(2, "}"); + + + sourceFormatter.AppendCodeLine(1, "}"); + sourceFormatter.AppendCodeLine(0, "}"); + + await testProject.AddDocumentAsync("TestLoader.cs", sourceFormatter.ReturnSource().TrimStartEndLines()); + } + + /// + /// Helper method that checks a project to make sure all required project references exist before building a test. + /// + /// Project to check. + /// Optional flag that determines if an exception should be thrown if the project is not configred, default is false. + /// True if configured with NUnit Tests, false if not. + /// Thrown if required data is missing. + public static async Task TestProjectIsConfiguredNUnitAsync(this VsProject project, bool throwError = false) + { + if (project == null) throw new CodeFactoryException("No test project was provided cannot build integration tests."); + + var projectRefs = await project.GetProjectReferencesAsync(); + + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.Logging.Abstractions")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.Logging.Abstractions'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.Configuration")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.Configuration'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.DependencyInjection")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.DependencyInjection'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.VisualStudio.TestPlatform.ObjectModel")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.VisualStudio.TestPlatform.ObjectModel'"); + return false; + } + if (!projectRefs.Any(r => r.Name.ToLower().Contains("nunit."))) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'NUnit'"); + return false; + } + return true; + } + } +} diff --git a/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/XUnit/IntegrationTestBuilder.cs b/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/XUnit/IntegrationTestBuilder.cs new file mode 100644 index 0000000..5eb2bb9 --- /dev/null +++ b/src/Automation/CodeFactory.Automation.NDF.Logic/Testing/XUnit/IntegrationTestBuilder.cs @@ -0,0 +1,573 @@ +using CodeFactory.Automation.NDF.Logic.AspNetCore.Service.Rest.Json; +using CodeFactory.Automation.Standard.Logic.Extensions; +using CodeFactory.WinVs; +using CodeFactory.WinVs.Models.CSharp; +using CodeFactory.WinVs.Models.CSharp.Builder; +using CodeFactory.WinVs.Models.ProjectSystem; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeFactory.Automation.NDF.Logic.Testing.XUnit +{ + /// + /// Automation logic that supports integration testing using the XUnit unit test framework. + /// + public static class IntegrationTestBuilder + { + /// + /// Automation to refresh the integration test implementation. + /// + /// CodeFactory automation access to Visual Studio. + /// The name of the test class to be refreshed. + /// The target contract to implement testing for. + /// The target project the target logic is implemented in. + /// + public static async Task RefreshIntegrationTestAsync(this IVsActions source, string testName, CsInterface contract, VsProject testProject) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the integration test."); + + if (string.IsNullOrEmpty(testName)) throw new CodeFactoryException("No test name was provided cannot update the integration test."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot update the integration test."); + + if (testProject == null) throw new CodeFactoryException($"No test project was provided cannot refresh the integration tests that support the contract '{contract.Name}'"); + + var isTestProject = await testProject.TestProjectIsConfiguredXUnitAsync(true); + + await testProject.CreateTestLoaderAsync(); + + var testClassName = testName; + + if (string.IsNullOrEmpty(testClassName)) throw new CodeFactoryException("Could not load the test class name. Cannot refresh the integration tests."); + + var testSource = await testProject.FindCSharpSourceByClassNameAsync(testClassName, false); + + if (testSource == null) await source.CreateTestAsync(contract, testProject, testClassName); + else await source.UpdateTestAsync(contract, testSource.SourceCode); + + } + + /// + /// Creates a new integration test; + /// + /// CodeFactory automation for Visual Studio. + /// The target interface to be tested. + /// The target project the test should be created in. + /// The name of the target test class. + /// Raised if required data is missing. + public static async Task CreateTestAsync(this IVsActions source, CsInterface contract, VsProject testProject, string testClassName) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the tests."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot create the tests."); + + if (testProject == null) throw new CodeFactoryException($"No test project was provided cannot create the tests that support the contract '{contract.Name}'"); + + if (string.IsNullOrEmpty(testClassName)) throw new CodeFactoryException($"The test class name was not provided cannot create the tests that support the contract '{contract.Name}'"); + + SourceFormatter testFormatter = new SourceFormatter(); + + testFormatter.AppendCodeLine(0, "using System;"); + testFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); + testFormatter.AppendCodeLine(0, "using System.Linq;"); + testFormatter.AppendCodeLine(0, "using System.Linq.Expressions;"); + testFormatter.AppendCodeLine(0, "using System.Text;"); + testFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); + testFormatter.AppendCodeLine(0, "using Xunit;"); + testFormatter.AppendCodeLine(0, "using CodeFactory.NDF;"); + testFormatter.AppendCodeLine(0, $"using {contract.Namespace};"); + testFormatter.AppendCodeLine(0); + + testFormatter.AppendCodeLine(0, $"namespace {testProject.DefaultNamespace}"); + testFormatter.AppendCodeLine(0, "{"); + + testFormatter.AppendCodeLine(1, "/// "); + testFormatter.AppendCodeLine(1, $"/// Integration test class that tests the contract "); + testFormatter.AppendCodeLine(1, "/// "); + testFormatter.AppendCodeLine(1, $"public class {testClassName}"); + testFormatter.AppendCodeLine(1, "{"); + + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"/// The contract being tested."); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"private readonly {contract.Name} _contract;"); + testFormatter.AppendCodeLine(0); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"/// Creates a new instances of the intergration test class for testing."); + testFormatter.AppendCodeLine(2, "/// "); + testFormatter.AppendCodeLine(2, $"public {testClassName}()"); + testFormatter.AppendCodeLine(2, "{"); + testFormatter.AppendCodeLine(3, $"_contract = TestLoader.GetRequiredService<{contract.Name}>();"); + testFormatter.AppendCodeLine(2, "}"); + testFormatter.AppendCodeLine(0); + + testFormatter.AppendCodeLine(1, "}"); + + testFormatter.AppendCodeLine(0, "}"); + + var doc = await testProject.AddDocumentAsync($"{testClassName}.cs", testFormatter.ReturnSource().TrimStartEndLines()); + + var testSourceCode = await doc.GetCSharpSourceModelAsync(); + + await source.UpdateTestAsync(contract, testSourceCode); + } + + /// + /// Updates a test class that performs intergration tests on the target contract. + /// + /// CodeFactory automation for visual studio. + /// The target contract that is being tested. + /// The target source code that is to be updated. + /// Throw if required data is missing. + public static async Task UpdateTestAsync(this IVsActions source, CsInterface contract, CsSource testClassSource) + { + if (source == null) throw new CodeFactoryException("Could not access the CodeFactory automation for visual studio cannot refresh the integration tests."); + + if (contract == null) throw new CodeFactoryException("No contract was provided cannot update the integration tests."); + + if (testClassSource == null) throw new CodeFactoryException($"The test class source was not provided cannot update the integration tests that support the contract '{contract.Name}'"); + + var currentSource = testClassSource; + + var testClass = currentSource.Classes.FirstOrDefault(); + + if (testClass == null) throw new CodeFactoryException($"The test class could not be loaded from the provided source. cannot update the integrationn tests that support the contract '{contract.Name}'"); + + var contractMethods = contract.GetAllInterfaceMethods(); + + if (!contractMethods.Any()) return; + + var sourceMethods = testClass.Methods ?? new List(); + + var AddTests = new List(); + + foreach (var contractMethod in contractMethods) + { + var testMethodName = contractMethod.FormatTestMethodName(); + + if (string.IsNullOrEmpty(testMethodName)) continue; + + if (!sourceMethods.Any(m => m.Name == testMethodName)) AddTests.Add(contractMethod); + } + + if (!AddTests.Any()) return; + + var sourceManager = new SourceClassManager(currentSource, testClass, source); + + foreach (var addMethod in AddTests) + { + StringBuilder parameterBuilder = new StringBuilder(); + var testMethodFormatter = new SourceFormatter(); + + testMethodFormatter.AppendCodeLine(0); + testMethodFormatter.AppendCodeLine(2, "/// "); + testMethodFormatter.AppendCodeLine(2, $"/// Integration test that tests the contract method \"{addMethod.Name}\""); + testMethodFormatter.AppendCodeLine(2, "/// "); + testMethodFormatter.AppendCodeLine(2, "[Fact]"); + + if (addMethod.IsAsync()) testMethodFormatter.AppendCodeLine(2, $"public async Task {addMethod.FormatTestMethodName()}()"); + else testMethodFormatter.AppendCodeLine(2, $"public void {addMethod.FormatTestMethodName()}()"); + testMethodFormatter.AppendCodeLine(2, "{"); + + // Build Arrange Section. + testMethodFormatter.AppendCode(ArrangeSectionBuilder(addMethod, AddTests)); + + testMethodFormatter.AppendCodeLine(3, "try"); + testMethodFormatter.AppendCodeLine(3, "{"); + + // Build Act Section + testMethodFormatter.AppendCode(ActSectionBuilder(addMethod, AddTests)); + testMethodFormatter.AppendCodeLine(0); + + // Build Assert Section. + testMethodFormatter.AppendCode(AssertSectionBuilder(addMethod, AddTests)); + + testMethodFormatter.AppendCodeLine(3, "}"); + testMethodFormatter.AppendCodeLine(3, "catch (Exception unhandled)"); + testMethodFormatter.AppendCodeLine(3, "{"); + testMethodFormatter.AppendCodeLine(4, "Assert.Fail($\"The following unhandled exception occurred '{unhandled.Message}' \");"); + testMethodFormatter.AppendCodeLine(3, "}"); + + testMethodFormatter.AppendCodeLine(3, "finally"); + testMethodFormatter.AppendCodeLine(3, "{"); + + // Build Finally Clause section. + testMethodFormatter.AppendCode(FinallyClauseSectionBuilder(addMethod, AddTests)); + + testMethodFormatter.AppendCodeLine(3, "}"); + testMethodFormatter.AppendCodeLine(0); + + testMethodFormatter.AppendCodeLine(2, "}"); + + await sourceManager.ConstructorsAddAfterAsync(testMethodFormatter.ReturnSource()); + + testMethodFormatter.ResetFormatter(); + } + + return; + } + + /// + /// Builds the Arange body contents of a test. + /// + /// Source formatter of the test. + /// Formatted test contents. + private static string ArrangeSectionBuilder(CsMethod method, List allMethods) + { + SourceFormatter source = new SourceFormatter(); + if (method == null) return source.ReturnSource(); + + var addMethod = allMethods.FirstOrDefault(x => x.Name.ToLower() == "addasync"); + + // Initialize parameter variables + source.AppendCodeLine(3, "// Arrange"); + source = method.GenerateNewTestObjectsFromParameters(source); + + // Initialize objects depending on CRUD methods. + if (method.Name.ToLower().StartsWith("getasync")) + { + // If AddAsync exist, new up an object so that we can use it later as a parameter to add a test record. + if (addMethod != null) + { + source = addMethod.GenerateNewTestObjectsFromParameters(source); + } + } + + // Initialize the result object + if (method.HasReturnType()) + { + var defaultValue = method.GetReturnType().GenerateCSharpDefaultValue(); + + source.AppendCodeLine(3, defaultValue != null + ? $"{method.GetReturnType().GenerateCSharpTypeName()}? result = new {method.GetReturnType().GenerateCSharpTypeName()}();" + : $"{method.GetReturnType().GenerateCSharpTypeName()} result;"); + source.AppendCodeLine(4); + } + + return source.ReturnSource().TrimEndLines(); + } + + /// + /// Builds the Act body contents of a test. + /// + /// Source formatter of the test. + /// Formatted test contents. + private static string ActSectionBuilder(CsMethod method, List allMethods) + { + SourceFormatter source = new SourceFormatter(); + if (method == null) return source.ReturnSource(); + + var addMethod = allMethods.FirstOrDefault(x => x.Name.ToLower() == "addasync"); + var getMethod = allMethods.FirstOrDefault(x => x.Name.ToLower().StartsWith("getasync")); + var updateMethod = allMethods.FirstOrDefault(x => x.Name.ToLower() == "updateasync"); + + if (!method.HasReturnType()) + return source.ReturnSource(); + + source.AppendCodeLine(4, "// Act"); + + // If AddAsync exist, call AddAsync to create a test record. + if (addMethod != null && method.Name.ToLower() == "addasync") + { + source.AppendCodeLine(4, $"result = await _contract.AddAsync({addMethod.GenerateParameterListAsCamelCased()});"); + } + + // If GetAsync exist, call GetAsync to get a record. + else if (getMethod != null && method.Name.ToLower() == "getasync" && addMethod != null) + { + source.AppendCodeLine(4, $"result = await _contract.AddAsync({addMethod.GenerateParameterListAsCamelCased()});"); + source.AppendCodeLine(4, $"result = await _contract.GetAsync({getMethod.GenerateParameterListAsProperCased()});"); + } + + // If AddAsync exist, call AddAsync to create a test record. + else if (updateMethod != null && method.Name.ToLower() == "updateasync" && addMethod != null) + { + source.AppendCodeLine(4, $"result = await _contract.AddAsync({addMethod.GenerateParameterListAsCamelCased()});"); + source.AppendCodeLine(4, $"result.{updateMethod.Parameters[0].ParameterType.GetFirstStringTypePropertyName()} += \" Update\";"); + source.AppendCodeLine(4, $"result = await _contract.UpdateAsync(result);"); + } + else + { + source.AppendCodeLine(4, method.HasReturnType() + ? $"result = {method.GenerateStringAwaitStatement()}_contract.{method.Name}({method.GenerateParameterListAsCamelCased()});" + : $"{method.GenerateStringAwaitStatement()}_contract.{method.Name}({method.GenerateParameterListAsCamelCased()});"); + + } + + source.AppendCodeLine(0); + return source.ReturnSource().TrimEndLines(); + } + + /// + /// Builds the Assert body contents of a test. + /// + /// Source formatter of the test. + /// Formatted test contents. + private static string AssertSectionBuilder(CsMethod method, List allMethods) + { + SourceFormatter source = new SourceFormatter(); + if (method == null) return source.ReturnSource(); + + var addMethod = allMethods.FirstOrDefault(x => x.Name.ToLower() == "addasync"); + var updateMethod = allMethods.FirstOrDefault(x => x.Name.ToLower() == "updateasync"); + var getMethod = allMethods.FirstOrDefault(x => x.Name.ToLower() == "getasync"); + + if (method.HasReturnType()) + { + source.AppendCodeLine(0); + source.AppendCodeLine(4, "// Assert"); + source.AppendCodeLine(4, "Assert.NotNull(result);"); + } + else + { + source.AppendCodeLine(4, "// Act and Assert"); + + // If DeleteAsync + if (method.Name.ToLower() == "deleteasync") + { + if (addMethod != null) + { + source.AppendCodeLine(4, $"var result = await _contract.AddAsync({addMethod.GenerateParameterListAsCamelCased()});"); + source.AppendCodeLine(4, $"await _contract.DeleteAsync(result);"); + source.AppendCodeLine(0); + } + } + + source.AppendCodeLine(4, $"await Assert.ThrowsAsync({getMethod.GenerateStringAsyncStatement()} ()=> {getMethod.GenerateStringAwaitStatement()}_contract.{getMethod.Name}({getMethod.GenerateParameterListAsProperCased()}));"); + } + + // If AddAsync, check the result to make sure the id is not 0. + if (addMethod != null && method.Name.ToLower() == "addasync") + { + source.AppendCodeLine(4, $"Assert.NotEqual(0, result.{addMethod.Parameters[0].ParameterType.GetPrimaryKeyName()});"); + } + + // If UpdateAsync, check that the result of the update call during the Act was updated with a " update" text on the first string property. + if (updateMethod != null && method.Name.ToLower() == "updateasync") + { + source.AppendCodeLine(4, $"Assert.Equal({updateMethod.Parameters[0].Name.GenerateCSharpCamelCase()}.{updateMethod.Parameters[0].ParameterType.GetFirstStringTypePropertyName()} + \" Update\", result.{updateMethod.Parameters[0].ParameterType.GetFirstStringTypePropertyName()});"); + } + + return source.ReturnSource().TrimStartEndLines(); + } + + /// + /// Builds the body for the finally clause in the try catch. + /// + /// Source formatter of the test. + /// Formatted test contents. + private static string FinallyClauseSectionBuilder(CsMethod method, List allMethods) + { + SourceFormatter source = new SourceFormatter(); + if (method == null) return source.ReturnSource(); + + source.AppendCodeLine(4, $"// Cleanup"); + var deleteMethod = allMethods.FirstOrDefault(x => x.Name.ToLower() == "deleteasync"); + + // Add additional assert checks. + if (method.Name.ToLower() == "updateasync" || method.Name.ToLower() == "getasync" || method.Name.ToLower() == "addasync") + { + // Delete the record that was created as a test, if one exist. + source.AppendCodeLine(4, $"await _contract.DeleteAsync(result!);"); + } + + source.AppendCodeLine(0); + source.AppendCodeLine(4, $"// Add any cleanup code if necessary"); + + return source.ReturnSource().TrimEndLines(); + } + + /// + /// Formats the repositories test class name. + /// + /// Source interface to convert to the test class name. + /// Formatted test class name or null if it was not found. + private static string FormatTestClassName(this CsInterface source) + { + if (source == null) return null; + + return $"{source.Name.GenerateCSharpFormattedClassName()}Test"; + } + + /// + /// Formats the name of the test method for the contract. + /// + /// Method model to generate the test method name from. + /// Formatted method name or null if it is not found. + private static string FormatTestMethodName(this CsMethod source) + { + if (source == null) return null; + + if (!source.HasParameters) return source.Name; + + StringBuilder testMethodBuilder = new StringBuilder(); + + testMethodBuilder.Append($"{source.Name}By"); + + foreach (var parameter in source.Parameters) + testMethodBuilder.Append(parameter.Name.GenerateCSharpProperCase()); + + return testMethodBuilder.ToString(); + } + + /// + /// Creates a new instance of the 'TestLoader' class. + /// + /// Target project to add the test loader to. + public static async Task CreateTestLoaderAsync(this VsProject testProject) + { + var sourceFile = await testProject.FindCSharpSourceByClassNameAsync("TestLoader", false); + + if (sourceFile != null) return; + + var sourceFormatter = new SourceFormatter(); + + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Configuration;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.DependencyInjection;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Logging;"); + sourceFormatter.AppendCodeLine(0, "using Microsoft.Extensions.Logging.Abstractions;"); + sourceFormatter.AppendCodeLine(0, "using System;"); + sourceFormatter.AppendCodeLine(0, "using System.Collections.Generic;"); + sourceFormatter.AppendCodeLine(0, "using System.Linq;"); + sourceFormatter.AppendCodeLine(0, "using System.Text;"); + sourceFormatter.AppendCodeLine(0, "using System.Threading.Tasks;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(0, $"namespace {testProject.DefaultNamespace}"); + sourceFormatter.AppendCodeLine(0, "{"); + sourceFormatter.AppendCodeLine(1, "/// "); + sourceFormatter.AppendCodeLine(1, "/// Loader class that is used by testing to load required services."); + sourceFormatter.AppendCodeLine(1, "/// "); + sourceFormatter.AppendCodeLine(1, "public static class TestLoader"); + sourceFormatter.AppendCodeLine(1, "{"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Backing field for the property."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly IConfiguration _configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Backing field for the property."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly IServiceProvider _serviceProvider;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Logging factory used for testing"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "private static readonly ILoggerFactory _loggerFactory;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Constructor that gets called when the class is accessed for the first time."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "static TestLoader()"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "//Loading the configuration"); + sourceFormatter.AppendCodeLine(3, "var configuration = new ConfigurationBuilder()"); + sourceFormatter.AppendCodeLine(3, " .AddJsonFile(\"appsettings.local.json\", true)"); + sourceFormatter.AppendCodeLine(3, " .AddEnvironmentVariables().Build();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Setting the config property."); + sourceFormatter.AppendCodeLine(3, "_configuration = configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Creating the service container"); + sourceFormatter.AppendCodeLine(3, "var services = new ServiceCollection();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Adding access to configuration from the service container"); + sourceFormatter.AppendCodeLine(3, "services.AddSingleton(Configuration);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "//Loading the libraries into the service collection."); + sourceFormatter.AppendCodeLine(3, "LoadLibraries(services, _configuration);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "_loggerFactory = new NullLoggerFactory();"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "services.AddSingleton(_loggerFactory);"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(3, "services.AddLogging();"); + sourceFormatter.AppendCodeLine(3, "//Building the service provider."); + sourceFormatter.AppendCodeLine(3, "_serviceProvider = services.BuildServiceProvider();"); + sourceFormatter.AppendCodeLine(0); + + sourceFormatter.AppendCodeLine(2, "}"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// The loaded configuration to be used with testing."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "public static IConfiguration Configuration => _configuration;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Service provider for the loaded dependency configuration."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "public static IServiceProvider ServiceProvider => _serviceProvider;"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Gets the required service to use with testing."); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Type of the service to be loaded."); + sourceFormatter.AppendCodeLine(2, "/// Instance of the service"); + sourceFormatter.AppendCodeLine(2, "public static T GetRequiredService() where T : notnull"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "return _serviceProvider.GetRequiredService();"); + + sourceFormatter.AppendCodeLine(2, "}"); + sourceFormatter.AppendCodeLine(0); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Loads the libraries into service collection"); + sourceFormatter.AppendCodeLine(2, "/// "); + sourceFormatter.AppendCodeLine(2, "/// Service collection to load."); + sourceFormatter.AppendCodeLine(2, "/// The configuration to be provided to services."); + sourceFormatter.AppendCodeLine(2, "public static void LoadLibraries(IServiceCollection services, IConfiguration configuration)"); + sourceFormatter.AppendCodeLine(2, "{"); + sourceFormatter.AppendCodeLine(3, "// Load Default Library Loader."); + sourceFormatter.AppendCodeLine(3, "var libraryLoader = new LibraryLoader();"); + sourceFormatter.AppendCodeLine(3, "libraryLoader.Load(services, configuration);"); + sourceFormatter.AppendCodeLine(2, "}"); + + + sourceFormatter.AppendCodeLine(1, "}"); + sourceFormatter.AppendCodeLine(0, "}"); + + await testProject.AddDocumentAsync("TestLoader.cs", sourceFormatter.ReturnSource().TrimStartEndLines()); + } + + /// + /// Helper method that checks a project to make sure all required project references exist before building a test. + /// + /// Project to check. + /// Optional flag that determines if an exception should be thrown if the project is not configred, default is false. + /// True if configured with xUnit Tests, false if not. + /// Thrown if required data is missing. + public static async Task TestProjectIsConfiguredXUnitAsync(this VsProject project, bool throwError = false) + { + if (project == null) throw new CodeFactoryException("No test project was provided cannot build integration tests."); + + var projectRefs = await project.GetProjectReferencesAsync(); + + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.Logging.Abstractions")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.Logging.Abstractions'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.Configuration")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.Configuration'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.Extensions.DependencyInjection")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.Extensions.DependencyInjection'"); + return false; + } + if (!projectRefs.Any(r => r.Name == "Microsoft.VisualStudio.TestPlatform.ObjectModel")) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'Microsoft.VisualStudio.TestPlatform.ObjectModel'"); + return false; + } + if (!projectRefs.Any(r => r.Name.Contains("xunit."))) + { + if (throwError) throw new CodeFactoryException("The test project must reference 'xunit'"); + return false; + } + return true; + } + } +} diff --git a/src/Automation/CodeFactory.Automation.Standard.Logic/CodeFactory.Automation.Standard.Logic.csproj b/src/Automation/CodeFactory.Automation.Standard.Logic/CodeFactory.Automation.Standard.Logic.csproj index 32ab3ec..025ebf0 100644 --- a/src/Automation/CodeFactory.Automation.Standard.Logic/CodeFactory.Automation.Standard.Logic.csproj +++ b/src/Automation/CodeFactory.Automation.Standard.Logic/CodeFactory.Automation.Standard.Logic.csproj @@ -50,18 +50,27 @@ - - + + + + + + + + + + - + - + - + +