From 2d01c2e8927ee53ac3e1c829e402ac95953fd071 Mon Sep 17 00:00:00 2001 From: Piotr Date: Thu, 22 Sep 2022 14:28:23 -0400 Subject: [PATCH 01/39] work in progress Signed-off-by: Piotr --- samples/My.Hr/My.Hr.Functions/README.md | 1 + .../DatabaseServiceCollectionExtensions.cs | 2 +- src/Templates/readme.md | 53 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/Templates/readme.md diff --git a/samples/My.Hr/My.Hr.Functions/README.md b/samples/My.Hr/My.Hr.Functions/README.md index df14d20f..eeda67a3 100644 --- a/samples/My.Hr/My.Hr.Functions/README.md +++ b/samples/My.Hr/My.Hr.Functions/README.md @@ -21,6 +21,7 @@ Sample configuration for `local.settings.json` "VerificationResultsQueueName": "verificationResults", "ServiceBusConnection__fullyQualifiedNamespace": "coreex.servicebus.windows.net", + "AzureWebJobs.ServiceBusExecuteVerificationFunction.Disabled": true, // disable when service bus is not available "HttpLogContent": "true", "AzureFunctionsJobHost__logging__logLevel__CoreEx": "Debug", diff --git a/src/CoreEx.Database/DatabaseServiceCollectionExtensions.cs b/src/CoreEx.Database/DatabaseServiceCollectionExtensions.cs index 16f89f17..14ffbc33 100644 --- a/src/CoreEx.Database/DatabaseServiceCollectionExtensions.cs +++ b/src/CoreEx.Database/DatabaseServiceCollectionExtensions.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.DependencyInjection { /// - /// Provides extenstion methods. + /// Provides extension methods. /// public static class DatabaseServiceCollectionExtensions { diff --git a/src/Templates/readme.md b/src/Templates/readme.md new file mode 100644 index 00000000..004623be --- /dev/null +++ b/src/Templates/readme.md @@ -0,0 +1,53 @@ +# Temp readme file + +## How to create templates + +* [Samples](https://github.com/dotnet/samples/tree/main/core/tutorials/cli-templates-create-item-template) +* [Wiki](https://github.com/dotnet/templating/wiki) +* [Docs](https://learn.microsoft.com/en-us/dotnet/core/tools/custom-templates) +* [Tutorial](https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-item-template) +* [NTangle template](https://github.com/Avanade/NTangle/tree/main/tools/NTangle.Template) + +## Dev container + +Add [Dev Container](https://code.visualstudio.com/docs/remote/create-dev-container#_use-docker-compose) with docker-compose support that would run the solution. + +Extensions required: + +* azure functions +* function tools +* az CLI +* Pulumi CLI +* dotnet SDK +* Pulumi VS Extension +* Azurite Extension +* REST Client + +Expose ports for function, app service and sql server + +## Update readme to use REST Client + Create: [POST] http://localhost:7071/api/api/employees + + Delete: [DELETE] http://localhost:7071/api/api/employees/{id} + + Get: [GET] http://localhost:7071/api/api/employees/{id} + + GetAll: [GET] http://localhost:7071/api/api/employees + + HealthInfo: [GET] http://localhost:7071/api/health + + HttpTriggerQueueVerificationFunction: [POST] http://localhost:7071/api/employee/verify + + Patch: [PATCH] http://localhost:7071/api/api/employees/{id} + + RenderOAuth2Redirect: [GET] http://localhost:7071/api/oauth2-redirect.html + + RenderOpenApiDocument: [GET] http://localhost:7071/api/openapi/{version}.{extension} + + RenderSwaggerDocument: [GET] http://localhost:7071/api/swagger.{extension} + + RenderSwaggerUI: [GET] http://localhost:7071/api/swagger/ui + + Update: [PUT] http://localhost:7071/api/api/employees/{id} + +## Add file that contains recommended VS CODE extensions From 2b2a39ab208aa04129d2281ff652caec2efcf331 Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 23 Sep 2022 14:22:18 -0400 Subject: [PATCH 02/39] init commit for templates Signed-off-by: Piotr --- .../content/.devcontainer/Dockerfile | 12 + .../content/.devcontainer/devcontainer.json | 44 ++ .../content/.devcontainer/devinit.json | 0 .../content/.template.config/template.json | 125 ++++++ .../Company.AppName.Api.csproj | 27 ++ .../Controllers/EmployeeController.cs | 89 ++++ .../Controllers/HealthController.cs | 23 + .../Controllers/ReferenceDataController.cs | 41 ++ .../Controllers/SwaggerController.cs | 19 + .../content/Company.AppName.Api/Dockerfile | 44 ++ .../Company.AppName.Api/ImplicitUsings.cs | 23 + .../content/Company.AppName.Api/Program.cs | 1 + .../Properties/launchSettings.json | 31 ++ .../content/Company.AppName.Api/Startup.cs | 83 ++++ .../appsettings.Development.json | 14 + .../Company.AppName.Api/appsettings.json | 8 + .../Company.AppName.Business.csproj | 22 + .../Data/EmployeeConfiguration.cs | 21 + .../Company.AppName.Business/Data/HrDb.cs | 11 + .../Data/HrDbContext.cs | 28 ++ .../Company.AppName.Business/Data/HrEfDb.cs | 33 ++ .../Data/UsStateConfiguration.cs | 23 + .../External/AgifyServiceClient.cs | 32 ++ .../External/Contracts/AgifyResponse.cs | 7 + .../Contracts/EmployeeVerificationRequest.cs | 8 + .../Contracts/EmployeeVerificationResponse.cs | 18 + .../External/Contracts/GenderizeResponse.cs | 8 + .../External/Contracts/NationalizeResponse.cs | 14 + .../External/GenderizeApiClient.cs | 32 ++ .../External/NationalizeApiClient.cs | 32 ++ .../Company.AppName.Business/HrSettings.cs | 44 ++ .../ImplicitUsings.cs | 16 + .../Models/Employee.cs | 69 +++ .../Company.AppName.Business/Models/Gender.cs | 10 + .../Models/UsState.cs | 8 + .../Services/EmployeeService.cs | 68 +++ .../Services/EmployeeService2.cs | 48 +++ .../Services/IEmployeeService.cs | 17 + .../Services/ReferenceDataService.cs | 25 ++ .../Services/VerificationService.cs | 69 +++ .../Validators/EmployeeValidator.cs | 17 + .../EmployeeVerificationValidator.cs | 13 + .../Company.AppName.Database.csproj | 32 ++ .../Data/RefData.yaml | 72 ++++ .../Company.AppName.Database/Dockerfile | 58 +++ .../20190101-000001-create-Hr-schema.sql | 2 + .../20200909-162702-create-Hr-Employee.sql | 24 ++ ...0909-163321-create-Hr-EmergencyContact.sql | 14 + .../20200909-164735-create-hr-gender.sql | 18 + ...909-164828-create-hr-terminationreason.sql | 18 + ...0909-165308-create-hr-relationshiptype.sql | 18 + .../20200909-165752-create-hr-usstate.sql | 18 + ...915-160812-create-Hr-PerformanceReview.sql | 19 + ...15-161927-create-hr-performanceoutcome.sql | 18 + ...208-001509-create-hr-eventoutbox-table.sql | 11 + ...001509-create-hr-eventoutboxdata-table.sql | 15 + .../Company.AppName.Database/Program.cs | 31 ++ .../Properties/launchSettings.json | 8 + .../Company.AppName.Database/entrypoint.sh | 20 + .../Company.AppName.Database/wait-for-it.sh | 183 ++++++++ .../Company.AppName.Functions/.dockerignore | 1 + .../Company.AppName.Functions/.gitignore | 264 ++++++++++++ .../.vscode/extensions.json | 5 + .../Company.AppName.Functions.csproj | 31 ++ .../Company.AppName.Functions/Dockerfile | 53 +++ .../Functions/EmployeeFunction.cs | 73 ++++ .../Functions/HttpHealthFunction.cs | 31 ++ .../HttpTriggerQueueVerificationFunction.cs | 37 ++ .../ServiceBusExecuteVerificationFunction.cs | 28 ++ .../MyHrApiConfigurationOptions.cs | 27 ++ .../Company.AppName.Functions/README.md | 35 ++ .../Company.AppName.Functions/Startup.cs | 82 ++++ .../Company.AppName.Functions/host.json | 14 + .../Company.AppName.Infra.Tests.csproj | 25 ++ .../CoreExStackTests.cs | 81 ++++ .../Company.AppName.Infra.Tests/Testing.cs | 90 ++++ .../TestingExtensions.cs | 21 + .../Company.AppName.Infra.Tests/Usings.cs | 5 + .../Company.AppName.Infra.csproj | 28 ++ .../Company.AppName.Infra/Components/Apps.cs | 309 ++++++++++++++ .../Components/Diagnostics.cs | 46 ++ .../Components/Messaging.cs | 90 ++++ .../Company.AppName.Infra/Components/Sql.cs | 141 ++++++ .../Components/Storage.cs | 93 ++++ .../Company.AppName.Infra/CoreExStack.cs | 143 +++++++ .../Company.AppName.Infra/Extensions.cs | 53 +++ .../content/Company.AppName.Infra/Program.cs | 29 ++ .../content/Company.AppName.Infra/Pulumi.yaml | 14 + .../content/Company.AppName.Infra/Readme.md | 57 +++ .../Roles/BuiltInRolesIds.cs | 64 +++ .../Services/DbOperations.cs | 48 +++ .../Services/IDbOperations.cs | 10 + .../Company.AppName.UnitTest.csproj | 44 ++ .../Company.AppName.UnitTest/Data/Data.yaml | 10 + .../EmployeeControllerTest.cs | 400 ++++++++++++++++++ .../EmployeeControllerTest2.cs | 390 +++++++++++++++++ .../EmployeeFunctionTest.cs | 361 ++++++++++++++++ ...ttpTriggerQueueVerificationFunctionTest.cs | 30 ++ .../ReferenceDataControllerTest.cs | 99 +++++ .../Resources/VerificationResult.Unix.json | 29 ++ .../Resources/VerificationResult.Win32.json | 29 ++ ...rviceBusExecuteVerificationFunctionTest.cs | 37 ++ .../appsettings.unittest.json | 14 + src/Templates/content/Company.AppName.sln | 52 +++ src/Templates/readme.md | 1 + 105 files changed, 5380 insertions(+) create mode 100644 src/Templates/content/.devcontainer/Dockerfile create mode 100644 src/Templates/content/.devcontainer/devcontainer.json create mode 100644 src/Templates/content/.devcontainer/devinit.json create mode 100644 src/Templates/content/.template.config/template.json create mode 100644 src/Templates/content/Company.AppName.Api/Company.AppName.Api.csproj create mode 100644 src/Templates/content/Company.AppName.Api/Controllers/EmployeeController.cs create mode 100644 src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs create mode 100644 src/Templates/content/Company.AppName.Api/Controllers/ReferenceDataController.cs create mode 100644 src/Templates/content/Company.AppName.Api/Controllers/SwaggerController.cs create mode 100644 src/Templates/content/Company.AppName.Api/Dockerfile create mode 100644 src/Templates/content/Company.AppName.Api/ImplicitUsings.cs create mode 100644 src/Templates/content/Company.AppName.Api/Program.cs create mode 100644 src/Templates/content/Company.AppName.Api/Properties/launchSettings.json create mode 100644 src/Templates/content/Company.AppName.Api/Startup.cs create mode 100644 src/Templates/content/Company.AppName.Api/appsettings.Development.json create mode 100644 src/Templates/content/Company.AppName.Api/appsettings.json create mode 100644 src/Templates/content/Company.AppName.Business/Company.AppName.Business.csproj create mode 100644 src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs create mode 100644 src/Templates/content/Company.AppName.Business/Data/HrDb.cs create mode 100644 src/Templates/content/Company.AppName.Business/Data/HrDbContext.cs create mode 100644 src/Templates/content/Company.AppName.Business/Data/HrEfDb.cs create mode 100644 src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs create mode 100644 src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs create mode 100644 src/Templates/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs create mode 100644 src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs create mode 100644 src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs create mode 100644 src/Templates/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs create mode 100644 src/Templates/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs create mode 100644 src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs create mode 100644 src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs create mode 100644 src/Templates/content/Company.AppName.Business/HrSettings.cs create mode 100644 src/Templates/content/Company.AppName.Business/ImplicitUsings.cs create mode 100644 src/Templates/content/Company.AppName.Business/Models/Employee.cs create mode 100644 src/Templates/content/Company.AppName.Business/Models/Gender.cs create mode 100644 src/Templates/content/Company.AppName.Business/Models/UsState.cs create mode 100644 src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs create mode 100644 src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs create mode 100644 src/Templates/content/Company.AppName.Business/Services/IEmployeeService.cs create mode 100644 src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs create mode 100644 src/Templates/content/Company.AppName.Business/Services/VerificationService.cs create mode 100644 src/Templates/content/Company.AppName.Business/Validators/EmployeeValidator.cs create mode 100644 src/Templates/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs create mode 100644 src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj create mode 100644 src/Templates/content/Company.AppName.Database/Data/RefData.yaml create mode 100644 src/Templates/content/Company.AppName.Database/Dockerfile create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-Hr-schema.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-Hr-Employee.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-Hr-EmergencyContact.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-hr-gender.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-hr-terminationreason.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-hr-relationshiptype.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-hr-usstate.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-Hr-PerformanceReview.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-hr-performanceoutcome.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutbox-table.sql create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutboxdata-table.sql create mode 100644 src/Templates/content/Company.AppName.Database/Program.cs create mode 100644 src/Templates/content/Company.AppName.Database/Properties/launchSettings.json create mode 100644 src/Templates/content/Company.AppName.Database/entrypoint.sh create mode 100644 src/Templates/content/Company.AppName.Database/wait-for-it.sh create mode 100644 src/Templates/content/Company.AppName.Functions/.dockerignore create mode 100644 src/Templates/content/Company.AppName.Functions/.gitignore create mode 100644 src/Templates/content/Company.AppName.Functions/.vscode/extensions.json create mode 100644 src/Templates/content/Company.AppName.Functions/Company.AppName.Functions.csproj create mode 100644 src/Templates/content/Company.AppName.Functions/Dockerfile create mode 100644 src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs create mode 100644 src/Templates/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs create mode 100644 src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs create mode 100644 src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs create mode 100644 src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs create mode 100644 src/Templates/content/Company.AppName.Functions/README.md create mode 100644 src/Templates/content/Company.AppName.Functions/Startup.cs create mode 100644 src/Templates/content/Company.AppName.Functions/host.json create mode 100644 src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj create mode 100644 src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs create mode 100644 src/Templates/content/Company.AppName.Infra.Tests/Testing.cs create mode 100644 src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs create mode 100644 src/Templates/content/Company.AppName.Infra.Tests/Usings.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Company.AppName.Infra.csproj create mode 100644 src/Templates/content/Company.AppName.Infra/Components/Apps.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Components/Messaging.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Components/Sql.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Components/Storage.cs create mode 100644 src/Templates/content/Company.AppName.Infra/CoreExStack.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Extensions.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Program.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Pulumi.yaml create mode 100644 src/Templates/content/Company.AppName.Infra/Readme.md create mode 100644 src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Services/DbOperations.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Services/IDbOperations.cs create mode 100644 src/Templates/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj create mode 100644 src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml create mode 100644 src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest.cs create mode 100644 src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs create mode 100644 src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs create mode 100644 src/Templates/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs create mode 100644 src/Templates/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs create mode 100644 src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json create mode 100644 src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json create mode 100644 src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs create mode 100644 src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json create mode 100644 src/Templates/content/Company.AppName.sln diff --git a/src/Templates/content/.devcontainer/Dockerfile b/src/Templates/content/.devcontainer/Dockerfile new file mode 100644 index 00000000..5f57b239 --- /dev/null +++ b/src/Templates/content/.devcontainer/Dockerfile @@ -0,0 +1,12 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/dotnet/.devcontainer/base.Dockerfile + +# [Choice] .NET version: 6.0, 5.0, 3.1, 2.1 +ARG VARIANT="6.0" +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT} + +# Set up machine requirements to build the repo +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends cmake llvm-9 clang-9 \ + build-essential python curl git lldb-6.0 liblldb-6.0-dev \ + libunwind8 libunwind8-dev gettext libicu-dev liblttng-ust-dev \ + libssl-dev libnuma-dev libkrb5-dev zlib1g-dev ninja-build \ No newline at end of file diff --git a/src/Templates/content/.devcontainer/devcontainer.json b/src/Templates/content/.devcontainer/devcontainer.json new file mode 100644 index 00000000..929cb0c8 --- /dev/null +++ b/src/Templates/content/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.140.1/containers/dotnetcore +{ + "name": "C# (.NET 6)", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "6.0", + } + }, + "settings": { + "files.associations": { + "*.csproj": "msbuild", + "*.fsproj": "msbuild", + "*.globalconfig": "ini", + "*.manifest": "xml", + "*.nuspec": "xml", + "*.pkgdef": "ini", + "*.projitems": "msbuild", + "*.props": "msbuild", + "*.resx": "xml", + "*.rsp": "Powershell", + "*.shproj": "msbuild", + "*.slnf": "json", + "*.targets": "msbuild", + "*.vbproj": "msbuild", + "*.vsixmanifest": "xml", + "*.vstemplate": "xml" + }, + // ms-dotnettools.csharp settings + "omnisharp.disableMSBuildDiagnosticWarning": true, + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableImportCompletion": true, + "omnisharp.enableRoslynAnalyzers": true, + "omnisharp.useModernNet": true, + "omnisharp.enableAsyncCompletion": true, + }, + "extensions": [ + "ms-dotnettools.csharp", + "EditorConfig.EditorConfig", + "tintoy.msbuild-project-tools" + ], + "postCreateCommand": "dotnet restore" + } \ No newline at end of file diff --git a/src/Templates/content/.devcontainer/devinit.json b/src/Templates/content/.devcontainer/devinit.json new file mode 100644 index 00000000..e69de29b diff --git a/src/Templates/content/.template.config/template.json b/src/Templates/content/.template.config/template.json new file mode 100644 index 00000000..80eb8b3a --- /dev/null +++ b/src/Templates/content/.template.config/template.json @@ -0,0 +1,125 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "CoreEx (https://github.com/avanade/coreex)", + "classifications": [ "CoreEx", "Common", "Library", "Web", "WebAPI", "Console", "Test", "NUnit", "Solution" ], + "identity": "CoreEx.Solution", + "groupIdentity": "CoreEx", + "name": "CoreEx (Core Building blocks for enterprise) solutions", + "shortName": "coreex", + "tags": { + "language": "C#", + "type": "project" + }, + "defaultName": "CoreEx", + "description": "CoreEx ", + "sourceName": "Company.AppName", // Not acutally used; template uses the below parameters exclusively. + "preferNameDirectory": true, + "symbols": { + "company": { + "type": "parameter", + "replaces": "Company", + "fileRename": "Company", + "isRequired": false, + "datatype": "text", + "description": "The company name 'Company' used to define the namespace etc; e.g. 'Company.AppName'." + }, + "appname": { + "type": "parameter", + "replaces": "AppName", + "fileRename": "AppName", + "isRequired": false, + "datatype": "text", + "description": "The application (domain) name 'AppName' used to define the namespace etc; e.g. 'Company.AppName'." + }, + "datasource": { + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "Cosmos", + "description": "Indicates that the data source is Cosmos DB." + }, + { + "choice": "Database", + "description": "Indicates that the data source is a SQL Database accessed via Stored Procedures." + }, + { + "choice": "EntityFramework", + "description": "Indicates that the data source is a SQL Database accessed via Entity Framework Core." + }, + { + "choice": "HttpAgent", + "description": "Indicates that the data source is to be accessed via an Http Agent." + }, + { + "choice": "None", + "description": "Indicates that no data source is to be implemented." + } + ], + "defaultValue": "Database", + "description": "The data source implementation option." + }, + "implement_cosmos": { + "type": "computed", + "value": "(datasource == \"Cosmos\")" + }, + "implement_database": { + "type": "computed", + "value": "(datasource == \"Database\")" + }, + "implement_entityframework": { + "type": "computed", + "value": "(datasource == \"EntityFramework\")" + }, + "implement_httpagent": { + "type": "computed", + "value": "(datasource == \"HttpAgent\")" + }, + "implement_none": { + "type": "computed", + "value": "(datasource == \"None\")" + }, + "created_date": { + "type": "generated", + "generator": "now", + "parameters": { + "format": "yyyyMMdd" + }, + "replaces": "20190101" + } + }, + "sources": [ + { + "modifiers": [ + { + "condition": "(implement_none)", + "exclude": [ "Company.AppName.Business/Validation/**/*", "Company.AppName.Business/Data/PersonData.cs" ] + }, + { + "condition": "(implement_cosmos || implement_httpagent || implement_none)", + "exclude": [ "Company.AppName.Database/**/*" ] + }, + { + "condition": "(!implement_entityframework)", + "exclude": [ "Company.AppName.Business/Data/AppNameEfDb.cs", "Company.AppName.Business/Data/AppNameEfDbContext.cs" ] + }, + { + "condition": "(!implement_cosmos)", + "exclude": [ "Company.AppName.Business/Data/AppNameCosmosDb.cs", "Company.AppName.Test/Cosmos/**/*" ] + }, + { + "condition": "(!implement_httpagent)", + "exclude": [ "Company.AppName.Business/Data/XxxAgent.cs", "Company.AppName.Business/Data/ReferenceDataData.cs" ] + }, + { + "condition": "(implement_httpagent)", + "exclude": [ "Company.AppName.Business/Data/PersonData.cs", "Company.AppName.Business/Validation/PersonArgsValidator.cs" ] + }, + { + "condition": "(!implement_database && !implement_entityframework)", + "exclude": [ "Company.AppName.Business/Data/AppNameDb.cs", "Company.AppName.Test/Data/**/*" ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/Company.AppName.Api.csproj b/src/Templates/content/Company.AppName.Api/Company.AppName.Api.csproj new file mode 100644 index 00000000..3fcc54d2 --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Company.AppName.Api.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + + + + true + $(NoWarn);1591 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/Controllers/EmployeeController.cs b/src/Templates/content/Company.AppName.Api/Controllers/EmployeeController.cs new file mode 100644 index 00000000..3f640418 --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Controllers/EmployeeController.cs @@ -0,0 +1,89 @@ +namespace Company.AppName.Api.Controllers; + +[Route("api/employees")] +[Produces(MediaTypeNames.Application.Json)] +public class EmployeeController : ControllerBase +{ + private readonly WebApi _webApi; + private readonly IEmployeeService _service; + + public EmployeeController(WebApi webApi, IEmployeeService service) + { + _webApi = webApi; + _service = service; + } + + /// + /// Gets the specified . + /// + /// The identifier. + /// The selected where found. + [HttpGet("{id}", Name = "Get")] + [ProducesResponseType(typeof(Employee), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public Task GetAsync(Guid id) + => _webApi.GetAsync(Request, _ => _service.GetEmployeeAsync(id)); + + /// + /// Gets all . + /// + /// All . + [HttpGet("", Name = "GetAll")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + public Task GetAllAsync() + => _webApi.GetAsync(Request, p => _service.GetAllAsync(p.RequestOptions.Paging)); + + /// + /// Creates a new . + /// + /// The validator. + /// The created . + [HttpPost("", Name = "Create")] + [AcceptsBody(typeof(Employee))] + [ProducesResponseType(typeof(Employee), (int)HttpStatusCode.Created)] + public Task CreateAsync([FromServices] IValidator validator) + => _webApi.PostAsync(Request, p => _service.AddEmployeeAsync(p.Value!), + statusCode: HttpStatusCode.Created, validator: validator, locationUri: e => new Uri($"api/employees/{e.Id}", UriKind.RelativeOrAbsolute)); + + /// + /// Updates an existing . + /// + /// The identifier. + /// The validator. + /// The updated . + [HttpPut("{id}", Name = "Update")] + [AcceptsBody(typeof(Employee))] + [ProducesResponseType(typeof(Employee), (int)HttpStatusCode.OK)] + public Task UpdateAsync(Guid id, [FromServices] IValidator validator) + => _webApi.PutAsync(Request, p => _service.UpdateEmployeeAsync(p.Value!, id), validator: validator); + + /// + /// Patches an existing . + /// + /// The identifier. + /// The validator. + /// The updated . + [HttpPatch("{id}", Name = "Patch")] + [AcceptsBody(typeof(Employee), HttpConsts.MergePatchMediaTypeName)] + [ProducesResponseType(typeof(Employee), (int)HttpStatusCode.OK)] + public Task PatchAsync(Guid id, [FromServices] IValidator validator) + => _webApi.PatchAsync(Request, get: _ => _service.GetEmployeeAsync(id), put: p => _service.UpdateEmployeeAsync(p.Value!, id), validator: validator); + + /// + /// Deletes the specified . + /// + /// The Id. + [HttpDelete("{id}", Name = "Delete")] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public Task DeleteAsync(Guid id) + => _webApi.DeleteAsync(Request, _ => _service.DeleteEmployeeAsync(id)); + + /// + /// Performs verification in an asynchronous process. + /// + [HttpPost("{id}/verify", Name = "Verify")] + [ProducesResponseType((int)HttpStatusCode.Accepted)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public Task VerifyAsync(Guid id) + => _webApi.PostAsync(Request, apiParam => _service.VerifyEmployeeAsync(id), HttpStatusCode.Accepted); +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs b/src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs new file mode 100644 index 00000000..52b6400f --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs @@ -0,0 +1,23 @@ +namespace Company.AppName.Api.Controllers; + +/// +/// Health Controller +/// +[ApiController] +[Route("[controller]")] +public class HealthController : ControllerBase +{ + private readonly HealthService _health; + + public HealthController(HealthService health) + { + _health = health; + } + + /// + /// Health Endpoint + /// + [HttpGet()] + [Route("/health")] + public async Task Index() => await _health.RunAsync().ConfigureAwait(false); +} diff --git a/src/Templates/content/Company.AppName.Api/Controllers/ReferenceDataController.cs b/src/Templates/content/Company.AppName.Api/Controllers/ReferenceDataController.cs new file mode 100644 index 00000000..886eb72c --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Controllers/ReferenceDataController.cs @@ -0,0 +1,41 @@ +namespace Company.AppName.Api.Controllers; + +[Route("api/ref")] +[Produces(MediaTypeNames.Application.Json)] +public class ReferenceDataController : ControllerBase +{ + private readonly ReferenceDataContentWebApi _webApi; + private readonly ReferenceDataOrchestrator _orchestrator; + + public ReferenceDataController(ReferenceDataContentWebApi webApi, ReferenceDataOrchestrator orchestrator) + { + _webApi = webApi; + _orchestrator = orchestrator; + } + + /// + /// Gets all of the reference data items that match the specified criteria. + /// + /// The reference data code list. + /// The reference data text (including wildcards). + /// A . + [HttpGet("usstates")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + public Task USStateGetAll([FromQuery] IEnumerable? codes = default, string? text = default) => + _webApi.GetAsync(Request, x => _orchestrator.GetWithFilterAsync(codes, text, x.RequestOptions.IncludeInactive)); + + /// + /// Gets all of the reference data items that match the specified criteria. + /// + /// The reference data code list. + /// The reference data text (including wildcards). + /// A . + [HttpGet("genders")] + [ProducesResponseType(typeof(ReferenceDataMultiCollection), (int)HttpStatusCode.OK)] + public Task GenderGetAll([FromQuery] IEnumerable? codes = default, string? text = default) => + _webApi.GetAsync(Request, x => _orchestrator.GetWithFilterAsync(codes, text, x.RequestOptions.IncludeInactive)); + + [HttpGet()] + [ProducesResponseType(typeof(ReferenceDataMultiCollection), (int)HttpStatusCode.OK)] + public Task GetNamed() => _webApi.GetAsync(Request, p => _orchestrator.GetNamedAsync(p.RequestOptions)); +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/Controllers/SwaggerController.cs b/src/Templates/content/Company.AppName.Api/Controllers/SwaggerController.cs new file mode 100644 index 00000000..a7e1b3b8 --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Controllers/SwaggerController.cs @@ -0,0 +1,19 @@ +namespace Company.AppName.Api.Controllers; + +/// +/// Swagger/OpenAPI documentation for the API. +/// +[ApiController] +[Route("[controller]")] +public class SwaggerController : ControllerBase +{ + /// + /// Swagger/OpenAPI documentation for the API. + /// + [HttpGet()] + [Route("/")] + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } +} diff --git a/src/Templates/content/Company.AppName.Api/Dockerfile b/src/Templates/content/Company.AppName.Api/Dockerfile new file mode 100644 index 00000000..5eeb08e1 --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Dockerfile @@ -0,0 +1,44 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src + +# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles +# to take advantage of Docker's build cache, to speed up local container builds +COPY "samples/Company.AppName/Company.AppName.sln" "samples/Company.AppName/Company.AppName.sln" + +COPY "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" +COPY "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" +COPY "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" +COPY "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" +COPY "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" + +COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" +COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" +COPY "src/CoreEx.Azure/CoreEx.Azure.csproj" "src/CoreEx.Azure/CoreEx.Azure.csproj" +COPY "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" +COPY "src/CoreEx.Database/CoreEx.Database.csproj" "src/CoreEx.Database/CoreEx.Database.csproj" +COPY "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" +COPY "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" +COPY "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" +COPY "src/CoreEx.Validation/CoreEx.Validation.csproj" "src/CoreEx.Validation/CoreEx.Validation.csproj" + +RUN dotnet restore "samples/Company.AppName/Company.AppName.sln" + +COPY . . +WORKDIR /src/samples/Company.AppName/Company.AppName.Api +RUN dotnet publish --no-restore -c Release -o /app + +FROM build as unittest +WORKDIR /src/samples/Company.AppName/Company.AppName.Test +# can run tests here on buils + +FROM build AS publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "Company.AppName.Api.dll"] diff --git a/src/Templates/content/Company.AppName.Api/ImplicitUsings.cs b/src/Templates/content/Company.AppName.Api/ImplicitUsings.cs new file mode 100644 index 00000000..2fb4360f --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/ImplicitUsings.cs @@ -0,0 +1,23 @@ +global using CoreEx; +global using CoreEx.Configuration; +global using CoreEx.Entities; +global using CoreEx.Events; +global using CoreEx.HealthChecks; +global using CoreEx.Http; +global using CoreEx.Json; +global using CoreEx.RefData; +global using CoreEx.RefData.Models; +global using CoreEx.Validation; +global using CoreEx.WebApis; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.Logging; +global using Company.AppName.Business; +global using Company.AppName.Business.Data; +global using Company.AppName.Business.External; +global using Company.AppName.Business.External.Contracts; +global using Company.AppName.Business.Models; +global using Company.AppName.Business.Services; +global using System.Net; +global using System.Net.Mime; +global using System.Reflection; \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/Program.cs b/src/Templates/content/Company.AppName.Api/Program.cs new file mode 100644 index 00000000..22ca10e3 --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Program.cs @@ -0,0 +1 @@ +Host.CreateDefaultBuilder().ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()).Build().Run(); diff --git a/src/Templates/content/Company.AppName.Api/Properties/launchSettings.json b/src/Templates/content/Company.AppName.Api/Properties/launchSettings.json new file mode 100644 index 00000000..04d88d6f --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:46085", + "sslPort": 44328 + } + }, + "profiles": { + "Company.AppName.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7129;http://localhost:5272", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Templates/content/Company.AppName.Api/Startup.cs b/src/Templates/content/Company.AppName.Api/Startup.cs new file mode 100644 index 00000000..fc2451d2 --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Startup.cs @@ -0,0 +1,83 @@ +using CoreEx.Azure.HealthChecks; +using CoreEx.Database; +using CoreEx.DataBase.HealthChecks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Company.AppName.Api; + +public class Startup +{ + // todo: add azure app configuration (conditional?) + + /// + /// The configure services method called by the runtime; use this method to add services to the container. + /// + /// The . + public void ConfigureServices(IServiceCollection services) + { + // Register the core services. + services + .AddSettings() + .AddReferenceDataOrchestrator(sp => new ReferenceDataOrchestrator(sp).Register()) + .AddExecutionContext() + .AddJsonSerializer() + .AddEventDataSerializer() + .AddEventDataFormatter() + .AddEventPublisher() + .AddAzureServiceBusSender() + .AddAzureServiceBusPurger() + .AddAzureServiceBusClient(connectionName: nameof(HrSettings.ServiceBusConnection)) + .AddJsonMergePatch() + .AddWebApi(c => c.UnhandledExceptionAsync = (ex, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? new ConcurrencyException().ToResult() : null)) + .AddReferenceDataContentWebApi() + .AddRequestCache(); + + // Register the business services. + services + .AddScoped() + .AddScoped() + .AddFluentValidators(); + + // Register the database and EF services, including required AutoMapper. + services.AddDatabase(sp => new HrDb(sp.GetRequiredService())) + .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) + .AddScoped() + .AddAutoMapper(typeof(HrEfDb).Assembly) + .AddAutoMapperWrapper(); + + // Register the health checks. + services + .AddScoped() + .AddHealthChecks() + .AddTypeActivatedCheck("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(HrSettings.ServiceBusConnection), nameof(HrSettings.VerificationQueueName)) + .AddTypeActivatedCheck("SQL Server", HealthStatus.Unhealthy, tags: default!, timeout: TimeSpan.FromSeconds(15), nameof(HrSettings.ConnectionStrings__Database)); + + + services.AddControllers(); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(options => + { + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + options.OperationFilter(); // Needed to support AcceptsBodyAttribue where body parameter not explicitly defined. + }); + } + + /// + /// The configure method called by the runtime; use this method to configure the HTTP request pipeline. + /// + /// The . + public void Configure(IApplicationBuilder app) + => app + .UseWebApiExceptionHandler() + .UseSwagger() + .UseSwaggerUI() + .UseHttpsRedirection() + .UseRouting() + .UseAuthorization() + .UseExecutionContext() + .UseEndpoints(endpoints => endpoints.MapControllers()); +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/appsettings.Development.json b/src/Templates/content/Company.AppName.Api/appsettings.Development.json new file mode 100644 index 00000000..de23200b --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/appsettings.Development.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "VerificationQueueName": "pendingVerifications", + "ServiceBusConnection": "coreex.servicebus.windows.net", + "ConnectionStrings": { + "Database": "Data Source=.;Initial Catalog=Company.AppNameDb;Integrated Security=True;TrustServerCertificate=true" + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/appsettings.json b/src/Templates/content/Company.AppName.Api/appsettings.json new file mode 100644 index 00000000..5e9fd7fb --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Company.AppName.Business.csproj b/src/Templates/content/Company.AppName.Business/Company.AppName.Business.csproj new file mode 100644 index 00000000..5d982917 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Company.AppName.Business.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs b/src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs new file mode 100644 index 00000000..1da9434a --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs @@ -0,0 +1,21 @@ +namespace Company.AppName.Business.Data; + +public class EmployeeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Employee", "Hr"); + builder.Property(p => p.Id).HasColumnName("EmployeeId").HasColumnType("UNIQUEIDENTIFIER"); + builder.Property(p => p.Email).HasColumnType("NVARCHAR(250)"); + builder.Property(p => p.FirstName).HasColumnType("NVARCHAR(100)"); + builder.Property(p => p.LastName).HasColumnType("NVARCHAR(100)"); + builder.Property(p => p.Gender).HasColumnName("GenderCode").HasColumnType("NVARCHAR(50)").HasConversion(v => v!.Code, v => (Gender?)v); + builder.Property(p => p.Birthday).HasColumnType("DATE"); + builder.Property(p => p.StartDate).HasColumnType("DATE"); + builder.Property(p => p.TerminationDate).HasColumnType("DATE"); + builder.Property(p => p.TerminationReasonCode).HasColumnType("NVARCHAR(50)"); + builder.Property(p => p.PhoneNo).HasColumnType("NVARCHAR(50)"); + builder.Property(p => p.ETag).HasColumnName("RowVersion").IsRowVersion().HasConversion(s => s == null ? Array.Empty() : Convert.FromBase64String(s), d => Convert.ToBase64String(d)); + builder.HasKey("Id"); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Data/HrDb.cs b/src/Templates/content/Company.AppName.Business/Data/HrDb.cs new file mode 100644 index 00000000..dec00972 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Data/HrDb.cs @@ -0,0 +1,11 @@ +using CoreEx.Database; +using CoreEx.Database.SqlServer; +using Microsoft.Data.SqlClient; + +namespace Company.AppName.Business.Data +{ + public class HrDb : SqlServerDatabase + { + public HrDb(SettingsBase settings) : base(() => new SqlConnection(settings.GetRequiredValue("ConnectionStrings:Database"))) { } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Data/HrDbContext.cs b/src/Templates/content/Company.AppName.Business/Data/HrDbContext.cs new file mode 100644 index 00000000..49ce0e5b --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Data/HrDbContext.cs @@ -0,0 +1,28 @@ +using CoreEx.Database; +using CoreEx.EntityFrameworkCore; + +namespace Company.AppName.Business.Data; + +public class HrDbContext : DbContext, IEfDbContext +{ + public IDatabase BaseDatabase { get; } + + public DbSet USStates { get; set; } + + public DbSet Genders { get; set; } + + public DbSet Employees { get; set; } + +#pragma warning disable CS8618 // Non-nullable property - properties set by Entity Framework Core + public HrDbContext(DbContextOptions options, IDatabase database) : base(options) => BaseDatabase = database ?? throw new ArgumentNullException(nameof(database)); +#pragma warning restore CS8618 // Non-nullable property + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .ApplyConfiguration(new UsStateConfiguration()) + .ApplyConfiguration(new EmployeeConfiguration()); + + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Data/HrEfDb.cs b/src/Templates/content/Company.AppName.Business/Data/HrEfDb.cs new file mode 100644 index 00000000..c6199b49 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Data/HrEfDb.cs @@ -0,0 +1,33 @@ +using CoreEx.Mapping; + +namespace Company.AppName.Business.Data +{ + /// + /// Enables the Company.AppName database using Entity Framework. + /// + public interface IHrEfDb : IEfDb + { + /// + /// Gets the entity. + /// + EfDbEntity Employees { get; } + } + + /// + /// Represents the Company.AppName database using Entity Framework. + /// + public class HrEfDb : EfDb, IHrEfDb + { + /// + /// Initializes a new instance of the class. + /// + /// The entity framework database context. + /// The . + public HrEfDb(HrDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } + + /// + /// Gets the encapsulated entity. + /// + public EfDbEntity Employees => new(this); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs b/src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs new file mode 100644 index 00000000..7d4de479 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs @@ -0,0 +1,23 @@ +namespace Company.AppName.Business.Data; + +public class UsStateConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.ToTable("USState", "Hr"); + entity.HasKey("Id"); + entity.Property(p => p.Id).HasColumnName("USStateId").HasColumnType("UNIQUEIDENTIFIER"); + entity.Property(p => p.Code).HasColumnType("NVARCHAR(50)"); + entity.Property(p => p.Text).HasColumnType("NVARCHAR(250)"); + entity.Property(p => p.IsActive).HasColumnType("BIT"); + entity.Property(p => p.SortOrder).HasColumnType("INT"); + entity.Property(p => p.ETag).HasColumnName("RowVersion").IsRowVersion().HasConversion(s => s == null ? Array.Empty() : Convert.FromBase64String(s), d => Convert.ToBase64String(d)); + entity.Ignore(p => p.EndDate); + entity.Ignore(p => p.StartDate); + entity.Ignore(p => p.Description); + entity.Ignore(p => p.IsReadOnly); + entity.Ignore(p => p.IsValid); + entity.Ignore(p => p.IsChanged); + entity.Ignore(p => p.IsInitial); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs b/src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs new file mode 100644 index 00000000..2dc896dc --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs @@ -0,0 +1,32 @@ +namespace Company.AppName.Business.External; + +/// +/// Http client for https://agify.io/ +/// +public class AgifyApiClient : TypedHttpClientCore +{ + public AgifyApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger> logger) + : base(client, jsonSerializer, executionContext, settings, logger) + { + if (!Uri.IsWellFormedUriString(settings.AgifyApiEndpointUri, UriKind.Absolute)) + throw new InvalidOperationException(@$"The Api endpoint URI is not valid: {settings.AgifyApiEndpointUri}. Provide valid Api endpoint URI in the configuration '{nameof(settings.AgifyApiEndpointUri)}'. + If Api Client is not needed - remove all references to {nameof(AgifyApiClient)}."); + + client.BaseAddress = new Uri(settings.AgifyApiEndpointUri); + } + + public override Task HealthCheckAsync(CancellationToken cancellationToken) + { + return base.HeadAsync(string.Empty, null, new HttpArg[] { new HttpArg("name", "health") }, cancellationToken); + } + + public async Task GetAgeAsync(string name) + { + var response = await + WithRetry(1, 5) + .ThrowTransientException() + .GetAsync(string.Empty, null, HttpArgs.Create(new HttpArg("name", name))); + + return response.Value; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs b/src/Templates/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs new file mode 100644 index 00000000..ca4c4890 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs @@ -0,0 +1,7 @@ +namespace Company.AppName.Business.External.Contracts; + +public class AgifyResponse +{ + public string? Name { get; set; } + public int Age { get; set; } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs b/src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs new file mode 100644 index 00000000..0c252939 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs @@ -0,0 +1,8 @@ +namespace Company.AppName.Business.External.Contracts; + +public class EmployeeVerificationRequest +{ + public string? Name { get; set; } + public int Age { get; set; } + public string? Gender { get; set; } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs b/src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs new file mode 100644 index 00000000..0599d381 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs @@ -0,0 +1,18 @@ +namespace Company.AppName.Business.External.Contracts; + +public class EmployeeVerificationResponse +{ + public EmployeeVerificationResponse(EmployeeVerificationRequest request) => Request = request; + + public int Age { get; set; } + + public string? Gender { get; set; } + + public float GenderProbability { get; set; } + + public List Country { get; set; } = new List(); + + public List VerificationMessages { get; set; } = new List(); + + public EmployeeVerificationRequest Request { get; } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs b/src/Templates/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs new file mode 100644 index 00000000..1e18db80 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs @@ -0,0 +1,8 @@ +namespace Company.AppName.Business.External.Contracts; + +public class GenderizeResponse +{ + public string? Name { get; set; } + public string? Gender { get; set; } + public float Probability { get; set; } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs b/src/Templates/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs new file mode 100644 index 00000000..0784e4fa --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs @@ -0,0 +1,14 @@ +namespace Company.AppName.Business.External.Contracts; + +public class NationalizeResponse +{ + public string? Name { get; set; } + + public List? Country { get; set; } + + public class CountryResponse + { + public string? Country_Id { get; set; } + public float Probability { get; set; } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs b/src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs new file mode 100644 index 00000000..e79591de --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs @@ -0,0 +1,32 @@ +namespace Company.AppName.Business.External; + +/// +/// Http client for https://genderize.io/ +/// +public class GenderizeApiClient : TypedHttpClientCore +{ + public GenderizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger> logger) + : base(client, jsonSerializer, executionContext, settings, logger) + { + if (!Uri.IsWellFormedUriString(settings.GenderizeApiClientApiEndpointUri, UriKind.Absolute)) + throw new InvalidOperationException(@$"The Api endpoint URI is not valid: {settings.GenderizeApiClientApiEndpointUri}. Provide valid Api endpoint URI in the configuration '{nameof(settings.GenderizeApiClientApiEndpointUri)}'. + If Api Client is not needed - remove all references to {nameof(GenderizeApiClient)}."); + + client.BaseAddress = new Uri(settings.GenderizeApiClientApiEndpointUri); + } + + public override Task HealthCheckAsync(CancellationToken cancellationToken = default) + { + return base.HeadAsync(string.Empty, null, HttpArgs.Create(new HttpArg("name", "health")), cancellationToken); + } + + public async Task GetGenderAsync(string name) + { + var response = await + WithRetry(1, 5) + .ThrowTransientException() + .GetAsync(string.Empty, null, HttpArgs.Create(new HttpArg("name", name))); + + return response.Value; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs b/src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs new file mode 100644 index 00000000..fcb519d5 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs @@ -0,0 +1,32 @@ +namespace Company.AppName.Business.External; + +/// +/// Http client for https://nationalize.io/ +/// +public class NationalizeApiClient : TypedHttpClientCore +{ + public NationalizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger> logger) + : base(client, jsonSerializer, executionContext, settings, logger) + { + if (!Uri.IsWellFormedUriString(settings.NationalizeApiClientApiEndpointUri, UriKind.Absolute)) + throw new InvalidOperationException(@$"The Api endpoint URI is not valid: {settings.NationalizeApiClientApiEndpointUri}. Provide valid Api endpoint URI in the configuration '{nameof(settings.NationalizeApiClientApiEndpointUri)}'. + If Api Client is not needed - remove all references to {nameof(NationalizeApiClient)}."); + + client.BaseAddress = new Uri(settings.NationalizeApiClientApiEndpointUri); + } + + public override Task HealthCheckAsync(CancellationToken cancellationToken = default) + { + return base.HeadAsync(string.Empty, null, HttpArgs.Create(new HttpArg("name", "health")), cancellationToken); + } + + public async Task GetNationalityAsync(string name) + { + var response = await + WithRetry(1, 5) + .ThrowTransientException() + .GetAsync(string.Empty, null, HttpArgs.Create(new HttpArg("name", name))); + + return response.Value; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/HrSettings.cs b/src/Templates/content/Company.AppName.Business/HrSettings.cs new file mode 100644 index 00000000..1af8541f --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/HrSettings.cs @@ -0,0 +1,44 @@ +namespace Company.AppName.Business; + +public class HrSettings : SettingsBase +{ + /// + /// Gets the setting prefixes in order of precedence. + /// + public static string[] Prefixes { get; } = { "Hr/", "Common/" }; + + /// + /// Initializes a new instance of the class. + /// + /// The . + public HrSettings(IConfiguration configuration) : base(configuration, Prefixes) { } + + public string AgifyApiEndpointUri => GetValue(); + + public string NationalizeApiClientApiEndpointUri => GetValue(); + + public string GenderizeApiClientApiEndpointUri => GetValue(); + + public string VerificationQueueName => GetValue(); + + public string VerificationResultsQueueName => GetValue(); + + /// + /// The Azure Service Bus connection string used for Publishing in . + /// + /// It defaults to managed identity connection string used by triggers 'ServiceBusConnection__fullyQualifiedNamespace' + public string ServiceBusConnection => GetValue(defaultValue: ServiceBusConnection__fullyQualifiedNamespace); + + /// + /// The Azure Service Bus connection string used by Triggers using managed identity. + /// + /// Caution this key is used implicitly by function triggers when 'ServiceBusConnection' is not set. + /// Underscores in environment variables are replaced by semicolon ':' in configuration object, hence lookup also replaces '__' with ':' + public string ServiceBusConnection__fullyQualifiedNamespace => GetValue(); + + /// + /// SQL Server connection string used by the app (depending on the value it may use managed identity or username/password) + /// + /// Underscores in environment variables are replaced by semicolon ':' in configuration object, hence lookup also replaces '__' with ':' + public string ConnectionStrings__Database => GetValue(); +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/ImplicitUsings.cs b/src/Templates/content/Company.AppName.Business/ImplicitUsings.cs new file mode 100644 index 00000000..0c4f5c95 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/ImplicitUsings.cs @@ -0,0 +1,16 @@ +global using CoreEx; +global using CoreEx.Configuration; +global using CoreEx.Entities; +global using CoreEx.EntityFrameworkCore; +global using CoreEx.Events; +global using CoreEx.Http; +global using CoreEx.Json; +global using CoreEx.RefData; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.Metadata.Builders; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.Logging; +global using Company.AppName.Business.Data; +global using Company.AppName.Business.External; +global using Company.AppName.Business.External.Contracts; +global using Company.AppName.Business.Models; \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Models/Employee.cs b/src/Templates/content/Company.AppName.Business/Models/Employee.cs new file mode 100644 index 00000000..798a8574 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Models/Employee.cs @@ -0,0 +1,69 @@ +using System.Diagnostics; + +namespace Company.AppName.Business.Models; + +/// +/// Represents the Entity Framework (EF) model for database object 'Hr.Employee'. +/// +public class Employee : IIdentifier, IETag +{ + /// + /// Gets or sets the 'EmployeeId' column value. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets the 'Email' column value. + /// + public string? Email { get; set; } + + /// + /// Gets or sets the 'FirstName' column value. + /// + public string? FirstName { get; set; } + + /// + /// Gets or sets the 'LastName' column value. + /// + public string? LastName { get; set; } + + /// + /// Gets or sets the 'GenderCode' column value. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public Gender? Gender { get; set; } + + /// + /// Gets or sets the 'Birthday' column value. + /// + public DateTime? Birthday { get; set; } + + /// + /// Gets or sets the 'StartDate' column value. + /// + public DateTime? StartDate { get; set; } + + /// + /// Gets or sets the 'TerminationDate' column value. + /// + public DateTime? TerminationDate { get; set; } + + /// + /// Gets or sets the 'TerminationReasonCode' column value. + /// + public string? TerminationReasonCode { get; set; } + + /// + /// Gets or sets the 'PhoneNo' column value. + /// + public string? PhoneNo { get; set; } + + /// + /// Gets or sets the 'RowVersion' column value. + /// + public string? ETag { get; set; } +} + +public class EmployeeCollection : List { } + +public class EmployeeCollectionResult : CollectionResult { } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Models/Gender.cs b/src/Templates/content/Company.AppName.Business/Models/Gender.cs new file mode 100644 index 00000000..69b1e0de --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Models/Gender.cs @@ -0,0 +1,10 @@ +using CoreEx.RefData; + +namespace Company.AppName.Business.Models; + +public class Gender : ReferenceDataBase +{ + public static implicit operator Gender?(string? code) => ConvertFromCode(code); +} + +public class GenderCollection : ReferenceDataCollectionBase { } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Models/UsState.cs b/src/Templates/content/Company.AppName.Business/Models/UsState.cs new file mode 100644 index 00000000..e1d629bf --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Models/UsState.cs @@ -0,0 +1,8 @@ +namespace Company.AppName.Business.Models; + +public class USState : ReferenceDataBase +{ + public static implicit operator USState?(string? code) => ConvertFromCode(code); +} + +public class USStateCollection : ReferenceDataCollectionBase { } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs b/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs new file mode 100644 index 00000000..3937526b --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs @@ -0,0 +1,68 @@ +namespace Company.AppName.Business.Services; + +public class EmployeeService : IEmployeeService +{ + private readonly HrDbContext _dbContext; + private readonly IEventPublisher _publisher; + private readonly HrSettings _settings; + + public EmployeeService(HrDbContext dbContext, IEventPublisher publisher, HrSettings settings) + { + _dbContext = dbContext; + _publisher = publisher; + _settings = settings; + } + + public async Task GetEmployeeAsync(Guid id) + => await _dbContext.Employees.FirstOrDefaultAsync(e => e.Id == id); + + public Task GetAllAsync(PagingArgs? paging) + => _dbContext.Employees.OrderBy(x => x.LastName).ThenBy(x => x.FirstName).ToCollectionResultAsync(paging); + + public async Task AddEmployeeAsync(Employee employee) + { + _dbContext.Employees.Add(employee); + await _dbContext.SaveChangesAsync(); + return employee; + } + + public async Task UpdateEmployeeAsync(Employee employee, Guid id) + { + if (!await _dbContext.Employees.AnyAsync(e => e.Id == id).ConfigureAwait(false)) + throw new NotFoundException(); + + employee.Id = id; + _dbContext.Employees.Update(employee); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + return employee; + } + + public async Task DeleteEmployeeAsync(Guid id) + { + var employee = await _dbContext.Employees.FirstOrDefaultAsync(e => e.Id == id); + if (employee != null) + { + _dbContext.Employees.Remove(employee); + await _dbContext.SaveChangesAsync(); + } + } + + public async Task VerifyEmployeeAsync(Guid id) + { + // Get the employee. + var employee = await GetEmployeeAsync(id); + if (employee == null) + throw new NotFoundException(); + + // Publish message to service bus for employee verification. + var verification = new EmployeeVerificationRequest + { + Name = employee.FirstName, + Age = DateTime.UtcNow.Subtract(employee.Birthday.GetValueOrDefault()).Days / 365, + Gender = employee.Gender?.Code + }; + + _publisher.Publish(_settings.VerificationQueueName, new EventData { Value = verification }); + await _publisher.SendAsync(); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs b/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs new file mode 100644 index 00000000..4320a114 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs @@ -0,0 +1,48 @@ +namespace Company.AppName.Business.Services; + +/// +/// Example using that largely encapsulates/simplifies the EF access. +/// +public class EmployeeService2 : IEmployeeService +{ + private readonly IHrEfDb _efDb; + private readonly IEventPublisher _publisher; + private readonly HrSettings _settings; + + public EmployeeService2(IHrEfDb efDb, IEventPublisher publisher, HrSettings settings) + { + _efDb = efDb; + _publisher = publisher; + _settings = settings; + } + + public Task GetEmployeeAsync(Guid id) => _efDb.Employees.GetAsync(id); + + public Task GetAllAsync(PagingArgs? paging) + => _efDb.Employees.Query(q => q.OrderBy(x => x.LastName).ThenBy(x => x.FirstName)).WithPaging(paging).SelectResultAsync(); + + public Task AddEmployeeAsync(Employee employee) => _efDb.Employees.CreateAsync(employee); + + public Task UpdateEmployeeAsync(Employee employee, Guid id) => _efDb.Employees.UpdateAsync(employee.Adjust(x => x.Id = id)); + + public Task DeleteEmployeeAsync(Guid id) => _efDb.Employees.DeleteAsync(id); + + public async Task VerifyEmployeeAsync(Guid id) + { + // Get the employee. + var employee = await GetEmployeeAsync(id); + if (employee == null) + throw new NotFoundException(); + + // Publish message to service bus for employee verification. + var verification = new EmployeeVerificationRequest + { + Name = employee.FirstName, + Age = DateTime.UtcNow.Subtract(employee.Birthday.GetValueOrDefault()).Days / 365, + Gender = employee.Gender?.Code + }; + + _publisher.Publish(_settings.VerificationQueueName, new EventData { Value = verification }); + await _publisher.SendAsync(); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Services/IEmployeeService.cs b/src/Templates/content/Company.AppName.Business/Services/IEmployeeService.cs new file mode 100644 index 00000000..8627345d --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Services/IEmployeeService.cs @@ -0,0 +1,17 @@ +namespace Company.AppName.Business.Services +{ + public interface IEmployeeService + { + Task GetEmployeeAsync(Guid id); + + Task GetAllAsync(PagingArgs? paging); + + Task AddEmployeeAsync(Employee employee); + + Task UpdateEmployeeAsync(Employee employee, Guid id); + + Task DeleteEmployeeAsync(Guid id); + + Task VerifyEmployeeAsync(Guid id); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs b/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs new file mode 100644 index 00000000..6c2ccf5b --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs @@ -0,0 +1,25 @@ +using CoreEx.Database; +using CoreEx.Database.Extended; + +namespace Company.AppName.Business.Services; + +public class ReferenceDataService : IReferenceDataProvider +{ + private readonly IDatabase _db; + private readonly HrDbContext _dbContext; + + public ReferenceDataService(IDatabase db, HrDbContext dbContext) + { + _db = db; + _dbContext = dbContext; + } + + public Type[] Types => new Type[] { typeof(USState), typeof(Gender) }; + + public async Task GetAsync(Type type, CancellationToken cancellationToken = default) => type switch + { + Type t when t == typeof(USState) => await USStateCollection.CreateAsync(_dbContext.USStates.AsNoTracking(), cancellationToken).ConfigureAwait(false), + Type t when t == typeof(Gender) => await _db.ReferenceData("Hr", "Gender").LoadAsync("GenderId", cancellationToken: cancellationToken).ConfigureAwait(false), + _ => throw new InvalidOperationException($"Type {type.FullName} is not a known {nameof(IReferenceData)}.") + }; +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Services/VerificationService.cs b/src/Templates/content/Company.AppName.Business/Services/VerificationService.cs new file mode 100644 index 00000000..a4759712 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Services/VerificationService.cs @@ -0,0 +1,69 @@ +namespace Company.AppName.Business.Services; + +public class VerificationService +{ + private readonly AgifyApiClient _agifyApiClient; + private readonly GenderizeApiClient _genderizeApiClient; + private readonly NationalizeApiClient _nationalizeApiClient; + private readonly HrSettings _settings; + private readonly IEventPublisher _publisher; + + public VerificationService(AgifyApiClient agifyApiClient, GenderizeApiClient genderizeApiClient, NationalizeApiClient nationalizeApiClient, HrSettings settings, IEventPublisher publisher) + { + _agifyApiClient = agifyApiClient; + _genderizeApiClient = genderizeApiClient; + _nationalizeApiClient = nationalizeApiClient; + _settings = settings; + _publisher = publisher; + } + + public async Task> VerifyAsync(string name) + { + var agifyTask = _agifyApiClient.GetAgeAsync(name); + var genderizeTask = _genderizeApiClient.GetGenderAsync(name); + var nationalizeTask = _nationalizeApiClient.GetNationalityAsync(name); + + await Task.WhenAll(agifyTask, genderizeTask, nationalizeTask); + + return new Tuple(agifyTask.Result, genderizeTask.Result, nationalizeTask.Result); + } + + public async Task VerifyAndPublish(EmployeeVerificationRequest request) + { + var result = await VerifyAsync(request.Name!); + + var response = new EmployeeVerificationResponse(request) + { + Age = result.Item1.Age, + Gender = result.Item2.Gender, + GenderProbability = result.Item2.Probability + }; + + response.Country.AddRange(result.Item3.Country!); + + var nationality = response.Country.OrderByDescending(c => c.Probability).First(); + + response.VerificationMessages.Add( + @$"Performed verification for {request.Name}, {request.Gender} age {request.Age}. + Engine predicted age was {response.Age}. + Engine predicted gender was {response.Gender} with {ToPercents(response.GenderProbability)} probability. + Most likely nationality of {request.Name} is {nationality.Country_Id} with {ToPercents(nationality.Probability)} probability" + ); + + // first check age + if (Math.Abs(request.Age - response.Age) >= 10) + { + response.VerificationMessages.Add($"Employee age ({request.Age}) is not within range of 10 years of predicted age: {response.Age}"); + } + + if (response.GenderProbability > 0.5 && !response.Gender!.Equals(request.Gender, StringComparison.InvariantCultureIgnoreCase)) + { + response.VerificationMessages.Add($"Employee gender ({request.Gender}) doesn't match predicted gender: {response.Gender}"); + } + + _publisher.Publish(_settings.VerificationResultsQueueName, new EventData { Value = response }); + await _publisher.SendAsync(); + } + + private static string ToPercents(float value) => (int)(value * 100) + "%"; +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Validators/EmployeeValidator.cs b/src/Templates/content/Company.AppName.Business/Validators/EmployeeValidator.cs new file mode 100644 index 00000000..fcc8243e --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Validators/EmployeeValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; + +namespace Company.AppName.Business.Validators; + +public class EmployeeValidator : AbstractValidator +{ + public EmployeeValidator() + { + RuleFor(x => x.Email).NotNull().EmailAddress(); + RuleFor(x => x.FirstName).NotNull().MaximumLength(100); + RuleFor(x => x.LastName).NotNull().MaximumLength(100); + RuleFor(x => x.Gender).NotNull().IsValid(); + RuleFor(x => x.Birthday).NotNull().LessThanOrEqualTo(DateTime.UtcNow.AddYears(-18)).WithMessage("Birthday is invalid as the Employee must be at least 18 years of age."); + RuleFor(x => x.StartDate).NotNull().GreaterThanOrEqualTo(new DateTime(1999, 01, 01, 0, 0, 0, DateTimeKind.Utc)).WithMessage("January 1, 1999"); + RuleFor(x => x.PhoneNo).NotNull().MaximumLength(50); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs b/src/Templates/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs new file mode 100644 index 00000000..c753c543 --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace Company.AppName.Business.Validators; + +public class EmployeeVerificationValidator : AbstractValidator +{ + public EmployeeVerificationValidator() + { + RuleFor(x => x.Name).NotNull().MaximumLength(100); + RuleFor(x => x.Gender).NotNull().MaximumLength(50); // todo: validate if reference data exists + RuleFor(x => x.Age).NotNull().GreaterThanOrEqualTo(18).LessThanOrEqualTo(120).WithMessage("Age has to be between 18 and 120"); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj b/src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj new file mode 100644 index 00000000..246cf97c --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj @@ -0,0 +1,32 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Data/RefData.yaml b/src/Templates/content/Company.AppName.Database/Data/RefData.yaml new file mode 100644 index 00000000..2fad84b8 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Data/RefData.yaml @@ -0,0 +1,72 @@ +Hr: + - $Gender: + - F: Female + - M: Male + - N: Not specified + - $TerminationReason: + - RE: Resigned + - RD: Redundant + - TM: Terminated + - $RelationshipType: + - SPO: Spouse + - PTN: Partner + - PAR: Parent + - CHI: Child + - SIB: Sibling + - EXF: Extended family + - FRD: Friend + - $USState: + - AL: Alabama + - AK: Alaska + - AZ: Arizona + - AR: Arkansas + - CA: California + - CO: Colorado + - CT: Connecticut + - DE: Delaware + - FL: Florida + - GA: Georgia + - HI: Hawaii + - ID: Idaho + - IL: Illinois + - IN: Indiana + - IA: Iowa + - KS: Kansas + - KY: Kentucky + - LA: Louisiana + - ME: Maine + - MD: Maryland + - MA: Massachusetts + - MI: Michigan + - MN: Minnesota + - MS: Mississippi + - MO: Missouri + - MT: Montana + - NE: Nebraska + - NV: Nevada + - NH: New Hampshire + - NJ: New Jersey + - NM: New Mexico + - NY: New York + - NC: North Carolina + - ND: North Dakota + - OH: Ohio + - OK: Oklahoma + - OR: Oregon + - PA: Pennsylvania + - RI: Rhode Island + - SC: South Carolina + - SD: South Dakota + - TN: Tennessee + - TX: Texas + - UT: Utah + - VT: Vermont + - VA: Virginia + - WA: Washington + - WV: West Virginia + - WI: Wisconsin + - WY: Wyoming + - $PerformanceOutcome: + - DN: Does not meet expectations + - ME: Meets expectations + - EE: Exceeds expectations \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Dockerfile b/src/Templates/content/Company.AppName.Database/Dockerfile new file mode 100644 index 00000000..9675a310 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Dockerfile @@ -0,0 +1,58 @@ +FROM mcr.microsoft.com/mssql/server:2019-latest AS base +USER root +# Install dotnet sdk +RUN apt-get update; \ + apt-get install -y apt-transport-https && \ + apt-get update && \ + apt-get install -y dotnet-runtime-6.0 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src + +# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles +# to take advantage of Docker's build cache, to speed up local container builds +COPY "samples/Company.AppName/Company.AppName.sln" "samples/Company.AppName/Company.AppName.sln" + +COPY "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" +COPY "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" +COPY "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" +COPY "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" +COPY "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" + +COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" +COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" +COPY "src/CoreEx.Azure/CoreEx.Azure.csproj" "src/CoreEx.Azure/CoreEx.Azure.csproj" +COPY "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" +COPY "src/CoreEx.Database/CoreEx.Database.csproj" "src/CoreEx.Database/CoreEx.Database.csproj" +COPY "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" +COPY "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" +COPY "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" +COPY "src/CoreEx.Validation/CoreEx.Validation.csproj" "src/CoreEx.Validation/CoreEx.Validation.csproj" + + +RUN dotnet restore "samples/Company.AppName/Company.AppName.sln" + +COPY . . +WORKDIR /src/samples/Company.AppName/Company.AppName.Database +RUN dotnet build -c Release -o /dbex/build + +FROM base as final +USER root + +ENV ACCEPT_EULA Y +ENV MSSQL_SA_PASSWORD sAPWD23.^0 +ENV MSSQL_TCP_PORT 1433 +ENV MSSQL_AGENT_ENABLED true +ENV ConnectionStrings__sqlserver:MyHr Data Source=localhost, $MSSQL_TCP_PORT;Initial Catalog=Company.AppName;User id=sa;Password=$MSSQL_SA_PASSWORD;TrustServerCertificate=true + + +# Copy setup scripts +WORKDIR /usr/local/ +COPY --from=build /dbex/build /dbex +COPY samples/Company.AppName/Company.AppName.Database/wait-for-it.sh samples/Company.AppName/Company.AppName.Database/entrypoint.sh ./ + +RUN chmod +x ./*.sh + +ENTRYPOINT ["/usr/local/entrypoint.sh"] +CMD ["sql"] \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-Hr-schema.sql b/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-Hr-schema.sql new file mode 100644 index 00000000..5fc766a5 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-Hr-schema.sql @@ -0,0 +1,2 @@ +CREATE SCHEMA [Hr] + AUTHORIZATION [dbo]; \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-Hr-Employee.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-Hr-Employee.sql new file mode 100644 index 00000000..e6d953e1 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-Hr-Employee.sql @@ -0,0 +1,24 @@ +-- Migration Script + +BEGIN TRANSACTION + +CREATE TABLE [Hr].[Employee] ( + [EmployeeId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, -- This is the primary key + [Email] NVARCHAR(250) NULL UNIQUE, -- This is the employee's unique email address + [FirstName] NVARCHAR(100) NULL, + [LastName] NVARCHAR(100) NULL, + [GenderCode] NVARCHAR(50) NULL, -- This is the related Gender code; see Ref.Gender table + [Birthday] DATE NULL, + [StartDate] DATE NULL, + [TerminationDate] DATE NULL, + [TerminationReasonCode] NVARCHAR(50) NULL, -- This is the related Termination Reason code; see Ref.TerminationReason table + [PhoneNo] NVARCHAR(50) NULL, + [AddressJson] NVARCHAR(500) NULL, -- This is the full address persisted as JSON. + [RowVersion] TIMESTAMP NOT NULL, -- This is used for concurrency version checking. + [CreatedBy] NVARCHAR(250) NULL, -- The following are standard audit columns. + [CreatedDate] DATETIME2 NULL, + [UpdatedBy] NVARCHAR(250) NULL, + [UpdatedDate] DATETIME2 NULL +); + +COMMIT TRANSACTION \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-Hr-EmergencyContact.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-Hr-EmergencyContact.sql new file mode 100644 index 00000000..45a6652a --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-Hr-EmergencyContact.sql @@ -0,0 +1,14 @@ +-- Migration Script + +BEGIN TRANSACTION + +CREATE TABLE [Hr].[EmergencyContact] ( + [EmergencyContactId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, + [EmployeeId] UNIQUEIDENTIFIER NOT NULL, + [FirstName] NVARCHAR(100) NULL, + [LastName] NVARCHAR(100) NULL, + [PhoneNo] NVARCHAR(50) NULL, + [RelationshipTypeCode] NVARCHAR(50) NULL +); + +COMMIT TRANSACTION \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-hr-gender.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-hr-gender.sql new file mode 100644 index 00000000..08c2c383 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-hr-gender.sql @@ -0,0 +1,18 @@ +-- Migration Script + +BEGIN TRANSACTION + +CREATE TABLE [Hr].[Gender] ( + [GenderId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, + [Code] NVARCHAR(50) NOT NULL UNIQUE, + [Text] NVARCHAR(250) NULL, + [IsActive] BIT NULL, + [SortOrder] INT NULL, + [RowVersion] TIMESTAMP NOT NULL, + [CreatedBy] NVARCHAR(250) NULL, + [CreatedDate] DATETIME2 NULL, + [UpdatedBy] NVARCHAR(250) NULL, + [UpdatedDate] DATETIME2 NULL +); + +COMMIT TRANSACTION \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-hr-terminationreason.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-hr-terminationreason.sql new file mode 100644 index 00000000..cd4534e5 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-hr-terminationreason.sql @@ -0,0 +1,18 @@ +-- Migration Script + +BEGIN TRANSACTION + +CREATE TABLE [Hr].[TerminationReason] ( + [TerminationReasonId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, + [Code] NVARCHAR(50) NOT NULL UNIQUE, + [Text] NVARCHAR(250) NULL, + [IsActive] BIT NULL, + [SortOrder] INT NULL, + [RowVersion] TIMESTAMP NOT NULL, + [CreatedBy] NVARCHAR(250) NULL, + [CreatedDate] DATETIME2 NULL, + [UpdatedBy] NVARCHAR(250) NULL, + [UpdatedDate] DATETIME2 NULL +); + +COMMIT TRANSACTION \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-hr-relationshiptype.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-hr-relationshiptype.sql new file mode 100644 index 00000000..692fdc36 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-hr-relationshiptype.sql @@ -0,0 +1,18 @@ +-- Migration Script + +BEGIN TRANSACTION + +CREATE TABLE [Hr].[RelationshipType] ( + [RelationshipTypeId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, + [Code] NVARCHAR(50) NOT NULL UNIQUE, + [Text] NVARCHAR(250) NULL, + [IsActive] BIT NULL, + [SortOrder] INT NULL, + [RowVersion] TIMESTAMP NOT NULL, + [CreatedBy] NVARCHAR(250) NULL, + [CreatedDate] DATETIME2 NULL, + [UpdatedBy] NVARCHAR(250) NULL, + [UpdatedDate] DATETIME2 NULL +); + +COMMIT TRANSACTION \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-hr-usstate.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-hr-usstate.sql new file mode 100644 index 00000000..12f0be9d --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-hr-usstate.sql @@ -0,0 +1,18 @@ +-- Migration Script + +BEGIN TRANSACTION + +CREATE TABLE [Hr].[USState] ( + [USStateId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, + [Code] NVARCHAR(50) NOT NULL UNIQUE, + [Text] NVARCHAR(250) NULL, + [IsActive] BIT NULL, + [SortOrder] INT NULL, + [RowVersion] TIMESTAMP NOT NULL, + [CreatedBy] NVARCHAR(250) NULL, + [CreatedDate] DATETIME2 NULL, + [UpdatedBy] NVARCHAR(250) NULL, + [UpdatedDate] DATETIME2 NULL +); + +COMMIT TRANSACTION \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-Hr-PerformanceReview.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-Hr-PerformanceReview.sql new file mode 100644 index 00000000..5582abf7 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-Hr-PerformanceReview.sql @@ -0,0 +1,19 @@ +-- Migration Script + +BEGIN TRANSACTION + +CREATE TABLE [Hr].[PerformanceReview] ( + [PerformanceReviewId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, + [EmployeeId] UNIQUEIDENTIFIER NOT NULL, + [Date] DATETIME2 NULL, + [PerformanceOutcomeCode] NVARCHAR(50) NULL, + [Reviewer] NVARCHAR(100) NULL, + [Notes] NVARCHAR(4000) NULL, + [RowVersion] TIMESTAMP NOT NULL, + [CreatedBy] NVARCHAR(250) NULL, + [CreatedDate] DATETIME2 NULL, + [UpdatedBy] NVARCHAR(250) NULL, + [UpdatedDate] DATETIME2 NULL +); + +COMMIT TRANSACTION \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-hr-performanceoutcome.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-hr-performanceoutcome.sql new file mode 100644 index 00000000..c61c2baf --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-hr-performanceoutcome.sql @@ -0,0 +1,18 @@ +-- Migration Script + +BEGIN TRANSACTION + +CREATE TABLE [Hr].[PerformanceOutcome] ( + [PerformanceOutcomeId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, + [Code] NVARCHAR(50) NOT NULL UNIQUE, + [Text] NVARCHAR(250) NULL, + [IsActive] BIT NULL, + [SortOrder] INT NULL, + [RowVersion] TIMESTAMP NOT NULL, + [CreatedBy] NVARCHAR(250) NULL, + [CreatedDate] DATETIME2 NULL, + [UpdatedBy] NVARCHAR(250) NULL, + [UpdatedDate] DATETIME2 NULL +); + +COMMIT TRANSACTION \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutbox-table.sql b/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutbox-table.sql new file mode 100644 index 00000000..fa6059d4 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutbox-table.sql @@ -0,0 +1,11 @@ +CREATE TABLE [Hr].[EventOutbox] ( + /* + * This is automatically generated; any changes will be lost. + */ + + [EventOutboxId] BIGINT IDENTITY (1, 1) NOT NULL PRIMARY KEY NONCLUSTERED ([EventOutboxId] ASC), + [EnqueuedDate] DATETIME2 NOT NULL, + [PartitionKey] NVARCHAR(128) NULL, + [DequeuedDate] DATETIME2 NULL, + CONSTRAINT [IX_Hr_EventOutbox_DequeuedDate] UNIQUE CLUSTERED ([DequeuedDate], [EventOutboxId]) +); \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutboxdata-table.sql b/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutboxdata-table.sql new file mode 100644 index 00000000..971dce15 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutboxdata-table.sql @@ -0,0 +1,15 @@ +CREATE TABLE [Hr].[EventOutboxData] ( + /* + * This is automatically generated; any changes will be lost. + */ + + [EventOutboxId] BIGINT NOT NULL PRIMARY KEY CLUSTERED ([EventOutboxId] ASC), + [EventId] UNIQUEIDENTIFIER, + [Subject] NVARCHAR(1024), + [Action] NVARCHAR(128) NULL, + [CorrelationId] NVARCHAR(64) NULL, + [TenantId] UNIQUEIDENTIFIER NULL, + [PartitionKey] NVARCHAR(128) NULL, + [ValueType] NVARCHAR(1024) NULL, + [EventData] VARBINARY(MAX) NULL +); \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Program.cs b/src/Templates/content/Company.AppName.Database/Program.cs new file mode 100644 index 00000000..c3b2479a --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Program.cs @@ -0,0 +1,31 @@ +using DbEx.Console; +using System.Reflection; + +namespace Company.AppName.Database +{ + /// + /// Represents the database utilities program (capability). + /// + public class Program + { + /// + /// Main startup. + /// + /// The startup arguments. + /// The status code whereby zero indicates success. + internal static Task Main(string[] args) => RunMigrator("Data Source=.;Initial Catalog=Company.AppNameDb;Integrated Security=True;TrustServerCertificate=true", null, args); + + public static Task RunMigrator(string connectionString, Assembly? assembly = null, params string[] args) + => SqlServerMigratorConsole + .Create(connectionString) + .ConsoleArgs(a => + { + a.ConnectionStringEnvironmentVariableName = "My_HrDb"; + a.DataParserArgs.RefDataColumnDefaults.TryAdd("IsActive", _ => true); + a.DataParserArgs.RefDataColumnDefaults.TryAdd("SortOrder", i => i); + if (assembly != null) + a.AddAssembly(assembly); + }) + .RunAsync(args); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Properties/launchSettings.json b/src/Templates/content/Company.AppName.Database/Properties/launchSettings.json new file mode 100644 index 00000000..d7b05887 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Company.AppName.Database": { + "commandName": "Project", + "commandLineArgs": "all" + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/entrypoint.sh b/src/Templates/content/Company.AppName.Database/entrypoint.sh new file mode 100644 index 00000000..c19e6d1d --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/entrypoint.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -e + +if [ "$1" = 'sql' ]; then + if ! [[ -f /var/opt/mssql/.initialized ]]; + then + ./wait-for-it.sh localhost:1433 -t 30 -- sleep 10 && echo "db is up" + + echo "Creating $DB_NAME database..." + + #run the setup script to create the DB and the schema in the DB + dotnet /dbex/Company.AppName.Database.dll all + + echo "Database scripts complete" + touch /var/opt/mssql/.initialized + fi & + exec /opt/mssql/bin/sqlservr +fi + +exec "$@" \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/wait-for-it.sh b/src/Templates/content/Company.AppName.Database/wait-for-it.sh new file mode 100644 index 00000000..127f18c4 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/wait-for-it.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available +# Source: https://github.com/vishnubob/wait-for-it + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/.dockerignore b/src/Templates/content/Company.AppName.Functions/.dockerignore new file mode 100644 index 00000000..1927772b --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/.dockerignore @@ -0,0 +1 @@ +local.settings.json \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/.gitignore b/src/Templates/content/Company.AppName.Functions/.gitignore new file mode 100644 index 00000000..3c3f4e6a --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/.vscode/extensions.json b/src/Templates/content/Company.AppName.Functions/.vscode/extensions.json new file mode 100644 index 00000000..dde673dc --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions" + ] +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/Company.AppName.Functions.csproj b/src/Templates/content/Company.AppName.Functions/Company.AppName.Functions.csproj new file mode 100644 index 00000000..a7451df5 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/Company.AppName.Functions.csproj @@ -0,0 +1,31 @@ + + + net6.0 + v4 + <_FunctionsSkipCleanOutput>true + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/Dockerfile b/src/Templates/content/Company.AppName.Functions/Dockerfile new file mode 100644 index 00000000..b08dc2bf --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/Dockerfile @@ -0,0 +1,53 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS installer-env +# set to true for local runs - switches functions auth to anoymous +ARG LOCAL + +# Build requires 3.1 SDK +COPY --from=mcr.microsoft.com/dotnet/core/sdk:3.1 /usr/share/dotnet /usr/share/dotnet + +WORKDIR /src +# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles +# to take advantage of Docker's build cache, to speed up local container builds +COPY "samples/Company.AppName/Company.AppName.sln" "samples/Company.AppName/Company.AppName.sln" + +COPY "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" +COPY "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" +COPY "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" +COPY "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" +COPY "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" + +COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" +COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" +COPY "src/CoreEx.Azure/CoreEx.Azure.csproj" "src/CoreEx.Azure/CoreEx.Azure.csproj" +COPY "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" +COPY "src/CoreEx.Database/CoreEx.Database.csproj" "src/CoreEx.Database/CoreEx.Database.csproj" +COPY "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" +COPY "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" +COPY "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" +COPY "src/CoreEx.Validation/CoreEx.Validation.csproj" "src/CoreEx.Validation/CoreEx.Validation.csproj" + +RUN dotnet restore "samples/Company.AppName/Company.AppName.sln" + +COPY . . + +WORKDIR /src/samples/Company.AppName/Company.AppName.Functions + +RUN mkdir -p /home/site/wwwroot && \ + dotnet publish *.csproj --no-restore -c Debug --output /home/site/wwwroot && \ + echo LOCAL is "$LOCAL" && \ + echo $(if [ "$LOCAL" = "true" ] ; then find / \( -type f -name .git -prune \) -o -type f -name "function.json" -print0 | xargs -0 sed -i 's/authLevel\": \"function/authLevel\": \"anonymous/g' ; fi) + +# To enable ssh & remote debugging on app service change the base image to the one below +# FROM mcr.microsoft.com/azure-functions/dotnet:4-appservice +# FROM mcr.microsoft.com/azure-functions/dotnet:4 +FROM mcr.microsoft.com/azure-functions/dotnet:4-appservice +ARG LOCAL + +ENV AzureWebJobsScriptRoot=/home/site/wwwroot +ENV AzureFunctionsJobHost__Logging__Console__IsEnabled=true +ENV AzureFunctionsJobHost__Logging__LogLevel__CoreEx=Debug +ENV AzureFunctionsJobHost__Logging__LogToConsole=true +ENV AzureFunctionsJobHost__Logging__LogToConsoleColor=true + +COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"] \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs b/src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs new file mode 100644 index 00000000..5f60edc1 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs @@ -0,0 +1,73 @@ +using CoreEx.Validation; +using CoreEx.WebApis; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; +using Microsoft.OpenApi.Models; +using Company.AppName.Business.Models; +using Company.AppName.Business.Services; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Mime; +using System.Threading.Tasks; + +namespace Company.AppName.Functions.Functions; + +public class EmployeeFunction +{ + private readonly WebApi _webApi; + private readonly EmployeeService _service; + private readonly IValidator _validator; + + public EmployeeFunction(WebApi webApi, EmployeeService service, IValidator validator) + { + _webApi = webApi; + _service = service; + _validator = validator; + } + + [FunctionName("Get")] + [OpenApiOperation(operationId: "Get", tags: new[] { "employee" })] + [OpenApiParameter(name: "id", Description = "The employee id", Required = true, In = ParameterLocation.Path, Type = typeof(Guid))] + [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] + [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: MediaTypeNames.Application.Json, bodyType: typeof(Employee), Description = "Employee record")] + [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NotFound, Description = "Not found")] + public Task GetAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/employees/{id}")] HttpRequest request, Guid id) + => _webApi.GetAsync(request, _ => _service.GetEmployeeAsync(id)); + + [FunctionName("GetAll")] + [OpenApiOperation(operationId: "GetAll", tags: new[] { "employee" })] + [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] + [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: MediaTypeNames.Application.Json, bodyType: typeof(List), Description = "Employee records")] + public Task GetAllAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/employees")] HttpRequest request) + => _webApi.GetAsync(request, p => _service.GetAllAsync(p.RequestOptions.Paging)); + + [FunctionName("Create")] + [OpenApiOperation(operationId: "Create", tags: new[] { "employee" })] + [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] + [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.Created, Description = "Created employee record")] + public Task CreateAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "api/employees")] HttpRequest request) + => _webApi.PostAsync(request, p => _service.AddEmployeeAsync(p.Value!), + statusCode: HttpStatusCode.Created, validator: _validator, locationUri: e => new Uri($"api/employees/{e.Id}", UriKind.RelativeOrAbsolute)); + + [FunctionName("Update")] + [OpenApiOperation(operationId: "Update", tags: new[] { "employee" })] + [OpenApiParameter(name: "id", Description = "The employee id", Required = true, In = ParameterLocation.Path, Type = typeof(Guid))] + [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] + [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: MediaTypeNames.Application.Json, bodyType: typeof(Employee), Description = "Employee record")] + [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NotFound, Description = "Not found")] + public Task UpdateAsync([HttpTrigger(AuthorizationLevel.Function, "put", Route = "api/employees/{id}")] HttpRequest request, Guid id) + => _webApi.PutAsync(request, p => _service.UpdateEmployeeAsync(p.Value!, id), validator: _validator); + + [FunctionName("Patch")] + public Task PatchAsync([HttpTrigger(AuthorizationLevel.Function, "patch", Route = "api/employees/{id}")] HttpRequest request, Guid id) + => _webApi.PatchAsync(request, get: _ => _service.GetEmployeeAsync(id), put: p => _service.UpdateEmployeeAsync(p.Value!, id), validator: _validator); + + [FunctionName("Delete")] + public Task DeleteAsync([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "api/employees/{id}")] HttpRequest request, Guid id) + => _webApi.DeleteAsync(request, _ => _service.DeleteEmployeeAsync(id)); +} diff --git a/src/Templates/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs b/src/Templates/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs new file mode 100644 index 00000000..386c54e7 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs @@ -0,0 +1,31 @@ +using CoreEx.HealthChecks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.OpenApi.Models; +using System.Net; +using System.Net.Mime; +using System.Threading.Tasks; + +namespace Company.AppName.Functions; + +public class HttpHealthFunction +{ + private readonly HealthService _health; + + public HttpHealthFunction(HealthService health) + { + _health = health; + } + + [FunctionName("HealthInfo")] + [OpenApiOperation(operationId: "Run", tags: new[] { "health" })] + [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] + [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: MediaTypeNames.Application.Json, bodyType: typeof(HealthReportEntry), Description = "The OK response")] + public async Task RunAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "health")] HttpRequest req) + => await _health.RunAsync().ConfigureAwait(false); +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs b/src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs new file mode 100644 index 00000000..b1c02987 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs @@ -0,0 +1,37 @@ +using System.Net; +using System.Net.Mime; +using System.Threading.Tasks; +using CoreEx.FluentValidation; +using CoreEx.WebApis; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; +using Microsoft.OpenApi.Models; +using Company.AppName.Business; +using Company.AppName.Business.External.Contracts; +using Company.AppName.Business.Validators; + +namespace Company.AppName.Functions; + +public class HttpTriggerQueueVerificationFunction +{ + private readonly WebApiPublisher _webApiPublisher; + private readonly HrSettings _settings; + + public HttpTriggerQueueVerificationFunction(WebApiPublisher webApiPublisher, HrSettings settings) + { + _webApiPublisher = webApiPublisher; + _settings = settings; + } + + [FunctionName(nameof(HttpTriggerQueueVerificationFunction))] + [OpenApiOperation(operationId: "Run", tags: new[] { "employee" })] + [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] + [OpenApiRequestBody(MediaTypeNames.Application.Json, typeof(EmployeeVerificationRequest), Description = "The **EmployeeVerification** payload")] + [OpenApiResponseWithBody(statusCode: HttpStatusCode.Accepted, contentType: MediaTypeNames.Text.Plain, bodyType: typeof(string), Description = "The OK response")] + public Task RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employee/verify")] HttpRequest request) + => _webApiPublisher.PublishAsync(request, _settings.VerificationQueueName, validator: new EmployeeVerificationValidator().Wrap()); +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs b/src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs new file mode 100644 index 00000000..3e5eb7de --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using FluentValidation; +using CoreEx.Azure.ServiceBus; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.ServiceBus; +using Company.AppName.Business; +using Company.AppName.Business.Services; +using Company.AppName.Business.Validators; + +namespace Company.AppName.Functions; + +public class ServiceBusExecuteVerificationFunction +{ + private readonly ServiceBusSubscriber _subscriber; + private readonly VerificationService _service; + + public ServiceBusExecuteVerificationFunction(ServiceBusSubscriber subscriber, VerificationService service) + { + _subscriber = subscriber; + _service = service; + } + + [FunctionName(nameof(ServiceBusExecuteVerificationFunction))] + [ExponentialBackoffRetry(3, "00:02:00", "00:30:00")] + public Task RunAsync([ServiceBusTrigger("%" + nameof(HrSettings.VerificationQueueName) + "%", Connection = nameof(HrSettings.ServiceBusConnection))] ServiceBusReceivedMessage message, ServiceBusMessageActions messageActions) + => _subscriber.ReceiveAsync(message, messageActions, ed => _service.VerifyAndPublish(ed.Value), validator: new EmployeeVerificationValidator().Wrap()); +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs b/src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs new file mode 100644 index 00000000..abedf728 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Configurations; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; +using Microsoft.OpenApi.Models; + +namespace Company.AppName.Functions; + +/// Configuration options for . +public class MyOpenApiConfigurationOptions : DefaultOpenApiConfigurationOptions +{ + public override OpenApiInfo Info { get; set; } = new OpenApiInfo() + { + Version = "1.0.1", + Title = "CoreEx My HR Sample", + Description = "A serverless Azure Function which demonstrates the use of CoreEx.", + TermsOfService = new Uri("https://github.com/Avanade/CoreEx"), + + License = new OpenApiLicense() + { + Name = "MIT", + Url = new Uri("http://opensource.org/licenses/MIT"), + } + }; + + public override OpenApiVersionType OpenApiVersion { get; set; } = OpenApiVersionType.V3; + +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/README.md b/src/Templates/content/Company.AppName.Functions/README.md new file mode 100644 index 00000000..eeda67a3 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/README.md @@ -0,0 +1,35 @@ +# About + +tbd + +## Configuration + +Sample configuration for `local.settings.json` + +```json +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + + "AgifyApiEndpointUri": "https://api.agify.io", + "NationalizeApiClientApiEndpointUri": "https://api.nationalize.io", + "GenderizeApiClientApiEndpointUri": "https://api.genderize.io", + + "VerificationQueueName": "pendingVerifications", + "VerificationResultsQueueName": "verificationResults", + + "ServiceBusConnection__fullyQualifiedNamespace": "coreex.servicebus.windows.net", + "AzureWebJobs.ServiceBusExecuteVerificationFunction.Disabled": true, // disable when service bus is not available + + "HttpLogContent": "true", + "AzureFunctionsJobHost__logging__logLevel__CoreEx": "Debug", + "AzureFunctionsJobHost__logging__logToConsole": "true", + "AzureFunctionsJobHost__logging__logToConsoleColor": "true", + "AzureFunctionsJobHost__logging__console__isEnabled": "true", + + "MassPublishQueueName": "mass-publish" + } +} +``` diff --git a/src/Templates/content/Company.AppName.Functions/Startup.cs b/src/Templates/content/Company.AppName.Functions/Startup.cs new file mode 100644 index 00000000..fc559372 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/Startup.cs @@ -0,0 +1,82 @@ +using System.Threading.Tasks; +using CoreEx; +using CoreEx.Azure.HealthChecks; +using CoreEx.Database; +using CoreEx.DataBase.HealthChecks; +using CoreEx.HealthChecks; +using CoreEx.RefData; +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Company.AppName.Business; +using Company.AppName.Business.Data; +using Company.AppName.Business.External; +using Company.AppName.Business.Services; + +[assembly: FunctionsStartup(typeof(Company.AppName.Functions.Startup))] + +namespace Company.AppName.Functions; + +public class Startup : FunctionsStartup +{ + public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder) + { + } + + public override void Configure(IFunctionsHostBuilder builder) + { + try + { + // Register the core services. + builder.Services + .AddSettings() + .AddReferenceDataOrchestrator(sp => new ReferenceDataOrchestrator(sp, new MemoryCache(new MemoryCacheOptions())).Register()) + .AddExecutionContext() + .AddJsonSerializer() + .AddEventDataSerializer() + .AddEventDataFormatter() + .AddEventPublisher() + .AddAzureServiceBusSender() + .AddWebApi(c => c.UnhandledExceptionAsync = (ex, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? new ConcurrencyException().ToResult() : null)) + .AddJsonMergePatch() + .AddWebApiPublisher() + .AddAzureServiceBusSubscriber() + .AddAzureServiceBusClient(connectionName: nameof(HrSettings.ServiceBusConnection)); + + // Register the health checks. + builder.Services + .AddScoped() + .AddHealthChecks() + .AddTypeActivatedCheck>("Genderize API") + .AddTypeActivatedCheck>("Agify API") + .AddTypeActivatedCheck>("Nationalize API") + .AddTypeActivatedCheck("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(HrSettings.ServiceBusConnection), nameof(HrSettings.VerificationQueueName)) + .AddTypeActivatedCheck("SQL Server", HealthStatus.Unhealthy, tags: default, timeout: System.TimeSpan.FromSeconds(15), nameof(HrSettings.ConnectionStrings__Database)); + + // Register the business services. + builder.Services + .AddScoped() + .AddScoped() + .AddScoped() + .AddFluentValidators(); + + // Register the typed backend http clients. + builder.Services.AddTypedHttpClient("Agify"); + builder.Services.AddTypedHttpClient("Genderize"); + builder.Services.AddTypedHttpClient("Nationalize"); + + // Database + builder.Services.AddScoped(); + // builder.Services.AddDatabase(sp => new HrDb(sp.GetRequiredService())); + builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); + } + catch (System.Exception ex) + { + // try catch block for running the function in docker container, without it, it may fail silently. + System.Console.Error.WriteLine(ex); + throw; + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/host.json b/src/Templates/content/Company.AppName.Functions/host.json new file mode 100644 index 00000000..3a74677c --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/host.json @@ -0,0 +1,14 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + }, + "logLevel": { + "default": "Warning" + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj b/src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj new file mode 100644 index 00000000..f279b038 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + diff --git a/src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs b/src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs new file mode 100644 index 00000000..f17b8221 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs @@ -0,0 +1,81 @@ +// https://www.pulumi.com/blog/unit-testing-cloud-deployments-with-dotnet/ + +using Pulumi.AzureNative.Resources; + +namespace Company.AppName.Infra.Tests; + +public class CoreExStackTests +{ + [Test] + public async Task ResourceGroupHasNameTag() + { + var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); + + var rgs = resources.OfType(); + var rg = rgs.First(); + var tags = await rg.Tags.GetValueAsync(); + + // Assert + rgs.Should().HaveCount(1); + rg.Should().NotBeNull(); + tags.Should().ContainKey("App"); + } + + [Test] + public async Task AllResourcesHaveNameTag() + { + // unfortunately this doesn't test tags created by auto-tagging dove via ResourceTransformation + var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); + + var rs = resources.Select(async r => + { + var tagsProp = r.GetType().GetProperty("Tags"); + + return tagsProp != null + ? (resource: r, tags: await tagsProp!.GetValue(r)!.GetValueAsync?>()) + : (resource: r, tags: null); + }); + + var result = (await Task.WhenAll(rs)).Where(anyResource => anyResource.tags != null); + + // Assert + result.Should().HaveCountGreaterThan(5); + result.Should().AllSatisfy(r => r.tags!.ContainsKey("App"), because: "All resources should be tagged"); + } + + [Test] + public async Task FunctionIsCreatedWithAUrl() + { + var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); + + var healthUrl = await outputs["FunctionHealthUrl"]!.GetValueAsync(); + var appSwaggerUrl = await outputs["AppSwaggerUrl"]!.GetValueAsync(); + + // Assert + healthUrl.Should().Be("https://unittest.azurewebsites.net/api/health?code=key", because: "mock values set in Testing class"); + appSwaggerUrl.Should().Be("https://unittest.azurewebsites.net/swagger/index.html", because: "mock values set in Testing class"); + } + + [Test] + public async Task SqlIsCreatedWithConnectionString() + { + var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); + + var connectionString = await outputs["SqlDatabaseConnectionString"]!.GetValueAsync(); + + // Assert + connectionString.Should().Be("Server=sql-server-stack.database.windows.net; Authentication=Active Directory Default; Database=sqldb", because: "mock values set in Testing class"); + } + + [Test] + public async Task DbOperationsShouldExecute() + { + var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); + + // Assert + // because DB schema deployment is enabled in Testing class + dbOperationsMock.Verify(op => op.DeployDbSchemaAsync(It.IsAny())); + dbOperationsMock.Verify(op => op.ProvisionUsers(It.IsAny>(), It.IsAny())); + } +} + diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Testing.cs b/src/Templates/content/Company.AppName.Infra.Tests/Testing.cs new file mode 100644 index 00000000..c0607b74 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra.Tests/Testing.cs @@ -0,0 +1,90 @@ +using System.Collections.Immutable; +using System.Text.Json; +using Company.AppName.Infra.Services; + +namespace Company.AppName.Infra.Tests; + +public static class Testing +{ + public static async Task<(ImmutableArray Resources, IDictionary Outputs, Mock)> RunAsync() + { + var config = new Dictionary{ + {"unittest:sqlAdAdmin", "sqlAdAdmin"}, + {"unittest:sqlAdPassword", "sqlAdPassword"}, + {"unittest:isAppsDeploymentEnabled", "true"}, + {"unittest:isDBSchemaDeploymentEnabled", "true"} + }; + + Environment.SetEnvironmentVariable("PULUMI_CONFIG", JsonSerializer.Serialize(config)); + var mocks = new Mocks(); + var dbOperationsMock = new Mock(); + + TestOptions options = new() + { + IsPreview = false, + ProjectName = "unittest" + }; + + var (resources, outputs) = await Deployment.TestAsync(mocks, options, () => CoreExStack.ExecuteStackAsync(dbOperationsMock.Object)); + + return (resources, outputs, dbOperationsMock); + } + + public class Mocks : IMocks + { + public Task CallAsync(MockCallArgs args) + { + var outputs = ImmutableDictionary.CreateBuilder(); + outputs.AddRange(args.Args); + + switch (args.Token) + { + case "azure:keyvault/getKeyVault:getKeyVault": + outputs.Add("id", Guid.NewGuid().ToString()); + break; + + case "azure-native:web:listWebAppHostKeys": + outputs.Add("masterKey", "key"); + break; + + case "azure-native:storage:listStorageAccountKeys": + var kvJson = JsonDocument.Parse("[{\"value\":\"valueKeyStorage\"}]").RootElement; + outputs.Add("keys", kvJson); + break; + + case "azuread:index/getDomains:getDomains": + var adJson = JsonDocument.Parse("[{\"domainName\":\"myDomain.onmicrosoft.com\"}]").RootElement; + outputs.Add("domains", adJson); + break; + + default: + throw new InvalidOperationException($"Operation {args.Token} is not supported"); + } + + return Task.FromResult((object)outputs); + } + + public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args) + { + var outputs = ImmutableDictionary.CreateBuilder(); + + // Forward all input parameters as resource outputs, so that we could test them. + outputs.AddRange(args.Inputs); + + // Set the name to resource name if it's not set explicitly in inputs. + if (!args.Inputs.ContainsKey("name")) + outputs.Add("name", args.Name ?? "name"); + + // <-- We'll customize the mocks here + if (args.Type == "azure-native:web:WebApp") + { + outputs["outboundIpAddresses"] = "192.167.12.1,24.56.76.1"; + outputs["defaultHostName"] = "unittest.azurewebsites.net"; + } + + // Default the resource ID to `{name}_id`. + string? id = $"{args.Id}_{args.Name}_id"; + return Task.FromResult<(string? id, object state)>((id, state: outputs)); + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs b/src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs new file mode 100644 index 00000000..4944eed4 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs @@ -0,0 +1,21 @@ +namespace Company.AppName.Infra.Tests; + +public static class TestingExtensions +{ + public static Task GetValueAsync(this Output output) + { + var tcs = new TaskCompletionSource(); + output.Apply(v => { tcs.SetResult(v); return v; }); + return tcs.Task; + } + + public static Task GetValueAsync(this object outputObj) + { + if (outputObj is Output output) + { + return output.GetValueAsync(); + } + + return Task.FromException(new ArgumentException("Provided object is not Output", nameof(outputObj))); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Usings.cs b/src/Templates/content/Company.AppName.Infra.Tests/Usings.cs new file mode 100644 index 00000000..9b5d4f20 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra.Tests/Usings.cs @@ -0,0 +1,5 @@ +global using NUnit.Framework; +global using Moq; +global using FluentAssertions; +global using Pulumi.Testing; +global using Pulumi; diff --git a/src/Templates/content/Company.AppName.Infra/Company.AppName.Infra.csproj b/src/Templates/content/Company.AppName.Infra/Company.AppName.Infra.csproj new file mode 100644 index 00000000..6a6f35af --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Company.AppName.Infra.csproj @@ -0,0 +1,28 @@ + + + + Exe + net6.0 + enable + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Templates/content/Company.AppName.Infra/Components/Apps.cs b/src/Templates/content/Company.AppName.Infra/Components/Apps.cs new file mode 100644 index 00000000..914e2684 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Components/Apps.cs @@ -0,0 +1,309 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Threading.Tasks; +using Pulumi; +using Pulumi.AzureNative.Storage; +using Pulumi.AzureNative.Web; +using Pulumi.AzureNative.Web.Inputs; +using AzureNative = Pulumi.AzureNative; + +namespace Company.AppName.Infra.Components; + +public class Apps : ComponentResource +{ + private readonly FunctionArgs args; + + public Output FunctionHealthUrl { get; } = default!; + public Output FunctionSwaggerUrl { get; } = default!; + public Output AppSwaggerUrl { get; } = default!; + public Output FunctionPrincipalId { get; } = default!; + public Output AppPrincipalId { get; } = default!; + public Output FunctionOutboundIps { get; } = default!; + public Output AppOutboundIps { get; } = default!; + + public Apps(string name, FunctionArgs args, ComponentResourceOptions? options = null) + : base("coreexinfra:web:apps", name, options) + { + this.args = args; + + // publish app and push zip packages to blob storage when app deployment is done via pulumi + Output> packageZips = args.IsAppDeploymentEnabled.Apply(async isEnabled => + { + if (isEnabled) + { + await PublishApp(); + + var appZipUrl = PrepareAppForDeployment("app", "../Company.AppName.Api/bin/Release/net6.0/publish"); + var funZipUrl = PrepareAppForDeployment("function", "../Company.AppName.Functions/bin/Release/net6.0/publish"); + + return Output.Tuple(appZipUrl, funZipUrl); + } + + return Output.Create((string.Empty, string.Empty)); + }); + + var packageUrls = packageZips.Apply(t => t); + + // https://github.com/pulumi/examples/blob/master/azure-cs-functions/FunctionsStack.cs + var appServicePlan = new AppServicePlan("apps-linux-asp", new() + { + HyperV = false, + IsSpot = false, + IsXenon = false, + Kind = "Linux", // what kinds are supported? "app" is one of them + MaximumElasticWorkerCount = 1, + PerSiteScaling = false, + + // For Linux, you need to change the plan to have Reserved = true property. + Reserved = true, + + ResourceGroupName = args.ResourceGroupName, + Sku = new SkuDescriptionArgs + { + Capacity = 1, + Family = "B1", + Name = "B1", + Size = "B1", + Tier = "Basic", + }, + TargetWorkerCount = 0, + TargetWorkerSizeId = 0, + + Tags = args.Tags + }, new CustomResourceOptions { Parent = this }); + + var app = new WebApp("app", new WebAppArgs + { + ResourceGroupName = args.ResourceGroupName, + HttpsOnly = true, + ServerFarmId = appServicePlan.Id, + Identity = new ManagedServiceIdentityArgs { Type = ManagedServiceIdentityType.SystemAssigned }, + + SiteConfig = new SiteConfigArgs + { + LinuxFxVersion = "DOTNETCORE|6.0", + AppSettings = new[] + { + new NameValuePairArgs{ + Name = "WEBSITE_RUN_FROM_PACKAGE", + // set to 1 if app is going to be deployed separately + Value = args.IsAppDeploymentEnabled.Apply(isEnabled => isEnabled ? packageUrls.Apply(p => p.appZipUrl) : Output.Create("1")) + }, + new NameValuePairArgs{ + Name = "AzureWebJobsStorage__accountName", + Value = args.StorageAccountName + }, + new NameValuePairArgs{ + Name = "APPINSIGHTS_INSTRUMENTATIONKEY", + Value = args.ApplicationInsightsInstrumentationKey + }, + new NameValuePairArgs{ + Name = "APPLICATIONINSIGHTS_CONNECTION_STRING", + Value = args.ApplicationInsightsInstrumentationKey.Apply(key => $"InstrumentationKey={key}"), + }, + new NameValuePairArgs{ + Name = "ApplicationInsightsAgent_EXTENSION_VERSION", + Value = "~2", + }, + new NameValuePairArgs{ + Name = "ServiceBusConnection__fullyQualifiedNamespace", + Value = Output.Format($"{args.ServiceBusNamespaceName}.servicebus.windows.net"), + }, + new NameValuePairArgs{ + Name = "HttpLogContent", + Value = "true", + }, + new NameValuePairArgs{ + Name = "AzureFunctionsJobHost__logging__logLevel__CoreEx", + Value = "Debug", + }, + new NameValuePairArgs{ + Name = "ConnectionStrings__Database", + Value = args.SqlConnectionString, + }, + new NameValuePairArgs{ + Name = "VerificationQueueName", + Value = args.VerificationResultsQueue, + }, + }, + }, + Tags = args.Tags, + }, new CustomResourceOptions { Parent = this }); + + var functionApp = new WebApp("funApp", new WebAppArgs + { + Kind = "FunctionApp", + ResourceGroupName = args.ResourceGroupName, + ServerFarmId = appServicePlan.Id, + HttpsOnly = true, + Identity = new ManagedServiceIdentityArgs { Type = AzureNative.Web.ManagedServiceIdentityType.SystemAssigned }, + SiteConfig = new SiteConfigArgs + { + AppSettings = new[] + { + new NameValuePairArgs{ + Name = "AzureWebJobsStorage__accountName", + Value = args.StorageAccountName + }, + new NameValuePairArgs{ + Name = "FUNCTIONS_EXTENSION_VERSION", + Value = "~4", + }, + new NameValuePairArgs{ + Name = "FUNCTIONS_WORKER_RUNTIME", + Value = "dotnet", + }, + new NameValuePairArgs{ + Name = "WEBSITE_RUN_FROM_PACKAGE", + // set to 1 if app is going to be deployed separately + Value = args.IsAppDeploymentEnabled.Apply(isEnabled => isEnabled ? packageUrls.Apply(p => p.funZipUrl) : Output.Create("1")) + }, + new NameValuePairArgs{ + Name = "APPLICATIONINSIGHTS_CONNECTION_STRING", + Value = Output.Format($"InstrumentationKey={args.ApplicationInsightsInstrumentationKey}"), + }, + new NameValuePairArgs{ + Name = "ServiceBusConnection__fullyQualifiedNamespace", + Value = Output.Format($"{args.ServiceBusNamespaceName}.servicebus.windows.net"), + }, + new NameValuePairArgs{ + Name = "AgifyApiEndpointUri", + Value = "https://api.agify.io", + }, + new NameValuePairArgs{ + Name = "NationalizeApiClientApiEndpointUri", + Value = "https://api.nationalize.io", + }, + new NameValuePairArgs{ + Name = "GenderizeApiClientApiEndpointUri", + Value = "https://api.genderize.io", + }, + new NameValuePairArgs{ + Name = "VerificationQueueName", + Value = args.VerificationResultsQueue, + }, + new NameValuePairArgs{ + Name = "VerificationResultsQueueName", + Value = args.VerificationResultsQueue, + }, + new NameValuePairArgs{ + Name = "MassPublishQueueName", + Value = args.MassPublishQueue, + }, + new NameValuePairArgs{ + Name = "HttpLogContent", + Value = "true", + }, + new NameValuePairArgs{ + Name = "AzureFunctionsJobHost__logging__logLevel__CoreEx", + Value = "Debug", + }, + new NameValuePairArgs{ + Name = "ConnectionStrings__Database", + Value = args.SqlConnectionString, + }, + }, + }, + Tags = args.Tags, + }, new CustomResourceOptions + { + Parent = this, + CustomTimeouts = new CustomTimeouts + { + Create = TimeSpan.FromMinutes(4) + } + }); + + FunctionPrincipalId = functionApp.Identity.Apply(identity => identity?.PrincipalId ?? "11111111-1111-1111-1111-111111111111"); + AppPrincipalId = app.Identity.Apply(identity => identity?.PrincipalId ?? "11111111-1111-1111-1111-111111111111"); + + FunctionOutboundIps = functionApp.OutboundIpAddresses; + AppOutboundIps = app.OutboundIpAddresses; + + // sleep 10s because Azure... List Host Keys method sometimes fails with HTTP 400, re-running stack fixes it. + if (!Deployment.Instance.IsDryRun) + { + Log.Info("Waiting 10s before calling function to get host keys"); + System.Threading.Thread.Sleep(10000); + } + + var keys = Output.CreateSecret(ListWebAppHostKeys.Invoke(new ListWebAppHostKeysInvokeArgs + { + Name = functionApp.Name, + ResourceGroupName = args.ResourceGroupName + }, new InvokeOptions { Parent = functionApp })); + + Output.Tuple(args.IsAppDeploymentEnabled.ToOutput(), functionApp.DefaultHostName, keys) + .Apply(t => + { + var (isAppDeploymentEnabled, defaultHostName, keys) = t; + + if (isAppDeploymentEnabled) + { + Log.Info("Syncing triggers for azure function"); + var syncUrl = $"https://{defaultHostName}/admin/host/synctriggers?code={keys.MasterKey}"; + + using var httpClient = new HttpClient(); + return httpClient.PostAsync(syncUrl, null); + } + + return Task.FromResult(default!); + }); + + FunctionHealthUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/health?code={keys.Apply(k => k.MasterKey)}"); + FunctionSwaggerUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/swagger/ui?code={keys.Apply(k => k.MasterKey)}"); + AppSwaggerUrl = Output.Format($"https://{app.DefaultHostName}/swagger/index.html"); + + RegisterOutputs(); + } + + private static async Task PublishApp() + { + Log.Info("Setting up deployments from zip for the app and function and executing [dotnet publish]"); + + var sw = Stopwatch.StartNew(); + var publishProcess = Process.Start(new ProcessStartInfo + { + WorkingDirectory = "../", + FileName = "dotnet", + Arguments = "publish --nologo -c RELEASE", + RedirectStandardOutput = true, + RedirectStandardError = true + }); + await publishProcess!.WaitForExitAsync(); + sw.Stop(); + Log.Info($"[dotnet publish] completed in {sw.Elapsed}"); + } + + private Output PrepareAppForDeployment(string name, string path) + { + var blob = new Blob($"{name}_zip", new BlobArgs + { + AccountName = args.StorageAccountName, + ContainerName = args.StorageDeploymentContainerName, + ResourceGroupName = args.ResourceGroupName, + Type = BlobType.Block, + Source = new FileArchive(path) + }, new CustomResourceOptions { Parent = this }); + + var codeBlobUrl = Output.Format($"https://{args.StorageAccountName}.blob.core.windows.net/{args.StorageDeploymentContainerName}/{blob.Name}"); + + return codeBlobUrl; + } + + public class FunctionArgs + { + public Input ResourceGroupName { get; set; } = default!; + public Input StorageAccountName { get; set; } = default!; + public Input ServiceBusNamespaceName { get; set; } = default!; + public Input SqlConnectionString { get; set; } = default!; + public Input ApplicationInsightsInstrumentationKey { get; set; } = default!; + public InputMap Tags { get; set; } = default!; + public string PendingVerificationsQueue { get; set; } = default!; + public string VerificationResultsQueue { get; set; } = default!; + public string MassPublishQueue { get; set; } = default!; + public Input IsAppDeploymentEnabled { get; set; } = default!; + public Input StorageDeploymentContainerName { get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs b/src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs new file mode 100644 index 00000000..4e332804 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs @@ -0,0 +1,46 @@ +using Pulumi; +using Pulumi.AzureNative.Insights.V20200202; +using Pulumi.AzureNative.OperationalInsights; + +namespace Company.AppName.Infra.Components; + +public class Diagnostics : ComponentResource +{ + public Output InstrumentationKey { get; } = default!; + + public Diagnostics(string name, DiagnosticsArgs args, ComponentResourceOptions? options = null) + : base("coreexinfra:web:diagnostics", name, options) + { + // Log Analytics Workspace + var workspace = new Workspace("workspace", new() + { + ResourceGroupName = args.ResourceGroupName, + RetentionInDays = 30, + Sku = new Pulumi.AzureNative.OperationalInsights.Inputs.WorkspaceSkuArgs + { + Name = "PerGB2018", + }, + Tags = args.Tags, + WorkspaceName = "lw-workspace", + }, new CustomResourceOptions { Parent = this }); + + // Application insights + var appInsights = new Component("appInsights", new ComponentArgs + { + ApplicationType = ApplicationType.Web, + Kind = "web", + ResourceGroupName = args.ResourceGroupName, + WorkspaceResourceId = workspace.Id, + Tags = args.Tags + }, new CustomResourceOptions { Parent = this }); + + InstrumentationKey = appInsights.InstrumentationKey; + RegisterOutputs(); + } + + public class DiagnosticsArgs + { + public Input ResourceGroupName { get; set; } = default!; + public InputMap Tags { get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Components/Messaging.cs b/src/Templates/content/Company.AppName.Infra/Components/Messaging.cs new file mode 100644 index 00000000..45c57d2a --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Components/Messaging.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using Pulumi; +using Pulumi.AzureNative.Authorization; +using Pulumi.AzureNative.ServiceBus; +using AzureNative = Pulumi.AzureNative; + +namespace Company.AppName.Infra.Components; + +public class Messaging : ComponentResource +{ + private readonly MessagingArgs args; + private readonly string name; + + public Output NamespaceName { get; } = default!; + public Output NamespaceId { get; } = default!; + + public Messaging(string name, MessagingArgs args, ComponentResourceOptions? options = null) + : base("coreexinfra:web:messaging", name, options) + { + this.args = args; + this.name = name; + + var @namespace = new Namespace(name, new NamespaceArgs + { + ResourceGroupName = args.ResourceGroupName, + Sku = new AzureNative.ServiceBus.Inputs.SBSkuArgs + { + Name = SkuName.Standard, + Tier = SkuTier.Standard, + }, + Tags = args.Tags + }, new CustomResourceOptions { Parent = this }); + + NamespaceName = @namespace.Name; + NamespaceId = @namespace.Id; + + RegisterOutputs(); + } + + public Queue AddQueue(string queueName, bool batchOperationsEnabled = false) + { + return new Queue($"{name}-queue-{queueName}", new() + { + EnablePartitioning = false, + NamespaceName = NamespaceName, + QueueName = queueName, + ResourceGroupName = args.ResourceGroupName, + MaxDeliveryCount = 3, + EnableBatchedOperations = batchOperationsEnabled + }, new CustomResourceOptions { Parent = this }); + } + + public IEnumerable AddAccess(Input principalId, string name) + { + var receive_permission = new RoleAssignment( + $"receive-for-{name}", + new RoleAssignmentArgs + { + Description = $"{name} receiving data from service bus", + PrincipalId = principalId, + PrincipalType = "ServicePrincipal", + RoleDefinitionId = Roles.BuiltInRolesIds.ServiceBusDataReceiver, + Scope = NamespaceId + }, + new CustomResourceOptions { Parent = this } + ); + + var send_permission = new RoleAssignment( + $"send-for-{name}", + new RoleAssignmentArgs + { + Description = $"{name} sending data to service bus", + PrincipalId = principalId, + PrincipalType = "ServicePrincipal", + + RoleDefinitionId = Roles.BuiltInRolesIds.ServiceBusDataSender, + Scope = NamespaceId + }, + new CustomResourceOptions { Parent = this } + ); + + return new[] { receive_permission, send_permission }; + } + + public class MessagingArgs + { + public Input ResourceGroupName { get; set; } = default!; + public InputMap Tags { get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Components/Sql.cs b/src/Templates/content/Company.AppName.Infra/Components/Sql.cs new file mode 100644 index 00000000..293be7cd --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Components/Sql.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using System.Net.Http; +using Company.AppName.Infra.Services; +using Pulumi; +using Pulumi.AzureNative.Sql; +using Pulumi.AzureNative.Sql.Inputs; +using AD = Pulumi.AzureAD; +using Deployment = Pulumi.Deployment; + +namespace Company.AppName.Infra.Components; + +public class Sql : ComponentResource +{ + private readonly SqlArgs args; + private readonly HashSet firewallAllowedIps = new(); + + public Output SqlDatabaseConnectionString { get; } + public Output SqlServerName { get; } + public Output SqlDatabaseAuthorizedGroupId { get; } + + public Sql(string name, SqlArgs args, IDbOperations dbOperations, ComponentResourceOptions? options = null) + : base("coreexinfra:web:sql", name, options) + { + this.args = args; + var sqlAdAdmin = new AD.User("sqlAdmin", new AD.UserArgs + { + UserPrincipalName = args.SqlAdAdminLogin, + Password = args.SqlAdAdminPassword, + DisplayName = $"Global SQL Admin {Deployment.Instance.StackName}" + }, new CustomResourceOptions { Parent = this }); + + var sqlServer = new Server($"sql-server-{Deployment.Instance.StackName}", new ServerArgs + { + ResourceGroupName = args.ResourceGroupName, + Administrators = new ServerExternalAdministratorArgs + { + Login = sqlAdAdmin.UserPrincipalName, + Sid = sqlAdAdmin.Id, + AzureADOnlyAuthentication = true, + AdministratorType = AdministratorType.ActiveDirectory, + PrincipalType = PrincipalType.User, + }, + MinimalTlsVersion = "1.2", + Tags = args.Tags + }, new CustomResourceOptions { Parent = this }); + + var publicIp = Output.Create(new HttpClient().GetStringAsync("https://api.ipify.org")); + + var enableLocalMachine = new FirewallRule("AllowLocalMachine", new FirewallRuleArgs + { + ResourceGroupName = args.ResourceGroupName, + ServerName = sqlServer.Name, + StartIpAddress = publicIp, + EndIpAddress = publicIp + }, new CustomResourceOptions { Parent = this }); + + var database = new Pulumi.AzureNative.Sql.Database("sqldb", new DatabaseArgs + { + ResourceGroupName = args.ResourceGroupName, + ServerName = sqlServer.Name, + DatabaseName = "CoreExDB", + Sku = new SkuArgs + { + Name = "Basic" + }, + Tags = args.Tags + }, new CustomResourceOptions { Parent = this }); + + string sqlDatabaseAuthorizedGroupName = $"SqlDbUsersGroup{Deployment.Instance.StackName}"; + var sqlDatabaseAuthorizedGroup = new AD.Group(sqlDatabaseAuthorizedGroupName, new AD.GroupArgs + { + DisplayName = sqlDatabaseAuthorizedGroupName, + SecurityEnabled = true, + Owners = new InputList { sqlAdAdmin.Id } + }, new CustomResourceOptions { Parent = this }); + + var sqlADConnectionString = Output.Format($"Server={sqlServer.Name}.database.windows.net; Authentication=Active Directory Password; User={args.SqlAdAdminLogin}; Password={args.SqlAdAdminPassword}; Database={database.Name}"); + + // login with AD admin credentials to give access to AD group that contains App and Function managed identity users + dbOperations.ProvisionUsers(sqlADConnectionString!, sqlDatabaseAuthorizedGroupName); + + // create SQL user and setup schema + if (args.IsDBSchemaDeploymentEnabled) + { + sqlADConnectionString.Apply(async cs => + { + return await dbOperations.DeployDbSchemaAsync(cs); + }); + } + + // https://docs.microsoft.com/en-us/sql/connect/ado-net/sql/azure-active-directory-authentication?view=sql-server-ver15#using-active-directory-default-authentication + SqlDatabaseConnectionString = Output.Format($"Server={sqlServer.Name}.database.windows.net; Authentication=Active Directory Default; Database={database.Name}"); + SqlServerName = sqlServer.Name; + SqlDatabaseAuthorizedGroupId = sqlDatabaseAuthorizedGroup.Id; + + RegisterOutputs(); + } + + public void AddFirewallRule(Output ips, string name) + { + // this shows warning in Pulumi, but there's no other way to create this resource without doing it in Apply + ips.Apply(ips => + { + foreach (var address in ips.Split(",")) + { + if (!firewallAllowedIps.Contains(address)) + { + firewallAllowedIps.Add(address); + + var enableIp = new FirewallRule("Enable_" + name + "_" + address, new FirewallRuleArgs + { + ResourceGroupName = args.ResourceGroupName, + ServerName = SqlServerName, + StartIpAddress = address, + EndIpAddress = address + }, new CustomResourceOptions { Parent = this }); + } + } + + return true; + }); + } + + public void AddToSqlDatabaseAuthorizedGroup(string name, Output principalId) + { + var appGroupMember = new AD.GroupMember(name, new() + { + GroupObjectId = SqlDatabaseAuthorizedGroupId, + MemberObjectId = principalId, + }, new CustomResourceOptions { Parent = this }); + } + + public class SqlArgs + { + public Input ResourceGroupName { get; set; } = default!; + public InputMap Tags { get; set; } = default!; + public Input SqlAdAdminLogin { get; set; } = default!; + public Input SqlAdAdminPassword { get; set; } = default!; + public bool IsDBSchemaDeploymentEnabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Components/Storage.cs b/src/Templates/content/Company.AppName.Infra/Components/Storage.cs new file mode 100644 index 00000000..97458a6e --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Components/Storage.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Pulumi; +using Pulumi.AzureNative.Authorization; +using Pulumi.AzureNative.Storage; + +using AzureNative = Pulumi.AzureNative; + +namespace Company.AppName.Infra.Components; + +public class Storage : ComponentResource +{ + private readonly Output id = default!; + public Output AccountName { get; private set; } = default!; + public Output DeploymentContainerName { get; private set; } = default!; + public Output ConnectionString { get; private set; } = default!; + + public Storage(string name, StorageArgs args, ComponentResourceOptions? options = null) + : base("coreexinfra:web:storage", name, options) + { + // Create an Azure resource (Storage Account) + var storageAccount = new StorageAccount(name, new StorageAccountArgs + { + ResourceGroupName = args.ResourceGroupName, + Sku = new AzureNative.Storage.Inputs.SkuArgs + { + Name = SkuName.Standard_LRS + }, + Kind = Kind.StorageV2, + AllowBlobPublicAccess = false, + EnableHttpsTrafficOnly = true, + MinimumTlsVersion = "TLS1_2", + Tags = args.Tags + }, new CustomResourceOptions { Parent = this }); + + var deploymentContainer = new BlobContainer("zips-container", new BlobContainerArgs + { + AccountName = storageAccount.Name, + PublicAccess = PublicAccess.None, + ResourceGroupName = args.ResourceGroupName, + }, new CustomResourceOptions { Parent = this }); + + var connectionString = GetConnectionString(args.ResourceGroupName, storageAccount.Name); + + AccountName = storageAccount.Name!; + id = storageAccount.Id!; + ConnectionString = connectionString; + DeploymentContainerName = deploymentContainer.Name; + + RegisterOutputs(); + } + + private static Output GetConnectionString(Input resourceGroupNameInput, Input accountNameInput) + { + return Output.Tuple(resourceGroupNameInput, accountNameInput) + .Apply(async t => + { + var (resourceGroupName, accountName) = t; + + var storageAccountKeys = await ListStorageAccountKeys.InvokeAsync(new ListStorageAccountKeysArgs + { + ResourceGroupName = resourceGroupName, + AccountName = accountName + }); + + // Retrieve the primary storage account key. + return $"DefaultEndpointsProtocol=https;AccountName={accountNameInput};AccountKey={storageAccountKeys.Keys.First().Value}"; + }); + } + + public RoleAssignment AddAccess(Input principalId, string name) + { + return new RoleAssignment( + $"useblob-for-{name}", + new RoleAssignmentArgs + { + Description = $"{name} accessing storage account", + PrincipalId = principalId, + PrincipalType = "ServicePrincipal", + RoleDefinitionId = Roles.BuiltInRolesIds.StorageBlobDataOwner, + Scope = id + }, + new CustomResourceOptions { Parent = this } + ); + } + + public class StorageArgs + { + public Input ResourceGroupName { get; set; } = default!; + public InputMap Tags { get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/CoreExStack.cs b/src/Templates/content/Company.AppName.Infra/CoreExStack.cs new file mode 100644 index 00000000..cff6be05 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/CoreExStack.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Company.AppName.Infra.Services; +using Pulumi; +using Pulumi.AzureNative.Resources; +using AD = Pulumi.AzureAD; + +namespace Company.AppName.Infra; + +public static class CoreExStack +{ + public static async Task> ExecuteStackAsync(IDbOperations dbOperations) + { + var config = await StackConfiguration.CreateConfiguration(); + Log.Info("Configuration completed"); + + var tags = new InputMap { { "App", "CoreEx" } }; + + // Create an Azure Resource Group + var resourceGroup = new ResourceGroup($"coreEx-{Pulumi.Deployment.Instance.StackName}", new ResourceGroupArgs + { + Tags = tags + }); + + var serviceBus = new Components.Messaging("coreExBus", new Components.Messaging.MessagingArgs + { + ResourceGroupName = resourceGroup.Name, + Tags = tags + }); + serviceBus.AddQueue(config.PendingVerificationsQueue); + serviceBus.AddQueue(config.VerificationResultsQueue!); + serviceBus.AddQueue(config.MassPublishQueue, batchOperationsEnabled: true); + + var storage = new Components.Storage("sa", new Components.Storage.StorageArgs + { + ResourceGroupName = resourceGroup.Name, + Tags = tags + }); + + var appInsights = new Components.Diagnostics("insights", new Components.Diagnostics.DiagnosticsArgs + { + ResourceGroupName = resourceGroup.Name, + Tags = tags + }); + + var sql = new Components.Sql("sql", new Components.Sql.SqlArgs + { + ResourceGroupName = resourceGroup.Name, + SqlAdAdminLogin = config.SqlAdAdminLogin!, + SqlAdAdminPassword = config.SqlAdAdminPassword!, + IsDBSchemaDeploymentEnabled = config.IsDBSchemaDeploymentEnabled, + Tags = tags + }, dbOperations); + + var apps = new Components.Apps("apps", new Components.Apps.FunctionArgs + { + ResourceGroupName = resourceGroup.Name, + StorageAccountName = storage.AccountName, + StorageDeploymentContainerName = storage.DeploymentContainerName, + ServiceBusNamespaceName = serviceBus.NamespaceName, + SqlConnectionString = sql.SqlDatabaseConnectionString, + ApplicationInsightsInstrumentationKey = appInsights.InstrumentationKey, + PendingVerificationsQueue = config.PendingVerificationsQueue, + VerificationResultsQueue = config.VerificationResultsQueue, + MassPublishQueue = config.MassPublishQueue, + IsAppDeploymentEnabled = config.IsAppsDeploymentEnabled, + Tags = tags + }); + + // Permissions for function app + storage.AddAccess(apps.FunctionPrincipalId, "functionApp"); + serviceBus.AddAccess(apps.FunctionPrincipalId, "functionApp"); + + // Permissions for app service + storage.AddAccess(apps.AppPrincipalId, "appService"); + serviceBus.AddAccess(apps.AppPrincipalId, "appService"); + + // allow app and function to query/use DB + sql.AddToSqlDatabaseAuthorizedGroup("functionGroupMember", apps.FunctionPrincipalId); + sql.AddToSqlDatabaseAuthorizedGroup("appGroupMember", apps.AppPrincipalId); + + // allow app and function through SQL firewall + sql.AddFirewallRule(apps.FunctionOutboundIps, "appService"); + sql.AddFirewallRule(apps.AppOutboundIps, "appService"); + + return new Dictionary + { + ["SqlDatabaseConnectionString"] = sql.SqlDatabaseConnectionString, + ["FunctionHealthUrl"] = apps.FunctionHealthUrl, + ["FunctionSwaggerUrl"] = apps.FunctionSwaggerUrl, + ["AppSwaggerUrl"] = apps.AppSwaggerUrl, + }; + } + + public class StackConfiguration + { + public Input? SqlAdAdminLogin { get; private set; } + public Input? SqlAdAdminPassword { get; private set; } + public bool IsAppsDeploymentEnabled { get; private set; } + public bool IsDBSchemaDeploymentEnabled { get; private set; } + public string PendingVerificationsQueue { get; private set; } = default!; + public string VerificationResultsQueue { get; private set; } = default!; + public string MassPublishQueue { get; private set; } = default!; + + private StackConfiguration() { } + + public static async Task CreateConfiguration() + { + // read stack config + var config = new Config(); + + // get some info from Azure AD + var domainResult = await AD.GetDomains.InvokeAsync(new AD.GetDomainsArgs { OnlyDefault = true }); + var defaultUsername = $"sqlGlobalAdAdmin{Pulumi.Deployment.Instance.StackName}@{domainResult.Domains[0].DomainName}"; + var defaultPassword = new Pulumi.Random.RandomPassword("sqlAdPassword", new() + { + Length = 32, + Upper = true, + Number = true, + Special = true, + OverrideSpecial = "@", + MinLower = 2, + MinUpper = 2, + MinSpecial = 2, + MinNumeric = 2 + }).Result; + + Log.Info($"Default username is: {defaultUsername}"); + + return new StackConfiguration + { + SqlAdAdminLogin = Extensions.GetConfigValue("sqlAdAdmin", defaultUsername), + SqlAdAdminPassword = Extensions.GetConfigValue("sqlAdPassword", defaultPassword), + IsAppsDeploymentEnabled = config.GetBoolean("isAppsDeploymentEnabled") ?? false, + IsDBSchemaDeploymentEnabled = config.GetBoolean("isDBSchemaDeploymentEnabled") ?? false, + + PendingVerificationsQueue = config.Get("pendingVerificationsQueue") ?? "pendingVerifications", + VerificationResultsQueue = config.Get("verificationResultsQueue") ?? "verificationResults", + MassPublishQueue = config.Get("massPublishQueue") ?? "massPublish" + }; + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Extensions.cs b/src/Templates/content/Company.AppName.Infra/Extensions.cs new file mode 100644 index 00000000..c6cc88e6 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Extensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using Pulumi; + +namespace Company.AppName.Infra; + +public static class Extensions +{ + public static Input GetConfigValue(string name, Input defaultValue) + { + var config = new Config(); + + var configValue = config.Get(name); + + if (string.IsNullOrEmpty(configValue)) + { + Log.Info($"Defaulting {name} because it wasn't present in configuration"); + return defaultValue.ToOutput(); + } + else + { + return Output.Create(configValue); + } + } + + public static Task GetValue(this Output output) => output.GetValue(_ => _); + + public static Task GetValue(this Output output, Func valueResolver) + { + var tcs = new TaskCompletionSource(); + output.Apply(_ => + { + var result = valueResolver(_); + tcs.SetResult(result); + return result; + }); + return tcs.Task; + } + + public static Task GetValue(this Input input) => input.GetValue(_ => _); + + public static Task GetValue(this Input input, Func valueResolver) + { + var tcs = new TaskCompletionSource(); + input.Apply(_ => + { + var result = valueResolver(_); + tcs.SetResult(result); + return result; + }); + return tcs.Task; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Program.cs b/src/Templates/content/Company.AppName.Infra/Program.cs new file mode 100644 index 00000000..111c6283 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Program.cs @@ -0,0 +1,29 @@ +// build CoreEx stack +using Company.AppName.Infra.Services; +using Pulumi; + +return await Deployment.RunAsync(() => +{ + // create and use actual instance of DB Operations service + return Company.AppName.Infra.CoreExStack.ExecuteStackAsync(new DbOperations()); +}, new StackOptions +{ + // apply auto-tagging transformation + // https://gist.github.com/dbeattie71/1f8a1a9264ceb8161ad4c49de1ee3bb3 + ResourceTransformations = new System.Collections.Generic.List{ + (args) => { + var tagsProp = args.Args.GetType().GetProperty("Tags"); + + if(tagsProp?.GetValue(args.Args) is InputMap tags) + { + Log.Debug("Adding tags to " + args.Resource.GetResourceName()); + + tags.Add("user:Stack", Deployment.Instance.StackName); + tags.Add("user:Project", Deployment.Instance.ProjectName); + tags.Add("App:Name", "CoreEx"); + } + + return new ResourceTransformationResult(args.Args, args.Options); + } + } +}); diff --git a/src/Templates/content/Company.AppName.Infra/Pulumi.yaml b/src/Templates/content/Company.AppName.Infra/Pulumi.yaml new file mode 100644 index 00000000..fc356f26 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Pulumi.yaml @@ -0,0 +1,14 @@ +name: Company.AppName.Infra +runtime: dotnet +description: Infrastructure for CoreEx sample application +template: + config: + azure-native:location: + description: The Azure region to deploy into + default: EastUs + Company.AppName.Infra:isAppsDeploymentEnabled: + description: Whether Application code should be deployed + default: true + Company.AppName.Infra:isDBSchemaDeploymentEnabled: + description: Whether Database schema should be deployed + default: true diff --git a/src/Templates/content/Company.AppName.Infra/Readme.md b/src/Templates/content/Company.AppName.Infra/Readme.md new file mode 100644 index 00000000..dc69f9cd --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Readme.md @@ -0,0 +1,57 @@ +# About + +Infrastructure is built with [Pulumi](https://www.pulumi.com/). + +The easiest way to deploy it is by using Pulumi account (Free), but it's not mandatory. + +Prerequisites: + +1. [Pulumi CLI](https://www.pulumi.com/docs/get-started/install/) +2. Azure CLI - logged in to Azure + +## Pulumi with azure storage + +Pulumi can be used without Pulumi Account, by using [Azure Storage as backend](https://www.techwatching.dev/posts/pulumi-azure-backend). + +1. set the `AZURE_STORAGE_ACCOUNT` environment variable to specify the Azure storage account to use +1. set the `AZURE_STORAGE_KEY` or the `AZURE_STORAGE_SAS_TOKEN` environment variables to let Pulumi access the storage +1. execute the following command `pulumi login azblob://` where container-path is the path to a blob container in the storage account + +## Configuring Pulumi (optional) + +Infrastructure project has only 2 settings: + +* `Company.AppName.Infra:isAppsDeploymentEnabled` for controlling application deployment via zip deploy +* `Company.AppName.Infra:isDBSchemaDeploymentEnabled` for publishing Database schema and data + +> When `isAppsDeploymentEnabled` flag is set, pulumi code executes `dotnet publish -c RELEASE` to create app packages. + +Pulumi can be configured and previewed with: + +```bash +pulumi preview -c azure-native:location=EastUs -c Company.AppName.Infra:isAppsDeploymentEnabled=true -c Company.AppName.Infra:isDBSchemaDeploymentEnabled=true +``` + +which creates a stack config file `Pulumi.dev.yaml` + +```yaml +config: + azure-native:location: EastUs + Company.AppName.Infra:isAppsDeploymentEnabled: true + Company.AppName.Infra:isDBSchemaDeploymentEnabled: true +``` + +## Deploy with Pulumi + +To deploy in `samples/Company.AppName/Company.AppName.Infra` run `pulumi up -c azure-native:location=EastUs -c Company.AppName.Infra:isAppsDeploymentEnabled=true -c Company.AppName.Infra:isDBSchemaDeploymentEnabled=true` + +To display outputs of the stack deployment run: `pulumi stack output --show-secrets` which will display function links with secret api key. + +## Alternative deployment methods + +Apps can also be deployed with Azure CLI, once published apps are zipped. + +```bash +az webapp deploy --resource-group coreEx-dev4011fb65 --name app17b7c4c8 --src-path app.zip +az functionapp deployment source config-zip -g coreEx-dev4011fb65 -n fun17b7c4c8 --src fun.zip +``` diff --git a/src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs b/src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs new file mode 100644 index 00000000..02a19045 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Serialization; +using Pulumi.AzureNative.Authorization; + +namespace Company.AppName.Infra.Roles; + +public static class BuiltInRolesIds +{ + public const string StorageBlobDataOwner = "/providers/Microsoft.Authorization/roleDefinitions/b7e6dc6d-f1e8-4753-8033-0f276bb0955b"; + + // https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-receiver + public const string ServiceBusDataReceiver = "/providers/Microsoft.Authorization/roleDefinitions/4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0"; + + // https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-sender + public const string ServiceBusDataSender = "/providers/Microsoft.Authorization/roleDefinitions/69a216fc-b8fb-44d8-bc22-1f3c2cd27a39"; + + /// + /// Gets role id based on the provided role name. This method can be used instead of hardcoded role Ids above + /// + /// + /// + /// + /// Thrown when request fails + /// code from: https://github.com/pulumi/examples/blob/28b559d68eb6a67f3e6b5fb3d2a337b5b9ed35d5/azure-cs-call-azure-api/Program.cs#L45 + public static async System.Threading.Tasks.Task GetRoleIdByName(string roleName, string? scope = null) + { + var config = await GetClientConfig.InvokeAsync(); + var token = await GetClientToken.InvokeAsync(); + + // Unfortunately, Microsoft hasn't shipped an .NET5-compatible SDK at the time of writing this. + // So, we have to hand-craft an HTTP request to retrieve a role definition. + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + var response = await httpClient.GetAsync($"https://management.azure.com/subscriptions/{config.SubscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-01-01-preview&$filter=roleName%20eq%20'{roleName}'"); + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Request failed with {response.StatusCode}"); + } + var body = await response.Content.ReadAsStringAsync(); + var definition = JsonSerializer.Deserialize(body)!; + return definition.Value[0].id; + } + + public class RoleDefinition + { + [JsonPropertyName("value")] + public List Value { get; set; } = default!; + } + public class RoleDefinitionValue + { + [JsonPropertyName("id")] + public string id { get; set; } = default!; + + [JsonPropertyName("type")] + public string Type { get; set; } = default!; + + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Services/DbOperations.cs b/src/Templates/content/Company.AppName.Infra/Services/DbOperations.cs new file mode 100644 index 00000000..213ffec1 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Services/DbOperations.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using Dapper; +using Microsoft.Data.SqlClient; +using Pulumi; + +namespace Company.AppName.Infra.Services; + +public class DbOperations : IDbOperations +{ + public void ProvisionUsers(Input connectionString, string groupName) + { + if (Deployment.Instance.IsDryRun) + // skip in dry run + return; + + Log.Info($"Provisioning user {groupName} in SQL DB"); + string commandText = @$" + IF NOT EXISTS (SELECT [name] + FROM [sys].[database_principals] + WHERE [type] = N'X' AND [name] = N'{groupName}') + BEGIN + CREATE USER {groupName} FROM EXTERNAL PROVIDER; + END + + ALTER ROLE db_datareader ADD MEMBER {groupName}; + ALTER ROLE db_datawriter ADD MEMBER {groupName}; + "; + + connectionString.Apply(async cs => + { + using SqlConnection conn = new(cs); + await conn.OpenAsync(); + + var result = await conn.ExecuteAsync(commandText); + return true; + }); + } + + public Task DeployDbSchemaAsync(string connectionString) + { + if (Deployment.Instance.IsDryRun) + // skip in dry run + return Task.FromResult(0); + + Log.Info($"Deploying DB schema using {connectionString}"); + return Database.Program.RunMigrator(connectionString, assembly: typeof(Company.AppName.Database.Program).Assembly, "DeployWithData"); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Services/IDbOperations.cs b/src/Templates/content/Company.AppName.Infra/Services/IDbOperations.cs new file mode 100644 index 00000000..9137891d --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Services/IDbOperations.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Pulumi; + +namespace Company.AppName.Infra.Services; + +public interface IDbOperations +{ + Task DeployDbSchemaAsync(string connectionString); + void ProvisionUsers(Input connectionString, string groupName); +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj b/src/Templates/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj new file mode 100644 index 00000000..47dddf38 --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj @@ -0,0 +1,44 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml b/src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml new file mode 100644 index 00000000..ae4d854e --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml @@ -0,0 +1,10 @@ +Hr: + - Employee: + - { EmployeeId: 1, Email: w.jones@org.com, FirstName: Wendy, LastName: Jones, GenderCode: F, Birthday: 1985-03-18, StartDate: 2000-12-11, PhoneNo: (425) 612 8113 } + - { EmployeeId: 2, Email: b.smith@org.com, FirstName: Brian, LastName: Smith, GenderCode: M, Birthday: 1994-11-07, StartDate: 2013-08-06, TerminationDate: 2015-04-08, TerminationReasonCode: RE, PhoneNo: (429) 120 0098 } + - { EmployeeId: 3, Email: r.Browne@org.com, FirstName: Rachael, LastName: Browne, GenderCode: F, Birthday: 1972-06-28, StartDate: 2019-11-06, PhoneNo: (421) 783 2343 } + - { EmployeeId: 4, Email: w.smither@org.com, FirstName: Waylon, LastName: Smithers, GenderCode: M, Birthday: 1952-02-21, StartDate: 2001-01-22, PhoneNo: (428) 893 2793, AddressJson: '{ "street1": "8365 851 PL NE", "city": "Redmond", "state": "WA", "postCode": "98052" }' } + - EmergencyContact: + - { EmergencyContactId: 201, EmployeeId: 2, FirstName: Garth, LastName: Smith, PhoneNo: (443) 678 1827, RelationshipTypeCode: PAR } + - { EmergencyContactId: 202, EmployeeId: 2, FirstName: Sarah, LastName: Smith, PhoneNo: (443) 234 3837, RelationshipTypeCode: PAR } + - { EmergencyContactId: 401, EmployeeId: 4, FirstName: Michael, LastName: Manners, PhoneNo: (234) 297 9834, RelationshipTypeCode: FRD } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest.cs b/src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest.cs new file mode 100644 index 00000000..3fbbed10 --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest.cs @@ -0,0 +1,400 @@ +using CoreEx.Entities; +using CoreEx.Events; +using CoreEx.Http; +using DbEx.Migration; +using DbEx.Migration.Data; +using Microsoft.Extensions.Configuration; +using Company.AppName.Api; +using Company.AppName.Api.Controllers; +using Company.AppName.Business.Models; +using Company.AppName.Business.External.Contracts; +using NUnit.Framework; +using System; +using System.Linq; +using System.Threading.Tasks; +using UnitTestEx; +using UnitTestEx.Expectations; +using UnitTestEx.NUnit; + +namespace Company.AppName.UnitTest +{ + [TestFixture] + [Category("WithDB")] + public class EmployeeControllerTest + { + [OneTimeSetUp] + public static async Task Init() + { + HttpConsts.IncludeFieldsQueryStringName = "include-fields"; + + using var test = ApiTester.Create(); + var cs = test.Configuration.GetConnectionString("Database"); + if (await Database.Program.RunMigrator(cs, typeof(EmployeeControllerTest).Assembly, MigrationCommand.ResetAndAll.ToString()).ConfigureAwait(false) != 0) + Assert.Fail("Database migration failed."); + } + + [Test] + public void A100_Get_NotFound() + { + using var test = ApiTester.Create(); + + test.Controller() + .Run(c => c.GetAsync(404.ToGuid())) + .AssertNotFound(); + } + + [Test] + public void A110_Get_Found() + { + using var test = ApiTester.Create(); + + test.Controller() + .Run(c => c.GetAsync(1.ToGuid())) + .AssertOK() + .Assert(new Employee + { + Id = 1.ToGuid(), + Email = "w.jones@org.com", + FirstName = "Wendy", + LastName = "Jones", + Gender = "F", + Birthday = new DateTime(1985, 03, 18, 0, 0, 0, DateTimeKind.Unspecified), + StartDate = new DateTime(2000, 12, 11, 0, 0, 0, DateTimeKind.Unspecified), + PhoneNo = "(425) 612 8113" + }, nameof(Employee.ETag)); + } + + [Test] + public void A120_Get_NotModifed() + { + using var test = ApiTester.Create(); + + var e = test.Controller() + .Run(c => c.GetAsync(1.ToGuid())) + .AssertOK() + .GetValue()!; + + test.Controller() + .Run(c => c.GetAsync(e.Id), requestOptions: new HttpRequestOptions { ETag = e.ETag }) + .AssertNotModified(); + } + + [Test] + public void A130_Get_IncludeFields() + { + using var test = ApiTester.Create(); + + test.Controller() + .Run(c => c.GetAsync(1.ToGuid()), requestOptions: new HttpRequestOptions().Include("FirstName", "LastName")) + .AssertOK() + .AssertJson("{\"firstName\":\"Wendy\",\"lastName\":\"Jones\"}"); + } + + [Test] + public void B100_GetAll_All() + { + using var test = ApiTester.Create(); + + var v = test.Controller() + .Run(c => c.GetAllAsync()) + .AssertOK() + .GetValue(); + + Assert.IsNotNull(v?.Collection); + Assert.AreEqual(4, v!.Collection.Count); + Assert.AreEqual(new string[] { "Browne", "Jones", "Smith", "Smithers" }, v.Collection.Select(x => x.LastName).ToArray()); + } + + [Test] + public void B110_GetAll_Paging() + { + using var test = ApiTester.Create(); + + var v = test.Controller() + .Run(c => c.GetAllAsync(), requestOptions: new HttpRequestOptions { Paging = PagingArgs.CreateSkipAndTake(1, 2, true) }) + .AssertOK() + .GetValue(); + + Assert.IsNotNull(v?.Collection); + Assert.AreEqual(2, v!.Collection.Count); + Assert.AreEqual(new string[] { "Jones", "Smith" }, v.Collection.Select(x => x.LastName).ToArray()); + Assert.IsNotNull(v.Paging); + Assert.AreEqual(4, v.Paging!.TotalCount); + } + + [Test] + public void B120_GetAll_PagingAndIncludeFields() + { + using var test = ApiTester.Create(); + + var v = test.Controller() + .Run(c => c.GetAllAsync(), requestOptions: new HttpRequestOptions { Paging = PagingArgs.CreateSkipAndTake(1, 2) }.Include("lastname")) + .AssertOK() + .AssertJson("[ { \"lastName\": \"Jones\" }, { \"lastName\": \"Smith\" } ]") + .GetValue(); + + Assert.IsNull(v!.Paging!.TotalCount); // No count requested. + } + + [Test] + public void C100_Create_Error() + { + using var test = ApiTester.Create(); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "Z", + PhoneNo = "555 123 4567", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.Controller() + .Run(c => c.CreateAsync(null!), e) + .AssertErrors( + new ApiError("Email", "'Email' must not be empty."), + new ApiError("Gender", "'Gender' is invalid.")); + } + + [Test] + public void C110_Create_Success() + { + using var test = ApiTester.Create(); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + Email = "rs@email.com", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + var v = test.Controller() + .Run(c => c.CreateAsync(null!), e) + .AssertCreated() + .Assert(e, "Id", "ETag") + .AssertLocationHeader(v => new Uri($"api/employees/{v!.Id}", UriKind.Relative)) + .GetValue(); + + // Do a GET to make sure it is in the database and all fields equal. + test.Controller() + .Run(c => c.GetAsync(v!.Id)) + .AssertOK() + .Assert(v); + } + + [Test] + public void D100_Update_Error() + { + using var test = ApiTester.Create(); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.Controller() + .Run(c => c.UpdateAsync(404.ToGuid(), null!), e) + .AssertErrors( + new ApiError("Email", "'Email' must not be empty.")); + } + + [Test] + public void D110_Update_NotFound() + { + using var test = ApiTester.Create(); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + Email = "rs@email.com", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.Controller() + .Run(c => c.UpdateAsync(404.ToGuid(), null!), e) + .AssertNotFound(); + } + + [Test] + public void D120_Update_Success() + { + using var test = ApiTester.Create(); + + // Get current. + var v = test.Controller() + .Run(c => c.GetAsync(2.ToGuid())) + .AssertOK() + .GetValue()!; + + // Update it. + v.FirstName += "X"; + + v = test.Controller() + .Run(c => c.UpdateAsync(v.Id, null!), v) + .AssertOK() + .Assert(v, "ETag") + .GetValue()!; + + // Get again and check all. + test.Controller() + .Run(c => c.GetAsync(v.Id)) + .AssertOK() + .Assert(v); + } + + [Test] + public void D130_Update_ConcurrencyError() + { + using var test = ApiTester.Create(); + + // Get current. + var v = test.Controller() + .Run(c => c.GetAsync(2.ToGuid())) + .AssertOK() + .GetValue()!; + + // Update it with errant etag. + v.FirstName += "X"; + v.ETag = "ZZZZZZZZZZZZ"; + + test.Controller() + .Run(c => c.UpdateAsync(v.Id, null!), v) + .AssertPreconditionFailed(); + } + + [Test] + public void E100_Delete() + { + using var test = ApiTester.Create(); + + // Get current. + test.Controller() + .Run(c => c.GetAsync(2.ToGuid())) + .AssertOK(); + + // Delete it. + test.Controller() + .Run(c => c.DeleteAsync(2.ToGuid())) + .AssertNoContent(); + + // Must not exist. + test.Controller() + .Run(c => c.GetAsync(2.ToGuid())) + .AssertNotFound(); + + // Delete it again; should appear as if deleted as operation is considered idempotent. + test.Controller() + .Run(c => c.DeleteAsync(2.ToGuid())) + .AssertNoContent(); + } + + [Test] + public void F100_Patch_NotFound() + { + using var test = ApiTester.Create(); + + test.Controller() + .RunContent(c => c.PatchAsync(404.ToGuid(), null!), "{}", HttpConsts.MergePatchMediaTypeName) + .AssertNotFound(); + } + + [Test] + public void F110_Patch_Concurrency() + { + using var test = ApiTester.Create(); + + // Get current. + var v = test.Controller() + .Run(c => c.GetAsync(4.ToGuid())) + .AssertOK() + .GetValue()!; + + // Patch it with errant etag. + v.FirstName += "X"; + + test.Controller() + .RunContent(c => c.PatchAsync(v.Id, null!), $"{{ \"firstName\": \"{v.FirstName}\" }}", new HttpRequestOptions { ETag = "ZZZZZZZZZZZZ" }, HttpConsts.MergePatchMediaTypeName) + .AssertPreconditionFailed(); + } + + [Test] + public void F120_Patch() + { + using var test = ApiTester.Create(); + + // Get current. + var v = test.Controller() + .Run(c => c.GetAsync(4.ToGuid())) + .AssertOK() + .GetValue()!; + + // Patch it with errant etag. + v.FirstName += "X"; + + v = test.Controller() + .RunContent(c => c.PatchAsync(v.Id, null!), $"{{ \"firstName\": \"{v.FirstName}\" }}", new HttpRequestOptions { ETag = v.ETag }, HttpConsts.MergePatchMediaTypeName) + .AssertOK() + .Assert(v, "ETag") + .GetValue()!; + + // Get again and check all. + test.Controller() + .Run(c => c.GetAsync(v.Id)) + .AssertOK() + .Assert(v); + } + + [Test] + public void G100_Verify_NotFound() + { + using var test = ApiTester.Create(); + + test.Controller() + .Run(c => c.VerifyAsync(404.ToGuid())) + .AssertNotFound(); + } + + [Test] + public void G100_Verify_Publish() + { + using var test = ApiTester.Create(); + var imp = new InMemoryPublisher(test.Logger); + + test.ReplaceScoped(_ => imp) + .Controller() + .Run(c => c.VerifyAsync(1.ToGuid())) + .AssertAccepted(); + + Assert.AreEqual(1, imp.GetNames().Length); + var e = imp.GetEvents("pendingVerifications"); + Assert.AreEqual(1, e.Length); + ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" }, e[0].Value); + } + + [Test] + public void G100_Verify_Publish_WithExpectations() + { + using var test = ApiTester.Create(); + test.UseExpectedEvents() + .Controller() + .ExpectDestinationEvent("pendingVerifications", new EventData { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" } }) + .ExpectStatusCode(System.Net.HttpStatusCode.Accepted) + .Run(c => c.VerifyAsync(1.ToGuid())); + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs b/src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs new file mode 100644 index 00000000..7627eff2 --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs @@ -0,0 +1,390 @@ +using CoreEx.Entities; +using CoreEx.Events; +using CoreEx.Http; +using Company.AppName.Api; +using Company.AppName.Api.Controllers; +using Company.AppName.Business.Models; +using Company.AppName.Business.External.Contracts; +using NUnit.Framework; +using System; +using System.Linq; +using System.Threading.Tasks; +using UnitTestEx; +using UnitTestEx.Expectations; +using UnitTestEx.NUnit; +using Company.AppName.Business.Services; + +namespace Company.AppName.UnitTest +{ + [TestFixture] + [Category("WithDB")] + public class EmployeeControllerTest2 + { + [OneTimeSetUp] + public static Task Init() => EmployeeControllerTest.Init(); + + [Test] + public void A100_Get_NotFound() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + test.Controller() + .Run(c => c.GetAsync(404.ToGuid())) + .AssertNotFound(); + } + + [Test] + public void A110_Get_Found() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + test.Controller() + .Run(c => c.GetAsync(1.ToGuid())) + .AssertOK() + .Assert(new Employee + { + Id = 1.ToGuid(), + Email = "w.jones@org.com", + FirstName = "Wendy", + LastName = "Jones", + Gender = "F", + Birthday = new DateTime(1985, 03, 18, 0, 0, 0, DateTimeKind.Unspecified), + StartDate = new DateTime(2000, 12, 11, 0, 0, 0, DateTimeKind.Unspecified), + PhoneNo = "(425) 612 8113" + }, nameof(Employee.ETag)); + } + + [Test] + public void A120_Get_NotModifed() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + var e = test.Controller() + .Run(c => c.GetAsync(1.ToGuid())) + .AssertOK() + .GetValue()!; + + test.Controller() + .Run(c => c.GetAsync(e.Id), requestOptions: new HttpRequestOptions { ETag = e.ETag }) + .AssertNotModified(); + } + + [Test] + public void A130_Get_IncludeFields() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + test.Controller() + .Run(c => c.GetAsync(1.ToGuid()), requestOptions: new HttpRequestOptions().Include("FirstName", "LastName")) + .AssertOK() + .AssertJson("{\"firstName\":\"Wendy\",\"lastName\":\"Jones\"}"); + } + + [Test] + public void B100_GetAll_All() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + var v = test.Controller() + .Run(c => c.GetAllAsync()) + .AssertOK() + .GetValue(); + + Assert.IsNotNull(v?.Collection); + Assert.AreEqual(4, v!.Collection.Count); + Assert.AreEqual(new string[] { "Browne", "Jones", "Smith", "Smithers" }, v.Collection.Select(x => x.LastName).ToArray()); + } + + [Test] + public void B110_GetAll_Paging() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + var v = test.Controller() + .Run(c => c.GetAllAsync(), requestOptions: new HttpRequestOptions { Paging = PagingArgs.CreateSkipAndTake(1, 2, true) }) + .AssertOK() + .GetValue(); + + Assert.IsNotNull(v?.Collection); + Assert.AreEqual(2, v!.Collection.Count); + Assert.AreEqual(new string[] { "Jones", "Smith" }, v.Collection.Select(x => x.LastName).ToArray()); + Assert.IsNotNull(v.Paging); + Assert.AreEqual(4, v.Paging!.TotalCount); + } + + [Test] + public void B120_GetAll_PagingAndIncludeFields() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + var v = test.Controller() + .Run(c => c.GetAllAsync(), requestOptions: new HttpRequestOptions { Paging = PagingArgs.CreateSkipAndTake(1, 2) }.Include("lastname")) + .AssertOK() + .AssertJson("[ { \"lastName\": \"Jones\" }, { \"lastName\": \"Smith\" } ]") + .GetValue(); + + Assert.IsNull(v!.Paging!.TotalCount); // No count requested. + } + + [Test] + public void C100_Create_Error() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "Z", + PhoneNo = "555 123 4567", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.Controller() + .Run(c => c.CreateAsync(null!), e) + .AssertErrors( + new ApiError("Email", "'Email' must not be empty."), + new ApiError("Gender", "'Gender' is invalid.")); + } + + [Test] + public void C110_Create_Success() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + Email = "rs@email.com", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + var v = test.Controller() + .Run(c => c.CreateAsync(null!), e) + .AssertCreated() + .Assert(e, "Id", "ETag") + .AssertLocationHeader(v => new Uri($"api/employees/{v!.Id}", UriKind.Relative)) + .GetValue(); + + // Do a GET to make sure it is in the database and all fields equal. + test.Controller() + .Run(c => c.GetAsync(v!.Id)) + .AssertOK() + .Assert(v); + } + + [Test] + public void D100_Update_Error() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.Controller() + .Run(c => c.UpdateAsync(404.ToGuid(), null!), e) + .AssertErrors( + new ApiError("Email", "'Email' must not be empty.")); + } + + [Test] + public void D110_Update_NotFound() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + Email = "rs@email.com", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.Controller() + .Run(c => c.UpdateAsync(404.ToGuid(), null!), e) + .AssertNotFound(); + } + + [Test] + public void D120_Update_Success() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + // Get current. + var v = test.Controller() + .Run(c => c.GetAsync(2.ToGuid())) + .AssertOK() + .GetValue()!; + + // Update it. + v.FirstName += "X"; + + v = test.Controller() + .Run(c => c.UpdateAsync(v.Id, null!), v) + .AssertOK() + .Assert(v, "ETag") + .GetValue()!; + + // Get again and check all. + test.Controller() + .Run(c => c.GetAsync(v.Id)) + .AssertOK() + .Assert(v); + } + + [Test] + public void D130_Update_ConcurrencyError() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + // Get current. + var v = test.Controller() + .Run(c => c.GetAsync(2.ToGuid())) + .AssertOK() + .GetValue()!; + + // Update it with errant etag. + v.FirstName += "X"; + v.ETag = "ZZZZZZZZZZZZ"; + + test.Controller() + .Run(c => c.UpdateAsync(v.Id, null!), v) + .AssertPreconditionFailed(); + } + + [Test] + public void E100_Delete() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + // Get current. + test.Controller() + .Run(c => c.GetAsync(2.ToGuid())) + .AssertOK(); + + // Delete it. + test.Controller() + .Run(c => c.DeleteAsync(2.ToGuid())) + .AssertNoContent(); + + // Must not exist. + test.Controller() + .Run(c => c.GetAsync(2.ToGuid())) + .AssertNotFound(); + + // Delete it again; should appear as if deleted as operation is considered idempotent. + test.Controller() + .Run(c => c.DeleteAsync(2.ToGuid())) + .AssertNoContent(); + } + + [Test] + public void F100_Patch_NotFound() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + test.Controller() + .RunContent(c => c.PatchAsync(404.ToGuid(), null!), "{}", HttpConsts.MergePatchMediaTypeName) + .AssertNotFound(); + } + + [Test] + public void F110_Patch_Concurrency() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + // Get current. + var v = test.Controller() + .Run(c => c.GetAsync(4.ToGuid())) + .AssertOK() + .GetValue()!; + + // Patch it with errant etag. + v.FirstName += "X"; + + test.Controller() + .RunContent(c => c.PatchAsync(v.Id, null!), $"{{ \"firstName\": \"{v.FirstName}\" }}", new HttpRequestOptions { ETag = "ZZZZZZZZZZZZ" }, HttpConsts.MergePatchMediaTypeName) + .AssertPreconditionFailed(); + } + + [Test] + public void F120_Patch() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + // Get current. + var v = test.Controller() + .Run(c => c.GetAsync(4.ToGuid())) + .AssertOK() + .GetValue()!; + + // Patch it with errant etag. + v.FirstName += "X"; + + v = test.Controller() + .RunContent(c => c.PatchAsync(v.Id, null!), $"{{ \"firstName\": \"{v.FirstName}\" }}", new HttpRequestOptions { ETag = v.ETag }, HttpConsts.MergePatchMediaTypeName) + .AssertOK() + .Assert(v, "ETag") + .GetValue()!; + + // Get again and check all. + test.Controller() + .Run(c => c.GetAsync(v.Id)) + .AssertOK() + .Assert(v); + } + + [Test] + public void G100_Verify_NotFound() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + + test.Controller() + .Run(c => c.VerifyAsync(404.ToGuid())) + .AssertNotFound(); + } + + [Test] + public void G100_Verify_Publish() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + var imp = new InMemoryPublisher(test.Logger); + + test.ReplaceScoped(_ => imp) + .Controller() + .Run(c => c.VerifyAsync(1.ToGuid())) + .AssertAccepted(); + + Assert.AreEqual(1, imp.GetNames().Length); + var e = imp.GetEvents("pendingVerifications"); + Assert.AreEqual(1, e.Length); + ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" }, e[0].Value); + } + + [Test] + public void G100_Verify_Publish_WithExpectations() + { + using var test = ApiTester.Create().ConfigureServices(sc => sc.ReplaceScoped()); + test.UseExpectedEvents() + .Controller() + .ExpectDestinationEvent("pendingVerifications", new EventData { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" } }) + .ExpectStatusCode(System.Net.HttpStatusCode.Accepted) + .Run(c => c.VerifyAsync(1.ToGuid())); + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs b/src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs new file mode 100644 index 00000000..bf424c3c --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs @@ -0,0 +1,361 @@ +using CoreEx.Entities; +using CoreEx.Http; +using CoreEx.WebApis; +using DbEx.Migration; +using DbEx.Migration.Data; +using Microsoft.Extensions.Configuration; +using Company.AppName.Business.Models; +using Company.AppName.Functions; +using Company.AppName.Functions.Functions; +using NUnit.Framework; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using UnitTestEx; +using UnitTestEx.NUnit; + +namespace Company.AppName.UnitTest +{ + [TestFixture] + [Category("WithDB")] + public class EmployeeFunctionTest + { + [OneTimeSetUp] + public async Task Init() + { + HttpConsts.IncludeFieldsQueryStringName = "include-fields"; + + using var test = FunctionTester.Create(); + var cs = test.Configuration.GetConnectionString("Database"); + if (await Database.Program.RunMigrator(cs, typeof(EmployeeControllerTest).Assembly, MigrationCommand.ResetAndAll.ToString()).ConfigureAwait(false) != 0) + Assert.Fail("Database migration failed."); + } + + [Test] + public void A100_Get_NotFound() + { + using var test = FunctionTester.Create(); + + test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{404.ToGuid()}"), 404.ToGuid())) + .AssertNotFound(); + } + + [Test] + public void A110_Get_Found() + { + using var test = FunctionTester.Create(); + + test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{1.ToGuid()}"), 1.ToGuid())) + .AssertOK() + .Assert(new Employee + { + Id = 1.ToGuid(), + Email = "w.jones@org.com", + FirstName = "Wendy", + LastName = "Jones", + Gender = "F", + Birthday = new DateTime(1985, 03, 18, 0, 0, 0, DateTimeKind.Unspecified), + StartDate = new DateTime(2000, 12, 11, 0, 0, 0, DateTimeKind.Unspecified), + PhoneNo = "(425) 612 8113" + }, nameof(Employee.ETag)); + } + + [Test] + public void A120_Get_NotModified() + { + using var test = FunctionTester.Create(); + + var e = test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{1.ToGuid()}"), 1.ToGuid())) + .AssertOK() + .GetValue()!; + + test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{1.ToGuid()}", new CoreEx.Http.HttpRequestOptions { ETag = e.ETag }), 1.ToGuid())) + .AssertNotModified(); + } + + [Test] + public void A130_Get_IncludeFields() + { + using var test = FunctionTester.Create(); + + var e = test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{1.ToGuid()}", new CoreEx.Http.HttpRequestOptions().Include("FirstName", "LastName")), 1.ToGuid())) + .AssertOK() + .AssertJson("{\"firstName\":\"Wendy\",\"lastName\":\"Jones\"}"); + } + + [Test] + public void B100_GetAll_All() + { + using var test = FunctionTester.Create(); + + var v = test.HttpTrigger() + .Run(f => f.GetAllAsync(test.CreateHttpRequest(HttpMethod.Get, "api/employees"))) + .GetValue(); + + Assert.IsNotNull(v?.Collection); + Assert.AreEqual(4, v!.Collection.Count); + Assert.AreEqual(new string[] { "Browne", "Jones", "Smith", "Smithers" }, v.Collection.Select(x => x.LastName).ToArray()); + } + + [Test] + public void B110_GetAll_Paging() + { + using var test = FunctionTester.Create(); + + var v = test.HttpTrigger() + .Run(f => f.GetAllAsync(test.CreateHttpRequest(HttpMethod.Get, "api/employees", new CoreEx.Http.HttpRequestOptions { Paging = PagingArgs.CreateSkipAndTake(1, 2, true) }))) + .AssertOK() + .GetValue(); + + Assert.IsNotNull(v?.Collection); + Assert.AreEqual(2, v!.Collection.Count); + Assert.AreEqual(new string[] { "Jones", "Smith" }, v.Collection.Select(x => x.LastName).ToArray()); + Assert.IsNotNull(v.Paging); + Assert.AreEqual(4, v.Paging!.TotalCount); + } + + [Test] + public void B120_GetAll_PagingAndIncludeFields() + { + using var test = FunctionTester.Create(); + + var v = test.HttpTrigger() + .Run(f => f.GetAllAsync(test.CreateHttpRequest(HttpMethod.Get, "api/employees", new CoreEx.Http.HttpRequestOptions { Paging = PagingArgs.CreateSkipAndTake(1, 2, false) }.Include("lastname")))) + .AssertOK() + .AssertJson("[ { \"lastName\": \"Jones\" }, { \"lastName\": \"Smith\" } ]") + .GetValue(); + + Assert.IsNull(v!.Paging!.TotalCount); // No count requested. + } + + [Test] + public void C100_Create_Error() + { + using var test = FunctionTester.Create(); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.HttpTrigger() + .Run(c => c.CreateAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "api/employees", e))) + .AssertErrors( + new ApiError("Email", "'Email' must not be empty.")); + } + + [Test] + public void C110_Create_Success() + { + using var test = FunctionTester.Create(); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + Email = "rs@email.com", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + var v = test.HttpTrigger() + .Run(c => c.CreateAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "api/employees", e))) + .AssertCreated() + .Assert(e, "Id", "ETag") + .AssertLocationHeader(v => new Uri($"api/employees/{v!.Id}", UriKind.Relative)) + .GetValue(); + + // Do a GET to make sure it is in the database and all fields equal. + test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{v!.Id}"), v.Id)) + .AssertOK() + .Assert(v); + } + + [Test] + public void D100_Update_Error() + { + using var test = FunctionTester.Create(); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.HttpTrigger() + .Run(f => f.UpdateAsync(test.CreateJsonHttpRequest(HttpMethod.Put, $"api/employees/{404.ToGuid()}", e), 404.ToGuid())) + .AssertErrors( + new ApiError("Email", "'Email' must not be empty.")); + } + + [Test] + public void D110_Update_NotFound() + { + using var test = FunctionTester.Create(); + + var e = new Employee + { + FirstName = "Rebecca", + LastName = "Smythe", + Birthday = new DateTime(2000, 01, 01, 0, 0, 0, DateTimeKind.Unspecified), + Gender = "M", + PhoneNo = "555 123 4567", + Email = "rs@email.com", + StartDate = new DateTime(2020, 01, 08, 0, 0, 0, DateTimeKind.Unspecified) + }; + + test.HttpTrigger() + .Run(f => f.UpdateAsync(test.CreateJsonHttpRequest(HttpMethod.Put, $"api/employees/{404.ToGuid()}", e), 404.ToGuid())) + .AssertNotFound(); + } + + [Test] + public void D120_Update_Success() + { + using var test = FunctionTester.Create(); + + // Get current. + var v = test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{2.ToGuid()}"), 2.ToGuid())) + .AssertOK() + .GetValue()!; + + // Update it. + v.FirstName += "X"; + + v = test.HttpTrigger() + .Run(f => f.UpdateAsync(test.CreateJsonHttpRequest(HttpMethod.Put, $"api/employees/{v.Id}", v), v.Id)) + .AssertOK() + .Assert(v, "ETag") + .GetValue()!; + + // Get again and check all. + test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{v.Id}"), v.Id)) + .AssertOK() + .Assert(v); + } + + [Test] + public void D130_Update_ConcurrencyError() + { + using var test = FunctionTester.Create(); + + // Get current. + var v = test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{2.ToGuid()}"), 2.ToGuid())) + .AssertOK() + .GetValue()!; + + // Update it with errant etag. + v.FirstName += "X"; + v.ETag = "ZZZZZZZZZZZZ"; + + test.HttpTrigger() + .Run(f => f.UpdateAsync(test.CreateJsonHttpRequest(HttpMethod.Put, $"api/employees/{v.Id}", v), v.Id)) + .AssertPreconditionFailed(); + } + + [Test] + public void E100_Delete() + { + using var test = FunctionTester.Create(); + + // Get current. + test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{2.ToGuid()}"), 2.ToGuid())) + .AssertOK(); + + // Delete it. + test.HttpTrigger() + .Run(f => f.DeleteAsync(test.CreateHttpRequest(HttpMethod.Delete, $"api/employees/{2.ToGuid()}"), 2.ToGuid())) + .AssertNoContent(); + + // Must not exist. + test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{2.ToGuid()}"), 2.ToGuid())) + .AssertNotFound(); + + // Delete it again; should appear as if deleted as operation is considered idempotent. + test.HttpTrigger() + .Run(f => f.DeleteAsync(test.CreateHttpRequest(HttpMethod.Delete, $"api/employees/{2.ToGuid()}"), 2.ToGuid())) + .AssertNoContent(); + } + + [Test] + public void F100_Patch_NotFound() + { + using var test = FunctionTester.Create(); + + test.HttpTrigger() + .Run(f => f.PatchAsync(test.CreateHttpRequest(HttpMethod.Patch, $"api/employees/{404.ToGuid()}", "{}", HttpConsts.MergePatchMediaTypeName), 404.ToGuid())) + .AssertNotFound(); + } + + [Test] + public void F110_Patch_Concurrency() + { + using var test = FunctionTester.Create(); + + // Get current. + var v = test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{4.ToGuid()}"), 4.ToGuid())) + .AssertOK() + .GetValue()!; + + // Patch it with errant etag. + v.FirstName += "X"; + + var req = test.CreateHttpRequest(HttpMethod.Patch, $"api/employees/{v.Id}", $"{{ \"firstName\": \"{v.FirstName}\" }}", new CoreEx.Http.HttpRequestOptions { ETag = "ZZZZZZZZZZZZ" }, HttpConsts.MergePatchMediaTypeName); + test.HttpTrigger() + .Run(f => f.PatchAsync(req, v.Id)) + .AssertPreconditionFailed(); + } + + [Test] + public void F120_Patch() + { + using var test = FunctionTester.Create(); + + // Get current. + var v = test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{4.ToGuid()}"), 4.ToGuid())) + .AssertOK() + .GetValue()!; + + // Patch it with errant etag. + v.FirstName += "X"; + + var req = test.CreateHttpRequest(HttpMethod.Patch, $"api/employees/{v.Id}", $"{{ \"firstName\": \"{v.FirstName}\" }}", new CoreEx.Http.HttpRequestOptions { ETag = v.ETag }, HttpConsts.MergePatchMediaTypeName); + v = test.HttpTrigger() + .Run(f => f.PatchAsync(req, v.Id)) + .AssertOK() + .Assert(v, "ETag") + .GetValue()!; + + // Get again and check all. + test.HttpTrigger() + .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{v.Id}"), v.Id)) + .AssertOK() + .Assert(v); + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs b/src/Templates/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs new file mode 100644 index 00000000..02b9d9e4 --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs @@ -0,0 +1,30 @@ +using CoreEx.Events; +using Company.AppName.Business.External.Contracts; +using Company.AppName.Functions; +using NUnit.Framework; +using System.Net.Http; +using UnitTestEx.NUnit; + +namespace Company.AppName.UnitTest +{ + [TestFixture] + public class HttpTriggerQueueVerificationFunctionTest + { + [Test] + public void A110_Verify_Success() + { + var test = FunctionTester.Create(); + var imp = new InMemoryPublisher(test.Logger); + + test.ReplaceScoped(_ => imp) + .HttpTrigger() + .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "employee/verify", new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" }))) + .AssertAccepted(); + + Assert.AreEqual(1, imp.GetNames().Length); + var e = imp.GetEvents("pendingVerifications"); + Assert.AreEqual(1, e.Length); + ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" }, e[0].Value); + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs b/src/Templates/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs new file mode 100644 index 00000000..473597e8 --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs @@ -0,0 +1,99 @@ +using CoreEx.Http; +using Company.AppName.Api; +using Company.AppName.Api.Controllers; +using Company.AppName.Business.Models; +using NUnit.Framework; +using System.Linq; +using System.Threading.Tasks; +using UnitTestEx.NUnit; + +namespace Company.AppName.UnitTest +{ + [TestFixture] + [Category("WithDB")] + public class ReferenceDataControllerTest + { + [OneTimeSetUp] + public Task Init() => EmployeeControllerTest.Init(); + + [Test] + public void A100_USState_All() + { + using var test = ApiTester.Create().UseJsonSerializer(new CoreEx.Text.Json.ReferenceDataContentJsonSerializer()); + + var v = test.Controller() + .Run(c => c.USStateGetAll(null, null)) + .AssertOK() + .GetValue()!; + + Assert.AreEqual(50, v.Length); + } + + [Test] + public void A110_USState_Codes() + { + using var test = ApiTester.Create().UseJsonSerializer(new CoreEx.Text.Json.ReferenceDataContentJsonSerializer()); + + var v = test.Controller() + .Run(c => c.USStateGetAll(new string[] { "WA", "CO" }, null)) + .AssertOK() + .GetValue()!; + + Assert.AreEqual(2, v.Length); + Assert.AreEqual(new string[] { "CO", "WA" }, v.Select(x => x.Code)); + } + + [Test] + public void A120_USState_Text() + { + using var test = ApiTester.Create().UseJsonSerializer(new CoreEx.Text.Json.ReferenceDataContentJsonSerializer()); + + var v = test.Controller() + .Run(c => c.USStateGetAll(null, "*or*")) + .AssertOK() + .GetValue()!; + + Assert.AreEqual(8, v.Length); + var x = v.Select(x => x.Code); + Assert.AreEqual(new string[] { "CA", "CO", "FL", "GA", "NY", "NC", "ND", "OR" }, v.Select(x => x.Code)); + } + + [Test] + public void A130_USState_FieldsAndNotModified() + { + using var test = ApiTester.Create().UseJsonSerializer(new CoreEx.Text.Json.ReferenceDataContentJsonSerializer()); + + var r = test.Controller() + .Run(c => c.USStateGetAll(new string[] { "WA", "CO" }, null), new HttpRequestOptions().Include("code", "text")) + .AssertOK() + .AssertJson("[{\"code\":\"CO\",\"text\":\"Colorado\"},{\"code\":\"WA\",\"text\":\"Washington\"}]"); + + test.Controller() + .Run(c => c.USStateGetAll(new string[] { "WA", "CO" }, null), new HttpRequestOptions { ETag = r.Response?.Headers?.ETag?.Tag }.Include("code", "text")) + .AssertNotModified(); + } + + [Test] + public void B100_Gender_All() + { + using var test = ApiTester.Create().UseJsonSerializer(new CoreEx.Text.Json.ReferenceDataContentJsonSerializer()); + + var v = test.Controller() + .Run(c => c.GenderGetAll(null, null)) + .AssertOK() + .GetValue()!; + + Assert.AreEqual(3, v.Length); + } + + [Test] + public void C100_Named() + { + using var test = ApiTester.Create().UseJsonSerializer(new CoreEx.Text.Json.ReferenceDataContentJsonSerializer()); + + var r = test.Controller() + .Run(c => c.GetNamed(), new HttpRequestOptions { UrlQueryString = "gender&usstate" }) + .AssertOK(); + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json b/src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json new file mode 100644 index 00000000..1aab9ecb --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json @@ -0,0 +1,29 @@ +{ + "age": 64, + "gender": "female", + "genderProbability": 0.97, + "country": [ + { + "country_Id": "SV", + "probability": 0.07477553 + }, + { + "country_Id": "GT", + "probability": 0.07223318 + }, + { + "country_Id": "NL", + "probability": 0.067494206 + } + ], + "verificationMessages": [ + "Performed verification for Wendy, F age 37. \n Engine predicted age was 64. \n Engine predicted gender was female with 97% probability.\n Most likely nationality of Wendy is SV with 7% probability", + "Employee age (37) is not within range of 10 years of predicted age: 64", + "Employee gender (F) doesn\u0027t match predicted gender: female" + ], + "request": { + "name": "Wendy", + "age": 37, + "gender": "F" + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json b/src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json new file mode 100644 index 00000000..651f0321 --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json @@ -0,0 +1,29 @@ +{ + "age": 64, + "gender": "female", + "genderProbability": 0.97, + "country": [ + { + "country_Id": "SV", + "probability": 0.07477553 + }, + { + "country_Id": "GT", + "probability": 0.07223318 + }, + { + "country_Id": "NL", + "probability": 0.067494206 + } + ], + "verificationMessages": [ + "Performed verification for Wendy, F age 37. \r\n Engine predicted age was 64. \r\n Engine predicted gender was female with 97% probability.\r\n Most likely nationality of Wendy is SV with 7% probability", + "Employee age (37) is not within range of 10 years of predicted age: 64", + "Employee gender (F) doesn\u0027t match predicted gender: female" + ], + "request": { + "name": "Wendy", + "age": 37, + "gender": "F" + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs b/src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs new file mode 100644 index 00000000..7e4d7f9c --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs @@ -0,0 +1,37 @@ +using CoreEx.Events; +using Microsoft.Azure.WebJobs.ServiceBus; +using Moq; +using Company.AppName.Business.External.Contracts; +using Company.AppName.Functions; +using NUnit.Framework; +using System; +using UnitTestEx.NUnit; + +namespace Company.AppName.UnitTest +{ + [TestFixture] + public class ServiceBusExecuteVerificationFunctionTest + { + [Test] + public void A110_Verify_Success() + { + var test = FunctionTester.Create(); + var imp = new InMemoryPublisher(test.Logger); + var sbm = test.CreateServiceBusMessage(new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" }); + var sba = new Mock(); + + test.ReplaceScoped(_ => imp) + .ServiceBusTrigger() + .Run(f => f.RunAsync(sbm, sba.Object)) + .AssertSuccess(); + + Assert.AreEqual(1, imp.GetNames().Length); + var e = imp.GetEvents("verificationResults"); + Assert.AreEqual(1, e.Length); + if (Environment.OSVersion.Platform == PlatformID.Unix) + ObjectComparer.Assert(UnitTestEx.Resource.GetJsonValue("VerificationResult.Unix.json"), e[0].Value); + else + ObjectComparer.Assert(UnitTestEx.Resource.GetJsonValue("VerificationResult.Win32.json"), e[0].Value); + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json b/src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json new file mode 100644 index 00000000..c7e9a5c1 --- /dev/null +++ b/src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json @@ -0,0 +1,14 @@ +{ + "DOTNET_ENVIRONMENT": "Development", + "VerificationResultsQueueName": "verificationResults", + "VerificationQueueName": "pendingVerifications", + "ServiceBusConnection__fullyQualifiedNamespace": "topsecret", + "AgifyApiEndpointUri": "https://api.agify.io", + "NationalizeApiClientApiEndpointUri": "https://api.nationalize.io", + "GenderizeApiClientApiEndpointUri": "https://api.genderize.io", + "logging": { + "logLevel": { + "default": "debug" + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.sln b/src/Templates/content/Company.AppName.sln new file mode 100644 index 00000000..d6654aba --- /dev/null +++ b/src/Templates/content/Company.AppName.sln @@ -0,0 +1,52 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.Api", "Company.AppName.Api\Company.AppName.Api.csproj", "{F69909C8-9E50-4A26-8609-5B25D4F8C315}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.Database", "Company.AppName.Database\Company.AppName.Database.csproj", "{092B22E9-F40D-4155-B174-EED58F0F3207}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.Business", "Company.AppName.Business\Company.AppName.Business.csproj", "{BA634B6B-E0CF-4066-A225-E6D9C43D7F0B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.Functions", "Company.AppName.Functions\Company.AppName.Functions.csproj", "{A62BAA55-0737-4671-BF31-89D4BE7C4097}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.Infra", "Company.AppName.Infra\Company.AppName.Infra.csproj", "{E448EFD6-5CA6-4C71-B575-1149DD7181C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.Infra.Tests", "Company.AppName.Infra.Tests\Company.AppName.Infra.Tests.csproj", "{01B0FC8E-738D-47BB-AA57-F880532A501D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F69909C8-9E50-4A26-8609-5B25D4F8C315}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F69909C8-9E50-4A26-8609-5B25D4F8C315}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F69909C8-9E50-4A26-8609-5B25D4F8C315}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F69909C8-9E50-4A26-8609-5B25D4F8C315}.Release|Any CPU.Build.0 = Release|Any CPU + {092B22E9-F40D-4155-B174-EED58F0F3207}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {092B22E9-F40D-4155-B174-EED58F0F3207}.Debug|Any CPU.Build.0 = Debug|Any CPU + {092B22E9-F40D-4155-B174-EED58F0F3207}.Release|Any CPU.ActiveCfg = Release|Any CPU + {092B22E9-F40D-4155-B174-EED58F0F3207}.Release|Any CPU.Build.0 = Release|Any CPU + {BA634B6B-E0CF-4066-A225-E6D9C43D7F0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA634B6B-E0CF-4066-A225-E6D9C43D7F0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA634B6B-E0CF-4066-A225-E6D9C43D7F0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA634B6B-E0CF-4066-A225-E6D9C43D7F0B}.Release|Any CPU.Build.0 = Release|Any CPU + {A62BAA55-0737-4671-BF31-89D4BE7C4097}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A62BAA55-0737-4671-BF31-89D4BE7C4097}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A62BAA55-0737-4671-BF31-89D4BE7C4097}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A62BAA55-0737-4671-BF31-89D4BE7C4097}.Release|Any CPU.Build.0 = Release|Any CPU + {E448EFD6-5CA6-4C71-B575-1149DD7181C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E448EFD6-5CA6-4C71-B575-1149DD7181C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E448EFD6-5CA6-4C71-B575-1149DD7181C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E448EFD6-5CA6-4C71-B575-1149DD7181C6}.Release|Any CPU.Build.0 = Release|Any CPU + {01B0FC8E-738D-47BB-AA57-F880532A501D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01B0FC8E-738D-47BB-AA57-F880532A501D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01B0FC8E-738D-47BB-AA57-F880532A501D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01B0FC8E-738D-47BB-AA57-F880532A501D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Templates/readme.md b/src/Templates/readme.md index 004623be..a7d51169 100644 --- a/src/Templates/readme.md +++ b/src/Templates/readme.md @@ -7,6 +7,7 @@ * [Docs](https://learn.microsoft.com/en-us/dotnet/core/tools/custom-templates) * [Tutorial](https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-item-template) * [NTangle template](https://github.com/Avanade/NTangle/tree/main/tools/NTangle.Template) +* [Template Analyzer](https://github.com/sayedihashimi/template-sample#template-analyzer) ## Dev container From 0894f7635e764a24c4b7dededb026f26442c13de Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 23 Sep 2022 16:26:14 -0400 Subject: [PATCH 03/39] docker for template Signed-off-by: Piotr --- src/Templates/content/.dockerignore | 37 + src/Templates/content/.gitignore | 757 ++++++++++++++++++ .../content/.template.config/template.json | 11 +- .../content/Company.AppName.Api/Dockerfile | 34 +- .../Company.AppName.Database/Dockerfile | 31 +- .../Company.AppName.Functions/Dockerfile | 32 +- src/Templates/content/Docker.md | 58 ++ .../content/docker-compose.override.yml | 50 ++ src/Templates/content/docker-compose.yml | 25 + 9 files changed, 970 insertions(+), 65 deletions(-) create mode 100644 src/Templates/content/.dockerignore create mode 100644 src/Templates/content/.gitignore create mode 100644 src/Templates/content/Docker.md create mode 100644 src/Templates/content/docker-compose.override.yml create mode 100644 src/Templates/content/docker-compose.yml diff --git a/src/Templates/content/.dockerignore b/src/Templates/content/.dockerignore new file mode 100644 index 00000000..f2dfd3bc --- /dev/null +++ b/src/Templates/content/.dockerignore @@ -0,0 +1,37 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ diff --git a/src/Templates/content/.gitignore b/src/Templates/content/.gitignore new file mode 100644 index 00000000..054745e3 --- /dev/null +++ b/src/Templates/content/.gitignore @@ -0,0 +1,757 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig + +# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,macos,csharp,dotnetcore,jupyternotebooks,linux,node,python +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,macos,csharp,dotnetcore,jupyternotebooks,linux,node,python + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Nuget personal access tokens and Credentials +nuget.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml + +### DotnetCore ### +# .NET Core build folders +bin/ +obj/ + +# Common node modules locations +/node_modules +/wwwroot/node_modules + +### JupyterNotebooks ### +# gitignore template for Jupyter Notebooks +# website: http://jupyter.org/ + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Remove previous ipynb_checkpoints +# git rm -r .ipynb_checkpoints/ + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +### Python ### +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook + +# IPython + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +### VisualStudioCode ### + +# Local History for Visual Studio Code + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,macos,csharp,dotnetcore,jupyternotebooks,linux,node,python + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) +__azurite_db_blob__.json +__azurite_db_blob_extent__.json +__blobstorage__/ +docker-compose.local.override.yml +__azurite_db_* + +# Pulumi +Pulumi.*.yaml \ No newline at end of file diff --git a/src/Templates/content/.template.config/template.json b/src/Templates/content/.template.config/template.json index 80eb8b3a..7481e6c1 100644 --- a/src/Templates/content/.template.config/template.json +++ b/src/Templates/content/.template.config/template.json @@ -121,5 +121,14 @@ } ] } - ] + ], + "postActions": [{ + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [{ + "text": "Run 'dotnet restore'" + }], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + }] } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/Dockerfile b/src/Templates/content/Company.AppName.Api/Dockerfile index 5eeb08e1..da1edfc7 100644 --- a/src/Templates/content/Company.AppName.Api/Dockerfile +++ b/src/Templates/content/Company.AppName.Api/Dockerfile @@ -7,33 +7,23 @@ WORKDIR /src # It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles # to take advantage of Docker's build cache, to speed up local container builds -COPY "samples/Company.AppName/Company.AppName.sln" "samples/Company.AppName/Company.AppName.sln" - -COPY "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" -COPY "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" -COPY "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" -COPY "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" -COPY "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" -COPY "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" - -COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" -COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" -COPY "src/CoreEx.Azure/CoreEx.Azure.csproj" "src/CoreEx.Azure/CoreEx.Azure.csproj" -COPY "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" -COPY "src/CoreEx.Database/CoreEx.Database.csproj" "src/CoreEx.Database/CoreEx.Database.csproj" -COPY "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" -COPY "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" -COPY "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" -COPY "src/CoreEx.Validation/CoreEx.Validation.csproj" "src/CoreEx.Validation/CoreEx.Validation.csproj" - -RUN dotnet restore "samples/Company.AppName/Company.AppName.sln" +COPY "Company.AppName.sln" "Company.AppName.sln" + +COPY "Company.AppName.Api/Company.AppName.Api.csproj" "Company.AppName.Api/Company.AppName.Api.csproj" +COPY "Company.AppName.Business/Company.AppName.Business.csproj" "Company.AppName.Business/Company.AppName.Business.csproj" +COPY "Company.AppName.Database/Company.AppName.Database.csproj" "Company.AppName.Database/Company.AppName.Database.csproj" +COPY "Company.AppName.Functions/Company.AppName.Functions.csproj" "Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "Company.AppName.Infra/Company.AppName.Infra.csproj" "Company.AppName.Infra/Company.AppName.Infra.csproj" +COPY "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" + +RUN dotnet restore "Company.AppName.sln" COPY . . -WORKDIR /src/samples/Company.AppName/Company.AppName.Api +WORKDIR /src/Company.AppName.Api RUN dotnet publish --no-restore -c Release -o /app FROM build as unittest -WORKDIR /src/samples/Company.AppName/Company.AppName.Test +WORKDIR /src/Company.AppName.Test # can run tests here on buils FROM build AS publish diff --git a/src/Templates/content/Company.AppName.Database/Dockerfile b/src/Templates/content/Company.AppName.Database/Dockerfile index 9675a310..2fc459f4 100644 --- a/src/Templates/content/Company.AppName.Database/Dockerfile +++ b/src/Templates/content/Company.AppName.Database/Dockerfile @@ -11,30 +11,19 @@ WORKDIR /src # It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles # to take advantage of Docker's build cache, to speed up local container builds -COPY "samples/Company.AppName/Company.AppName.sln" "samples/Company.AppName/Company.AppName.sln" +COPY "Company.AppName.sln" "Company.AppName.sln" -COPY "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" -COPY "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" -COPY "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" -COPY "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" -COPY "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" -COPY "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" +COPY "Company.AppName.Api/Company.AppName.Api.csproj" "Company.AppName.Api/Company.AppName.Api.csproj" +COPY "Company.AppName.Business/Company.AppName.Business.csproj" "Company.AppName.Business/Company.AppName.Business.csproj" +COPY "Company.AppName.Database/Company.AppName.Database.csproj" "Company.AppName.Database/Company.AppName.Database.csproj" +COPY "Company.AppName.Functions/Company.AppName.Functions.csproj" "Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "Company.AppName.Infra/Company.AppName.Infra.csproj" "Company.AppName.Infra/Company.AppName.Infra.csproj" +COPY "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" -COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" -COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" -COPY "src/CoreEx.Azure/CoreEx.Azure.csproj" "src/CoreEx.Azure/CoreEx.Azure.csproj" -COPY "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" -COPY "src/CoreEx.Database/CoreEx.Database.csproj" "src/CoreEx.Database/CoreEx.Database.csproj" -COPY "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" -COPY "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" -COPY "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" -COPY "src/CoreEx.Validation/CoreEx.Validation.csproj" "src/CoreEx.Validation/CoreEx.Validation.csproj" - - -RUN dotnet restore "samples/Company.AppName/Company.AppName.sln" +RUN dotnet restore "Company.AppName.sln" COPY . . -WORKDIR /src/samples/Company.AppName/Company.AppName.Database +WORKDIR /src/Company.AppName.Database RUN dotnet build -c Release -o /dbex/build FROM base as final @@ -50,7 +39,7 @@ ENV ConnectionStrings__sqlserver:MyHr Data Source=localhost, $MSSQL_TCP_PORT;Ini # Copy setup scripts WORKDIR /usr/local/ COPY --from=build /dbex/build /dbex -COPY samples/Company.AppName/Company.AppName.Database/wait-for-it.sh samples/Company.AppName/Company.AppName.Database/entrypoint.sh ./ +COPY Company.AppName.Database/wait-for-it.sh Company.AppName.Database/entrypoint.sh ./ RUN chmod +x ./*.sh diff --git a/src/Templates/content/Company.AppName.Functions/Dockerfile b/src/Templates/content/Company.AppName.Functions/Dockerfile index b08dc2bf..a725bc1c 100644 --- a/src/Templates/content/Company.AppName.Functions/Dockerfile +++ b/src/Templates/content/Company.AppName.Functions/Dockerfile @@ -8,30 +8,20 @@ COPY --from=mcr.microsoft.com/dotnet/core/sdk:3.1 /usr/share/dotnet /usr/share/d WORKDIR /src # It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles # to take advantage of Docker's build cache, to speed up local container builds -COPY "samples/Company.AppName/Company.AppName.sln" "samples/Company.AppName/Company.AppName.sln" - -COPY "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" "samples/Company.AppName/Company.AppName.Api/Company.AppName.Api.csproj" -COPY "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" "samples/Company.AppName/Company.AppName.Business/Company.AppName.Business.csproj" -COPY "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" "samples/Company.AppName/Company.AppName.Database/Company.AppName.Database.csproj" -COPY "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" "samples/Company.AppName/Company.AppName.Functions/Company.AppName.Functions.csproj" -COPY "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" "samples/Company.AppName/Company.AppName.Infra/Company.AppName.Infra.csproj" -COPY "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "samples/Company.AppName/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" - -COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" -COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" -COPY "src/CoreEx.Azure/CoreEx.Azure.csproj" "src/CoreEx.Azure/CoreEx.Azure.csproj" -COPY "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" "src/CoreEx.Cosmos/CoreEx.Cosmos.csproj" -COPY "src/CoreEx.Database/CoreEx.Database.csproj" "src/CoreEx.Database/CoreEx.Database.csproj" -COPY "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" "src/CoreEx.EntityFrameworkCore/CoreEx.EntityFrameworkCore.csproj" -COPY "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" "src/CoreEx.FluentValidation/CoreEx.FluentValidation.csproj" -COPY "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" "src/CoreEx.Newtonsoft/CoreEx.Newtonsoft.csproj" -COPY "src/CoreEx.Validation/CoreEx.Validation.csproj" "src/CoreEx.Validation/CoreEx.Validation.csproj" - -RUN dotnet restore "samples/Company.AppName/Company.AppName.sln" +COPY "Company.AppName.sln" "Company.AppName.sln" + +COPY "Company.AppName.Api/Company.AppName.Api.csproj" "Company.AppName.Api/Company.AppName.Api.csproj" +COPY "Company.AppName.Business/Company.AppName.Business.csproj" "Company.AppName.Business/Company.AppName.Business.csproj" +COPY "Company.AppName.Database/Company.AppName.Database.csproj" "Company.AppName.Database/Company.AppName.Database.csproj" +COPY "Company.AppName.Functions/Company.AppName.Functions.csproj" "Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "Company.AppName.Infra/Company.AppName.Infra.csproj" "Company.AppName.Infra/Company.AppName.Infra.csproj" +COPY "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" + +RUN dotnet restore "Company.AppName.sln" COPY . . -WORKDIR /src/samples/Company.AppName/Company.AppName.Functions +WORKDIR /src/Company.AppName.Functions RUN mkdir -p /home/site/wwwroot && \ dotnet publish *.csproj --no-restore -c Debug --output /home/site/wwwroot && \ diff --git a/src/Templates/content/Docker.md b/src/Templates/content/Docker.md new file mode 100644 index 00000000..c4b3b95f --- /dev/null +++ b/src/Templates/content/Docker.md @@ -0,0 +1,58 @@ +# About + +To run with docker-compose: + +```bash +docker-compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.local.override.yml up +``` + +where `docker-compose.local.override.yml` should include connection string to service bus: + +```yaml +version: '3.4' + +services: + app-functions: + environment: + - ServiceBusConnection=Endpoint=sb://Company.AppName.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=xxxxxx + + app-api: + environment: + - ServiceBusConnection=Endpoint=sb://Company.AppName.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=xxxxxx +``` + +Service Bus should have `pendingverifications` queue used by *Company.AppName* sample. + +## To build + +```bash +docker-compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.local.override.yml build --build-arg LOCAL=true +``` + +## Services + +Available services: + +* Database at port 5433 +* API at port 5103 +* Functions at 5104 + +Sample curl commands: + +### Function + +```bash +curl localhost:5104/api/health # to [get] to 'HealthInfo' +curl localhost:5104/api/employee/verify # to [post] to 'HttpTriggerQueueVerificationFunction' +curl localhost:5104/api/oauth2-redirect.html # to [GET] to 'OAuth2Redirect' +curl localhost:5104/api/openapi/{version}.{extension} # to [GET] to 'OpenApiDocument' +curl localhost:5104/api/swagger.{extension} # to [GET] to 'SwaggerDocument' +curl localhost:5104/api/swagger/ui # to [GET] to 'SwaggerUI' +``` + +### API + +```bash +curl localhost:5103/health # to [get] to 'HealthInfo' +curl localhost:5103/swagger/index.html # to [GET] to 'SwaggerUI' +``` diff --git a/src/Templates/content/docker-compose.override.yml b/src/Templates/content/docker-compose.override.yml new file mode 100644 index 00000000..6f79f8ae --- /dev/null +++ b/src/Templates/content/docker-compose.override.yml @@ -0,0 +1,50 @@ +version: '3.4' + +# The default docker-compose.override file can use the "localhost" as the external name for testing web apps within the same dev machine. +# The ESHOP_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: +# ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost +# but values present in the environment vars at runtime will always override those defined inside the .env file +# An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. + +services: + + sqldata: + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=sAPWD23.^0 + - MSSQL_TCP_PORT=1433 + - MSSQL_AGENT_ENABLED=true + - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__sqlserver:Company.AppName=Data Source=localhost,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true + ports: + - "5433:1433" + volumes: + - app-sqldata:/var/opt/mssql + + app-api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:80 + - ConnectionStrings__Database=Data Source=sqldata,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true + - PORT=80 + ports: + - "5103:80" + + app-functions: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - AZURE_FUNCTIONS_ENVIRONMENT=Development + - OpenApi__HideSwaggerUI=false + - AgifyApiEndpointUri=https://api.agify.io + - NationalizeApiClientApiEndpointUri=https://api.nationalize.io + - GenderizeApiClientApiEndpointUri=https://api.genderize.io + - VerificationQueueName=pendingVerifications + - VerificationResultsQueueName=verificationResults + - ConnectionStrings__Database=Data Source=sqldata,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true + - PORT=80 + ports: + - "5104:80" + +volumes: + app-sqldata: + external: false diff --git a/src/Templates/content/docker-compose.yml b/src/Templates/content/docker-compose.yml new file mode 100644 index 00000000..fc72c047 --- /dev/null +++ b/src/Templates/content/docker-compose.yml @@ -0,0 +1,25 @@ +# To run: docker-compose -f docker-compose.yml -f docker-compose.override.yml up +version: '3.4' + +services: + + sqldata: + build: + context: . + dockerfile: Company.AppName.Database/Dockerfile + + app-api: + build: + context: . + dockerfile: Company.AppName.Api/Dockerfile + depends_on: + - sqldata + + app-functions: + build: + context: . + args: + LOCAL: "true" + dockerfile: Company.AppName.Functions/Dockerfile + depends_on: + - sqldata \ No newline at end of file From 54fa7da9ebb7e28573319168a1654046e2d35627 Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 23 Sep 2022 18:18:50 -0400 Subject: [PATCH 04/39] settings and vs extensions Signed-off-by: Piotr --- .../content/.template.config/template.json | 16 ++++++++++++---- src/Templates/content/.vscode/extensions.json | 15 +++++++++++++++ .../content/Company.AppName.Api/Startup.cs | 10 +++++----- .../{HrSettings.cs => AppNameSettings.cs} | 6 +++--- .../External/AgifyServiceClient.cs | 2 +- .../External/GenderizeApiClient.cs | 2 +- .../External/NationalizeApiClient.cs | 2 +- .../Services/EmployeeService.cs | 4 ++-- .../Services/EmployeeService2.cs | 4 ++-- .../Services/VerificationService.cs | 4 ++-- .../HttpTriggerQueueVerificationFunction.cs | 4 ++-- .../ServiceBusExecuteVerificationFunction.cs | 2 +- .../content/Company.AppName.Functions/Startup.cs | 10 +++++----- src/Templates/content/Docker.md | 4 ++-- 14 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 src/Templates/content/.vscode/extensions.json rename src/Templates/content/Company.AppName.Business/{HrSettings.cs => AppNameSettings.cs} (90%) diff --git a/src/Templates/content/.template.config/template.json b/src/Templates/content/.template.config/template.json index 7481e6c1..38e1d107 100644 --- a/src/Templates/content/.template.config/template.json +++ b/src/Templates/content/.template.config/template.json @@ -12,14 +12,14 @@ }, "defaultName": "CoreEx", "description": "CoreEx ", - "sourceName": "Company.AppName", // Not acutally used; template uses the below parameters exclusively. + "sourceName": "XXCompany.AppNameXX", // Not acutally used; template uses the below parameters exclusively. "preferNameDirectory": true, "symbols": { "company": { "type": "parameter", "replaces": "Company", "fileRename": "Company", - "isRequired": false, + "isRequired": true, "datatype": "text", "description": "The company name 'Company' used to define the namespace etc; e.g. 'Company.AppName'." }, @@ -27,7 +27,7 @@ "type": "parameter", "replaces": "AppName", "fileRename": "AppName", - "isRequired": false, + "isRequired": true, "datatype": "text", "description": "The application (domain) name 'AppName' used to define the namespace etc; e.g. 'Company.AppName'." }, @@ -122,6 +122,11 @@ ] } ], + "primaryOutputs": [ + { + "path": "./Company.AppName.sln" + } + ], "postActions": [{ "condition": "(!skipRestore)", "description": "Restore NuGet packages required by this project.", @@ -129,6 +134,9 @@ "text": "Run 'dotnet restore'" }], "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", - "continueOnError": true + "continueOnError": true, + "args": { + "files": [ "Company.AppName.sln" ] + } }] } \ No newline at end of file diff --git a/src/Templates/content/.vscode/extensions.json b/src/Templates/content/.vscode/extensions.json new file mode 100644 index 00000000..ac9e785c --- /dev/null +++ b/src/Templates/content/.vscode/extensions.json @@ -0,0 +1,15 @@ +{ + "recommendations": [ + "ms-vscode.csharp", + "VisualStudioExptTeam.vscodeintellicode", + "ms-mssql.mssql", + "ms-vscode.azure-account", + "ms-azuretools.vscode-azureappservice", + "ms-azuretools.vscode-azurefunctions", + "ms-azuretools.vscode-docker", + "ms-dotnettools.vscode-dotnet-runtime", + "MS-vsliveshare.vsliveshare-pack", + "Azurite.azurite", + "humao.rest-client" + ] +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/Startup.cs b/src/Templates/content/Company.AppName.Api/Startup.cs index fc2451d2..8d2b6ab2 100644 --- a/src/Templates/content/Company.AppName.Api/Startup.cs +++ b/src/Templates/content/Company.AppName.Api/Startup.cs @@ -18,7 +18,7 @@ public void ConfigureServices(IServiceCollection services) { // Register the core services. services - .AddSettings() + .AddSettings() .AddReferenceDataOrchestrator(sp => new ReferenceDataOrchestrator(sp).Register()) .AddExecutionContext() .AddJsonSerializer() @@ -27,7 +27,7 @@ public void ConfigureServices(IServiceCollection services) .AddEventPublisher() .AddAzureServiceBusSender() .AddAzureServiceBusPurger() - .AddAzureServiceBusClient(connectionName: nameof(HrSettings.ServiceBusConnection)) + .AddAzureServiceBusClient(connectionName: nameof(AppNameSettings.ServiceBusConnection)) .AddJsonMergePatch() .AddWebApi(c => c.UnhandledExceptionAsync = (ex, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? new ConcurrencyException().ToResult() : null)) .AddReferenceDataContentWebApi() @@ -40,7 +40,7 @@ public void ConfigureServices(IServiceCollection services) .AddFluentValidators(); // Register the database and EF services, including required AutoMapper. - services.AddDatabase(sp => new HrDb(sp.GetRequiredService())) + services.AddDatabase(sp => new HrDb(sp.GetRequiredService())) .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) .AddScoped() .AddAutoMapper(typeof(HrEfDb).Assembly) @@ -50,8 +50,8 @@ public void ConfigureServices(IServiceCollection services) services .AddScoped() .AddHealthChecks() - .AddTypeActivatedCheck("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(HrSettings.ServiceBusConnection), nameof(HrSettings.VerificationQueueName)) - .AddTypeActivatedCheck("SQL Server", HealthStatus.Unhealthy, tags: default!, timeout: TimeSpan.FromSeconds(15), nameof(HrSettings.ConnectionStrings__Database)); + .AddTypeActivatedCheck("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(AppNameSettings.ServiceBusConnection), nameof(AppNameSettings.VerificationQueueName)) + .AddTypeActivatedCheck("SQL Server", HealthStatus.Unhealthy, tags: default!, timeout: TimeSpan.FromSeconds(15), nameof(AppNameSettings.ConnectionStrings__Database)); services.AddControllers(); diff --git a/src/Templates/content/Company.AppName.Business/HrSettings.cs b/src/Templates/content/Company.AppName.Business/AppNameSettings.cs similarity index 90% rename from src/Templates/content/Company.AppName.Business/HrSettings.cs rename to src/Templates/content/Company.AppName.Business/AppNameSettings.cs index 1af8541f..b5cf9b00 100644 --- a/src/Templates/content/Company.AppName.Business/HrSettings.cs +++ b/src/Templates/content/Company.AppName.Business/AppNameSettings.cs @@ -1,6 +1,6 @@ namespace Company.AppName.Business; -public class HrSettings : SettingsBase +public class AppNameSettings : SettingsBase { /// /// Gets the setting prefixes in order of precedence. @@ -8,10 +8,10 @@ public class HrSettings : SettingsBase public static string[] Prefixes { get; } = { "Hr/", "Common/" }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The . - public HrSettings(IConfiguration configuration) : base(configuration, Prefixes) { } + public AppNameSettings(IConfiguration configuration) : base(configuration, Prefixes) { } public string AgifyApiEndpointUri => GetValue(); diff --git a/src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs b/src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs index 2dc896dc..e5d488b1 100644 --- a/src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs +++ b/src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs @@ -5,7 +5,7 @@ namespace Company.AppName.Business.External; /// public class AgifyApiClient : TypedHttpClientCore { - public AgifyApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger> logger) + public AgifyApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, AppNameSettings settings, ILogger> logger) : base(client, jsonSerializer, executionContext, settings, logger) { if (!Uri.IsWellFormedUriString(settings.AgifyApiEndpointUri, UriKind.Absolute)) diff --git a/src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs b/src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs index e79591de..a601ebd0 100644 --- a/src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs +++ b/src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs @@ -5,7 +5,7 @@ namespace Company.AppName.Business.External; /// public class GenderizeApiClient : TypedHttpClientCore { - public GenderizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger> logger) + public GenderizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, AppNameSettings settings, ILogger> logger) : base(client, jsonSerializer, executionContext, settings, logger) { if (!Uri.IsWellFormedUriString(settings.GenderizeApiClientApiEndpointUri, UriKind.Absolute)) diff --git a/src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs b/src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs index fcb519d5..9ebfa3c2 100644 --- a/src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs +++ b/src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs @@ -5,7 +5,7 @@ namespace Company.AppName.Business.External; /// public class NationalizeApiClient : TypedHttpClientCore { - public NationalizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger> logger) + public NationalizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, AppNameSettings settings, ILogger> logger) : base(client, jsonSerializer, executionContext, settings, logger) { if (!Uri.IsWellFormedUriString(settings.NationalizeApiClientApiEndpointUri, UriKind.Absolute)) diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs b/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs index 3937526b..153bdd15 100644 --- a/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs +++ b/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs @@ -4,9 +4,9 @@ public class EmployeeService : IEmployeeService { private readonly HrDbContext _dbContext; private readonly IEventPublisher _publisher; - private readonly HrSettings _settings; + private readonly AppNameSettings _settings; - public EmployeeService(HrDbContext dbContext, IEventPublisher publisher, HrSettings settings) + public EmployeeService(HrDbContext dbContext, IEventPublisher publisher, AppNameSettings settings) { _dbContext = dbContext; _publisher = publisher; diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs b/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs index 4320a114..6b6d4f61 100644 --- a/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs +++ b/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs @@ -7,9 +7,9 @@ public class EmployeeService2 : IEmployeeService { private readonly IHrEfDb _efDb; private readonly IEventPublisher _publisher; - private readonly HrSettings _settings; + private readonly AppNameSettings _settings; - public EmployeeService2(IHrEfDb efDb, IEventPublisher publisher, HrSettings settings) + public EmployeeService2(IHrEfDb efDb, IEventPublisher publisher, AppNameSettings settings) { _efDb = efDb; _publisher = publisher; diff --git a/src/Templates/content/Company.AppName.Business/Services/VerificationService.cs b/src/Templates/content/Company.AppName.Business/Services/VerificationService.cs index a4759712..74fd503f 100644 --- a/src/Templates/content/Company.AppName.Business/Services/VerificationService.cs +++ b/src/Templates/content/Company.AppName.Business/Services/VerificationService.cs @@ -5,10 +5,10 @@ public class VerificationService private readonly AgifyApiClient _agifyApiClient; private readonly GenderizeApiClient _genderizeApiClient; private readonly NationalizeApiClient _nationalizeApiClient; - private readonly HrSettings _settings; + private readonly AppNameSettings _settings; private readonly IEventPublisher _publisher; - public VerificationService(AgifyApiClient agifyApiClient, GenderizeApiClient genderizeApiClient, NationalizeApiClient nationalizeApiClient, HrSettings settings, IEventPublisher publisher) + public VerificationService(AgifyApiClient agifyApiClient, GenderizeApiClient genderizeApiClient, NationalizeApiClient nationalizeApiClient, AppNameSettings settings, IEventPublisher publisher) { _agifyApiClient = agifyApiClient; _genderizeApiClient = genderizeApiClient; diff --git a/src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs b/src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs index b1c02987..e73d62ee 100644 --- a/src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs +++ b/src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs @@ -19,9 +19,9 @@ namespace Company.AppName.Functions; public class HttpTriggerQueueVerificationFunction { private readonly WebApiPublisher _webApiPublisher; - private readonly HrSettings _settings; + private readonly AppNameSettings _settings; - public HttpTriggerQueueVerificationFunction(WebApiPublisher webApiPublisher, HrSettings settings) + public HttpTriggerQueueVerificationFunction(WebApiPublisher webApiPublisher, AppNameSettings settings) { _webApiPublisher = webApiPublisher; _settings = settings; diff --git a/src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs b/src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs index 3e5eb7de..fd5bed76 100644 --- a/src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs +++ b/src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs @@ -23,6 +23,6 @@ public ServiceBusExecuteVerificationFunction(ServiceBusSubscriber subscriber, Ve [FunctionName(nameof(ServiceBusExecuteVerificationFunction))] [ExponentialBackoffRetry(3, "00:02:00", "00:30:00")] - public Task RunAsync([ServiceBusTrigger("%" + nameof(HrSettings.VerificationQueueName) + "%", Connection = nameof(HrSettings.ServiceBusConnection))] ServiceBusReceivedMessage message, ServiceBusMessageActions messageActions) + public Task RunAsync([ServiceBusTrigger("%" + nameof(AppNameSettings.VerificationQueueName) + "%", Connection = nameof(AppNameSettings.ServiceBusConnection))] ServiceBusReceivedMessage message, ServiceBusMessageActions messageActions) => _subscriber.ReceiveAsync(message, messageActions, ed => _service.VerifyAndPublish(ed.Value), validator: new EmployeeVerificationValidator().Wrap()); } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/Startup.cs b/src/Templates/content/Company.AppName.Functions/Startup.cs index fc559372..78047b2a 100644 --- a/src/Templates/content/Company.AppName.Functions/Startup.cs +++ b/src/Templates/content/Company.AppName.Functions/Startup.cs @@ -31,7 +31,7 @@ public override void Configure(IFunctionsHostBuilder builder) { // Register the core services. builder.Services - .AddSettings() + .AddSettings() .AddReferenceDataOrchestrator(sp => new ReferenceDataOrchestrator(sp, new MemoryCache(new MemoryCacheOptions())).Register()) .AddExecutionContext() .AddJsonSerializer() @@ -43,7 +43,7 @@ public override void Configure(IFunctionsHostBuilder builder) .AddJsonMergePatch() .AddWebApiPublisher() .AddAzureServiceBusSubscriber() - .AddAzureServiceBusClient(connectionName: nameof(HrSettings.ServiceBusConnection)); + .AddAzureServiceBusClient(connectionName: nameof(AppNameSettings.ServiceBusConnection)); // Register the health checks. builder.Services @@ -52,8 +52,8 @@ public override void Configure(IFunctionsHostBuilder builder) .AddTypeActivatedCheck>("Genderize API") .AddTypeActivatedCheck>("Agify API") .AddTypeActivatedCheck>("Nationalize API") - .AddTypeActivatedCheck("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(HrSettings.ServiceBusConnection), nameof(HrSettings.VerificationQueueName)) - .AddTypeActivatedCheck("SQL Server", HealthStatus.Unhealthy, tags: default, timeout: System.TimeSpan.FromSeconds(15), nameof(HrSettings.ConnectionStrings__Database)); + .AddTypeActivatedCheck("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(AppNameSettings.ServiceBusConnection), nameof(AppNameSettings.VerificationQueueName)) + .AddTypeActivatedCheck("SQL Server", HealthStatus.Unhealthy, tags: default, timeout: System.TimeSpan.FromSeconds(15), nameof(AppNameSettings.ConnectionStrings__Database)); // Register the business services. builder.Services @@ -69,7 +69,7 @@ public override void Configure(IFunctionsHostBuilder builder) // Database builder.Services.AddScoped(); - // builder.Services.AddDatabase(sp => new HrDb(sp.GetRequiredService())); + // builder.Services.AddDatabase(sp => new HrDb(sp.GetRequiredService())); builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); } catch (System.Exception ex) diff --git a/src/Templates/content/Docker.md b/src/Templates/content/Docker.md index c4b3b95f..21269492 100644 --- a/src/Templates/content/Docker.md +++ b/src/Templates/content/Docker.md @@ -14,11 +14,11 @@ version: '3.4' services: app-functions: environment: - - ServiceBusConnection=Endpoint=sb://Company.AppName.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=xxxxxx + - ServiceBusConnection=Endpoint=sb://Company-AppName.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=xxxxxx app-api: environment: - - ServiceBusConnection=Endpoint=sb://Company.AppName.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=xxxxxx + - ServiceBusConnection=Endpoint=sb://Company-AppName.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=xxxxxx ``` Service Bus should have `pendingverifications` queue used by *Company.AppName* sample. From 8d4557f1fe3886fa7110f37551f52609e5dd0596 Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 23 Sep 2022 18:28:09 -0400 Subject: [PATCH 05/39] renaming database Signed-off-by: Piotr --- samples/My.Hr/My.Hr.Api/Startup.cs | 2 +- samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs | 4 ++-- samples/My.Hr/My.Hr.Functions/Startup.cs | 2 +- src/Templates/content/Company.AppName.Api/Startup.cs | 2 +- .../Company.AppName.Business/AppNameSettings.cs | 2 +- .../Company.AppName.Business/Data/AppNameDb.cs | 11 +++++++++++ .../Data/{HrDbContext.cs => AppNameDbContext.cs} | 4 ++-- .../Data/{HrEfDb.cs => AppNameEfDb.cs} | 4 ++-- .../content/Company.AppName.Business/Data/HrDb.cs | 11 ----------- .../Services/EmployeeService.cs | 4 ++-- .../content/Company.AppName.Database/Dockerfile | 2 +- .../content/Company.AppName.Functions/Startup.cs | 2 +- 12 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs rename src/Templates/content/Company.AppName.Business/Data/{HrDbContext.cs => AppNameDbContext.cs} (76%) rename src/Templates/content/Company.AppName.Business/Data/{HrEfDb.cs => AppNameEfDb.cs} (86%) delete mode 100644 src/Templates/content/Company.AppName.Business/Data/HrDb.cs diff --git a/samples/My.Hr/My.Hr.Api/Startup.cs b/samples/My.Hr/My.Hr.Api/Startup.cs index e2ac1079..1e8320d8 100644 --- a/samples/My.Hr/My.Hr.Api/Startup.cs +++ b/samples/My.Hr/My.Hr.Api/Startup.cs @@ -41,7 +41,7 @@ public void ConfigureServices(IServiceCollection services) // Register the database and EF services, including required AutoMapper. services.AddDatabase(sp => new HrDb(sp.GetRequiredService())) - .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) + .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) .AddScoped() .AddAutoMapper(typeof(HrEfDb).Assembly) .AddAutoMapperWrapper(); diff --git a/samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs b/samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs index 440f4d68..41938b99 100644 --- a/samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs +++ b/samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs @@ -16,14 +16,14 @@ public interface IHrEfDb : IEfDb /// /// Represents the My.Hr database using Entity Framework. /// - public class HrEfDb : EfDb, IHrEfDb + public class HrEfDb : EfDb, IHrEfDb { /// /// Initializes a new instance of the class. /// /// The entity framework database context. /// The . - public HrEfDb(HrDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } + public HrEfDb(AppNameDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } /// /// Gets the encapsulated entity. diff --git a/samples/My.Hr/My.Hr.Functions/Startup.cs b/samples/My.Hr/My.Hr.Functions/Startup.cs index 64315403..5897cf11 100644 --- a/samples/My.Hr/My.Hr.Functions/Startup.cs +++ b/samples/My.Hr/My.Hr.Functions/Startup.cs @@ -69,7 +69,7 @@ public override void Configure(IFunctionsHostBuilder builder) // Database builder.Services.AddDatabase(sp => new HrDb(sp.GetRequiredService())); - builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); + builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); } catch (System.Exception ex) { diff --git a/src/Templates/content/Company.AppName.Api/Startup.cs b/src/Templates/content/Company.AppName.Api/Startup.cs index 8d2b6ab2..1bfe2737 100644 --- a/src/Templates/content/Company.AppName.Api/Startup.cs +++ b/src/Templates/content/Company.AppName.Api/Startup.cs @@ -41,7 +41,7 @@ public void ConfigureServices(IServiceCollection services) // Register the database and EF services, including required AutoMapper. services.AddDatabase(sp => new HrDb(sp.GetRequiredService())) - .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) + .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) .AddScoped() .AddAutoMapper(typeof(HrEfDb).Assembly) .AddAutoMapperWrapper(); diff --git a/src/Templates/content/Company.AppName.Business/AppNameSettings.cs b/src/Templates/content/Company.AppName.Business/AppNameSettings.cs index b5cf9b00..f58a132a 100644 --- a/src/Templates/content/Company.AppName.Business/AppNameSettings.cs +++ b/src/Templates/content/Company.AppName.Business/AppNameSettings.cs @@ -5,7 +5,7 @@ public class AppNameSettings : SettingsBase /// /// Gets the setting prefixes in order of precedence. /// - public static string[] Prefixes { get; } = { "Hr/", "Common/" }; + public static string[] Prefixes { get; } = { "AppName/", "Common/" }; /// /// Initializes a new instance of the class. diff --git a/src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs b/src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs new file mode 100644 index 00000000..0af3ba7c --- /dev/null +++ b/src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs @@ -0,0 +1,11 @@ +using CoreEx.Database; +using CoreEx.Database.SqlServer; +using Microsoft.Data.SqlClient; + +namespace Company.AppName.Business.Data +{ + public class AppName : SqlServerDatabase + { + public AppName(SettingsBase settings) : base(() => new SqlConnection(settings.GetRequiredValue("ConnectionStrings:Database"))) { } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Data/HrDbContext.cs b/src/Templates/content/Company.AppName.Business/Data/AppNameDbContext.cs similarity index 76% rename from src/Templates/content/Company.AppName.Business/Data/HrDbContext.cs rename to src/Templates/content/Company.AppName.Business/Data/AppNameDbContext.cs index 49ce0e5b..a1e5f13e 100644 --- a/src/Templates/content/Company.AppName.Business/Data/HrDbContext.cs +++ b/src/Templates/content/Company.AppName.Business/Data/AppNameDbContext.cs @@ -3,7 +3,7 @@ namespace Company.AppName.Business.Data; -public class HrDbContext : DbContext, IEfDbContext +public class AppNameDbContext : DbContext, IEfDbContext { public IDatabase BaseDatabase { get; } @@ -14,7 +14,7 @@ public class HrDbContext : DbContext, IEfDbContext public DbSet Employees { get; set; } #pragma warning disable CS8618 // Non-nullable property - properties set by Entity Framework Core - public HrDbContext(DbContextOptions options, IDatabase database) : base(options) => BaseDatabase = database ?? throw new ArgumentNullException(nameof(database)); + public AppNameDbContext(DbContextOptions options, IDatabase database) : base(options) => BaseDatabase = database ?? throw new ArgumentNullException(nameof(database)); #pragma warning restore CS8618 // Non-nullable property protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/src/Templates/content/Company.AppName.Business/Data/HrEfDb.cs b/src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs similarity index 86% rename from src/Templates/content/Company.AppName.Business/Data/HrEfDb.cs rename to src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs index c6199b49..40e60742 100644 --- a/src/Templates/content/Company.AppName.Business/Data/HrEfDb.cs +++ b/src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs @@ -16,14 +16,14 @@ public interface IHrEfDb : IEfDb /// /// Represents the Company.AppName database using Entity Framework. /// - public class HrEfDb : EfDb, IHrEfDb + public class HrEfDb : EfDb, IHrEfDb { /// /// Initializes a new instance of the class. /// /// The entity framework database context. /// The . - public HrEfDb(HrDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } + public HrEfDb(AppNameDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } /// /// Gets the encapsulated entity. diff --git a/src/Templates/content/Company.AppName.Business/Data/HrDb.cs b/src/Templates/content/Company.AppName.Business/Data/HrDb.cs deleted file mode 100644 index dec00972..00000000 --- a/src/Templates/content/Company.AppName.Business/Data/HrDb.cs +++ /dev/null @@ -1,11 +0,0 @@ -using CoreEx.Database; -using CoreEx.Database.SqlServer; -using Microsoft.Data.SqlClient; - -namespace Company.AppName.Business.Data -{ - public class HrDb : SqlServerDatabase - { - public HrDb(SettingsBase settings) : base(() => new SqlConnection(settings.GetRequiredValue("ConnectionStrings:Database"))) { } - } -} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs b/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs index 153bdd15..9a0765dd 100644 --- a/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs +++ b/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs @@ -2,11 +2,11 @@ namespace Company.AppName.Business.Services; public class EmployeeService : IEmployeeService { - private readonly HrDbContext _dbContext; + private readonly AppNameDbContext _dbContext; private readonly IEventPublisher _publisher; private readonly AppNameSettings _settings; - public EmployeeService(HrDbContext dbContext, IEventPublisher publisher, AppNameSettings settings) + public EmployeeService(AppNameDbContext dbContext, IEventPublisher publisher, AppNameSettings settings) { _dbContext = dbContext; _publisher = publisher; diff --git a/src/Templates/content/Company.AppName.Database/Dockerfile b/src/Templates/content/Company.AppName.Database/Dockerfile index 2fc459f4..338c49e6 100644 --- a/src/Templates/content/Company.AppName.Database/Dockerfile +++ b/src/Templates/content/Company.AppName.Database/Dockerfile @@ -33,7 +33,7 @@ ENV ACCEPT_EULA Y ENV MSSQL_SA_PASSWORD sAPWD23.^0 ENV MSSQL_TCP_PORT 1433 ENV MSSQL_AGENT_ENABLED true -ENV ConnectionStrings__sqlserver:MyHr Data Source=localhost, $MSSQL_TCP_PORT;Initial Catalog=Company.AppName;User id=sa;Password=$MSSQL_SA_PASSWORD;TrustServerCertificate=true +ENV ConnectionStrings__sqlserver:CompanyAppName Data Source=localhost, $MSSQL_TCP_PORT;Initial Catalog=Company.AppName;User id=sa;Password=$MSSQL_SA_PASSWORD;TrustServerCertificate=true # Copy setup scripts diff --git a/src/Templates/content/Company.AppName.Functions/Startup.cs b/src/Templates/content/Company.AppName.Functions/Startup.cs index 78047b2a..963d8c8c 100644 --- a/src/Templates/content/Company.AppName.Functions/Startup.cs +++ b/src/Templates/content/Company.AppName.Functions/Startup.cs @@ -70,7 +70,7 @@ public override void Configure(IFunctionsHostBuilder builder) // Database builder.Services.AddScoped(); // builder.Services.AddDatabase(sp => new HrDb(sp.GetRequiredService())); - builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); + builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); } catch (System.Exception ex) { From aadaa4862763f09cca507a9c9f8e947be87c68d0 Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 23 Sep 2022 18:32:03 -0400 Subject: [PATCH 06/39] renaming database Signed-off-by: Piotr --- src/Templates/content/.vscode/extensions.json | 2 +- .../content/Company.AppName.Business/Data/AppNameEfDb.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Templates/content/.vscode/extensions.json b/src/Templates/content/.vscode/extensions.json index ac9e785c..091aa53e 100644 --- a/src/Templates/content/.vscode/extensions.json +++ b/src/Templates/content/.vscode/extensions.json @@ -1,6 +1,6 @@ { "recommendations": [ - "ms-vscode.csharp", + "ms-dotnettools.csharp", "VisualStudioExptTeam.vscodeintellicode", "ms-mssql.mssql", "ms-vscode.azure-account", diff --git a/src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs b/src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs index 40e60742..79c1628c 100644 --- a/src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs +++ b/src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs @@ -5,7 +5,7 @@ namespace Company.AppName.Business.Data /// /// Enables the Company.AppName database using Entity Framework. /// - public interface IHrEfDb : IEfDb + public interface IAppNameEfDb : IEfDb { /// /// Gets the entity. @@ -16,14 +16,14 @@ public interface IHrEfDb : IEfDb /// /// Represents the Company.AppName database using Entity Framework. /// - public class HrEfDb : EfDb, IHrEfDb + public class AppNameEfDb : EfDb, IAppNameEfDb { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The entity framework database context. /// The . - public HrEfDb(AppNameDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } + public AppNameEfDb(AppNameDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } /// /// Gets the encapsulated entity. From 9f5e050ca07b5b96ffadddb93f2babaf2eedfc62 Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 23 Sep 2022 18:43:28 -0400 Subject: [PATCH 07/39] Fixes to DBName Signed-off-by: Piotr --- src/Templates/content/Company.AppName.Api/Startup.cs | 6 +++--- .../content/Company.AppName.Business/Data/AppNameDb.cs | 4 ++-- .../Company.AppName.Business/Services/EmployeeService2.cs | 4 ++-- .../Services/ReferenceDataService.cs | 4 ++-- src/Templates/content/Company.AppName.Database/Program.cs | 2 +- src/Templates/content/Company.AppName.Functions/Startup.cs | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Templates/content/Company.AppName.Api/Startup.cs b/src/Templates/content/Company.AppName.Api/Startup.cs index 1bfe2737..ea076dee 100644 --- a/src/Templates/content/Company.AppName.Api/Startup.cs +++ b/src/Templates/content/Company.AppName.Api/Startup.cs @@ -40,10 +40,10 @@ public void ConfigureServices(IServiceCollection services) .AddFluentValidators(); // Register the database and EF services, including required AutoMapper. - services.AddDatabase(sp => new HrDb(sp.GetRequiredService())) + services.AddDatabase(sp => new AppNameDb(sp.GetRequiredService())) .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) - .AddScoped() - .AddAutoMapper(typeof(HrEfDb).Assembly) + .AddScoped() + .AddAutoMapper(typeof(AppNameEfDb).Assembly) .AddAutoMapperWrapper(); // Register the health checks. diff --git a/src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs b/src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs index 0af3ba7c..ddf61c5f 100644 --- a/src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs +++ b/src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs @@ -4,8 +4,8 @@ namespace Company.AppName.Business.Data { - public class AppName : SqlServerDatabase + public class AppNameDb : SqlServerDatabase { - public AppName(SettingsBase settings) : base(() => new SqlConnection(settings.GetRequiredValue("ConnectionStrings:Database"))) { } + public AppNameDb(SettingsBase settings) : base(() => new SqlConnection(settings.GetRequiredValue("ConnectionStrings:Database"))) { } } } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs b/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs index 6b6d4f61..04d72c3d 100644 --- a/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs +++ b/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs @@ -5,11 +5,11 @@ namespace Company.AppName.Business.Services; /// public class EmployeeService2 : IEmployeeService { - private readonly IHrEfDb _efDb; + private readonly IAppNameEfDb _efDb; private readonly IEventPublisher _publisher; private readonly AppNameSettings _settings; - public EmployeeService2(IHrEfDb efDb, IEventPublisher publisher, AppNameSettings settings) + public EmployeeService2(IAppNameEfDb efDb, IEventPublisher publisher, AppNameSettings settings) { _efDb = efDb; _publisher = publisher; diff --git a/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs b/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs index 6c2ccf5b..bb247653 100644 --- a/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs +++ b/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs @@ -6,9 +6,9 @@ namespace Company.AppName.Business.Services; public class ReferenceDataService : IReferenceDataProvider { private readonly IDatabase _db; - private readonly HrDbContext _dbContext; + private readonly AppNameDbContext _dbContext; - public ReferenceDataService(IDatabase db, HrDbContext dbContext) + public ReferenceDataService(IDatabase db, AppNameDbContext dbContext) { _db = db; _dbContext = dbContext; diff --git a/src/Templates/content/Company.AppName.Database/Program.cs b/src/Templates/content/Company.AppName.Database/Program.cs index c3b2479a..ca23a85c 100644 --- a/src/Templates/content/Company.AppName.Database/Program.cs +++ b/src/Templates/content/Company.AppName.Database/Program.cs @@ -20,7 +20,7 @@ public static Task RunMigrator(string connectionString, Assembly? assembly .Create(connectionString) .ConsoleArgs(a => { - a.ConnectionStringEnvironmentVariableName = "My_HrDb"; + a.ConnectionStringEnvironmentVariableName = "Company_AppNameDb"; a.DataParserArgs.RefDataColumnDefaults.TryAdd("IsActive", _ => true); a.DataParserArgs.RefDataColumnDefaults.TryAdd("SortOrder", i => i); if (assembly != null) diff --git a/src/Templates/content/Company.AppName.Functions/Startup.cs b/src/Templates/content/Company.AppName.Functions/Startup.cs index 963d8c8c..359c3d09 100644 --- a/src/Templates/content/Company.AppName.Functions/Startup.cs +++ b/src/Templates/content/Company.AppName.Functions/Startup.cs @@ -68,8 +68,8 @@ public override void Configure(IFunctionsHostBuilder builder) builder.Services.AddTypedHttpClient("Nationalize"); // Database - builder.Services.AddScoped(); - // builder.Services.AddDatabase(sp => new HrDb(sp.GetRequiredService())); + builder.Services.AddScoped(); + builder.Services.AddDatabase(sp => new AppNameDb(sp.GetRequiredService())); builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); } catch (System.Exception ex) From 3e594529d37ccd87e968e65eb5f57e0684b68497 Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 23 Sep 2022 18:50:29 -0400 Subject: [PATCH 08/39] excluding conditions for now Signed-off-by: Piotr --- .../content/.template.config/template.json | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Templates/content/.template.config/template.json b/src/Templates/content/.template.config/template.json index 38e1d107..31078920 100644 --- a/src/Templates/content/.template.config/template.json +++ b/src/Templates/content/.template.config/template.json @@ -90,36 +90,36 @@ }, "sources": [ { - "modifiers": [ - { - "condition": "(implement_none)", - "exclude": [ "Company.AppName.Business/Validation/**/*", "Company.AppName.Business/Data/PersonData.cs" ] - }, - { - "condition": "(implement_cosmos || implement_httpagent || implement_none)", - "exclude": [ "Company.AppName.Database/**/*" ] - }, - { - "condition": "(!implement_entityframework)", - "exclude": [ "Company.AppName.Business/Data/AppNameEfDb.cs", "Company.AppName.Business/Data/AppNameEfDbContext.cs" ] - }, - { - "condition": "(!implement_cosmos)", - "exclude": [ "Company.AppName.Business/Data/AppNameCosmosDb.cs", "Company.AppName.Test/Cosmos/**/*" ] - }, - { - "condition": "(!implement_httpagent)", - "exclude": [ "Company.AppName.Business/Data/XxxAgent.cs", "Company.AppName.Business/Data/ReferenceDataData.cs" ] - }, - { - "condition": "(implement_httpagent)", - "exclude": [ "Company.AppName.Business/Data/PersonData.cs", "Company.AppName.Business/Validation/PersonArgsValidator.cs" ] - }, - { - "condition": "(!implement_database && !implement_entityframework)", - "exclude": [ "Company.AppName.Business/Data/AppNameDb.cs", "Company.AppName.Test/Data/**/*" ] - } - ] + // "modifiers": [ + // { + // "condition": "(implement_none)", + // "exclude": [ "Company.AppName.Business/Validation/**/*", "Company.AppName.Business/Data/PersonData.cs" ] + // }, + // { + // "condition": "(implement_cosmos || implement_httpagent || implement_none)", + // "exclude": [ "Company.AppName.Database/**/*" ] + // }, + // { + // "condition": "(!implement_entityframework)", + // "exclude": [ "Company.AppName.Business/Data/AppNameEfDb.cs", "Company.AppName.Business/Data/AppNameEfDbContext.cs" ] + // }, + // { + // "condition": "(!implement_cosmos)", + // "exclude": [ "Company.AppName.Business/Data/AppNameCosmosDb.cs", "Company.AppName.Test/Cosmos/**/*" ] + // }, + // { + // "condition": "(!implement_httpagent)", + // "exclude": [ "Company.AppName.Business/Data/XxxAgent.cs", "Company.AppName.Business/Data/ReferenceDataData.cs" ] + // }, + // { + // "condition": "(implement_httpagent)", + // "exclude": [ "Company.AppName.Business/Data/PersonData.cs", "Company.AppName.Business/Validation/PersonArgsValidator.cs" ] + // }, + // { + // "condition": "(!implement_database && !implement_entityframework)", + // "exclude": [ "Company.AppName.Business/Data/AppNameDb.cs", "Company.AppName.Test/Data/**/*" ] + // } + // ] } ], "primaryOutputs": [ From c91d89898b4f6a0b3f51f685754e027b7b65889c Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 23 Sep 2022 18:57:07 -0400 Subject: [PATCH 09/39] build for template Signed-off-by: Piotr --- .github/workflows/CI.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b3aac827..ce0b062f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -48,3 +48,6 @@ jobs: - name: Test Docker Build run: docker-compose -f docker-compose.myHr.yml -f docker-compose.myHr.override.yml build --build-arg LOCAL=true + + - name: Build Template + run: dotnet build src/Templates/content From 67a69554f5749348ec2aecb7e242c4d0bbbaffd2 Mon Sep 17 00:00:00 2001 From: Piotr Date: Sun, 25 Sep 2022 21:07:23 -0400 Subject: [PATCH 10/39] rename database Signed-off-by: Piotr --- .../Company.AppName.Business/Data/EmployeeConfiguration.cs | 2 +- .../Company.AppName.Business/Data/UsStateConfiguration.cs | 2 +- .../content/Company.AppName.Business/Models/Employee.cs | 2 +- .../Company.AppName.Business/Services/ReferenceDataService.cs | 2 +- .../Company.AppName.Database/Company.AppName.Database.csproj | 2 +- .../content/Company.AppName.Database/Data/RefData.yaml | 2 +- .../Migrations/20190101-000001-create-AppName-schema.sql | 2 ++ .../Migrations/20190101-000001-create-Hr-schema.sql | 2 -- ...ployee.sql => 20200909-162702-create-AppName-Employee.sql} | 2 +- ...ql => 20200909-163321-create-AppName-EmergencyContact.sql} | 2 +- ...r-gender.sql => 20200909-164735-create-AppName-gender.sql} | 2 +- ...l => 20200909-164828-create-AppName-terminationreason.sql} | 2 +- ...ql => 20200909-165308-create-AppName-relationshiptype.sql} | 2 +- ...usstate.sql => 20200909-165752-create-AppName-usstate.sql} | 2 +- ...l => 20200915-160812-create-AppName-PerformanceReview.sql} | 2 +- ... => 20200915-161927-create-AppName-performanceoutcome.sql} | 2 +- ...l => 20211208-001509-create-AppName-eventoutbox-table.sql} | 4 ++-- ... 20211208-001509-create-AppName-eventoutboxdata-table.sql} | 2 +- .../Company.AppName.Functions/MyHrApiConfigurationOptions.cs | 4 ++-- src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml | 2 +- 20 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql delete mode 100644 src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-Hr-schema.sql rename src/Templates/content/Company.AppName.Database/Migrations/{20200909-162702-create-Hr-Employee.sql => 20200909-162702-create-AppName-Employee.sql} (97%) rename src/Templates/content/Company.AppName.Database/Migrations/{20200909-163321-create-Hr-EmergencyContact.sql => 20200909-163321-create-AppName-EmergencyContact.sql} (88%) rename src/Templates/content/Company.AppName.Database/Migrations/{20200909-164735-create-hr-gender.sql => 20200909-164735-create-AppName-gender.sql} (92%) rename src/Templates/content/Company.AppName.Database/Migrations/{20200909-164828-create-hr-terminationreason.sql => 20200909-164828-create-AppName-terminationreason.sql} (90%) rename src/Templates/content/Company.AppName.Database/Migrations/{20200909-165308-create-hr-relationshiptype.sql => 20200909-165308-create-AppName-relationshiptype.sql} (90%) rename src/Templates/content/Company.AppName.Database/Migrations/{20200909-165752-create-hr-usstate.sql => 20200909-165752-create-AppName-usstate.sql} (92%) rename src/Templates/content/Company.AppName.Database/Migrations/{20200915-160812-create-Hr-PerformanceReview.sql => 20200915-160812-create-AppName-PerformanceReview.sql} (91%) rename src/Templates/content/Company.AppName.Database/Migrations/{20200915-161927-create-hr-performanceoutcome.sql => 20200915-161927-create-AppName-performanceoutcome.sql} (90%) rename src/Templates/content/Company.AppName.Database/Migrations/{20211208-001509-create-hr-eventoutbox-table.sql => 20211208-001509-create-AppName-eventoutbox-table.sql} (66%) rename src/Templates/content/Company.AppName.Database/Migrations/{20211208-001509-create-hr-eventoutboxdata-table.sql => 20211208-001509-create-AppName-eventoutboxdata-table.sql} (90%) diff --git a/src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs b/src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs index 1da9434a..157a03a7 100644 --- a/src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs +++ b/src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs @@ -4,7 +4,7 @@ public class EmployeeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("Employee", "Hr"); + builder.ToTable("Employee", "AppName"); builder.Property(p => p.Id).HasColumnName("EmployeeId").HasColumnType("UNIQUEIDENTIFIER"); builder.Property(p => p.Email).HasColumnType("NVARCHAR(250)"); builder.Property(p => p.FirstName).HasColumnType("NVARCHAR(100)"); diff --git a/src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs b/src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs index 7d4de479..813190d9 100644 --- a/src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs +++ b/src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs @@ -4,7 +4,7 @@ public class UsStateConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder entity) { - entity.ToTable("USState", "Hr"); + entity.ToTable("USState", "AppName"); entity.HasKey("Id"); entity.Property(p => p.Id).HasColumnName("USStateId").HasColumnType("UNIQUEIDENTIFIER"); entity.Property(p => p.Code).HasColumnType("NVARCHAR(50)"); diff --git a/src/Templates/content/Company.AppName.Business/Models/Employee.cs b/src/Templates/content/Company.AppName.Business/Models/Employee.cs index 798a8574..064d1040 100644 --- a/src/Templates/content/Company.AppName.Business/Models/Employee.cs +++ b/src/Templates/content/Company.AppName.Business/Models/Employee.cs @@ -3,7 +3,7 @@ namespace Company.AppName.Business.Models; /// -/// Represents the Entity Framework (EF) model for database object 'Hr.Employee'. +/// Represents the Entity Framework (EF) model for database object 'AppName.Employee'. /// public class Employee : IIdentifier, IETag { diff --git a/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs b/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs index bb247653..5cb75c40 100644 --- a/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs +++ b/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs @@ -19,7 +19,7 @@ public ReferenceDataService(IDatabase db, AppNameDbContext dbContext) public async Task GetAsync(Type type, CancellationToken cancellationToken = default) => type switch { Type t when t == typeof(USState) => await USStateCollection.CreateAsync(_dbContext.USStates.AsNoTracking(), cancellationToken).ConfigureAwait(false), - Type t when t == typeof(Gender) => await _db.ReferenceData("Hr", "Gender").LoadAsync("GenderId", cancellationToken: cancellationToken).ConfigureAwait(false), + Type t when t == typeof(Gender) => await _db.ReferenceData("AppName", "Gender").LoadAsync("GenderId", cancellationToken: cancellationToken).ConfigureAwait(false), _ => throw new InvalidOperationException($"Type {type.FullName} is not a known {nameof(IReferenceData)}.") }; } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj b/src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj index 246cf97c..533b883a 100644 --- a/src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj +++ b/src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Templates/content/Company.AppName.Database/Data/RefData.yaml b/src/Templates/content/Company.AppName.Database/Data/RefData.yaml index 2fad84b8..58c788d4 100644 --- a/src/Templates/content/Company.AppName.Database/Data/RefData.yaml +++ b/src/Templates/content/Company.AppName.Database/Data/RefData.yaml @@ -1,4 +1,4 @@ -Hr: +AppName: - $Gender: - F: Female - M: Male diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql b/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql new file mode 100644 index 00000000..4c44aac9 --- /dev/null +++ b/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql @@ -0,0 +1,2 @@ +CREATE SCHEMA [AppName] + AUTHORIZATION [dbo]; \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-Hr-schema.sql b/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-Hr-schema.sql deleted file mode 100644 index 5fc766a5..00000000 --- a/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-Hr-schema.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE SCHEMA [Hr] - AUTHORIZATION [dbo]; \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-Hr-Employee.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-AppName-Employee.sql similarity index 97% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-Hr-Employee.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-AppName-Employee.sql index e6d953e1..e342ff8c 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-Hr-Employee.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-AppName-Employee.sql @@ -2,7 +2,7 @@ BEGIN TRANSACTION -CREATE TABLE [Hr].[Employee] ( +CREATE TABLE [AppName].[Employee] ( [EmployeeId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, -- This is the primary key [Email] NVARCHAR(250) NULL UNIQUE, -- This is the employee's unique email address [FirstName] NVARCHAR(100) NULL, diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-Hr-EmergencyContact.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-AppName-EmergencyContact.sql similarity index 88% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-Hr-EmergencyContact.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-AppName-EmergencyContact.sql index 45a6652a..3b688084 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-Hr-EmergencyContact.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-AppName-EmergencyContact.sql @@ -2,7 +2,7 @@ BEGIN TRANSACTION -CREATE TABLE [Hr].[EmergencyContact] ( +CREATE TABLE [AppName].[EmergencyContact] ( [EmergencyContactId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, [EmployeeId] UNIQUEIDENTIFIER NOT NULL, [FirstName] NVARCHAR(100) NULL, diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-hr-gender.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-AppName-gender.sql similarity index 92% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-hr-gender.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-AppName-gender.sql index 08c2c383..9c2ac005 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-hr-gender.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-AppName-gender.sql @@ -2,7 +2,7 @@ BEGIN TRANSACTION -CREATE TABLE [Hr].[Gender] ( +CREATE TABLE [AppName].[Gender] ( [GenderId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, [Code] NVARCHAR(50) NOT NULL UNIQUE, [Text] NVARCHAR(250) NULL, diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-hr-terminationreason.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-AppName-terminationreason.sql similarity index 90% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-hr-terminationreason.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-AppName-terminationreason.sql index cd4534e5..4c774454 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-hr-terminationreason.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-AppName-terminationreason.sql @@ -2,7 +2,7 @@ BEGIN TRANSACTION -CREATE TABLE [Hr].[TerminationReason] ( +CREATE TABLE [AppName].[TerminationReason] ( [TerminationReasonId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, [Code] NVARCHAR(50) NOT NULL UNIQUE, [Text] NVARCHAR(250) NULL, diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-hr-relationshiptype.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-AppName-relationshiptype.sql similarity index 90% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-hr-relationshiptype.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-AppName-relationshiptype.sql index 692fdc36..dc84b6ec 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-hr-relationshiptype.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-AppName-relationshiptype.sql @@ -2,7 +2,7 @@ BEGIN TRANSACTION -CREATE TABLE [Hr].[RelationshipType] ( +CREATE TABLE [AppName].[RelationshipType] ( [RelationshipTypeId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, [Code] NVARCHAR(50) NOT NULL UNIQUE, [Text] NVARCHAR(250) NULL, diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-hr-usstate.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-AppName-usstate.sql similarity index 92% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-hr-usstate.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-AppName-usstate.sql index 12f0be9d..d9c725be 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-hr-usstate.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-AppName-usstate.sql @@ -2,7 +2,7 @@ BEGIN TRANSACTION -CREATE TABLE [Hr].[USState] ( +CREATE TABLE [AppName].[USState] ( [USStateId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, [Code] NVARCHAR(50) NOT NULL UNIQUE, [Text] NVARCHAR(250) NULL, diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-Hr-PerformanceReview.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-AppName-PerformanceReview.sql similarity index 91% rename from src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-Hr-PerformanceReview.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-AppName-PerformanceReview.sql index 5582abf7..93765bed 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-Hr-PerformanceReview.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-AppName-PerformanceReview.sql @@ -2,7 +2,7 @@ BEGIN TRANSACTION -CREATE TABLE [Hr].[PerformanceReview] ( +CREATE TABLE [AppName].[PerformanceReview] ( [PerformanceReviewId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, [EmployeeId] UNIQUEIDENTIFIER NOT NULL, [Date] DATETIME2 NULL, diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-hr-performanceoutcome.sql b/src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-AppName-performanceoutcome.sql similarity index 90% rename from src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-hr-performanceoutcome.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-AppName-performanceoutcome.sql index c61c2baf..8bcebcb0 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-hr-performanceoutcome.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-AppName-performanceoutcome.sql @@ -2,7 +2,7 @@ BEGIN TRANSACTION -CREATE TABLE [Hr].[PerformanceOutcome] ( +CREATE TABLE [AppName].[PerformanceOutcome] ( [PerformanceOutcomeId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWSEQUENTIALID()) PRIMARY KEY, [Code] NVARCHAR(50) NOT NULL UNIQUE, [Text] NVARCHAR(250) NULL, diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutbox-table.sql b/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutbox-table.sql similarity index 66% rename from src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutbox-table.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutbox-table.sql index fa6059d4..62773c7b 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutbox-table.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutbox-table.sql @@ -1,4 +1,4 @@ -CREATE TABLE [Hr].[EventOutbox] ( +CREATE TABLE [AppName].[EventOutbox] ( /* * This is automatically generated; any changes will be lost. */ @@ -7,5 +7,5 @@ CREATE TABLE [Hr].[EventOutbox] ( [EnqueuedDate] DATETIME2 NOT NULL, [PartitionKey] NVARCHAR(128) NULL, [DequeuedDate] DATETIME2 NULL, - CONSTRAINT [IX_Hr_EventOutbox_DequeuedDate] UNIQUE CLUSTERED ([DequeuedDate], [EventOutboxId]) + CONSTRAINT [IX_AppName_EventOutbox_DequeuedDate] UNIQUE CLUSTERED ([DequeuedDate], [EventOutboxId]) ); \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutboxdata-table.sql b/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutboxdata-table.sql similarity index 90% rename from src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutboxdata-table.sql rename to src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutboxdata-table.sql index 971dce15..dc88305b 100644 --- a/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-hr-eventoutboxdata-table.sql +++ b/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutboxdata-table.sql @@ -1,4 +1,4 @@ -CREATE TABLE [Hr].[EventOutboxData] ( +CREATE TABLE [AppName].[EventOutboxData] ( /* * This is automatically generated; any changes will be lost. */ diff --git a/src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs b/src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs index abedf728..a5260d66 100644 --- a/src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs +++ b/src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs @@ -11,8 +11,8 @@ public class MyOpenApiConfigurationOptions : DefaultOpenApiConfigurationOptions public override OpenApiInfo Info { get; set; } = new OpenApiInfo() { Version = "1.0.1", - Title = "CoreEx My HR Sample", - Description = "A serverless Azure Function which demonstrates the use of CoreEx.", + Title = "Company AppName API", + Description = "A serverless Azure Function which demonstrates the use of CoreEx for Company AppName - to be updated", TermsOfService = new Uri("https://github.com/Avanade/CoreEx"), License = new OpenApiLicense() diff --git a/src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml b/src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml index ae4d854e..20e76226 100644 --- a/src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml +++ b/src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml @@ -1,4 +1,4 @@ -Hr: +AppName: - Employee: - { EmployeeId: 1, Email: w.jones@org.com, FirstName: Wendy, LastName: Jones, GenderCode: F, Birthday: 1985-03-18, StartDate: 2000-12-11, PhoneNo: (425) 612 8113 } - { EmployeeId: 2, Email: b.smith@org.com, FirstName: Brian, LastName: Smith, GenderCode: M, Birthday: 1994-11-07, StartDate: 2013-08-06, TerminationDate: 2015-04-08, TerminationReasonCode: RE, PhoneNo: (429) 120 0098 } From 774b2cf537ecb49b24780601c1e9d733314467eb Mon Sep 17 00:00:00 2001 From: Piotr Date: Mon, 26 Sep 2022 12:33:08 -0400 Subject: [PATCH 11/39] dev container configured Signed-off-by: Piotr --- .../content/.devcontainer/Dockerfile | 9 +- .../content/.devcontainer/devcontainer.json | 95 +++++++++++-------- .../content/.devcontainer/docker-compose.yml | 55 +++++++++++ src/Templates/content/.vscode/extensions.json | 26 ++--- src/Templates/content/.vscode/launch.json | 11 +++ src/Templates/content/.vscode/settings.json | 7 ++ src/Templates/content/.vscode/tasks.json | 81 ++++++++++++++++ .../Company.AppName.Database/Dockerfile | 4 + src/Templates/readme.md | 6 ++ 9 files changed, 237 insertions(+), 57 deletions(-) create mode 100644 src/Templates/content/.devcontainer/docker-compose.yml create mode 100644 src/Templates/content/.vscode/launch.json create mode 100644 src/Templates/content/.vscode/settings.json create mode 100644 src/Templates/content/.vscode/tasks.json diff --git a/src/Templates/content/.devcontainer/Dockerfile b/src/Templates/content/.devcontainer/Dockerfile index 5f57b239..f68bef0e 100644 --- a/src/Templates/content/.devcontainer/Dockerfile +++ b/src/Templates/content/.devcontainer/Dockerfile @@ -6,7 +6,8 @@ FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT} # Set up machine requirements to build the repo RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install --no-install-recommends cmake llvm-9 clang-9 \ - build-essential python curl git lldb-6.0 liblldb-6.0-dev \ - libunwind8 libunwind8-dev gettext libicu-dev liblttng-ust-dev \ - libssl-dev libnuma-dev libkrb5-dev zlib1g-dev ninja-build \ No newline at end of file + && apt-get -y install --no-install-recommends curl git + +# install pulumi CLI +RUN curl -fsSL https://get.pulumi.com | sh +ENV PATH="${PATH}:/root/.pulumi/bin" diff --git a/src/Templates/content/.devcontainer/devcontainer.json b/src/Templates/content/.devcontainer/devcontainer.json index 929cb0c8..7267552d 100644 --- a/src/Templates/content/.devcontainer/devcontainer.json +++ b/src/Templates/content/.devcontainer/devcontainer.json @@ -1,44 +1,59 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.140.1/containers/dotnetcore { - "name": "C# (.NET 6)", - "build": { - "dockerfile": "Dockerfile", - "args": { - "VARIANT": "6.0", - } + "name": "C# (.NET 6)", + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": "docker-compose.yml", + + // The 'service' property is the name of the service for the container that VS Code should use. + "service": "dev", + "workspaceFolder": "/workspace", + + "settings": { + "files.associations": { + "*.csproj": "msbuild", + "*.fsproj": "msbuild", + "*.globalconfig": "ini", + "*.manifest": "xml", + "*.nuspec": "xml", + "*.pkgdef": "ini", + "*.projitems": "msbuild", + "*.props": "msbuild", + "*.resx": "xml", + "*.rsp": "Powershell", + "*.shproj": "msbuild", + "*.slnf": "json", + "*.targets": "msbuild", + "*.vbproj": "msbuild", + "*.vsixmanifest": "xml", + "*.vstemplate": "xml" }, - "settings": { - "files.associations": { - "*.csproj": "msbuild", - "*.fsproj": "msbuild", - "*.globalconfig": "ini", - "*.manifest": "xml", - "*.nuspec": "xml", - "*.pkgdef": "ini", - "*.projitems": "msbuild", - "*.props": "msbuild", - "*.resx": "xml", - "*.rsp": "Powershell", - "*.shproj": "msbuild", - "*.slnf": "json", - "*.targets": "msbuild", - "*.vbproj": "msbuild", - "*.vsixmanifest": "xml", - "*.vstemplate": "xml" - }, - // ms-dotnettools.csharp settings - "omnisharp.disableMSBuildDiagnosticWarning": true, - "omnisharp.enableEditorConfigSupport": true, - "omnisharp.enableImportCompletion": true, - "omnisharp.enableRoslynAnalyzers": true, - "omnisharp.useModernNet": true, - "omnisharp.enableAsyncCompletion": true, - }, - "extensions": [ - "ms-dotnettools.csharp", - "EditorConfig.EditorConfig", - "tintoy.msbuild-project-tools" - ], - "postCreateCommand": "dotnet restore" - } \ No newline at end of file + // ms-dotnettools.csharp settings + "omnisharp.disableMSBuildDiagnosticWarning": true, + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableImportCompletion": true, + "omnisharp.enableRoslynAnalyzers": true, + "omnisharp.useModernNet": true, + "omnisharp.enableAsyncCompletion": true + }, + "extensions": [ + "ms-dotnettools.csharp", + "EditorConfig.EditorConfig", + "tintoy.msbuild-project-tools", + "ms-dotnettools.csharp", + "VisualStudioExptTeam.vscodeintellicode", + "ms-mssql.mssql", + "ms-vscode.azure-account", + "ms-azuretools.vscode-azureappservice", + "ms-azuretools.vscode-azurefunctions", + "ms-azuretools.vscode-docker", + "ms-dotnettools.vscode-dotnet-runtime", + "MS-vsliveshare.vsliveshare-pack", + "Azurite.azurite", + "humao.rest-client" + ], + "postCreateCommand": "dotnet restore", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [80, 1433] +} \ No newline at end of file diff --git a/src/Templates/content/.devcontainer/docker-compose.yml b/src/Templates/content/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..86bcffdb --- /dev/null +++ b/src/Templates/content/.devcontainer/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.8' + +services: + dev: + build: + context: . + dockerfile: Dockerfile + args: + # Update 'VARIANT' to pick an NET VERSION + VARIANT: "6.0" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:80 + - ConnectionStrings__Database=Data Source=sqldata,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true + - PORT=80 + volumes: + - ..:/workspace:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:sqldata + + # Uncomment the next line to use a non-root user for all processes. + # user: node + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + sqldata: + build: + context: ../ + dockerfile: Company.AppName.Database/Dockerfile + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=sAPWD23.^0 + - MSSQL_TCP_PORT=1433 + - MSSQL_AGENT_ENABLED=true + - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__sqlserver:Company.AppName=Data Source=localhost,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true + volumes: + - app-sqldata:/var/opt/mssql + + # Uncomment to change startup options + # environment: + # MONGO_INITDB_ROOT_USERNAME: root + # MONGO_INITDB_ROOT_PASSWORD: example + # MONGO_INITDB_DATABASE: your-database-here + + # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + +volumes: + app-sqldata: \ No newline at end of file diff --git a/src/Templates/content/.vscode/extensions.json b/src/Templates/content/.vscode/extensions.json index 091aa53e..c581f1a8 100644 --- a/src/Templates/content/.vscode/extensions.json +++ b/src/Templates/content/.vscode/extensions.json @@ -1,15 +1,15 @@ { - "recommendations": [ - "ms-dotnettools.csharp", - "VisualStudioExptTeam.vscodeintellicode", - "ms-mssql.mssql", - "ms-vscode.azure-account", - "ms-azuretools.vscode-azureappservice", - "ms-azuretools.vscode-azurefunctions", - "ms-azuretools.vscode-docker", - "ms-dotnettools.vscode-dotnet-runtime", - "MS-vsliveshare.vsliveshare-pack", - "Azurite.azurite", - "humao.rest-client" - ] + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp", + "VisualStudioExptTeam.vscodeintellicode", + "ms-mssql.mssql", + "ms-vscode.azure-account", + "ms-azuretools.vscode-azureappservice", + "ms-azuretools.vscode-docker", + "ms-dotnettools.vscode-dotnet-runtime", + "MS-vsliveshare.vsliveshare-pack", + "Azurite.azurite", + "humao.rest-client" + ] } \ No newline at end of file diff --git a/src/Templates/content/.vscode/launch.json b/src/Templates/content/.vscode/launch.json new file mode 100644 index 00000000..d526e723 --- /dev/null +++ b/src/Templates/content/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ] +} \ No newline at end of file diff --git a/src/Templates/content/.vscode/settings.json b/src/Templates/content/.vscode/settings.json new file mode 100644 index 00000000..2bb65da3 --- /dev/null +++ b/src/Templates/content/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "azureFunctions.deploySubpath": "Company.AppName.Functions/bin/Release/net6.0/publish", + "azureFunctions.projectLanguage": "C#", + "azureFunctions.projectRuntime": "~4", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "publish (functions)" +} \ No newline at end of file diff --git a/src/Templates/content/.vscode/tasks.json b/src/Templates/content/.vscode/tasks.json new file mode 100644 index 00000000..1da50713 --- /dev/null +++ b/src/Templates/content/.vscode/tasks.json @@ -0,0 +1,81 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean (functions)", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/Company.AppName.Functions" + } + }, + { + "label": "build (functions)", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean (functions)", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/Company.AppName.Functions" + } + }, + { + "label": "clean release (functions)", + "command": "dotnet", + "args": [ + "clean", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/Company.AppName.Functions" + } + }, + { + "label": "publish (functions)", + "command": "dotnet", + "args": [ + "publish", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean release (functions)", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/Company.AppName.Functions" + } + }, + { + "type": "func", + "dependsOn": "build (functions)", + "options": { + "cwd": "${workspaceFolder}/Company.AppName.Functions/bin/Debug/net6.0" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" + } + ] +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Database/Dockerfile b/src/Templates/content/Company.AppName.Database/Dockerfile index 338c49e6..41b9f1aa 100644 --- a/src/Templates/content/Company.AppName.Database/Dockerfile +++ b/src/Templates/content/Company.AppName.Database/Dockerfile @@ -20,6 +20,10 @@ COPY "Company.AppName.Functions/Company.AppName.Functions.csproj" "Company.AppNa COPY "Company.AppName.Infra/Company.AppName.Infra.csproj" "Company.AppName.Infra/Company.AppName.Infra.csproj" COPY "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" +# todo: - remove this +COPY "NuGet.config" "NuGet.config" +COPY "nuget-publish" "nuget-publish" + RUN dotnet restore "Company.AppName.sln" COPY . . diff --git a/src/Templates/readme.md b/src/Templates/readme.md index a7d51169..249e5e33 100644 --- a/src/Templates/readme.md +++ b/src/Templates/readme.md @@ -52,3 +52,9 @@ Expose ports for function, app service and sql server Update: [PUT] http://localhost:7071/api/api/employees/{id} ## Add file that contains recommended VS CODE extensions + +DONE + +## Readme on configuring ADO + +https://github.com/Azure-Samples/todo-csharp-sql/tree/main/.azdo/pipelines \ No newline at end of file From 5adf94fbfda867e79ec8bce3d9c094e8127d9c62 Mon Sep 17 00:00:00 2001 From: Piotr Date: Mon, 26 Sep 2022 14:30:38 -0400 Subject: [PATCH 12/39] adding features and function core tools Signed-off-by: Piotr --- src/Templates/content/.devcontainer/Dockerfile | 13 +++++++++++++ .../content/.devcontainer/devcontainer.json | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Templates/content/.devcontainer/Dockerfile b/src/Templates/content/.devcontainer/Dockerfile index f68bef0e..98ae8d54 100644 --- a/src/Templates/content/.devcontainer/Dockerfile +++ b/src/Templates/content/.devcontainer/Dockerfile @@ -11,3 +11,16 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # install pulumi CLI RUN curl -fsSL https://get.pulumi.com | sh ENV PATH="${PATH}:/root/.pulumi/bin" + +# install azure functions core tools v4 +# https://github.com/Azure/azure-functions-core-tools#debian-9--10 +# DEBIAN_VERSION=11 +RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.asc.gpg\ + && sudo mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/ \ + && wget -q https://packages.microsoft.com/config/debian/11/prod.list \ + && sudo mv prod.list /etc/apt/sources.list.d/microsoft-prod.list \ + && sudo chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg \ + && sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list \ + && echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \ + && sudo apt-get update \ + && sudo apt-get -y install azure-functions-core-tools-4 \ No newline at end of file diff --git a/src/Templates/content/.devcontainer/devcontainer.json b/src/Templates/content/.devcontainer/devcontainer.json index 7267552d..73ea1783 100644 --- a/src/Templates/content/.devcontainer/devcontainer.json +++ b/src/Templates/content/.devcontainer/devcontainer.json @@ -9,7 +9,12 @@ // The 'service' property is the name of the service for the container that VS Code should use. "service": "dev", "workspaceFolder": "/workspace", - + "features": { + "github-cli": "2", + "azure-cli": "2.38", + "dotnet": "6.0", + "docker-from-docker": "20.10" + }, "settings": { "files.associations": { "*.csproj": "msbuild", From ad2e720167d1200deb9a8e3a48bedb2298c9d5b8 Mon Sep 17 00:00:00 2001 From: Piotr Date: Tue, 27 Sep 2022 10:40:28 -0400 Subject: [PATCH 13/39] fixing connections tring in docker containers Signed-off-by: Piotr --- .../content/.devcontainer/docker-compose.yml | 2 +- .../content/Company.AppName.Database/Dockerfile | 17 +++++++++++------ .../content/Company.AppName.Database/Program.cs | 2 +- .../content/docker-compose.override.yml | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Templates/content/.devcontainer/docker-compose.yml b/src/Templates/content/.devcontainer/docker-compose.yml index 86bcffdb..3425780d 100644 --- a/src/Templates/content/.devcontainer/docker-compose.yml +++ b/src/Templates/content/.devcontainer/docker-compose.yml @@ -38,7 +38,7 @@ services: - MSSQL_TCP_PORT=1433 - MSSQL_AGENT_ENABLED=true - ASPNETCORE_ENVIRONMENT=Development - - ConnectionStrings__sqlserver:Company.AppName=Data Source=localhost,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true + - ConnectionStrings__Database=Data Source=localhost,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true volumes: - app-sqldata:/var/opt/mssql diff --git a/src/Templates/content/Company.AppName.Database/Dockerfile b/src/Templates/content/Company.AppName.Database/Dockerfile index 41b9f1aa..0c50e588 100644 --- a/src/Templates/content/Company.AppName.Database/Dockerfile +++ b/src/Templates/content/Company.AppName.Database/Dockerfile @@ -1,10 +1,15 @@ -FROM mcr.microsoft.com/mssql/server:2019-latest AS base +FROM mcr.microsoft.com/mssql/server:2022-latest AS base USER root # Install dotnet sdk -RUN apt-get update; \ - apt-get install -y apt-transport-https && \ - apt-get update && \ - apt-get install -y dotnet-runtime-6.0 +# RUN apt-get update; \ +# apt-get install -y apt-transport-https && \ +# apt-get update && \ +# apt-get install -y dotnet-runtime-6.0 + +RUN wget -q https://dot.net/v1/dotnet-install.sh \ + && chmod +x ./dotnet-install.sh \ + && ./dotnet-install.sh --runtime aspnetcore -c 6.0 +ENV PATH="${PATH}:/root/.dotnet" FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src @@ -37,7 +42,7 @@ ENV ACCEPT_EULA Y ENV MSSQL_SA_PASSWORD sAPWD23.^0 ENV MSSQL_TCP_PORT 1433 ENV MSSQL_AGENT_ENABLED true -ENV ConnectionStrings__sqlserver:CompanyAppName Data Source=localhost, $MSSQL_TCP_PORT;Initial Catalog=Company.AppName;User id=sa;Password=$MSSQL_SA_PASSWORD;TrustServerCertificate=true +ENV ConnectionStrings__Database="Data Source=localhost, $MSSQL_TCP_PORT;Initial Catalog=Company.AppName;User id=sa;Password=$MSSQL_SA_PASSWORD;TrustServerCertificate=true" # Copy setup scripts diff --git a/src/Templates/content/Company.AppName.Database/Program.cs b/src/Templates/content/Company.AppName.Database/Program.cs index ca23a85c..2c8fef28 100644 --- a/src/Templates/content/Company.AppName.Database/Program.cs +++ b/src/Templates/content/Company.AppName.Database/Program.cs @@ -20,7 +20,7 @@ public static Task RunMigrator(string connectionString, Assembly? assembly .Create(connectionString) .ConsoleArgs(a => { - a.ConnectionStringEnvironmentVariableName = "Company_AppNameDb"; + a.ConnectionStringEnvironmentVariableName = "ConnectionStrings__Database"; a.DataParserArgs.RefDataColumnDefaults.TryAdd("IsActive", _ => true); a.DataParserArgs.RefDataColumnDefaults.TryAdd("SortOrder", i => i); if (assembly != null) diff --git a/src/Templates/content/docker-compose.override.yml b/src/Templates/content/docker-compose.override.yml index 6f79f8ae..a30fcaae 100644 --- a/src/Templates/content/docker-compose.override.yml +++ b/src/Templates/content/docker-compose.override.yml @@ -15,7 +15,7 @@ services: - MSSQL_TCP_PORT=1433 - MSSQL_AGENT_ENABLED=true - ASPNETCORE_ENVIRONMENT=Development - - ConnectionStrings__sqlserver:Company.AppName=Data Source=localhost,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true + - ConnectionStrings__Database=Data Source=localhost,1433;Initial Catalog=Company.AppName;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true ports: - "5433:1433" volumes: From 5cc8b57bd96fbfb4f18dac5e4e0a108c615105db Mon Sep 17 00:00:00 2001 From: Piotr Date: Tue, 27 Sep 2022 13:57:22 -0400 Subject: [PATCH 14/39] HTTP files Signed-off-by: Piotr --- .../content/.devcontainer/devcontainer.json | 2 +- .../content/Company.AppName.Api/Api.http | 58 +++++++++++++++++++ .../Controllers/HealthController.cs | 2 +- .../Company.AppName.Functions/.dockerignore | 1 - .../Company.AppName.Functions/.gitignore | 3 - .../Company.AppName.Functions/Functions.http | 58 +++++++++++++++++++ .../Company.AppName.Functions/README.md | 32 +--------- .../local.settings.json | 25 ++++++++ src/Templates/readme.md | 2 + 9 files changed, 147 insertions(+), 36 deletions(-) create mode 100644 src/Templates/content/Company.AppName.Api/Api.http delete mode 100644 src/Templates/content/Company.AppName.Functions/.dockerignore create mode 100644 src/Templates/content/Company.AppName.Functions/Functions.http create mode 100644 src/Templates/content/Company.AppName.Functions/local.settings.json diff --git a/src/Templates/content/.devcontainer/devcontainer.json b/src/Templates/content/.devcontainer/devcontainer.json index 73ea1783..8da43c15 100644 --- a/src/Templates/content/.devcontainer/devcontainer.json +++ b/src/Templates/content/.devcontainer/devcontainer.json @@ -60,5 +60,5 @@ ], "postCreateCommand": "dotnet restore", // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [80, 1433] + "forwardPorts": [80, 1433, 7071, 7129] } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Api/Api.http b/src/Templates/content/Company.AppName.Api/Api.http new file mode 100644 index 00000000..5a49347f --- /dev/null +++ b/src/Templates/content/Company.AppName.Api/Api.http @@ -0,0 +1,58 @@ +@port = 7129 +@hostname = localhost +@scheme = https +@host = {{scheme}}://{{hostname}}:{{port}} + + + +### Health endpoint +GET {{host}}/api/health + +# Swagger + +### Get openapi json +GET {{host}}/api/openapi/1.0 + +### Get swagger json +GET {{host}}/api/swagger.json + +### Get swagger UI +GET {{host}}/api/swagger/ui + +# Employee CRUD operations +# todo: add payloads +### Get all Employees +GET {{host}}/api/employees +x-correlation-id: 123-my-correlation-id + +### Get Employee +GET {{host}}/api/employees/{id} +x-correlation-id: 123-my-correlation-id + +### Create Employee +POST {{host}}/api/employees +x-correlation-id: 123-my-correlation-id + +### Update Employee +PUT {{host}}/api/employees/{id} +x-correlation-id: 123-my-correlation-id + +### Path Employee +PATCH {{host}}/api/employees/{id} +x-correlation-id: 123-my-correlation-id + +### Delete Employee +DELETE {{host}}/api/employees/{id} +x-correlation-id: 123-my-correlation-id + + +### Employee Verification scenario with service bus +POST {{host}}/api/employee/verify +Content-Type: application/json +x-correlation-id: 123-my-correlation-id + +{ + "name": "John", + "age": 27, + "gender": "male" +} diff --git a/src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs b/src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs index 52b6400f..10a9786d 100644 --- a/src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs +++ b/src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs @@ -4,7 +4,7 @@ namespace Company.AppName.Api.Controllers; /// Health Controller /// [ApiController] -[Route("[controller]")] +[Route("api/[controller]")] public class HealthController : ControllerBase { private readonly HealthService _health; diff --git a/src/Templates/content/Company.AppName.Functions/.dockerignore b/src/Templates/content/Company.AppName.Functions/.dockerignore deleted file mode 100644 index 1927772b..00000000 --- a/src/Templates/content/Company.AppName.Functions/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -local.settings.json \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/.gitignore b/src/Templates/content/Company.AppName.Functions/.gitignore index 3c3f4e6a..2c42eb3f 100644 --- a/src/Templates/content/Company.AppName.Functions/.gitignore +++ b/src/Templates/content/Company.AppName.Functions/.gitignore @@ -1,9 +1,6 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -# Azure Functions localsettings file -local.settings.json - # User-specific files *.suo *.user diff --git a/src/Templates/content/Company.AppName.Functions/Functions.http b/src/Templates/content/Company.AppName.Functions/Functions.http new file mode 100644 index 00000000..94d3c323 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/Functions.http @@ -0,0 +1,58 @@ +@port = 7071 +@hostname = localhost +@scheme = http +@host = {{scheme}}://{{hostname}}:{{port}} + + + +### Health endpoint +GET {{host}}/api/health + +# Swagger + +### Get openapi json +GET {{host}}/api/openapi/1.0 + +### Get swagger json +GET {{host}}/api/swagger.json + +### Get swagger UI +GET {{host}}/api/swagger/ui + +# Employee CRUD operations +# todo: add payloads +### Get all Employees +GET {{host}}/api/employees +x-correlation-id: 123-my-correlation-id + +### Get Employee +GET {{host}}/api/employees/{id} +x-correlation-id: 123-my-correlation-id + +### Create Employee +POST {{host}}/api/employees +x-correlation-id: 123-my-correlation-id + +### Update Employee +PUT {{host}}/api/employees/{id} +x-correlation-id: 123-my-correlation-id + +### Path Employee +PATCH {{host}}/api/employees/{id} +x-correlation-id: 123-my-correlation-id + +### Delete Employee +DELETE {{host}}/api/employees/{id} +x-correlation-id: 123-my-correlation-id + + +### Employee Verification scenario with service bus +POST {{host}}/api/employee/verify +Content-Type: application/json +x-correlation-id: 123-my-correlation-id + +{ + "name": "John", + "age": 27, + "gender": "male" +} diff --git a/src/Templates/content/Company.AppName.Functions/README.md b/src/Templates/content/Company.AppName.Functions/README.md index eeda67a3..81bc1ee7 100644 --- a/src/Templates/content/Company.AppName.Functions/README.md +++ b/src/Templates/content/Company.AppName.Functions/README.md @@ -1,35 +1,7 @@ # About -tbd +Functions project ## Configuration -Sample configuration for `local.settings.json` - -```json -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - - "AgifyApiEndpointUri": "https://api.agify.io", - "NationalizeApiClientApiEndpointUri": "https://api.nationalize.io", - "GenderizeApiClientApiEndpointUri": "https://api.genderize.io", - - "VerificationQueueName": "pendingVerifications", - "VerificationResultsQueueName": "verificationResults", - - "ServiceBusConnection__fullyQualifiedNamespace": "coreex.servicebus.windows.net", - "AzureWebJobs.ServiceBusExecuteVerificationFunction.Disabled": true, // disable when service bus is not available - - "HttpLogContent": "true", - "AzureFunctionsJobHost__logging__logLevel__CoreEx": "Debug", - "AzureFunctionsJobHost__logging__logToConsole": "true", - "AzureFunctionsJobHost__logging__logToConsoleColor": "true", - "AzureFunctionsJobHost__logging__console__isEnabled": "true", - - "MassPublishQueueName": "mass-publish" - } -} -``` +Update configuration in `local.settings.json` to update service bus namespace and enable SB triggered functions diff --git a/src/Templates/content/Company.AppName.Functions/local.settings.json b/src/Templates/content/Company.AppName.Functions/local.settings.json new file mode 100644 index 00000000..080ef750 --- /dev/null +++ b/src/Templates/content/Company.AppName.Functions/local.settings.json @@ -0,0 +1,25 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + + "AgifyApiEndpointUri": "https://api.agify.io", + "NationalizeApiClientApiEndpointUri": "https://api.nationalize.io", + "GenderizeApiClientApiEndpointUri": "https://api.genderize.io", + + "VerificationQueueName": "pendingVerifications", + "VerificationResultsQueueName": "verificationResults", + + "ServiceBusConnection__fullyQualifiedNamespace": "coreex.servicebus.windows.net", + "AzureWebJobs.ServiceBusExecuteVerificationFunction.Disabled": true, // disable when service bus is not available + + "HttpLogContent": "true", + "AzureFunctionsJobHost__logging__logLevel__CoreEx": "Debug", + "AzureFunctionsJobHost__logging__logToConsole": "true", + "AzureFunctionsJobHost__logging__logToConsoleColor": "true", + "AzureFunctionsJobHost__logging__console__isEnabled": "true", + + "MassPublishQueueName": "mass-publish" + } +} \ No newline at end of file diff --git a/src/Templates/readme.md b/src/Templates/readme.md index 249e5e33..0dd29b3e 100644 --- a/src/Templates/readme.md +++ b/src/Templates/readme.md @@ -24,6 +24,8 @@ Extensions required: * Azurite Extension * REST Client +--> DONE + Expose ports for function, app service and sql server ## Update readme to use REST Client From 128301e89568f81347a0fa46e2e7df98aa7645b0 Mon Sep 17 00:00:00 2001 From: Piotr Date: Tue, 27 Sep 2022 14:35:02 -0400 Subject: [PATCH 15/39] minor updates to infra stack Signed-off-by: Piotr --- .../Components/DevSetup.cs | 20 ++++++ .../Company.AppName.Infra/CoreExStack.cs | 52 +--------------- .../StackConfiguration.cs | 61 +++++++++++++++++++ 3 files changed, 83 insertions(+), 50 deletions(-) create mode 100644 src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs create mode 100644 src/Templates/content/Company.AppName.Infra/StackConfiguration.cs diff --git a/src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs b/src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs new file mode 100644 index 00000000..9a5180a5 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs @@ -0,0 +1,20 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Threading.Tasks; +using Pulumi; +using Pulumi.AzureNative.Storage; +using Pulumi.AzureNative.Web; +using Pulumi.AzureNative.Web.Inputs; +using AzureNative = Pulumi.AzureNative; + +namespace Company.AppName.Infra.Components; + +public class DevSetup : ComponentResource +{ + public DevSetup(string name, Input developerEmails, ComponentResourceOptions? options = null) + : base("coreexinfra:developer:setup", name, options) + { + // + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/CoreExStack.cs b/src/Templates/content/Company.AppName.Infra/CoreExStack.cs index cff6be05..7d0a8394 100644 --- a/src/Templates/content/Company.AppName.Infra/CoreExStack.cs +++ b/src/Templates/content/Company.AppName.Infra/CoreExStack.cs @@ -3,7 +3,6 @@ using Company.AppName.Infra.Services; using Pulumi; using Pulumi.AzureNative.Resources; -using AD = Pulumi.AzureAD; namespace Company.AppName.Infra; @@ -67,6 +66,8 @@ public static class CoreExStack Tags = tags }); + + // Permissions for function app storage.AddAccess(apps.FunctionPrincipalId, "functionApp"); serviceBus.AddAccess(apps.FunctionPrincipalId, "functionApp"); @@ -91,53 +92,4 @@ public static class CoreExStack ["AppSwaggerUrl"] = apps.AppSwaggerUrl, }; } - - public class StackConfiguration - { - public Input? SqlAdAdminLogin { get; private set; } - public Input? SqlAdAdminPassword { get; private set; } - public bool IsAppsDeploymentEnabled { get; private set; } - public bool IsDBSchemaDeploymentEnabled { get; private set; } - public string PendingVerificationsQueue { get; private set; } = default!; - public string VerificationResultsQueue { get; private set; } = default!; - public string MassPublishQueue { get; private set; } = default!; - - private StackConfiguration() { } - - public static async Task CreateConfiguration() - { - // read stack config - var config = new Config(); - - // get some info from Azure AD - var domainResult = await AD.GetDomains.InvokeAsync(new AD.GetDomainsArgs { OnlyDefault = true }); - var defaultUsername = $"sqlGlobalAdAdmin{Pulumi.Deployment.Instance.StackName}@{domainResult.Domains[0].DomainName}"; - var defaultPassword = new Pulumi.Random.RandomPassword("sqlAdPassword", new() - { - Length = 32, - Upper = true, - Number = true, - Special = true, - OverrideSpecial = "@", - MinLower = 2, - MinUpper = 2, - MinSpecial = 2, - MinNumeric = 2 - }).Result; - - Log.Info($"Default username is: {defaultUsername}"); - - return new StackConfiguration - { - SqlAdAdminLogin = Extensions.GetConfigValue("sqlAdAdmin", defaultUsername), - SqlAdAdminPassword = Extensions.GetConfigValue("sqlAdPassword", defaultPassword), - IsAppsDeploymentEnabled = config.GetBoolean("isAppsDeploymentEnabled") ?? false, - IsDBSchemaDeploymentEnabled = config.GetBoolean("isDBSchemaDeploymentEnabled") ?? false, - - PendingVerificationsQueue = config.Get("pendingVerificationsQueue") ?? "pendingVerifications", - VerificationResultsQueue = config.Get("verificationResultsQueue") ?? "verificationResults", - MassPublishQueue = config.Get("massPublishQueue") ?? "massPublish" - }; - } - } } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs b/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs new file mode 100644 index 00000000..6bef7960 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs @@ -0,0 +1,61 @@ +using System.Threading.Tasks; +using Pulumi; +using AD = Pulumi.AzureAD; + +namespace Company.AppName.Infra; + +public class StackConfiguration +{ + public Input? SqlAdAdminLogin { get; private set; } + public Input? SqlAdAdminPassword { get; private set; } + public bool IsAppsDeploymentEnabled { get; private set; } + public bool IsDBSchemaDeploymentEnabled { get; private set; } + public string PendingVerificationsQueue { get; private set; } = default!; + public string VerificationResultsQueue { get; private set; } = default!; + public string MassPublishQueue { get; private set; } = default!; + + /// + /// Emails for developer team, that will be added to Developers AD group. + /// + public string DeveloperEmails { get; private set; } = default!; + + private StackConfiguration() { } + + public static async Task CreateConfiguration() + { + // read stack config + var config = new Config(); + + // get some info from Azure AD + var domainResult = await AD.GetDomains.InvokeAsync(new AD.GetDomainsArgs { OnlyDefault = true }); + var defaultUsername = $"sqlGlobalAdAdmin{Pulumi.Deployment.Instance.StackName}@{domainResult.Domains[0].DomainName}"; + var defaultPassword = new Pulumi.Random.RandomPassword("sqlAdPassword", new() + { + Length = 32, + Upper = true, + Number = true, + Special = true, + OverrideSpecial = "@", + MinLower = 2, + MinUpper = 2, + MinSpecial = 2, + MinNumeric = 2 + }).Result; + + Log.Info($"Default username is: {defaultUsername}"); + + return new StackConfiguration + { + SqlAdAdminLogin = Extensions.GetConfigValue("sqlAdAdmin", defaultUsername), + SqlAdAdminPassword = Extensions.GetConfigValue("sqlAdPassword", defaultPassword), + IsAppsDeploymentEnabled = config.GetBoolean("isAppsDeploymentEnabled") ?? false, + IsDBSchemaDeploymentEnabled = config.GetBoolean("isDBSchemaDeploymentEnabled") ?? false, + + PendingVerificationsQueue = config.Get("pendingVerificationsQueue") ?? "pendingVerifications", + VerificationResultsQueue = config.Get("verificationResultsQueue") ?? "verificationResults", + MassPublishQueue = config.Get("massPublishQueue") ?? "massPublish", + + DeveloperEmails = config.Get("developerEmails") + }; + } +} \ No newline at end of file From 42ea8a95f590ed9c44ab58fcd2ef849966a96b30 Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 09:32:53 -0400 Subject: [PATCH 16/39] developer setup in Infra Signed-off-by: Piotr --- samples/My.Hr/My.Hr.Infra/Readme.md | 4 +- .../content/Company.AppName.Api/Dockerfile | 1 + .../Company.AppName.Business/Models/Gender.cs | 2 +- .../Services/EmployeeService2.cs | 2 +- .../Company.AppName.Database/Dockerfile | 1 + .../Company.AppName.Functions/Dockerfile | 1 + .../Company.AppName.Infra.Tests.csproj | 3 +- .../CoreExStackTests.cs | 10 +- .../Services/AzureApiClientTests.cs | 29 +++++ .../Services/AzureApiServiceTests.cs | 117 ++++++++++++++++++ .../Company.AppName.Infra.Tests/Testing.cs | 74 +++++++++-- .../TestingExtensions.cs | 2 +- .../Company.AppName.Infra/Components/Apps.cs | 49 +++----- .../Components/DevSetup.cs | 48 +++++-- .../Components/Diagnostics.cs | 2 +- .../Components/Messaging.cs | 8 +- .../Company.AppName.Infra/Components/Sql.cs | 15 +-- .../Components/Storage.cs | 6 +- .../Company.AppName.Infra/CoreExStack.cs | 22 +++- .../content/Company.AppName.Infra/Program.cs | 8 +- .../content/Company.AppName.Infra/Readme.md | 4 +- .../Roles/BuiltInRolesIds.cs | 52 -------- .../Services/AzureApiClient.cs | 33 +++++ .../Services/AzureApiService.cs | 112 +++++++++++++++++ .../Services/PulumiLogger.cs | 43 +++++++ .../StackConfiguration.cs | 5 +- ...rviceBusExecuteVerificationFunctionTest.cs | 46 ++++++- .../appsettings.unittest.json | 6 +- src/Templates/content/Company.AppName.sln | 6 + src/Templates/readme.md | 10 +- 30 files changed, 572 insertions(+), 149 deletions(-) create mode 100644 src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs create mode 100644 src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Services/AzureApiClient.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Services/AzureApiService.cs create mode 100644 src/Templates/content/Company.AppName.Infra/Services/PulumiLogger.cs diff --git a/samples/My.Hr/My.Hr.Infra/Readme.md b/samples/My.Hr/My.Hr.Infra/Readme.md index a3d78adb..61b69148 100644 --- a/samples/My.Hr/My.Hr.Infra/Readme.md +++ b/samples/My.Hr/My.Hr.Infra/Readme.md @@ -7,7 +7,9 @@ The easiest way to deploy it is by using Pulumi account (Free), but it's not man Prerequisites: 1. [Pulumi CLI](https://www.pulumi.com/docs/get-started/install/) -2. Azure CLI - logged in to Azure +2. Azure CLI - logged in to Azure subscription with permissions to create service principals + +> Note: Some corporate AAD restrict what can be done in AAD. Since this sample creates AAD User and Group, infrastructure needs to be created in AAD tenant that allows it. ## Pulumi with azure storage diff --git a/src/Templates/content/Company.AppName.Api/Dockerfile b/src/Templates/content/Company.AppName.Api/Dockerfile index da1edfc7..edd03c91 100644 --- a/src/Templates/content/Company.AppName.Api/Dockerfile +++ b/src/Templates/content/Company.AppName.Api/Dockerfile @@ -13,6 +13,7 @@ COPY "Company.AppName.Api/Company.AppName.Api.csproj" "Company.AppName.Api/Compa COPY "Company.AppName.Business/Company.AppName.Business.csproj" "Company.AppName.Business/Company.AppName.Business.csproj" COPY "Company.AppName.Database/Company.AppName.Database.csproj" "Company.AppName.Database/Company.AppName.Database.csproj" COPY "Company.AppName.Functions/Company.AppName.Functions.csproj" "Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "Company.AppName.UnitTest/Company.AppName.UnitTest.csproj" "Company.AppName.UnitTest/Company.AppName.UnitTest.csproj" COPY "Company.AppName.Infra/Company.AppName.Infra.csproj" "Company.AppName.Infra/Company.AppName.Infra.csproj" COPY "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" diff --git a/src/Templates/content/Company.AppName.Business/Models/Gender.cs b/src/Templates/content/Company.AppName.Business/Models/Gender.cs index 69b1e0de..19a87987 100644 --- a/src/Templates/content/Company.AppName.Business/Models/Gender.cs +++ b/src/Templates/content/Company.AppName.Business/Models/Gender.cs @@ -2,7 +2,7 @@ namespace Company.AppName.Business.Models; -public class Gender : ReferenceDataBase +public class Gender : ReferenceDataBase { public static implicit operator Gender?(string? code) => ConvertFromCode(code); } diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs b/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs index 04d72c3d..346107d0 100644 --- a/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs +++ b/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs @@ -18,7 +18,7 @@ public EmployeeService2(IAppNameEfDb efDb, IEventPublisher publisher, AppNameSet public Task GetEmployeeAsync(Guid id) => _efDb.Employees.GetAsync(id); - public Task GetAllAsync(PagingArgs? paging) + public Task GetAllAsync(PagingArgs? paging) => _efDb.Employees.Query(q => q.OrderBy(x => x.LastName).ThenBy(x => x.FirstName)).WithPaging(paging).SelectResultAsync(); public Task AddEmployeeAsync(Employee employee) => _efDb.Employees.CreateAsync(employee); diff --git a/src/Templates/content/Company.AppName.Database/Dockerfile b/src/Templates/content/Company.AppName.Database/Dockerfile index 0c50e588..9a6dd08d 100644 --- a/src/Templates/content/Company.AppName.Database/Dockerfile +++ b/src/Templates/content/Company.AppName.Database/Dockerfile @@ -22,6 +22,7 @@ COPY "Company.AppName.Api/Company.AppName.Api.csproj" "Company.AppName.Api/Compa COPY "Company.AppName.Business/Company.AppName.Business.csproj" "Company.AppName.Business/Company.AppName.Business.csproj" COPY "Company.AppName.Database/Company.AppName.Database.csproj" "Company.AppName.Database/Company.AppName.Database.csproj" COPY "Company.AppName.Functions/Company.AppName.Functions.csproj" "Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "Company.AppName.UnitTest/Company.AppName.UnitTest.csproj" "Company.AppName.UnitTest/Company.AppName.UnitTest.csproj" COPY "Company.AppName.Infra/Company.AppName.Infra.csproj" "Company.AppName.Infra/Company.AppName.Infra.csproj" COPY "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" diff --git a/src/Templates/content/Company.AppName.Functions/Dockerfile b/src/Templates/content/Company.AppName.Functions/Dockerfile index a725bc1c..d7613da2 100644 --- a/src/Templates/content/Company.AppName.Functions/Dockerfile +++ b/src/Templates/content/Company.AppName.Functions/Dockerfile @@ -14,6 +14,7 @@ COPY "Company.AppName.Api/Company.AppName.Api.csproj" "Company.AppName.Api/Compa COPY "Company.AppName.Business/Company.AppName.Business.csproj" "Company.AppName.Business/Company.AppName.Business.csproj" COPY "Company.AppName.Database/Company.AppName.Database.csproj" "Company.AppName.Database/Company.AppName.Database.csproj" COPY "Company.AppName.Functions/Company.AppName.Functions.csproj" "Company.AppName.Functions/Company.AppName.Functions.csproj" +COPY "Company.AppName.UnitTest/Company.AppName.UnitTest.csproj" "Company.AppName.UnitTest/Company.AppName.UnitTest.csproj" COPY "Company.AppName.Infra/Company.AppName.Infra.csproj" "Company.AppName.Infra/Company.AppName.Infra.csproj" COPY "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" "Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj" diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj b/src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj index f279b038..351a6dc5 100644 --- a/src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj +++ b/src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj @@ -10,12 +10,13 @@ - + + diff --git a/src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs b/src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs index f17b8221..8982e84b 100644 --- a/src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs +++ b/src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs @@ -9,7 +9,7 @@ public class CoreExStackTests [Test] public async Task ResourceGroupHasNameTag() { - var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); + var (resources, _, _) = await Testing.RunAsync(); var rgs = resources.OfType(); var rg = rgs.First(); @@ -46,25 +46,25 @@ public async Task AllResourcesHaveNameTag() [Test] public async Task FunctionIsCreatedWithAUrl() { - var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); + var (_, outputs, _) = await Testing.RunAsync(); var healthUrl = await outputs["FunctionHealthUrl"]!.GetValueAsync(); var appSwaggerUrl = await outputs["AppSwaggerUrl"]!.GetValueAsync(); // Assert - healthUrl.Should().Be("https://unittest.azurewebsites.net/api/health?code=key", because: "mock values set in Testing class"); + healthUrl.Should().Be("https://unittest.azurewebsites.net/api/health?code=mocked-key", because: "mock values set in Testing class"); appSwaggerUrl.Should().Be("https://unittest.azurewebsites.net/swagger/index.html", because: "mock values set in Testing class"); } [Test] public async Task SqlIsCreatedWithConnectionString() { - var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); + var (_, outputs, _) = await Testing.RunAsync(); var connectionString = await outputs["SqlDatabaseConnectionString"]!.GetValueAsync(); // Assert - connectionString.Should().Be("Server=sql-server-stack.database.windows.net; Authentication=Active Directory Default; Database=sqldb", because: "mock values set in Testing class"); + connectionString.Should().Be("Server=sql-server-unit-test-stack.database.windows.net; Authentication=Active Directory Default; Database=sqldb", because: "mock values set in Testing class"); } [Test] diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs b/src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs new file mode 100644 index 00000000..f1d2e507 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs @@ -0,0 +1,29 @@ + +using System.Net; +using Company.AppName.Infra.Services; +using UnitTestEx.NUnit; + +namespace Company.AppName.Infra.Tests.Services; + +public class AzureApiClientTests +{ + [Test] + public async Task GetMyIP_Should_ReturnIP() + { + // Arrange + const string mockedIp = "192.168.1.1"; + var mcf = MockHttpClientFactory.Create(); + var mc = mcf.CreateClient("ip"); + + mc.Request(HttpMethod.Get, "https://api.ipify.org") + .Respond.With(new StringContent(mockedIp)); + + var target = new AzureApiClient(mcf.GetHttpClient("ip")!); + + // Act + var result = await target.GetMyIP(); + + // Assert + result.Should().Be(mockedIp); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs b/src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs new file mode 100644 index 00000000..c669517d --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs @@ -0,0 +1,117 @@ + +using System.Net; +using Company.AppName.Infra.Services; +using UnitTestEx.NUnit; + +namespace Company.AppName.Infra.Tests.Services; + +public class AzureApiServiceTests +{ + [Test] + public async Task GetHostKeys_Should_GetHostKeyFromAzure() + { + // Arrange + const string resourceGroupName = "resource-group-name"; + const string functionAppName = "function-app"; + const string mockedKey = "mocked-key"; + var resourceGroup = Output.Create(resourceGroupName); + var functionApp = Output.Create(functionAppName); + var mcf = MockHttpClientFactory.Create(); + var mc = mcf.CreateClient("azure"); + + mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{Testing.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{functionAppName}/host/default/listkeys?api-version=2022-03-01") + .Respond.WithJson(new AzureApiService.HostKeys { FunctionKeys = new AzureApiService.FunctionKeysValue { Key = mockedKey } }); + + var target = new AzureApiService(new AzureApiClient(mcf.GetHttpClient("azure")!)); + + // Act + var resultOutput = await Testing.RunAsync(() => target.GetHostKeys(resourceGroup, functionApp)); + var result = await resultOutput.GetValueAsync(); + + // Assert + result.Should().Be(mockedKey); + mcf.VerifyAll(); + } + + [Test] + [Category("long")] + [Description("Test retries for the azure call and because of that takes long time (2 min)")] + public async Task GetHostKeys_Should_GetHostKeyFromAzure_When_FirstCallFails() + { + // Arrange + const string resourceGroupName = "resource-group-name"; + const string functionAppName = "function-app"; + const string mockedKey = "mocked-key"; + var resourceGroup = Output.Create(resourceGroupName); + var functionApp = Output.Create(functionAppName); + var mcf = MockHttpClientFactory.Create(); + var mc = mcf.CreateClient("azure"); + + mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{Testing.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{functionAppName}/host/default/listkeys?api-version=2022-03-01") + .Respond.WithSequence(s => + { + s.Respond().With(HttpStatusCode.NotFound); + s.Respond().With(HttpStatusCode.BadRequest); + s.Respond().With(HttpStatusCode.BadRequest); + s.Respond().WithJson(new AzureApiService.HostKeys { FunctionKeys = new AzureApiService.FunctionKeysValue { Key = mockedKey } }); + }); + + var target = new AzureApiService(new AzureApiClient(mcf.GetHttpClient("azure")!)); + + // Act + var resultOutput = await Testing.RunAsync(() => target.GetHostKeys(resourceGroup, functionApp)); + var result = await resultOutput.GetValueAsync(); + + // Assert + result.Should().Be(mockedKey); + mcf.VerifyAll(); + } + + [Test] + public async Task GetRoleIdByName_Should_ReturnRoleId() + { + // Arrange + const string mockedId = "mocked-id"; + const string roleName = "StorageBlobContributor"; + var mcf = MockHttpClientFactory.Create(); + var mc = mcf.CreateClient("azure"); + + mc.Request(HttpMethod.Get, $"https://management.azure.com/subscriptions/{Testing.SubscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-01-01-preview&$filter=roleName eq '{roleName}'") + .Respond.WithJson(new AzureApiService.RoleDefinition { Value = new() { new AzureApiService.RoleDefinitionValue { Id = mockedId } } }); + + var target = new AzureApiService(new AzureApiClient(mcf.GetHttpClient("azure")!)); + + // Act + var resultTask = await Testing.RunAsync(() => target.GetRoleIdByName(roleName)); + var result = await resultTask; + + // Assert + result.Should().Be(mockedId); + mcf.VerifyAll(); + } + + [Test] + public async Task SyncTriggers_Should_SyncTriggersOnTheFunctionApp() + { + // Arrange + const string resourceGroupName = "resource-group-name"; + const string functionAppName = "function-app"; + var resourceGroup = Output.Create(resourceGroupName); + var functionApp = Output.Create(functionAppName); + var mcf = MockHttpClientFactory.Create(); + var mc = mcf.CreateClient("azure"); + + mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{Testing.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{functionAppName}/syncfunctiontriggers?api-version=2016-08-01") + .Respond.With(statusCode: HttpStatusCode.NoContent); + + var target = new AzureApiService(new AzureApiClient(mcf.GetHttpClient("azure")!)); + + // Act + var resultOutput = await Testing.RunAsync(() => target.SyncFunctionAppTriggers(resourceGroup, functionApp)); + var result = await resultOutput.GetValueAsync(); + + // Assert + result.Should().BeTrue(); + mcf.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Testing.cs b/src/Templates/content/Company.AppName.Infra.Tests/Testing.cs index c0607b74..699a6666 100644 --- a/src/Templates/content/Company.AppName.Infra.Tests/Testing.cs +++ b/src/Templates/content/Company.AppName.Infra.Tests/Testing.cs @@ -1,18 +1,45 @@ using System.Collections.Immutable; using System.Text.Json; using Company.AppName.Infra.Services; +using UnitTestEx.NUnit; namespace Company.AppName.Infra.Tests; public static class Testing { + // mocked subscription Id + public const string SubscriptionId = "622637dd-a3e9-4e54-test-56d9247c70ee"; + public const string StackName = "unit-test-stack"; + public static async Task<(ImmutableArray Resources, IDictionary Outputs, Mock)> RunAsync() + { + var dbOperationsMock = new Mock(); + var mcf = MockHttpClientFactory.Create(); + var mc = mcf.CreateClient("azure"); + + mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{SubscriptionId}/resourceGroups/coreEx-{StackName}/providers/Microsoft.Web/sites/funApp/host/default/listkeys?api-version=2022-03-01") + .Respond.WithJson(new AzureApiService.HostKeys { FunctionKeys = new AzureApiService.FunctionKeysValue { Key = "mocked-key" } }); + + mc.Request(HttpMethod.Get, "https://api.ipify.org") + .Respond.With(new StringContent("215.45.1.567")); + + mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{SubscriptionId}/resourceGroups/coreEx-{StackName}/providers/Microsoft.Web/sites/funApp/syncfunctiontriggers?api-version=2016-08-01") + .Respond.With(statusCode: System.Net.HttpStatusCode.NoContent); + + var (resources, outputs) = await RunAsync(() => CoreExStack.ExecuteStackAsync(dbOperationsMock.Object, mcf.GetHttpClient("azure")!)); + + return (resources, outputs, dbOperationsMock); + } + + public static async Task<(ImmutableArray Resources, IDictionary Outputs)> RunAsync(Func>> createResources) { var config = new Dictionary{ {"unittest:sqlAdAdmin", "sqlAdAdmin"}, {"unittest:sqlAdPassword", "sqlAdPassword"}, {"unittest:isAppsDeploymentEnabled", "true"}, - {"unittest:isDBSchemaDeploymentEnabled", "true"} + {"unittest:isDBSchemaDeploymentEnabled", "true"}, + {"unittest:developerEmails", "dev1@somedomain.com,dev2@somedomain.com"} + }; Environment.SetEnvironmentVariable("PULUMI_CONFIG", JsonSerializer.Serialize(config)); @@ -22,12 +49,27 @@ public static class Testing TestOptions options = new() { IsPreview = false, - ProjectName = "unittest" + ProjectName = "unittest", + StackName = StackName }; - var (resources, outputs) = await Deployment.TestAsync(mocks, options, () => CoreExStack.ExecuteStackAsync(dbOperationsMock.Object)); + var (resources, outputs) = await Deployment.TestAsync(mocks, options, () => createResources()); - return (resources, outputs, dbOperationsMock); + return (resources, outputs); + } + + public static async Task RunAsync(Func createResources) where T : class + { + var (resources, outputs) = await RunAsync(() => + { + var result = createResources(); + return Task.FromResult>(new Dictionary + { + ["result"] = result + }); + }); + + return (T)outputs["result"]!; } public class Mocks : IMocks @@ -43,10 +85,6 @@ public Task CallAsync(MockCallArgs args) outputs.Add("id", Guid.NewGuid().ToString()); break; - case "azure-native:web:listWebAppHostKeys": - outputs.Add("masterKey", "key"); - break; - case "azure-native:storage:listStorageAccountKeys": var kvJson = JsonDocument.Parse("[{\"value\":\"valueKeyStorage\"}]").RootElement; outputs.Add("keys", kvJson); @@ -57,8 +95,24 @@ public Task CallAsync(MockCallArgs args) outputs.Add("domains", adJson); break; + case "azuread:index/getClientConfig:getClientConfig": + outputs.Add("objectId", "current-user-guid"); + break; + + case "azuread:index/getUser:getUser": + outputs.Add("id", "id-for-user"); + break; + + case "azure-native:authorization:getClientConfig": + outputs.Add("subscriptionId", SubscriptionId); + break; + + case "azure-native:authorization:getClientToken": + outputs.Add("token", "not-a-real-token"); + break; + default: - throw new InvalidOperationException($"Operation {args.Token} is not supported"); + throw new InvalidOperationException($"Operation {args.Token} is not supported. Fix your mock setup."); } return Task.FromResult((object)outputs); @@ -75,7 +129,7 @@ public Task CallAsync(MockCallArgs args) if (!args.Inputs.ContainsKey("name")) outputs.Add("name", args.Name ?? "name"); - // <-- We'll customize the mocks here + // Mocks customizations if (args.Type == "azure-native:web:WebApp") { outputs["outboundIpAddresses"] = "192.167.12.1,24.56.76.1"; diff --git a/src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs b/src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs index 4944eed4..9de7483a 100644 --- a/src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs +++ b/src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs @@ -13,7 +13,7 @@ public static Task GetValueAsync(this object outputObj) { if (outputObj is Output output) { - return output.GetValueAsync(); + return output.GetValueAsync(); } return Task.FromException(new ArgumentException("Provided object is not Output", nameof(outputObj))); diff --git a/src/Templates/content/Company.AppName.Infra/Components/Apps.cs b/src/Templates/content/Company.AppName.Infra/Components/Apps.cs index 914e2684..ae843cac 100644 --- a/src/Templates/content/Company.AppName.Infra/Components/Apps.cs +++ b/src/Templates/content/Company.AppName.Infra/Components/Apps.cs @@ -1,7 +1,8 @@ using System; using System.Diagnostics; -using System.Net.Http; +using System.IO; using System.Threading.Tasks; +using Company.AppName.Infra.Services; using Pulumi; using Pulumi.AzureNative.Storage; using Pulumi.AzureNative.Web; @@ -22,8 +23,8 @@ public class Apps : ComponentResource public Output FunctionOutboundIps { get; } = default!; public Output AppOutboundIps { get; } = default!; - public Apps(string name, FunctionArgs args, ComponentResourceOptions? options = null) - : base("coreexinfra:web:apps", name, options) + public Apps(string name, FunctionArgs args, AzureApiService azureApiService, ComponentResourceOptions? options = null) + : base("Company:AppName:web:apps", name, options) { this.args = args; @@ -221,38 +222,13 @@ public Apps(string name, FunctionArgs args, ComponentResourceOptions? options = FunctionOutboundIps = functionApp.OutboundIpAddresses; AppOutboundIps = app.OutboundIpAddresses; - // sleep 10s because Azure... List Host Keys method sometimes fails with HTTP 400, re-running stack fixes it. - if (!Deployment.Instance.IsDryRun) - { - Log.Info("Waiting 10s before calling function to get host keys"); - System.Threading.Thread.Sleep(10000); - } - - var keys = Output.CreateSecret(ListWebAppHostKeys.Invoke(new ListWebAppHostKeysInvokeArgs - { - Name = functionApp.Name, - ResourceGroupName = args.ResourceGroupName - }, new InvokeOptions { Parent = functionApp })); - - Output.Tuple(args.IsAppDeploymentEnabled.ToOutput(), functionApp.DefaultHostName, keys) - .Apply(t => - { - var (isAppDeploymentEnabled, defaultHostName, keys) = t; + var functionKey = Output.CreateSecret(azureApiService.GetHostKeys(args.ResourceGroupName, functionApp.Name)); - if (isAppDeploymentEnabled) - { - Log.Info("Syncing triggers for azure function"); - var syncUrl = $"https://{defaultHostName}/admin/host/synctriggers?code={keys.MasterKey}"; + // sync function app service triggers + azureApiService.SyncFunctionAppTriggers(args.ResourceGroupName, functionApp.Name); - using var httpClient = new HttpClient(); - return httpClient.PostAsync(syncUrl, null); - } - - return Task.FromResult(default!); - }); - - FunctionHealthUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/health?code={keys.Apply(k => k.MasterKey)}"); - FunctionSwaggerUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/swagger/ui?code={keys.Apply(k => k.MasterKey)}"); + FunctionHealthUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/health?code={functionKey}"); + FunctionSwaggerUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/swagger/ui?code={functionKey}"); AppSwaggerUrl = Output.Format($"https://{app.DefaultHostName}/swagger/index.html"); RegisterOutputs(); @@ -260,6 +236,13 @@ public Apps(string name, FunctionArgs args, ComponentResourceOptions? options = private static async Task PublishApp() { + if (Deployment.Instance.IsDryRun) + { + Directory.CreateDirectory("../Company.AppName.Api/bin/Release/net6.0/publish"); + Directory.CreateDirectory("../Company.AppName.Functions/bin/Release/net6.0/publish"); + return; + } + Log.Info("Setting up deployments from zip for the app and function and executing [dotnet publish]"); var sw = Stopwatch.StartNew(); diff --git a/src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs b/src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs index 9a5180a5..3853eb9e 100644 --- a/src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs +++ b/src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs @@ -1,20 +1,48 @@ using System; -using System.Diagnostics; -using System.Net.Http; -using System.Threading.Tasks; using Pulumi; -using Pulumi.AzureNative.Storage; -using Pulumi.AzureNative.Web; -using Pulumi.AzureNative.Web.Inputs; -using AzureNative = Pulumi.AzureNative; +using AD = Pulumi.AzureAD; namespace Company.AppName.Infra.Components; public class DevSetup : ComponentResource { - public DevSetup(string name, Input developerEmails, ComponentResourceOptions? options = null) - : base("coreexinfra:developer:setup", name, options) + public string DevelopersGroupName { get; } = $"Developers-Company-AppName-{Deployment.Instance.StackName}"; + + public Output DevelopersGroupId { get; private set; } = default!; + + public DevSetup(string name, string emailsCommaSeparated, ComponentResourceOptions? options = null) + : base("Company:AppName:developer:setup", name, options) { - // + // get current user + var current = Output.Create(AD.GetClientConfig.InvokeAsync()); + + var developersAuthorizedGroup = new AD.Group(DevelopersGroupName, new AD.GroupArgs + { + DisplayName = DevelopersGroupName, + SecurityEnabled = true, + Owners = new InputList { current.Apply(current => current.ObjectId) }, + Members = new InputList { current.Apply(current => current.ObjectId) } + }, new CustomResourceOptions { Parent = this }); + + DevelopersGroupId = developersAuthorizedGroup.Id; + + Log.Info("Provisioning access for developers: " + emailsCommaSeparated); + var emails = emailsCommaSeparated.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (var email in emails) + { + var user = AD.GetUser.Invoke(new() + { + UserPrincipalName = email, + }, new InvokeOptions { Parent = this }); + + var groupMember = new AD.GroupMember($"developerGroupMember{Deployment.Instance.StackName}-{email}", new() + { + GroupObjectId = DevelopersGroupId, + MemberObjectId = user.Apply(usr => usr.Id), + }, new CustomResourceOptions { Parent = this }); + } + + RegisterOutputs(); } } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs b/src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs index 4e332804..d9dfb828 100644 --- a/src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs +++ b/src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs @@ -9,7 +9,7 @@ public class Diagnostics : ComponentResource public Output InstrumentationKey { get; } = default!; public Diagnostics(string name, DiagnosticsArgs args, ComponentResourceOptions? options = null) - : base("coreexinfra:web:diagnostics", name, options) + : base("Company:AppName:web:diagnostics", name, options) { // Log Analytics Workspace var workspace = new Workspace("workspace", new() diff --git a/src/Templates/content/Company.AppName.Infra/Components/Messaging.cs b/src/Templates/content/Company.AppName.Infra/Components/Messaging.cs index 45c57d2a..149c3098 100644 --- a/src/Templates/content/Company.AppName.Infra/Components/Messaging.cs +++ b/src/Templates/content/Company.AppName.Infra/Components/Messaging.cs @@ -15,7 +15,7 @@ public class Messaging : ComponentResource public Output NamespaceId { get; } = default!; public Messaging(string name, MessagingArgs args, ComponentResourceOptions? options = null) - : base("coreexinfra:web:messaging", name, options) + : base("Company:AppName:web:messaging", name, options) { this.args = args; this.name = name; @@ -50,7 +50,7 @@ public Queue AddQueue(string queueName, bool batchOperationsEnabled = false) }, new CustomResourceOptions { Parent = this }); } - public IEnumerable AddAccess(Input principalId, string name) + public IEnumerable AddAccess(Input principalId, string name, string principalType = "ServicePrincipal") { var receive_permission = new RoleAssignment( $"receive-for-{name}", @@ -58,7 +58,7 @@ public IEnumerable AddAccess(Input principalId, string n { Description = $"{name} receiving data from service bus", PrincipalId = principalId, - PrincipalType = "ServicePrincipal", + PrincipalType = principalType, RoleDefinitionId = Roles.BuiltInRolesIds.ServiceBusDataReceiver, Scope = NamespaceId }, @@ -71,7 +71,7 @@ public IEnumerable AddAccess(Input principalId, string n { Description = $"{name} sending data to service bus", PrincipalId = principalId, - PrincipalType = "ServicePrincipal", + PrincipalType = principalType, RoleDefinitionId = Roles.BuiltInRolesIds.ServiceBusDataSender, Scope = NamespaceId diff --git a/src/Templates/content/Company.AppName.Infra/Components/Sql.cs b/src/Templates/content/Company.AppName.Infra/Components/Sql.cs index 293be7cd..9ec33c3f 100644 --- a/src/Templates/content/Company.AppName.Infra/Components/Sql.cs +++ b/src/Templates/content/Company.AppName.Infra/Components/Sql.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Net.Http; +using System.Collections.Concurrent; using Company.AppName.Infra.Services; using Pulumi; using Pulumi.AzureNative.Sql; @@ -12,14 +11,14 @@ namespace Company.AppName.Infra.Components; public class Sql : ComponentResource { private readonly SqlArgs args; - private readonly HashSet firewallAllowedIps = new(); + private readonly ConcurrentDictionary firewallAllowedIps = new(); public Output SqlDatabaseConnectionString { get; } public Output SqlServerName { get; } public Output SqlDatabaseAuthorizedGroupId { get; } - public Sql(string name, SqlArgs args, IDbOperations dbOperations, ComponentResourceOptions? options = null) - : base("coreexinfra:web:sql", name, options) + public Sql(string name, SqlArgs args, IDbOperations dbOperations, AzureApiClient apiClient, ComponentResourceOptions? options = null) + : base("Company:AppName:web:sql", name, options) { this.args = args; var sqlAdAdmin = new AD.User("sqlAdmin", new AD.UserArgs @@ -44,7 +43,7 @@ public Sql(string name, SqlArgs args, IDbOperations dbOperations, ComponentResou Tags = args.Tags }, new CustomResourceOptions { Parent = this }); - var publicIp = Output.Create(new HttpClient().GetStringAsync("https://api.ipify.org")); + var publicIp = Output.Create(apiClient.GetMyIP()); var enableLocalMachine = new FirewallRule("AllowLocalMachine", new FirewallRuleArgs { @@ -103,10 +102,8 @@ public void AddFirewallRule(Output ips, string name) { foreach (var address in ips.Split(",")) { - if (!firewallAllowedIps.Contains(address)) + if (firewallAllowedIps.TryAdd(address, default)) { - firewallAllowedIps.Add(address); - var enableIp = new FirewallRule("Enable_" + name + "_" + address, new FirewallRuleArgs { ResourceGroupName = args.ResourceGroupName, diff --git a/src/Templates/content/Company.AppName.Infra/Components/Storage.cs b/src/Templates/content/Company.AppName.Infra/Components/Storage.cs index 97458a6e..822ba202 100644 --- a/src/Templates/content/Company.AppName.Infra/Components/Storage.cs +++ b/src/Templates/content/Company.AppName.Infra/Components/Storage.cs @@ -17,7 +17,7 @@ public class Storage : ComponentResource public Output ConnectionString { get; private set; } = default!; public Storage(string name, StorageArgs args, ComponentResourceOptions? options = null) - : base("coreexinfra:web:storage", name, options) + : base("Company:AppName:web:storage", name, options) { // Create an Azure resource (Storage Account) var storageAccount = new StorageAccount(name, new StorageAccountArgs @@ -69,7 +69,7 @@ private static Output GetConnectionString(Input resourceGroupNam }); } - public RoleAssignment AddAccess(Input principalId, string name) + public RoleAssignment AddAccess(Input principalId, string name, string principalType = "ServicePrincipal") { return new RoleAssignment( $"useblob-for-{name}", @@ -77,7 +77,7 @@ public RoleAssignment AddAccess(Input principalId, string name) { Description = $"{name} accessing storage account", PrincipalId = principalId, - PrincipalType = "ServicePrincipal", + PrincipalType = principalType, RoleDefinitionId = Roles.BuiltInRolesIds.StorageBlobDataOwner, Scope = id }, diff --git a/src/Templates/content/Company.AppName.Infra/CoreExStack.cs b/src/Templates/content/Company.AppName.Infra/CoreExStack.cs index 7d0a8394..66290b3f 100644 --- a/src/Templates/content/Company.AppName.Infra/CoreExStack.cs +++ b/src/Templates/content/Company.AppName.Infra/CoreExStack.cs @@ -1,20 +1,26 @@ using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; using Company.AppName.Infra.Services; using Pulumi; using Pulumi.AzureNative.Resources; +using AD = Pulumi.AzureAD; namespace Company.AppName.Infra; public static class CoreExStack { - public static async Task> ExecuteStackAsync(IDbOperations dbOperations) + public static async Task> ExecuteStackAsync(IDbOperations dbOperations, HttpClient client) { var config = await StackConfiguration.CreateConfiguration(); Log.Info("Configuration completed"); var tags = new InputMap { { "App", "CoreEx" } }; + // Create Azure API client for direct HTTP calls + var azureApiClient = new AzureApiClient(client); + var azureApiService = new AzureApiService(azureApiClient); + // Create an Azure Resource Group var resourceGroup = new ResourceGroup($"coreEx-{Pulumi.Deployment.Instance.StackName}", new ResourceGroupArgs { @@ -49,7 +55,7 @@ public static class CoreExStack SqlAdAdminPassword = config.SqlAdAdminPassword!, IsDBSchemaDeploymentEnabled = config.IsDBSchemaDeploymentEnabled, Tags = tags - }, dbOperations); + }, dbOperations, azureApiClient); var apps = new Components.Apps("apps", new Components.Apps.FunctionArgs { @@ -64,9 +70,10 @@ public static class CoreExStack MassPublishQueue = config.MassPublishQueue, IsAppDeploymentEnabled = config.IsAppsDeploymentEnabled, Tags = tags - }); - + }, azureApiService); + // Developer group + var devSetup = new Components.DevSetup("devs", config.DeveloperEmails); // Permissions for function app storage.AddAccess(apps.FunctionPrincipalId, "functionApp"); @@ -76,10 +83,17 @@ public static class CoreExStack storage.AddAccess(apps.AppPrincipalId, "appService"); serviceBus.AddAccess(apps.AppPrincipalId, "appService"); + // Permissions for dev group + storage.AddAccess(devSetup.DevelopersGroupId, "devGroup", principalType: "Group"); + serviceBus.AddAccess(devSetup.DevelopersGroupId, "devGroup", principalType: "Group"); + // allow app and function to query/use DB sql.AddToSqlDatabaseAuthorizedGroup("functionGroupMember", apps.FunctionPrincipalId); sql.AddToSqlDatabaseAuthorizedGroup("appGroupMember", apps.AppPrincipalId); + // allow dev team to query/use DB + sql.AddToSqlDatabaseAuthorizedGroup("devGroupMember", devSetup.DevelopersGroupId); + // allow app and function through SQL firewall sql.AddFirewallRule(apps.FunctionOutboundIps, "appService"); sql.AddFirewallRule(apps.AppOutboundIps, "appService"); diff --git a/src/Templates/content/Company.AppName.Infra/Program.cs b/src/Templates/content/Company.AppName.Infra/Program.cs index 111c6283..474e893b 100644 --- a/src/Templates/content/Company.AppName.Infra/Program.cs +++ b/src/Templates/content/Company.AppName.Infra/Program.cs @@ -4,8 +4,10 @@ return await Deployment.RunAsync(() => { + // running with using statement (to Dispose) doesn't work with Pulumi + var client = new System.Net.Http.HttpClient(); // create and use actual instance of DB Operations service - return Company.AppName.Infra.CoreExStack.ExecuteStackAsync(new DbOperations()); + return Company.AppName.Infra.CoreExStack.ExecuteStackAsync(new DbOperations(), client); }, new StackOptions { // apply auto-tagging transformation @@ -17,10 +19,10 @@ if(tagsProp?.GetValue(args.Args) is InputMap tags) { Log.Debug("Adding tags to " + args.Resource.GetResourceName()); - + tags.Add("user:Stack", Deployment.Instance.StackName); tags.Add("user:Project", Deployment.Instance.ProjectName); - tags.Add("App:Name", "CoreEx"); + tags.Add("App:Name", "Company:AppName"); } return new ResourceTransformationResult(args.Args, args.Options); diff --git a/src/Templates/content/Company.AppName.Infra/Readme.md b/src/Templates/content/Company.AppName.Infra/Readme.md index dc69f9cd..9ca64930 100644 --- a/src/Templates/content/Company.AppName.Infra/Readme.md +++ b/src/Templates/content/Company.AppName.Infra/Readme.md @@ -7,7 +7,9 @@ The easiest way to deploy it is by using Pulumi account (Free), but it's not man Prerequisites: 1. [Pulumi CLI](https://www.pulumi.com/docs/get-started/install/) -2. Azure CLI - logged in to Azure +2. Azure CLI - logged in to Azure subscription with permissions to create service principals + +> Note: Some corporate AAD restrict what can be done in AAD. Since this sample creates AAD User and Group, infrastructure needs to be created in AAD tenant that allows it. ## Pulumi with azure storage diff --git a/src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs b/src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs index 02a19045..f77a8ce6 100644 --- a/src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs +++ b/src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs @@ -1,11 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.Json; -using System.Text.Json.Serialization; -using Pulumi.AzureNative.Authorization; - namespace Company.AppName.Infra.Roles; public static class BuiltInRolesIds @@ -17,48 +9,4 @@ public static class BuiltInRolesIds // https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-sender public const string ServiceBusDataSender = "/providers/Microsoft.Authorization/roleDefinitions/69a216fc-b8fb-44d8-bc22-1f3c2cd27a39"; - - /// - /// Gets role id based on the provided role name. This method can be used instead of hardcoded role Ids above - /// - /// - /// - /// - /// Thrown when request fails - /// code from: https://github.com/pulumi/examples/blob/28b559d68eb6a67f3e6b5fb3d2a337b5b9ed35d5/azure-cs-call-azure-api/Program.cs#L45 - public static async System.Threading.Tasks.Task GetRoleIdByName(string roleName, string? scope = null) - { - var config = await GetClientConfig.InvokeAsync(); - var token = await GetClientToken.InvokeAsync(); - - // Unfortunately, Microsoft hasn't shipped an .NET5-compatible SDK at the time of writing this. - // So, we have to hand-craft an HTTP request to retrieve a role definition. - using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); - var response = await httpClient.GetAsync($"https://management.azure.com/subscriptions/{config.SubscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-01-01-preview&$filter=roleName%20eq%20'{roleName}'"); - if (!response.IsSuccessStatusCode) - { - throw new Exception($"Request failed with {response.StatusCode}"); - } - var body = await response.Content.ReadAsStringAsync(); - var definition = JsonSerializer.Deserialize(body)!; - return definition.Value[0].id; - } - - public class RoleDefinition - { - [JsonPropertyName("value")] - public List Value { get; set; } = default!; - } - public class RoleDefinitionValue - { - [JsonPropertyName("id")] - public string id { get; set; } = default!; - - [JsonPropertyName("type")] - public string Type { get; set; } = default!; - - [JsonPropertyName("name")] - public string Name { get; set; } = default!; - } } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Services/AzureApiClient.cs b/src/Templates/content/Company.AppName.Infra/Services/AzureApiClient.cs new file mode 100644 index 00000000..e1587299 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Services/AzureApiClient.cs @@ -0,0 +1,33 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using CoreEx.Configuration; +using CoreEx.Http; +using Microsoft.Extensions.Configuration; + +namespace Company.AppName.Infra.Services; + +/// +/// Http client for Azure APIs +/// +public class AzureApiClient : TypedHttpClientCore +{ + public AzureApiClient(HttpClient client) + : base(client, CoreEx.Json.JsonSerializer.Default, new CoreEx.ExecutionContext(), new DefaultSettings(new ConfigurationBuilder().Build()), PulumiLogger>.Instance) + { + } + + protected override async Task OnBeforeRequest(HttpRequestMessage request, CancellationToken cancellationToken) + { + var token = await Pulumi.AzureNative.Authorization.GetClientToken.InvokeAsync(); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token); + + await base.OnBeforeRequest(request, cancellationToken); + } + + public async Task GetMyIP() + { + // use base client directly to skip OnBeforeRequest + return await base.Client.GetStringAsync("https://api.ipify.org"); + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Services/AzureApiService.cs b/src/Templates/content/Company.AppName.Infra/Services/AzureApiService.cs new file mode 100644 index 00000000..a6dbd06b --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Services/AzureApiService.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Polly.Extensions.Http; +using Pulumi; +using Pulumi.AzureNative.Authorization; + +namespace Company.AppName.Infra.Services; + +public class AzureApiService +{ + + public AzureApiClient ApiClient { get; private set; } + + public AzureApiService(AzureApiClient apiClient) + { + ApiClient = apiClient; + } + + /// + /// Gets role id based on the provided role name. This method can be used instead of hardcoded role Ids above + /// + /// + /// + /// + /// Thrown when request fails + /// code from: https://github.com/pulumi/examples/blob/28b559d68eb6a67f3e6b5fb3d2a337b5b9ed35d5/azure-cs-call-azure-api/Program.cs#L45 + public async Task GetRoleIdByName(string roleName, string? scope = null) + { + var config = await GetClientConfig.InvokeAsync(); + + var result = await ApiClient + .ThrowTransientException() + .WithRetry() + .GetAsync($"https://management.azure.com/subscriptions/{config.SubscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-01-01-preview&$filter=roleName%20eq%20'{roleName}'"); + + return result.Value.Value[0].Id; + } + + public Output GetHostKeys(Output rgName, Output functionName) + { + return Output.Tuple(rgName, functionName).Apply(async t => + { + var (resourceGroupName, siteName) = t; + Log.Info("Getting host keys for: " + siteName); + + var config = await GetClientConfig.InvokeAsync(); + + var result = await ApiClient + .WithRetry(count: 4, seconds: 5) + // Azure returns 400 BadRequest when Function App is not ready + .WithCustomRetryPolicy(HttpPolicyExtensions.HandleTransientHttpError().OrResult(http => http.StatusCode == System.Net.HttpStatusCode.BadRequest || http.StatusCode == System.Net.HttpStatusCode.NotFound)) + .PostAsync($"https://management.azure.com/subscriptions/{config.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{siteName}/host/default/listkeys?api-version=2022-03-01"); + + return result.Value.FunctionKeys.Key; + }); + } + + // https://docs.microsoft.com/en-us/azure/azure-functions/functions-deployment-technologies#trigger-syncing + public Output SyncFunctionAppTriggers(Output rgName, Output functionName) + { + return Output.Tuple(rgName, functionName).Apply(async t => + { + var (resourceGroupName, siteName) = t; + Log.Info("Syncing Function App triggers"); + + var config = await GetClientConfig.InvokeAsync(); + + var url = $"https://management.azure.com/subscriptions/{config.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{siteName}/syncfunctiontriggers?api-version=2016-08-01"; + + await ApiClient + .WithRetry() + .ThrowTransientException() + .PostAsync(url); + + return true; + }); + } + + public class HostKeys + { + [JsonPropertyName("masterKey")] + public string MasterKey { get; set; } = default!; + + [JsonPropertyName("functionKeys")] + public FunctionKeysValue FunctionKeys { get; set; } = default!; + } + + public class FunctionKeysValue + { + [JsonPropertyName("default")] + public string Key { get; set; } = default!; + } + + public class RoleDefinition + { + [JsonPropertyName("value")] + public List Value { get; set; } = default!; + } + public class RoleDefinitionValue + { + [JsonPropertyName("id")] + public string Id { get; set; } = default!; + + [JsonPropertyName("type")] + public string Type { get; set; } = default!; + + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/Services/PulumiLogger.cs b/src/Templates/content/Company.AppName.Infra/Services/PulumiLogger.cs new file mode 100644 index 00000000..c00957a4 --- /dev/null +++ b/src/Templates/content/Company.AppName.Infra/Services/PulumiLogger.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Company.AppName.Infra.Services; + +public class PulumiLogger : ILogger +{ + public static readonly PulumiLogger Instance = new(); + + private PulumiLogger() + { } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + throw new NotImplementedException(); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + Pulumi.Log.Debug(formatter(state, exception)); + break; + case LogLevel.Information: + Pulumi.Log.Info(formatter(state, exception)); + break; + case LogLevel.Warning: + Pulumi.Log.Warn(formatter(state, exception)); + break; + case LogLevel.Error: + case LogLevel.Critical: + Pulumi.Log.Error(formatter(state, exception)); + break; + } + } +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs b/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs index 6bef7960..37ee9407 100644 --- a/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs +++ b/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs @@ -43,6 +43,7 @@ public static async Task CreateConfiguration() }).Result; Log.Info($"Default username is: {defaultUsername}"); + Log.Info($"developerEmails: {config.Get("developerEmails")}"); return new StackConfiguration { @@ -55,7 +56,7 @@ public static async Task CreateConfiguration() VerificationResultsQueue = config.Get("verificationResultsQueue") ?? "verificationResults", MassPublishQueue = config.Get("massPublishQueue") ?? "massPublish", - DeveloperEmails = config.Get("developerEmails") - }; + DeveloperEmails = config.Get("developerEmails") ?? string.Empty + }; } } \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs b/src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs index 7e4d7f9c..a2721a35 100644 --- a/src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs +++ b/src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs @@ -1,10 +1,11 @@ using CoreEx.Events; +using System; +using System.Net.Http; using Microsoft.Azure.WebJobs.ServiceBus; using Moq; using Company.AppName.Business.External.Contracts; using Company.AppName.Functions; using NUnit.Framework; -using System; using UnitTestEx.NUnit; namespace Company.AppName.UnitTest @@ -17,10 +18,51 @@ public void A110_Verify_Success() { var test = FunctionTester.Create(); var imp = new InMemoryPublisher(test.Logger); - var sbm = test.CreateServiceBusMessage(new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" }); + var evr = new EmployeeVerificationRequest { Name = "Wendy", Age = 37, Gender = "F" }; + var sbm = test.CreateServiceBusMessage(evr); var sba = new Mock(); + var mcf = MockHttpClientFactory.Create(); + var agify = mcf.CreateClient("Agify"); + var nationalize = mcf.CreateClient("Nationalize"); + var genderize = mcf.CreateClient("Genderize"); + + agify.Request(HttpMethod.Get, $"https://api.agify.mock.io/?name={evr.Name}") + .Respond.WithJson(new + { + age = 64, + count = 82293, + name = evr.Name + }); + nationalize.Request(HttpMethod.Get, $"https://api.nationalize.mock.io/?name={evr.Name}") + .Respond.WithJson(new + { + country = new[]{ + new { + country_Id= "SV", + probability= 0.07477553 + }, + new { + country_Id= "GT", + probability= 0.07223318 + }, + new { + country_Id= "NL", + probability= 0.067494206 + }}, + name = evr.Name + }); + genderize.Request(HttpMethod.Get, $"https://api.genderize.mock.io/?name={evr.Name}") + .Respond.WithJson(new + { + count = 176697, + gender = "female", + name = evr.Name, + probability = 0.97 + }); + test.ReplaceScoped(_ => imp) + .ReplaceHttpClientFactory(mcf) .ServiceBusTrigger() .Run(f => f.RunAsync(sbm, sba.Object)) .AssertSuccess(); diff --git a/src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json b/src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json index c7e9a5c1..cf829b3f 100644 --- a/src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json +++ b/src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json @@ -3,9 +3,9 @@ "VerificationResultsQueueName": "verificationResults", "VerificationQueueName": "pendingVerifications", "ServiceBusConnection__fullyQualifiedNamespace": "topsecret", - "AgifyApiEndpointUri": "https://api.agify.io", - "NationalizeApiClientApiEndpointUri": "https://api.nationalize.io", - "GenderizeApiClientApiEndpointUri": "https://api.genderize.io", + "AgifyApiEndpointUri": "https://api.agify.mock.io", + "NationalizeApiClientApiEndpointUri": "https://api.nationalize.mock.io", + "GenderizeApiClientApiEndpointUri": "https://api.genderize.mock.io", "logging": { "logLevel": { "default": "debug" diff --git a/src/Templates/content/Company.AppName.sln b/src/Templates/content/Company.AppName.sln index d6654aba..141d4812 100644 --- a/src/Templates/content/Company.AppName.sln +++ b/src/Templates/content/Company.AppName.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.Infra", "Co EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.Infra.Tests", "Company.AppName.Infra.Tests\Company.AppName.Infra.Tests.csproj", "{01B0FC8E-738D-47BB-AA57-F880532A501D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Company.AppName.UnitTest", "Company.AppName.UnitTest\Company.AppName.UnitTest.csproj", "{E745B80D-DD85-4A83-8C9C-A6F8564EBDCF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,5 +50,9 @@ Global {01B0FC8E-738D-47BB-AA57-F880532A501D}.Debug|Any CPU.Build.0 = Debug|Any CPU {01B0FC8E-738D-47BB-AA57-F880532A501D}.Release|Any CPU.ActiveCfg = Release|Any CPU {01B0FC8E-738D-47BB-AA57-F880532A501D}.Release|Any CPU.Build.0 = Release|Any CPU + {E745B80D-DD85-4A83-8C9C-A6F8564EBDCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E745B80D-DD85-4A83-8C9C-A6F8564EBDCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E745B80D-DD85-4A83-8C9C-A6F8564EBDCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E745B80D-DD85-4A83-8C9C-A6F8564EBDCF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Templates/readme.md b/src/Templates/readme.md index 0dd29b3e..25793d74 100644 --- a/src/Templates/readme.md +++ b/src/Templates/readme.md @@ -27,8 +27,9 @@ Extensions required: --> DONE Expose ports for function, app service and sql server +--> DONE -## Update readme to use REST Client +## Update readme to use REST Client -> IN PROGRESS Create: [POST] http://localhost:7071/api/api/employees Delete: [DELETE] http://localhost:7071/api/api/employees/{id} @@ -59,4 +60,9 @@ DONE ## Readme on configuring ADO -https://github.com/Azure-Samples/todo-csharp-sql/tree/main/.azdo/pipelines \ No newline at end of file +https://github.com/Azure-Samples/todo-csharp-sql/tree/main/.azdo/pipelines + +## Readme on running the DB container locally + +## Cosmos Tests +todo: update cosmos tests with env variable \ No newline at end of file From 0537c1d4f6592aec0c63565efe734f60b73189cc Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 12:17:34 -0400 Subject: [PATCH 17/39] remove infra from samples Signed-off-by: Piotr --- .github/workflows/CI.yml | 3 + Docker.md | 8 + .../Functions/EmployeeFunction.cs | 14 +- .../My.Hr.Infra.Tests/CoreExStackTests.cs | 81 ----- .../My.Hr.Infra.Tests.csproj | 26 -- .../Services/AzureApiClientTests.cs | 29 -- .../Services/AzureApiServiceTests.cs | 117 ------- samples/My.Hr/My.Hr.Infra.Tests/Testing.cs | 135 -------- .../My.Hr.Infra.Tests/TestingExtensions.cs | 21 -- samples/My.Hr/My.Hr.Infra.Tests/Usings.cs | 5 - samples/My.Hr/My.Hr.Infra/Components/Apps.cs | 288 ------------------ .../My.Hr.Infra/Components/Diagnostics.cs | 46 --- .../My.Hr/My.Hr.Infra/Components/Messaging.cs | 90 ------ samples/My.Hr/My.Hr.Infra/Components/Sql.cs | 140 --------- .../My.Hr/My.Hr.Infra/Components/Storage.cs | 93 ------ samples/My.Hr/My.Hr.Infra/CoreExStack.cs | 148 --------- samples/My.Hr/My.Hr.Infra/Extensions.cs | 53 ---- samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj | 28 -- samples/My.Hr/My.Hr.Infra/Program.cs | 30 -- samples/My.Hr/My.Hr.Infra/Pulumi.yaml | 14 - samples/My.Hr/My.Hr.Infra/Readme.md | 59 ---- .../My.Hr.Infra/Roles/BuiltInRolesIds.cs | 12 - .../My.Hr.Infra/Services/AzureApiClient.cs | 33 -- .../My.Hr.Infra/Services/AzureApiService.cs | 112 ------- .../My.Hr.Infra/Services/DbOperations.cs | 48 --- .../My.Hr.Infra/Services/IDbOperations.cs | 10 - .../My.Hr.Infra/Services/PulumiLogger.cs | 43 --- samples/My.Hr/My.Hr.sln | 12 - .../content/Company.AppName.Api/Api.http | 53 +++- .../Company.AppName.Functions/Functions.http | 51 +++- .../Functions/EmployeeFunction.cs | 14 +- .../content/Company.AppName.Infra/Readme.md | 26 +- .../StackConfiguration.cs | 3 +- .../content/docker-compose.DB.only.yml | 22 ++ 34 files changed, 154 insertions(+), 1713 deletions(-) delete mode 100644 samples/My.Hr/My.Hr.Infra.Tests/CoreExStackTests.cs delete mode 100644 samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj delete mode 100644 samples/My.Hr/My.Hr.Infra.Tests/Services/AzureApiClientTests.cs delete mode 100644 samples/My.Hr/My.Hr.Infra.Tests/Services/AzureApiServiceTests.cs delete mode 100644 samples/My.Hr/My.Hr.Infra.Tests/Testing.cs delete mode 100644 samples/My.Hr/My.Hr.Infra.Tests/TestingExtensions.cs delete mode 100644 samples/My.Hr/My.Hr.Infra.Tests/Usings.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Components/Apps.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Components/Diagnostics.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Components/Messaging.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Components/Sql.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Components/Storage.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/CoreExStack.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Extensions.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj delete mode 100644 samples/My.Hr/My.Hr.Infra/Program.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Pulumi.yaml delete mode 100644 samples/My.Hr/My.Hr.Infra/Readme.md delete mode 100644 samples/My.Hr/My.Hr.Infra/Roles/BuiltInRolesIds.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Services/AzureApiClient.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Services/AzureApiService.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Services/DbOperations.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Services/IDbOperations.cs delete mode 100644 samples/My.Hr/My.Hr.Infra/Services/PulumiLogger.cs create mode 100644 src/Templates/content/docker-compose.DB.only.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c3addd01..1f3f339a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -68,3 +68,6 @@ jobs: - name: Build Template run: dotnet build src/Templates/content + + - name: Test Template + run: dotnet test src/Templates/content --filter Category=!WithCosmos --no-build --verbosity normal /p:CollectCoverage=true /p:Exclude="[CoreEx.TestFunction]*" /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov3.info diff --git a/Docker.md b/Docker.md index 0909e1a1..99de8164 100644 --- a/Docker.md +++ b/Docker.md @@ -23,6 +23,14 @@ services: Service Bus should have `pendingverifications` queue used by *My.Hr* sample. +## Running SQL database only + +It's possible to run only DB container for local development with + +```bash +docker-compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.DB.only.yml up +``` + ## To build ```bash diff --git a/samples/My.Hr/My.Hr.Functions/Functions/EmployeeFunction.cs b/samples/My.Hr/My.Hr.Functions/Functions/EmployeeFunction.cs index b06eb2c5..0ae7d36a 100644 --- a/samples/My.Hr/My.Hr.Functions/Functions/EmployeeFunction.cs +++ b/samples/My.Hr/My.Hr.Functions/Functions/EmployeeFunction.cs @@ -36,23 +36,23 @@ public EmployeeFunction(WebApi webApi, EmployeeService service, IValidator GetAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/employees/{id}")] HttpRequest request, Guid id) + public Task GetAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "employees/{id}")] HttpRequest request, Guid id) => _webApi.GetAsync(request, _ => _service.GetEmployeeAsync(id)); [FunctionName("GetAll")] [OpenApiOperation(operationId: "GetAll", tags: new[] { "employee" })] [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: MediaTypeNames.Application.Json, bodyType: typeof(List), Description = "Employee records")] - public Task GetAllAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/employees")] HttpRequest request) + public Task GetAllAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "employees")] HttpRequest request) => _webApi.GetAsync(request, p => _service.GetAllAsync(p.RequestOptions.Paging)); [FunctionName("Create")] [OpenApiOperation(operationId: "Create", tags: new[] { "employee" })] [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.Created, Description = "Created employee record")] - public Task CreateAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "api/employees")] HttpRequest request) + public Task CreateAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employees")] HttpRequest request) => _webApi.PostAsync(request, p => _service.AddEmployeeAsync(p.Value!), - statusCode: HttpStatusCode.Created, validator: _validator, locationUri: e => new Uri($"api/employees/{e.Id}", UriKind.RelativeOrAbsolute)); + statusCode: HttpStatusCode.Created, validator: _validator, locationUri: e => new Uri($"employees/{e.Id}", UriKind.RelativeOrAbsolute)); [FunctionName("Update")] [OpenApiOperation(operationId: "Update", tags: new[] { "employee" })] @@ -60,14 +60,14 @@ public Task CreateAsync([HttpTrigger(AuthorizationLevel.Function, [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: MediaTypeNames.Application.Json, bodyType: typeof(Employee), Description = "Employee record")] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NotFound, Description = "Not found")] - public Task UpdateAsync([HttpTrigger(AuthorizationLevel.Function, "put", Route = "api/employees/{id}")] HttpRequest request, Guid id) + public Task UpdateAsync([HttpTrigger(AuthorizationLevel.Function, "put", Route = "employees/{id}")] HttpRequest request, Guid id) => _webApi.PutAsync(request, p => _service.UpdateEmployeeAsync(p.Value!, id), validator: _validator); [FunctionName("Patch")] - public Task PatchAsync([HttpTrigger(AuthorizationLevel.Function, "patch", Route = "api/employees/{id}")] HttpRequest request, Guid id) + public Task PatchAsync([HttpTrigger(AuthorizationLevel.Function, "patch", Route = "employees/{id}")] HttpRequest request, Guid id) => _webApi.PatchAsync(request, get: _ => _service.GetEmployeeAsync(id), put: p => _service.UpdateEmployeeAsync(p.Value!, id), validator: _validator); [FunctionName("Delete")] - public Task DeleteAsync([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "api/employees/{id}")] HttpRequest request, Guid id) + public Task DeleteAsync([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "employees/{id}")] HttpRequest request, Guid id) => _webApi.DeleteAsync(request, _ => _service.DeleteEmployeeAsync(id)); } diff --git a/samples/My.Hr/My.Hr.Infra.Tests/CoreExStackTests.cs b/samples/My.Hr/My.Hr.Infra.Tests/CoreExStackTests.cs deleted file mode 100644 index c6a4ac2e..00000000 --- a/samples/My.Hr/My.Hr.Infra.Tests/CoreExStackTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -// https://www.pulumi.com/blog/unit-testing-cloud-deployments-with-dotnet/ - -using Pulumi.AzureNative.Resources; - -namespace My.Hr.Infra.Tests; - -public class CoreExStackTests -{ - [Test] - public async Task ResourceGroupHasNameTag() - { - var (resources, _, _) = await Testing.RunAsync(); - - var rgs = resources.OfType(); - var rg = rgs.First(); - var tags = await rg.Tags.GetValueAsync(); - - // Assert - rgs.Should().HaveCount(1); - rg.Should().NotBeNull(); - tags.Should().ContainKey("App"); - } - - [Test] - public async Task AllResourcesHaveNameTag() - { - // unfortunately this doesn't test tags created by auto-tagging dove via ResourceTransformation - var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); - - var rs = resources.Select(async r => - { - var tagsProp = r.GetType().GetProperty("Tags"); - - return tagsProp != null - ? (resource: r, tags: await tagsProp!.GetValue(r)!.GetValueAsync?>()) - : (resource: r, tags: null); - }); - - var result = (await Task.WhenAll(rs)).Where(anyResource => anyResource.tags != null); - - // Assert - result.Should().HaveCountGreaterThan(5); - result.Should().AllSatisfy(r => r.tags!.ContainsKey("App"), because: "All resources should be tagged"); - } - - [Test] - public async Task FunctionIsCreatedWithAUrl() - { - var (_, outputs, _) = await Testing.RunAsync(); - - var healthUrl = await outputs["FunctionHealthUrl"]!.GetValueAsync(); - var appSwaggerUrl = await outputs["AppSwaggerUrl"]!.GetValueAsync(); - - // Assert - healthUrl.Should().Be("https://unittest.azurewebsites.net/api/health?code=mocked-key", because: "mock values set in Testing class"); - appSwaggerUrl.Should().Be("https://unittest.azurewebsites.net/swagger/index.html", because: "mock values set in Testing class"); - } - - [Test] - public async Task SqlIsCreatedWithConnectionString() - { - var (_, outputs, _) = await Testing.RunAsync(); - - var connectionString = await outputs["SqlDatabaseConnectionString"]!.GetValueAsync(); - - // Assert - connectionString.Should().Be("Server=sql-server-unit-test-stack.database.windows.net; Authentication=Active Directory Default; Database=sqldb", because: "mock values set in Testing class"); - } - - [Test] - public async Task DbOperationsShouldExecute() - { - var (resources, outputs, dbOperationsMock) = await Testing.RunAsync(); - - // Assert - // because DB schema deployment is enabled in Testing class - dbOperationsMock.Verify(op => op.DeployDbSchemaAsync(It.IsAny())); - dbOperationsMock.Verify(op => op.ProvisionUsers(It.IsAny>(), It.IsAny())); - } -} - diff --git a/samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj b/samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj deleted file mode 100644 index 04c6ce46..00000000 --- a/samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net6.0 - enable - enable - - false - - - - - - - - - - - - - - - - - - diff --git a/samples/My.Hr/My.Hr.Infra.Tests/Services/AzureApiClientTests.cs b/samples/My.Hr/My.Hr.Infra.Tests/Services/AzureApiClientTests.cs deleted file mode 100644 index 296b9d60..00000000 --- a/samples/My.Hr/My.Hr.Infra.Tests/Services/AzureApiClientTests.cs +++ /dev/null @@ -1,29 +0,0 @@ - -using System.Net; -using My.Hr.Infra.Services; -using UnitTestEx.NUnit; - -namespace My.Hr.Infra.Tests.Services; - -public class AzureApiClientTests -{ - [Test] - public async Task GetMyIP_Should_ReturnIP() - { - // Arrange - const string mockedIp = "192.168.1.1"; - var mcf = MockHttpClientFactory.Create(); - var mc = mcf.CreateClient("ip"); - - mc.Request(HttpMethod.Get, "https://api.ipify.org") - .Respond.With(new StringContent(mockedIp)); - - var target = new AzureApiClient(mcf.GetHttpClient("ip")!); - - // Act - var result = await target.GetMyIP(); - - // Assert - result.Should().Be(mockedIp); - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra.Tests/Services/AzureApiServiceTests.cs b/samples/My.Hr/My.Hr.Infra.Tests/Services/AzureApiServiceTests.cs deleted file mode 100644 index fe734bb7..00000000 --- a/samples/My.Hr/My.Hr.Infra.Tests/Services/AzureApiServiceTests.cs +++ /dev/null @@ -1,117 +0,0 @@ - -using System.Net; -using My.Hr.Infra.Services; -using UnitTestEx.NUnit; - -namespace My.Hr.Infra.Tests.Services; - -public class AzureApiServiceTests -{ - [Test] - public async Task GetHostKeys_Should_GetHostKeyFromAzure() - { - // Arrange - const string resourceGroupName = "resource-group-name"; - const string functionAppName = "function-app"; - const string mockedKey = "mocked-key"; - var resourceGroup = Output.Create(resourceGroupName); - var functionApp = Output.Create(functionAppName); - var mcf = MockHttpClientFactory.Create(); - var mc = mcf.CreateClient("azure"); - - mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{Testing.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{functionAppName}/host/default/listkeys?api-version=2022-03-01") - .Respond.WithJson(new AzureApiService.HostKeys { FunctionKeys = new AzureApiService.FunctionKeysValue { Key = mockedKey } }); - - var target = new AzureApiService(new AzureApiClient(mcf.GetHttpClient("azure")!)); - - // Act - var resultOutput = await Testing.RunAsync(() => target.GetHostKeys(resourceGroup, functionApp)); - var result = await resultOutput.GetValueAsync(); - - // Assert - result.Should().Be(mockedKey); - mcf.VerifyAll(); - } - - [Test] - [Category("long")] - [Description("Test retries for the azure call and because of that takes long time (2 min)")] - public async Task GetHostKeys_Should_GetHostKeyFromAzure_When_FirstCallFails() - { - // Arrange - const string resourceGroupName = "resource-group-name"; - const string functionAppName = "function-app"; - const string mockedKey = "mocked-key"; - var resourceGroup = Output.Create(resourceGroupName); - var functionApp = Output.Create(functionAppName); - var mcf = MockHttpClientFactory.Create(); - var mc = mcf.CreateClient("azure"); - - mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{Testing.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{functionAppName}/host/default/listkeys?api-version=2022-03-01") - .Respond.WithSequence(s => - { - s.Respond().With(HttpStatusCode.NotFound); - s.Respond().With(HttpStatusCode.BadRequest); - s.Respond().With(HttpStatusCode.BadRequest); - s.Respond().WithJson(new AzureApiService.HostKeys { FunctionKeys = new AzureApiService.FunctionKeysValue { Key = mockedKey } }); - }); - - var target = new AzureApiService(new AzureApiClient(mcf.GetHttpClient("azure")!)); - - // Act - var resultOutput = await Testing.RunAsync(() => target.GetHostKeys(resourceGroup, functionApp)); - var result = await resultOutput.GetValueAsync(); - - // Assert - result.Should().Be(mockedKey); - mcf.VerifyAll(); - } - - [Test] - public async Task GetRoleIdByName_Should_ReturnRoleId() - { - // Arrange - const string mockedId = "mocked-id"; - const string roleName = "StorageBlobContributor"; - var mcf = MockHttpClientFactory.Create(); - var mc = mcf.CreateClient("azure"); - - mc.Request(HttpMethod.Get, $"https://management.azure.com/subscriptions/{Testing.SubscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-01-01-preview&$filter=roleName eq '{roleName}'") - .Respond.WithJson(new AzureApiService.RoleDefinition { Value = new() { new AzureApiService.RoleDefinitionValue { Id = mockedId } } }); - - var target = new AzureApiService(new AzureApiClient(mcf.GetHttpClient("azure")!)); - - // Act - var resultTask = await Testing.RunAsync(() => target.GetRoleIdByName(roleName)); - var result = await resultTask; - - // Assert - result.Should().Be(mockedId); - mcf.VerifyAll(); - } - - [Test] - public async Task SyncTriggers_Should_SyncTriggersOnTheFunctionApp() - { - // Arrange - const string resourceGroupName = "resource-group-name"; - const string functionAppName = "function-app"; - var resourceGroup = Output.Create(resourceGroupName); - var functionApp = Output.Create(functionAppName); - var mcf = MockHttpClientFactory.Create(); - var mc = mcf.CreateClient("azure"); - - mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{Testing.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{functionAppName}/syncfunctiontriggers?api-version=2016-08-01") - .Respond.With(statusCode: HttpStatusCode.NoContent); - - var target = new AzureApiService(new AzureApiClient(mcf.GetHttpClient("azure")!)); - - // Act - var resultOutput = await Testing.RunAsync(() => target.SyncFunctionAppTriggers(resourceGroup, functionApp)); - var result = await resultOutput.GetValueAsync(); - - // Assert - result.Should().BeTrue(); - mcf.VerifyAll(); - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra.Tests/Testing.cs b/samples/My.Hr/My.Hr.Infra.Tests/Testing.cs deleted file mode 100644 index af033ad3..00000000 --- a/samples/My.Hr/My.Hr.Infra.Tests/Testing.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Collections.Immutable; -using System.Text.Json; -using My.Hr.Infra.Services; -using UnitTestEx.NUnit; - -namespace My.Hr.Infra.Tests; - -public static class Testing -{ - // mocked subscription Id - public const string SubscriptionId = "622637dd-a3e9-4e54-test-56d9247c70ee"; - public const string StackName = "unit-test-stack"; - - public static async Task<(ImmutableArray Resources, IDictionary Outputs, Mock)> RunAsync() - { - var dbOperationsMock = new Mock(); - var mcf = MockHttpClientFactory.Create(); - var mc = mcf.CreateClient("azure"); - - mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{SubscriptionId}/resourceGroups/coreEx-{StackName}/providers/Microsoft.Web/sites/funApp/host/default/listkeys?api-version=2022-03-01") - .Respond.WithJson(new AzureApiService.HostKeys { FunctionKeys = new AzureApiService.FunctionKeysValue { Key = "mocked-key" } }); - - mc.Request(HttpMethod.Get, "https://api.ipify.org") - .Respond.With(new StringContent("215.45.1.567")); - - mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{SubscriptionId}/resourceGroups/coreEx-{StackName}/providers/Microsoft.Web/sites/funApp/syncfunctiontriggers?api-version=2016-08-01") - .Respond.With(statusCode: System.Net.HttpStatusCode.NoContent); - - var (resources, outputs) = await RunAsync(() => CoreExStack.ExecuteStackAsync(dbOperationsMock.Object, mcf.GetHttpClient("azure")!)); - - return (resources, outputs, dbOperationsMock); - } - - public static async Task<(ImmutableArray Resources, IDictionary Outputs)> RunAsync(Func>> createResources) - { - var config = new Dictionary{ - {"unittest:sqlAdAdmin", "sqlAdAdmin"}, - {"unittest:sqlAdPassword", "sqlAdPassword"}, - {"unittest:isAppsDeploymentEnabled", "true"}, - {"unittest:isDBSchemaDeploymentEnabled", "true"} - }; - - Environment.SetEnvironmentVariable("PULUMI_CONFIG", JsonSerializer.Serialize(config)); - var mocks = new Mocks(); - var dbOperationsMock = new Mock(); - - TestOptions options = new() - { - IsPreview = false, - ProjectName = "unittest", - StackName = StackName - }; - - var (resources, outputs) = await Deployment.TestAsync(mocks, options, () => createResources()); - - return (resources, outputs); - } - - public static async Task RunAsync(Func createResources) where T : class - { - var (resources, outputs) = await RunAsync(() => - { - var result = createResources(); - return Task.FromResult>(new Dictionary - { - ["result"] = result - }); - }); - - return (T)outputs["result"]!; - } - - public class Mocks : IMocks - { - public Task CallAsync(MockCallArgs args) - { - var outputs = ImmutableDictionary.CreateBuilder(); - outputs.AddRange(args.Args); - - // mock responses for API calls - switch (args.Token) - { - case "azure:keyvault/getKeyVault:getKeyVault": - outputs.Add("id", Guid.NewGuid().ToString()); - break; - - case "azure-native:storage:listStorageAccountKeys": - var kvJson = JsonDocument.Parse("[{\"value\":\"valueKeyStorage\"}]").RootElement; - outputs.Add("keys", kvJson); - break; - - case "azuread:index/getDomains:getDomains": - var adJson = JsonDocument.Parse("[{\"domainName\":\"myDomain.onmicrosoft.com\"}]").RootElement; - outputs.Add("domains", adJson); - break; - - case "azure-native:authorization:getClientConfig": - outputs.Add("subscriptionId", SubscriptionId); - break; - - case "azure-native:authorization:getClientToken": - outputs.Add("token", "not-a-real-token"); - break; - - default: - throw new InvalidOperationException($"Operation {args.Token} is not supported. Fix your mock setup."); - } - - return Task.FromResult((object)outputs); - } - - public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args) - { - var outputs = ImmutableDictionary.CreateBuilder(); - - // Forward all input parameters as resource outputs, so that we could test them. - outputs.AddRange(args.Inputs); - - // Set the name to resource name if it's not set explicitly in inputs. - if (!args.Inputs.ContainsKey("name")) - outputs.Add("name", args.Name ?? "name"); - - // Mocks customizations - if (args.Type == "azure-native:web:WebApp") - { - outputs["outboundIpAddresses"] = "192.167.12.1,24.56.76.1"; - outputs["defaultHostName"] = "unittest.azurewebsites.net"; - } - - // Default the resource ID to `{name}_id`. - string? id = $"{args.Id}_{args.Name}_id"; - return Task.FromResult<(string? id, object state)>((id, state: outputs)); - } - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra.Tests/TestingExtensions.cs b/samples/My.Hr/My.Hr.Infra.Tests/TestingExtensions.cs deleted file mode 100644 index 664c28f6..00000000 --- a/samples/My.Hr/My.Hr.Infra.Tests/TestingExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace My.Hr.Infra.Tests; - -public static class TestingExtensions -{ - public static Task GetValueAsync(this Output output) - { - var tcs = new TaskCompletionSource(); - output.Apply(v => { tcs.SetResult(v); return v; }); - return tcs.Task; - } - - public static Task GetValueAsync(this object outputObj) - { - if (outputObj is Output output) - { - return output.GetValueAsync(); - } - - return Task.FromException(new ArgumentException("Provided object is not Output", nameof(outputObj))); - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra.Tests/Usings.cs b/samples/My.Hr/My.Hr.Infra.Tests/Usings.cs deleted file mode 100644 index 9b5d4f20..00000000 --- a/samples/My.Hr/My.Hr.Infra.Tests/Usings.cs +++ /dev/null @@ -1,5 +0,0 @@ -global using NUnit.Framework; -global using Moq; -global using FluentAssertions; -global using Pulumi.Testing; -global using Pulumi; diff --git a/samples/My.Hr/My.Hr.Infra/Components/Apps.cs b/samples/My.Hr/My.Hr.Infra/Components/Apps.cs deleted file mode 100644 index 098e201d..00000000 --- a/samples/My.Hr/My.Hr.Infra/Components/Apps.cs +++ /dev/null @@ -1,288 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net.Http; -using System.Threading.Tasks; -using My.Hr.Infra.Services; -using Pulumi; -using Pulumi.AzureNative.Storage; -using Pulumi.AzureNative.Web; -using Pulumi.AzureNative.Web.Inputs; -using AzureNative = Pulumi.AzureNative; - -namespace My.Hr.Infra.Components; - -public class Apps : ComponentResource -{ - private readonly FunctionArgs args; - - public Output FunctionHealthUrl { get; } = default!; - public Output FunctionSwaggerUrl { get; } = default!; - public Output AppSwaggerUrl { get; } = default!; - public Output FunctionPrincipalId { get; } = default!; - public Output AppPrincipalId { get; } = default!; - public Output FunctionOutboundIps { get; } = default!; - public Output AppOutboundIps { get; } = default!; - - public Apps(string name, FunctionArgs args, AzureApiService azureApiService, ComponentResourceOptions? options = null) - : base("coreexinfra:web:apps", name, options) - { - this.args = args; - - // publish app and push zip packages to blob storage when app deployment is done via pulumi - Output> packageZips = args.IsAppDeploymentEnabled.Apply(async isEnabled => - { - if (isEnabled) - { - await PublishApp(); - - var appZipUrl = PrepareAppForDeployment("app", "../My.Hr.Api/bin/Release/net6.0/publish"); - var funZipUrl = PrepareAppForDeployment("function", "../My.Hr.Functions/bin/Release/net6.0/publish"); - - return Output.Tuple(appZipUrl, funZipUrl); - } - - return Output.Create((string.Empty, string.Empty)); - }); - - var packageUrls = packageZips.Apply(t => t); - - // https://github.com/pulumi/examples/blob/master/azure-cs-functions/FunctionsStack.cs - var appServicePlan = new AppServicePlan("apps-linux-asp", new() - { - HyperV = false, - IsSpot = false, - IsXenon = false, - Kind = "Linux", // what kinds are supported? "app" is one of them - MaximumElasticWorkerCount = 1, - PerSiteScaling = false, - - // For Linux, you need to change the plan to have Reserved = true property. - Reserved = true, - - ResourceGroupName = args.ResourceGroupName, - Sku = new SkuDescriptionArgs - { - Capacity = 1, - Family = "B1", - Name = "B1", - Size = "B1", - Tier = "Basic", - }, - TargetWorkerCount = 0, - TargetWorkerSizeId = 0, - - Tags = args.Tags - }, new CustomResourceOptions { Parent = this }); - - var app = new WebApp("app", new WebAppArgs - { - ResourceGroupName = args.ResourceGroupName, - HttpsOnly = true, - ServerFarmId = appServicePlan.Id, - Identity = new ManagedServiceIdentityArgs { Type = ManagedServiceIdentityType.SystemAssigned }, - - SiteConfig = new SiteConfigArgs - { - LinuxFxVersion = "DOTNETCORE|6.0", - AppSettings = new[] - { - new NameValuePairArgs{ - Name = "WEBSITE_RUN_FROM_PACKAGE", - // set to 1 if app is going to be deployed separately - Value = args.IsAppDeploymentEnabled.Apply(isEnabled => isEnabled ? packageUrls.Apply(p => p.appZipUrl) : Output.Create("1")) - }, - new NameValuePairArgs{ - Name = "AzureWebJobsStorage__accountName", - Value = args.StorageAccountName - }, - new NameValuePairArgs{ - Name = "APPINSIGHTS_INSTRUMENTATIONKEY", - Value = args.ApplicationInsightsInstrumentationKey - }, - new NameValuePairArgs{ - Name = "APPLICATIONINSIGHTS_CONNECTION_STRING", - Value = args.ApplicationInsightsInstrumentationKey.Apply(key => $"InstrumentationKey={key}"), - }, - new NameValuePairArgs{ - Name = "ApplicationInsightsAgent_EXTENSION_VERSION", - Value = "~2", - }, - new NameValuePairArgs{ - Name = "ServiceBusConnection__fullyQualifiedNamespace", - Value = Output.Format($"{args.ServiceBusNamespaceName}.servicebus.windows.net"), - }, - new NameValuePairArgs{ - Name = "HttpLogContent", - Value = "true", - }, - new NameValuePairArgs{ - Name = "AzureFunctionsJobHost__logging__logLevel__CoreEx", - Value = "Debug", - }, - new NameValuePairArgs{ - Name = "ConnectionStrings__Database", - Value = args.SqlConnectionString, - }, - new NameValuePairArgs{ - Name = "VerificationQueueName", - Value = args.VerificationResultsQueue, - }, - }, - }, - Tags = args.Tags, - }, new CustomResourceOptions { Parent = this }); - - var functionApp = new WebApp("funApp", new WebAppArgs - { - Kind = "FunctionApp", - ResourceGroupName = args.ResourceGroupName, - ServerFarmId = appServicePlan.Id, - HttpsOnly = true, - Identity = new ManagedServiceIdentityArgs { Type = AzureNative.Web.ManagedServiceIdentityType.SystemAssigned }, - SiteConfig = new SiteConfigArgs - { - AppSettings = new[] - { - new NameValuePairArgs{ - Name = "AzureWebJobsStorage__accountName", - Value = args.StorageAccountName - }, - new NameValuePairArgs{ - Name = "FUNCTIONS_EXTENSION_VERSION", - Value = "~4", - }, - new NameValuePairArgs{ - Name = "FUNCTIONS_WORKER_RUNTIME", - Value = "dotnet", - }, - new NameValuePairArgs{ - Name = "WEBSITE_RUN_FROM_PACKAGE", - // set to 1 if app is going to be deployed separately - Value = args.IsAppDeploymentEnabled.Apply(isEnabled => isEnabled ? packageUrls.Apply(p => p.funZipUrl) : Output.Create("1")) - }, - new NameValuePairArgs{ - Name = "APPLICATIONINSIGHTS_CONNECTION_STRING", - Value = Output.Format($"InstrumentationKey={args.ApplicationInsightsInstrumentationKey}"), - }, - new NameValuePairArgs{ - Name = "ServiceBusConnection__fullyQualifiedNamespace", - Value = Output.Format($"{args.ServiceBusNamespaceName}.servicebus.windows.net"), - }, - new NameValuePairArgs{ - Name = "AgifyApiEndpointUri", - Value = "https://api.agify.io", - }, - new NameValuePairArgs{ - Name = "NationalizeApiClientApiEndpointUri", - Value = "https://api.nationalize.io", - }, - new NameValuePairArgs{ - Name = "GenderizeApiClientApiEndpointUri", - Value = "https://api.genderize.io", - }, - new NameValuePairArgs{ - Name = "VerificationQueueName", - Value = args.VerificationResultsQueue, - }, - new NameValuePairArgs{ - Name = "VerificationResultsQueueName", - Value = args.VerificationResultsQueue, - }, - new NameValuePairArgs{ - Name = "MassPublishQueueName", - Value = args.MassPublishQueue, - }, - new NameValuePairArgs{ - Name = "HttpLogContent", - Value = "true", - }, - new NameValuePairArgs{ - Name = "AzureFunctionsJobHost__logging__logLevel__CoreEx", - Value = "Debug", - }, - new NameValuePairArgs{ - Name = "ConnectionStrings__Database", - Value = args.SqlConnectionString, - }, - }, - }, - Tags = args.Tags, - }, new CustomResourceOptions - { - Parent = this, - CustomTimeouts = new CustomTimeouts - { - Create = TimeSpan.FromMinutes(4) - } - }); - - FunctionPrincipalId = functionApp.Identity.Apply(identity => identity?.PrincipalId ?? "11111111-1111-1111-1111-111111111111"); - AppPrincipalId = app.Identity.Apply(identity => identity?.PrincipalId ?? "11111111-1111-1111-1111-111111111111"); - - FunctionOutboundIps = functionApp.OutboundIpAddresses; - AppOutboundIps = app.OutboundIpAddresses; - - var functionKey = Output.CreateSecret(azureApiService.GetHostKeys(args.ResourceGroupName, functionApp.Name)); - - // sync function app service triggers - azureApiService.SyncFunctionAppTriggers(args.ResourceGroupName, functionApp.Name); - - FunctionHealthUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/health?code={functionKey}"); - FunctionSwaggerUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/swagger/ui?code={functionKey}"); - AppSwaggerUrl = Output.Format($"https://{app.DefaultHostName}/swagger/index.html"); - - RegisterOutputs(); - } - - private static async Task PublishApp() - { - if (Deployment.Instance.IsDryRun) - return; - - Log.Info("Setting up deployments from zip for the app and function and executing [dotnet publish]"); - - var sw = Stopwatch.StartNew(); - var publishProcess = Process.Start(new ProcessStartInfo - { - WorkingDirectory = "../", - FileName = "dotnet", - Arguments = "publish --nologo -c RELEASE", - RedirectStandardOutput = true, - RedirectStandardError = true - }); - await publishProcess!.WaitForExitAsync(); - sw.Stop(); - Log.Info($"[dotnet publish] completed in {sw.Elapsed}"); - } - - private Output PrepareAppForDeployment(string name, string path) - { - var blob = new Blob($"{name}_zip", new BlobArgs - { - AccountName = args.StorageAccountName, - ContainerName = args.StorageDeploymentContainerName, - ResourceGroupName = args.ResourceGroupName, - Type = BlobType.Block, - Source = new FileArchive(path) - }, new CustomResourceOptions { Parent = this }); - - var codeBlobUrl = Output.Format($"https://{args.StorageAccountName}.blob.core.windows.net/{args.StorageDeploymentContainerName}/{blob.Name}"); - - return codeBlobUrl; - } - - public class FunctionArgs - { - public Input ResourceGroupName { get; set; } = default!; - public Input StorageAccountName { get; set; } = default!; - public Input ServiceBusNamespaceName { get; set; } = default!; - public Input SqlConnectionString { get; set; } = default!; - public Input ApplicationInsightsInstrumentationKey { get; set; } = default!; - public InputMap Tags { get; set; } = default!; - public string PendingVerificationsQueue { get; set; } = default!; - public string VerificationResultsQueue { get; set; } = default!; - public string MassPublishQueue { get; set; } = default!; - public Input IsAppDeploymentEnabled { get; set; } = default!; - public Input StorageDeploymentContainerName { get; set; } = default!; - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Components/Diagnostics.cs b/samples/My.Hr/My.Hr.Infra/Components/Diagnostics.cs deleted file mode 100644 index ec112c40..00000000 --- a/samples/My.Hr/My.Hr.Infra/Components/Diagnostics.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Pulumi; -using Pulumi.AzureNative.Insights.V20200202; -using Pulumi.AzureNative.OperationalInsights; - -namespace My.Hr.Infra.Components; - -public class Diagnostics : ComponentResource -{ - public Output InstrumentationKey { get; } = default!; - - public Diagnostics(string name, DiagnosticsArgs args, ComponentResourceOptions? options = null) - : base("coreexinfra:web:diagnostics", name, options) - { - // Log Analytics Workspace - var workspace = new Workspace("workspace", new() - { - ResourceGroupName = args.ResourceGroupName, - RetentionInDays = 30, - Sku = new Pulumi.AzureNative.OperationalInsights.Inputs.WorkspaceSkuArgs - { - Name = "PerGB2018", - }, - Tags = args.Tags, - WorkspaceName = "lw-workspace", - }, new CustomResourceOptions { Parent = this }); - - // Application insights - var appInsights = new Component("appInsights", new ComponentArgs - { - ApplicationType = ApplicationType.Web, - Kind = "web", - ResourceGroupName = args.ResourceGroupName, - WorkspaceResourceId = workspace.Id, - Tags = args.Tags - }, new CustomResourceOptions { Parent = this }); - - InstrumentationKey = appInsights.InstrumentationKey; - RegisterOutputs(); - } - - public class DiagnosticsArgs - { - public Input ResourceGroupName { get; set; } = default!; - public InputMap Tags { get; set; } = default!; - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Components/Messaging.cs b/samples/My.Hr/My.Hr.Infra/Components/Messaging.cs deleted file mode 100644 index 5ee62f91..00000000 --- a/samples/My.Hr/My.Hr.Infra/Components/Messaging.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Collections.Generic; -using Pulumi; -using Pulumi.AzureNative.Authorization; -using Pulumi.AzureNative.ServiceBus; -using AzureNative = Pulumi.AzureNative; - -namespace My.Hr.Infra.Components; - -public class Messaging : ComponentResource -{ - private readonly MessagingArgs args; - private readonly string name; - - public Output NamespaceName { get; } = default!; - public Output NamespaceId { get; } = default!; - - public Messaging(string name, MessagingArgs args, ComponentResourceOptions? options = null) - : base("coreexinfra:web:messaging", name, options) - { - this.args = args; - this.name = name; - - var @namespace = new Namespace(name, new NamespaceArgs - { - ResourceGroupName = args.ResourceGroupName, - Sku = new AzureNative.ServiceBus.Inputs.SBSkuArgs - { - Name = SkuName.Standard, - Tier = SkuTier.Standard, - }, - Tags = args.Tags - }, new CustomResourceOptions { Parent = this }); - - NamespaceName = @namespace.Name; - NamespaceId = @namespace.Id; - - RegisterOutputs(); - } - - public Queue AddQueue(string queueName, bool batchOperationsEnabled = false) - { - return new Queue($"{name}-queue-{queueName}", new() - { - EnablePartitioning = false, - NamespaceName = NamespaceName, - QueueName = queueName, - ResourceGroupName = args.ResourceGroupName, - MaxDeliveryCount = 3, - EnableBatchedOperations = batchOperationsEnabled - }, new CustomResourceOptions { Parent = this }); - } - - public IEnumerable AddAccess(Input principalId, string name) - { - var receive_permission = new RoleAssignment( - $"receive-for-{name}", - new RoleAssignmentArgs - { - Description = $"{name} receiving data from service bus", - PrincipalId = principalId, - PrincipalType = "ServicePrincipal", - RoleDefinitionId = Roles.BuiltInRolesIds.ServiceBusDataReceiver, - Scope = NamespaceId - }, - new CustomResourceOptions { Parent = this } - ); - - var send_permission = new RoleAssignment( - $"send-for-{name}", - new RoleAssignmentArgs - { - Description = $"{name} sending data to service bus", - PrincipalId = principalId, - PrincipalType = "ServicePrincipal", - - RoleDefinitionId = Roles.BuiltInRolesIds.ServiceBusDataSender, - Scope = NamespaceId - }, - new CustomResourceOptions { Parent = this } - ); - - return new[] { receive_permission, send_permission }; - } - - public class MessagingArgs - { - public Input ResourceGroupName { get; set; } = default!; - public InputMap Tags { get; set; } = default!; - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Components/Sql.cs b/samples/My.Hr/My.Hr.Infra/Components/Sql.cs deleted file mode 100644 index 6695e296..00000000 --- a/samples/My.Hr/My.Hr.Infra/Components/Sql.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Collections.Generic; -using My.Hr.Infra.Services; -using Pulumi; -using Pulumi.AzureNative.Sql; -using Pulumi.AzureNative.Sql.Inputs; -using AD = Pulumi.AzureAD; -using Deployment = Pulumi.Deployment; - -namespace My.Hr.Infra.Components; - -public class Sql : ComponentResource -{ - private readonly SqlArgs args; - private readonly HashSet firewallAllowedIps = new(); - - public Output SqlDatabaseConnectionString { get; } - public Output SqlServerName { get; } - public Output SqlDatabaseAuthorizedGroupId { get; } - - public Sql(string name, SqlArgs args, IDbOperations dbOperations, AzureApiClient apiClient, ComponentResourceOptions? options = null) - : base("coreexinfra:web:sql", name, options) - { - this.args = args; - var sqlAdAdmin = new AD.User("sqlAdmin", new AD.UserArgs - { - UserPrincipalName = args.SqlAdAdminLogin, - Password = args.SqlAdAdminPassword, - DisplayName = $"Global SQL Admin {Deployment.Instance.StackName}" - }, new CustomResourceOptions { Parent = this }); - - var sqlServer = new Server($"sql-server-{Deployment.Instance.StackName}", new ServerArgs - { - ResourceGroupName = args.ResourceGroupName, - Administrators = new ServerExternalAdministratorArgs - { - Login = sqlAdAdmin.UserPrincipalName, - Sid = sqlAdAdmin.Id, - AzureADOnlyAuthentication = true, - AdministratorType = AdministratorType.ActiveDirectory, - PrincipalType = PrincipalType.User, - }, - MinimalTlsVersion = "1.2", - Tags = args.Tags - }, new CustomResourceOptions { Parent = this }); - - var publicIp = Output.Create(apiClient.GetMyIP()); - - var enableLocalMachine = new FirewallRule("AllowLocalMachine", new FirewallRuleArgs - { - ResourceGroupName = args.ResourceGroupName, - ServerName = sqlServer.Name, - StartIpAddress = publicIp, - EndIpAddress = publicIp - }, new CustomResourceOptions { Parent = this }); - - var database = new Pulumi.AzureNative.Sql.Database("sqldb", new DatabaseArgs - { - ResourceGroupName = args.ResourceGroupName, - ServerName = sqlServer.Name, - DatabaseName = "CoreExDB", - Sku = new SkuArgs - { - Name = "Basic" - }, - Tags = args.Tags - }, new CustomResourceOptions { Parent = this }); - - string sqlDatabaseAuthorizedGroupName = $"SqlDbUsersGroup{Deployment.Instance.StackName}"; - var sqlDatabaseAuthorizedGroup = new AD.Group(sqlDatabaseAuthorizedGroupName, new AD.GroupArgs - { - DisplayName = sqlDatabaseAuthorizedGroupName, - SecurityEnabled = true, - Owners = new InputList { sqlAdAdmin.Id } - }, new CustomResourceOptions { Parent = this }); - - var sqlADConnectionString = Output.Format($"Server={sqlServer.Name}.database.windows.net; Authentication=Active Directory Password; User={args.SqlAdAdminLogin}; Password={args.SqlAdAdminPassword}; Database={database.Name}"); - - // login with AD admin credentials to give access to AD group that contains App and Function managed identity users - dbOperations.ProvisionUsers(sqlADConnectionString!, sqlDatabaseAuthorizedGroupName); - - // create SQL user and setup schema - if (args.IsDBSchemaDeploymentEnabled) - { - sqlADConnectionString.Apply(async cs => - { - return await dbOperations.DeployDbSchemaAsync(cs); - }); - } - - // https://docs.microsoft.com/en-us/sql/connect/ado-net/sql/azure-active-directory-authentication?view=sql-server-ver15#using-active-directory-default-authentication - SqlDatabaseConnectionString = Output.Format($"Server={sqlServer.Name}.database.windows.net; Authentication=Active Directory Default; Database={database.Name}"); - SqlServerName = sqlServer.Name; - SqlDatabaseAuthorizedGroupId = sqlDatabaseAuthorizedGroup.Id; - - RegisterOutputs(); - } - - public void AddFirewallRule(Output ips, string name) - { - // this shows warning in Pulumi, but there's no other way to create this resource without doing it in Apply - ips.Apply(ips => - { - foreach (var address in ips.Split(",")) - { - if (!firewallAllowedIps.Contains(address)) - { - firewallAllowedIps.Add(address); - - var enableIp = new FirewallRule("Enable_" + name + "_" + address, new FirewallRuleArgs - { - ResourceGroupName = args.ResourceGroupName, - ServerName = SqlServerName, - StartIpAddress = address, - EndIpAddress = address - }, new CustomResourceOptions { Parent = this }); - } - } - - return true; - }); - } - - public void AddToSqlDatabaseAuthorizedGroup(string name, Output principalId) - { - var appGroupMember = new AD.GroupMember(name, new() - { - GroupObjectId = SqlDatabaseAuthorizedGroupId, - MemberObjectId = principalId, - }, new CustomResourceOptions { Parent = this }); - } - - public class SqlArgs - { - public Input ResourceGroupName { get; set; } = default!; - public InputMap Tags { get; set; } = default!; - public Input SqlAdAdminLogin { get; set; } = default!; - public Input SqlAdAdminPassword { get; set; } = default!; - public bool IsDBSchemaDeploymentEnabled { get; set; } - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Components/Storage.cs b/samples/My.Hr/My.Hr.Infra/Components/Storage.cs deleted file mode 100644 index 7f9b9139..00000000 --- a/samples/My.Hr/My.Hr.Infra/Components/Storage.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Pulumi; -using Pulumi.AzureNative.Authorization; -using Pulumi.AzureNative.Storage; - -using AzureNative = Pulumi.AzureNative; - -namespace My.Hr.Infra.Components; - -public class Storage : ComponentResource -{ - private readonly Output id = default!; - public Output AccountName { get; private set; } = default!; - public Output DeploymentContainerName { get; private set; } = default!; - public Output ConnectionString { get; private set; } = default!; - - public Storage(string name, StorageArgs args, ComponentResourceOptions? options = null) - : base("coreexinfra:web:storage", name, options) - { - // Create an Azure resource (Storage Account) - var storageAccount = new StorageAccount(name, new StorageAccountArgs - { - ResourceGroupName = args.ResourceGroupName, - Sku = new AzureNative.Storage.Inputs.SkuArgs - { - Name = SkuName.Standard_LRS - }, - Kind = Kind.StorageV2, - AllowBlobPublicAccess = false, - EnableHttpsTrafficOnly = true, - MinimumTlsVersion = "TLS1_2", - Tags = args.Tags - }, new CustomResourceOptions { Parent = this }); - - var deploymentContainer = new BlobContainer("zips-container", new BlobContainerArgs - { - AccountName = storageAccount.Name, - PublicAccess = PublicAccess.None, - ResourceGroupName = args.ResourceGroupName, - }, new CustomResourceOptions { Parent = this }); - - var connectionString = GetConnectionString(args.ResourceGroupName, storageAccount.Name); - - AccountName = storageAccount.Name!; - id = storageAccount.Id!; - ConnectionString = connectionString; - DeploymentContainerName = deploymentContainer.Name; - - RegisterOutputs(); - } - - private static Output GetConnectionString(Input resourceGroupNameInput, Input accountNameInput) - { - return Output.Tuple(resourceGroupNameInput, accountNameInput) - .Apply(async t => - { - var (resourceGroupName, accountName) = t; - - var storageAccountKeys = await ListStorageAccountKeys.InvokeAsync(new ListStorageAccountKeysArgs - { - ResourceGroupName = resourceGroupName, - AccountName = accountName - }); - - // Retrieve the primary storage account key. - return $"DefaultEndpointsProtocol=https;AccountName={accountNameInput};AccountKey={storageAccountKeys.Keys.First().Value}"; - }); - } - - public RoleAssignment AddAccess(Input principalId, string name) - { - return new RoleAssignment( - $"useblob-for-{name}", - new RoleAssignmentArgs - { - Description = $"{name} accessing storage account", - PrincipalId = principalId, - PrincipalType = "ServicePrincipal", - RoleDefinitionId = Roles.BuiltInRolesIds.StorageBlobDataOwner, - Scope = id - }, - new CustomResourceOptions { Parent = this } - ); - } - - public class StorageArgs - { - public Input ResourceGroupName { get; set; } = default!; - public InputMap Tags { get; set; } = default!; - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/CoreExStack.cs b/samples/My.Hr/My.Hr.Infra/CoreExStack.cs deleted file mode 100644 index 63a133f4..00000000 --- a/samples/My.Hr/My.Hr.Infra/CoreExStack.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using My.Hr.Infra.Services; -using Pulumi; -using Pulumi.AzureNative.Resources; -using AD = Pulumi.AzureAD; - -namespace My.Hr.Infra; - -public static class CoreExStack -{ - public static async Task> ExecuteStackAsync(IDbOperations dbOperations, HttpClient client) - { - var config = await StackConfiguration.CreateConfiguration(); - Log.Info("Configuration completed"); - - var tags = new InputMap { { "App", "CoreEx" } }; - - // Create Azure API client for direct HTTP calls - var azureApiClient = new AzureApiClient(client); - var azureApiService = new AzureApiService(azureApiClient); - - // Create an Azure Resource Group - var resourceGroup = new ResourceGroup($"coreEx-{Pulumi.Deployment.Instance.StackName}", new ResourceGroupArgs - { - Tags = tags - }); - - var serviceBus = new Components.Messaging("coreExBus", new Components.Messaging.MessagingArgs - { - ResourceGroupName = resourceGroup.Name, - Tags = tags - }); - serviceBus.AddQueue(config.PendingVerificationsQueue); - serviceBus.AddQueue(config.VerificationResultsQueue!); - serviceBus.AddQueue(config.MassPublishQueue, batchOperationsEnabled: true); - - var storage = new Components.Storage("sa", new Components.Storage.StorageArgs - { - ResourceGroupName = resourceGroup.Name, - Tags = tags - }); - - var appInsights = new Components.Diagnostics("insights", new Components.Diagnostics.DiagnosticsArgs - { - ResourceGroupName = resourceGroup.Name, - Tags = tags - }); - - var sql = new Components.Sql("sql", new Components.Sql.SqlArgs - { - ResourceGroupName = resourceGroup.Name, - SqlAdAdminLogin = config.SqlAdAdminLogin!, - SqlAdAdminPassword = config.SqlAdAdminPassword!, - IsDBSchemaDeploymentEnabled = config.IsDBSchemaDeploymentEnabled, - Tags = tags - }, dbOperations, azureApiClient); - - var apps = new Components.Apps("apps", new Components.Apps.FunctionArgs - { - ResourceGroupName = resourceGroup.Name, - StorageAccountName = storage.AccountName, - StorageDeploymentContainerName = storage.DeploymentContainerName, - ServiceBusNamespaceName = serviceBus.NamespaceName, - SqlConnectionString = sql.SqlDatabaseConnectionString, - ApplicationInsightsInstrumentationKey = appInsights.InstrumentationKey, - PendingVerificationsQueue = config.PendingVerificationsQueue, - VerificationResultsQueue = config.VerificationResultsQueue, - MassPublishQueue = config.MassPublishQueue, - IsAppDeploymentEnabled = config.IsAppsDeploymentEnabled, - Tags = tags - }, azureApiService); - - // Permissions for function app - storage.AddAccess(apps.FunctionPrincipalId, "functionApp"); - serviceBus.AddAccess(apps.FunctionPrincipalId, "functionApp"); - - // Permissions for app service - storage.AddAccess(apps.AppPrincipalId, "appService"); - serviceBus.AddAccess(apps.AppPrincipalId, "appService"); - - // allow app and function to query/use DB - sql.AddToSqlDatabaseAuthorizedGroup("functionGroupMember", apps.FunctionPrincipalId); - sql.AddToSqlDatabaseAuthorizedGroup("appGroupMember", apps.AppPrincipalId); - - // allow app and function through SQL firewall - sql.AddFirewallRule(apps.FunctionOutboundIps, "appService"); - sql.AddFirewallRule(apps.AppOutboundIps, "appService"); - - return new Dictionary - { - ["SqlDatabaseConnectionString"] = sql.SqlDatabaseConnectionString, - ["FunctionHealthUrl"] = apps.FunctionHealthUrl, - ["FunctionSwaggerUrl"] = apps.FunctionSwaggerUrl, - ["AppSwaggerUrl"] = apps.AppSwaggerUrl, - }; - } - - public class StackConfiguration - { - public Input? SqlAdAdminLogin { get; private set; } - public Input? SqlAdAdminPassword { get; private set; } - public bool IsAppsDeploymentEnabled { get; private set; } - public bool IsDBSchemaDeploymentEnabled { get; private set; } - public string PendingVerificationsQueue { get; private set; } = default!; - public string VerificationResultsQueue { get; private set; } = default!; - public string MassPublishQueue { get; private set; } = default!; - - private StackConfiguration() { } - - public static async Task CreateConfiguration() - { - // read stack config - var config = new Config(); - - // get some info from Azure AD - var domainResult = await AD.GetDomains.InvokeAsync(new AD.GetDomainsArgs { OnlyDefault = true }); - var defaultUsername = $"sqlGlobalAdAdmin{Pulumi.Deployment.Instance.StackName}@{domainResult.Domains[0].DomainName}"; - var defaultPassword = new Pulumi.Random.RandomPassword("sqlAdPassword", new() - { - Length = 32, - Upper = true, - Number = true, - Special = true, - OverrideSpecial = "@", - MinLower = 2, - MinUpper = 2, - MinSpecial = 2, - MinNumeric = 2 - }).Result; - - Log.Info($"Default username is: {defaultUsername}"); - - return new StackConfiguration - { - SqlAdAdminLogin = Extensions.GetConfigValue("sqlAdAdmin", defaultUsername), - SqlAdAdminPassword = Extensions.GetConfigValue("sqlAdPassword", defaultPassword), - IsAppsDeploymentEnabled = config.GetBoolean("isAppsDeploymentEnabled") ?? false, - IsDBSchemaDeploymentEnabled = config.GetBoolean("isDBSchemaDeploymentEnabled") ?? false, - - PendingVerificationsQueue = config.Get("pendingVerificationsQueue") ?? "pendingVerifications", - VerificationResultsQueue = config.Get("verificationResultsQueue") ?? "verificationResults", - MassPublishQueue = config.Get("massPublishQueue") ?? "massPublish" - }; - } - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Extensions.cs b/samples/My.Hr/My.Hr.Infra/Extensions.cs deleted file mode 100644 index 35e844d0..00000000 --- a/samples/My.Hr/My.Hr.Infra/Extensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Threading.Tasks; -using Pulumi; - -namespace My.Hr.Infra; - -public static class Extensions -{ - public static Input GetConfigValue(string name, Input defaultValue) - { - var config = new Config(); - - var configValue = config.Get(name); - - if (string.IsNullOrEmpty(configValue)) - { - Log.Info($"Defaulting {name} because it wasn't present in configuration"); - return defaultValue.ToOutput(); - } - else - { - return Output.Create(configValue); - } - } - - public static Task GetValue(this Output output) => output.GetValue(_ => _); - - public static Task GetValue(this Output output, Func valueResolver) - { - var tcs = new TaskCompletionSource(); - output.Apply(_ => - { - var result = valueResolver(_); - tcs.SetResult(result); - return result; - }); - return tcs.Task; - } - - public static Task GetValue(this Input input) => input.GetValue(_ => _); - - public static Task GetValue(this Input input, Func valueResolver) - { - var tcs = new TaskCompletionSource(); - input.Apply(_ => - { - var result = valueResolver(_); - tcs.SetResult(result); - return result; - }); - return tcs.Task; - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj b/samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj deleted file mode 100644 index 8fb22fea..00000000 --- a/samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - Exe - net6.0 - enable - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/samples/My.Hr/My.Hr.Infra/Program.cs b/samples/My.Hr/My.Hr.Infra/Program.cs deleted file mode 100644 index 4bc17535..00000000 --- a/samples/My.Hr/My.Hr.Infra/Program.cs +++ /dev/null @@ -1,30 +0,0 @@ -// build CoreEx stack -using My.Hr.Infra.Services; -using Pulumi; - -return await Deployment.RunAsync(() => -{ - var client = new System.Net.Http.HttpClient(); - // create and use actual instance of DB Operations service - return My.Hr.Infra.CoreExStack.ExecuteStackAsync(new DbOperations(), client); -}, new StackOptions -{ - // apply auto-tagging transformation - // https://gist.github.com/dbeattie71/1f8a1a9264ceb8161ad4c49de1ee3bb3 - ResourceTransformations = new System.Collections.Generic.List{ - (args) => { - var tagsProp = args.Args.GetType().GetProperty("Tags"); - - if(tagsProp?.GetValue(args.Args) is InputMap tags) - { - Log.Debug("Adding tags to " + args.Resource.GetResourceName()); - - tags.Add("user:Stack", Deployment.Instance.StackName); - tags.Add("user:Project", Deployment.Instance.ProjectName); - tags.Add("App:Name", "CoreEx"); - } - - return new ResourceTransformationResult(args.Args, args.Options); - } - } -}); diff --git a/samples/My.Hr/My.Hr.Infra/Pulumi.yaml b/samples/My.Hr/My.Hr.Infra/Pulumi.yaml deleted file mode 100644 index 4b527ea0..00000000 --- a/samples/My.Hr/My.Hr.Infra/Pulumi.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: My.Hr.Infra -runtime: dotnet -description: Infrastructure for CoreEx sample application -template: - config: - azure-native:location: - description: The Azure region to deploy into - default: EastUs - My.Hr.Infra:isAppsDeploymentEnabled: - description: Whether Application code should be deployed - default: true - My.Hr.Infra:isDBSchemaDeploymentEnabled: - description: Whether Database schema should be deployed - default: true diff --git a/samples/My.Hr/My.Hr.Infra/Readme.md b/samples/My.Hr/My.Hr.Infra/Readme.md deleted file mode 100644 index 61b69148..00000000 --- a/samples/My.Hr/My.Hr.Infra/Readme.md +++ /dev/null @@ -1,59 +0,0 @@ -# About - -Infrastructure is built with [Pulumi](https://www.pulumi.com/). - -The easiest way to deploy it is by using Pulumi account (Free), but it's not mandatory. - -Prerequisites: - -1. [Pulumi CLI](https://www.pulumi.com/docs/get-started/install/) -2. Azure CLI - logged in to Azure subscription with permissions to create service principals - -> Note: Some corporate AAD restrict what can be done in AAD. Since this sample creates AAD User and Group, infrastructure needs to be created in AAD tenant that allows it. - -## Pulumi with azure storage - -Pulumi can be used without Pulumi Account, by using [Azure Storage as backend](https://www.techwatching.dev/posts/pulumi-azure-backend). - -1. set the `AZURE_STORAGE_ACCOUNT` environment variable to specify the Azure storage account to use -1. set the `AZURE_STORAGE_KEY` or the `AZURE_STORAGE_SAS_TOKEN` environment variables to let Pulumi access the storage -1. execute the following command `pulumi login azblob://` where container-path is the path to a blob container in the storage account - -## Configuring Pulumi (optional) - -Infrastructure project has only 2 settings: - -* `My.Hr.Infra:isAppsDeploymentEnabled` for controlling application deployment via zip deploy -* `My.Hr.Infra:isDBSchemaDeploymentEnabled` for publishing Database schema and data - -> When `isAppsDeploymentEnabled` flag is set, pulumi code executes `dotnet publish -c RELEASE` to create app packages. - -Pulumi can be configured and previewed with: - -```bash -pulumi preview -c azure-native:location=EastUs -c My.Hr.Infra:isAppsDeploymentEnabled=true -c My.Hr.Infra:isDBSchemaDeploymentEnabled=true -``` - -which creates a stack config file `Pulumi.dev.yaml` - -```yaml -config: - azure-native:location: EastUs - My.Hr.Infra:isAppsDeploymentEnabled: true - My.Hr.Infra:isDBSchemaDeploymentEnabled: true -``` - -## Deploy with Pulumi - -To deploy in `samples/My.Hr/My.Hr.Infra` run `pulumi up -c azure-native:location=EastUs -c My.Hr.Infra:isAppsDeploymentEnabled=true -c My.Hr.Infra:isDBSchemaDeploymentEnabled=true` - -To display outputs of the stack deployment run: `pulumi stack output --show-secrets` which will display function links with secret api key. - -## Alternative deployment methods - -Apps can also be deployed with Azure CLI, once published apps are zipped. - -```bash -az webapp deploy --resource-group coreEx-dev4011fb65 --name app17b7c4c8 --src-path app.zip -az functionapp deployment source config-zip -g coreEx-dev4011fb65 -n fun17b7c4c8 --src fun.zip -``` diff --git a/samples/My.Hr/My.Hr.Infra/Roles/BuiltInRolesIds.cs b/samples/My.Hr/My.Hr.Infra/Roles/BuiltInRolesIds.cs deleted file mode 100644 index cad5d8d2..00000000 --- a/samples/My.Hr/My.Hr.Infra/Roles/BuiltInRolesIds.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace My.Hr.Infra.Roles; - -public static class BuiltInRolesIds -{ - public const string StorageBlobDataOwner = "/providers/Microsoft.Authorization/roleDefinitions/b7e6dc6d-f1e8-4753-8033-0f276bb0955b"; - - // https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-receiver - public const string ServiceBusDataReceiver = "/providers/Microsoft.Authorization/roleDefinitions/4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0"; - - // https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-sender - public const string ServiceBusDataSender = "/providers/Microsoft.Authorization/roleDefinitions/69a216fc-b8fb-44d8-bc22-1f3c2cd27a39"; -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Services/AzureApiClient.cs b/samples/My.Hr/My.Hr.Infra/Services/AzureApiClient.cs deleted file mode 100644 index e9d49d08..00000000 --- a/samples/My.Hr/My.Hr.Infra/Services/AzureApiClient.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using CoreEx.Configuration; -using CoreEx.Http; -using Microsoft.Extensions.Configuration; - -namespace My.Hr.Infra.Services; - -/// -/// Http client for Azure APIs -/// -public class AzureApiClient : TypedHttpClientCore -{ - public AzureApiClient(HttpClient client) - : base(client, CoreEx.Json.JsonSerializer.Default, new CoreEx.ExecutionContext(), new DefaultSettings(new ConfigurationBuilder().Build()), PulumiLogger>.Instance) - { - } - - protected override async Task OnBeforeRequest(HttpRequestMessage request, CancellationToken cancellationToken) - { - var token = await Pulumi.AzureNative.Authorization.GetClientToken.InvokeAsync(); - request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token); - - await base.OnBeforeRequest(request, cancellationToken); - } - - public async Task GetMyIP() - { - // use base client directly to skip OnBeforeRequest - return await base.Client.GetStringAsync("https://api.ipify.org"); - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Services/AzureApiService.cs b/samples/My.Hr/My.Hr.Infra/Services/AzureApiService.cs deleted file mode 100644 index c7989421..00000000 --- a/samples/My.Hr/My.Hr.Infra/Services/AzureApiService.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Polly.Extensions.Http; -using Pulumi; -using Pulumi.AzureNative.Authorization; - -namespace My.Hr.Infra.Services; - -public class AzureApiService -{ - - public AzureApiClient ApiClient { get; private set; } - - public AzureApiService(AzureApiClient apiClient) - { - ApiClient = apiClient; - } - - /// - /// Gets role id based on the provided role name. This method can be used instead of hardcoded role Ids above - /// - /// - /// - /// - /// Thrown when request fails - /// code from: https://github.com/pulumi/examples/blob/28b559d68eb6a67f3e6b5fb3d2a337b5b9ed35d5/azure-cs-call-azure-api/Program.cs#L45 - public async Task GetRoleIdByName(string roleName, string? scope = null) - { - var config = await GetClientConfig.InvokeAsync(); - - var result = await ApiClient - .ThrowTransientException() - .WithRetry() - .GetAsync($"https://management.azure.com/subscriptions/{config.SubscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-01-01-preview&$filter=roleName%20eq%20'{roleName}'"); - - return result.Value.Value[0].Id; - } - - public Output GetHostKeys(Output rgName, Output functionName) - { - return Output.Tuple(rgName, functionName).Apply(async t => - { - var (resourceGroupName, siteName) = t; - Log.Info("Getting host keys for: " + siteName); - - var config = await GetClientConfig.InvokeAsync(); - - var result = await ApiClient - .WithRetry(count: 4, seconds: 5) - // Azure returns 400 BadRequest when Function App is not ready - .WithCustomRetryPolicy(HttpPolicyExtensions.HandleTransientHttpError().OrResult(http => http.StatusCode == System.Net.HttpStatusCode.BadRequest || http.StatusCode == System.Net.HttpStatusCode.NotFound)) - .PostAsync($"https://management.azure.com/subscriptions/{config.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{siteName}/host/default/listkeys?api-version=2022-03-01"); - - return result.Value.FunctionKeys.Key; - }); - } - - // https://docs.microsoft.com/en-us/azure/azure-functions/functions-deployment-technologies#trigger-syncing - public Output SyncFunctionAppTriggers(Output rgName, Output functionName) - { - return Output.Tuple(rgName, functionName).Apply(async t => - { - var (resourceGroupName, siteName) = t; - Log.Info("Syncing Function App triggers"); - - var config = await GetClientConfig.InvokeAsync(); - - var url = $"https://management.azure.com/subscriptions/{config.SubscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{siteName}/syncfunctiontriggers?api-version=2016-08-01"; - - await ApiClient - .WithRetry() - .ThrowTransientException() - .PostAsync(url); - - return true; - }); - } - - public class HostKeys - { - [JsonPropertyName("masterKey")] - public string MasterKey { get; set; } = default!; - - [JsonPropertyName("functionKeys")] - public FunctionKeysValue FunctionKeys { get; set; } = default!; - } - - public class FunctionKeysValue - { - [JsonPropertyName("default")] - public string Key { get; set; } = default!; - } - - public class RoleDefinition - { - [JsonPropertyName("value")] - public List Value { get; set; } = default!; - } - public class RoleDefinitionValue - { - [JsonPropertyName("id")] - public string Id { get; set; } = default!; - - [JsonPropertyName("type")] - public string Type { get; set; } = default!; - - [JsonPropertyName("name")] - public string Name { get; set; } = default!; - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Services/DbOperations.cs b/samples/My.Hr/My.Hr.Infra/Services/DbOperations.cs deleted file mode 100644 index 13f8b08a..00000000 --- a/samples/My.Hr/My.Hr.Infra/Services/DbOperations.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; -using Dapper; -using Microsoft.Data.SqlClient; -using Pulumi; - -namespace My.Hr.Infra.Services; - -public class DbOperations : IDbOperations -{ - public void ProvisionUsers(Input connectionString, string groupName) - { - if (Deployment.Instance.IsDryRun) - // skip in dry run - return; - - Log.Info($"Provisioning user {groupName} in SQL DB"); - string commandText = @$" - IF NOT EXISTS (SELECT [name] - FROM [sys].[database_principals] - WHERE [type] = N'X' AND [name] = N'{groupName}') - BEGIN - CREATE USER {groupName} FROM EXTERNAL PROVIDER; - END - - ALTER ROLE db_datareader ADD MEMBER {groupName}; - ALTER ROLE db_datawriter ADD MEMBER {groupName}; - "; - - connectionString.Apply(async cs => - { - using SqlConnection conn = new(cs); - await conn.OpenAsync(); - - var result = await conn.ExecuteAsync(commandText); - return true; - }); - } - - public Task DeployDbSchemaAsync(string connectionString) - { - if (Deployment.Instance.IsDryRun) - // skip in dry run - return Task.FromResult(0); - - Log.Info($"Deploying DB schema using {connectionString}"); - return Database.Program.RunMigrator(connectionString, assembly: typeof(My.Hr.Database.Program).Assembly, "DeployWithData"); - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Services/IDbOperations.cs b/samples/My.Hr/My.Hr.Infra/Services/IDbOperations.cs deleted file mode 100644 index f681adfc..00000000 --- a/samples/My.Hr/My.Hr.Infra/Services/IDbOperations.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; -using Pulumi; - -namespace My.Hr.Infra.Services; - -public interface IDbOperations -{ - Task DeployDbSchemaAsync(string connectionString); - void ProvisionUsers(Input connectionString, string groupName); -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Infra/Services/PulumiLogger.cs b/samples/My.Hr/My.Hr.Infra/Services/PulumiLogger.cs deleted file mode 100644 index 9a14fd0f..00000000 --- a/samples/My.Hr/My.Hr.Infra/Services/PulumiLogger.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.Extensions.Logging; - -namespace My.Hr.Infra.Services; - -public class PulumiLogger : ILogger -{ - public static readonly PulumiLogger Instance = new(); - - private PulumiLogger() - { } - - public IDisposable BeginScope(TState state) - { - throw new NotImplementedException(); - } - - public bool IsEnabled(LogLevel logLevel) - { - throw new NotImplementedException(); - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - switch (logLevel) - { - case LogLevel.Trace: - case LogLevel.Debug: - Pulumi.Log.Debug(formatter(state, exception)); - break; - case LogLevel.Information: - Pulumi.Log.Info(formatter(state, exception)); - break; - case LogLevel.Warning: - Pulumi.Log.Warn(formatter(state, exception)); - break; - case LogLevel.Error: - case LogLevel.Critical: - Pulumi.Log.Error(formatter(state, exception)); - break; - } - } -} \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.sln b/samples/My.Hr/My.Hr.sln index e7600107..1a020941 100644 --- a/samples/My.Hr/My.Hr.sln +++ b/samples/My.Hr/My.Hr.sln @@ -11,10 +11,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "My.Hr.Business", "My.Hr.Bus EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "My.Hr.Functions", "My.Hr.Functions\My.Hr.Functions.csproj", "{A62BAA55-0737-4671-BF31-89D4BE7C4097}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "My.Hr.Infra", "My.Hr.Infra\My.Hr.Infra.csproj", "{E448EFD6-5CA6-4C71-B575-1149DD7181C6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "My.Hr.Infra.Tests", "My.Hr.Infra.Tests\My.Hr.Infra.Tests.csproj", "{01B0FC8E-738D-47BB-AA57-F880532A501D}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "My.Hr.UnitTest", "My.Hr.UnitTest\My.Hr.UnitTest.csproj", "{EE307518-D5FD-45B3-9A61-4451DFC44835}" EndProject Global @@ -42,14 +38,6 @@ Global {A62BAA55-0737-4671-BF31-89D4BE7C4097}.Debug|Any CPU.Build.0 = Debug|Any CPU {A62BAA55-0737-4671-BF31-89D4BE7C4097}.Release|Any CPU.ActiveCfg = Release|Any CPU {A62BAA55-0737-4671-BF31-89D4BE7C4097}.Release|Any CPU.Build.0 = Release|Any CPU - {E448EFD6-5CA6-4C71-B575-1149DD7181C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E448EFD6-5CA6-4C71-B575-1149DD7181C6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E448EFD6-5CA6-4C71-B575-1149DD7181C6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E448EFD6-5CA6-4C71-B575-1149DD7181C6}.Release|Any CPU.Build.0 = Release|Any CPU - {01B0FC8E-738D-47BB-AA57-F880532A501D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01B0FC8E-738D-47BB-AA57-F880532A501D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01B0FC8E-738D-47BB-AA57-F880532A501D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01B0FC8E-738D-47BB-AA57-F880532A501D}.Release|Any CPU.Build.0 = Release|Any CPU {EE307518-D5FD-45B3-9A61-4451DFC44835}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE307518-D5FD-45B3-9A61-4451DFC44835}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE307518-D5FD-45B3-9A61-4451DFC44835}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Templates/content/Company.AppName.Api/Api.http b/src/Templates/content/Company.AppName.Api/Api.http index 5a49347f..78ce9198 100644 --- a/src/Templates/content/Company.AppName.Api/Api.http +++ b/src/Templates/content/Company.AppName.Api/Api.http @@ -4,7 +4,6 @@ @host = {{scheme}}://{{hostname}}:{{port}} - ### Health endpoint GET {{host}}/api/health @@ -23,36 +22,66 @@ GET {{host}}/api/swagger/ui # todo: add payloads ### Get all Employees GET {{host}}/api/employees -x-correlation-id: 123-my-correlation-id +x-correlation-id: 123-my-correlation-id-getall ### Get Employee -GET {{host}}/api/employees/{id} -x-correlation-id: 123-my-correlation-id +GET {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 +x-correlation-id: 123-my-correlation-id-get ### Create Employee POST {{host}}/api/employees -x-correlation-id: 123-my-correlation-id +x-correlation-id: 123-my-correlation-id-create +Content-Type: application/json + +{ + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "email": "alice@alice.com", + "firstName": "Alice", + "lastName": "Doe", + "gender": "F", + "birthday": "2000-09-28T15:36:55.730Z", + "startDate": "2022-09-01T15:36:55.730Z", + "phoneNo": "765-123-5687" +} ### Update Employee -PUT {{host}}/api/employees/{id} -x-correlation-id: 123-my-correlation-id +# todo: this fails with The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded +PUT {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 +x-correlation-id: 123-my-correlation-id-update +Content-Type: application/json -### Path Employee -PATCH {{host}}/api/employees/{id} +{ + "email": "alice@alice.com", + "firstName": "Alice", + "lastName": "Doe", + "gender": "F", + "birthday": "2000-09-28T15:36:55.730Z", + "startDate": "2022-09-01T15:36:55.730Z", + "phoneNo": "765-123-0000" +} + +### Patch Employee +PATCH {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 x-correlation-id: 123-my-correlation-id +Content-Type: application/json +If-Match: AAAAAAAACBg= + +{ + "phoneNo": "765-123-0000" +} ### Delete Employee -DELETE {{host}}/api/employees/{id} +DELETE {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 x-correlation-id: 123-my-correlation-id ### Employee Verification scenario with service bus POST {{host}}/api/employee/verify Content-Type: application/json -x-correlation-id: 123-my-correlation-id +x-correlation-id: 123-my-correlation-id-verify { "name": "John", "age": 27, "gender": "male" -} +} \ No newline at end of file diff --git a/src/Templates/content/Company.AppName.Functions/Functions.http b/src/Templates/content/Company.AppName.Functions/Functions.http index 94d3c323..c81a7c19 100644 --- a/src/Templates/content/Company.AppName.Functions/Functions.http +++ b/src/Templates/content/Company.AppName.Functions/Functions.http @@ -4,7 +4,6 @@ @host = {{scheme}}://{{hostname}}:{{port}} - ### Health endpoint GET {{host}}/api/health @@ -23,33 +22,63 @@ GET {{host}}/api/swagger/ui # todo: add payloads ### Get all Employees GET {{host}}/api/employees -x-correlation-id: 123-my-correlation-id +x-correlation-id: 123-my-correlation-id-getall ### Get Employee -GET {{host}}/api/employees/{id} -x-correlation-id: 123-my-correlation-id +GET {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 +x-correlation-id: 123-my-correlation-id-get ### Create Employee POST {{host}}/api/employees -x-correlation-id: 123-my-correlation-id +x-correlation-id: 123-my-correlation-id-create +Content-Type: application/json + +{ + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "email": "alice@alice.com", + "firstName": "Alice", + "lastName": "Doe", + "gender": "F", + "birthday": "2000-09-28T15:36:55.730Z", + "startDate": "2022-09-01T15:36:55.730Z", + "phoneNo": "765-123-5687" +} ### Update Employee -PUT {{host}}/api/employees/{id} -x-correlation-id: 123-my-correlation-id +# todo: this fails with The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded +PUT {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 +x-correlation-id: 123-my-correlation-id-update +Content-Type: application/json -### Path Employee -PATCH {{host}}/api/employees/{id} +{ + "email": "alice@alice.com", + "firstName": "Alice", + "lastName": "Doe", + "gender": "F", + "birthday": "2000-09-28T15:36:55.730Z", + "startDate": "2022-09-01T15:36:55.730Z", + "phoneNo": "765-123-0000" +} + +### Patch Employee +PATCH {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 x-correlation-id: 123-my-correlation-id +Content-Type: application/json +If-Match: AAAAAAAACBg= + +{ + "phoneNo": "765-123-0000" +} ### Delete Employee -DELETE {{host}}/api/employees/{id} +DELETE {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 x-correlation-id: 123-my-correlation-id ### Employee Verification scenario with service bus POST {{host}}/api/employee/verify Content-Type: application/json -x-correlation-id: 123-my-correlation-id +x-correlation-id: 123-my-correlation-id-verify { "name": "John", diff --git a/src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs b/src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs index 5f60edc1..d14c6e45 100644 --- a/src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs +++ b/src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs @@ -36,23 +36,23 @@ public EmployeeFunction(WebApi webApi, EmployeeService service, IValidator GetAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/employees/{id}")] HttpRequest request, Guid id) + public Task GetAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "employees/{id}")] HttpRequest request, Guid id) => _webApi.GetAsync(request, _ => _service.GetEmployeeAsync(id)); [FunctionName("GetAll")] [OpenApiOperation(operationId: "GetAll", tags: new[] { "employee" })] [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: MediaTypeNames.Application.Json, bodyType: typeof(List), Description = "Employee records")] - public Task GetAllAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/employees")] HttpRequest request) + public Task GetAllAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "employees")] HttpRequest request) => _webApi.GetAsync(request, p => _service.GetAllAsync(p.RequestOptions.Paging)); [FunctionName("Create")] [OpenApiOperation(operationId: "Create", tags: new[] { "employee" })] [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.Created, Description = "Created employee record")] - public Task CreateAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "api/employees")] HttpRequest request) + public Task CreateAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employees")] HttpRequest request) => _webApi.PostAsync(request, p => _service.AddEmployeeAsync(p.Value!), - statusCode: HttpStatusCode.Created, validator: _validator, locationUri: e => new Uri($"api/employees/{e.Id}", UriKind.RelativeOrAbsolute)); + statusCode: HttpStatusCode.Created, validator: _validator, locationUri: e => new Uri($"employees/{e.Id}", UriKind.RelativeOrAbsolute)); [FunctionName("Update")] [OpenApiOperation(operationId: "Update", tags: new[] { "employee" })] @@ -60,14 +60,14 @@ public Task CreateAsync([HttpTrigger(AuthorizationLevel.Function, [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: MediaTypeNames.Application.Json, bodyType: typeof(Employee), Description = "Employee record")] [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NotFound, Description = "Not found")] - public Task UpdateAsync([HttpTrigger(AuthorizationLevel.Function, "put", Route = "api/employees/{id}")] HttpRequest request, Guid id) + public Task UpdateAsync([HttpTrigger(AuthorizationLevel.Function, "put", Route = "employees/{id}")] HttpRequest request, Guid id) => _webApi.PutAsync(request, p => _service.UpdateEmployeeAsync(p.Value!, id), validator: _validator); [FunctionName("Patch")] - public Task PatchAsync([HttpTrigger(AuthorizationLevel.Function, "patch", Route = "api/employees/{id}")] HttpRequest request, Guid id) + public Task PatchAsync([HttpTrigger(AuthorizationLevel.Function, "patch", Route = "employees/{id}")] HttpRequest request, Guid id) => _webApi.PatchAsync(request, get: _ => _service.GetEmployeeAsync(id), put: p => _service.UpdateEmployeeAsync(p.Value!, id), validator: _validator); [FunctionName("Delete")] - public Task DeleteAsync([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "api/employees/{id}")] HttpRequest request, Guid id) + public Task DeleteAsync([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "employees/{id}")] HttpRequest request, Guid id) => _webApi.DeleteAsync(request, _ => _service.DeleteEmployeeAsync(id)); } diff --git a/src/Templates/content/Company.AppName.Infra/Readme.md b/src/Templates/content/Company.AppName.Infra/Readme.md index 9ca64930..66834cf2 100644 --- a/src/Templates/content/Company.AppName.Infra/Readme.md +++ b/src/Templates/content/Company.AppName.Infra/Readme.md @@ -21,10 +21,11 @@ Pulumi can be used without Pulumi Account, by using [Azure Storage as backend](h ## Configuring Pulumi (optional) -Infrastructure project has only 2 settings: +Infrastructure project has only few settings: * `Company.AppName.Infra:isAppsDeploymentEnabled` for controlling application deployment via zip deploy * `Company.AppName.Infra:isDBSchemaDeploymentEnabled` for publishing Database schema and data +* `Company.AppName.Infra:developerEmails` comma separated list of developer team emails that will get access to created resources > When `isAppsDeploymentEnabled` flag is set, pulumi code executes `dotnet publish -c RELEASE` to create app packages. @@ -41,8 +42,31 @@ config: azure-native:location: EastUs Company.AppName.Infra:isAppsDeploymentEnabled: true Company.AppName.Infra:isDBSchemaDeploymentEnabled: true + Company.AppName.Infra:developerEmails: "bob@mycustomad.onmicrosoft.com, alice@mycustomad.onmicrosoft.com" ``` +### Note on best practices + +Infrastructure project has built-in ability to deploy application code and database schema, in real-life scenarios those operations should be separated out. Code will most likely be deployed more often than infrastructure piece, with additional options for tagging, versioning etc. + +It's also important to keep infrastructure project up to date and deploy it often. Pulumi state change analysis is quick and should not add a lot to deployment time. + +## Infrastructure deployed + +Pulumi creates full stack infrastructure designed for production. Resources deployed include: + +* Storage account with RBAC enabled for app service and function app managed identities +* App Service Plan +* Log analytics workspace and Application Insights +* SQL Server with SQL Database enabled for Azure AD access with permissions setup for app service and function app managed identities +* Service Bus with queues and topics and permissions setup for app service and function app managed identities +* Function App and App Service +* Azure AD group for developer access + +> **Considerations for enhancing security pasture** +> +> Networking stack can be enhanced with private networking capabilities - private endpoints, service endpoints. Tunneling traffic via API Management with WAF, cross region replication and more. + ## Deploy with Pulumi To deploy in `samples/Company.AppName/Company.AppName.Infra` run `pulumi up -c azure-native:location=EastUs -c Company.AppName.Infra:isAppsDeploymentEnabled=true -c Company.AppName.Infra:isDBSchemaDeploymentEnabled=true` diff --git a/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs b/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs index 37ee9407..a3d0c870 100644 --- a/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs +++ b/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs @@ -28,7 +28,7 @@ public static async Task CreateConfiguration() // get some info from Azure AD var domainResult = await AD.GetDomains.InvokeAsync(new AD.GetDomainsArgs { OnlyDefault = true }); - var defaultUsername = $"sqlGlobalAdAdmin{Pulumi.Deployment.Instance.StackName}@{domainResult.Domains[0].DomainName}"; + var defaultUsername = $"sqlGlobalAdAdminCompanyAppName{Pulumi.Deployment.Instance.StackName}@{domainResult.Domains[0].DomainName}"; var defaultPassword = new Pulumi.Random.RandomPassword("sqlAdPassword", new() { Length = 32, @@ -43,7 +43,6 @@ public static async Task CreateConfiguration() }).Result; Log.Info($"Default username is: {defaultUsername}"); - Log.Info($"developerEmails: {config.Get("developerEmails")}"); return new StackConfiguration { diff --git a/src/Templates/content/docker-compose.DB.only.yml b/src/Templates/content/docker-compose.DB.only.yml new file mode 100644 index 00000000..647070c6 --- /dev/null +++ b/src/Templates/content/docker-compose.DB.only.yml @@ -0,0 +1,22 @@ +version: '3.4' + +# The default docker-compose.override file can use the "localhost" as the external name for testing web apps within the same dev machine. +# The ESHOP_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: +# ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost +# but values present in the environment vars at runtime will always override those defined inside the .env file +# An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. + +services: + + sqldata: + + app-api: + entrypoint: ["echo", "Service api disabled"] + + app-functions: + entrypoint: ["echo", "Service functions disabled"] + + +volumes: + app-sqldata: + external: false From a6f763264510da0944bb1236030a7c4a054fe63b Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 12:20:08 -0400 Subject: [PATCH 18/39] fixing rename that happend by accident Signed-off-by: Piotr --- samples/My.Hr/My.Hr.Api/Startup.cs | 2 +- samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs | 4 ++-- samples/My.Hr/My.Hr.Functions/Startup.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/My.Hr/My.Hr.Api/Startup.cs b/samples/My.Hr/My.Hr.Api/Startup.cs index 1e8320d8..e2ac1079 100644 --- a/samples/My.Hr/My.Hr.Api/Startup.cs +++ b/samples/My.Hr/My.Hr.Api/Startup.cs @@ -41,7 +41,7 @@ public void ConfigureServices(IServiceCollection services) // Register the database and EF services, including required AutoMapper. services.AddDatabase(sp => new HrDb(sp.GetRequiredService())) - .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) + .AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())) .AddScoped() .AddAutoMapper(typeof(HrEfDb).Assembly) .AddAutoMapperWrapper(); diff --git a/samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs b/samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs index 41938b99..440f4d68 100644 --- a/samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs +++ b/samples/My.Hr/My.Hr.Business/Data/HrEfDb.cs @@ -16,14 +16,14 @@ public interface IHrEfDb : IEfDb /// /// Represents the My.Hr database using Entity Framework. /// - public class HrEfDb : EfDb, IHrEfDb + public class HrEfDb : EfDb, IHrEfDb { /// /// Initializes a new instance of the class. /// /// The entity framework database context. /// The . - public HrEfDb(AppNameDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } + public HrEfDb(HrDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { } /// /// Gets the encapsulated entity. diff --git a/samples/My.Hr/My.Hr.Functions/Startup.cs b/samples/My.Hr/My.Hr.Functions/Startup.cs index 5897cf11..64315403 100644 --- a/samples/My.Hr/My.Hr.Functions/Startup.cs +++ b/samples/My.Hr/My.Hr.Functions/Startup.cs @@ -69,7 +69,7 @@ public override void Configure(IFunctionsHostBuilder builder) // Database builder.Services.AddDatabase(sp => new HrDb(sp.GetRequiredService())); - builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); + builder.Services.AddDbContext((sp, o) => o.UseSqlServer(sp.GetRequiredService().GetConnection())); } catch (System.Exception ex) { From 813cfcd1c14a0144fdef882d5f89d0823873a08b Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 12:23:59 -0400 Subject: [PATCH 19/39] fixing some comments Signed-off-by: Piotr --- docker-compose.myHr.override.yml | 6 ------ src/Templates/content/.devcontainer/docker-compose.yml | 8 +------- src/Templates/content/docker-compose.DB.only.yml | 6 ------ src/Templates/content/docker-compose.override.yml | 6 ------ 4 files changed, 1 insertion(+), 25 deletions(-) diff --git a/docker-compose.myHr.override.yml b/docker-compose.myHr.override.yml index 3f0b1d66..2df2ac3e 100644 --- a/docker-compose.myHr.override.yml +++ b/docker-compose.myHr.override.yml @@ -1,11 +1,5 @@ version: '3.4' -# The default docker-compose.override file can use the "localhost" as the external name for testing web apps within the same dev machine. -# The ESHOP_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: -# ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost -# but values present in the environment vars at runtime will always override those defined inside the .env file -# An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. - services: sqldata: diff --git a/src/Templates/content/.devcontainer/docker-compose.yml b/src/Templates/content/.devcontainer/docker-compose.yml index 3425780d..6db685cc 100644 --- a/src/Templates/content/.devcontainer/docker-compose.yml +++ b/src/Templates/content/.devcontainer/docker-compose.yml @@ -42,13 +42,7 @@ services: volumes: - app-sqldata:/var/opt/mssql - # Uncomment to change startup options - # environment: - # MONGO_INITDB_ROOT_USERNAME: root - # MONGO_INITDB_ROOT_PASSWORD: example - # MONGO_INITDB_DATABASE: your-database-here - - # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. + # Add "forwardPorts": ["1433"] to **devcontainer.json** to forward SQL locally. # (Adding the "ports" property to this file will not forward from a Codespace.) volumes: diff --git a/src/Templates/content/docker-compose.DB.only.yml b/src/Templates/content/docker-compose.DB.only.yml index 647070c6..134f9e6a 100644 --- a/src/Templates/content/docker-compose.DB.only.yml +++ b/src/Templates/content/docker-compose.DB.only.yml @@ -1,11 +1,5 @@ version: '3.4' -# The default docker-compose.override file can use the "localhost" as the external name for testing web apps within the same dev machine. -# The ESHOP_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: -# ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost -# but values present in the environment vars at runtime will always override those defined inside the .env file -# An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. - services: sqldata: diff --git a/src/Templates/content/docker-compose.override.yml b/src/Templates/content/docker-compose.override.yml index a30fcaae..494d8b21 100644 --- a/src/Templates/content/docker-compose.override.yml +++ b/src/Templates/content/docker-compose.override.yml @@ -1,11 +1,5 @@ version: '3.4' -# The default docker-compose.override file can use the "localhost" as the external name for testing web apps within the same dev machine. -# The ESHOP_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: -# ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost -# but values present in the environment vars at runtime will always override those defined inside the .env file -# An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. - services: sqldata: From 2819e08e2f43f840dae61550ebb7d85ba1a69d0f Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 18:07:25 -0400 Subject: [PATCH 20/39] fixign tests Signed-off-by: Piotr --- CoreEx.sln | 17 ---- samples/My.Hr/My.Hr.Api/Dockerfile | 2 - samples/My.Hr/My.Hr.Database/Dockerfile | 2 - samples/My.Hr/My.Hr.Functions/Dockerfile | 2 - .../content/.azuredevops/app-template.yml | 53 ++++++++++++ .../.azuredevops/function-template.yml | 76 +++++++++++++++++ .../content/.azuredevops/infra-template.yml | 61 +++++++++++++ .../.azuredevops/pipeline-build copy.yml | 85 +++++++++++++++++++ .../content/.azuredevops/pipeline-build.yml | 72 ++++++++++++++++ .../.azuredevops/pipeline-release copy.yml | 58 +++++++++++++ .../content/.azuredevops/pipeline-release.yml | 69 +++++++++++++++ .../pull_request_template/branches/main.md | 27 ++++++ .../pull_request_template/branches/preprod.md | 27 ++++++ .../pull_request_template/branches/test.md | 27 ++++++ .../pull_request_template.md | 7 ++ .../content/Company.AppName.Api/Api.http | 2 +- .../Company.AppName.Functions/Functions.http | 1 + src/Templates/readme.md | 2 +- 18 files changed, 565 insertions(+), 25 deletions(-) create mode 100644 src/Templates/content/.azuredevops/app-template.yml create mode 100644 src/Templates/content/.azuredevops/function-template.yml create mode 100644 src/Templates/content/.azuredevops/infra-template.yml create mode 100644 src/Templates/content/.azuredevops/pipeline-build copy.yml create mode 100644 src/Templates/content/.azuredevops/pipeline-build.yml create mode 100644 src/Templates/content/.azuredevops/pipeline-release copy.yml create mode 100644 src/Templates/content/.azuredevops/pipeline-release.yml create mode 100644 src/Templates/content/.azuredevops/pull_request_template/branches/main.md create mode 100644 src/Templates/content/.azuredevops/pull_request_template/branches/preprod.md create mode 100644 src/Templates/content/.azuredevops/pull_request_template/branches/test.md create mode 100644 src/Templates/content/.azuredevops/pull_request_template/pull_request_template.md diff --git a/CoreEx.sln b/CoreEx.sln index 05d54c85..0c5b89a1 100644 --- a/CoreEx.sln +++ b/CoreEx.sln @@ -59,12 +59,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreEx.Cosmos", "src\CoreEx EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreEx.Cosmos.Test", "tests\CoreEx.Cosmos.Test\CoreEx.Cosmos.Test.csproj", "{C8021CF0-006F-427C-827F-B997F26E5FF6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "My.Hr.Infra.Tests", "samples\My.Hr\My.Hr.Infra.Tests\My.Hr.Infra.Tests.csproj", "{7DA61666-8109-4B0C-8433-EDA87370DA28}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "My.Hr.Infra", "samples\My.Hr\My.Hr.Infra\My.Hr.Infra.csproj", "{3601F5D1-AFAC-417B-AC9F-1E4260148B22}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infra", "Infra", "{601379B6-8FF6-4272-884C-473693BE0E90}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -143,14 +137,6 @@ Global {C8021CF0-006F-427C-827F-B997F26E5FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8021CF0-006F-427C-827F-B997F26E5FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8021CF0-006F-427C-827F-B997F26E5FF6}.Release|Any CPU.Build.0 = Release|Any CPU - {7DA61666-8109-4B0C-8433-EDA87370DA28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7DA61666-8109-4B0C-8433-EDA87370DA28}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7DA61666-8109-4B0C-8433-EDA87370DA28}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7DA61666-8109-4B0C-8433-EDA87370DA28}.Release|Any CPU.Build.0 = Release|Any CPU - {3601F5D1-AFAC-417B-AC9F-1E4260148B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3601F5D1-AFAC-417B-AC9F-1E4260148B22}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3601F5D1-AFAC-417B-AC9F-1E4260148B22}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3601F5D1-AFAC-417B-AC9F-1E4260148B22}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -175,9 +161,6 @@ Global {F3384ADC-1DA8-4538-B991-DBD2BC591AF1} = {4B6BC31E-93B1-42B0-AE09-AD85AC4DB657} {8D0CC3FD-65C2-4302-99F4-A90AC7680E1B} = {4B6BC31E-93B1-42B0-AE09-AD85AC4DB657} {C8021CF0-006F-427C-827F-B997F26E5FF6} = {3145DCB9-98FB-4519-BCC0-75A22A252EDC} - {7DA61666-8109-4B0C-8433-EDA87370DA28} = {601379B6-8FF6-4272-884C-473693BE0E90} - {3601F5D1-AFAC-417B-AC9F-1E4260148B22} = {601379B6-8FF6-4272-884C-473693BE0E90} - {601379B6-8FF6-4272-884C-473693BE0E90} = {F53B0E83-87F8-4679-94B8-268FE6A9C0AD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B4566D2-9B22-4E27-9654-402BDBA6C744} diff --git a/samples/My.Hr/My.Hr.Api/Dockerfile b/samples/My.Hr/My.Hr.Api/Dockerfile index 3a50f56f..4406f788 100644 --- a/samples/My.Hr/My.Hr.Api/Dockerfile +++ b/samples/My.Hr/My.Hr.Api/Dockerfile @@ -14,8 +14,6 @@ COPY "samples/My.Hr/My.Hr.Business/My.Hr.Business.csproj" "samples/My.Hr/My.Hr.B COPY "samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj" "samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj" COPY "samples/My.Hr/My.Hr.Functions/My.Hr.Functions.csproj" "samples/My.Hr/My.Hr.Functions/My.Hr.Functions.csproj" COPY "samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj" "samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj" -COPY "samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj" "samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj" -COPY "samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj" "samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj" COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" diff --git a/samples/My.Hr/My.Hr.Database/Dockerfile b/samples/My.Hr/My.Hr.Database/Dockerfile index 30af452c..18b0564a 100644 --- a/samples/My.Hr/My.Hr.Database/Dockerfile +++ b/samples/My.Hr/My.Hr.Database/Dockerfile @@ -18,8 +18,6 @@ COPY "samples/My.Hr/My.Hr.Business/My.Hr.Business.csproj" "samples/My.Hr/My.Hr.B COPY "samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj" "samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj" COPY "samples/My.Hr/My.Hr.Functions/My.Hr.Functions.csproj" "samples/My.Hr/My.Hr.Functions/My.Hr.Functions.csproj" COPY "samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj" "samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj" -COPY "samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj" "samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj" -COPY "samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj" "samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj" COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" diff --git a/samples/My.Hr/My.Hr.Functions/Dockerfile b/samples/My.Hr/My.Hr.Functions/Dockerfile index 5721d057..3c8acc8e 100644 --- a/samples/My.Hr/My.Hr.Functions/Dockerfile +++ b/samples/My.Hr/My.Hr.Functions/Dockerfile @@ -15,8 +15,6 @@ COPY "samples/My.Hr/My.Hr.Business/My.Hr.Business.csproj" "samples/My.Hr/My.Hr.B COPY "samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj" "samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj" COPY "samples/My.Hr/My.Hr.Functions/My.Hr.Functions.csproj" "samples/My.Hr/My.Hr.Functions/My.Hr.Functions.csproj" COPY "samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj" "samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj" -COPY "samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj" "samples/My.Hr/My.Hr.Infra/My.Hr.Infra.csproj" -COPY "samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj" "samples/My.Hr/My.Hr.Infra.Tests/My.Hr.Infra.Tests.csproj" COPY "src/CoreEx/CoreEx.csproj" "src/CoreEx/CoreEx.csproj" COPY "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" "src/CoreEx.AutoMapper/CoreEx.AutoMapper.csproj" diff --git a/src/Templates/content/.azuredevops/app-template.yml b/src/Templates/content/.azuredevops/app-template.yml new file mode 100644 index 00000000..9773edb0 --- /dev/null +++ b/src/Templates/content/.azuredevops/app-template.yml @@ -0,0 +1,53 @@ +parameters: +- name: AzureSubscription + type: string +- name: env + type: string + displayName: 'Environment shorthand - dev, test, etc.' +- name: ado_environment + type: string + displayName: 'Name of ADO environment to deploy to' +- name: AppSrvName + type: string + displayName: 'Name of azure app service to deploy to' + + +jobs: +- deployment: Deploy + displayName: "Deploy ${{ parameters.env }}" + environment: ${{ parameters.ado_environment }} + variables: + - name: global_buildDate + value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] + - group: VG-D365-${{ parameters.env }} + pool: + vmImage: $(vmImageName) + strategy: + runOnce: + deploy: + steps: + - task: AzureWebApp@1 + displayName: 'Deployment app ${{ parameters.AppSrvName }} to env: ${{ parameters.env }}' + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + appType: 'webApp' + appName: '${{ parameters.AppSrvName }}' + deploymentMethod: zipDeploy + package: '$(Pipeline.Workspace)/**/Nte.Int.D365.Employee.ScimApp.zip' + appSettings: '-Deployment.By "$(Build.RequestedForEmail)" + -Deployment.Build "$(resources.pipeline.D365-Employee.runName)" + -Deployment.Name "$(Build.BuildNumber)" + -Deployment.Version "$(resources.pipeline.D365-Employee.sourceBranch)-$(resources.pipeline.D365-Employee.sourceCommit)" + -Deployment.Date "$(global_buildDate)" + -AppConfigConnectionString "$(AppConfigConnectionString)" + -ApplicationName "D365-Employee"' + + - task: AzureCLI@2 + displayName: Tag Azure + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + scriptType: ps + scriptLocation: inlineScript + inlineScript: | + $fun_id=$(az resource show --name ${{ parameters.AppSrvName }} -g $(ResourceGroupName) --resource-type "Microsoft.Web/sites" --query id --output tsv) + az tag update --resource-id $fun_id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.D365-Employee.runName)"' 'Deployment.Version="$(resources.pipeline.D365-Employee.sourceBranch)-$(resources.pipeline.D365-Employee.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' diff --git a/src/Templates/content/.azuredevops/function-template.yml b/src/Templates/content/.azuredevops/function-template.yml new file mode 100644 index 00000000..ae59a437 --- /dev/null +++ b/src/Templates/content/.azuredevops/function-template.yml @@ -0,0 +1,76 @@ +parameters: +- name: AzureSubscription + type: string +- name: env + type: string + displayName: 'Environment shorthand - dev, test, etc.' +- name: ado_environment + type: string + displayName: 'Name of ADO environment to deploy to' +- name: FunctionAppName + type: string + displayName: 'Name of azure function to deploy to' +- name: MovementJournalQueueName + type: string + displayName: 'Movement Journal Queue Name' +- name: GoogleInventoryFullFeedTimer + type: string + displayName: 'Google Inventory Full Feed Timer' +# - name: PricingSubscriberQueueName +# type: string +# displayName: 'Pricing Subscriber Queue Name' +# - name: AppsQueueName +# type: string +# displayName: 'Name of ADO environment to deploy to' + +jobs: +- deployment: Deploy + displayName: "Deploy ${{ parameters.env }}" + environment: ${{ parameters.ado_environment }} + variables: + - name: global_buildDate + value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] + - group: VG-D365-${{ parameters.env }} + pool: + vmImage: $(vmImageName) + strategy: + runOnce: + deploy: + steps: + + - task: AzureFunctionApp@1 + displayName: 'Deployment az fun ${{ parameters.FunctionAppName }} to env: ${{ parameters.env }}' + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + appType: 'functionApp' + appName: '${{ parameters.FunctionAppName }}' + # deployToSlotOrASE: true + resourceGroupName: '$(ResourceGroupName)' + #slotName: '$(DeploymentSlot)' + package: '$(Pipeline.Workspace)/D365-Apps/D365-Apps_Package/Nte.Int.D365.Apps.FunctionApps.zip' + deploymentMethod: 'auto' + appSettings: '-Deployment.By "$(Build.RequestedForEmail)" + -Deployment.Build "$(resources.pipeline.D365-Apps.runName)" + -Deployment.Name "$(Build.BuildNumber)" + -Deployment.Version "$(resources.pipeline.D365-Apps.sourceBranch)-$(resources.pipeline.D365-Apps.sourceCommit)" + -Deployment.Date "$(global_buildDate)" + + -ApplicationName "D365-Apps" + -MovementJournalQueueName "${{parameters.MovementJournalQueueName}}" + -GoogleInventoryFullFeedTimer "${{parameters.GoogleInventoryFullFeedTimer}}" + -AppConfigConnectionString "$(AppConfigConnectionString)" + -ServiceBusConnection__fullyQualifiedNamespace "$(ServiceBusConnection__fullyQualifiedNamespace)" + -Publisher_ServiceBusConnection "$(ServiceBusConnection__fullyQualifiedNamespace)" + -AzureFunctionsJobHost__logging__logLevel__default "Warning" + -AzureFunctionsJobHost__logging__logLevel__Nte__Int__D365__Common "Information"' + + - task: AzureCLI@2 + displayName: Tag Azure + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + scriptType: ps + scriptLocation: inlineScript + inlineScript: | + $fun_id=$(az resource show --name ${{ parameters.FunctionAppName }} -g $(ResourceGroupName) --resource-type "Microsoft.Web/sites" --query id --output tsv) + az tag update --resource-id $fun_id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.D365-Apps.runName)"' 'Deployment.Version="$(resources.pipeline.D365-Apps.sourceBranch)-$(resources.pipeline.D365-Apps.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' + diff --git a/src/Templates/content/.azuredevops/infra-template.yml b/src/Templates/content/.azuredevops/infra-template.yml new file mode 100644 index 00000000..52d74e3e --- /dev/null +++ b/src/Templates/content/.azuredevops/infra-template.yml @@ -0,0 +1,61 @@ +parameters: +- name: AzureSubscription + type: string +- name: env + type: string + displayName: 'Environment shorthand - dev, test, etc.' +- name: ado_environment + type: string + displayName: 'Name of ADO environment to deploy to' +- name: AppSrvName + type: string + displayName: 'Name of azure app service to deploy to' + + +jobs: +- deployment: Deploy + displayName: "Deploy ${{ parameters.env }}" + environment: ${{ parameters.ado_environment }} + variables: + - name: global_buildDate + value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] + - group: VG-D365-${{ parameters.env }} + pool: + vmImage: $(vmImageName) + strategy: + runOnce: + deploy: + steps: + - task: Bash@3 + displayName: 'Install Pulumi' + inputs: + targetType: 'inline' + script: | + curl -fsSL https://get.pulumi.com | sh + # export PATH="${PATH}:/root/.pulumi/bin" + echo '##vso[task.prependpath]/root/.pulumi/bin' + - task: AzureWebApp@1 + displayName: 'Deployment app ${{ parameters.AppSrvName }} to env: ${{ parameters.env }}' + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + appType: 'webApp' + appName: '${{ parameters.AppSrvName }}' + deploymentMethod: zipDeploy + package: '$(Pipeline.Workspace)/**/Nte.Int.D365.Employee.ScimApp.zip' + appSettings: '-Deployment.By "$(Build.RequestedForEmail)" + -Deployment.Build "$(resources.pipeline.D365-Employee.runName)" + -Deployment.Name "$(Build.BuildNumber)" + -Deployment.Version "$(resources.pipeline.D365-Employee.sourceBranch)-$(resources.pipeline.D365-Employee.sourceCommit)" + -Deployment.Date "$(global_buildDate)" + -AppConfigConnectionString "$(AppConfigConnectionString)" + -ApplicationName "D365-Employee"' + + - task: AzureCLI@2 + displayName: Tag Azure + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + scriptType: ps + scriptLocation: inlineScript + inlineScript: | + $fun_id=$(az resource show --name ${{ parameters.AppSrvName }} -g $(ResourceGroupName) --resource-type "Microsoft.Web/sites" --query id --output tsv) + az tag update --resource-id $fun_id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.D365-Employee.runName)"' 'Deployment.Version="$(resources.pipeline.D365-Employee.sourceBranch)-$(resources.pipeline.D365-Employee.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' diff --git a/src/Templates/content/.azuredevops/pipeline-build copy.yml b/src/Templates/content/.azuredevops/pipeline-build copy.yml new file mode 100644 index 00000000..ea9ddf32 --- /dev/null +++ b/src/Templates/content/.azuredevops/pipeline-build copy.yml @@ -0,0 +1,85 @@ +name: $(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +pool: + vmImage: windows-latest + +variables: + buildConfiguration: 'Release' + +trigger: + branches: + include: + - develop + - main + - test + - preprod + +steps: +- task: CopyFiles@2 + displayName: Copy Files + inputs: + contents: $(Build.Repository.LocalPath)/** + targetFolder: $(Build.ArtifactStagingDirectory) + +- task: NuGetAuthenticate@0 + +- task: NuGetToolInstaller@1 + displayName: 'NuGet tool installer' + +- task: Cache@2 + continueOnError: true + displayName: 'NuGet Cache' + inputs: + key: 'nuget | "$(Agent.OS)" | **/packages.lock.json' + path: '' + cacheHitVar: 'CACHE_RESTORED' + +- task: DotNetCoreCLI@2 + displayName: Restore + condition: ne(variables.CACHE_RESTORED, true) + inputs: + command: 'restore' + projects: '**/source/*.sln' + feedsToUse: 'select' + vstsFeed: '5c1bf9a9-e22e-4f76-9993-1a6b447130f8/c1502c9b-c232-4f35-8045-b01575cce6bc' + +- task: DotNetCoreCLI@2 + displayName: Build + inputs: + command: build + projects: '**/source/*.sln' + arguments: '--configuration $(buildConfiguration)' + +- task: DotNetCoreCLI@2 + displayName: 'Install .NET tools from local manifest' + inputs: + command: custom + custom: tool + arguments: 'restore' + +- task: DotNetCoreCLI@2 + displayName: 'Run unit tests - $(buildConfiguration)' + inputs: + command: 'test' + arguments: '--no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' + publishTestResults: true + projects: '**/*.UnitTests.csproj' + +- task: PublishCodeCoverageResults@1 + displayName: 'Publish code coverage report' + inputs: + codeCoverageTool: 'Cobertura' + summaryFileLocation: '$(Build.SourcesDirectory)/TestResults/Coverage/**/coverage.cobertura.xml' + +- task: DotNetCoreCLI@2 + inputs: + command: 'publish' + projects: '**/source/Nte.Int.D365.Employee.ScimApp/Nte.Int.D365.Employee.ScimApp.csproj' + publishWebProjects: false + arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' + +- task: PublishBuildArtifacts@1 + inputs: + pathToPublish: '$(Build.ArtifactStagingDirectory)/Nte.Int.D365.Employee.ScimApp.zip' + artifactName: D365-Employee-$(Build.BuildId) + diff --git a/src/Templates/content/.azuredevops/pipeline-build.yml b/src/Templates/content/.azuredevops/pipeline-build.yml new file mode 100644 index 00000000..629beca8 --- /dev/null +++ b/src/Templates/content/.azuredevops/pipeline-build.yml @@ -0,0 +1,72 @@ +name: $(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +pool: + vmImage: windows-latest + +variables: + buildConfiguration: 'Release' + +trigger: + branches: + include: + - develop + - main + - test + - preprod + +steps: +- task: CopyFiles@2 + displayName: Copy Files + inputs: + contents: $(Build.Repository.LocalPath)/** + targetFolder: $(Build.ArtifactStagingDirectory) + +- task: NuGetAuthenticate@0 + +- task: DotNetCoreCLI@2 + displayName: Restore + inputs: + command: 'restore' + projects: '**/source/*.sln' + feedsToUse: 'select' + vstsFeed: '5c1bf9a9-e22e-4f76-9993-1a6b447130f8/c1502c9b-c232-4f35-8045-b01575cce6bc' + +- task: DotNetCoreCLI@2 + displayName: Build + inputs: + command: build + projects: '**/source/*.sln' + arguments: '--configuration $(buildConfiguration)' + +# - task: DotNetCoreCLI@2 +# displayName: 'Install .NET tools from local manifest' +# inputs: +# command: custom +# custom: tool +# arguments: 'restore' + +- task: DotNetCoreCLI@2 + displayName: 'Run unit tests - $(buildConfiguration)' + inputs: + command: 'test' + arguments: '--no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' + publishTestResults: true + projects: '**/*.UnitTests.csproj' + +- task: PublishCodeCoverageResults@1 + displayName: 'Publish code coverage report' + inputs: + codeCoverageTool: 'Cobertura' + summaryFileLocation: '$(Build.SourcesDirectory)/TestResults/Coverage/**/coverage.cobertura.xml' + +- task: DotNetCoreCLI@2 + inputs: + command: 'publish' + projects: '**/source/Nte.Int.D365.Apps.FunctionApps/Nte.Int.D365.Apps.FunctionApps.csproj' + publishWebProjects: false + arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' + +- task: PublishBuildArtifacts@1 + inputs: + pathToPublish: '$(Build.ArtifactStagingDirectory)/Nte.Int.D365.Apps.FunctionApps.zip' + artifactName: D365-Apps_Package diff --git a/src/Templates/content/.azuredevops/pipeline-release copy.yml b/src/Templates/content/.azuredevops/pipeline-release copy.yml new file mode 100644 index 00000000..4e3fb9c6 --- /dev/null +++ b/src/Templates/content/.azuredevops/pipeline-release copy.yml @@ -0,0 +1,58 @@ +variables: +- name: vmImageName + value: 'windows-latest' + +# Explicitly set none for repository trigger +trigger: +- none + +resources: + pipelines: + - pipeline: 'D365-Employee' # Name of the pipeline resource + source: 'D365-Employee' # Name of the triggering pipeline + trigger: + branches: + - develop + - test + - preprod + - main + +stages: +- stage: Dev + jobs: + - template: app-template.yml + parameters: + AzureSubscription: rg-priv-inte-dev-nte-na-01 + env: dev + ado_environment: development + AppSrvName: appsvc-priv-inte-dataload-dev-nte-na-01 + +- stage: Test + condition: or(eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/test'), eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/preprod'), eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/main')) + jobs: + - template: app-template.yml + parameters: + AzureSubscription: rg-priv-inte-test-nte-na-01 + env: test + ado_environment: D365-test + AppSrvName: appsvc-priv-inte-dataload-test-nte-na-01 + +- stage: PreProd + condition: or(eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/preprod'), eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/main')) + jobs: + - template: app-template.yml + parameters: + AzureSubscription: rg-priv-inte-preprod-nte-na-01 + env: preprod + ado_environment: D365-preprod + AppSrvName: appsvc-priv-inte-dataload-preprod-nte-na-01 + +- stage: Prod + condition: eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/main') + jobs: + - template: app-template.yml + parameters: + AzureSubscription: rg-priv-inte-prod-nte-na-01 + env: prod + ado_environment: D365-prod + AppSrvName: appsvcpl-priv-inte-prod-nte-na-01 diff --git a/src/Templates/content/.azuredevops/pipeline-release.yml b/src/Templates/content/.azuredevops/pipeline-release.yml new file mode 100644 index 00000000..56108fa3 --- /dev/null +++ b/src/Templates/content/.azuredevops/pipeline-release.yml @@ -0,0 +1,69 @@ +name: D365-Apps-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +variables: +- name: vmImageName + value: 'windows-latest' + +# Explicitly set none for repository trigger +trigger: none + +resources: + pipelines: + - pipeline: 'D365-Apps' # Name of the pipeline resource + source: 'D365-Apps' # Name of the triggering pipeline + trigger: + branches: + - develop + - test + - preprod + - main + +# Note: Azure Service connection passed as param due to: https://developercommunity.visualstudio.com/t/using-a-variable-for-the-service-connection-result/676259 + +stages: +- stage: Dev + jobs: + - template: function-template.yml + parameters: + AzureSubscription: rg-priv-inte-dev-nte-na-01 + env: dev + ado_environment: development + FunctionAppName: fnc-priv-inte-dev-nte-na-34 + MovementJournalQueueName: d365-apps-movementjournal + GoogleInventoryFullFeedTimer: 0 */10 * * * * + +- stage: Test + condition: or(eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/test'), eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/preprod'), eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/main')) + jobs: + - template: function-template.yml + parameters: + AzureSubscription: rg-priv-inte-test-nte-na-01 + env: test + ado_environment: D365-test + FunctionAppName: fnc-priv-inte-test-nte-na-34 + MovementJournalQueueName: d365-apps-movementjournal + GoogleInventoryFullFeedTimer: 0 0 * * * * + +- stage: Preprod + condition: or(eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/preprod'), eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/main')) + jobs: + - template: function-template.yml + parameters: + env: preprod + AzureSubscription: rg-priv-inte-preprod-nte-na-01 + ado_environment: D365-preprod + FunctionAppName: fnc-priv-inte-preprod-nte-na-34 + MovementJournalQueueName: d365-apps-movementjournal + GoogleInventoryFullFeedTimer: 0 0 * * * * + +- stage: Prod + condition: eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/main') + jobs: + - template: function-template.yml + parameters: + env: prod + AzureSubscription: rg-priv-inte-prod-nte-na-01 + ado_environment: D365-prod + FunctionAppName: fnc-priv-inte-prod-nte-na-34 + MovementJournalQueueName: d365-apps-movementjournal + GoogleInventoryFullFeedTimer: 0 0 0 * * * \ No newline at end of file diff --git a/src/Templates/content/.azuredevops/pull_request_template/branches/main.md b/src/Templates/content/.azuredevops/pull_request_template/branches/main.md new file mode 100644 index 00000000..b80ff458 --- /dev/null +++ b/src/Templates/content/.azuredevops/pull_request_template/branches/main.md @@ -0,0 +1,27 @@ +# Note + +> This PR will commit changes to the "MAIN" branch, which is deployed to PRODUCTION environment. +> Required approvers will automatically by added by ADO to Pull Request targeting test/preprod/main branches. + +## Summary of the Issue + +[The summary highlights the defect and observed failure. If the bug is recorded in ADO or there is a ticket, then provide the link. Here are a few examples:] + +1. *DO order from EFG order entry are getting created for orders that are rejected by Secura for fraud* +2. *GFS orders rejected by the store are not being updated to RE hold code* + +## Issue Details + +**Root Cause/Fix details**: [Outline the Root Cause/fix that was done to address the problem statement above.] + +**Test plan**: [Outline a high-level test plan to validate the issue. If there is a test plan already in ADO, then please share the link.] + +**Impact to other functions**: [List any other functional area(if any) that could be impacted with the code fix.] + +## Tips + +Here are some tips: + +* Use plain language. +* Make it simple, clear, and easily understandable. Don’t load with heavy technical terms. +* Keep the release notes short and precise. Be precise and include only the most relevant and important information. diff --git a/src/Templates/content/.azuredevops/pull_request_template/branches/preprod.md b/src/Templates/content/.azuredevops/pull_request_template/branches/preprod.md new file mode 100644 index 00000000..f28c2c5c --- /dev/null +++ b/src/Templates/content/.azuredevops/pull_request_template/branches/preprod.md @@ -0,0 +1,27 @@ +# Note + +> This PR will commit changes to the "PREPROD" branch, which is deployed to PREPROD environment. +> Required approvers will automatically by added by ADO to Pull Request targeting test/preprod/main branches. + +## Summary of the Issue + +[The summary highlights the defect and observed failure. If the bug is recorded in ADO or there is a ticket, then provide the link. Here are a few examples:] + +1. *DO order from EFG order entry are getting created for orders that are rejected by Secura for fraud* +2. *GFS orders rejected by the store are not being updated to RE hold code* + +## Issue Details + +**Root Cause/Fix details**: [Outline the Root Cause/fix that was done to address the problem statement above.] + +**Test plan**: [Outline a high-level test plan to validate the issue. If there is a test plan already in ADO, then please share the link.] + +**Impact to other functions**: [List any other functional area(if any) that could be impacted with the code fix.] + +## Tips + +Here are some tips: + +* Use plain language. +* Make it simple, clear, and easily understandable. Don’t load with heavy technical terms. +* Keep the release notes short and precise. Be precise and include only the most relevant and important information. diff --git a/src/Templates/content/.azuredevops/pull_request_template/branches/test.md b/src/Templates/content/.azuredevops/pull_request_template/branches/test.md new file mode 100644 index 00000000..287dc897 --- /dev/null +++ b/src/Templates/content/.azuredevops/pull_request_template/branches/test.md @@ -0,0 +1,27 @@ +# Note + +> This PR will commit changes to the "TEST" branch, which is deployed to TEST environment. +> Required approvers will automatically by added by ADO to Pull Request targeting test/preprod/main branches. + +## Summary of the Issue + +[The summary highlights the defect and observed failure. If the bug is recorded in ADO or there is a ticket, then provide the link. Here are a few examples:] + +1. *DO order from EFG order entry are getting created for orders that are rejected by Secura for fraud* +2. *GFS orders rejected by the store are not being updated to RE hold code* + +## Issue Details + +**Root Cause/Fix details**: [Outline the Root Cause/fix that was done to address the problem statement above.] + +**Test plan**: [Outline a high-level test plan to validate the issue. If there is a test plan already in ADO, then please share the link.] + +**Impact to other functions**: [List any other functional area(if any) that could be impacted with the code fix.] + +## Tips + +Here are some tips: + +* Use plain language. +* Make it simple, clear, and easily understandable. Don’t load with heavy technical terms. +* Keep the release notes short and precise. Be precise and include only the most relevant and important information. diff --git a/src/Templates/content/.azuredevops/pull_request_template/pull_request_template.md b/src/Templates/content/.azuredevops/pull_request_template/pull_request_template.md new file mode 100644 index 00000000..859d201f --- /dev/null +++ b/src/Templates/content/.azuredevops/pull_request_template/pull_request_template.md @@ -0,0 +1,7 @@ +Thank you for your contribution to the Company AppName repository. +Before submitting this PR, please make sure: + +- [ ] Your code builds clean without any errors or warnings +- [ ] You are using approved terminology +- [ ] You have added unit tests +- [ ] You've added required configuration to Azure App Config diff --git a/src/Templates/content/Company.AppName.Api/Api.http b/src/Templates/content/Company.AppName.Api/Api.http index 78ce9198..0f3cc5a2 100644 --- a/src/Templates/content/Company.AppName.Api/Api.http +++ b/src/Templates/content/Company.AppName.Api/Api.http @@ -45,10 +45,10 @@ Content-Type: application/json } ### Update Employee -# todo: this fails with The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded PUT {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 x-correlation-id: 123-my-correlation-id-update Content-Type: application/json +If-Match: AAAAAAAACBg= { "email": "alice@alice.com", diff --git a/src/Templates/content/Company.AppName.Functions/Functions.http b/src/Templates/content/Company.AppName.Functions/Functions.http index c81a7c19..6d758cea 100644 --- a/src/Templates/content/Company.AppName.Functions/Functions.http +++ b/src/Templates/content/Company.AppName.Functions/Functions.http @@ -49,6 +49,7 @@ Content-Type: application/json PUT {{host}}/api/employees/3fa85f64-5717-4562-b3fc-2c963f66afa6 x-correlation-id: 123-my-correlation-id-update Content-Type: application/json +If-Match: AAAAAAAACBg= { "email": "alice@alice.com", diff --git a/src/Templates/readme.md b/src/Templates/readme.md index 25793d74..b0828290 100644 --- a/src/Templates/readme.md +++ b/src/Templates/readme.md @@ -29,7 +29,7 @@ Extensions required: Expose ports for function, app service and sql server --> DONE -## Update readme to use REST Client -> IN PROGRESS +## Update readme to use REST Client -> DONE Create: [POST] http://localhost:7071/api/api/employees Delete: [DELETE] http://localhost:7071/api/api/employees/{id} From eb6959d2559020ff65def1739760c39490eff9a8 Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 18:22:47 -0400 Subject: [PATCH 21/39] fix my HR function test Signed-off-by: Piotr --- samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs b/samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs index 8dc69378..ff1cebef 100644 --- a/samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs +++ b/samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs @@ -175,7 +175,7 @@ public void C110_Create_Success() .Run(c => c.CreateAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "api/employees", e))) .AssertCreated() .Assert(e, "Id", "ETag") - .AssertLocationHeader(v => new Uri($"api/employees/{v!.Id}", UriKind.Relative)) + .AssertLocationHeader(v => new Uri($"employees/{v!.Id}", UriKind.Relative)) .GetValue(); // Do a GET to make sure it is in the database and all fields equal. From ee653384e3848caf8c5a05daf22c565d6cfb29d1 Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 18:39:39 -0400 Subject: [PATCH 22/39] fix CI Signed-off-by: Piotr --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1f3f339a..3bce99f0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -70,4 +70,4 @@ jobs: run: dotnet build src/Templates/content - name: Test Template - run: dotnet test src/Templates/content --filter Category=!WithCosmos --no-build --verbosity normal /p:CollectCoverage=true /p:Exclude="[CoreEx.TestFunction]*" /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov3.info + run: dotnet test src/Templates/content --filter Category!=WithCosmos --no-build --verbosity normal /p:CollectCoverage=true /p:Exclude="[CoreEx.TestFunction]*" /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov3.info From 5c1f2bec16387f4cf751e0b9c8beff08c2685570 Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 21:06:29 -0400 Subject: [PATCH 23/39] test fix Signed-off-by: Piotr --- .../content/Company.AppName.UnitTest/EmployeeFunctionTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs b/src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs index bf424c3c..6c62f4b7 100644 --- a/src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs +++ b/src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs @@ -175,7 +175,7 @@ public void C110_Create_Success() .Run(c => c.CreateAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "api/employees", e))) .AssertCreated() .Assert(e, "Id", "ETag") - .AssertLocationHeader(v => new Uri($"api/employees/{v!.Id}", UriKind.Relative)) + .AssertLocationHeader(v => new Uri($"employees/{v!.Id}", UriKind.Relative)) .GetValue(); // Do a GET to make sure it is in the database and all fields equal. From 23d3715cb243dc7687721d87cf399691ab49fd26 Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 21:09:15 -0400 Subject: [PATCH 24/39] moving to tools folder Signed-off-by: Piotr --- .../CoreEx.Template}/content/.azuredevops/app-template.yml | 0 .../CoreEx.Template}/content/.azuredevops/function-template.yml | 0 .../CoreEx.Template}/content/.azuredevops/infra-template.yml | 0 .../CoreEx.Template}/content/.azuredevops/pipeline-build copy.yml | 0 .../CoreEx.Template}/content/.azuredevops/pipeline-build.yml | 0 .../content/.azuredevops/pipeline-release copy.yml | 0 .../CoreEx.Template}/content/.azuredevops/pipeline-release.yml | 0 .../content/.azuredevops/pull_request_template/branches/main.md | 0 .../.azuredevops/pull_request_template/branches/preprod.md | 0 .../content/.azuredevops/pull_request_template/branches/test.md | 0 .../.azuredevops/pull_request_template/pull_request_template.md | 0 .../CoreEx.Template}/content/.devcontainer/Dockerfile | 0 .../CoreEx.Template}/content/.devcontainer/devcontainer.json | 0 .../CoreEx.Template}/content/.devcontainer/devinit.json | 0 .../CoreEx.Template}/content/.devcontainer/docker-compose.yml | 0 {src/Templates => tools/CoreEx.Template}/content/.dockerignore | 0 {src/Templates => tools/CoreEx.Template}/content/.gitignore | 0 .../CoreEx.Template}/content/.template.config/template.json | 0 .../CoreEx.Template}/content/.vscode/extensions.json | 0 .../CoreEx.Template}/content/.vscode/launch.json | 0 .../CoreEx.Template}/content/.vscode/settings.json | 0 .../CoreEx.Template}/content/.vscode/tasks.json | 0 .../CoreEx.Template}/content/Company.AppName.Api/Api.http | 0 .../content/Company.AppName.Api/Company.AppName.Api.csproj | 0 .../content/Company.AppName.Api/Controllers/EmployeeController.cs | 0 .../content/Company.AppName.Api/Controllers/HealthController.cs | 0 .../Company.AppName.Api/Controllers/ReferenceDataController.cs | 0 .../content/Company.AppName.Api/Controllers/SwaggerController.cs | 0 .../CoreEx.Template}/content/Company.AppName.Api/Dockerfile | 0 .../content/Company.AppName.Api/ImplicitUsings.cs | 0 .../CoreEx.Template}/content/Company.AppName.Api/Program.cs | 0 .../content/Company.AppName.Api/Properties/launchSettings.json | 0 .../CoreEx.Template}/content/Company.AppName.Api/Startup.cs | 0 .../content/Company.AppName.Api/appsettings.Development.json | 0 .../CoreEx.Template}/content/Company.AppName.Api/appsettings.json | 0 .../content/Company.AppName.Business/AppNameSettings.cs | 0 .../Company.AppName.Business/Company.AppName.Business.csproj | 0 .../content/Company.AppName.Business/Data/AppNameDb.cs | 0 .../content/Company.AppName.Business/Data/AppNameDbContext.cs | 0 .../content/Company.AppName.Business/Data/AppNameEfDb.cs | 0 .../Company.AppName.Business/Data/EmployeeConfiguration.cs | 0 .../content/Company.AppName.Business/Data/UsStateConfiguration.cs | 0 .../Company.AppName.Business/External/AgifyServiceClient.cs | 0 .../Company.AppName.Business/External/Contracts/AgifyResponse.cs | 0 .../External/Contracts/EmployeeVerificationRequest.cs | 0 .../External/Contracts/EmployeeVerificationResponse.cs | 0 .../External/Contracts/GenderizeResponse.cs | 0 .../External/Contracts/NationalizeResponse.cs | 0 .../Company.AppName.Business/External/GenderizeApiClient.cs | 0 .../Company.AppName.Business/External/NationalizeApiClient.cs | 0 .../content/Company.AppName.Business/ImplicitUsings.cs | 0 .../content/Company.AppName.Business/Models/Employee.cs | 0 .../content/Company.AppName.Business/Models/Gender.cs | 0 .../content/Company.AppName.Business/Models/UsState.cs | 0 .../content/Company.AppName.Business/Services/EmployeeService.cs | 0 .../content/Company.AppName.Business/Services/EmployeeService2.cs | 0 .../content/Company.AppName.Business/Services/IEmployeeService.cs | 0 .../Company.AppName.Business/Services/ReferenceDataService.cs | 0 .../Company.AppName.Business/Services/VerificationService.cs | 0 .../Company.AppName.Business/Validators/EmployeeValidator.cs | 0 .../Validators/EmployeeVerificationValidator.cs | 0 .../Company.AppName.Database/Company.AppName.Database.csproj | 0 .../content/Company.AppName.Database/Data/RefData.yaml | 0 .../CoreEx.Template}/content/Company.AppName.Database/Dockerfile | 0 .../Migrations/20190101-000001-create-AppName-schema.sql | 0 .../Migrations/20200909-162702-create-AppName-Employee.sql | 0 .../20200909-163321-create-AppName-EmergencyContact.sql | 0 .../Migrations/20200909-164735-create-AppName-gender.sql | 0 .../20200909-164828-create-AppName-terminationreason.sql | 0 .../20200909-165308-create-AppName-relationshiptype.sql | 0 .../Migrations/20200909-165752-create-AppName-usstate.sql | 0 .../20200915-160812-create-AppName-PerformanceReview.sql | 0 .../20200915-161927-create-AppName-performanceoutcome.sql | 0 .../20211208-001509-create-AppName-eventoutbox-table.sql | 0 .../20211208-001509-create-AppName-eventoutboxdata-table.sql | 0 .../CoreEx.Template}/content/Company.AppName.Database/Program.cs | 0 .../Company.AppName.Database/Properties/launchSettings.json | 0 .../content/Company.AppName.Database/entrypoint.sh | 0 .../content/Company.AppName.Database/wait-for-it.sh | 0 .../CoreEx.Template}/content/Company.AppName.Functions/.gitignore | 0 .../content/Company.AppName.Functions/.vscode/extensions.json | 0 .../Company.AppName.Functions/Company.AppName.Functions.csproj | 0 .../CoreEx.Template}/content/Company.AppName.Functions/Dockerfile | 0 .../content/Company.AppName.Functions/Functions.http | 0 .../Company.AppName.Functions/Functions/EmployeeFunction.cs | 0 .../Company.AppName.Functions/Functions/HttpHealthFunction.cs | 0 .../Functions/HttpTriggerQueueVerificationFunction.cs | 0 .../Functions/ServiceBusExecuteVerificationFunction.cs | 0 .../Company.AppName.Functions/MyHrApiConfigurationOptions.cs | 0 .../CoreEx.Template}/content/Company.AppName.Functions/README.md | 0 .../CoreEx.Template}/content/Company.AppName.Functions/Startup.cs | 0 .../CoreEx.Template}/content/Company.AppName.Functions/host.json | 0 .../content/Company.AppName.Functions/local.settings.json | 0 .../Company.AppName.Infra.Tests.csproj | 0 .../content/Company.AppName.Infra.Tests/CoreExStackTests.cs | 0 .../Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs | 0 .../Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs | 0 .../content/Company.AppName.Infra.Tests/Testing.cs | 0 .../content/Company.AppName.Infra.Tests/TestingExtensions.cs | 0 .../content/Company.AppName.Infra.Tests/Usings.cs | 0 .../content/Company.AppName.Infra/Company.AppName.Infra.csproj | 0 .../content/Company.AppName.Infra/Components/Apps.cs | 0 .../content/Company.AppName.Infra/Components/DevSetup.cs | 0 .../content/Company.AppName.Infra/Components/Diagnostics.cs | 0 .../content/Company.AppName.Infra/Components/Messaging.cs | 0 .../content/Company.AppName.Infra/Components/Sql.cs | 0 .../content/Company.AppName.Infra/Components/Storage.cs | 0 .../CoreEx.Template}/content/Company.AppName.Infra/CoreExStack.cs | 0 .../CoreEx.Template}/content/Company.AppName.Infra/Extensions.cs | 0 .../CoreEx.Template}/content/Company.AppName.Infra/Program.cs | 0 .../CoreEx.Template}/content/Company.AppName.Infra/Pulumi.yaml | 0 .../CoreEx.Template}/content/Company.AppName.Infra/Readme.md | 0 .../content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs | 0 .../content/Company.AppName.Infra/Services/AzureApiClient.cs | 0 .../content/Company.AppName.Infra/Services/AzureApiService.cs | 0 .../content/Company.AppName.Infra/Services/DbOperations.cs | 0 .../content/Company.AppName.Infra/Services/IDbOperations.cs | 0 .../content/Company.AppName.Infra/Services/PulumiLogger.cs | 0 .../content/Company.AppName.Infra/StackConfiguration.cs | 0 .../Company.AppName.UnitTest/Company.AppName.UnitTest.csproj | 0 .../content/Company.AppName.UnitTest/Data/Data.yaml | 0 .../content/Company.AppName.UnitTest/EmployeeControllerTest.cs | 0 .../content/Company.AppName.UnitTest/EmployeeControllerTest2.cs | 0 .../content/Company.AppName.UnitTest/EmployeeFunctionTest.cs | 0 .../HttpTriggerQueueVerificationFunctionTest.cs | 0 .../Company.AppName.UnitTest/ReferenceDataControllerTest.cs | 0 .../Resources/VerificationResult.Unix.json | 0 .../Resources/VerificationResult.Win32.json | 0 .../ServiceBusExecuteVerificationFunctionTest.cs | 0 .../content/Company.AppName.UnitTest/appsettings.unittest.json | 0 .../CoreEx.Template}/content/Company.AppName.sln | 0 {src/Templates => tools/CoreEx.Template}/content/Docker.md | 0 .../CoreEx.Template}/content/docker-compose.DB.only.yml | 0 .../CoreEx.Template}/content/docker-compose.override.yml | 0 .../CoreEx.Template}/content/docker-compose.yml | 0 {src/Templates => tools/CoreEx.Template}/readme.md | 0 136 files changed, 0 insertions(+), 0 deletions(-) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/app-template.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/function-template.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/infra-template.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/pipeline-build copy.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/pipeline-build.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/pipeline-release copy.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/pipeline-release.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/pull_request_template/branches/main.md (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/pull_request_template/branches/preprod.md (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/pull_request_template/branches/test.md (100%) rename {src/Templates => tools/CoreEx.Template}/content/.azuredevops/pull_request_template/pull_request_template.md (100%) rename {src/Templates => tools/CoreEx.Template}/content/.devcontainer/Dockerfile (100%) rename {src/Templates => tools/CoreEx.Template}/content/.devcontainer/devcontainer.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/.devcontainer/devinit.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/.devcontainer/docker-compose.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/.dockerignore (100%) rename {src/Templates => tools/CoreEx.Template}/content/.gitignore (100%) rename {src/Templates => tools/CoreEx.Template}/content/.template.config/template.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/.vscode/extensions.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/.vscode/launch.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/.vscode/settings.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/.vscode/tasks.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Api.http (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Company.AppName.Api.csproj (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Controllers/EmployeeController.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Controllers/HealthController.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Controllers/ReferenceDataController.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Controllers/SwaggerController.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Dockerfile (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/ImplicitUsings.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Program.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Properties/launchSettings.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/Startup.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/appsettings.Development.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Api/appsettings.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/AppNameSettings.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Company.AppName.Business.csproj (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Data/AppNameDb.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Data/AppNameDbContext.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Data/AppNameEfDb.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Data/EmployeeConfiguration.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Data/UsStateConfiguration.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/External/AgifyServiceClient.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/External/GenderizeApiClient.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/External/NationalizeApiClient.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/ImplicitUsings.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Models/Employee.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Models/Gender.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Models/UsState.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Services/EmployeeService.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Services/EmployeeService2.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Services/IEmployeeService.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Services/ReferenceDataService.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Services/VerificationService.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Validators/EmployeeValidator.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Company.AppName.Database.csproj (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Data/RefData.yaml (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Dockerfile (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20200909-162702-create-AppName-Employee.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20200909-163321-create-AppName-EmergencyContact.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20200909-164735-create-AppName-gender.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20200909-164828-create-AppName-terminationreason.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20200909-165308-create-AppName-relationshiptype.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20200909-165752-create-AppName-usstate.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20200915-160812-create-AppName-PerformanceReview.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20200915-161927-create-AppName-performanceoutcome.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutbox-table.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutboxdata-table.sql (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Program.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/Properties/launchSettings.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/entrypoint.sh (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Database/wait-for-it.sh (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/.gitignore (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/.vscode/extensions.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/Company.AppName.Functions.csproj (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/Dockerfile (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/Functions.http (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/Functions/EmployeeFunction.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/README.md (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/Startup.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/host.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Functions/local.settings.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra.Tests/CoreExStackTests.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra.Tests/Testing.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra.Tests/TestingExtensions.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra.Tests/Usings.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Company.AppName.Infra.csproj (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Components/Apps.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Components/DevSetup.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Components/Diagnostics.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Components/Messaging.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Components/Sql.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Components/Storage.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/CoreExStack.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Extensions.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Program.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Pulumi.yaml (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Readme.md (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Services/AzureApiClient.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Services/AzureApiService.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Services/DbOperations.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Services/IDbOperations.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/Services/PulumiLogger.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.Infra/StackConfiguration.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/Data/Data.yaml (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/EmployeeControllerTest.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.UnitTest/appsettings.unittest.json (100%) rename {src/Templates => tools/CoreEx.Template}/content/Company.AppName.sln (100%) rename {src/Templates => tools/CoreEx.Template}/content/Docker.md (100%) rename {src/Templates => tools/CoreEx.Template}/content/docker-compose.DB.only.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/docker-compose.override.yml (100%) rename {src/Templates => tools/CoreEx.Template}/content/docker-compose.yml (100%) rename {src/Templates => tools/CoreEx.Template}/readme.md (100%) diff --git a/src/Templates/content/.azuredevops/app-template.yml b/tools/CoreEx.Template/content/.azuredevops/app-template.yml similarity index 100% rename from src/Templates/content/.azuredevops/app-template.yml rename to tools/CoreEx.Template/content/.azuredevops/app-template.yml diff --git a/src/Templates/content/.azuredevops/function-template.yml b/tools/CoreEx.Template/content/.azuredevops/function-template.yml similarity index 100% rename from src/Templates/content/.azuredevops/function-template.yml rename to tools/CoreEx.Template/content/.azuredevops/function-template.yml diff --git a/src/Templates/content/.azuredevops/infra-template.yml b/tools/CoreEx.Template/content/.azuredevops/infra-template.yml similarity index 100% rename from src/Templates/content/.azuredevops/infra-template.yml rename to tools/CoreEx.Template/content/.azuredevops/infra-template.yml diff --git a/src/Templates/content/.azuredevops/pipeline-build copy.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-build copy.yml similarity index 100% rename from src/Templates/content/.azuredevops/pipeline-build copy.yml rename to tools/CoreEx.Template/content/.azuredevops/pipeline-build copy.yml diff --git a/src/Templates/content/.azuredevops/pipeline-build.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml similarity index 100% rename from src/Templates/content/.azuredevops/pipeline-build.yml rename to tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml diff --git a/src/Templates/content/.azuredevops/pipeline-release copy.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-release copy.yml similarity index 100% rename from src/Templates/content/.azuredevops/pipeline-release copy.yml rename to tools/CoreEx.Template/content/.azuredevops/pipeline-release copy.yml diff --git a/src/Templates/content/.azuredevops/pipeline-release.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml similarity index 100% rename from src/Templates/content/.azuredevops/pipeline-release.yml rename to tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml diff --git a/src/Templates/content/.azuredevops/pull_request_template/branches/main.md b/tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/main.md similarity index 100% rename from src/Templates/content/.azuredevops/pull_request_template/branches/main.md rename to tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/main.md diff --git a/src/Templates/content/.azuredevops/pull_request_template/branches/preprod.md b/tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/preprod.md similarity index 100% rename from src/Templates/content/.azuredevops/pull_request_template/branches/preprod.md rename to tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/preprod.md diff --git a/src/Templates/content/.azuredevops/pull_request_template/branches/test.md b/tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/test.md similarity index 100% rename from src/Templates/content/.azuredevops/pull_request_template/branches/test.md rename to tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/test.md diff --git a/src/Templates/content/.azuredevops/pull_request_template/pull_request_template.md b/tools/CoreEx.Template/content/.azuredevops/pull_request_template/pull_request_template.md similarity index 100% rename from src/Templates/content/.azuredevops/pull_request_template/pull_request_template.md rename to tools/CoreEx.Template/content/.azuredevops/pull_request_template/pull_request_template.md diff --git a/src/Templates/content/.devcontainer/Dockerfile b/tools/CoreEx.Template/content/.devcontainer/Dockerfile similarity index 100% rename from src/Templates/content/.devcontainer/Dockerfile rename to tools/CoreEx.Template/content/.devcontainer/Dockerfile diff --git a/src/Templates/content/.devcontainer/devcontainer.json b/tools/CoreEx.Template/content/.devcontainer/devcontainer.json similarity index 100% rename from src/Templates/content/.devcontainer/devcontainer.json rename to tools/CoreEx.Template/content/.devcontainer/devcontainer.json diff --git a/src/Templates/content/.devcontainer/devinit.json b/tools/CoreEx.Template/content/.devcontainer/devinit.json similarity index 100% rename from src/Templates/content/.devcontainer/devinit.json rename to tools/CoreEx.Template/content/.devcontainer/devinit.json diff --git a/src/Templates/content/.devcontainer/docker-compose.yml b/tools/CoreEx.Template/content/.devcontainer/docker-compose.yml similarity index 100% rename from src/Templates/content/.devcontainer/docker-compose.yml rename to tools/CoreEx.Template/content/.devcontainer/docker-compose.yml diff --git a/src/Templates/content/.dockerignore b/tools/CoreEx.Template/content/.dockerignore similarity index 100% rename from src/Templates/content/.dockerignore rename to tools/CoreEx.Template/content/.dockerignore diff --git a/src/Templates/content/.gitignore b/tools/CoreEx.Template/content/.gitignore similarity index 100% rename from src/Templates/content/.gitignore rename to tools/CoreEx.Template/content/.gitignore diff --git a/src/Templates/content/.template.config/template.json b/tools/CoreEx.Template/content/.template.config/template.json similarity index 100% rename from src/Templates/content/.template.config/template.json rename to tools/CoreEx.Template/content/.template.config/template.json diff --git a/src/Templates/content/.vscode/extensions.json b/tools/CoreEx.Template/content/.vscode/extensions.json similarity index 100% rename from src/Templates/content/.vscode/extensions.json rename to tools/CoreEx.Template/content/.vscode/extensions.json diff --git a/src/Templates/content/.vscode/launch.json b/tools/CoreEx.Template/content/.vscode/launch.json similarity index 100% rename from src/Templates/content/.vscode/launch.json rename to tools/CoreEx.Template/content/.vscode/launch.json diff --git a/src/Templates/content/.vscode/settings.json b/tools/CoreEx.Template/content/.vscode/settings.json similarity index 100% rename from src/Templates/content/.vscode/settings.json rename to tools/CoreEx.Template/content/.vscode/settings.json diff --git a/src/Templates/content/.vscode/tasks.json b/tools/CoreEx.Template/content/.vscode/tasks.json similarity index 100% rename from src/Templates/content/.vscode/tasks.json rename to tools/CoreEx.Template/content/.vscode/tasks.json diff --git a/src/Templates/content/Company.AppName.Api/Api.http b/tools/CoreEx.Template/content/Company.AppName.Api/Api.http similarity index 100% rename from src/Templates/content/Company.AppName.Api/Api.http rename to tools/CoreEx.Template/content/Company.AppName.Api/Api.http diff --git a/src/Templates/content/Company.AppName.Api/Company.AppName.Api.csproj b/tools/CoreEx.Template/content/Company.AppName.Api/Company.AppName.Api.csproj similarity index 100% rename from src/Templates/content/Company.AppName.Api/Company.AppName.Api.csproj rename to tools/CoreEx.Template/content/Company.AppName.Api/Company.AppName.Api.csproj diff --git a/src/Templates/content/Company.AppName.Api/Controllers/EmployeeController.cs b/tools/CoreEx.Template/content/Company.AppName.Api/Controllers/EmployeeController.cs similarity index 100% rename from src/Templates/content/Company.AppName.Api/Controllers/EmployeeController.cs rename to tools/CoreEx.Template/content/Company.AppName.Api/Controllers/EmployeeController.cs diff --git a/src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs b/tools/CoreEx.Template/content/Company.AppName.Api/Controllers/HealthController.cs similarity index 100% rename from src/Templates/content/Company.AppName.Api/Controllers/HealthController.cs rename to tools/CoreEx.Template/content/Company.AppName.Api/Controllers/HealthController.cs diff --git a/src/Templates/content/Company.AppName.Api/Controllers/ReferenceDataController.cs b/tools/CoreEx.Template/content/Company.AppName.Api/Controllers/ReferenceDataController.cs similarity index 100% rename from src/Templates/content/Company.AppName.Api/Controllers/ReferenceDataController.cs rename to tools/CoreEx.Template/content/Company.AppName.Api/Controllers/ReferenceDataController.cs diff --git a/src/Templates/content/Company.AppName.Api/Controllers/SwaggerController.cs b/tools/CoreEx.Template/content/Company.AppName.Api/Controllers/SwaggerController.cs similarity index 100% rename from src/Templates/content/Company.AppName.Api/Controllers/SwaggerController.cs rename to tools/CoreEx.Template/content/Company.AppName.Api/Controllers/SwaggerController.cs diff --git a/src/Templates/content/Company.AppName.Api/Dockerfile b/tools/CoreEx.Template/content/Company.AppName.Api/Dockerfile similarity index 100% rename from src/Templates/content/Company.AppName.Api/Dockerfile rename to tools/CoreEx.Template/content/Company.AppName.Api/Dockerfile diff --git a/src/Templates/content/Company.AppName.Api/ImplicitUsings.cs b/tools/CoreEx.Template/content/Company.AppName.Api/ImplicitUsings.cs similarity index 100% rename from src/Templates/content/Company.AppName.Api/ImplicitUsings.cs rename to tools/CoreEx.Template/content/Company.AppName.Api/ImplicitUsings.cs diff --git a/src/Templates/content/Company.AppName.Api/Program.cs b/tools/CoreEx.Template/content/Company.AppName.Api/Program.cs similarity index 100% rename from src/Templates/content/Company.AppName.Api/Program.cs rename to tools/CoreEx.Template/content/Company.AppName.Api/Program.cs diff --git a/src/Templates/content/Company.AppName.Api/Properties/launchSettings.json b/tools/CoreEx.Template/content/Company.AppName.Api/Properties/launchSettings.json similarity index 100% rename from src/Templates/content/Company.AppName.Api/Properties/launchSettings.json rename to tools/CoreEx.Template/content/Company.AppName.Api/Properties/launchSettings.json diff --git a/src/Templates/content/Company.AppName.Api/Startup.cs b/tools/CoreEx.Template/content/Company.AppName.Api/Startup.cs similarity index 100% rename from src/Templates/content/Company.AppName.Api/Startup.cs rename to tools/CoreEx.Template/content/Company.AppName.Api/Startup.cs diff --git a/src/Templates/content/Company.AppName.Api/appsettings.Development.json b/tools/CoreEx.Template/content/Company.AppName.Api/appsettings.Development.json similarity index 100% rename from src/Templates/content/Company.AppName.Api/appsettings.Development.json rename to tools/CoreEx.Template/content/Company.AppName.Api/appsettings.Development.json diff --git a/src/Templates/content/Company.AppName.Api/appsettings.json b/tools/CoreEx.Template/content/Company.AppName.Api/appsettings.json similarity index 100% rename from src/Templates/content/Company.AppName.Api/appsettings.json rename to tools/CoreEx.Template/content/Company.AppName.Api/appsettings.json diff --git a/src/Templates/content/Company.AppName.Business/AppNameSettings.cs b/tools/CoreEx.Template/content/Company.AppName.Business/AppNameSettings.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/AppNameSettings.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/AppNameSettings.cs diff --git a/src/Templates/content/Company.AppName.Business/Company.AppName.Business.csproj b/tools/CoreEx.Template/content/Company.AppName.Business/Company.AppName.Business.csproj similarity index 100% rename from src/Templates/content/Company.AppName.Business/Company.AppName.Business.csproj rename to tools/CoreEx.Template/content/Company.AppName.Business/Company.AppName.Business.csproj diff --git a/src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Data/AppNameDb.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Data/AppNameDb.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Data/AppNameDb.cs diff --git a/src/Templates/content/Company.AppName.Business/Data/AppNameDbContext.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Data/AppNameDbContext.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Data/AppNameDbContext.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Data/AppNameDbContext.cs diff --git a/src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Data/AppNameEfDb.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Data/AppNameEfDb.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Data/AppNameEfDb.cs diff --git a/src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Data/EmployeeConfiguration.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Data/EmployeeConfiguration.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Data/EmployeeConfiguration.cs diff --git a/src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Data/UsStateConfiguration.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Data/UsStateConfiguration.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Data/UsStateConfiguration.cs diff --git a/src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs b/tools/CoreEx.Template/content/Company.AppName.Business/External/AgifyServiceClient.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/External/AgifyServiceClient.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/External/AgifyServiceClient.cs diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs b/tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/AgifyResponse.cs diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs b/tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/EmployeeVerificationRequest.cs diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs b/tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/EmployeeVerificationResponse.cs diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs b/tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/GenderizeResponse.cs diff --git a/src/Templates/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs b/tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/External/Contracts/NationalizeResponse.cs diff --git a/src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs b/tools/CoreEx.Template/content/Company.AppName.Business/External/GenderizeApiClient.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/External/GenderizeApiClient.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/External/GenderizeApiClient.cs diff --git a/src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs b/tools/CoreEx.Template/content/Company.AppName.Business/External/NationalizeApiClient.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/External/NationalizeApiClient.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/External/NationalizeApiClient.cs diff --git a/src/Templates/content/Company.AppName.Business/ImplicitUsings.cs b/tools/CoreEx.Template/content/Company.AppName.Business/ImplicitUsings.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/ImplicitUsings.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/ImplicitUsings.cs diff --git a/src/Templates/content/Company.AppName.Business/Models/Employee.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Models/Employee.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Models/Employee.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Models/Employee.cs diff --git a/src/Templates/content/Company.AppName.Business/Models/Gender.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Models/Gender.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Models/Gender.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Models/Gender.cs diff --git a/src/Templates/content/Company.AppName.Business/Models/UsState.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Models/UsState.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Models/UsState.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Models/UsState.cs diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Services/EmployeeService.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Services/EmployeeService.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Services/EmployeeService.cs diff --git a/src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Services/EmployeeService2.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Services/EmployeeService2.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Services/EmployeeService2.cs diff --git a/src/Templates/content/Company.AppName.Business/Services/IEmployeeService.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Services/IEmployeeService.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Services/IEmployeeService.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Services/IEmployeeService.cs diff --git a/src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Services/ReferenceDataService.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Services/ReferenceDataService.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Services/ReferenceDataService.cs diff --git a/src/Templates/content/Company.AppName.Business/Services/VerificationService.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Services/VerificationService.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Services/VerificationService.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Services/VerificationService.cs diff --git a/src/Templates/content/Company.AppName.Business/Validators/EmployeeValidator.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Validators/EmployeeValidator.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Validators/EmployeeValidator.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Validators/EmployeeValidator.cs diff --git a/src/Templates/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs b/tools/CoreEx.Template/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs similarity index 100% rename from src/Templates/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs rename to tools/CoreEx.Template/content/Company.AppName.Business/Validators/EmployeeVerificationValidator.cs diff --git a/src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj b/tools/CoreEx.Template/content/Company.AppName.Database/Company.AppName.Database.csproj similarity index 100% rename from src/Templates/content/Company.AppName.Database/Company.AppName.Database.csproj rename to tools/CoreEx.Template/content/Company.AppName.Database/Company.AppName.Database.csproj diff --git a/src/Templates/content/Company.AppName.Database/Data/RefData.yaml b/tools/CoreEx.Template/content/Company.AppName.Database/Data/RefData.yaml similarity index 100% rename from src/Templates/content/Company.AppName.Database/Data/RefData.yaml rename to tools/CoreEx.Template/content/Company.AppName.Database/Data/RefData.yaml diff --git a/src/Templates/content/Company.AppName.Database/Dockerfile b/tools/CoreEx.Template/content/Company.AppName.Database/Dockerfile similarity index 100% rename from src/Templates/content/Company.AppName.Database/Dockerfile rename to tools/CoreEx.Template/content/Company.AppName.Database/Dockerfile diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20190101-000001-create-AppName-schema.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-AppName-Employee.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-162702-create-AppName-Employee.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-162702-create-AppName-Employee.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-162702-create-AppName-Employee.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-AppName-EmergencyContact.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-163321-create-AppName-EmergencyContact.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-163321-create-AppName-EmergencyContact.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-163321-create-AppName-EmergencyContact.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-AppName-gender.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-164735-create-AppName-gender.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-164735-create-AppName-gender.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-164735-create-AppName-gender.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-AppName-terminationreason.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-164828-create-AppName-terminationreason.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-164828-create-AppName-terminationreason.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-164828-create-AppName-terminationreason.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-AppName-relationshiptype.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-165308-create-AppName-relationshiptype.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-165308-create-AppName-relationshiptype.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-165308-create-AppName-relationshiptype.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-AppName-usstate.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-165752-create-AppName-usstate.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20200909-165752-create-AppName-usstate.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200909-165752-create-AppName-usstate.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-AppName-PerformanceReview.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200915-160812-create-AppName-PerformanceReview.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20200915-160812-create-AppName-PerformanceReview.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200915-160812-create-AppName-PerformanceReview.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-AppName-performanceoutcome.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200915-161927-create-AppName-performanceoutcome.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20200915-161927-create-AppName-performanceoutcome.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20200915-161927-create-AppName-performanceoutcome.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutbox-table.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutbox-table.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutbox-table.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutbox-table.sql diff --git a/src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutboxdata-table.sql b/tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutboxdata-table.sql similarity index 100% rename from src/Templates/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutboxdata-table.sql rename to tools/CoreEx.Template/content/Company.AppName.Database/Migrations/20211208-001509-create-AppName-eventoutboxdata-table.sql diff --git a/src/Templates/content/Company.AppName.Database/Program.cs b/tools/CoreEx.Template/content/Company.AppName.Database/Program.cs similarity index 100% rename from src/Templates/content/Company.AppName.Database/Program.cs rename to tools/CoreEx.Template/content/Company.AppName.Database/Program.cs diff --git a/src/Templates/content/Company.AppName.Database/Properties/launchSettings.json b/tools/CoreEx.Template/content/Company.AppName.Database/Properties/launchSettings.json similarity index 100% rename from src/Templates/content/Company.AppName.Database/Properties/launchSettings.json rename to tools/CoreEx.Template/content/Company.AppName.Database/Properties/launchSettings.json diff --git a/src/Templates/content/Company.AppName.Database/entrypoint.sh b/tools/CoreEx.Template/content/Company.AppName.Database/entrypoint.sh similarity index 100% rename from src/Templates/content/Company.AppName.Database/entrypoint.sh rename to tools/CoreEx.Template/content/Company.AppName.Database/entrypoint.sh diff --git a/src/Templates/content/Company.AppName.Database/wait-for-it.sh b/tools/CoreEx.Template/content/Company.AppName.Database/wait-for-it.sh similarity index 100% rename from src/Templates/content/Company.AppName.Database/wait-for-it.sh rename to tools/CoreEx.Template/content/Company.AppName.Database/wait-for-it.sh diff --git a/src/Templates/content/Company.AppName.Functions/.gitignore b/tools/CoreEx.Template/content/Company.AppName.Functions/.gitignore similarity index 100% rename from src/Templates/content/Company.AppName.Functions/.gitignore rename to tools/CoreEx.Template/content/Company.AppName.Functions/.gitignore diff --git a/src/Templates/content/Company.AppName.Functions/.vscode/extensions.json b/tools/CoreEx.Template/content/Company.AppName.Functions/.vscode/extensions.json similarity index 100% rename from src/Templates/content/Company.AppName.Functions/.vscode/extensions.json rename to tools/CoreEx.Template/content/Company.AppName.Functions/.vscode/extensions.json diff --git a/src/Templates/content/Company.AppName.Functions/Company.AppName.Functions.csproj b/tools/CoreEx.Template/content/Company.AppName.Functions/Company.AppName.Functions.csproj similarity index 100% rename from src/Templates/content/Company.AppName.Functions/Company.AppName.Functions.csproj rename to tools/CoreEx.Template/content/Company.AppName.Functions/Company.AppName.Functions.csproj diff --git a/src/Templates/content/Company.AppName.Functions/Dockerfile b/tools/CoreEx.Template/content/Company.AppName.Functions/Dockerfile similarity index 100% rename from src/Templates/content/Company.AppName.Functions/Dockerfile rename to tools/CoreEx.Template/content/Company.AppName.Functions/Dockerfile diff --git a/src/Templates/content/Company.AppName.Functions/Functions.http b/tools/CoreEx.Template/content/Company.AppName.Functions/Functions.http similarity index 100% rename from src/Templates/content/Company.AppName.Functions/Functions.http rename to tools/CoreEx.Template/content/Company.AppName.Functions/Functions.http diff --git a/src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs b/tools/CoreEx.Template/content/Company.AppName.Functions/Functions/EmployeeFunction.cs similarity index 100% rename from src/Templates/content/Company.AppName.Functions/Functions/EmployeeFunction.cs rename to tools/CoreEx.Template/content/Company.AppName.Functions/Functions/EmployeeFunction.cs diff --git a/src/Templates/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs b/tools/CoreEx.Template/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs similarity index 100% rename from src/Templates/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs rename to tools/CoreEx.Template/content/Company.AppName.Functions/Functions/HttpHealthFunction.cs diff --git a/src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs b/tools/CoreEx.Template/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs similarity index 100% rename from src/Templates/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs rename to tools/CoreEx.Template/content/Company.AppName.Functions/Functions/HttpTriggerQueueVerificationFunction.cs diff --git a/src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs b/tools/CoreEx.Template/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs similarity index 100% rename from src/Templates/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs rename to tools/CoreEx.Template/content/Company.AppName.Functions/Functions/ServiceBusExecuteVerificationFunction.cs diff --git a/src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs b/tools/CoreEx.Template/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs similarity index 100% rename from src/Templates/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs rename to tools/CoreEx.Template/content/Company.AppName.Functions/MyHrApiConfigurationOptions.cs diff --git a/src/Templates/content/Company.AppName.Functions/README.md b/tools/CoreEx.Template/content/Company.AppName.Functions/README.md similarity index 100% rename from src/Templates/content/Company.AppName.Functions/README.md rename to tools/CoreEx.Template/content/Company.AppName.Functions/README.md diff --git a/src/Templates/content/Company.AppName.Functions/Startup.cs b/tools/CoreEx.Template/content/Company.AppName.Functions/Startup.cs similarity index 100% rename from src/Templates/content/Company.AppName.Functions/Startup.cs rename to tools/CoreEx.Template/content/Company.AppName.Functions/Startup.cs diff --git a/src/Templates/content/Company.AppName.Functions/host.json b/tools/CoreEx.Template/content/Company.AppName.Functions/host.json similarity index 100% rename from src/Templates/content/Company.AppName.Functions/host.json rename to tools/CoreEx.Template/content/Company.AppName.Functions/host.json diff --git a/src/Templates/content/Company.AppName.Functions/local.settings.json b/tools/CoreEx.Template/content/Company.AppName.Functions/local.settings.json similarity index 100% rename from src/Templates/content/Company.AppName.Functions/local.settings.json rename to tools/CoreEx.Template/content/Company.AppName.Functions/local.settings.json diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj similarity index 100% rename from src/Templates/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj rename to tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj diff --git a/src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/CoreExStackTests.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra.Tests/CoreExStackTests.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra.Tests/CoreExStackTests.cs diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Services/AzureApiClientTests.cs diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Services/AzureApiServiceTests.cs diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Testing.cs b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Testing.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra.Tests/Testing.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Testing.cs diff --git a/src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/TestingExtensions.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra.Tests/TestingExtensions.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra.Tests/TestingExtensions.cs diff --git a/src/Templates/content/Company.AppName.Infra.Tests/Usings.cs b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Usings.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra.Tests/Usings.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Usings.cs diff --git a/src/Templates/content/Company.AppName.Infra/Company.AppName.Infra.csproj b/tools/CoreEx.Template/content/Company.AppName.Infra/Company.AppName.Infra.csproj similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Company.AppName.Infra.csproj rename to tools/CoreEx.Template/content/Company.AppName.Infra/Company.AppName.Infra.csproj diff --git a/src/Templates/content/Company.AppName.Infra/Components/Apps.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Components/Apps.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs diff --git a/src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/DevSetup.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Components/DevSetup.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Components/DevSetup.cs diff --git a/src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Diagnostics.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Components/Diagnostics.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Components/Diagnostics.cs diff --git a/src/Templates/content/Company.AppName.Infra/Components/Messaging.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Messaging.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Components/Messaging.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Components/Messaging.cs diff --git a/src/Templates/content/Company.AppName.Infra/Components/Sql.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Sql.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Components/Sql.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Components/Sql.cs diff --git a/src/Templates/content/Company.AppName.Infra/Components/Storage.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Storage.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Components/Storage.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Components/Storage.cs diff --git a/src/Templates/content/Company.AppName.Infra/CoreExStack.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/CoreExStack.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/CoreExStack.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/CoreExStack.cs diff --git a/src/Templates/content/Company.AppName.Infra/Extensions.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Extensions.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Extensions.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Extensions.cs diff --git a/src/Templates/content/Company.AppName.Infra/Program.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Program.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Program.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Program.cs diff --git a/src/Templates/content/Company.AppName.Infra/Pulumi.yaml b/tools/CoreEx.Template/content/Company.AppName.Infra/Pulumi.yaml similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Pulumi.yaml rename to tools/CoreEx.Template/content/Company.AppName.Infra/Pulumi.yaml diff --git a/src/Templates/content/Company.AppName.Infra/Readme.md b/tools/CoreEx.Template/content/Company.AppName.Infra/Readme.md similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Readme.md rename to tools/CoreEx.Template/content/Company.AppName.Infra/Readme.md diff --git a/src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Roles/BuiltInRolesIds.cs diff --git a/src/Templates/content/Company.AppName.Infra/Services/AzureApiClient.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/AzureApiClient.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Services/AzureApiClient.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Services/AzureApiClient.cs diff --git a/src/Templates/content/Company.AppName.Infra/Services/AzureApiService.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/AzureApiService.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Services/AzureApiService.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Services/AzureApiService.cs diff --git a/src/Templates/content/Company.AppName.Infra/Services/DbOperations.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/DbOperations.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Services/DbOperations.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Services/DbOperations.cs diff --git a/src/Templates/content/Company.AppName.Infra/Services/IDbOperations.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/IDbOperations.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Services/IDbOperations.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Services/IDbOperations.cs diff --git a/src/Templates/content/Company.AppName.Infra/Services/PulumiLogger.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/PulumiLogger.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/Services/PulumiLogger.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/Services/PulumiLogger.cs diff --git a/src/Templates/content/Company.AppName.Infra/StackConfiguration.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/StackConfiguration.cs similarity index 100% rename from src/Templates/content/Company.AppName.Infra/StackConfiguration.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/StackConfiguration.cs diff --git a/src/Templates/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj b/tools/CoreEx.Template/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj diff --git a/src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml b/tools/CoreEx.Template/content/Company.AppName.UnitTest/Data/Data.yaml similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/Data/Data.yaml rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/Data/Data.yaml diff --git a/src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest.cs b/tools/CoreEx.Template/content/Company.AppName.UnitTest/EmployeeControllerTest.cs similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest.cs rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/EmployeeControllerTest.cs diff --git a/src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs b/tools/CoreEx.Template/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/EmployeeControllerTest2.cs diff --git a/src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs b/tools/CoreEx.Template/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/EmployeeFunctionTest.cs diff --git a/src/Templates/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs b/tools/CoreEx.Template/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/HttpTriggerQueueVerificationFunctionTest.cs diff --git a/src/Templates/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs b/tools/CoreEx.Template/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/ReferenceDataControllerTest.cs diff --git a/src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json b/tools/CoreEx.Template/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/Resources/VerificationResult.Unix.json diff --git a/src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json b/tools/CoreEx.Template/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/Resources/VerificationResult.Win32.json diff --git a/src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs b/tools/CoreEx.Template/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/ServiceBusExecuteVerificationFunctionTest.cs diff --git a/src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json b/tools/CoreEx.Template/content/Company.AppName.UnitTest/appsettings.unittest.json similarity index 100% rename from src/Templates/content/Company.AppName.UnitTest/appsettings.unittest.json rename to tools/CoreEx.Template/content/Company.AppName.UnitTest/appsettings.unittest.json diff --git a/src/Templates/content/Company.AppName.sln b/tools/CoreEx.Template/content/Company.AppName.sln similarity index 100% rename from src/Templates/content/Company.AppName.sln rename to tools/CoreEx.Template/content/Company.AppName.sln diff --git a/src/Templates/content/Docker.md b/tools/CoreEx.Template/content/Docker.md similarity index 100% rename from src/Templates/content/Docker.md rename to tools/CoreEx.Template/content/Docker.md diff --git a/src/Templates/content/docker-compose.DB.only.yml b/tools/CoreEx.Template/content/docker-compose.DB.only.yml similarity index 100% rename from src/Templates/content/docker-compose.DB.only.yml rename to tools/CoreEx.Template/content/docker-compose.DB.only.yml diff --git a/src/Templates/content/docker-compose.override.yml b/tools/CoreEx.Template/content/docker-compose.override.yml similarity index 100% rename from src/Templates/content/docker-compose.override.yml rename to tools/CoreEx.Template/content/docker-compose.override.yml diff --git a/src/Templates/content/docker-compose.yml b/tools/CoreEx.Template/content/docker-compose.yml similarity index 100% rename from src/Templates/content/docker-compose.yml rename to tools/CoreEx.Template/content/docker-compose.yml diff --git a/src/Templates/readme.md b/tools/CoreEx.Template/readme.md similarity index 100% rename from src/Templates/readme.md rename to tools/CoreEx.Template/readme.md From 71f4f47538088a5d3af4b9018c5e74678419f2d5 Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 21:11:03 -0400 Subject: [PATCH 25/39] update to CI Signed-off-by: Piotr --- .github/workflows/CI.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3bce99f0..298edb9f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -67,7 +67,7 @@ jobs: run: docker-compose -f docker-compose.myHr.yml -f docker-compose.myHr.override.yml build --build-arg LOCAL=true - name: Build Template - run: dotnet build src/Templates/content + run: dotnet build tools/CoreEx.Template/content - - name: Test Template - run: dotnet test src/Templates/content --filter Category!=WithCosmos --no-build --verbosity normal /p:CollectCoverage=true /p:Exclude="[CoreEx.TestFunction]*" /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov3.info + - name: Test generic Template + run: dotnet test tools/CoreEx.Template/content --filter Category!=WithCosmos --no-build --verbosity normal /p:CollectCoverage=true /p:Exclude="[CoreEx.TestFunction]*" /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov3.info From d53ab46ce02ec7dedeaeab6caf381238f8c98c93 Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 28 Sep 2022 21:31:39 -0400 Subject: [PATCH 26/39] build template Signed-off-by: Piotr --- .../.azuredevops/pipeline-build copy.yml | 85 --------- .../content/.azuredevops/pipeline-build.yml | 162 ++++++++++++------ .../Company.AppName.Infra.Tests.csproj | 9 +- .../Company.AppName.UnitTest.csproj | 9 +- 4 files changed, 121 insertions(+), 144 deletions(-) delete mode 100644 tools/CoreEx.Template/content/.azuredevops/pipeline-build copy.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-build copy.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-build copy.yml deleted file mode 100644 index ea9ddf32..00000000 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-build copy.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: $(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) - -pool: - vmImage: windows-latest - -variables: - buildConfiguration: 'Release' - -trigger: - branches: - include: - - develop - - main - - test - - preprod - -steps: -- task: CopyFiles@2 - displayName: Copy Files - inputs: - contents: $(Build.Repository.LocalPath)/** - targetFolder: $(Build.ArtifactStagingDirectory) - -- task: NuGetAuthenticate@0 - -- task: NuGetToolInstaller@1 - displayName: 'NuGet tool installer' - -- task: Cache@2 - continueOnError: true - displayName: 'NuGet Cache' - inputs: - key: 'nuget | "$(Agent.OS)" | **/packages.lock.json' - path: '' - cacheHitVar: 'CACHE_RESTORED' - -- task: DotNetCoreCLI@2 - displayName: Restore - condition: ne(variables.CACHE_RESTORED, true) - inputs: - command: 'restore' - projects: '**/source/*.sln' - feedsToUse: 'select' - vstsFeed: '5c1bf9a9-e22e-4f76-9993-1a6b447130f8/c1502c9b-c232-4f35-8045-b01575cce6bc' - -- task: DotNetCoreCLI@2 - displayName: Build - inputs: - command: build - projects: '**/source/*.sln' - arguments: '--configuration $(buildConfiguration)' - -- task: DotNetCoreCLI@2 - displayName: 'Install .NET tools from local manifest' - inputs: - command: custom - custom: tool - arguments: 'restore' - -- task: DotNetCoreCLI@2 - displayName: 'Run unit tests - $(buildConfiguration)' - inputs: - command: 'test' - arguments: '--no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' - publishTestResults: true - projects: '**/*.UnitTests.csproj' - -- task: PublishCodeCoverageResults@1 - displayName: 'Publish code coverage report' - inputs: - codeCoverageTool: 'Cobertura' - summaryFileLocation: '$(Build.SourcesDirectory)/TestResults/Coverage/**/coverage.cobertura.xml' - -- task: DotNetCoreCLI@2 - inputs: - command: 'publish' - projects: '**/source/Nte.Int.D365.Employee.ScimApp/Nte.Int.D365.Employee.ScimApp.csproj' - publishWebProjects: false - arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' - -- task: PublishBuildArtifacts@1 - inputs: - pathToPublish: '$(Build.ArtifactStagingDirectory)/Nte.Int.D365.Employee.ScimApp.zip' - artifactName: D365-Employee-$(Build.BuildId) - diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml index 629beca8..79426e6f 100644 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml @@ -1,7 +1,7 @@ name: $(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) pool: - vmImage: windows-latest + vmImage: ubuntu-latest variables: buildConfiguration: 'Release' @@ -14,59 +14,107 @@ trigger: - test - preprod -steps: -- task: CopyFiles@2 - displayName: Copy Files - inputs: - contents: $(Build.Repository.LocalPath)/** - targetFolder: $(Build.ArtifactStagingDirectory) - -- task: NuGetAuthenticate@0 - -- task: DotNetCoreCLI@2 - displayName: Restore - inputs: - command: 'restore' - projects: '**/source/*.sln' - feedsToUse: 'select' - vstsFeed: '5c1bf9a9-e22e-4f76-9993-1a6b447130f8/c1502c9b-c232-4f35-8045-b01575cce6bc' - -- task: DotNetCoreCLI@2 - displayName: Build - inputs: - command: build - projects: '**/source/*.sln' - arguments: '--configuration $(buildConfiguration)' - -# - task: DotNetCoreCLI@2 -# displayName: 'Install .NET tools from local manifest' -# inputs: -# command: custom -# custom: tool -# arguments: 'restore' - -- task: DotNetCoreCLI@2 - displayName: 'Run unit tests - $(buildConfiguration)' - inputs: - command: 'test' - arguments: '--no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' - publishTestResults: true - projects: '**/*.UnitTests.csproj' - -- task: PublishCodeCoverageResults@1 - displayName: 'Publish code coverage report' - inputs: - codeCoverageTool: 'Cobertura' - summaryFileLocation: '$(Build.SourcesDirectory)/TestResults/Coverage/**/coverage.cobertura.xml' - -- task: DotNetCoreCLI@2 - inputs: - command: 'publish' - projects: '**/source/Nte.Int.D365.Apps.FunctionApps/Nte.Int.D365.Apps.FunctionApps.csproj' - publishWebProjects: false - arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' - -- task: PublishBuildArtifacts@1 - inputs: - pathToPublish: '$(Build.ArtifactStagingDirectory)/Nte.Int.D365.Apps.FunctionApps.zip' - artifactName: D365-Apps_Package +jobs: +- job: BuildJob + displayName: Build, Test and Publish apps + workspace: + clean: outputs + steps: + - task: CopyFiles@2 + displayName: Copy Files + inputs: + contents: $(Build.Repository.LocalPath)/** + targetFolder: $(Build.ArtifactStagingDirectory) + + - task: NuGetAuthenticate@0 + + - task: DotNetCoreCLI@2 + displayName: Restore + inputs: + command: 'restore' + projects: '*.sln' + # vstsFeed: '5c1bf9a9-e22e-4f76-9993-1a6b447130f8/c1502c9b-c232-4f35-8045-b01575cce6bc' # uncomment when using ADO Artifacts feed + + - task: DotNetCoreCLI@2 + displayName: Build + inputs: + command: build + projects: '*.sln' + arguments: '--configuration $(buildConfiguration)' + + - task: Bash@3 + displayName: 'Run SQL Server in docker container' + inputs: + targetType: 'inline' + script: | + docker pull mcr.microsoft.com/mssql/server:2022-latest + docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=sAPWD23.^0" --name sqlserver -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest + + # wait for DB to start + chmod +x ./Company.AppName.Database/wait-for-it.sh + ./Company.AppName.Database/wait-for-it.sh localhost:1433 -t 30 -- sleep 10 && echo "db is up" + + echo '##vso[task.setvariable variable=ConnectionStrings__Database]Data Source=localhost,1433;Initial Catalog=My.Hr;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true' + + mkdir -p $(Build.SourcesDirectory)/TestResults/Coverage/ + + - task: DotNetCoreCLI@2 + displayName: 'Run unit tests - $(buildConfiguration)' + inputs: + command: 'test' + arguments: '-c $(buildConfiguration) --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' + publishTestResults: true + projects: '**/*.UnitTest.csproj' + + - task: DotNetCoreCLI@2 + displayName: 'Run infrastructure tests - $(buildConfiguration)' + inputs: + command: 'test' + arguments: '-c $(buildConfiguration) --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' + publishTestResults: true + projects: '**/*.Infra.Tests.csproj' + + - task: Bash@3 + displayName: 'display code coverage files' + inputs: + targetType: 'inline' + script: | + # Write your commands here + + ls -R $(Build.SourcesDirectory)/TestResults/Coverage/ + + - task: PublishCodeCoverageResults@1 + displayName: 'Publish code coverage report' + inputs: + codeCoverageTool: 'Cobertura' + summaryFileLocation: '$(Build.SourcesDirectory)/TestResults/Coverage/**/*.cobertura.xml' + pathToSources: '$(Build.SourcesDirectory)' + reportDirectory: '$(Build.SourcesDirectory)/TestResults/Report' + + - task: DotNetCoreCLI@2 + displayName: 'Publish function app' + inputs: + command: 'publish' + projects: '**/Company.AppName.Functions.csproj' + publishWebProjects: false + arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish function app artifact' + inputs: + pathToPublish: '$(Build.ArtifactStagingDirectory)/Company.AppName.Functions.zip' + artifactName: Company-AppName-Functions_Package + + - task: DotNetCoreCLI@2 + displayName: 'Publish appservice app' + inputs: + command: 'publish' + projects: '**/Company.AppName.Api.csproj' + publishWebProjects: false + arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish appservice app artifact' + inputs: + pathToPublish: '$(Build.ArtifactStagingDirectory)/Company.AppName.Api.zip' + artifactName: Company-AppName-Api_Package \ No newline at end of file diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj index 351a6dc5..e8f1ea7a 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj +++ b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Company.AppName.Infra.Tests.csproj @@ -15,7 +15,14 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/tools/CoreEx.Template/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj b/tools/CoreEx.Template/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj index 47dddf38..f290bc13 100644 --- a/tools/CoreEx.Template/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj +++ b/tools/CoreEx.Template/content/Company.AppName.UnitTest/Company.AppName.UnitTest.csproj @@ -23,7 +23,14 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + From aa81bbca051549b005ed6adf32fe3abd34a95370 Mon Sep 17 00:00:00 2001 From: Piotr Date: Thu, 29 Sep 2022 14:37:30 -0400 Subject: [PATCH 27/39] removing extra files Signed-off-by: Piotr --- .../content/.azuredevops/app-template.yml | 53 ------------- .../.azuredevops/function-template.yml | 76 ------------------- .../content/.azuredevops/infra-template.yml | 61 --------------- .../content/.azuredevops/pipeline-build.yml | 4 +- .../.azuredevops/pipeline-release copy.yml | 58 -------------- .../content/.azuredevops/pipeline-release.yml | 69 ----------------- 6 files changed, 2 insertions(+), 319 deletions(-) delete mode 100644 tools/CoreEx.Template/content/.azuredevops/app-template.yml delete mode 100644 tools/CoreEx.Template/content/.azuredevops/function-template.yml delete mode 100644 tools/CoreEx.Template/content/.azuredevops/infra-template.yml delete mode 100644 tools/CoreEx.Template/content/.azuredevops/pipeline-release copy.yml delete mode 100644 tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/app-template.yml b/tools/CoreEx.Template/content/.azuredevops/app-template.yml deleted file mode 100644 index 9773edb0..00000000 --- a/tools/CoreEx.Template/content/.azuredevops/app-template.yml +++ /dev/null @@ -1,53 +0,0 @@ -parameters: -- name: AzureSubscription - type: string -- name: env - type: string - displayName: 'Environment shorthand - dev, test, etc.' -- name: ado_environment - type: string - displayName: 'Name of ADO environment to deploy to' -- name: AppSrvName - type: string - displayName: 'Name of azure app service to deploy to' - - -jobs: -- deployment: Deploy - displayName: "Deploy ${{ parameters.env }}" - environment: ${{ parameters.ado_environment }} - variables: - - name: global_buildDate - value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] - - group: VG-D365-${{ parameters.env }} - pool: - vmImage: $(vmImageName) - strategy: - runOnce: - deploy: - steps: - - task: AzureWebApp@1 - displayName: 'Deployment app ${{ parameters.AppSrvName }} to env: ${{ parameters.env }}' - inputs: - azureSubscription: '${{ parameters.AzureSubscription }}' - appType: 'webApp' - appName: '${{ parameters.AppSrvName }}' - deploymentMethod: zipDeploy - package: '$(Pipeline.Workspace)/**/Nte.Int.D365.Employee.ScimApp.zip' - appSettings: '-Deployment.By "$(Build.RequestedForEmail)" - -Deployment.Build "$(resources.pipeline.D365-Employee.runName)" - -Deployment.Name "$(Build.BuildNumber)" - -Deployment.Version "$(resources.pipeline.D365-Employee.sourceBranch)-$(resources.pipeline.D365-Employee.sourceCommit)" - -Deployment.Date "$(global_buildDate)" - -AppConfigConnectionString "$(AppConfigConnectionString)" - -ApplicationName "D365-Employee"' - - - task: AzureCLI@2 - displayName: Tag Azure - inputs: - azureSubscription: '${{ parameters.AzureSubscription }}' - scriptType: ps - scriptLocation: inlineScript - inlineScript: | - $fun_id=$(az resource show --name ${{ parameters.AppSrvName }} -g $(ResourceGroupName) --resource-type "Microsoft.Web/sites" --query id --output tsv) - az tag update --resource-id $fun_id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.D365-Employee.runName)"' 'Deployment.Version="$(resources.pipeline.D365-Employee.sourceBranch)-$(resources.pipeline.D365-Employee.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' diff --git a/tools/CoreEx.Template/content/.azuredevops/function-template.yml b/tools/CoreEx.Template/content/.azuredevops/function-template.yml deleted file mode 100644 index ae59a437..00000000 --- a/tools/CoreEx.Template/content/.azuredevops/function-template.yml +++ /dev/null @@ -1,76 +0,0 @@ -parameters: -- name: AzureSubscription - type: string -- name: env - type: string - displayName: 'Environment shorthand - dev, test, etc.' -- name: ado_environment - type: string - displayName: 'Name of ADO environment to deploy to' -- name: FunctionAppName - type: string - displayName: 'Name of azure function to deploy to' -- name: MovementJournalQueueName - type: string - displayName: 'Movement Journal Queue Name' -- name: GoogleInventoryFullFeedTimer - type: string - displayName: 'Google Inventory Full Feed Timer' -# - name: PricingSubscriberQueueName -# type: string -# displayName: 'Pricing Subscriber Queue Name' -# - name: AppsQueueName -# type: string -# displayName: 'Name of ADO environment to deploy to' - -jobs: -- deployment: Deploy - displayName: "Deploy ${{ parameters.env }}" - environment: ${{ parameters.ado_environment }} - variables: - - name: global_buildDate - value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] - - group: VG-D365-${{ parameters.env }} - pool: - vmImage: $(vmImageName) - strategy: - runOnce: - deploy: - steps: - - - task: AzureFunctionApp@1 - displayName: 'Deployment az fun ${{ parameters.FunctionAppName }} to env: ${{ parameters.env }}' - inputs: - azureSubscription: '${{ parameters.AzureSubscription }}' - appType: 'functionApp' - appName: '${{ parameters.FunctionAppName }}' - # deployToSlotOrASE: true - resourceGroupName: '$(ResourceGroupName)' - #slotName: '$(DeploymentSlot)' - package: '$(Pipeline.Workspace)/D365-Apps/D365-Apps_Package/Nte.Int.D365.Apps.FunctionApps.zip' - deploymentMethod: 'auto' - appSettings: '-Deployment.By "$(Build.RequestedForEmail)" - -Deployment.Build "$(resources.pipeline.D365-Apps.runName)" - -Deployment.Name "$(Build.BuildNumber)" - -Deployment.Version "$(resources.pipeline.D365-Apps.sourceBranch)-$(resources.pipeline.D365-Apps.sourceCommit)" - -Deployment.Date "$(global_buildDate)" - - -ApplicationName "D365-Apps" - -MovementJournalQueueName "${{parameters.MovementJournalQueueName}}" - -GoogleInventoryFullFeedTimer "${{parameters.GoogleInventoryFullFeedTimer}}" - -AppConfigConnectionString "$(AppConfigConnectionString)" - -ServiceBusConnection__fullyQualifiedNamespace "$(ServiceBusConnection__fullyQualifiedNamespace)" - -Publisher_ServiceBusConnection "$(ServiceBusConnection__fullyQualifiedNamespace)" - -AzureFunctionsJobHost__logging__logLevel__default "Warning" - -AzureFunctionsJobHost__logging__logLevel__Nte__Int__D365__Common "Information"' - - - task: AzureCLI@2 - displayName: Tag Azure - inputs: - azureSubscription: '${{ parameters.AzureSubscription }}' - scriptType: ps - scriptLocation: inlineScript - inlineScript: | - $fun_id=$(az resource show --name ${{ parameters.FunctionAppName }} -g $(ResourceGroupName) --resource-type "Microsoft.Web/sites" --query id --output tsv) - az tag update --resource-id $fun_id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.D365-Apps.runName)"' 'Deployment.Version="$(resources.pipeline.D365-Apps.sourceBranch)-$(resources.pipeline.D365-Apps.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' - diff --git a/tools/CoreEx.Template/content/.azuredevops/infra-template.yml b/tools/CoreEx.Template/content/.azuredevops/infra-template.yml deleted file mode 100644 index 52d74e3e..00000000 --- a/tools/CoreEx.Template/content/.azuredevops/infra-template.yml +++ /dev/null @@ -1,61 +0,0 @@ -parameters: -- name: AzureSubscription - type: string -- name: env - type: string - displayName: 'Environment shorthand - dev, test, etc.' -- name: ado_environment - type: string - displayName: 'Name of ADO environment to deploy to' -- name: AppSrvName - type: string - displayName: 'Name of azure app service to deploy to' - - -jobs: -- deployment: Deploy - displayName: "Deploy ${{ parameters.env }}" - environment: ${{ parameters.ado_environment }} - variables: - - name: global_buildDate - value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] - - group: VG-D365-${{ parameters.env }} - pool: - vmImage: $(vmImageName) - strategy: - runOnce: - deploy: - steps: - - task: Bash@3 - displayName: 'Install Pulumi' - inputs: - targetType: 'inline' - script: | - curl -fsSL https://get.pulumi.com | sh - # export PATH="${PATH}:/root/.pulumi/bin" - echo '##vso[task.prependpath]/root/.pulumi/bin' - - task: AzureWebApp@1 - displayName: 'Deployment app ${{ parameters.AppSrvName }} to env: ${{ parameters.env }}' - inputs: - azureSubscription: '${{ parameters.AzureSubscription }}' - appType: 'webApp' - appName: '${{ parameters.AppSrvName }}' - deploymentMethod: zipDeploy - package: '$(Pipeline.Workspace)/**/Nte.Int.D365.Employee.ScimApp.zip' - appSettings: '-Deployment.By "$(Build.RequestedForEmail)" - -Deployment.Build "$(resources.pipeline.D365-Employee.runName)" - -Deployment.Name "$(Build.BuildNumber)" - -Deployment.Version "$(resources.pipeline.D365-Employee.sourceBranch)-$(resources.pipeline.D365-Employee.sourceCommit)" - -Deployment.Date "$(global_buildDate)" - -AppConfigConnectionString "$(AppConfigConnectionString)" - -ApplicationName "D365-Employee"' - - - task: AzureCLI@2 - displayName: Tag Azure - inputs: - azureSubscription: '${{ parameters.AzureSubscription }}' - scriptType: ps - scriptLocation: inlineScript - inlineScript: | - $fun_id=$(az resource show --name ${{ parameters.AppSrvName }} -g $(ResourceGroupName) --resource-type "Microsoft.Web/sites" --query id --output tsv) - az tag update --resource-id $fun_id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.D365-Employee.runName)"' 'Deployment.Version="$(resources.pipeline.D365-Employee.sourceBranch)-$(resources.pipeline.D365-Employee.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml index 79426e6f..d83cde21 100644 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml @@ -62,7 +62,7 @@ jobs: displayName: 'Run unit tests - $(buildConfiguration)' inputs: command: 'test' - arguments: '-c $(buildConfiguration) --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' + arguments: '-c $(buildConfiguration) --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/coverageApp.json' publishTestResults: true projects: '**/*.UnitTest.csproj' @@ -70,7 +70,7 @@ jobs: displayName: 'Run infrastructure tests - $(buildConfiguration)' inputs: command: 'test' - arguments: '-c $(buildConfiguration) --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' + arguments: '-c $(buildConfiguration) --no-build --verbosity normal /p:MergeWith="$(Build.SourcesDirectory)/TestResults/Coverage/coverageApp.json" /p:CollectCoverage=true /p:CoverletOutputFormat="cobertura,json" /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' publishTestResults: true projects: '**/*.Infra.Tests.csproj' diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-release copy.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-release copy.yml deleted file mode 100644 index 4e3fb9c6..00000000 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-release copy.yml +++ /dev/null @@ -1,58 +0,0 @@ -variables: -- name: vmImageName - value: 'windows-latest' - -# Explicitly set none for repository trigger -trigger: -- none - -resources: - pipelines: - - pipeline: 'D365-Employee' # Name of the pipeline resource - source: 'D365-Employee' # Name of the triggering pipeline - trigger: - branches: - - develop - - test - - preprod - - main - -stages: -- stage: Dev - jobs: - - template: app-template.yml - parameters: - AzureSubscription: rg-priv-inte-dev-nte-na-01 - env: dev - ado_environment: development - AppSrvName: appsvc-priv-inte-dataload-dev-nte-na-01 - -- stage: Test - condition: or(eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/test'), eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/preprod'), eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/main')) - jobs: - - template: app-template.yml - parameters: - AzureSubscription: rg-priv-inte-test-nte-na-01 - env: test - ado_environment: D365-test - AppSrvName: appsvc-priv-inte-dataload-test-nte-na-01 - -- stage: PreProd - condition: or(eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/preprod'), eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/main')) - jobs: - - template: app-template.yml - parameters: - AzureSubscription: rg-priv-inte-preprod-nte-na-01 - env: preprod - ado_environment: D365-preprod - AppSrvName: appsvc-priv-inte-dataload-preprod-nte-na-01 - -- stage: Prod - condition: eq(variables['resources.pipeline.D365-Employee.sourceBranch'], 'refs/heads/main') - jobs: - - template: app-template.yml - parameters: - AzureSubscription: rg-priv-inte-prod-nte-na-01 - env: prod - ado_environment: D365-prod - AppSrvName: appsvcpl-priv-inte-prod-nte-na-01 diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml deleted file mode 100644 index 56108fa3..00000000 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: D365-Apps-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) - -variables: -- name: vmImageName - value: 'windows-latest' - -# Explicitly set none for repository trigger -trigger: none - -resources: - pipelines: - - pipeline: 'D365-Apps' # Name of the pipeline resource - source: 'D365-Apps' # Name of the triggering pipeline - trigger: - branches: - - develop - - test - - preprod - - main - -# Note: Azure Service connection passed as param due to: https://developercommunity.visualstudio.com/t/using-a-variable-for-the-service-connection-result/676259 - -stages: -- stage: Dev - jobs: - - template: function-template.yml - parameters: - AzureSubscription: rg-priv-inte-dev-nte-na-01 - env: dev - ado_environment: development - FunctionAppName: fnc-priv-inte-dev-nte-na-34 - MovementJournalQueueName: d365-apps-movementjournal - GoogleInventoryFullFeedTimer: 0 */10 * * * * - -- stage: Test - condition: or(eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/test'), eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/preprod'), eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/main')) - jobs: - - template: function-template.yml - parameters: - AzureSubscription: rg-priv-inte-test-nte-na-01 - env: test - ado_environment: D365-test - FunctionAppName: fnc-priv-inte-test-nte-na-34 - MovementJournalQueueName: d365-apps-movementjournal - GoogleInventoryFullFeedTimer: 0 0 * * * * - -- stage: Preprod - condition: or(eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/preprod'), eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/main')) - jobs: - - template: function-template.yml - parameters: - env: preprod - AzureSubscription: rg-priv-inte-preprod-nte-na-01 - ado_environment: D365-preprod - FunctionAppName: fnc-priv-inte-preprod-nte-na-34 - MovementJournalQueueName: d365-apps-movementjournal - GoogleInventoryFullFeedTimer: 0 0 * * * * - -- stage: Prod - condition: eq(variables['resources.pipeline.D365-Apps.sourceBranch'], 'refs/heads/main') - jobs: - - template: function-template.yml - parameters: - env: prod - AzureSubscription: rg-priv-inte-prod-nte-na-01 - ado_environment: D365-prod - FunctionAppName: fnc-priv-inte-prod-nte-na-34 - MovementJournalQueueName: d365-apps-movementjournal - GoogleInventoryFullFeedTimer: 0 0 0 * * * \ No newline at end of file From 237cdd64aceb7f2b43a179f1d5c95bb8040cb4a4 Mon Sep 17 00:00:00 2001 From: Piotr Date: Thu, 29 Sep 2022 16:35:53 -0400 Subject: [PATCH 28/39] more generic names Signed-off-by: Piotr --- .../{CoreExStack.cs => CompanyAppNameStack.cs} | 8 ++++---- .../content/Company.AppName.Infra/Components/Sql.cs | 2 +- .../content/Company.AppName.Infra/Program.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename tools/CoreEx.Template/content/Company.AppName.Infra/{CoreExStack.cs => CompanyAppNameStack.cs} (92%) diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/CoreExStack.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs similarity index 92% rename from tools/CoreEx.Template/content/Company.AppName.Infra/CoreExStack.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs index 66290b3f..4a363193 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/CoreExStack.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs @@ -8,26 +8,26 @@ namespace Company.AppName.Infra; -public static class CoreExStack +public static class CompanyAppNameStack { public static async Task> ExecuteStackAsync(IDbOperations dbOperations, HttpClient client) { var config = await StackConfiguration.CreateConfiguration(); Log.Info("Configuration completed"); - var tags = new InputMap { { "App", "CoreEx" } }; + var tags = new InputMap { { "App", "Company-AppName" } }; // Create Azure API client for direct HTTP calls var azureApiClient = new AzureApiClient(client); var azureApiService = new AzureApiService(azureApiClient); // Create an Azure Resource Group - var resourceGroup = new ResourceGroup($"coreEx-{Pulumi.Deployment.Instance.StackName}", new ResourceGroupArgs + var resourceGroup = new ResourceGroup($"Company-AppName-{Pulumi.Deployment.Instance.StackName}", new ResourceGroupArgs { Tags = tags }); - var serviceBus = new Components.Messaging("coreExBus", new Components.Messaging.MessagingArgs + var serviceBus = new Components.Messaging("CompanyAppNameBus", new Components.Messaging.MessagingArgs { ResourceGroupName = resourceGroup.Name, Tags = tags diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Sql.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Sql.cs index 9ec33c3f..0b899a9d 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Sql.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Sql.cs @@ -57,7 +57,7 @@ public Sql(string name, SqlArgs args, IDbOperations dbOperations, AzureApiClient { ResourceGroupName = args.ResourceGroupName, ServerName = sqlServer.Name, - DatabaseName = "CoreExDB", + DatabaseName = "CompanyAppNameDB", Sku = new SkuArgs { Name = "Basic" diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/Program.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Program.cs index 474e893b..6686fa1b 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/Program.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/Program.cs @@ -7,7 +7,7 @@ // running with using statement (to Dispose) doesn't work with Pulumi var client = new System.Net.Http.HttpClient(); // create and use actual instance of DB Operations service - return Company.AppName.Infra.CoreExStack.ExecuteStackAsync(new DbOperations(), client); + return Company.AppName.Infra.CompanyAppNameStack.ExecuteStackAsync(new DbOperations(), client); }, new StackOptions { // apply auto-tagging transformation From 08fbade547437817359870adc614b6f712f0ee7c Mon Sep 17 00:00:00 2001 From: Piotr Date: Thu, 29 Sep 2022 16:42:54 -0400 Subject: [PATCH 29/39] stack rename Signed-off-by: Piotr --- .../{CoreExStackTests.cs => CompanyAppNameStackTests.cs} | 2 +- .../content/Company.AppName.Infra.Tests/Testing.cs | 6 +++--- .../Company.AppName.Infra/Services/AzureApiService.cs | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) rename tools/CoreEx.Template/content/Company.AppName.Infra.Tests/{CoreExStackTests.cs => CompanyAppNameStackTests.cs} (96%) diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/CoreExStackTests.cs b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/CompanyAppNameStackTests.cs similarity index 96% rename from tools/CoreEx.Template/content/Company.AppName.Infra.Tests/CoreExStackTests.cs rename to tools/CoreEx.Template/content/Company.AppName.Infra.Tests/CompanyAppNameStackTests.cs index 8982e84b..e021755e 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/CoreExStackTests.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/CompanyAppNameStackTests.cs @@ -4,7 +4,7 @@ namespace Company.AppName.Infra.Tests; -public class CoreExStackTests +public class CompanyAppNameStackTests { [Test] public async Task ResourceGroupHasNameTag() diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Testing.cs b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Testing.cs index 699a6666..bd3fe58d 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Testing.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra.Tests/Testing.cs @@ -17,16 +17,16 @@ public static class Testing var mcf = MockHttpClientFactory.Create(); var mc = mcf.CreateClient("azure"); - mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{SubscriptionId}/resourceGroups/coreEx-{StackName}/providers/Microsoft.Web/sites/funApp/host/default/listkeys?api-version=2022-03-01") + mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{SubscriptionId}/resourceGroups/Company-AppName-{StackName}/providers/Microsoft.Web/sites/funApp/host/default/listkeys?api-version=2022-03-01") .Respond.WithJson(new AzureApiService.HostKeys { FunctionKeys = new AzureApiService.FunctionKeysValue { Key = "mocked-key" } }); mc.Request(HttpMethod.Get, "https://api.ipify.org") .Respond.With(new StringContent("215.45.1.567")); - mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{SubscriptionId}/resourceGroups/coreEx-{StackName}/providers/Microsoft.Web/sites/funApp/syncfunctiontriggers?api-version=2016-08-01") + mc.Request(HttpMethod.Post, $"https://management.azure.com/subscriptions/{SubscriptionId}/resourceGroups/Company-AppName-{StackName}/providers/Microsoft.Web/sites/funApp/syncfunctiontriggers?api-version=2016-08-01") .Respond.With(statusCode: System.Net.HttpStatusCode.NoContent); - var (resources, outputs) = await RunAsync(() => CoreExStack.ExecuteStackAsync(dbOperationsMock.Object, mcf.GetHttpClient("azure")!)); + var (resources, outputs) = await RunAsync(() => CompanyAppNameStack.ExecuteStackAsync(dbOperationsMock.Object, mcf.GetHttpClient("azure")!)); return (resources, outputs, dbOperationsMock); } diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/Services/AzureApiService.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/AzureApiService.cs index a6dbd06b..19e4eb44 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/Services/AzureApiService.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/AzureApiService.cs @@ -42,6 +42,10 @@ public Output GetHostKeys(Output rgName, Output function { return Output.Tuple(rgName, functionName).Apply(async t => { + // do not call in preview + if (Deployment.Instance.IsDryRun) + return "tbd"; + var (resourceGroupName, siteName) = t; Log.Info("Getting host keys for: " + siteName); @@ -62,6 +66,10 @@ public Output SyncFunctionAppTriggers(Output rgName, Output { + // do not call in preview + if (Deployment.Instance.IsDryRun) + return true; + var (resourceGroupName, siteName) = t; Log.Info("Syncing Function App triggers"); From a17e27dc01d47d9cf8626772c457782213256aae Mon Sep 17 00:00:00 2001 From: Piotr Date: Fri, 30 Sep 2022 11:33:21 -0400 Subject: [PATCH 30/39] infra deployments Signed-off-by: Piotr --- .../.azuredevops/infra-destroy-template.yml | 76 +++++++++ .../content/.azuredevops/infra-template.yml | 145 ++++++++++++++++++ .../content/.azuredevops/pipeline-build.yml | 2 +- .../content/.azuredevops/pipeline-infra.yml | 33 ++++ tools/CoreEx.Template/content/.gitignore | 2 +- .../CompanyAppNameStack.cs | 3 + .../Company.AppName.Infra/Components/Apps.cs | 4 + .../Services/DbOperations.cs | 10 +- .../content/docker-compose.DB.only.yml | 2 - 9 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml create mode 100644 tools/CoreEx.Template/content/.azuredevops/infra-template.yml create mode 100644 tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml b/tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml new file mode 100644 index 00000000..cac2cac9 --- /dev/null +++ b/tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml @@ -0,0 +1,76 @@ +# requires following env variables: +# * secret variable - PULUMI_CONFIG_PASSPHRASE - KeyVault can be used instead of pass phrase, see: https://www.pulumi.com/docs/intro/concepts/secrets/#azure-key-vault + +parameters: +- name: AzureSubscription + type: string + displayName: 'Name of the Azure Service Connection' +- name: storageAccountName + type: string + displayName: 'Name of the storage account used for pulumi state' +- name: env + type: string + displayName: 'Environment name - dev, test, etc. Can be longer e.g. Company-AppName-dev' +- name: region + type: string + default: eastus + displayName: 'Azure region to deploy to' + +jobs: +- deployment: Destroy + displayName: "Destroy infrastructure ${{ parameters.env }}" + environment: ${{ parameters.env }} + variables: + - name: global_buildDate + value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] + - name: stack_name + value: ${{ parameters.env }} # can modify stack name here + # - group: VG-Company-AppName-${{ parameters.env }} # uncomment to use variable groups in ADO + pool: + vmImage: ubuntu-latest + timeoutInMinutes: 1500 # job times out in 1 day and 1h + strategy: + runOnce: + deploy: + steps: + + - task: Bash@3 + displayName: 'Install Pulumi' + inputs: + targetType: 'inline' + script: | + curl -fsSL https://get.pulumi.com | sh + + echo "##vso[task.prependpath]$HOME/.pulumi/bin" + pulumi about + + - task: ManualValidation@0 + timeoutInMinutes: 1440 # task times out in 1 day + inputs: + # notifyUsers: | + # test@test.com + # example@example.com + instructions: 'Please confirm pulumi stack ${{ variables.stack_name }} can be destroyed' + onTimeout: 'reject' + + - task: AzureCLI@2 + displayName: Destroy Pulumi stack ${{ variables.stack_name }} + env: + PULUMI_CONFIG_PASSPHRASE: $(PULUMI_CONFIG_PASSPHRASE) + inputs: + scriptType: bash + azureSubscription: '${{ parameters.AzureSubscription }}' + scriptLocation: inlineScript + inlineScript: | + export AZURE_STORAGE_ACCOUNT="${{ parameters.storageAccountName }}" + export AZURE_STORAGE_KEY=$(az storage account keys list -n "$AZURE_STORAGE_ACCOUNT" --query "[0].value" -o tsv) + export ARM_CLIENT_ID=$servicePrincipalId + export ARM_CLIENT_SECRET=$servicePrincipalKey + export ARM_TENANT_ID=$tenantId + export ARM_SUBSCRIPTION_ID=$(az account show | jq '.id' --raw-output) + + # init pulumi stack + pulumi login --cloud-url azblob://state + + # select/create stack + pulumi destroy -s '${{ variables.stack_name }}' -y \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.azuredevops/infra-template.yml b/tools/CoreEx.Template/content/.azuredevops/infra-template.yml new file mode 100644 index 00000000..0e4fe343 --- /dev/null +++ b/tools/CoreEx.Template/content/.azuredevops/infra-template.yml @@ -0,0 +1,145 @@ +# requires following env variables: +# * secret variable - PULUMI_CONFIG_PASSPHRASE - KeyVault can be used instead of pass phrase, see: https://www.pulumi.com/docs/intro/concepts/secrets/#azure-key-vault + +parameters: +- name: AzureSubscription + type: string + displayName: 'Name of the Azure Service Connection' +- name: storageAccountName + type: string + displayName: 'Name of the storage account used for pulumi state' +- name: env + type: string + displayName: 'Environment name - dev, test, etc. Can be longer e.g. Company-AppName-dev' +- name: region + type: string + default: eastus + displayName: 'Azure region to deploy to' + +jobs: +- deployment: Deploy + displayName: "Deploy infrastructure ${{ parameters.env }}" + environment: ${{ parameters.env }} + variables: + - name: global_buildDate + value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] + - name: stack_name + value: Company-AppName-${{ parameters.env }} # can modify stack name here + # - group: VG-Company-AppName-${{ parameters.env }} # uncomment to use variable groups in ADO + pool: + vmImage: ubuntu-latest + strategy: + runOnce: + deploy: + steps: + + - checkout: self + + - task: Bash@3 + displayName: 'Install Pulumi' + inputs: + targetType: 'inline' + script: | + curl -fsSL https://get.pulumi.com | sh + + echo "##vso[task.prependpath]$HOME/.pulumi/bin" + pulumi about + + - task: DotNetCoreCLI@2 + displayName: Build Infra Project + inputs: + command: build + projects: '**/Company.AppName.Infra.csproj' + arguments: '--configuration $(buildConfiguration)' + + - task: AzureCLI@2 + displayName: Configure Pulumi + env: + PULUMI_CONFIG_PASSPHRASE: $(PULUMI_CONFIG_PASSPHRASE) + inputs: + scriptType: bash + workingDirectory: Company.AppName.Infra + azureSubscription: '${{ parameters.AzureSubscription }}' + scriptLocation: inlineScript + inlineScript: | + export AZURE_STORAGE_ACCOUNT="${{ parameters.storageAccountName }}" + echo "##vso[task.setvariable variable=AZURE_STORAGE_ACCOUNT;isOutput=false]$AZURE_STORAGE_ACCOUNT" + export AZURE_STORAGE_KEY=$(az storage account keys list -n "$AZURE_STORAGE_ACCOUNT" --query "[0].value" -o tsv) + echo "##vso[task.setvariable variable=AZURE_STORAGE_KEY;isOutput=false;issecret=true]$AZURE_STORAGE_KEY" + + # init pulumi stack + pulumi login --cloud-url azblob://state + + # select/create stack + pulumi stack select -c '${{ variables.stack_name }}' + pulumi config set azure-native:location ${{ parameters.region }} + pulumi config set Company.AppName.Infra:isDBSchemaDeploymentEnabled true + + + - task: AzureCLI@2 + displayName: Pulumi Preview + condition: and(succeeded(), or(eq(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.Reason'], 'Manual'))) + env: + AZURE_STORAGE_ACCOUNT: $(AZURE_STORAGE_ACCOUNT) + AZURE_STORAGE_KEY: $(AZURE_STORAGE_KEY) + PULUMI_CONFIG_PASSPHRASE: $(PULUMI_CONFIG_PASSPHRASE) + inputs: + scriptType: bash + azureSubscription: '${{ parameters.AzureSubscription }}' + addSpnToEnvironment: true + workingDirectory: Company.AppName.Infra + scriptLocation: inlineScript + failOnStandardError: true + inlineScript: | + export ARM_CLIENT_ID=$servicePrincipalId + export ARM_CLIENT_SECRET=$servicePrincipalKey + export ARM_TENANT_ID=$tenantId + export ARM_SUBSCRIPTION_ID=$(az account show | jq '.id' --raw-output) + + pulumi login --cloud-url azblob://state + pulumi preview -s '${{ variables.stack_name }}' + + + - task: AzureCLI@2 + condition: and(succeeded(), or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'))) + displayName: Pulumi Up + env: + AZURE_STORAGE_ACCOUNT: $(AZURE_STORAGE_ACCOUNT) + AZURE_STORAGE_KEY: $(AZURE_STORAGE_KEY) + PULUMI_CONFIG_PASSPHRASE: $(PULUMI_CONFIG_PASSPHRASE) + inputs: + scriptType: bash + azureSubscription: ${{ parameters.AzureSubscription }} + addSpnToEnvironment: true + workingDirectory: Company.AppName.Infra + scriptLocation: inlineScript + failOnStandardError: true + inlineScript: | + export ARM_CLIENT_ID=$servicePrincipalId + export ARM_CLIENT_SECRET=$servicePrincipalKey + export ARM_TENANT_ID=$tenantId + export ARM_SUBSCRIPTION_ID=$(az account show | jq '.id' --raw-output) + + pulumi login --cloud-url azblob://state + pulumi up -s '${{ variables.stack_name }}' --yes + + touch $(Build.ArtifactStagingDirectory)/output.sh + cat <> $(Build.ArtifactStagingDirectory)/output.sh + echo "##vso[task.setvariable variable=resourceGroupName;isOutput=true]$(pulumi stack output ResourceGroupName)" + echo "##vso[task.setvariable variable=appServiceName;isOutput=true]$(pulumi stack output AppServiceName)" + echo "##vso[task.setvariable variable=functionName;isOutput=true]$(pulumi stack output FunctionName)" + EOT + + cat $(Build.ArtifactStagingDirectory)/output.sh | sh + + - task: ArchiveFiles@2 + displayName: 'Zip infrastructure artifact' + inputs: + rootFolderOrFile: $(Build.ArtifactStagingDirectory)/output.sh + archiveFile: '$(Build.ArtifactStagingDirectory)/Infra-$(Build.BuildId).zip' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish infrastructure artifact' + inputs: + pathToPublish: '$(Build.ArtifactStagingDirectory)/Infra-$(Build.BuildId).zip' + artifactName: Company-AppName-Infra_Package diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml index d83cde21..1c3b0809 100644 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml @@ -70,7 +70,7 @@ jobs: displayName: 'Run infrastructure tests - $(buildConfiguration)' inputs: command: 'test' - arguments: '-c $(buildConfiguration) --no-build --verbosity normal /p:MergeWith="$(Build.SourcesDirectory)/TestResults/Coverage/coverageApp.json" /p:CollectCoverage=true /p:CoverletOutputFormat="cobertura,json" /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' + arguments: '-c $(buildConfiguration) --no-build --verbosity normal /p:MergeWith="$(Build.SourcesDirectory)/TestResults/Coverage/coverageApp.json" /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura%2cjson /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' publishTestResults: true projects: '**/*.Infra.Tests.csproj' diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml new file mode 100644 index 00000000..cf2bae11 --- /dev/null +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml @@ -0,0 +1,33 @@ +# requires following variables: +# * secret variable - PULUMI_CONFIG_PASSPHRASE - KeyVault can be used instead of pass phrase, see: https://www.pulumi.com/docs/intro/concepts/secrets/#azure-key-vault +# * + +name: Infra-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +variables: +- name: vmImageName + value: 'ubuntu-latest' +- name: buildConfiguration + value: 'Release' +- name: System.Debug + value: true + +trigger: + branches: + include: + - develop + - main + - test + - preprod + +stages: +- stage: Dev + jobs: + - template: infra-template.yml + parameters: + AzureSubscription: $(azure-subscription) + env: dev + storageAccountName: pulumistatestore112 # set to your azure storage account name that contains "state" container + +# add more stages (environments here) +# code deployment can happen in the same pipeline or separate one \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.gitignore b/tools/CoreEx.Template/content/.gitignore index 054745e3..773907c7 100644 --- a/tools/CoreEx.Template/content/.gitignore +++ b/tools/CoreEx.Template/content/.gitignore @@ -754,4 +754,4 @@ docker-compose.local.override.yml __azurite_db_* # Pulumi -Pulumi.*.yaml \ No newline at end of file +# Pulumi.*.yaml # comment out to store env information in git \ No newline at end of file diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs index 4a363193..6ee53ce0 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs @@ -104,6 +104,9 @@ public static class CompanyAppNameStack ["FunctionHealthUrl"] = apps.FunctionHealthUrl, ["FunctionSwaggerUrl"] = apps.FunctionSwaggerUrl, ["AppSwaggerUrl"] = apps.AppSwaggerUrl, + ["ResourceGroupName"] = resourceGroup.Name, + ["AppServiceName"] = apps.AppServiceName, + ["FunctionName"] = apps.FunctionName }; } } \ No newline at end of file diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs index ae843cac..afc82467 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs @@ -22,6 +22,8 @@ public class Apps : ComponentResource public Output AppPrincipalId { get; } = default!; public Output FunctionOutboundIps { get; } = default!; public Output AppOutboundIps { get; } = default!; + public Output AppServiceName { get; } = default!; + public Output FunctionName { get; } = default!; public Apps(string name, FunctionArgs args, AzureApiService azureApiService, ComponentResourceOptions? options = null) : base("Company:AppName:web:apps", name, options) @@ -230,6 +232,8 @@ public Apps(string name, FunctionArgs args, AzureApiService azureApiService, Com FunctionHealthUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/health?code={functionKey}"); FunctionSwaggerUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/swagger/ui?code={functionKey}"); AppSwaggerUrl = Output.Format($"https://{app.DefaultHostName}/swagger/index.html"); + AppServiceName = app.Name; + FunctionName = functionApp.Name; RegisterOutputs(); } diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/Services/DbOperations.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/DbOperations.cs index 213ffec1..f6e97ece 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/Services/DbOperations.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/Services/DbOperations.cs @@ -13,17 +13,17 @@ public void ProvisionUsers(Input connectionString, string groupName) // skip in dry run return; - Log.Info($"Provisioning user {groupName} in SQL DB"); + Log.Info($"Provisioning user group {groupName} in SQL DB"); string commandText = @$" IF NOT EXISTS (SELECT [name] FROM [sys].[database_principals] WHERE [type] = N'X' AND [name] = N'{groupName}') BEGIN - CREATE USER {groupName} FROM EXTERNAL PROVIDER; + CREATE USER [{groupName}] FROM EXTERNAL PROVIDER; END - ALTER ROLE db_datareader ADD MEMBER {groupName}; - ALTER ROLE db_datawriter ADD MEMBER {groupName}; + ALTER ROLE db_datareader ADD MEMBER [{groupName}]; + ALTER ROLE db_datawriter ADD MEMBER [{groupName}]; "; connectionString.Apply(async cs => @@ -42,7 +42,7 @@ public Task DeployDbSchemaAsync(string connectionString) // skip in dry run return Task.FromResult(0); - Log.Info($"Deploying DB schema using {connectionString}"); + Log.Info($"Deploying DB schema"); return Database.Program.RunMigrator(connectionString, assembly: typeof(Company.AppName.Database.Program).Assembly, "DeployWithData"); } } \ No newline at end of file diff --git a/tools/CoreEx.Template/content/docker-compose.DB.only.yml b/tools/CoreEx.Template/content/docker-compose.DB.only.yml index 134f9e6a..79c6b7de 100644 --- a/tools/CoreEx.Template/content/docker-compose.DB.only.yml +++ b/tools/CoreEx.Template/content/docker-compose.DB.only.yml @@ -2,8 +2,6 @@ version: '3.4' services: - sqldata: - app-api: entrypoint: ["echo", "Service api disabled"] From 951083390c97102a8ad028aa55ebbe22ec788c31 Mon Sep 17 00:00:00 2001 From: Piotr Date: Sat, 1 Oct 2022 23:55:06 -0400 Subject: [PATCH 31/39] replacing . with _ in deployment info Signed-off-by: Piotr --- src/CoreEx/Configuration/DeploymentInfo.cs | 10 +++++----- tests/CoreEx.TestFunction/CoreEx.TestFunction.csproj | 1 + tests/CoreEx.TestFunction/local.settings.json | 10 +++++----- .../Controllers/HealthController.cs | 1 - .../Company.AppName.Infra/CompanyAppNameStack.cs | 1 + .../content/Company.AppName.Infra/Components/Apps.cs | 2 ++ 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/CoreEx/Configuration/DeploymentInfo.cs b/src/CoreEx/Configuration/DeploymentInfo.cs index d2b95c88..d4d5a5a1 100644 --- a/src/CoreEx/Configuration/DeploymentInfo.cs +++ b/src/CoreEx/Configuration/DeploymentInfo.cs @@ -20,26 +20,26 @@ public class DeploymentInfo /// /// Gets the username who performed the deployment. /// - public virtual string By => _configuration.GetValue("Deployment.By"); + public virtual string By => _configuration.GetValue("Deployment_By"); /// /// Gets the deployment build number. /// - public virtual string Build => _configuration.GetValue("Deployment.Build"); + public virtual string Build => _configuration.GetValue("Deployment_Build"); /// /// Gets the name of the deployment job that deployed the . /// - public virtual string Name => _configuration.GetValue("Deployment.Name"); + public virtual string Name => _configuration.GetValue("Deployment_Name"); /// /// Gets the deployment build version, such as the Git information (branch and commit) of the deployed . /// - public virtual string Version => _configuration.GetValue("Deployment.Version"); + public virtual string Version => _configuration.GetValue("Deployment_Version"); /// /// Gets the date and time (UTC) when deployment was performed. /// - public virtual string DateUtc => _configuration.GetValue("Deployment.Date"); + public virtual string DateUtc => _configuration.GetValue("Deployment_Date"); } } \ No newline at end of file diff --git a/tests/CoreEx.TestFunction/CoreEx.TestFunction.csproj b/tests/CoreEx.TestFunction/CoreEx.TestFunction.csproj index d0bca0c9..8cb3520e 100644 --- a/tests/CoreEx.TestFunction/CoreEx.TestFunction.csproj +++ b/tests/CoreEx.TestFunction/CoreEx.TestFunction.csproj @@ -3,6 +3,7 @@ netcoreapp3.1 v4 enable + <_FunctionsSkipCleanOutput>true diff --git a/tests/CoreEx.TestFunction/local.settings.json b/tests/CoreEx.TestFunction/local.settings.json index 4c5a3118..89d319f2 100644 --- a/tests/CoreEx.TestFunction/local.settings.json +++ b/tests/CoreEx.TestFunction/local.settings.json @@ -7,10 +7,10 @@ "Test/BackendBaseAddress": "https://backend/", "BindingRedirects": "[ { \"ShortName\": \"System.Memory.Data\", \"RedirectToVersion\": \"6.0.0.0\", \"PublicKeyToken\": \"cc7b13ffcd2ddd51\" } ]", - "Deployment.By": "me", - "Deployment.Build": "build no", - "Deployment.Name": "my deployment", - "Deployment.Version": "1.0.0", - "Deployment.Date": "today" + "Deployment_By": "me", + "Deployment_Build": "build no", + "Deployment_Name": "my deployment", + "Deployment_Version": "1.0.0", + "Deployment_Date": "today" } } \ No newline at end of file diff --git a/tools/CoreEx.Template/content/Company.AppName.Api/Controllers/HealthController.cs b/tools/CoreEx.Template/content/Company.AppName.Api/Controllers/HealthController.cs index 10a9786d..79039f35 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Api/Controllers/HealthController.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Api/Controllers/HealthController.cs @@ -18,6 +18,5 @@ public HealthController(HealthService health) /// Health Endpoint /// [HttpGet()] - [Route("/health")] public async Task Index() => await _health.RunAsync().ConfigureAwait(false); } diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs index 6ee53ce0..063765d7 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/CompanyAppNameStack.cs @@ -102,6 +102,7 @@ public static class CompanyAppNameStack { ["SqlDatabaseConnectionString"] = sql.SqlDatabaseConnectionString, ["FunctionHealthUrl"] = apps.FunctionHealthUrl, + ["AppHealthUrl"] = apps.AppHealthUrl, ["FunctionSwaggerUrl"] = apps.FunctionSwaggerUrl, ["AppSwaggerUrl"] = apps.AppSwaggerUrl, ["ResourceGroupName"] = resourceGroup.Name, diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs index afc82467..e549d2e6 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/Components/Apps.cs @@ -16,6 +16,7 @@ public class Apps : ComponentResource private readonly FunctionArgs args; public Output FunctionHealthUrl { get; } = default!; + public Output AppHealthUrl { get; } = default!; public Output FunctionSwaggerUrl { get; } = default!; public Output AppSwaggerUrl { get; } = default!; public Output FunctionPrincipalId { get; } = default!; @@ -230,6 +231,7 @@ public Apps(string name, FunctionArgs args, AzureApiService azureApiService, Com azureApiService.SyncFunctionAppTriggers(args.ResourceGroupName, functionApp.Name); FunctionHealthUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/health?code={functionKey}"); + AppHealthUrl = Output.Format($"https://{app.DefaultHostName}/api/health"); FunctionSwaggerUrl = Output.Format($"https://{functionApp.DefaultHostName}/api/swagger/ui?code={functionKey}"); AppSwaggerUrl = Output.Format($"https://{app.DefaultHostName}/swagger/index.html"); AppServiceName = app.Name; From 9d7af42fb2848a948df5ac42b9192c9d25532068 Mon Sep 17 00:00:00 2001 From: Piotr Date: Sat, 1 Oct 2022 23:55:23 -0400 Subject: [PATCH 32/39] pipeline files Signed-off-by: Piotr --- .../.azuredevops/infra-destroy-template.yml | 18 +++++++++++ .../content/.azuredevops/infra-template.yml | 12 ++++--- .../.azuredevops/pipeline-infra-destroy.yml | 20 ++++++++++++ .../content/.azuredevops/pipeline-infra.yml | 4 --- .../content/.azuredevops/pipeline-release.yml | 31 +++++++++++++++++++ 5 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml create mode 100644 tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml b/tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml index cac2cac9..1b741076 100644 --- a/tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml +++ b/tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml @@ -17,7 +17,25 @@ parameters: displayName: 'Azure region to deploy to' jobs: +- job: manual_approval + displayName: "Manual Approval for ${{ variables.stack_name }} stack destroy" + pool: server + variables: + - name: stack_name + value: ${{ parameters.env }} # can modify stack name here + steps: + - task: ManualValidation@0 + displayName: "Approve ${{ variables.stack_name }} stack destroy" + timeoutInMinutes: 1440 # task times out in 1 day + inputs: + # notifyUsers: | + # test@test.com + # example@example.com + instructions: 'Please confirm pulumi stack ${{ variables.stack_name }} can be deleted' + onTimeout: 'reject' + - deployment: Destroy + dependsOn: manual_approval displayName: "Destroy infrastructure ${{ parameters.env }}" environment: ${{ parameters.env }} variables: diff --git a/tools/CoreEx.Template/content/.azuredevops/infra-template.yml b/tools/CoreEx.Template/content/.azuredevops/infra-template.yml index 0e4fe343..a9535664 100644 --- a/tools/CoreEx.Template/content/.azuredevops/infra-template.yml +++ b/tools/CoreEx.Template/content/.azuredevops/infra-template.yml @@ -101,7 +101,7 @@ jobs: - task: AzureCLI@2 - condition: and(succeeded(), or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'))) + condition: or(succeeded(), or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'))) displayName: Pulumi Up env: AZURE_STORAGE_ACCOUNT: $(AZURE_STORAGE_ACCOUNT) @@ -123,14 +123,16 @@ jobs: pulumi login --cloud-url azblob://state pulumi up -s '${{ variables.stack_name }}' --yes - touch $(Build.ArtifactStagingDirectory)/output.sh - cat <> $(Build.ArtifactStagingDirectory)/output.sh + echo creating $(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh + touch $(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh + cat <> $(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh echo "##vso[task.setvariable variable=resourceGroupName;isOutput=true]$(pulumi stack output ResourceGroupName)" echo "##vso[task.setvariable variable=appServiceName;isOutput=true]$(pulumi stack output AppServiceName)" echo "##vso[task.setvariable variable=functionName;isOutput=true]$(pulumi stack output FunctionName)" + echo "##vso[task.setvariable variable=functionHealthUrl;isOutput=true]$(pulumi stack output --show-secrets FunctionHealthUrl)" EOT - cat $(Build.ArtifactStagingDirectory)/output.sh | sh + cat $(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh | sh - task: ArchiveFiles@2 displayName: 'Zip infrastructure artifact' @@ -141,5 +143,5 @@ jobs: - task: PublishBuildArtifacts@1 displayName: 'Publish infrastructure artifact' inputs: - pathToPublish: '$(Build.ArtifactStagingDirectory)/Infra-$(Build.BuildId).zip' + pathToPublish: '$(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh' artifactName: Company-AppName-Infra_Package diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml new file mode 100644 index 00000000..f884a18c --- /dev/null +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml @@ -0,0 +1,20 @@ +# requires following env variables: +# * secret variable - PULUMI_CONFIG_PASSPHRASE - KeyVault can be used instead of pass phrase, see: https://www.pulumi.com/docs/intro/concepts/secrets/#azure-key-vault + +name: Infra-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +variables: +- name: buildConfiguration + value: 'Release' + +trigger: none + + +stages: +- stage: Dev + jobs: + - template: infra-destroy-template.yml + parameters: + AzureSubscription: piotr-subscription + env: dev + storageAccountName: pulumistatestore112 \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml index cf2bae11..34d74e4a 100644 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml @@ -5,12 +5,8 @@ name: Infra-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) variables: -- name: vmImageName - value: 'ubuntu-latest' - name: buildConfiguration value: 'Release' -- name: System.Debug - value: true trigger: branches: diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml new file mode 100644 index 00000000..8075e843 --- /dev/null +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml @@ -0,0 +1,31 @@ +name: D365-Apps-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +variables: +- name: vmImageName + value: 'ubuntu-latest' + +# Explicitly set none for repository trigger +trigger: none + +resources: + pipelines: + - pipeline: 'Apple-Store-Infra_Package' # Name of the pipeline resource + source: 'Infra Deployment' # Name of the triggering pipeline + - pipeline: 'Apple-Store-Api_Package' # Name of the pipeline resource + source: 'Application build' # Name of the triggering pipeline + trigger: + branches: + - develop + - test + - preprod + - main + +# Note: Azure Service connection passed as param due to: https://developercommunity.visualstudio.com/t/using-a-variable-for-the-service-connection-result/676259 + +stages: +- stage: Dev + jobs: + - template: app-template.yml + parameters: + AzureSubscription: piotr-subscription + env: dev From 97120d8fe937197bed403ba83c197e4f9b82bfdf Mon Sep 17 00:00:00 2001 From: Piotr Date: Sat, 1 Oct 2022 23:55:59 -0400 Subject: [PATCH 33/39] templates Signed-off-by: Piotr --- .../.azuredevops/{ => templates}/infra-destroy-template.yml | 0 .../content/.azuredevops/{ => templates}/infra-template.yml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tools/CoreEx.Template/content/.azuredevops/{ => templates}/infra-destroy-template.yml (100%) rename tools/CoreEx.Template/content/.azuredevops/{ => templates}/infra-template.yml (100%) diff --git a/tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/infra-destroy-template.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/infra-destroy-template.yml rename to tools/CoreEx.Template/content/.azuredevops/templates/infra-destroy-template.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/infra-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/infra-template.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/infra-template.yml rename to tools/CoreEx.Template/content/.azuredevops/templates/infra-template.yml From 64bcf34f78c2667b73488332291253922dcff747 Mon Sep 17 00:00:00 2001 From: Piotr Date: Sun, 2 Oct 2022 00:08:02 -0400 Subject: [PATCH 34/39] pipeline files Signed-off-by: Piotr --- .../.azuredevops/pipeline-infra-destroy.yml | 21 +++-- .../content/.azuredevops/pipeline-infra.yml | 16 ++-- .../content/.azuredevops/pipeline-release.yml | 29 +++++-- .../.azuredevops/templates/app-template.yml | 62 +++++++++++++ .../templates/function-template.yml | 86 +++++++++++++++++++ .../templates/infra-destroy-template.yml | 32 ++----- .../.azuredevops/templates/infra-template.yml | 23 ++--- 7 files changed, 213 insertions(+), 56 deletions(-) create mode 100644 tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml create mode 100644 tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml index f884a18c..c7cce693 100644 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml @@ -1,7 +1,15 @@ # requires following env variables: # * secret variable - PULUMI_CONFIG_PASSPHRASE - KeyVault can be used instead of pass phrase, see: https://www.pulumi.com/docs/intro/concepts/secrets/#azure-key-vault -name: Infra-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) +name: Infra-Destroy_$(Date:yyyyMMdd)$(Rev:.r) + +parameters: +- name: stack_name + type: string + displayName: Name of the stack to delete +- name: env + type: string + displayName: Name of the environment in ADO - env, dev, etc. variables: - name: buildConfiguration @@ -11,10 +19,11 @@ trigger: none stages: -- stage: Dev +- stage: Delete jobs: - - template: infra-destroy-template.yml + - template: templates/infra-destroy-template.yml parameters: - AzureSubscription: piotr-subscription - env: dev - storageAccountName: pulumistatestore112 \ No newline at end of file + AzureSubscription: $(AzureSubscription) # replace with the name of service connection + env: ${{ parameters.env }} + storageAccountName: pulumistatestore112 + stack_name: ${{ parameters.stack_name }} \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml index 34d74e4a..7873e98b 100644 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml @@ -1,6 +1,5 @@ -# requires following variables: +# requires following env variables: # * secret variable - PULUMI_CONFIG_PASSPHRASE - KeyVault can be used instead of pass phrase, see: https://www.pulumi.com/docs/intro/concepts/secrets/#azure-key-vault -# * name: Infra-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) @@ -19,11 +18,16 @@ trigger: stages: - stage: Dev jobs: - - template: infra-template.yml + - template: templates/infra-template.yml parameters: - AzureSubscription: $(azure-subscription) + AzureSubscription: $(AzureSubscription) # replace with the name of service connection env: dev storageAccountName: pulumistatestore112 # set to your azure storage account name that contains "state" container -# add more stages (environments here) -# code deployment can happen in the same pipeline or separate one \ No newline at end of file +- stage: Test + jobs: + - template: templates/infra-template.yml + parameters: + AzureSubscription: $(AzureSubscription) # replace with the name of service connection + env: test + storageAccountName: pulumistatestore112 # set to your azure storage account name that contains "state" container diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml b/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml index 8075e843..c44e4eaf 100644 --- a/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml +++ b/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml @@ -1,4 +1,4 @@ -name: D365-Apps-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) +name: Apps-Deploy-$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) variables: - name: vmImageName @@ -9,10 +9,10 @@ trigger: none resources: pipelines: - - pipeline: 'Apple-Store-Infra_Package' # Name of the pipeline resource - source: 'Infra Deployment' # Name of the triggering pipeline - - pipeline: 'Apple-Store-Api_Package' # Name of the pipeline resource - source: 'Application build' # Name of the triggering pipeline + - pipeline: 'Company-AppName-Infra' # Name of the pipeline resource (alias) + source: 'Infra Deployment' # Name of the triggering pipeline that created infrastructure + - pipeline: 'Company-AppName-Apps' # Name of the pipeline resource (alias) + source: 'Application build' # Name of the triggering pipeline that created application packages trigger: branches: - develop @@ -25,7 +25,22 @@ resources: stages: - stage: Dev jobs: - - template: app-template.yml + - template: templates/app-template.yml parameters: - AzureSubscription: piotr-subscription + AzureSubscription: $(AzureSubscription) # replace with the name of service connection env: dev + - template: templates/function-template.yml + parameters: + AzureSubscription: $(AzureSubscription) # replace with the name of service connection + env: dev + +- stage: Test + jobs: + - template: templates/app-template.yml + parameters: + AzureSubscription: $(AzureSubscription) # replace with the name of service connection + env: test + - template: templates/function-template.yml + parameters: + AzureSubscription: $(AzureSubscription) # replace with the name of service connection + env: test diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml new file mode 100644 index 00000000..4af3dff6 --- /dev/null +++ b/tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml @@ -0,0 +1,62 @@ +parameters: +- name: AzureSubscription + type: string +- name: env + type: string + displayName: 'Environment shorthand - dev, test, etc.' + +jobs: +- deployment: Deploy_App + displayName: "Deploy app service to ${{ parameters.env }}" + environment: ${{ parameters.env }} + variables: + - name: global_buildDate + value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] + # - group: VG-Company-AppName-${{ parameters.env }} # uncomment to use variable groups in ADO + pool: + vmImage: ubuntu-latest + strategy: + runOnce: + deploy: + steps: + + - task: Bash@3 + displayName: 'Setup variables' + inputs: + targetType: 'inline' + script: | + source $(Pipeline.Workspace)/Company-AppName-Infra/Company-AppName-Infra_Package-${{ parameters.env }}/setup-${{ parameters.env }}.sh + + - task: Bash@3 + displayName: 'Display variables' + inputs: + targetType: 'inline' + script: | + echo resourceGroupName $(RESOURCEGROUPNAME) + echo appServiceName $(APPSERVICENAME) + echo functionName $(FUNCTIONNAME) + + - task: AzureWebApp@1 + displayName: 'Deployment app ${{ variables.APPSERVICENAME }} to env: ${{ parameters.env }}' + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + appType: 'webApp' + appName: '$(APPSERVICENAME)' + deploymentMethod: zipDeploy + package: '$(Pipeline.Workspace)/**/Company.AppName.Api.zip' + appSettings: '-Deployment_By "$(Build.RequestedForEmail)" + -Deployment_Build "$(resources.pipeline.Company-AppName-Apps.runName)" + -Deployment_Name "$(Build.BuildNumber)" + -Deployment_Version "$(resources.pipeline.Company-AppName-Apps.sourceBranch)-$(resources.pipeline.Company-AppName-Apps.sourceCommit)" + -Deployment_Date "$(global_buildDate)" + -ApplicationName "Company AppName API"' + + - task: AzureCLI@2 + displayName: Tag Azure + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + id=$(az resource show --name $(APPSERVICENAME) -g $(RESOURCEGROUPNAME) --resource-type "Microsoft.Web/sites" --query id --output tsv) + az tag update --resource-id $id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.Company-AppName-Apps.runName)"' 'Deployment.Version="$(resources.pipeline.Company-AppName-Apps.sourceBranch)-$(resources.pipeline.Company-AppName-Apps.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml new file mode 100644 index 00000000..e08e9451 --- /dev/null +++ b/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml @@ -0,0 +1,86 @@ +parameters: +- name: AzureSubscription + type: string +- name: env + type: string + displayName: 'Environment shorthand - dev, test, etc.' + +jobs: +- deployment: Deploy_Fun + displayName: "Deploy function app to ${{ parameters.env }}" + environment: ${{ parameters.env }} + variables: + - name: global_buildDate + value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] + # - group: VG-Company-AppName-${{ parameters.env }} # uncomment to use variable groups in ADO + pool: + vmImage: ubuntu-latest + strategy: + runOnce: + deploy: + steps: + + - task: Bash@3 + displayName: 'Setup variables' + inputs: + targetType: 'inline' + script: | + source $(Pipeline.Workspace)/Apple-Store-Infra/Apple-Store-Infra_Package-${{ parameters.env }}/setup-${{ parameters.env }}.sh + + - task: Bash@3 + displayName: 'Display variables' + inputs: + targetType: 'inline' + script: | + echo resourceGroupName $(RESOURCEGROUPNAME) + echo appServiceName $(APPSERVICENAME) + echo functionName $(FUNCTIONNAME) + + - task: AzureFunctionApp@1 + displayName: 'Deployment az fun ${{ variables.FUNCTIONNAME }} to env: ${{ parameters.env }}' + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + appType: 'functionApp' + appName: '$(FUNCTIONNAME)' + resourceGroupName: '$(RESOURCEGROUPNAME)' + #slotName: '$(DeploymentSlot)' + package: '$(Pipeline.Workspace)/**/Apple.Store.Functions.zip' + deploymentMethod: 'auto' + appSettings: '-Deployment_By "$(Build.RequestedForEmail)" + -Deployment_Build "$(resources.pipeline.Apple-Store-Apps.runName)" + -Deployment_Name "$(Build.BuildNumber)" + -Deployment_Version "$(resources.pipeline.Apple-Store-Apps.sourceBranch)-$(resources.pipeline.Apple-Store-Apps.sourceCommit)" + -Deployment_Date "$(global_buildDate)" + -ApplicationName "Company AppName Functions" + -AzureFunctionsJobHost__logging__logLevel__default "Warning" + -AzureFunctionsJobHost__logging__logLevel__CoreEx "Information"' + + - task: AzureCLI@2 + displayName: Tag Azure + inputs: + azureSubscription: '${{ parameters.AzureSubscription }}' + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + id=$(az resource show --name $(FUNCTIONNAME) -g $(RESOURCEGROUPNAME) --resource-type "Microsoft.Web/sites" --query id --output tsv) + az tag update --resource-id $id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.Apple-Store-Apps.runName)"' 'Deployment.Version="$(resources.pipeline.Apple-Store-Apps.sourceBranch)-$(resources.pipeline.Apple-Store-Apps.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' + + - task: Bash@3 + displayName: 'Smoke Test' + env: + FUNCTIONHEALTHURL: $(FUNCTIONHEALTHURL) + inputs: + targetType: 'inline' + failOnStandardError: true + script: | + result=$(curl -s -o /dev/null -w "%{http_code}" $(FUNCTIONHEALTHURL)) + + if [ $result == "200" ] + then + echo "OK" + else + echo $result + exit 1 + fi + + diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/infra-destroy-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/infra-destroy-template.yml index 1b741076..594b822e 100644 --- a/tools/CoreEx.Template/content/.azuredevops/templates/infra-destroy-template.yml +++ b/tools/CoreEx.Template/content/.azuredevops/templates/infra-destroy-template.yml @@ -8,30 +8,26 @@ parameters: - name: storageAccountName type: string displayName: 'Name of the storage account used for pulumi state' +- name: stack_name + type: string + displayName: 'Name of the stack to delete' - name: env type: string displayName: 'Environment name - dev, test, etc. Can be longer e.g. Company-AppName-dev' -- name: region - type: string - default: eastus - displayName: 'Azure region to deploy to' jobs: - job: manual_approval - displayName: "Manual Approval for ${{ variables.stack_name }} stack destroy" + displayName: "Manual Approval for ${{ parameters.stack_name }} stack in env ${{ parameters.env }} to destroy" pool: server - variables: - - name: stack_name - value: ${{ parameters.env }} # can modify stack name here steps: - task: ManualValidation@0 - displayName: "Approve ${{ variables.stack_name }} stack destroy" + displayName: "Approve ${{ parameters.stack_name }} stack destroy" timeoutInMinutes: 1440 # task times out in 1 day inputs: # notifyUsers: | # test@test.com # example@example.com - instructions: 'Please confirm pulumi stack ${{ variables.stack_name }} can be deleted' + instructions: 'Please confirm pulumi stack ${{ parameters.stack_name }} can be deleted' onTimeout: 'reject' - deployment: Destroy @@ -41,8 +37,6 @@ jobs: variables: - name: global_buildDate value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] - - name: stack_name - value: ${{ parameters.env }} # can modify stack name here # - group: VG-Company-AppName-${{ parameters.env }} # uncomment to use variable groups in ADO pool: vmImage: ubuntu-latest @@ -62,22 +56,14 @@ jobs: echo "##vso[task.prependpath]$HOME/.pulumi/bin" pulumi about - - task: ManualValidation@0 - timeoutInMinutes: 1440 # task times out in 1 day - inputs: - # notifyUsers: | - # test@test.com - # example@example.com - instructions: 'Please confirm pulumi stack ${{ variables.stack_name }} can be destroyed' - onTimeout: 'reject' - - task: AzureCLI@2 - displayName: Destroy Pulumi stack ${{ variables.stack_name }} + displayName: Destroy Pulumi stack ${{ parameters.stack_name }} env: PULUMI_CONFIG_PASSPHRASE: $(PULUMI_CONFIG_PASSPHRASE) inputs: scriptType: bash azureSubscription: '${{ parameters.AzureSubscription }}' + addSpnToEnvironment: true scriptLocation: inlineScript inlineScript: | export AZURE_STORAGE_ACCOUNT="${{ parameters.storageAccountName }}" @@ -91,4 +77,4 @@ jobs: pulumi login --cloud-url azblob://state # select/create stack - pulumi destroy -s '${{ variables.stack_name }}' -y \ No newline at end of file + pulumi destroy -y -s '${{ parameters.stack_name }}' \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/infra-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/infra-template.yml index a9535664..1f395b9c 100644 --- a/tools/CoreEx.Template/content/.azuredevops/templates/infra-template.yml +++ b/tools/CoreEx.Template/content/.azuredevops/templates/infra-template.yml @@ -24,7 +24,7 @@ jobs: - name: global_buildDate value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] - name: stack_name - value: Company-AppName-${{ parameters.env }} # can modify stack name here + value: ${{ parameters.env }} # can modify stack name here # - group: VG-Company-AppName-${{ parameters.env }} # uncomment to use variable groups in ADO pool: vmImage: ubuntu-latest @@ -126,22 +126,17 @@ jobs: echo creating $(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh touch $(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh cat <> $(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh - echo "##vso[task.setvariable variable=resourceGroupName;isOutput=true]$(pulumi stack output ResourceGroupName)" - echo "##vso[task.setvariable variable=appServiceName;isOutput=true]$(pulumi stack output AppServiceName)" - echo "##vso[task.setvariable variable=functionName;isOutput=true]$(pulumi stack output FunctionName)" - echo "##vso[task.setvariable variable=functionHealthUrl;isOutput=true]$(pulumi stack output --show-secrets FunctionHealthUrl)" + echo exporting variables + echo "##vso[task.setvariable variable=RESOURCEGROUPNAME;isOutput=false]$(pulumi stack output ResourceGroupName)" + echo "##vso[task.setvariable variable=APPSERVICENAME;isOutput=false]$(pulumi stack output AppServiceName)" + echo "##vso[task.setvariable variable=FUNCTIONNAME;isOutput=false]$(pulumi stack output FunctionName)" + echo "##vso[task.setvariable variable=FUNCTIONHEALTHURL;isOutput=false;issecret=true]$(pulumi stack output --show-secrets FunctionHealthUrl)" + echo "##vso[task.setvariable variable=APPHEALTHURL;isOutput=false;issecret=true]$(pulumi stack output AppHealthUrl)" EOT - cat $(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh | sh - - - task: ArchiveFiles@2 - displayName: 'Zip infrastructure artifact' - inputs: - rootFolderOrFile: $(Build.ArtifactStagingDirectory)/output.sh - archiveFile: '$(Build.ArtifactStagingDirectory)/Infra-$(Build.BuildId).zip' - - task: PublishBuildArtifacts@1 + condition: succeeded() displayName: 'Publish infrastructure artifact' inputs: pathToPublish: '$(Build.ArtifactStagingDirectory)/setup-${{ parameters.env }}.sh' - artifactName: Company-AppName-Infra_Package + artifactName: Company-AppName-Infra_Package-${{ parameters.env }} From 4946903b5f9b933dea10b1ea864fcf5fd463f6b3 Mon Sep 17 00:00:00 2001 From: Piotr Date: Sun, 2 Oct 2022 00:17:36 -0400 Subject: [PATCH 35/39] minor Signed-off-by: Piotr --- tests/CoreEx.Test/Framework/Json/JsonEmployeeTest.cs | 4 ++-- .../.azuredevops/templates/function-template.yml | 10 +++++----- .../content/Company.AppName.Infra/Pulumi.yaml | 3 +++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/CoreEx.Test/Framework/Json/JsonEmployeeTest.cs b/tests/CoreEx.Test/Framework/Json/JsonEmployeeTest.cs index a3f022df..02757746 100644 --- a/tests/CoreEx.Test/Framework/Json/JsonEmployeeTest.cs +++ b/tests/CoreEx.Test/Framework/Json/JsonEmployeeTest.cs @@ -69,7 +69,7 @@ public class Employee public void SystemTextJson_Serialize_Deserialize() { // Arrange - var json = "{\n \"email\": \"piotr.karpala@avanade.com\",\n \"FirstName\": \"Piotr\",\n \"lastName\": \"Karpala\",\n \"genderCode\": \"male\",\n \"birthday\": \"1990-03-24T13:49:11.813Z\",\n \"startDate\": \"2022-03-24T13:49:11.813Z\",\n \"phoneNo\": \"985 657 9455\"\n}"; + var json = "{\n \"email\": \"john.doe@avanade.com\",\n \"FirstName\": \"John\",\n \"lastName\": \"Doe\",\n \"genderCode\": \"male\",\n \"birthday\": \"1990-03-24T13:49:11.813Z\",\n \"startDate\": \"2022-03-24T13:49:11.813Z\",\n \"phoneNo\": \"985 657 9455\"\n}"; var js = new CoreEx.Text.Json.JsonSerializer() as IJsonSerializer; // Act @@ -77,7 +77,7 @@ public void SystemTextJson_Serialize_Deserialize() // Assert employee.Should().NotBeNull(); - employee!.FirstName.Should().Be("Piotr"); + employee!.FirstName.Should().Be("John"); } } } \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml index e08e9451..55dc4a7b 100644 --- a/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml +++ b/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml @@ -25,7 +25,7 @@ jobs: inputs: targetType: 'inline' script: | - source $(Pipeline.Workspace)/Apple-Store-Infra/Apple-Store-Infra_Package-${{ parameters.env }}/setup-${{ parameters.env }}.sh + source $(Pipeline.Workspace)/Company-AppName-Infra/Company-AppName-Infra_Package-${{ parameters.env }}/setup-${{ parameters.env }}.sh - task: Bash@3 displayName: 'Display variables' @@ -44,12 +44,12 @@ jobs: appName: '$(FUNCTIONNAME)' resourceGroupName: '$(RESOURCEGROUPNAME)' #slotName: '$(DeploymentSlot)' - package: '$(Pipeline.Workspace)/**/Apple.Store.Functions.zip' + package: '$(Pipeline.Workspace)/**/Company.AppName.Functions.zip' deploymentMethod: 'auto' appSettings: '-Deployment_By "$(Build.RequestedForEmail)" - -Deployment_Build "$(resources.pipeline.Apple-Store-Apps.runName)" + -Deployment_Build "$(resources.pipeline.Company-AppName-Apps.runName)" -Deployment_Name "$(Build.BuildNumber)" - -Deployment_Version "$(resources.pipeline.Apple-Store-Apps.sourceBranch)-$(resources.pipeline.Apple-Store-Apps.sourceCommit)" + -Deployment_Version "$(resources.pipeline.Company-AppName-Apps.sourceBranch)-$(resources.pipeline.Company-AppName-Apps.sourceCommit)" -Deployment_Date "$(global_buildDate)" -ApplicationName "Company AppName Functions" -AzureFunctionsJobHost__logging__logLevel__default "Warning" @@ -63,7 +63,7 @@ jobs: scriptLocation: inlineScript inlineScript: | id=$(az resource show --name $(FUNCTIONNAME) -g $(RESOURCEGROUPNAME) --resource-type "Microsoft.Web/sites" --query id --output tsv) - az tag update --resource-id $id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.Apple-Store-Apps.runName)"' 'Deployment.Version="$(resources.pipeline.Apple-Store-Apps.sourceBranch)-$(resources.pipeline.Apple-Store-Apps.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' + az tag update --resource-id $id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.Company-AppName-Apps.runName)"' 'Deployment.Version="$(resources.pipeline.Company-AppName-Apps.sourceBranch)-$(resources.pipeline.Company-AppName-Apps.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' - task: Bash@3 displayName: 'Smoke Test' diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/Pulumi.yaml b/tools/CoreEx.Template/content/Company.AppName.Infra/Pulumi.yaml index fc356f26..66b5ab9b 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/Pulumi.yaml +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/Pulumi.yaml @@ -12,3 +12,6 @@ template: Company.AppName.Infra:isDBSchemaDeploymentEnabled: description: Whether Database schema should be deployed default: true + Company.AppName.Infra:developerEmails: + description: Comma separated list of developer team emails that will get access to created resources + default: \ No newline at end of file From 942f83a8885f23869ab3b93c800b7582dbc6bed0 Mon Sep 17 00:00:00 2001 From: Piotr Date: Sun, 2 Oct 2022 00:39:53 -0400 Subject: [PATCH 36/39] health check Signed-off-by: Piotr --- .../.azuredevops/templates/app-template.yml | 30 +++++++++++++++++++ .../templates/function-template.yml | 29 ++++++++++++------ .../content/Company.AppName.Infra/Readme.md | 9 ++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml index 4af3dff6..525752a6 100644 --- a/tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml +++ b/tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml @@ -60,3 +60,33 @@ jobs: inlineScript: | id=$(az resource show --name $(APPSERVICENAME) -g $(RESOURCEGROUPNAME) --resource-type "Microsoft.Web/sites" --query id --output tsv) az tag update --resource-id $id --operation merge --tags 'Deployment.By="$(Build.RequestedForEmail)"' 'Deployment.Name="$(Build.BuildNumber)"' 'Deployment.Build="$(resources.pipeline.Company-AppName-Apps.runName)"' 'Deployment.Version="$(resources.pipeline.Company-AppName-Apps.sourceBranch)-$(resources.pipeline.Company-AppName-Apps.sourceCommit)"' 'Deployment.Date="$(global_buildDate)"' + + - task: Bash@3 + displayName: 'Smoke Test' + env: + APPHEALTHURL: $(APPHEALTHURL) + inputs: + targetType: 'inline' + failOnStandardError: true + script: | + for attempt in 1 2 3 + do + result=$(curl -s -o /dev/null -w "%{http_code}" $(APPHEALTHURL)) + + if [ $result == "200" ] + then + echo "OK" + exit 0 + else + echo attempt $attempt failed with result $result sleeping 10 + + if [ "$attempt" -lt "3" ] + then + sleep 10 + else + exit 1 + fi + fi + done + + exit 1 \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml b/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml index 55dc4a7b..7d56ea33 100644 --- a/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml +++ b/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml @@ -73,14 +73,25 @@ jobs: targetType: 'inline' failOnStandardError: true script: | - result=$(curl -s -o /dev/null -w "%{http_code}" $(FUNCTIONHEALTHURL)) + for attempt in 1 2 3 + do + result=$(curl -s -o /dev/null -w "%{http_code}" $(FUNCTIONHEALTHURL)) - if [ $result == "200" ] - then - echo "OK" - else - echo $result - exit 1 - fi + if [ $result == "200" ] + then + echo "OK" + exit 0 + else + echo attempt $attempt failed with result $result sleeping 10 + + if [ "$attempt" -lt "3" ] + then + sleep 10 + else + exit 1 + fi + fi + done + + exit 1 - diff --git a/tools/CoreEx.Template/content/Company.AppName.Infra/Readme.md b/tools/CoreEx.Template/content/Company.AppName.Infra/Readme.md index 66834cf2..82b808ac 100644 --- a/tools/CoreEx.Template/content/Company.AppName.Infra/Readme.md +++ b/tools/CoreEx.Template/content/Company.AppName.Infra/Readme.md @@ -17,6 +17,7 @@ Pulumi can be used without Pulumi Account, by using [Azure Storage as backend](h 1. set the `AZURE_STORAGE_ACCOUNT` environment variable to specify the Azure storage account to use 1. set the `AZURE_STORAGE_KEY` or the `AZURE_STORAGE_SAS_TOKEN` environment variables to let Pulumi access the storage +1. create a container in the storage account 1. execute the following command `pulumi login azblob://` where container-path is the path to a blob container in the storage account ## Configuring Pulumi (optional) @@ -81,3 +82,11 @@ Apps can also be deployed with Azure CLI, once published apps are zipped. az webapp deploy --resource-group coreEx-dev4011fb65 --name app17b7c4c8 --src-path app.zip az functionapp deployment source config-zip -g coreEx-dev4011fb65 -n fun17b7c4c8 --src fun.zip ``` + +## Deploying from CI/CD with service principal + +When deploying using service principal, SP needs to be given appropriate permissions to be allowed to create resources: + +* Owner role on the subscription to be able to assign permissions to manage identities created (this can be achieved by creating/assigning custom role too). +* Azure AD create user role in order to create SQL Admin user and SQL access group. +* Microsoft Graph permissions according to [Terraform Docs](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/service_principal_configuration) to be able to query domain, create/read users and create/read groups. From 510ec22314926752f34b185e6df7e9a2bf55cae5 Mon Sep 17 00:00:00 2001 From: Piotr Date: Sun, 2 Oct 2022 21:34:28 -0400 Subject: [PATCH 37/39] Template project file Signed-off-by: Piotr --- CoreEx.sln | 9 +++ .../My.Hr.Database/My.Hr.Database.csproj | 1 - tools/CoreEx.Template/CoreEx.Template.csproj | 54 +++++++++++++ .../content/.template.config/template.json | 80 +------------------ 4 files changed, 64 insertions(+), 80 deletions(-) create mode 100644 tools/CoreEx.Template/CoreEx.Template.csproj diff --git a/CoreEx.sln b/CoreEx.sln index 0c5b89a1..8f0db93f 100644 --- a/CoreEx.sln +++ b/CoreEx.sln @@ -59,6 +59,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreEx.Cosmos", "src\CoreEx EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreEx.Cosmos.Test", "tests\CoreEx.Cosmos.Test\CoreEx.Cosmos.Test.csproj", "{C8021CF0-006F-427C-827F-B997F26E5FF6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{AD4128C1-A096-451B-BD61-705EC774ADEC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreEx.Template", "tools\CoreEx.Template\CoreEx.Template.csproj", "{16556C5C-E54F-48EA-A38F-CE612212EBCF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -137,6 +141,10 @@ Global {C8021CF0-006F-427C-827F-B997F26E5FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8021CF0-006F-427C-827F-B997F26E5FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8021CF0-006F-427C-827F-B997F26E5FF6}.Release|Any CPU.Build.0 = Release|Any CPU + {16556C5C-E54F-48EA-A38F-CE612212EBCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16556C5C-E54F-48EA-A38F-CE612212EBCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16556C5C-E54F-48EA-A38F-CE612212EBCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16556C5C-E54F-48EA-A38F-CE612212EBCF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -161,6 +169,7 @@ Global {F3384ADC-1DA8-4538-B991-DBD2BC591AF1} = {4B6BC31E-93B1-42B0-AE09-AD85AC4DB657} {8D0CC3FD-65C2-4302-99F4-A90AC7680E1B} = {4B6BC31E-93B1-42B0-AE09-AD85AC4DB657} {C8021CF0-006F-427C-827F-B997F26E5FF6} = {3145DCB9-98FB-4519-BCC0-75A22A252EDC} + {16556C5C-E54F-48EA-A38F-CE612212EBCF} = {AD4128C1-A096-451B-BD61-705EC774ADEC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B4566D2-9B22-4E27-9654-402BDBA6C744} diff --git a/samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj b/samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj index 659f4f07..e6714fff 100644 --- a/samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj +++ b/samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj @@ -23,7 +23,6 @@ - diff --git a/tools/CoreEx.Template/CoreEx.Template.csproj b/tools/CoreEx.Template/CoreEx.Template.csproj new file mode 100644 index 00000000..e9b70f48 --- /dev/null +++ b/tools/CoreEx.Template/CoreEx.Template.csproj @@ -0,0 +1,54 @@ + + + + netstandard2.1 + Template + CoreEx.Template + CoreEx + CoreEx .NET Standard Extensions. + Avanade + CoreEx template solution for use with 'dotnet new'. + coreex dotnet template solution + false + true + false + content + true + + + + + + + + true + contentFiles\any\any\Schema\ + true + + + true + true + + + true + true + + + true + true + + + true + true + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.template.config/template.json b/tools/CoreEx.Template/content/.template.config/template.json index 31078920..ef1ecee1 100644 --- a/tools/CoreEx.Template/content/.template.config/template.json +++ b/tools/CoreEx.Template/content/.template.config/template.json @@ -12,7 +12,7 @@ }, "defaultName": "CoreEx", "description": "CoreEx ", - "sourceName": "XXCompany.AppNameXX", // Not acutally used; template uses the below parameters exclusively. + "sourceName": "XXCompany.AppNameXX", // Not actually used; template uses the below parameters exclusively. "preferNameDirectory": true, "symbols": { "company": { @@ -31,54 +31,6 @@ "datatype": "text", "description": "The application (domain) name 'AppName' used to define the namespace etc; e.g. 'Company.AppName'." }, - "datasource": { - "type": "parameter", - "datatype": "choice", - "choices": [ - { - "choice": "Cosmos", - "description": "Indicates that the data source is Cosmos DB." - }, - { - "choice": "Database", - "description": "Indicates that the data source is a SQL Database accessed via Stored Procedures." - }, - { - "choice": "EntityFramework", - "description": "Indicates that the data source is a SQL Database accessed via Entity Framework Core." - }, - { - "choice": "HttpAgent", - "description": "Indicates that the data source is to be accessed via an Http Agent." - }, - { - "choice": "None", - "description": "Indicates that no data source is to be implemented." - } - ], - "defaultValue": "Database", - "description": "The data source implementation option." - }, - "implement_cosmos": { - "type": "computed", - "value": "(datasource == \"Cosmos\")" - }, - "implement_database": { - "type": "computed", - "value": "(datasource == \"Database\")" - }, - "implement_entityframework": { - "type": "computed", - "value": "(datasource == \"EntityFramework\")" - }, - "implement_httpagent": { - "type": "computed", - "value": "(datasource == \"HttpAgent\")" - }, - "implement_none": { - "type": "computed", - "value": "(datasource == \"None\")" - }, "created_date": { "type": "generated", "generator": "now", @@ -90,36 +42,6 @@ }, "sources": [ { - // "modifiers": [ - // { - // "condition": "(implement_none)", - // "exclude": [ "Company.AppName.Business/Validation/**/*", "Company.AppName.Business/Data/PersonData.cs" ] - // }, - // { - // "condition": "(implement_cosmos || implement_httpagent || implement_none)", - // "exclude": [ "Company.AppName.Database/**/*" ] - // }, - // { - // "condition": "(!implement_entityframework)", - // "exclude": [ "Company.AppName.Business/Data/AppNameEfDb.cs", "Company.AppName.Business/Data/AppNameEfDbContext.cs" ] - // }, - // { - // "condition": "(!implement_cosmos)", - // "exclude": [ "Company.AppName.Business/Data/AppNameCosmosDb.cs", "Company.AppName.Test/Cosmos/**/*" ] - // }, - // { - // "condition": "(!implement_httpagent)", - // "exclude": [ "Company.AppName.Business/Data/XxxAgent.cs", "Company.AppName.Business/Data/ReferenceDataData.cs" ] - // }, - // { - // "condition": "(implement_httpagent)", - // "exclude": [ "Company.AppName.Business/Data/PersonData.cs", "Company.AppName.Business/Validation/PersonArgsValidator.cs" ] - // }, - // { - // "condition": "(!implement_database && !implement_entityframework)", - // "exclude": [ "Company.AppName.Business/Data/AppNameDb.cs", "Company.AppName.Test/Data/**/*" ] - // } - // ] } ], "primaryOutputs": [ From 8c7a7d3ba1861fd4292b6ae8d56be5f7c8304cf8 Mon Sep 17 00:00:00 2001 From: Piotr Date: Sun, 2 Oct 2022 21:54:39 -0400 Subject: [PATCH 38/39] preparing for template nuget Signed-off-by: Piotr --- tools/CoreEx.Template/CoreEx.Template.csproj | 2 +- .../content/.template.config/template.json | 11 + .../Company.AppName.Functions/.gitignore | 261 ------------------ .../pipeline-build.yml | 0 .../pipeline-infra-destroy.yml | 0 .../pipeline-infra.yml | 0 .../pipeline-release.yml | 0 .../pull_request_template/branches/main.md | 0 .../pull_request_template/branches/preprod.md | 0 .../pull_request_template/branches/test.md | 0 .../pull_request_template.md | 0 .../templates/app-template.yml | 0 .../templates/function-template.yml | 0 .../templates/infra-destroy-template.yml | 0 .../templates/infra-template.yml | 0 .../Dockerfile | 0 .../devcontainer.json | 0 .../devinit.json | 0 .../docker-compose.yml | 0 .../content/{.dockerignore => _dockerignore} | 0 .../content/{.gitignore => _gitignore} | 0 .../{.vscode => _vscode}/extensions.json | 0 .../content/{.vscode => _vscode}/launch.json | 0 .../{.vscode => _vscode}/settings.json | 0 .../content/{.vscode => _vscode}/tasks.json | 0 25 files changed, 12 insertions(+), 262 deletions(-) delete mode 100644 tools/CoreEx.Template/content/Company.AppName.Functions/.gitignore rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/pipeline-build.yml (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/pipeline-infra-destroy.yml (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/pipeline-infra.yml (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/pipeline-release.yml (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/pull_request_template/branches/main.md (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/pull_request_template/branches/preprod.md (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/pull_request_template/branches/test.md (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/pull_request_template/pull_request_template.md (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/templates/app-template.yml (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/templates/function-template.yml (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/templates/infra-destroy-template.yml (100%) rename tools/CoreEx.Template/content/{.azuredevops => _azuredevops}/templates/infra-template.yml (100%) rename tools/CoreEx.Template/content/{.devcontainer => _devcontainer}/Dockerfile (100%) rename tools/CoreEx.Template/content/{.devcontainer => _devcontainer}/devcontainer.json (100%) rename tools/CoreEx.Template/content/{.devcontainer => _devcontainer}/devinit.json (100%) rename tools/CoreEx.Template/content/{.devcontainer => _devcontainer}/docker-compose.yml (100%) rename tools/CoreEx.Template/content/{.dockerignore => _dockerignore} (100%) rename tools/CoreEx.Template/content/{.gitignore => _gitignore} (100%) rename tools/CoreEx.Template/content/{.vscode => _vscode}/extensions.json (100%) rename tools/CoreEx.Template/content/{.vscode => _vscode}/launch.json (100%) rename tools/CoreEx.Template/content/{.vscode => _vscode}/settings.json (100%) rename tools/CoreEx.Template/content/{.vscode => _vscode}/tasks.json (100%) diff --git a/tools/CoreEx.Template/CoreEx.Template.csproj b/tools/CoreEx.Template/CoreEx.Template.csproj index e9b70f48..1cdb44b9 100644 --- a/tools/CoreEx.Template/CoreEx.Template.csproj +++ b/tools/CoreEx.Template/CoreEx.Template.csproj @@ -13,7 +13,7 @@ true false content - true + diff --git a/tools/CoreEx.Template/content/.template.config/template.json b/tools/CoreEx.Template/content/.template.config/template.json index ef1ecee1..f5a89533 100644 --- a/tools/CoreEx.Template/content/.template.config/template.json +++ b/tools/CoreEx.Template/content/.template.config/template.json @@ -42,6 +42,17 @@ }, "sources": [ { + "modifiers": [ + { + "rename": { + "_azuredevops": ".azuredevops", + "_gitignore": ".gitignore", + "_dockerignore": ".dockerignore", + "_devcontainer": ".devcontainer", + "_vscode": ".vscode" + } + } + ] } ], "primaryOutputs": [ diff --git a/tools/CoreEx.Template/content/Company.AppName.Functions/.gitignore b/tools/CoreEx.Template/content/Company.AppName.Functions/.gitignore deleted file mode 100644 index 2c42eb3f..00000000 --- a/tools/CoreEx.Template/content/Company.AppName.Functions/.gitignore +++ /dev/null @@ -1,261 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc \ No newline at end of file diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml b/tools/CoreEx.Template/content/_azuredevops/pipeline-build.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/pipeline-build.yml rename to tools/CoreEx.Template/content/_azuredevops/pipeline-build.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml b/tools/CoreEx.Template/content/_azuredevops/pipeline-infra-destroy.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/pipeline-infra-destroy.yml rename to tools/CoreEx.Template/content/_azuredevops/pipeline-infra-destroy.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml b/tools/CoreEx.Template/content/_azuredevops/pipeline-infra.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/pipeline-infra.yml rename to tools/CoreEx.Template/content/_azuredevops/pipeline-infra.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml b/tools/CoreEx.Template/content/_azuredevops/pipeline-release.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/pipeline-release.yml rename to tools/CoreEx.Template/content/_azuredevops/pipeline-release.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/main.md b/tools/CoreEx.Template/content/_azuredevops/pull_request_template/branches/main.md similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/main.md rename to tools/CoreEx.Template/content/_azuredevops/pull_request_template/branches/main.md diff --git a/tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/preprod.md b/tools/CoreEx.Template/content/_azuredevops/pull_request_template/branches/preprod.md similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/preprod.md rename to tools/CoreEx.Template/content/_azuredevops/pull_request_template/branches/preprod.md diff --git a/tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/test.md b/tools/CoreEx.Template/content/_azuredevops/pull_request_template/branches/test.md similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/pull_request_template/branches/test.md rename to tools/CoreEx.Template/content/_azuredevops/pull_request_template/branches/test.md diff --git a/tools/CoreEx.Template/content/.azuredevops/pull_request_template/pull_request_template.md b/tools/CoreEx.Template/content/_azuredevops/pull_request_template/pull_request_template.md similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/pull_request_template/pull_request_template.md rename to tools/CoreEx.Template/content/_azuredevops/pull_request_template/pull_request_template.md diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml b/tools/CoreEx.Template/content/_azuredevops/templates/app-template.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/templates/app-template.yml rename to tools/CoreEx.Template/content/_azuredevops/templates/app-template.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml b/tools/CoreEx.Template/content/_azuredevops/templates/function-template.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/templates/function-template.yml rename to tools/CoreEx.Template/content/_azuredevops/templates/function-template.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/infra-destroy-template.yml b/tools/CoreEx.Template/content/_azuredevops/templates/infra-destroy-template.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/templates/infra-destroy-template.yml rename to tools/CoreEx.Template/content/_azuredevops/templates/infra-destroy-template.yml diff --git a/tools/CoreEx.Template/content/.azuredevops/templates/infra-template.yml b/tools/CoreEx.Template/content/_azuredevops/templates/infra-template.yml similarity index 100% rename from tools/CoreEx.Template/content/.azuredevops/templates/infra-template.yml rename to tools/CoreEx.Template/content/_azuredevops/templates/infra-template.yml diff --git a/tools/CoreEx.Template/content/.devcontainer/Dockerfile b/tools/CoreEx.Template/content/_devcontainer/Dockerfile similarity index 100% rename from tools/CoreEx.Template/content/.devcontainer/Dockerfile rename to tools/CoreEx.Template/content/_devcontainer/Dockerfile diff --git a/tools/CoreEx.Template/content/.devcontainer/devcontainer.json b/tools/CoreEx.Template/content/_devcontainer/devcontainer.json similarity index 100% rename from tools/CoreEx.Template/content/.devcontainer/devcontainer.json rename to tools/CoreEx.Template/content/_devcontainer/devcontainer.json diff --git a/tools/CoreEx.Template/content/.devcontainer/devinit.json b/tools/CoreEx.Template/content/_devcontainer/devinit.json similarity index 100% rename from tools/CoreEx.Template/content/.devcontainer/devinit.json rename to tools/CoreEx.Template/content/_devcontainer/devinit.json diff --git a/tools/CoreEx.Template/content/.devcontainer/docker-compose.yml b/tools/CoreEx.Template/content/_devcontainer/docker-compose.yml similarity index 100% rename from tools/CoreEx.Template/content/.devcontainer/docker-compose.yml rename to tools/CoreEx.Template/content/_devcontainer/docker-compose.yml diff --git a/tools/CoreEx.Template/content/.dockerignore b/tools/CoreEx.Template/content/_dockerignore similarity index 100% rename from tools/CoreEx.Template/content/.dockerignore rename to tools/CoreEx.Template/content/_dockerignore diff --git a/tools/CoreEx.Template/content/.gitignore b/tools/CoreEx.Template/content/_gitignore similarity index 100% rename from tools/CoreEx.Template/content/.gitignore rename to tools/CoreEx.Template/content/_gitignore diff --git a/tools/CoreEx.Template/content/.vscode/extensions.json b/tools/CoreEx.Template/content/_vscode/extensions.json similarity index 100% rename from tools/CoreEx.Template/content/.vscode/extensions.json rename to tools/CoreEx.Template/content/_vscode/extensions.json diff --git a/tools/CoreEx.Template/content/.vscode/launch.json b/tools/CoreEx.Template/content/_vscode/launch.json similarity index 100% rename from tools/CoreEx.Template/content/.vscode/launch.json rename to tools/CoreEx.Template/content/_vscode/launch.json diff --git a/tools/CoreEx.Template/content/.vscode/settings.json b/tools/CoreEx.Template/content/_vscode/settings.json similarity index 100% rename from tools/CoreEx.Template/content/.vscode/settings.json rename to tools/CoreEx.Template/content/_vscode/settings.json diff --git a/tools/CoreEx.Template/content/.vscode/tasks.json b/tools/CoreEx.Template/content/_vscode/tasks.json similarity index 100% rename from tools/CoreEx.Template/content/.vscode/tasks.json rename to tools/CoreEx.Template/content/_vscode/tasks.json From 1b266d97e55eeb7322c5a43a6f1f67b30fa66800 Mon Sep 17 00:00:00 2001 From: Eric Sibly Date: Wed, 5 Oct 2022 09:14:30 -0700 Subject: [PATCH 39/39] Update nuget publish powershell. --- nuget-publish.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nuget-publish.ps1 b/nuget-publish.ps1 index b901d4eb..ddd713fc 100644 --- a/nuget-publish.ps1 +++ b/nuget-publish.ps1 @@ -56,7 +56,8 @@ param( "src\CoreEx.Cosmos", "src\CoreEx.FluentValidation", "src\CoreEx.Newtonsoft", - "src\CoreEx.Validation") + "src\CoreEx.Validation", + "tools\CoreEx.Template") ) $ShouldPublishRemote = (![string]::IsNullOrEmpty($apiKey) -and ![string]::IsNullOrEmpty($NugetServer))