From 7e96d914e355309309d20d610fcf31933108f67c Mon Sep 17 00:00:00 2001 From: Roger Sears Date: Thu, 6 Jun 2024 10:35:37 -0400 Subject: [PATCH 1/2] COMMANDS Renamed commands with a suffix of Command. Added another test command for xUnit tests. Renamed the previous MSTest-related test to MSTest in all of it's references. Created an Extensions Folder in the Standards.Logic project, added several new extensions. Updated RefreshEFRepositoryCommand, where new config was added that gives the developer an option to ONLY create the CRUD operations, or they can also generate a default QueryAsync method and GetAsync method. QueryAsync currently will return back the first X number of records. In a later update of this method, we will add all non Date and primary key field properties as parameters. We could also use an Expression function to allow for more dynamic IQueryable functionality. The GetAsync method always returns the record, using the First property (usually this will be the primary key) and query the repository for that record. ANGULAR SUPPORT Added a new local model and service builder. These will generate a local typescript model from a csharp model, implementing a default constructor with default values. (this is currently being used, but several changes were made in this last revision - not fully tested, proceed with caution. Additional Typescript converters were created. Still in an early stage. TESTING Support for MSTest, Bunit, xUnit, and nUnit have been implemented. More love has been provided for generation of xUnit Tests. Still in testing, proceed with caution. EXTENSIONS Several new extensions were added, existing extensions were renamed and added to the new Extensions folder. Calling this one out specifically: I added several String extension methods that do various different things. You can now TrimStart, TrimEnd, TrimStartEnd, Pluralize, Prettify (breaks up a propercased string into string with spaces), and provided conversion from proper casing to kebab-case and snake_case. TrimStartEndLines() I did not implement this everywhere, but in the current state of code generation, you get source files with extra blank lines at the beginning and end. When you generate POCO models, there are a lot of extra blank lines within the class body, before and after each property. In another version of this code, every time I see .ReturnSource() I append .ReturnSource().TrimStartEndLines(). You can see this in the RefreshEFRepositoryCommand and in the RepositoryBuilder. The long-term solution here should be that the SDK automatically trims extra lines when the source formatter is returning the source. ENUMS I wrote additional extension methods to differentiate a VsDocument (so any file) with IsMarkupCode. This will let you know if the file you are dealing with is a view/presentation layer (html, cshtml, razor, xaml, etc). There are other VSDocument extensions that tell you what UI Frameworks are being used, based on project references, and what file types you're dealing with. So in the inverse, enums were created to let you dictate/manage your list of UI Frameworks I added several enums --- ...rs.cs => AddMissingLogicMembersCommand.cs} | 6 +- ... => AddMissingRepositoryMembersCommand.cs} | 13 +- .../CSharpFile/RefreshEFRepository.cs | 529 ---------- .../CSharpFile/RefreshEFRepositoryCommand.cs | 603 +++++++++++ ...n.cs => RefreshFluentValidationCommand.cs} | 22 +- ...RefreshLogic.cs => RefreshLogicCommand.cs} | 16 +- ...RefreshTest.cs => RefreshMSTestCommand.cs} | 37 +- ...ervice.cs => RefreshRestServiceCommand.cs} | 18 +- .../CSharpFile/RefreshXUnitTestCommand .cs | 179 ++++ ...cs => UpdateLogicImplementationCommand.cs} | 16 +- ...rchitecture.AspNetCore.Service.Rest.csproj | 26 +- ...cs => LoadExternalConfigurationCommand.cs} | 49 +- ...cs => RegisterTransientServicesCommand.cs} | 17 +- .../Solution/CountLinesOfCodeCommand.cs | 111 ++ ...> CreateAutomationConfigurationCommand.cs} | 17 +- ...> ReloadAutomationConfigurationCommand.cs} | 17 +- .../Service/Rest/Json/MethodExtensions.cs | 325 +++++- .../Json/RestJsonCSharpAbstractionBuilder.cs | 31 +- .../Rest/Json/RestJsonServiceBuilder.cs | 18 +- .../CodeFactory.Automation.NDF.Logic.csproj | 6 + .../Data/Sql/EF/RepositoryBuilder.cs | 951 +++++++++--------- .../DependencyInjectionBuilder.cs | 16 +- .../General/FluentValidationBuilder.cs | 18 +- .../General/ModelBuilder.cs | 644 ++++++------ .../Node/Angular/LocalModelBuilder.cs | 53 + .../Node/Angular/LocalServiceBuilder.cs | 171 ++++ .../Node/Angular/TypescriptConverter.cs | 128 +++ .../ProjectExtensions.cs | 17 +- .../Testing/BUnit/IntegrationTestBuilder.cs | 462 +++++++++ .../Testing/MSTest/IntegrationTestBuilder.cs | 20 +- .../Testing/NUnit/IntegrationTestBuilder.cs | 461 +++++++++ .../Testing/XUnit/IntegrationTestBuilder.cs | 573 +++++++++++ ...deFactory.Automation.Standard.Logic.csproj | 19 +- .../Enums/UIFrameworkEnum.cs | 96 ++ .../{ => Extensions}/CsClassExtensions.cs | 13 +- .../{ => Extensions}/CsInterfaceExtensions.cs | 12 +- .../Extensions/CsMethodExtensions.cs | 222 ++++ .../Extensions/CsTypeExtensions.cs | 147 +++ .../NameManagementExtensions.cs | 16 +- .../SourceManagerExtensions.cs | 18 +- .../Extensions/StringExtensions.cs | 232 +++++ .../Extensions/VsDocumentExtensions.cs | 208 ++++ .../Extensions/VsModelExtensions.cs | 22 + .../VsProjectExtensions.cs} | 35 +- .../Extensions/VsProjectFolderExtensions.cs | 81 ++ .../Extensions/VsSolutionExtensions.cs | 37 + .../FluentValidationBuilder.cs | 16 +- .../POCOBuilder.cs | 16 +- .../PropertyBuilderTransformNullabeTypes.cs | 11 +- 49 files changed, 5167 insertions(+), 1604 deletions(-) rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/{AddMissingLogicMembers.cs => AddMissingLogicMembersCommand.cs} (97%) rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/{AddMissingRepositoryMembers.cs => AddMissingRepositoryMembersCommand.cs} (96%) delete mode 100644 src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepository.cs create mode 100644 src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepositoryCommand.cs rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/{RefreshFluentValidation.cs => RefreshFluentValidationCommand.cs} (92%) rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/{RefreshLogic.cs => RefreshLogicCommand.cs} (96%) rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/{RefreshTest.cs => RefreshMSTestCommand.cs} (89%) rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/{RefreshRestService.cs => RefreshRestServiceCommand.cs} (95%) create mode 100644 src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshXUnitTestCommand .cs rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/{UpdateLogicImplementation.cs => UpdateLogicImplementationCommand.cs} (96%) rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/IDE/{LoadExternalConfiguration.cs => LoadExternalConfigurationCommand.cs} (61%) rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Project/{RegisterTransientServices.cs => RegisterTransientServicesCommand.cs} (87%) create mode 100644 src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/CountLinesOfCodeCommand.cs rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/{CreateAutomationConfiguration.cs => CreateAutomationConfigurationCommand.cs} (86%) rename src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/Solution/{ReloadAutomationConfiguration.cs => ReloadAutomationConfigurationCommand.cs} (86%) create mode 100644 src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/LocalModelBuilder.cs create mode 100644 src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/LocalServiceBuilder.cs create mode 100644 src/Automation/CodeFactory.Automation.NDF.Logic/Node/Angular/TypescriptConverter.cs create mode 100644 src/Automation/CodeFactory.Automation.NDF.Logic/Testing/BUnit/IntegrationTestBuilder.cs create mode 100644 src/Automation/CodeFactory.Automation.NDF.Logic/Testing/NUnit/IntegrationTestBuilder.cs create mode 100644 src/Automation/CodeFactory.Automation.NDF.Logic/Testing/XUnit/IntegrationTestBuilder.cs create mode 100644 src/Automation/CodeFactory.Automation.Standard.Logic/Enums/UIFrameworkEnum.cs rename src/Automation/CodeFactory.Automation.Standard.Logic/{ => Extensions}/CsClassExtensions.cs (84%) rename src/Automation/CodeFactory.Automation.Standard.Logic/{ => Extensions}/CsInterfaceExtensions.cs (92%) create mode 100644 src/Automation/CodeFactory.Automation.Standard.Logic/Extensions/CsMethodExtensions.cs create mode 100644 src/Automation/CodeFactory.Automation.Standard.Logic/Extensions/CsTypeExtensions.cs rename src/Automation/CodeFactory.Automation.Standard.Logic/{ => Extensions}/NameManagementExtensions.cs (86%) rename src/Automation/CodeFactory.Automation.Standard.Logic/{ => Extensions}/SourceManagerExtensions.cs (76%) create mode 100644 src/Automation/CodeFactory.Automation.Standard.Logic/Extensions/StringExtensions.cs create mode 100644 src/Automation/CodeFactory.Automation.Standard.Logic/Extensions/VsDocumentExtensions.cs create mode 100644 src/Automation/CodeFactory.Automation.Standard.Logic/Extensions/VsModelExtensions.cs rename src/Automation/CodeFactory.Automation.Standard.Logic/{ProjectExtensions.cs => Extensions/VsProjectExtensions.cs} (62%) create mode 100644 src/Automation/CodeFactory.Automation.Standard.Logic/Extensions/VsProjectFolderExtensions.cs create mode 100644 src/Automation/CodeFactory.Automation.Standard.Logic/Extensions/VsSolutionExtensions.cs 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..154a13d --- /dev/null +++ b/src/Architecture/CodeFactory.Architecture.AspNetCore.Service.Rest/CSharpFile/RefreshEFRepositoryCommand.cs @@ -0,0 +1,603 @@ +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"; + + /// + /// Optional, flag to generate default Get operations. + /// + public static string GenerateGetOperations = "GenerateGetOperations"; + + /// + /// Optional, flag to generate default GetAll operations. + /// + public static string GenerateQueryOperations = "GenerateQueryOperations"; + + /// + /// Default page size for records returned in a service response. + /// + public static string DefaultPageSize = "DefaultPageSize"; + + /// + /// 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" + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = GenerateGetOperations, + Guidance = "True by default. Defines whether the initial contracts include a method to Get a single record using the primary key as the default parameter.", + Value = "true" + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = GenerateQueryOperations, + Guidance = "False by default. Defines whether the initial contracts include a method to Query records for the entity type.", + Value = "false" + } + ) + .AddParameter + ( + new ConfigParameter + { + Name = DefaultPageSize, + Guidance = "100 records by default. Defines the default number of records to be returned for IQueryable operations.", + Value = "100" + } + ) + ) + .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)); + bool generateGetOperations = bool.Parse(command.Project(RepoContractProject).ParameterValue(GenerateGetOperations)); + bool generateQueryOperations = bool.Parse(command.Project(RepoContractProject).ParameterValue(GenerateQueryOperations)); + int defaultPageSize = int.Parse(command.Project(RepoContractProject).ParameterValue(DefaultPageSize)); + + var repoClass = await VisualStudioActions.RefreshEFRepositoryWithGetsAsync(repositoryName, efModel, repoProject, contractProject, appModel, + contextClass, supportsNDF, supportsLogging, repoFolder, contractFolder, generateCrudOperations, generateGetOperations, generateQueryOperations, defaultPageSize); + + 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..ea8e9e5 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,505 @@ -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; - - default: - injectFormatter.AppendCodeLine(0); - break; - } - - injectFormatter.AppendCodeLine(0, "}"); - - string syntax = injectFormatter.ReturnSource(); - await methodBuilder.InjectMethodAsync(missingMethod, repoManager, 2, syntax: syntax,defaultLogLevel:logLevel); - - injectFormatter.ResetFormatter(); - } - - 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; - - } - } + /// + /// 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 RefreshEFRepositoryWithGetsAsync(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, bool generateGetOperations = true, bool generateQueryOperations = false, int defaultPageSize = 100, 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, generateGetOperations, generateQueryOperations, defaultPageSize)) ?? 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, bool generateGetOperations = true, bool generateQueryOperations = false, int defaultPageSize = 100) + { + 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()});"); + } + + if (generateQueryOperations) + { + contractFormatter.AppendCodeLine(2); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"/// Query {poco?.Name.Pluralize()} for model."); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"Task> QueryAsync(int? pageSize = {defaultPageSize}, int? pageNumber = 1);"); + } + + if (generateGetOperations) + { + contractFormatter.AppendCodeLine(2); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"/// Gets a {poco?.Name} based on an id, ."); + contractFormatter.AppendCodeLine(2, "/// "); + contractFormatter.AppendCodeLine(2, $"Task<{poco?.Name}> GetAsync({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; + + case "GetAsync": + injectFormatter.AppendCodeLine(1, $"result = new {poco.Name}();"); + injectFormatter.AppendCodeLine(1, $"var model = context.{efEntity.Name.Pluralize()}.FirstOrDefault(m => m.{poco.Properties[0].Name} == {poco.Properties[0].Name.GenerateCSharpCamelCase()});"); + injectFormatter.AppendCodeLine(1, "result = model!.CreatePocoModel();"); + break; + + case "QueryAsync": + injectFormatter.AppendCodeLine(1, $"var models = context.Set<{efEntity.Name}>().AsNoTracking()"); + injectFormatter.AppendCodeLine(2, $".Skip(pageNumber - 1) * pageSize)"); + injectFormatter.AppendCodeLine(2, $".Take(pageSize).ToList();"); + injectFormatter.AppendCodeLine(1, "if (models.Any())"); + injectFormatter.AppendCodeLine(1, "{"); + injectFormatter.AppendCodeLine(2, $"result = new List<{poco.Name}>();"); + injectFormatter.AppendCodeLine(2, $"foreach({efEntity.Name} {efEntity.Name.GenerateCSharpCamelCase()} in models)"); + injectFormatter.AppendCodeLine(2, "{"); + injectFormatter.AppendCodeLine(3, $"result.Add({efEntity.Name.GenerateCSharpCamelCase()}.CreatePocoModel());"); + injectFormatter.AppendCodeLine(2, "}"); + injectFormatter.AppendCodeLine(1, "}"); + break; + default: + injectFormatter.AppendCodeLine(0); + break; + } + + injectFormatter.AppendCodeLine(0, "}"); + + string syntax = injectFormatter.ReturnSource().TrimStartEndLines(); + await methodBuilder.InjectMethodAsync(missingMethod, repoManager, 2, syntax: syntax, defaultLogLevel: logLevel); + + injectFormatter.ResetFormatter(); + } + + 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; + + } + } } 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 @@ - - + + + + + + + + + + - + - + - + +