├── .azdo ├── README.md └── pipelines │ └── azure-dev.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── README.md │ ├── azure-dev.yml │ ├── scheduled-azure-dev.yml │ └── scheduled-azure-teardown.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── 3rdParty.md ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Relecloud.sln ├── SECURITY.md ├── SUPPORT.md ├── additional-resources.md ├── assets ├── diagrams │ ├── modern-web-app-dotnet-dev.vsdx │ ├── modern-web-app-dotnet-vnet.vsdx │ └── modern-web-app-dotnet.vsdx ├── icons │ ├── modern-web-app-dotnet-dev.svg │ ├── modern-web-app-dotnet-vnet.svg │ └── modern-web-app-dotnet.svg ├── images │ ├── Azd-Env-New.png │ ├── AzdoSetup │ │ ├── 1CreateAPipeline.png │ │ ├── 2CreateAPipeline.png │ │ ├── 3CreateAPipeline.png │ │ ├── 4CreateAPipeline.png │ │ └── 5CreateAPipeline.png │ ├── Guide │ │ ├── AcaReplicaCount.png │ │ ├── AcaReplicaCountIncreased.png │ │ ├── AiTrace.png │ │ ├── AiTransactionSearch.png │ │ ├── DisabledFeatureFlag.png │ │ ├── ServiceBusMetrics.png │ │ ├── Simulating_AppInsightsTopRequests.png │ │ ├── Simulating_AppServiceRestart.png │ │ ├── Simulating_CircuitBreakerPart1.png │ │ ├── Simulating_CircuitBreakerPart2.png │ │ ├── Simulating_ConfigExplorer.png │ │ ├── Simulating_RetryPattern.png │ │ └── WebAppHomePage.png │ ├── WebAppHomePage.png │ ├── configure-multiple-startup-projects.png │ ├── mwa-architecture.png │ ├── mwa-communication.png │ ├── vscode-reopen-in-container-command.png │ └── vscode-reopen-in-container.png └── slo-calculation.md ├── azure.yaml ├── demo.md ├── developer-experience.md ├── global.json ├── infra ├── bicepconfig.json ├── core │ ├── compute │ │ ├── postDeploymentScript │ │ │ └── post-deployment.sh │ │ └── ubuntu-jumpbox.bicep │ ├── config │ │ └── app-configuration.bicep │ ├── containers │ │ ├── container-registry-replication.bicep │ │ └── container-registry.bicep │ ├── cost-management │ │ └── budget.bicep │ ├── database │ │ ├── azure-cache-for-redis.bicep │ │ ├── create-sql-user-and-role.bicep │ │ ├── scripts │ │ │ └── create-sql-user-and-role.ps1 │ │ ├── sql-database.bicep │ │ └── sql-server.bicep │ ├── hosting │ │ ├── app-service-plan.bicep │ │ ├── app-service.bicep │ │ └── container-app.bicep │ ├── identity │ │ ├── container-registry-role-assignments.bicep │ │ ├── managed-identity.bicep │ │ └── resource-group-role-assignment.bicep │ ├── monitor │ │ ├── application-insights.bicep │ │ └── log-analytics-workspace.bicep │ ├── network │ │ ├── bastion-host.bicep │ │ ├── ddos-protection-plan.bicep │ │ ├── firewall.bicep │ │ ├── network-security-group.bicep │ │ ├── peer-virtual-network.bicep │ │ ├── private-dns-zone-link.bicep │ │ ├── private-dns-zone.bicep │ │ ├── private-endpoint.bicep │ │ ├── public-ip-address.bicep │ │ ├── route-table.bicep │ │ └── virtual-network.bicep │ ├── security │ │ ├── front-door-route-approval.bicep │ │ ├── front-door-route.bicep │ │ ├── front-door-with-waf.bicep │ │ ├── key-vault-secrets.bicep │ │ ├── key-vault.bicep │ │ └── scripts │ │ │ └── front-door-route-approval.sh │ └── storage │ │ ├── storage-account-blob.bicep │ │ └── storage-account.bicep ├── main.bicep ├── main.parameters.json ├── modules │ ├── application-appservice.bicep │ ├── application-container-apps.bicep │ ├── application-post-config.bicep │ ├── application-resources.bicep │ ├── azure-fqdns.jsonc │ ├── azure-monitor.bicep │ ├── grant-secret-user.bicep │ ├── hub-network.bicep │ ├── naming.bicep │ ├── peer-networks.bicep │ ├── private-dns-zones.bicep │ ├── resource-groups.bicep │ ├── shared-frontdoor.bicep │ ├── spoke-network.bicep │ └── telemetry.bicep ├── naming.overrides.jsonc ├── scripts │ ├── postdeploy │ │ ├── show-webapp-uri.ps1 │ │ └── show-webapp-uri.sh │ ├── postprovision │ │ ├── call-create-app-registrations.ps1 │ │ ├── call-create-app-registrations.sh │ │ └── create-app-registrations.ps1 │ ├── predeploy │ │ ├── call-set-app-configuration.ps1 │ │ ├── call-set-app-configuration.sh │ │ └── set-app-configuration.ps1 │ ├── predown │ │ ├── call-cleanup.ps1 │ │ └── call-cleanup.sh │ └── preprovision │ │ ├── validate-params.ps1 │ │ ├── validate-params.sh │ │ ├── whats-my-ip.ps1 │ │ └── whats-my-ip.sh └── types │ ├── ApplicationIdentity.bicep │ ├── BuildAgentSettings.bicep │ ├── DeploymentSettings.bicep │ ├── DiagnosticSettings.bicep │ ├── FrontDoorSettings.bicep │ ├── PrivateEndpointSettings.bicep │ ├── RedisUser.bicep │ └── UserIdentity.bicep ├── known-issues.md ├── nuget.config ├── prerequisites.md ├── prod-deployment.md ├── src ├── Relecloud.Messaging │ ├── IMessageBus.cs │ ├── IMessageProcessor.cs │ ├── IMessageSender.cs │ ├── MessageBusOptions.cs │ ├── Messages │ │ ├── TicketRenderCompleteMessage.cs │ │ └── TicketRenderRequestMessage.cs │ ├── Relecloud.Messaging.csproj │ └── ServiceBus │ │ ├── AzureServiceBusMessageBus.cs │ │ ├── AzureServiceBusMessageBusExtensions.cs │ │ ├── AzureServiceBusMessageProcessor.cs │ │ └── AzureServiceBusMessageSender.cs ├── Relecloud.Models │ ├── ConcertContext │ │ ├── Concert.cs │ │ ├── CreateResult.cs │ │ ├── Customer.cs │ │ ├── DeleteResult.cs │ │ ├── PagedResult.cs │ │ ├── Ticket.cs │ │ ├── TicketNumber.cs │ │ ├── UpdateResult.cs │ │ └── User.cs │ ├── Relecloud.Models.csproj │ ├── Search │ │ ├── ConcertSearchResult.cs │ │ ├── SearchFacet.cs │ │ ├── SearchFacetValue.cs │ │ ├── SearchRequest.cs │ │ └── SearchResponse.cs │ ├── Services │ │ ├── IConcertContextService.cs │ │ ├── IConcertSearchService.cs │ │ └── IServiceProviderResult.cs │ └── TicketManagement │ │ ├── CountAvailableTicketsResult.cs │ │ ├── HaveTicketsBeenSoldResult.cs │ │ ├── Payment │ │ ├── CardTypes.cs │ │ └── PaymentDetails.cs │ │ ├── PurchaseTicketsRequest.cs │ │ ├── PurchaseTicketsResult.cs │ │ ├── PurchaseTicketsResultStatus.cs │ │ ├── ReserveTicketsResult.cs │ │ └── ReserveTicketsResultStatus.cs ├── Relecloud.TicketRenderer │ ├── Dockerfile │ ├── Extensions.cs │ ├── Fonts │ │ └── Open_Sans │ │ │ ├── OFL.txt │ │ │ ├── OpenSans-Bold.ttf │ │ │ └── OpenSans-Regular.ttf │ ├── Models │ │ ├── AzureStorageOptions.cs │ │ └── ResilienceOptions.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Relecloud.TicketRenderer.csproj │ ├── Services │ │ ├── AzureImageStorage.cs │ │ ├── IBarcodeGenerator.cs │ │ ├── IImageStorage.cs │ │ ├── ITicketRenderer.cs │ │ ├── RandomBarcodeGenerator.cs │ │ └── TicketRenderer.cs │ ├── TicketRenderRequestMessageHandler.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── Relecloud.Web.CallCenter.Api │ ├── Controllers │ │ ├── ConcertController.cs │ │ ├── ImageController.cs │ │ ├── SearchController.cs │ │ ├── TicketController.cs │ │ └── UserController.cs │ ├── FeatureFlags.cs │ ├── Infrastructure │ │ ├── ApplicationInitializer.cs │ │ ├── CacheKeys.cs │ │ ├── IntermittentErrorRequestMiddleware.cs │ │ ├── ModelStateDictionaryExtensions.cs │ │ └── Roles.cs │ ├── Migrations │ │ ├── 20220125000051_AddVisibleFields.Designer.cs │ │ ├── 20220125000051_AddVisibleFields.cs │ │ ├── 20220125000722_AddAuditFieldsToConcert.Designer.cs │ │ ├── 20220125000722_AddAuditFieldsToConcert.cs │ │ ├── 20220126181356_AddCheckoutTables.Designer.cs │ │ ├── 20220126181356_AddCheckoutTables.cs │ │ ├── 20220208203826_CreateTicketNumbers.Designer.cs │ │ ├── 20220208203826_CreateTicketNumbers.cs │ │ ├── 20220208231619_SelectTicketManagementService.Designer.cs │ │ ├── 20220208231619_SelectTicketManagementService.cs │ │ ├── 20220209201351_TicketServiceConcertIdIsNullable.Designer.cs │ │ ├── 20220209201351_TicketServiceConcertIdIsNullable.cs │ │ ├── 20220215010613_AddTicketNumberToTicket.Designer.cs │ │ ├── 20220215010613_AddTicketNumberToTicket.cs │ │ └── ConcertDataContextModelSnapshot.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Relecloud.Web.CallCenter.Api.csproj │ ├── Services │ │ ├── IConcertRepository.cs │ │ ├── IPaymentGatewayService.cs │ │ ├── MockServices │ │ │ ├── MockConcertRepository.cs │ │ │ ├── MockConcertSearchService.cs │ │ │ ├── MockPaymentGatewayService.cs │ │ │ ├── MockTicketImageService.cs │ │ │ ├── MockTicketManagementService.cs │ │ │ └── MockTicketRenderingService.cs │ │ ├── PaymentGatewayService │ │ │ ├── CapturePaymentRequest.cs │ │ │ ├── CapturePaymentResult.cs │ │ │ ├── CapturePaymentResultStatus.cs │ │ │ ├── PreAuthPaymentRequest.cs │ │ │ ├── PreAuthPaymentResult.cs │ │ │ └── PreAuthPaymentResultStatus.cs │ │ ├── Search │ │ │ ├── AzureSearchConcertSearchService.cs │ │ │ └── SqlDatabaseConcertSearchService.cs │ │ ├── SqlDatabaseConcertRepository │ │ │ ├── ConcertDataContext.cs │ │ │ └── SqlDatabaseConcertRepository.cs │ │ └── TicketManagementService │ │ │ ├── DistributedTicketRenderingService.cs │ │ │ ├── FeatureDependentTicketRenderingServiceFactory.cs │ │ │ ├── ITicketImageService.cs │ │ │ ├── ITicketManagementService.cs │ │ │ ├── ITicketRenderingService.cs │ │ │ ├── ITicketRenderingServiceFactory.cs │ │ │ ├── LocalTicketRenderingService.cs │ │ │ ├── TicketImageService.cs │ │ │ ├── TicketManagementService.cs │ │ │ └── TicketRenderCompleteMessageHandler.cs │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json └── Relecloud.Web.CallCenter │ ├── Controllers │ ├── CartController.cs │ ├── ConcertController.cs │ ├── HomeController.cs │ ├── ImageController.cs │ └── TicketController.cs │ ├── Infrastructure │ ├── CacheKeys.cs │ ├── ExtensionMethods.cs │ ├── RelecloudApiConfiguration.cs │ └── Roles.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Relecloud.Web.CallCenter.csproj │ ├── Services │ ├── ITicketImageService.cs │ ├── ITicketPurchaseService.cs │ ├── MockServices │ │ ├── MockConcertContextService.cs │ │ ├── MockConcertSearchService.cs │ │ ├── MockTicketImageService.cs │ │ └── MockTicketPurchaseService.cs │ └── RelecloudApiServices │ │ ├── RelecloudApiConcertSearchService.cs │ │ ├── RelecloudApiConcertService.cs │ │ ├── RelecloudApiOptions.cs │ │ ├── RelecloudApiTicketImageService.cs │ │ └── RelecloudApiTicketPurchaseService.cs │ ├── Startup.cs │ ├── ViewModels │ ├── CartViewModel.cs │ ├── CheckoutViewModel.cs │ ├── ConcertViewModel.cs │ └── TicketViewModel.cs │ ├── Views │ ├── Cart │ │ ├── Add.cshtml │ │ ├── Checkout.cshtml │ │ └── Index.cshtml │ ├── Concert │ │ ├── Create.cshtml │ │ ├── Delete.cshtml │ │ ├── Details.cshtml │ │ ├── Edit.cshtml │ │ ├── Index.cshtml │ │ ├── Search.cshtml │ │ └── Search.cshtml.cs │ ├── Home │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ ├── _Layout.cshtml.css │ │ └── _ValidationScriptsPartial.cshtml │ ├── Ticket │ │ └── Index.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ ├── css │ └── site.css │ ├── favicon.ico │ ├── img │ └── banner.jpg │ ├── js │ └── site.js │ ├── lib │ ├── bootstrap │ │ ├── LICENSE │ │ └── dist │ │ │ ├── css │ │ │ ├── bootstrap-grid.css │ │ │ ├── bootstrap-grid.css.map │ │ │ ├── bootstrap-grid.min.css │ │ │ ├── bootstrap-grid.min.css.map │ │ │ ├── bootstrap-grid.rtl.css │ │ │ ├── bootstrap-grid.rtl.css.map │ │ │ ├── bootstrap-grid.rtl.min.css │ │ │ ├── bootstrap-grid.rtl.min.css.map │ │ │ ├── bootstrap-reboot.css │ │ │ ├── bootstrap-reboot.css.map │ │ │ ├── bootstrap-reboot.min.css │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ ├── bootstrap-reboot.rtl.css │ │ │ ├── bootstrap-reboot.rtl.css.map │ │ │ ├── bootstrap-reboot.rtl.min.css │ │ │ ├── bootstrap-reboot.rtl.min.css.map │ │ │ ├── bootstrap-utilities.css │ │ │ ├── bootstrap-utilities.css.map │ │ │ ├── bootstrap-utilities.min.css │ │ │ ├── bootstrap-utilities.min.css.map │ │ │ ├── bootstrap-utilities.rtl.css │ │ │ ├── bootstrap-utilities.rtl.css.map │ │ │ ├── bootstrap-utilities.rtl.min.css │ │ │ ├── bootstrap-utilities.rtl.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ ├── bootstrap.min.css │ │ │ ├── bootstrap.min.css.map │ │ │ ├── bootstrap.rtl.css │ │ │ ├── bootstrap.rtl.css.map │ │ │ ├── bootstrap.rtl.min.css │ │ │ └── bootstrap.rtl.min.css.map │ │ │ └── js │ │ │ ├── bootstrap.bundle.js │ │ │ ├── bootstrap.bundle.js.map │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ ├── bootstrap.esm.js │ │ │ ├── bootstrap.esm.js.map │ │ │ ├── bootstrap.esm.min.js │ │ │ ├── bootstrap.esm.min.js.map │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.js.map │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ ├── jquery-validation-unobtrusive │ │ ├── LICENSE.txt │ │ ├── jquery.validate.unobtrusive.js │ │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ │ ├── LICENSE.md │ │ └── dist │ │ │ ├── additional-methods.js │ │ │ ├── additional-methods.min.js │ │ │ ├── jquery.validate.js │ │ │ └── jquery.validate.min.js │ └── jquery │ │ ├── LICENSE.txt │ │ └── dist │ │ ├── jquery.js │ │ ├── jquery.min.js │ │ └── jquery.min.map │ └── robots.txt ├── tests ├── Relecloud.Messaging.Tests │ ├── AzureServiceBusMessageBusTests.cs │ ├── AzureServiceBusMessageProcessorTests.cs │ ├── AzureServiceBusMessageSenderTests.cs │ ├── GlobalUsings.cs │ ├── MessageBusOptionsTests.cs │ └── Relecloud.Messaging.Tests.csproj ├── Relecloud.TestHelpers │ ├── ExpectedImages │ │ ├── test-ticket-linux.png │ │ └── test-ticket-windows.png │ ├── Relecloud.TestHelpers.csproj │ ├── RelecloudTestHelpers.cs │ ├── TestAzureResponse.cs │ ├── TestBlobClient.cs │ ├── TestServiceBusClient.cs │ ├── TestServiceBusProcessor.cs │ ├── TestServiceBusReceiver.cs │ └── TestServiceBusSender.cs ├── Relecloud.TicketRenderer.IntegrationTests │ ├── GlobalUsings.cs │ ├── Relecloud.TicketRenderer.IntegrationTests.csproj │ ├── TicketRendererFixture.cs │ └── TicketRenderingTest.cs ├── Relecloud.TicketRenderer.Tests │ ├── AzureImageStorageTests.cs │ ├── AzureStorageOptionsTests.cs │ ├── GlobalUsings.cs │ ├── Relecloud.TicketRenderer.Tests.csproj │ ├── ResilienceOptionsTests.cs │ ├── TestBarcodeGenerator.cs │ ├── TestContext.cs │ ├── TicketRenderRequestMessageHandlerTests.cs │ └── TicketRendererTests.cs └── Relecloud.Web.CallCenter.Api.Tests │ ├── DistributedTicketRenderingServiceTests.cs │ ├── FeatureDependentTicketRenderingServiceFactoryTests.cs │ ├── GlobalUsings.cs │ ├── Relecloud.Web.CallCenter.Api.Tests.csproj │ ├── TestHelpers.cs │ └── TicketRenderCompleteMessageHandlerTests.cs ├── testscripts ├── README.md ├── call-validate-deployment.sh ├── cleanup.ps1 ├── setup.ps1 ├── validate-deployment.ps1 └── validate-deployment.sh └── troubleshooting.md /.azdo/pipelines/azure-dev.yml: -------------------------------------------------------------------------------- 1 | # This file is part of the sample that you can use to build your devOps automation. 2 | # See the README markdown file for further details 3 | 4 | trigger: 5 | - main 6 | 7 | pool: 8 | vmImage: ubuntu-latest 9 | 10 | container: mcr.microsoft.com/azure-dev-cli-apps:1.5.0 11 | variables: 12 | - name: env_name 13 | value: $(AZD_AZURE_ENV_NAME)daily 14 | steps: 15 | 16 | - task: AzureCLI@2 17 | displayName: Azure Dev Provision 18 | inputs: 19 | azureSubscription: azconnection 20 | scriptType: bash 21 | scriptLocation: inlineScript 22 | inlineScript: | 23 | azd provision --no-prompt 24 | env: 25 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 26 | AZURE_ENV_NAME: $(env_name) 27 | AZURE_LOCATION: $(AZURE_LOCATION) 28 | 29 | - task: AzureCLI@2 30 | # temporary work around for known issue with multiple resource groups 31 | displayName: Set AZD resource group 32 | inputs: 33 | azureSubscription: azconnection 34 | scriptType: bash 35 | scriptLocation: inlineScript 36 | inlineScript: | 37 | azd env set AZURE_RESOURCE_GROUP $(env_name)-rg 38 | 39 | - task: AzureCLI@2 40 | displayName: Azure Dev Deploy 41 | inputs: 42 | azureSubscription: azconnection 43 | scriptType: bash 44 | scriptLocation: inlineScript 45 | inlineScript: | 46 | azd deploy --no-prompt 47 | env: 48 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 49 | AZURE_ENV_NAME: $(env_name) 50 | AZURE_LOCATION: $(AZURE_LOCATION) 51 | 52 | - task: AzureCLI@2 53 | displayName: QA - Validate Deployment 54 | inputs: 55 | azureSubscription: azconnection 56 | scriptType: bash 57 | scriptLocation: inlineScript 58 | inlineScript: | 59 | echo 'your tests here' 60 | 61 | - task: AzureCLI@2 62 | displayName: Azure Dev Down 63 | inputs: 64 | azureSubscription: azconnection 65 | scriptType: bash 66 | scriptLocation: inlineScript 67 | inlineScript: | 68 | azd down --force --purge --no-prompt 69 | env: 70 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 71 | AZURE_ENV_NAME: $(env_name) 72 | AZURE_LOCATION: $(AZURE_LOCATION) -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT 2 | FROM mcr.microsoft.com/vscode/devcontainers/dotnet:${VARIANT} -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-app-pattern-dotnet", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "args": { 6 | "VARIANT": "8.0-bookworm" 7 | } 8 | }, 9 | "runArgs": ["--init", "--privileged"], 10 | 11 | "customizations": { 12 | "vscode": { 13 | "extensions": [ 14 | "ms-azuretools.azure-dev", 15 | "ms-azuretools.vscode-azureappservice", 16 | "ms-azuretools.vscode-azureresourcegroups", 17 | "ms-azuretools.vscode-azurestorage", 18 | "ms-azuretools.vscode-bicep", 19 | "ms-azuretools.vscode-docker", 20 | "ms-dotnettools.csharp", 21 | "ms-mssql.mssql", 22 | "ms-vscode.azure-account", 23 | "ms-vscode.PowerShell" 24 | ] 25 | } 26 | }, 27 | "features": { 28 | "ghcr.io/devcontainers/features/dotnet:2": { 29 | "version": "8.0", 30 | "additionalVersions": "6.0" 31 | }, 32 | "ghcr.io/azure/azure-dev/azd:latest": { 33 | // keep this version in sync with the version defined in the following files: 34 | // - /infra/core/compute/postDeploymentScript/post-deployment.sh 35 | // - /.github/workflows/azure-dev.yml 36 | // - /.github/workflows/scheduled-azure-dev.yml 37 | // - /.github/workflows/scheduled-azure-teardown.yml 38 | "version": "1.10.2" 39 | }, 40 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 41 | "version": "latest" 42 | }, 43 | "ghcr.io/devcontainers/features/azure-cli:1": {}, 44 | "ghcr.io/devcontainers/features/common-utils:2": {}, 45 | "ghcr.io/devcontainers/features/github-cli:1": {}, 46 | "ghcr.io/devcontainers/features/powershell:1": { 47 | "version": "7.4.2", 48 | "modules": "Az,SqlServer" 49 | }, 50 | "ghcr.io/devcontainers/features/sshd:1": {} 51 | }, 52 | // resolves error: dubious ownership of the workspace folder 53 | "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}" 54 | } 55 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | !**/.gitignore 27 | !.git/HEAD 28 | !.git/config 29 | !.git/packed-refs 30 | !.git/refs/heads/** -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | # Don't use tabs for indentation. 8 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 9 | indent_style = space 10 | 11 | charset = utf-8 12 | 13 | # Where supported, trim trailing whitespace on all lines. 14 | trim_trailing_whitespace = true 15 | 16 | # Where supported (e.g. in VS Code but not VS), add a final newline to files. 17 | insert_final_newline = true 18 | 19 | # Ensure all source files include copyright notice 20 | file_header_template = Copyright (c) Microsoft Corporation. All Rights Reserved.\nLicensed under the MIT License. 21 | 22 | # Code files 23 | [*.{cs,csx,vb,vbx}] 24 | indent_size = 4 25 | dotnet_sort_system_directives_first = true:warning 26 | 27 | # Xml project files 28 | [*.{*proj,vcxproj.filters,projitems}] 29 | indent_size = 2 30 | 31 | # Xml config files 32 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,tasks,xml,yml}] 33 | indent_size = 2 34 | 35 | # JSON files 36 | [*.json] 37 | indent_size = 2 38 | 39 | # PowerShell 40 | [*.{ps1,psm1}] 41 | indent_size = 4 42 | 43 | # Shell 44 | [*.sh] 45 | indent_size = 4 46 | end_of_line = lf 47 | 48 | # Dotnet code style settings: 49 | [*.cs] 50 | # Sort using and Import directives with System.* appearing first 51 | dotnet_sort_system_directives_first = true 52 | 53 | # use int x = .. over Int32 54 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 55 | 56 | # use int.MaxValue over Int32.MaxValue 57 | dotnet_style_predefined_type_for_member_access = true:suggestion 58 | 59 | # Require var all the time. 60 | csharp_style_var_for_built_in_types = true:suggestion 61 | csharp_style_var_when_type_is_apparent = true:suggestion 62 | csharp_style_var_elsewhere = true:suggestion 63 | 64 | # Newline settings 65 | csharp_new_line_before_open_brace = all 66 | csharp_new_line_before_else = true 67 | csharp_new_line_before_catch = true 68 | csharp_new_line_before_finally = true 69 | csharp_new_line_before_members_in_object_initializers = true 70 | csharp_new_line_before_members_in_anonymous_types = true 71 | 72 | # IDE0073: The file header is missing or not located at the top of the file 73 | dotnet_diagnostic.IDE0073.severity = warning 74 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows, macOS] 28 | - Shell [e.g. PowerShell Core, Bash] 29 | - Shell Version [e.g. PowerShell 7.3.3] 30 | - az version [e.g.] 31 | ``` 32 | "azure-cli": "2.46.0", 33 | "azure-cli-core": "2.46.0", 34 | "azure-cli-telemetry": "1.0.8", 35 | "extensions": 36 | "aks-preview": "0.5.100", 37 | "containerapp": "0.3.10", 38 | "deploy-to-azure": "0.2.0" 39 | ``` 40 | - az bicep version [e.g., Bicep CLI version 0.15.31 (3ba6e06a8d)] 41 | - azd version [e.g., azd version 0.7.0-beta.1 (commit 9ce71659f7688d0dc3dda8b84e5accedca58cf01)] 42 | - dotnet --version [e.g., 6.0.407] 43 | 44 | **Additional context** 45 | Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | # This file is part of the sample that you can use to build your devOps automation. 2 | # See the README markdown file for further details 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | # on: 8 | # pull_request: 9 | # types: [opened, reopened] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | container: 15 | image: mcr.microsoft.com/azure-dev-cli-apps:1.10.2 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | # login to run ado commands such provision, deploy, and down 21 | - name: Log in with Azure (Client Credentials) 22 | if: ${{ env.AZURE_CREDENTIALS != '' }} 23 | run: | 24 | $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; 25 | Write-Host "::add-mask::$($info.clientSecret)" 26 | 27 | azd login ` 28 | --client-id "$($info.clientId)" ` 29 | --client-secret "$($info.clientSecret)" ` 30 | --tenant-id "$($info.tenantId)" 31 | shell: pwsh 32 | env: 33 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 34 | 35 | - name: Azure Dev Provision 36 | run: azd provision --no-prompt 37 | env: 38 | AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }}dev 39 | AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} 40 | AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 41 | 42 | - name: Azure Dev Deploy 43 | run: azd deploy --no-prompt 44 | env: 45 | AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }}dev 46 | AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} 47 | AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 48 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. 8 | "name": ".NET Core Launch (api)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/Relecloud.Web.CallCenter.Api/bin/Debug/net6.0/Relecloud.Web.CallCenter.Api.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/Relecloud.Web.CallCenter.Api", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "src/Relecloud.sln" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/Relecloud.sln", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/Relecloud.sln", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/src/Relecloud.sln" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /3rdParty.md: -------------------------------------------------------------------------------- 1 | **Third-Party Package Dependencies for Relecloud** 2 | 3 | The following third-party packages were added to the Relecloud solution. 4 | 5 | | Package Name | Package Link | License Type | License Link | Purpose | Date Added | 6 | |--------------|------------------------------------------------------------|--------------|-----------------------------------------------------------------------------------------|------------------------------------------|--------------| 7 | | **AspNetCore.HealthChecks.AzureServiceBus** | [GitHub - AspNetCore.Diagnostics.HealthChecks](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) | Apache 2.0 | [Apache 2.0 License](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/blob/master/LICENSE) | Used to monitor the health of the ticket rendering service's Azure Service Bus connection | 2023-12-15 | 8 | | **AspNetCore.HealthChecks.AzureStorage** | [GitHub - AspNetCore.Diagnostics.HealthChecks](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) | Apache 2.0 | [Apache 2.0 License](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/blob/master/LICENSE) | Used to monitor the health of the ticket rendering service's Azure Storage connection | 2023-12-15 | 9 | | **Open Sans**| [Google Fonts - Open Sans](https://github.com/googlefonts/opensans) | OFL | [Open Font License](https://openfontlicense.org/documents/OFL.txt) | Font used to render ticket images | 2023-12-18 | 10 | | **QRCoder** | [GitHub - QRCoder](https://github.com/codebude/QRCoder) | MIT | [MIT License](https://github.com/codebude/QRCoder/blob/master/LICENSE.txt) | Used to generate QR codes that ticket holders can use to quickly look up their tickets. | 2023-10-02 | 11 | | **SkiaSharp** | [GitHub - SkiaSharp](https://github.com/mono/SkiaSharp) | MIT | [MIT License](https://github.com/mono/SkiaSharp/blob/main/LICENSE.md) | Used to render ticket images in a cross-platform manner. | 2023-12-15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | ## Code changes and 3rd-party libraries 16 | 17 | This sample is provided as guidance that represents how to apply principles outlined in the Well-Architected Framework. 18 | The provided code, and guidance, should align with existing content in published Microsoft documentation. 19 | Usage of 3rd-party libraries should also be limited to those we recommend in published Microsot documentation or to those outlined with team approval in [Third-Party Package Dependencies for Relecloud](./3rdParty.md). 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## Microsoft Support Policy 4 | 5 | If issues are encountered when deploying this reference implemenation, users can engage Microsoft support via their usual channels. Please provide correlation IDs where possible when contacting support. For instructions on how to obtain deployments and correlation Ids, see [View deployment history with Azure Resource Manager](https://learn.microsoft.com/azure/azure-resource-manager/templates/deployment-history?tabs=azure-portal#get-deployments-and-correlation-id). 6 | 7 | The following list of issues is within the scope of Microsoft Support: 8 | 9 | * Portal deployment of the reference implmentation. 10 | * Underlying resource or resource provider issues when deploying the template. 11 | * Subscription creation via the Azure portal. 12 | * ARM deployment issues (e.g. template validation, CheckAccess API). 13 | 14 | Any issues that are deemed outside of the above list by Microsoft Support or requires bugfix in the reference implementation will be referred to GitHub issues. 15 | 16 | ## How to file issues and get help 17 | 18 | This project uses [GitHub Issues](https://github.com/azure/modern-web-app-pattern-dotnet/issues) to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicated. -------------------------------------------------------------------------------- /assets/diagrams/modern-web-app-dotnet-dev.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/diagrams/modern-web-app-dotnet-dev.vsdx -------------------------------------------------------------------------------- /assets/diagrams/modern-web-app-dotnet-vnet.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/diagrams/modern-web-app-dotnet-vnet.vsdx -------------------------------------------------------------------------------- /assets/diagrams/modern-web-app-dotnet.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/diagrams/modern-web-app-dotnet.vsdx -------------------------------------------------------------------------------- /assets/images/Azd-Env-New.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Azd-Env-New.png -------------------------------------------------------------------------------- /assets/images/AzdoSetup/1CreateAPipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/AzdoSetup/1CreateAPipeline.png -------------------------------------------------------------------------------- /assets/images/AzdoSetup/2CreateAPipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/AzdoSetup/2CreateAPipeline.png -------------------------------------------------------------------------------- /assets/images/AzdoSetup/3CreateAPipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/AzdoSetup/3CreateAPipeline.png -------------------------------------------------------------------------------- /assets/images/AzdoSetup/4CreateAPipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/AzdoSetup/4CreateAPipeline.png -------------------------------------------------------------------------------- /assets/images/AzdoSetup/5CreateAPipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/AzdoSetup/5CreateAPipeline.png -------------------------------------------------------------------------------- /assets/images/Guide/AcaReplicaCount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/AcaReplicaCount.png -------------------------------------------------------------------------------- /assets/images/Guide/AcaReplicaCountIncreased.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/AcaReplicaCountIncreased.png -------------------------------------------------------------------------------- /assets/images/Guide/AiTrace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/AiTrace.png -------------------------------------------------------------------------------- /assets/images/Guide/AiTransactionSearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/AiTransactionSearch.png -------------------------------------------------------------------------------- /assets/images/Guide/DisabledFeatureFlag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/DisabledFeatureFlag.png -------------------------------------------------------------------------------- /assets/images/Guide/ServiceBusMetrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/ServiceBusMetrics.png -------------------------------------------------------------------------------- /assets/images/Guide/Simulating_AppInsightsTopRequests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/Simulating_AppInsightsTopRequests.png -------------------------------------------------------------------------------- /assets/images/Guide/Simulating_AppServiceRestart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/Simulating_AppServiceRestart.png -------------------------------------------------------------------------------- /assets/images/Guide/Simulating_CircuitBreakerPart1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/Simulating_CircuitBreakerPart1.png -------------------------------------------------------------------------------- /assets/images/Guide/Simulating_CircuitBreakerPart2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/Simulating_CircuitBreakerPart2.png -------------------------------------------------------------------------------- /assets/images/Guide/Simulating_ConfigExplorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/Simulating_ConfigExplorer.png -------------------------------------------------------------------------------- /assets/images/Guide/Simulating_RetryPattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/Simulating_RetryPattern.png -------------------------------------------------------------------------------- /assets/images/Guide/WebAppHomePage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/Guide/WebAppHomePage.png -------------------------------------------------------------------------------- /assets/images/WebAppHomePage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/WebAppHomePage.png -------------------------------------------------------------------------------- /assets/images/configure-multiple-startup-projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/configure-multiple-startup-projects.png -------------------------------------------------------------------------------- /assets/images/mwa-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/mwa-architecture.png -------------------------------------------------------------------------------- /assets/images/mwa-communication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/mwa-communication.png -------------------------------------------------------------------------------- /assets/images/vscode-reopen-in-container-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/vscode-reopen-in-container-command.png -------------------------------------------------------------------------------- /assets/images/vscode-reopen-in-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/assets/images/vscode-reopen-in-container.png -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://azuresdkreleasepreview.blob.core.windows.net/azd/schema/azure.yaml.json 2 | 3 | name: modern-csharp-web 4 | metadata: 5 | template: modern-csharp-web@1.0.0 6 | hooks: 7 | preprovision: 8 | posix: 9 | interactive: true 10 | shell: sh 11 | run: ./infra/scripts/preprovision/validate-params.sh && ./infra/scripts/preprovision/whats-my-ip.sh 12 | windows: 13 | interactive: true 14 | shell: pwsh 15 | run: ./infra/scripts/preprovision/validate-params.ps1 && ./infra/scripts/preprovision/whats-my-ip.ps1 16 | postprovision: 17 | posix: 18 | interactive: true 19 | run: ./infra/scripts/postprovision/call-create-app-registrations.sh 20 | windows: 21 | interactive: true 22 | run: ./infra/scripts/postprovision/call-create-app-registrations.ps1 23 | predeploy: 24 | posix: 25 | interactive: true 26 | shell: sh 27 | run: ./infra/scripts/predeploy/call-set-app-configuration.sh 28 | windows: 29 | interactive: true 30 | shell: pwsh 31 | run: ./infra/scripts/predeploy/call-set-app-configuration.ps1 32 | postdeploy: 33 | posix: 34 | interactive: true 35 | run: ./infra/scripts/postdeploy/show-webapp-uri.sh 36 | windows: 37 | interactive: true 38 | run: ./infra/scripts/postdeploy/show-webapp-uri.ps1 39 | predown: 40 | posix: 41 | interactive: true 42 | run: ./infra/scripts/predown/call-cleanup.sh 43 | windows: 44 | interactive: true 45 | run: ./infra/scripts/predown/call-cleanup.ps1 46 | services: 47 | web-callcenter-service: 48 | project: src/Relecloud.Web.CallCenter.Api 49 | language: csharp 50 | host: appservice 51 | web-callcenter-frontend: 52 | project: src/Relecloud.Web.CallCenter 53 | language: csharp 54 | host: appservice 55 | rendering-service: 56 | project: src/Relecloud.TicketRenderer 57 | language: dotnet 58 | host: containerapp 59 | apiVersion: 2024-02-02-preview # Force `azd` to use this API version for GET/PATCH operations 60 | docker: 61 | # These paths are relative to the project directory 62 | path: ./Dockerfile 63 | context: ./../ 64 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | // updated to the latest released Major version 4 | // SDK and runtime version numbers do not require sync 5 | // https://learn.microsoft.com/dotnet/core/tools/global-json 6 | "version": "8.0.100", 7 | "rollForward": "latestFeature" 8 | } 9 | } -------------------------------------------------------------------------------- /infra/bicepconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "experimentalFeaturesEnabled": { 3 | "sourceMapping": true 4 | } 5 | } -------------------------------------------------------------------------------- /infra/core/identity/managed-identity.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** User-Assigned Managed Identity 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | */ 10 | 11 | // ======================================================================== 12 | // PARAMETERS 13 | // ======================================================================== 14 | 15 | @description('The Azure region for the resource.') 16 | param location string 17 | 18 | @description('The name of the primary resource') 19 | param name string 20 | 21 | @description('The tags to associate with this resource.') 22 | param tags object = {} 23 | 24 | // ======================================================================== 25 | // AZURE RESOURCES 26 | // ======================================================================== 27 | 28 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 29 | name: name 30 | location: location 31 | tags: tags 32 | } 33 | 34 | // ======================================================================== 35 | // OUTPUTS 36 | // ======================================================================== 37 | 38 | output id string = managedIdentity.id 39 | output name string = managedIdentity.name 40 | output principal_id string = managedIdentity.properties.principalId 41 | -------------------------------------------------------------------------------- /infra/core/identity/resource-group-role-assignment.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** Assigns a role to a managed identity 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | */ 10 | 11 | 12 | // ======================================================================== 13 | // PARAMETERS 14 | // ======================================================================== 15 | 16 | @description('The name of a managed identity.') 17 | param identityName string 18 | 19 | @description('Azure role id for assignment') 20 | param roleId string 21 | 22 | @description('A description of the purpose for the role assignment') 23 | param roleDescription string 24 | 25 | 26 | // ======================================================================== 27 | // AZURE RESOURCES 28 | // ======================================================================== 29 | 30 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 31 | name: identityName 32 | } 33 | 34 | resource devOpsIdentityRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 35 | name: guid(roleId, identityName, resourceGroup().id) 36 | scope: resourceGroup() 37 | properties: { 38 | principalType: 'ServicePrincipal' 39 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleId) 40 | principalId: managedIdentity.properties.principalId 41 | description: roleDescription 42 | } 43 | } 44 | 45 | // ======================================================================== 46 | // OUTPUTS 47 | // ======================================================================== 48 | 49 | output identity_name string = identityName 50 | -------------------------------------------------------------------------------- /infra/core/monitor/application-insights.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** Application Insights 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | ** 10 | ** Creates an Application Insights resource linked to the provided Log 11 | ** Analytics Workspace. 12 | */ 13 | 14 | // ======================================================================== 15 | // PARAMETERS 16 | // ======================================================================== 17 | 18 | @description('The Azure region for the resource.') 19 | param location string 20 | 21 | @description('The name of the primary resource') 22 | param name string 23 | 24 | @description('The tags to associate with this resource.') 25 | param tags object = {} 26 | 27 | /* 28 | ** Dependencies 29 | */ 30 | @description('The ID of the Log Analytics workspace to use for diagnostics and logging.') 31 | param logAnalyticsWorkspaceId string = '' 32 | 33 | /* 34 | ** Settings 35 | */ 36 | @allowed([ 'web', 'ios', 'other', 'store', 'java', 'phone' ]) 37 | @description('The kind of application being monitored.') 38 | param kind string = 'web' 39 | 40 | // ======================================================================== 41 | // AZURE RESOURCES 42 | // ======================================================================== 43 | 44 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 45 | name: name 46 | location: location 47 | tags: tags 48 | kind: kind 49 | properties: { 50 | Application_Type: kind == 'web' ? 'web' : 'other' 51 | WorkspaceResourceId: logAnalyticsWorkspaceId 52 | } 53 | } 54 | 55 | // ======================================================================== 56 | // OUTPUTS 57 | // ======================================================================== 58 | 59 | output id string = applicationInsights.id 60 | output name string = applicationInsights.name 61 | -------------------------------------------------------------------------------- /infra/core/monitor/log-analytics-workspace.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** Log Analytics Workspace 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | */ 10 | 11 | // ======================================================================== 12 | // PARAMETERS 13 | // ======================================================================== 14 | 15 | @description('The Azure region for the resource.') 16 | param location string 17 | 18 | @description('The name of the primary resource') 19 | param name string 20 | 21 | @description('The tags to associate with this resource.') 22 | param tags object = {} 23 | 24 | /* 25 | ** Dependencies 26 | */ 27 | @allowed([ 'PerGB2018', 'PerNode', 'Premium', 'Standalone', 'Standard' ]) 28 | @description('The name of the pricing SKU to use.') 29 | param sku string = 'PerGB2018' 30 | 31 | @minValue(0) 32 | @description('The workspace daily quota for ingestion. Use 0 for unlimited.') 33 | param dailyQuotaInGB int = 0 34 | 35 | // ======================================================================== 36 | // VARIABLES 37 | // ======================================================================== 38 | 39 | var skuProperties = { 40 | sku: { 41 | name: sku 42 | } 43 | } 44 | var quotaProperties = dailyQuotaInGB > 0 ? { dailyQuotaGb: dailyQuotaInGB } : {} 45 | 46 | var retentionProperties = { 47 | retentionInDays: 30 48 | } 49 | 50 | // ======================================================================== 51 | // AZURE RESOURCES 52 | // ======================================================================== 53 | 54 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 55 | name: name 56 | location: location 57 | tags: tags 58 | properties: union(skuProperties, quotaProperties, retentionProperties) 59 | } 60 | 61 | // ======================================================================== 62 | // OUTPUTS 63 | // ======================================================================== 64 | 65 | output id string = logAnalyticsWorkspace.id 66 | output name string = logAnalyticsWorkspace.name 67 | -------------------------------------------------------------------------------- /infra/core/network/ddos-protection-plan.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** DDoS Protection Plan 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | ** 10 | ** Create a DDoS Protection Plan. 11 | */ 12 | 13 | // ======================================================================== 14 | // PARAMETERS 15 | // ======================================================================== 16 | 17 | @description('The Azure region for the resource.') 18 | param location string 19 | 20 | @description('The name of the primary resource') 21 | param name string 22 | 23 | @description('The tags to associate with this resource.') 24 | param tags object = {} 25 | 26 | // ======================================================================== 27 | // AZURE RESOURCES 28 | // ======================================================================== 29 | 30 | resource ddosProtectionPlan 'Microsoft.Network/ddosProtectionPlans@2022-11-01' = { 31 | location: location 32 | name: name 33 | tags: tags 34 | properties: { 35 | 36 | } 37 | } 38 | 39 | // ======================================================================== 40 | // OUTPUTS 41 | // ======================================================================== 42 | 43 | output id string = ddosProtectionPlan.id 44 | output name string = ddosProtectionPlan.name 45 | -------------------------------------------------------------------------------- /infra/core/network/peer-virtual-network.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** Peer two virtual networks together. 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | */ 10 | 11 | // ======================================================================== 12 | // PARAMETERS 13 | // ======================================================================== 14 | 15 | @description('The name of the primary resource') 16 | param name string 17 | 18 | /* 19 | ** Dependencies 20 | */ 21 | @description('The name of the local virtual network.') 22 | param virtualNetworkName string = '' 23 | 24 | @description('The ID of the remote virtual network.') 25 | param remoteVirtualNetworkId string = '' 26 | 27 | // ======================================================================== 28 | // AZURE RESOURCES 29 | // ======================================================================== 30 | 31 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { 32 | name: virtualNetworkName 33 | 34 | resource peer 'virtualNetworkPeerings' = { 35 | name: name 36 | properties: { 37 | allowVirtualNetworkAccess: true 38 | allowGatewayTransit: false 39 | allowForwardedTraffic: false 40 | useRemoteGateways: false 41 | remoteVirtualNetwork: { 42 | id: remoteVirtualNetworkId 43 | } 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /infra/core/network/private-dns-zone-link.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** Private DNS Zone 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | ** 10 | ** Adds a vnet for DNS zone link to a private DNS zone. 11 | */ 12 | 13 | // ======================================================================== 14 | // PARAMETERS 15 | // ======================================================================== 16 | 17 | @description('The name of the primary resource') 18 | param name string 19 | 20 | /* 21 | ** Dependencies 22 | */ 23 | @description('Array of custom objects describing vNet links of the DNS zone. Each object should contain vnetName, vnetId, registrationEnabled') 24 | param virtualNetworkLinks array = [] 25 | 26 | // ======================================================================== 27 | // AZURE RESOURCES 28 | // ======================================================================== 29 | 30 | resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = { 31 | name: name 32 | } 33 | 34 | resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = [ for vnet in virtualNetworkLinks: { 35 | parent: privateDnsZone 36 | name: '${vnet.vnetName}-link' 37 | location: 'global' 38 | properties: { 39 | registrationEnabled: vnet.registrationEnabled 40 | virtualNetwork: { 41 | id: vnet.vnetId 42 | } 43 | } 44 | }] 45 | 46 | // ======================================================================== 47 | // OUTPUTS 48 | // ======================================================================== 49 | 50 | output id string = privateDnsZone.id 51 | output name string = privateDnsZone.name 52 | 53 | -------------------------------------------------------------------------------- /infra/core/network/private-dns-zone.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** Private DNS Zone 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | ** 10 | ** Creates a private DNS zone (mostly used for private endpoints) and links 11 | ** it to the specified virtual network. 12 | */ 13 | 14 | // ======================================================================== 15 | // PARAMETERS 16 | // ======================================================================== 17 | 18 | @description('The name of the primary resource') 19 | param name string 20 | 21 | @description('The tags to associate with this resource.') 22 | param tags object = {} 23 | 24 | /* 25 | ** Dependencies 26 | */ 27 | @description('Array of custom objects describing vNet links of the DNS zone. Each object should contain vnetName, vnetId, registrationEnabled') 28 | param virtualNetworkLinks array = [] 29 | 30 | // ======================================================================== 31 | // AZURE RESOURCES 32 | // ======================================================================== 33 | 34 | resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 35 | name: name 36 | location: 'global' 37 | tags: tags 38 | } 39 | 40 | resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = [ for vnet in virtualNetworkLinks: { 41 | parent: privateDnsZone 42 | name: '${vnet.vnetName}-link' 43 | location: 'global' 44 | properties: { 45 | registrationEnabled: vnet.registrationEnabled 46 | virtualNetwork: { 47 | id: vnet.vnetId 48 | } 49 | } 50 | }] 51 | 52 | // ======================================================================== 53 | // OUTPUTS 54 | // ======================================================================== 55 | 56 | output id string = privateDnsZone.id 57 | output name string = privateDnsZone.name 58 | 59 | -------------------------------------------------------------------------------- /infra/core/network/route-table.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** Route Table 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | */ 10 | 11 | // ======================================================================== 12 | // PARAMETERS 13 | // ======================================================================== 14 | 15 | @description('The Azure region for the resource.') 16 | param location string 17 | 18 | @description('The name of the primary resource') 19 | param name string 20 | 21 | @description('The tags to associate with this resource.') 22 | param tags object = {} 23 | 24 | /* 25 | ** Settings 26 | */ 27 | @description('Optional. Switch to disable BGP route propagation.') 28 | param disableBgpRoutePropagation bool = false 29 | 30 | @description('The list of routes to install in the route table') 31 | param routes object[] 32 | 33 | // ======================================================================== 34 | // AZURE RESOURCES 35 | // ======================================================================== 36 | 37 | resource routeTable 'Microsoft.Network/routeTables@2022-11-01' = { 38 | name: name 39 | location: location 40 | tags: tags 41 | properties: { 42 | routes: routes 43 | disableBgpRoutePropagation: disableBgpRoutePropagation 44 | } 45 | } 46 | 47 | // ======================================================================== 48 | // OUTPUTS 49 | // ======================================================================== 50 | 51 | output id string = routeTable.id 52 | output name string = routeTable.name 53 | -------------------------------------------------------------------------------- /infra/core/security/front-door-route-approval.bicep: -------------------------------------------------------------------------------- 1 | /* 2 | ** Azure Front Door Route Approval 3 | ** Copyright (C) 2023 Microsoft, Inc. 4 | ** All Rights Reserved 5 | ** 6 | *************************************************************************** 7 | */ 8 | 9 | // ===================================================================================================================== 10 | // PARAMETERS 11 | // ===================================================================================================================== 12 | 13 | @description('The Azure region used to host the deployment script') 14 | param location string 15 | 16 | @description('The owner managed identity used to auto-approve the private endpoint') 17 | param managedIdentityName string 18 | 19 | @description('Force the deployment script to run') 20 | param utcValue string = utcNow() 21 | 22 | // ===================================================================================================================== 23 | // AZURE RESOURCES 24 | // ===================================================================================================================== 25 | 26 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 27 | name: managedIdentityName 28 | } 29 | 30 | resource approval 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 31 | name: 'auto-approve-private-endpoint' 32 | location: location 33 | kind: 'AzureCLI' 34 | identity: { 35 | type: 'UserAssigned' 36 | userAssignedIdentities: { 37 | '${managedIdentity.id}': {} 38 | } 39 | } 40 | properties: { 41 | forceUpdateTag: utcValue 42 | azCliVersion: '2.47.0' 43 | timeout: 'PT30M' 44 | environmentVariables: [ 45 | { 46 | name: 'ResourceGroupName' 47 | value: resourceGroup().name 48 | } 49 | ] 50 | scriptContent: loadTextContent('./scripts/front-door-route-approval.sh') 51 | cleanupPreference: 'OnSuccess' // No need to keep around any background resources if succeeded 52 | retentionInterval: 'PT1H' 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /infra/core/security/key-vault-secrets.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* 4 | ** Write secrets to Key Vault 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | ** 10 | ** Writes a set of secrets to the connected Key Vault. 11 | */ 12 | 13 | // ======================================================================== 14 | // USER-DEFINED TYPES 15 | // ======================================================================== 16 | 17 | @description('The form of each Key Vault Secret to store.') 18 | @secure() 19 | type KeyVaultSecret = { 20 | @description('The key for the secret') 21 | key: string 22 | 23 | @description('The value of the secret') 24 | value: string 25 | } 26 | 27 | // ======================================================================== 28 | // PARAMETERS 29 | // ======================================================================== 30 | 31 | @description('The name of the Key Vault resource') 32 | param name string 33 | 34 | /* 35 | ** Settings 36 | */ 37 | @description('The list of secrets to store in the Key Vault') 38 | param secrets KeyVaultSecret[] 39 | 40 | // ======================================================================== 41 | // AZURE RESOURCES 42 | // ======================================================================== 43 | 44 | resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = { 45 | name: name 46 | } 47 | 48 | resource keyVaultSecretResources 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = [for secret in secrets: { 49 | name: secret.key 50 | parent: keyVault 51 | properties: { 52 | contentType: 'text/plain; charset=utf-8' 53 | value: secret.value 54 | } 55 | }] 56 | 57 | #disable-next-line outputs-should-not-contain-secrets // Doesn't contain a secret, just contains the ID references 58 | output secret_ids array = [for (secret, i) in secrets: keyVaultSecretResources[i].id] 59 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "clientIpAddress": { 6 | "value": "${AZD_IP_ADDRESS}" 7 | }, 8 | "differentiator": { 9 | "value": "${AZURE_CI_DIFFERENTIATOR=none}" 10 | }, 11 | "enableTelemetry": { 12 | "value": "${ENABLE_TELEMETRY=true}" 13 | }, 14 | "environmentName": { 15 | "value": "${AZURE_ENV_NAME}" 16 | }, 17 | "environmentType": { 18 | "value": "${ENVIRONMENT=dev}" 19 | }, 20 | "location": { 21 | "value": "${AZURE_LOCATION}" 22 | }, 23 | "principalId": { 24 | "value": "${AZURE_PRINCIPAL_ID}" 25 | }, 26 | "principalName": { 27 | "value": "${AZURE_PRINCIPAL_NAME=Developer}" 28 | }, 29 | "principalType": { 30 | "value": "${AZURE_PRINCIPAL_TYPE=User}" 31 | }, 32 | "deployHubNetwork": { 33 | "value": "${DEPLOY_HUB_NETWORK=auto}" 34 | }, 35 | "networkIsolation": { 36 | "value": "${NETWORK_ISOLATION=auto}" 37 | }, 38 | "ownerEmail": { 39 | "value": "${OWNER_EMAIL=noreply@contoso.com}" 40 | }, 41 | "ownerName": { 42 | "value": "${OWNER_NAME=notset}" 43 | }, 44 | "azureSecondaryLocation": { 45 | "value": "${AZURE_SECONDARY_LOCATION}" 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /infra/modules/azure-fqdns.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "azureMonitor": [ 3 | "dc.applicationinsights.azure.com", 4 | "dc.applicationinsights.microsoft.com", 5 | "dc.services.visualstudio.com", 6 | "*.in.applicationinsights.azure.com", 7 | "live.applicationinsights.azure.com", 8 | "rt.applicationinsights.microsoft.com", 9 | "rt.services.visualstudio.com", 10 | "*.livediagnostics.monitor.azure.com", 11 | "*.monitoring.azure.com", 12 | "agent.azureserviceprofiler.net", 13 | "*.agent.azureserviceprofiler.net", 14 | "*.monitor.azure.com" 15 | ], 16 | "certificateServices": [ 17 | "*.delivery.mp.microsoft.com", 18 | "ctldl.windowsupdate.com", 19 | "ocsp.msocsp.com", 20 | "oneocsp.microsoft.com", 21 | "crl.microsoft.com", 22 | "www.microsoft.com", 23 | "*.digicert.com", 24 | "*.symantec.com", 25 | "*.symcb.com", 26 | "*.d-trust.net" 27 | ], 28 | "coreServices": [ 29 | "management.azure.com", 30 | "management.core.windows.net", 31 | "login.microsoftonline.com", 32 | "login.windows.net", 33 | "login.live.com", 34 | "graph.windows.net" 35 | ], 36 | "developerServices": [ 37 | "github.com", 38 | "*.github.com", 39 | "*.nuget.org", 40 | "*.blob.core.windows.net", 41 | "raw.githubusercontent.com", 42 | "dev.azure.com", 43 | "portal.azure.com", 44 | "*.portal.azure.com", 45 | "*.portal.azure.net", 46 | "appservice.azureedge.net", 47 | "*.azurewebsites.net", 48 | "edge.management.azure.com", 49 | "*.azurefd.net" 50 | ], 51 | "managedIdentityServices": [ 52 | "*.identity.azure.net", 53 | "login.microsoftonline.com", 54 | "*.login.microsoftonline.com", 55 | "*.login.microsoft.com" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /infra/modules/telemetry.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | /* 4 | ** Enterprise App Patterns Telemetry 5 | ** Copyright (C) 2023 Microsoft, Inc. 6 | ** All Rights Reserved 7 | ** 8 | *************************************************************************** 9 | ** Review the enableTelemetry parameter to understand telemetry collection 10 | */ 11 | 12 | import { DeploymentSettings } from '../types/DeploymentSettings.bicep' 13 | 14 | // ======================================================================== 15 | // PARAMETERS 16 | // ======================================================================== 17 | 18 | @description('The deployment settings to use for this deployment.') 19 | param deploymentSettings DeploymentSettings 20 | 21 | // ======================================================================== 22 | // VARIABLES 23 | // ======================================================================== 24 | 25 | var telemetryId = '2e1b35cf-c556-45fd-87d5-bfc08ac2e8ba' 26 | 27 | // ======================================================================== 28 | // AZURE RESOURCES 29 | // ======================================================================== 30 | 31 | resource telemetrySubscription 'Microsoft.Resources/deployments@2021-04-01' = { 32 | name: '${telemetryId}-${deploymentSettings.location}' 33 | location: deploymentSettings.location 34 | properties: { 35 | mode: 'Incremental' 36 | template: { 37 | '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' 38 | contentVersion: '1.0.0.0' 39 | resources: {} 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /infra/scripts/postdeploy/show-webapp-uri.ps1: -------------------------------------------------------------------------------- 1 | 2 | # The AZD deploy command shows the links to the azurewebsites.net resources 3 | # We block access to these resources and instead want to show the Azure Front Door URL 4 | 5 | # Prompt formatting features 6 | 7 | $defaultColor = if ($Host.UI.SupportsVirtualTerminal) { "`e[0m" } else { "" } 8 | $highlightColor = if ($Host.UI.SupportsVirtualTerminal) { "`e[36m" } else { "" } 9 | 10 | # End of Prompt formatting features 11 | 12 | Write-Host "`nUse this URI to access the web app:" 13 | $azureFrontDoorUri=(azd env get-values --output json | ConvertFrom-Json).WEB_URI 14 | Write-Host "`t$($highlightColor)$azureFrontDoorUri$($defaultColor)" -------------------------------------------------------------------------------- /infra/scripts/postdeploy/show-webapp-uri.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The AZD deploy command shows the links to the azurewebsites.net resources 4 | # We block access to these resources and instead want to show the Azure Front Door URL 5 | 6 | pwsh ./infra/scripts/postdeploy/show-webapp-uri.ps1 -------------------------------------------------------------------------------- /infra/scripts/postprovision/call-create-app-registrations.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script will be run by the Azure Developer CLI, and will have access to the AZD_* vars 4 | This calls the create app registration.ps1 with the correct AZD provisioned resource group. 5 | 6 | .DESCRIPTION 7 | This script will be run by the Azure Developer CLI, and will set the required 8 | app configuration settings for the Relecloud web app as part of the code deployment process. 9 | 10 | Depends on the AZURE_RESOURCE_GROUP environment variable being set. AZD requires this to 11 | understand which resource group to deploy to so this script uses it to learn about the 12 | environment where the configuration settings should be set. 13 | 14 | #> 15 | 16 | # if this is CI/CD then we want to skip this step because the app registrations already exist 17 | $principalType = (azd env get-values --output json | ConvertFrom-Json).AZURE_PRINCIPAL_TYPE 18 | 19 | if ($principalType -eq "ServicePrincipal") { 20 | Write-Host "Skipping create-app-registrations.ps1 because principalType is ServicePrincipal" 21 | exit 0 22 | } 23 | 24 | $resourceGroupName=(azd env get-values --output json | ConvertFrom-Json).AZURE_RESOURCE_GROUP 25 | 26 | Write-Host "Calling create-app-registrations.ps1 for group:'$resourceGroupName'..." 27 | 28 | ./infra/scripts/postprovision/create-app-registrations.ps1 -ResourceGroupName $resourceGroupName -NoPrompt -------------------------------------------------------------------------------- /infra/scripts/postprovision/call-create-app-registrations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # if this is CI/CD then we want to skip this step because the app registrations already exist 4 | principalType=$((azd env get-values --output json) | jq -r .AZURE_PRINCIPAL_TYPE) 5 | 6 | if [ "$principalType" == "ServicePrincipal" ]; then 7 | echo "Skipping create-app-registrations.ps1 because principalType is ServicePrincipal" 8 | exit 0 9 | fi 10 | 11 | # This script is run by azd pre-provision hook and is part of the deployment lifecycle run when deploying the code for the Relecloud web app. 12 | resourceGroupName=$((azd env get-values --output json) | jq -r .AZURE_RESOURCE_GROUP) 13 | 14 | echo "Calling create-app-registrations.ps1 for group:'$resourceGroupName'..." 15 | 16 | pwsh ./infra/scripts/postprovision/create-app-registrations.ps1 -ResourceGroupName $resourceGroupName -NoPrompt -------------------------------------------------------------------------------- /infra/scripts/predeploy/call-set-app-configuration.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script will be run by the Azure Developer CLI, and will have access to the AZD_* vars 4 | This ensures the the app configuration service is reachable from the current environment. 5 | 6 | .DESCRIPTION 7 | This script will be run by the Azure Developer CLI, and will set the required 8 | app configuration settings for the Relecloud web app as part of the code deployment process. 9 | 10 | Depends on the AZURE_RESOURCE_GROUP environment variable being set. AZD requires this to 11 | understand which resource group to deploy to so this script uses it to learn about the 12 | environment where the configuration settings should be set. 13 | 14 | #> 15 | 16 | $resourceGroupName = ((azd env get-values --output json) | ConvertFrom-Json).AZURE_RESOURCE_GROUP 17 | $webUri = ((azd env get-values --output json) | ConvertFrom-Json).WEB_URI 18 | 19 | Write-Host "Calling set-app-configuration.ps1 for group:'$resourceGroupName' with webUri:'$webUri' ..." 20 | 21 | ./infra/scripts/predeploy/set-app-configuration.ps1 -ResourceGroupName $resourceGroupName -WebUri $webUri -NoPrompt -------------------------------------------------------------------------------- /infra/scripts/predeploy/call-set-app-configuration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is run by azd pre-provision hook and is part of the deployment lifecycle run when deploying the code for the Relecloud web app. 4 | resourceGroupName=$((azd env get-values --output json) | jq -r .AZURE_RESOURCE_GROUP) 5 | webUri=$((azd env get-values --output json) | jq -r .WEB_URI) 6 | 7 | echo "Calling set-app-configuration.ps1 for group:'$resourceGroupName' with webUri:'$webUri' ..." 8 | 9 | pwsh ./infra/scripts/predeploy/set-app-configuration.ps1 -ResourceGroupName $resourceGroupName -WebUri $webUri -NoPrompt -------------------------------------------------------------------------------- /infra/scripts/predown/call-cleanup.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script will be run by the Azure Developer CLI, and will have access to the AZD_* vars 4 | This calls the cleanup.ps1 script with the correct AZD resource group. 5 | 6 | .DESCRIPTION 7 | This script will be run by the Azure Developer CLI, and will remove resources 8 | that are not deleted as part of the `azd down` command such as the following: 9 | - App registrations 10 | - Azure budgets 11 | - Azure diagnostic settings 12 | 13 | Script also deletes private endpoints. 14 | 15 | Depends on the AZURE_RESOURCE_GROUP environment variable being set. AZD requires this to 16 | understand which resource group to deploy to so this script uses it to learn about the 17 | environment where the configuration settings should be set. 18 | 19 | #> 20 | 21 | $resourceGroupName=(azd env get-values --output json | ConvertFrom-Json).AZURE_RESOURCE_GROUP 22 | 23 | # if the resource group is not set, then exit 24 | if (-not $resourceGroupName) { 25 | Write-Host "AZURE_RESOURCE_GROUP not set..." 26 | exit 0 27 | } 28 | 29 | Write-Host "Calling cleanup.ps1 for group:'$resourceGroupName'..." 30 | 31 | ./testscripts/cleanup.ps1 -ResourceGroup $resourceGroupName -NoPrompt -Purge -------------------------------------------------------------------------------- /infra/scripts/predown/call-cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script will be run by the Azure Developer CLI, and will have access to the AZD_* vars 4 | # This calls the cleanup.ps1 script with the correct AZD resource group. 5 | 6 | # This script will be run by the Azure Developer CLI, and will remove resources 7 | # that are not deleted as part of the `azd down` command such as the following: 8 | # - App registrations 9 | # - Azure budgets 10 | # - Azure diagnostic settings 11 | # Script also deletes private endpoints. 12 | # Depends on the AZURE_RESOURCE_GROUP environment variable being set. AZD requires this to 13 | # understand which resource group to deploy to so this script uses it to learn about the 14 | # environment where the configuration settings should be set. 15 | 16 | resourceGroupName=$(azd env get-values --output json | jq -r '.AZURE_RESOURCE_GROUP') 17 | 18 | # if the resource group equals the string 'null', then exit 19 | if [ "$resourceGroupName" == "null" ]; then 20 | echo "AZURE_RESOURCE_GROUP not set..." 21 | exit 0 22 | fi 23 | 24 | 25 | echo "Calling cleanup.ps1 for group:'$resourceGroupName'..." 26 | 27 | pwsh ./testscripts/cleanup.ps1 -ResourceGroup "$resourceGroupName" -NoPrompt -Purge 28 | -------------------------------------------------------------------------------- /infra/scripts/preprovision/validate-params.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script validates the parameters for the deployment of the Azure DevOps environment. 4 | 5 | .DESCRIPTION 6 | The script retrieves the configuration values from Azure DevOps and validates the environment type and network isolation settings. 7 | It checks if the environment type is either 'dev' or 'prod' and if the network isolation is enabled for the 'prod' environment type. 8 | If any of the parameters are invalid, an error message is displayed and the script exits with a non-zero status code. 9 | 10 | .NOTES 11 | - This script requires the Azure CLI to be installed and logged in to Azure DevOps. 12 | - The configuration values are retrieved using the 'azd env get-values' command. 13 | 14 | .EXAMPLE 15 | ./validate-params.ps1 16 | 17 | This example runs the script to validate the parameters using the default configuration values. 18 | #> 19 | 20 | 21 | $azdConfig = azd env get-values -o json | ConvertFrom-Json -Depth 9 -AsHashtable 22 | 23 | $environmentType = $azdConfig['ENVIRONMENT'] ?? 'dev' 24 | 25 | # Block invalid deployment scenarios by helping the user understand the valid AZD options 26 | if ($environmentType -ne 'dev' -and $environmentType -ne 'prod') { 27 | Write-Error "Invalid AZD environment type: '$environmentType'. Valid values are 'dev' or 'prod'." 28 | exit 1 29 | } -------------------------------------------------------------------------------- /infra/scripts/preprovision/validate-params.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script validates the parameters for the deployment of the Azure DevOps environment. 4 | 5 | # The script retrieves the configuration values from Azure DevOps and validates the environment type and network isolation settings. 6 | # It checks if the environment type is either 'dev' or 'prod' and if the network isolation is enabled for the 'prod' environment type. 7 | # If any of the parameters are invalid, an error message is displayed and the script exits with a non-zero status code. 8 | 9 | # This script requires the Azure CLI to be installed and logged in to Azure DevOps. 10 | # The configuration values are retrieved using the 'azd env get-values' command. 11 | 12 | # Example usage: ./validate-params.sh 13 | 14 | environmentType=$(azd env get-values -o json | jq -r '.ENVIRONMENT') 15 | 16 | # default environmentType to dev if not set 17 | if [[ $environmentType == "null" ]]; then 18 | environmentType="dev" 19 | fi 20 | 21 | # Block invalid deployment scenarios by helping the user understand the valid AZD options 22 | if [[ $environmentType != "dev" && $environmentType != "prod" ]]; then 23 | echo "" 24 | echo " Invalid AZD environment type: '$environmentType'. Valid values are 'dev' or 'prod'." 25 | exit 1 26 | fi -------------------------------------------------------------------------------- /infra/scripts/preprovision/whats-my-ip.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script will be run by the Azure Developer CLI. 4 | 5 | Retrieves the public IP address of the current system, as seen by Azure. To do this, it 6 | uses ipinfo.io as an external service. Afterwards, it sets the AZD_IP_ADDRESS environment 7 | variable and sets the `azd env set` command to set it within Azure Developer CLI as well. 8 | #> 9 | 10 | $ipaddr = Invoke-RestMethod -Uri https://ipinfo.io/ip 11 | 12 | # if $ipaddress is empty, exit with error 13 | if ([string]::IsNullOrEmpty($ipaddr)) { 14 | Write-Error "Unable to retrieve public IP address" 15 | exit 1 16 | } 17 | 18 | $env:AZD_IP_ADDRESS = $ipaddr 19 | azd env set AZD_IP_ADDRESS $ipaddr -------------------------------------------------------------------------------- /infra/scripts/preprovision/whats-my-ip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script will be run by the Azure Developer CLI. 4 | # 5 | # Retrieves the public IP address of the current system, as seen by Azure. To do this, it 6 | # uses ipinfo.io as an external service. Afterwards, it sets the AZD_IP_ADDRESS environment 7 | # variable and sets the `azd env set` command to set it within Azure Developer CLI as well. 8 | 9 | echo '...make API call' 10 | ipaddress=`curl -s https://ipinfo.io/ip` 11 | 12 | # if $ipaddress is empty, exit with error 13 | if [ -z "$ipaddress" ]; then 14 | echo '...no IP address returned' 15 | exit 1 16 | fi 17 | 18 | echo '...export' 19 | export AZD_IP_ADDRESS=$ipaddress 20 | 21 | echo '...set value' 22 | azd env set AZD_IP_ADDRESS $ipaddress -------------------------------------------------------------------------------- /infra/types/ApplicationIdentity.bicep: -------------------------------------------------------------------------------- 1 | @export() 2 | @description('Type describing an application identity.') 3 | type ApplicationIdentity = { 4 | @description('The ID of the identity') 5 | principalId: string 6 | 7 | @description('The type of identity - either ServicePrincipal or User') 8 | principalType: 'ServicePrincipal' | 'User' 9 | } 10 | -------------------------------------------------------------------------------- /infra/types/BuildAgentSettings.bicep: -------------------------------------------------------------------------------- 1 | @export() 2 | @description('Describes the required settings for a Azure DevOps Pipeline runner') 3 | type AzureDevopsSettings = { 4 | @description('The URL of the Azure DevOps organization to use for this agent') 5 | organizationUrl: string 6 | 7 | @description('The Personal Access Token (PAT) to use for the Azure DevOps agent') 8 | token: string 9 | } 10 | 11 | @export() 12 | @description('Describes the required settings for a GitHub Actions runner') 13 | type GithubActionsSettings = { 14 | @description('The URL of the GitHub repository to use for this agent') 15 | repositoryUrl: string 16 | 17 | @description('The Personal Access Token (PAT) to use for the GitHub Actions runner') 18 | token: string 19 | } 20 | -------------------------------------------------------------------------------- /infra/types/DeploymentSettings.bicep: -------------------------------------------------------------------------------- 1 | @export() 2 | @description('Type that describes the global deployment settings') 3 | type DeploymentSettings = { 4 | @description('If \'true\', then two regional deployments will be performed.') 5 | isMultiLocationDeployment: bool 6 | 7 | @description('If \'true\', use production SKUs and settings.') 8 | isProduction: bool 9 | 10 | @description('If \'true\', isolate the workload in a virtual network.') 11 | isNetworkIsolated: bool 12 | 13 | @description('The Azure region to host resources') 14 | location: string 15 | 16 | @description('The Azure region to host primary resources. In a multi-region deployment, this will match \'location\' while deploying the primary region\'s resources.') 17 | primaryLocation: string 18 | 19 | @description('The secondary Azure region in a multi-region deployment. This will match \'location\' while deploying the secondary region\'s resources during a multi-region deployment.') 20 | secondaryLocation: string 21 | 22 | @description('The name of the workload.') 23 | name: string 24 | 25 | @description('The ID of the principal that is being used to deploy resources.') 26 | principalId: string 27 | 28 | @description('The name of the principal that is being used to deploy resources.') 29 | principalName: string 30 | 31 | @description('The type of the \'principalId\' property.') 32 | principalType: 'ServicePrincipal' | 'User' 33 | 34 | @description('The token to use for naming resources. This should be unique to the deployment.') 35 | resourceToken: string 36 | 37 | @description('The development stage for this application') 38 | stage: 'dev' | 'prod' 39 | 40 | @description('The common tags that should be used for all created resources') 41 | tags: object 42 | 43 | @description('The common tags that should be used for all workload resources') 44 | workloadTags: object 45 | } 46 | -------------------------------------------------------------------------------- /infra/types/DiagnosticSettings.bicep: -------------------------------------------------------------------------------- 1 | @export() 2 | @description('The diagnostic settings for a resource') 3 | type DiagnosticSettings = { 4 | @description('The number of days to retain log data.') 5 | logRetentionInDays: int 6 | 7 | @description('The number of days to retain metric data.') 8 | metricRetentionInDays: int 9 | 10 | @description('If true, enable diagnostic logging.') 11 | enableLogs: bool 12 | 13 | @description('If true, enable metrics logging.') 14 | enableMetrics: bool 15 | } 16 | -------------------------------------------------------------------------------- /infra/types/FrontDoorSettings.bicep: -------------------------------------------------------------------------------- 1 | @export() 2 | @description('Type describing the settings for Azure Front Door.') 3 | type FrontDoorSettings = { 4 | @description('The name of the Azure Front Door endpoint') 5 | endpointName: string 6 | 7 | @description('Front Door Id used for traffic restriction') 8 | frontDoorId: string 9 | 10 | @description('The hostname that can be used to access Azure Front Door content.') 11 | hostname: string 12 | 13 | @description('The profile name that is used for configuring Front Door routes.') 14 | profileName: string 15 | } 16 | -------------------------------------------------------------------------------- /infra/types/PrivateEndpointSettings.bicep: -------------------------------------------------------------------------------- 1 | @export() 2 | @description('Type describing the private endpoint settings.') 3 | type PrivateEndpointSettings = { 4 | @description('The name of the resource group to hold the Private DNS Zone. By default, this uses the same resource group as the resource.') 5 | dnsResourceGroupName: string 6 | 7 | @description('The name of the private endpoint resource.') 8 | name: string 9 | 10 | @description('The name of the resource group to hold the private endpoint.') 11 | resourceGroupName: string 12 | 13 | @description('The ID of the subnet to link the private endpoint to.') 14 | subnetId: string 15 | } 16 | -------------------------------------------------------------------------------- /infra/types/RedisUser.bicep: -------------------------------------------------------------------------------- 1 | @export() 2 | @description('Type describing the user for redis.') 3 | type RedisUser = { 4 | @description('The object id of the user.') 5 | objectId: string 6 | 7 | @description('The alias of the user') 8 | alias: string 9 | 10 | @description('Specify name of built-in access policy to use as assignment.') 11 | accessPolicy: 'Data Owner' | 'Data Contributor' | 'Data Reader' 12 | } 13 | -------------------------------------------------------------------------------- /infra/types/UserIdentity.bicep: -------------------------------------------------------------------------------- 1 | @export() 2 | @description('Type describing a user identity.') 3 | type UserIdentity = { 4 | @description('The ID of the user') 5 | principalId: string 6 | 7 | @description('The name of the user') 8 | principalName: string 9 | } 10 | -------------------------------------------------------------------------------- /known-issues.md: -------------------------------------------------------------------------------- 1 | # Known issues 2 | This document helps with troubleshooting and provides an introduction to the most requested features, gotchas, and questions. 3 | 4 | ## Data consistency for multi-regional deployments 5 | 6 | This sample includes a feature to deploy to two Azure regions. The feature is intended to support the high availability scenario by deploying resources in an active/passive configuration. The sample currently supports the ability to fail-over web-traffic so requests can be handled from a second region. However it does not support data synchronization between two regions. -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/IMessageBus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Messaging; 5 | 6 | public interface IMessageBus 7 | { 8 | IMessageSender CreateMessageSender(string path); 9 | 10 | Task SubscribeAsync( 11 | Func messageHandler, 12 | Func? errorHandler, 13 | string path, 14 | CancellationToken cancellationToken); 15 | } 16 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/IMessageProcessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Messaging; 5 | 6 | public interface IMessageProcessor : IAsyncDisposable 7 | { 8 | Task StopAsync(CancellationToken cancellationToken); 9 | } 10 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/IMessageSender.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Messaging; 5 | 6 | public interface IMessageSender : IAsyncDisposable 7 | { 8 | Task PublishAsync(T message, CancellationToken cancellationToken); 9 | 10 | Task CloseAsync(CancellationToken cancellationToken); 11 | } 12 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/MessageBusOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Relecloud.Messaging; 7 | 8 | public class MessageBusOptions 9 | { 10 | [Required] 11 | public string? Host { get; set; } 12 | 13 | [Required] 14 | public string? RenderRequestQueueName { get; set; } 15 | 16 | // This property is only required if messages should be generated 17 | // when ticket images are produced. 18 | public string? RenderCompleteQueueName { get; set; } 19 | 20 | public int MaxRetries { get; set; } = 3; 21 | 22 | public double BaseDelaySecondsBetweenRetries { get; set; } = 0.8; 23 | 24 | public double MaxDelaySeconds { get; set; } = 60; 25 | 26 | public double TryTimeoutSeconds { get; set; } = 60; 27 | } 28 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/Messages/TicketRenderCompleteMessage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Messaging.Messages; 5 | 6 | public record TicketRenderCompleteMessage(Guid MessageId, int TicketId, string OutputPath, DateTime CreationTime); 7 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/Messages/TicketRenderRequestMessage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | 6 | namespace Relecloud.Messaging.Messages; 7 | 8 | public record TicketRenderRequestMessage(Guid MessageId, Ticket Ticket, string? OutputPath, DateTime CreationTime); 9 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/Relecloud.Messaging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/ServiceBus/AzureServiceBusMessageBusExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Core; 5 | using Azure.Identity; 6 | using Azure.Messaging.ServiceBus; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Options; 9 | 10 | namespace Relecloud.Messaging.ServiceBus; 11 | 12 | public static class AzureServiceBusMessageBusExtensions 13 | { 14 | public static IServiceCollection AddMessageBusOptions(this IServiceCollection services, string configSectionPath) 15 | { 16 | services.AddOptions() 17 | .BindConfiguration(configSectionPath) 18 | .ValidateDataAnnotations() 19 | .ValidateOnStart(); 20 | 21 | return services; 22 | } 23 | 24 | public static IServiceCollection AddAzureServiceBusMessageBus(this IServiceCollection services, string configSectionPath, TokenCredential? azureCredential) 25 | { 26 | services.AddMessageBusOptions(configSectionPath); 27 | 28 | services.AddSingleton(); 29 | 30 | // ServiceBusClient is thread-safe and can be reused for the lifetime of the application. 31 | services.AddSingleton(sp => 32 | { 33 | var options = sp.GetRequiredService>().Value; 34 | var clientOptions = new ServiceBusClientOptions 35 | { 36 | RetryOptions = new ServiceBusRetryOptions 37 | { 38 | Mode = ServiceBusRetryMode.Exponential, 39 | MaxRetries = options.MaxRetries, 40 | Delay = TimeSpan.FromSeconds(options.BaseDelaySecondsBetweenRetries), 41 | MaxDelay = TimeSpan.FromSeconds(options.MaxDelaySeconds), 42 | TryTimeout = TimeSpan.FromSeconds(options.TryTimeoutSeconds) 43 | } 44 | }; 45 | 46 | return new ServiceBusClient(options.Host, azureCredential ?? new DefaultAzureCredential(), clientOptions); 47 | }); 48 | 49 | return services; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/ServiceBus/AzureServiceBusMessageProcessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Messaging.ServiceBus; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Relecloud.Messaging.ServiceBus; 8 | 9 | /// 10 | /// A disposable message processor for Azure Service Bus. 11 | /// 12 | internal class AzureServiceBusMessageProcessor(ILogger logger, ServiceBusProcessor processor) : IMessageProcessor 13 | { 14 | public async Task StopAsync(CancellationToken cancellationToken) 15 | { 16 | logger.LogDebug("Stopping message processor for {Namespace}/{Path}.", processor.FullyQualifiedNamespace, processor.EntityPath); 17 | await processor.StopProcessingAsync(cancellationToken); 18 | } 19 | 20 | public async ValueTask DisposeAsync() 21 | { 22 | await processor.DisposeAsync(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Relecloud.Messaging/ServiceBus/AzureServiceBusMessageSender.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Messaging.ServiceBus; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Relecloud.Messaging.ServiceBus; 8 | 9 | /// 10 | /// A message sender for publishing messages to specific Azure Service Bus queues or topics. 11 | /// 12 | internal sealed class AzureServiceBusMessageSender(ILogger> logger, ServiceBusSender sender) : IMessageSender 13 | { 14 | public async Task PublishAsync(T message, CancellationToken cancellationToken) 15 | { 16 | logger.LogDebug("Sending message to {Path}.", sender.EntityPath); 17 | 18 | // Automatically serialize the message body to JSON using System.Text.Json 19 | var sbMessage = new ServiceBusMessage(new BinaryData(message)); 20 | await sender.SendMessageAsync(sbMessage, cancellationToken); 21 | } 22 | 23 | public async Task CloseAsync(CancellationToken cancellationToken) 24 | { 25 | await sender.CloseAsync(cancellationToken); 26 | } 27 | 28 | public async ValueTask DisposeAsync() 29 | { 30 | await sender.DisposeAsync(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/Concert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.ConcertContext 5 | { 6 | public class Concert 7 | { 8 | public int Id { get; set; } 9 | public bool IsVisible { get; set; } 10 | public string Artist { get; set; } = string.Empty; 11 | public string Genre { get; set; } = string.Empty; 12 | public string Location { get; set; } = string.Empty; 13 | public string Title { get; set; } = string.Empty; 14 | public string Description { get; set; } = string.Empty; 15 | public double Price { get; set; } 16 | public DateTimeOffset StartTime { get; set; } = DateTimeOffset.UtcNow.AddDays(30); 17 | public DateTimeOffset CreatedOn { get; set; } 18 | public string CreatedBy { get; set; } = string.Empty; 19 | public DateTimeOffset UpdatedOn { get; set; } 20 | public string UpdatedBy { get; set; } = string.Empty; 21 | 22 | /// 23 | /// Required when the selected Ticket Management Service is not ReleCloud Api 24 | /// 25 | public string? TicketManagementServiceConcertId { get; set; } = string.Empty; 26 | 27 | 28 | /// 29 | /// This is a calculated column that does not exist in the DB 30 | /// 31 | public int? NumberOfTicketsForSale { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/CreateResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.ConcertContext 5 | { 6 | public class CreateResult : UpdateResult 7 | { 8 | public int NewId { get; set; } 9 | 10 | public static CreateResult SuccessResult(int id) 11 | { 12 | return new CreateResult 13 | { 14 | Success = true, 15 | NewId = id, 16 | }; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/Customer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Relecloud.Models.ConcertContext 7 | { 8 | public class Customer 9 | { 10 | public int Id { get; set; } 11 | 12 | [MaxLength(75)] 13 | public string Name { get; set; } = string.Empty; 14 | 15 | [MaxLength(75)] 16 | public string Email { get; set; } = string.Empty; 17 | 18 | [MaxLength(16)] 19 | public string Phone { get; set; } = string.Empty; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/DeleteResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.ConcertContext 5 | { 6 | public class DeleteResult : UpdateResult 7 | { 8 | public static new DeleteResult SuccessResult() 9 | { 10 | return new DeleteResult 11 | { 12 | Success = true, 13 | }; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/PagedResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.ConcertContext 5 | { 6 | public class PagedResult where T : new() 7 | { 8 | public PagedResult(ICollection pageOfData, int totalCount) 9 | { 10 | PageOfData = pageOfData; 11 | TotalCount = totalCount; 12 | } 13 | 14 | public int TotalCount { get; set; } 15 | public ICollection PageOfData { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/Ticket.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.ConcertContext 5 | { 6 | public class Ticket 7 | { 8 | public int Id { get; set; } 9 | public string ImageName { get; set; } = string.Empty; 10 | 11 | public int ConcertId { get; set; } 12 | public Concert? Concert { get; set; } 13 | 14 | public string UserId { get; set; } = string.Empty; 15 | public User? User { get; set; } 16 | 17 | public int CustomerId { get; set; } 18 | public Customer? Customer { get; set; } 19 | 20 | public string TicketNumber { get; set; } = string.Empty; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/TicketNumber.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.ConcertContext 5 | { 6 | public class TicketNumber 7 | { 8 | public int Id { get; set; } 9 | public string Number { get; set; } = string.Empty; 10 | 11 | public int? TicketId { get; set; } 12 | 13 | public Ticket? Ticket { get; set; } 14 | 15 | public int ConcertId { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/UpdateResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.ConcertContext 5 | { 6 | public class UpdateResult 7 | { 8 | public bool Success { get; set; } 9 | public IDictionary>? ErrorMessages { get; set; } 10 | 11 | public static UpdateResult SuccessResult() 12 | { 13 | return new UpdateResult { Success = true }; 14 | } 15 | 16 | public static IDictionary> CreateError(string errorMessage) 17 | { 18 | var errors = new Dictionary>(); 19 | errors[string.Empty] = new List 20 | { 21 | errorMessage 22 | }; 23 | 24 | return errors; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Relecloud.Models/ConcertContext/User.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.ConcertContext 5 | { 6 | public class User 7 | { 8 | public string Id { get; set; } = new Guid().ToString(); 9 | public string DisplayName { get; set; } = string.Empty; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Relecloud.Models/Relecloud.Models.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | Relecloud.Models 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Relecloud.Models/Search/ConcertSearchResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.Search 5 | { 6 | public class ConcertSearchResult 7 | { 8 | public string Id { get; set; } = string.Empty; 9 | public bool IsVisible { get; set; } 10 | public string Artist { get; set; } = string.Empty; 11 | public string Genre { get; set; } = string.Empty; 12 | public string Location { get; set; } = string.Empty; 13 | public string Title { get; set; } = string.Empty; 14 | public string Description { get; set; } = string.Empty; 15 | public double Price { get; set; } 16 | public DateTimeOffset StartTime { get; set; } 17 | public DateTimeOffset CreatedOn { get; set; } 18 | public string CreatedBy { get; set; } = string.Empty; 19 | public DateTimeOffset UpdatedOn { get; set; } 20 | public string UpdatedBy { get; set; } = string.Empty; 21 | 22 | public double Score { get; set; } 23 | public IList HitHighlights { get; set; } = new List(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Relecloud.Models/Search/SearchFacet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.Search 5 | { 6 | public class SearchFacet 7 | { 8 | public string FieldName { get; set; } 9 | public string DisplayName { get; set; } 10 | public IList Values { get; set; } 11 | 12 | public SearchFacet(string fieldName, string displayName, IList values) 13 | { 14 | this.FieldName = fieldName; 15 | this.DisplayName = displayName; 16 | this.Values = values ?? new SearchFacetValue[0]; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Relecloud.Models/Search/SearchFacetValue.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.Search 5 | { 6 | public class SearchFacetValue 7 | { 8 | public string Value { get; set; } 9 | public string DisplayName { get; set; } 10 | public long Count { get; set; } 11 | 12 | public SearchFacetValue(string value, string displayName, long count) 13 | { 14 | this.Value = value; 15 | this.DisplayName = displayName; 16 | this.Count = count; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Relecloud.Models/Search/SearchRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.Search 5 | { 6 | public class SearchRequest 7 | { 8 | public const int PageSize = 5; 9 | 10 | public int CurrentPage { get; set; } 11 | 12 | public string Query { get; set; } = string.Empty; 13 | public string SortOn { get; set; } = string.Empty; 14 | public bool SortDescending { get; set; } 15 | public string PriceRange { get; set; } = string.Empty; 16 | public string Genre { get; set; } = string.Empty; 17 | public string Location { get; set; } = string.Empty; 18 | 19 | public SearchRequest Clone() 20 | { 21 | return new SearchRequest 22 | { 23 | Query = this.Query, 24 | SortOn = this.SortOn, 25 | SortDescending = this.SortDescending, 26 | PriceRange = this.PriceRange, 27 | Genre = this.Genre, 28 | Location = this.Location 29 | }; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Relecloud.Models/Search/SearchResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.Search 5 | { 6 | public class SearchResponse 7 | { 8 | public long TotalCount { get; set; } 9 | public SearchRequest Request { get; set; } 10 | public ICollection Results { get; set; } 11 | public ICollection Facets { get; set; } 12 | 13 | public SearchResponse(SearchRequest request, ICollection results, ICollection facets) 14 | { 15 | this.Request = request; 16 | this.Results = results ?? new T[0]; 17 | this.Facets = facets ?? new SearchFacet[0]; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Relecloud.Models/Services/IConcertContextService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | 6 | namespace Relecloud.Models.Services 7 | { 8 | public interface IConcertContextService 9 | { 10 | Task CreateConcertAsync(Concert newConcert); 11 | Task UpdateConcertAsync(Concert model); 12 | Task DeleteConcertAsync(int id); 13 | Task GetConcertByIdAsync(int id); 14 | Task> GetConcertsByIdAsync(ICollection ids); 15 | Task> GetUpcomingConcertsAsync(int count); 16 | 17 | Task GetTicketByIdAsync(int id); 18 | Task> GetAllTicketsAsync(string userId, int skip, int take); 19 | 20 | Task CreateOrUpdateUserAsync(User user); 21 | Task GetUserByIdAsync(string id); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Relecloud.Models/Services/IConcertSearchService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.Search; 5 | 6 | namespace Relecloud.Models.Services 7 | { 8 | public interface IConcertSearchService 9 | { 10 | Task> SearchAsync(SearchRequest request); 11 | Task> SuggestAsync(string query); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Relecloud.Models/Services/IServiceProviderResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.Services 5 | { 6 | public interface IServiceProviderResult 7 | { 8 | public string ErrorMessage { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Relecloud.Models/TicketManagement/CountAvailableTicketsResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.Services; 5 | 6 | namespace Relecloud.Models.TicketManagement 7 | { 8 | public class CountAvailableTicketsResult : IServiceProviderResult 9 | { 10 | public int CountOfAvailableTickets { get; set; } 11 | public string ErrorMessage { get; set; } = string.Empty; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Relecloud.Models/TicketManagement/HaveTicketsBeenSoldResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.Services; 5 | 6 | namespace Relecloud.Models.TicketManagement 7 | { 8 | public class HaveTicketsBeenSoldResult : IServiceProviderResult 9 | { 10 | public bool HaveTicketsBeenSold { get; set; } 11 | public string ErrorMessage { get; set; } = string.Empty; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Relecloud.Models/TicketManagement/Payment/CardTypes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.TicketManagement.Payment 5 | { 6 | public enum CardTypes 7 | { 8 | VISA 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Relecloud.Models/TicketManagement/PurchaseTicketsRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.TicketManagement.Payment; 5 | 6 | namespace Relecloud.Models.TicketManagement 7 | { 8 | public class PurchaseTicketsRequest 9 | { 10 | public string? UserId { get; set; } 11 | public PaymentDetails? PaymentDetails { get; set; } 12 | public IDictionary? ConcertIdsAndTicketCounts { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relecloud.Models/TicketManagement/PurchaseTicketsResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.TicketManagement 5 | { 6 | public class PurchaseTicketsResult 7 | { 8 | public PurchaseTicketsResultStatus Status { get; set; } 9 | 10 | public IDictionary>? ErrorMessages { get; set; } 11 | 12 | public static PurchaseTicketsResult ErrorResponse(string errorMessage) 13 | { 14 | return ErrorResponse(new List { errorMessage }); 15 | } 16 | 17 | public static PurchaseTicketsResult ErrorResponse(IEnumerable errorMessages) 18 | { 19 | var errors = new Dictionary>(); 20 | errors[string.Empty] = errorMessages; 21 | 22 | return new PurchaseTicketsResult 23 | { 24 | Status = PurchaseTicketsResultStatus.UnableToProcess, 25 | ErrorMessages = errors, 26 | }; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Relecloud.Models/TicketManagement/PurchaseTicketsResultStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.TicketManagement 5 | { 6 | public enum PurchaseTicketsResultStatus 7 | { 8 | UnableToProcess, 9 | NotEnoughTicketsRemaining, 10 | Success, 11 | ConcertNotFound 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Relecloud.Models/TicketManagement/ReserveTicketsResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.TicketManagement 5 | { 6 | public class ReserveTicketsResult 7 | { 8 | public ICollection TicketNumbers { get; set; } = new List(); 9 | 10 | public ReserveTicketsResultStatus Status { get; set; } 11 | 12 | public string ErrorMessage { get; set; } = string.Empty; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relecloud.Models/TicketManagement/ReserveTicketsResultStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Models.TicketManagement 5 | { 6 | public enum ReserveTicketsResultStatus 7 | { 8 | NotEnoughTicketsRemaining, 9 | Success, 10 | ConcertNotFound 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Dockerfile: -------------------------------------------------------------------------------- 1 | ## Build Stage 2 | # Build in a separate stage to avoid copying the SDK into the final image 3 | FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build 4 | ARG BUILD_CONFIGURATION=Release 5 | WORKDIR /src 6 | 7 | # Restore packages 8 | COPY ["Relecloud.TicketRenderer/Relecloud.TicketRenderer.csproj", "Relecloud.TicketRenderer/"] 9 | COPY ["Relecloud.Messaging/Relecloud.Messaging.csproj", "Relecloud.Messaging/"] 10 | COPY ["Relecloud.Models/Relecloud.Models.csproj", "Relecloud.Models/"] 11 | RUN dotnet restore "./Relecloud.TicketRenderer/Relecloud.TicketRenderer.csproj" 12 | 13 | # Build and publish 14 | COPY . . 15 | WORKDIR "/src/Relecloud.TicketRenderer" 16 | RUN dotnet publish "./Relecloud.TicketRenderer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false 17 | 18 | ## Runtime Stage 19 | # Chiseled images contain only the minimal set of packages needed for .NET 8.0 20 | FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled AS final 21 | WORKDIR /app 22 | EXPOSE 8080 23 | 24 | # Copy the published app from the build stage 25 | COPY --from=build /app/publish . 26 | 27 | # Run as non-root user 28 | USER $APP_UID 29 | 30 | ENTRYPOINT ["dotnet", "./Relecloud.TicketRenderer.dll"] 31 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Fonts/Open_Sans/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/src/Relecloud.TicketRenderer/Fonts/Open_Sans/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Fonts/Open_Sans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/src/Relecloud.TicketRenderer/Fonts/Open_Sans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Models/AzureStorageOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Relecloud.TicketRenderer.Models; 7 | 8 | internal class AzureStorageOptions 9 | { 10 | [Required] 11 | public string? Uri { get; set; } 12 | 13 | [Required] 14 | public string? Container { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Models/ResilienceOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.TicketRenderer.Models; 5 | 6 | /// 7 | /// Options for retry policies. This is shared between 8 | /// HTTP (storage) requests and Service Bus requests. 9 | /// If there were a need for different retry behaviors, 10 | /// they could be handled separately. 11 | /// 12 | internal class ResilienceOptions 13 | { 14 | public int MaxRetries { get; set; } = 5; 15 | public double BaseDelaySecondsBetweenRetries { get; set; } = 0.8; 16 | public double MaxDelaySeconds { get; set; } = 60; 17 | public double MaxNetworkTimeoutSeconds { get; set; } = 90; 18 | } 19 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Relecloud.TicketRenderer": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DOTNET_ENVIRONMENT": "Development" 7 | }, 8 | "dotnetRunMessages": true 9 | }, 10 | "Container (Dockerfile)": { 11 | "commandName": "Docker", 12 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 13 | "environmentVariables": { 14 | "ASPNETCORE_HTTP_PORTS": "8080" 15 | }, 16 | "DockerfileRunArguments": "-p 5050:8080" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Services/AzureImageStorage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Storage.Blobs; 5 | using Microsoft.Extensions.Options; 6 | using Relecloud.TicketRenderer.Models; 7 | 8 | namespace Relecloud.TicketRenderer.Services; 9 | 10 | /// 11 | /// Stores images in Azure Blob Storage. 12 | /// 13 | internal class AzureImageStorage(ILogger logger, BlobServiceClient blobServiceClient, IOptionsMonitor options) : IImageStorage 14 | { 15 | public async Task StoreImageAsync(Stream image, string path, CancellationToken cancellationToken) 16 | { 17 | var blobClient = blobServiceClient.GetBlobContainerClient(options.CurrentValue.Container).GetBlobClient(path); 18 | var response = await blobClient.UploadAsync(image, overwrite: true, cancellationToken); 19 | 20 | if (response.GetRawResponse().IsError) 21 | { 22 | logger.LogError("Error storing image {BlobName} in Azure Blob Storage: {StatusCode}", path, response.GetRawResponse().Status); 23 | return false; 24 | } 25 | { 26 | logger.LogInformation("Successfully stored image {BlobName} in Azure Blob Storage", path); 27 | return true; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Services/IBarcodeGenerator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | 6 | namespace Relecloud.TicketRenderer.Services; 7 | 8 | public interface IBarcodeGenerator 9 | { 10 | IEnumerable GenerateBarcode(Ticket ticket); 11 | } 12 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Services/IImageStorage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.TicketRenderer.Services; 5 | 6 | public interface IImageStorage 7 | { 8 | Task StoreImageAsync(Stream image, string path, CancellationToken cancellation); 9 | } 10 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Services/ITicketRenderer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Messaging.Messages; 5 | 6 | namespace Relecloud.TicketRenderer.Services; 7 | 8 | public interface ITicketRenderer 9 | { 10 | /// 11 | /// Renders a ticket and returns the path to the rendered image. 12 | /// 13 | /// A message definition describing the ticket to render. 14 | /// The path to the rendered ticket in storage or null if no ticket could be rendered. 15 | Task RenderTicketAsync(TicketRenderRequestMessage request, CancellationToken cancellation); 16 | } 17 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/Services/RandomBarcodeGenerator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace Relecloud.TicketRenderer.Services; 8 | 9 | [ExcludeFromCodeCoverage] 10 | public class RandomBarcodeGenerator(int width, int? seed = null) : IBarcodeGenerator 11 | { 12 | private readonly Random random = seed is null 13 | ? new Random() 14 | : new Random(seed.Value); 15 | 16 | public IEnumerable GenerateBarcode(Ticket ticket) 17 | { 18 | var currentWidth = 0; 19 | while (currentWidth < width) 20 | { 21 | var nextWidth = random.Next(2, 5); 22 | currentWidth += nextWidth; 23 | yield return nextWidth; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Relecloud.TicketRenderer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Controllers/ImageController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.IdentityModel.Tokens; 7 | using Relecloud.Web.Api.Services.TicketManagementService; 8 | 9 | namespace Relecloud.Web.Controllers 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | public class ImageController : ControllerBase 14 | { 15 | private ITicketImageService ticketImageService; 16 | private ILogger logger; 17 | 18 | public ImageController(ITicketImageService ticketImageService, ILogger logger) 19 | { 20 | this.ticketImageService = ticketImageService; 21 | this.logger = logger; 22 | } 23 | 24 | [HttpGet("{imageName}")] 25 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(byte[]))] 26 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 27 | [Authorize] 28 | public async Task GetTicketImage(string imageName) 29 | { 30 | try 31 | { 32 | if (imageName.IsNullOrEmpty()) 33 | { 34 | return BadRequest(); 35 | } 36 | 37 | var imageStream = await this.ticketImageService.GetTicketImagesAsync(imageName); 38 | return File(imageStream, "application/octet-stream"); 39 | } 40 | catch (Exception ex) 41 | { 42 | logger.LogError(ex, $"Unable to retrive image {imageName}"); 43 | return Problem("Unable to get the image"); 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/FeatureFlags.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api; 5 | 6 | public static class FeatureFlags 7 | { 8 | public const string DistributedTicketRendering = "DistributedTicketRendering"; 9 | } 10 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Infrastructure/ApplicationInitializer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Web.Api.Services; 5 | 6 | namespace Relecloud.Web.Api.Infrastructure 7 | { 8 | public class ApplicationInitializer 9 | { 10 | private readonly IConcertRepository concertContextService; 11 | 12 | public ApplicationInitializer(IConcertRepository concertContextService) 13 | { 14 | this.concertContextService = concertContextService; 15 | } 16 | 17 | public void Initialize() 18 | { 19 | // Initialize all resources at application startup. 20 | concertContextService.Initialize(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Infrastructure/CacheKeys.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api.Infrastructure 5 | { 6 | public static class CacheKeys 7 | { 8 | public const string UpcomingConcerts = "UpcomingConcerts"; 9 | public const string ConcertIdServiceMap = "ConcertIdServiceMap"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Infrastructure/ModelStateDictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc.ModelBinding; 5 | 6 | namespace Relecloud.Web.Api.Infrastructure 7 | { 8 | public static class ModelStateDictionaryExtensions 9 | { 10 | public static IDictionary> ServerError(this ModelStateDictionary modelStateDictionary, string customErrorMessage) 11 | { 12 | var errors = new Dictionary>(); 13 | errors.Add(string.Empty, new List { customErrorMessage }); 14 | return errors; 15 | } 16 | 17 | public static IDictionary> ConvertToErrorMessages(this ModelStateDictionary modelStateDictionary) 18 | { 19 | var errorMessages = new Dictionary>(); 20 | if (modelStateDictionary == null) 21 | { 22 | return errorMessages; 23 | } 24 | 25 | foreach (var error in modelStateDictionary) 26 | { 27 | var errorMessageList = error.Value.Errors.Where(err => !string.IsNullOrEmpty(err.ToString())).Select(err => err.ToString()); 28 | if (errorMessageList != null) 29 | { 30 | errorMessages.Add(error.Key, errorMessageList!); 31 | } 32 | } 33 | 34 | return errorMessages; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Infrastructure/Roles.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api.Infrastructure 5 | { 6 | public static class Roles 7 | { 8 | public const string Administrator = "Administrator"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Migrations/20220125000051_AddVisibleFields.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | 6 | #nullable disable 7 | 8 | namespace Relecloud.Web.Api.Migrations 9 | { 10 | public partial class AddVisibleFields : Migration 11 | { 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Concerts", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "int", nullable: false) 19 | .Annotation("SqlServer:Identity", "1, 1"), 20 | IsVisible = table.Column(type: "bit", nullable: false), 21 | Artist = table.Column(type: "nvarchar(max)", nullable: false), 22 | Genre = table.Column(type: "nvarchar(max)", nullable: false), 23 | Location = table.Column(type: "nvarchar(max)", nullable: false), 24 | Title = table.Column(type: "nvarchar(max)", nullable: false), 25 | Description = table.Column(type: "nvarchar(max)", nullable: false), 26 | Price = table.Column(type: "float", nullable: false), 27 | StartTime = table.Column(type: "datetimeoffset", nullable: false) 28 | }, 29 | constraints: table => 30 | { 31 | table.PrimaryKey("PK_Concerts", x => x.Id); 32 | }); 33 | } 34 | 35 | protected override void Down(MigrationBuilder migrationBuilder) 36 | { 37 | migrationBuilder.DropTable( 38 | name: "Concerts"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Migrations/20220208203826_CreateTicketNumbers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | 6 | #nullable disable 7 | 8 | namespace Relecloud.Web.Api.Migrations 9 | { 10 | public partial class CreateTicketNumbers : Migration 11 | { 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "TicketNumbers", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "int", nullable: false) 19 | .Annotation("SqlServer:Identity", "1, 1"), 20 | Number = table.Column(type: "nvarchar(450)", nullable: false), 21 | TicketId = table.Column(type: "int", nullable: true), 22 | ConcertId = table.Column(type: "int", nullable: false) 23 | }, 24 | constraints: table => 25 | { 26 | table.PrimaryKey("PK_TicketNumbers", x => x.Id); 27 | }); 28 | 29 | migrationBuilder.CreateIndex( 30 | name: "IX_TicketNumbers_Number_ConcertId", 31 | table: "TicketNumbers", 32 | columns: new[] { "Number", "ConcertId" }, 33 | unique: true); 34 | } 35 | 36 | protected override void Down(MigrationBuilder migrationBuilder) 37 | { 38 | migrationBuilder.DropTable( 39 | name: "TicketNumbers"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Migrations/20220208231619_SelectTicketManagementService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | 6 | #nullable disable 7 | 8 | namespace Relecloud.Web.Api.Migrations 9 | { 10 | public partial class SelectTicketManagementService : Migration 11 | { 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AddColumn( 15 | name: "TicketManagementServiceConcertId", 16 | table: "Concerts", 17 | type: "nvarchar(max)", 18 | nullable: false, 19 | defaultValue: ""); 20 | 21 | migrationBuilder.AddColumn( 22 | name: "TicketManagementServiceProvider", 23 | table: "Concerts", 24 | type: "int", 25 | nullable: false, 26 | defaultValue: 0); 27 | } 28 | 29 | protected override void Down(MigrationBuilder migrationBuilder) 30 | { 31 | migrationBuilder.DropColumn( 32 | name: "TicketManagementServiceConcertId", 33 | table: "Concerts"); 34 | 35 | migrationBuilder.DropColumn( 36 | name: "TicketManagementServiceProvider", 37 | table: "Concerts"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Migrations/20220209201351_TicketServiceConcertIdIsNullable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | 6 | #nullable disable 7 | 8 | namespace Relecloud.Web.Api.Migrations 9 | { 10 | public partial class TicketServiceConcertIdIsNullable : Migration 11 | { 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AlterColumn( 15 | name: "TicketManagementServiceConcertId", 16 | table: "Concerts", 17 | type: "nvarchar(max)", 18 | nullable: true, 19 | oldClrType: typeof(string), 20 | oldType: "nvarchar(max)"); 21 | } 22 | 23 | protected override void Down(MigrationBuilder migrationBuilder) 24 | { 25 | migrationBuilder.AlterColumn( 26 | name: "TicketManagementServiceConcertId", 27 | table: "Concerts", 28 | type: "nvarchar(max)", 29 | nullable: false, 30 | defaultValue: "", 31 | oldClrType: typeof(string), 32 | oldType: "nvarchar(max)", 33 | oldNullable: true); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Migrations/20220215010613_AddTicketNumberToTicket.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | 6 | #nullable disable 7 | 8 | namespace Relecloud.Web.Api.Migrations 9 | { 10 | public partial class AddTicketNumberToTicket : Migration 11 | { 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AddColumn( 15 | name: "TicketNumber", 16 | table: "Tickets", 17 | type: "nvarchar(max)", 18 | nullable: false, 19 | defaultValue: ""); 20 | 21 | migrationBuilder.CreateIndex( 22 | name: "IX_TicketNumbers_TicketId", 23 | table: "TicketNumbers", 24 | column: "TicketId"); 25 | 26 | migrationBuilder.AddForeignKey( 27 | name: "FK_TicketNumbers_Tickets_TicketId", 28 | table: "TicketNumbers", 29 | column: "TicketId", 30 | principalTable: "Tickets", 31 | principalColumn: "Id"); 32 | } 33 | 34 | protected override void Down(MigrationBuilder migrationBuilder) 35 | { 36 | migrationBuilder.DropForeignKey( 37 | name: "FK_TicketNumbers_Tickets_TicketId", 38 | table: "TicketNumbers"); 39 | 40 | migrationBuilder.DropIndex( 41 | name: "IX_TicketNumbers_TicketId", 42 | table: "TicketNumbers"); 43 | 44 | migrationBuilder.DropColumn( 45 | name: "TicketNumber", 46 | table: "Tickets"); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:53073", 8 | "sslPort": 44379 9 | } 10 | }, 11 | "profiles": { 12 | "Relecloud.Web.Api": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7242;http://localhost:5242", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/IConcertRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | using Relecloud.Models.Services; 6 | 7 | namespace Relecloud.Web.Api.Services 8 | { 9 | public interface IConcertRepository : IConcertContextService 10 | { 11 | public void Initialize(); 12 | Task CreateCustomerAsync(Customer newCustomer); 13 | Task GetCustomerByEmailAsync(string email); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/IPaymentGatewayService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Web.Api.Services.PaymentGatewayService; 5 | 6 | namespace Relecloud.Web.Models.Services 7 | { 8 | public interface IPaymentGatewayService 9 | { 10 | Task PreAuthPaymentAsync(PreAuthPaymentRequest request); 11 | Task CapturePaymentAsync(CapturePaymentRequest request); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/MockServices/MockConcertSearchService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.Search; 5 | using Relecloud.Models.Services; 6 | 7 | namespace Relecloud.Web.Api.Services.MockServices 8 | { 9 | public class MockConcertSearchService : IConcertSearchService 10 | { 11 | public Task> SearchAsync(SearchRequest request) 12 | { 13 | return Task.FromResult(new SearchResponse(request, Array.Empty(), Array.Empty())); 14 | } 15 | 16 | public Task> SuggestAsync(string query) 17 | { 18 | return Task.FromResult>(Array.Empty()); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/MockServices/MockPaymentGatewayService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Web.Api.Services.PaymentGatewayService; 5 | using Relecloud.Web.Models.Services; 6 | 7 | namespace Relecloud.Web.Api.Services.MockServices 8 | { 9 | public class MockPaymentGatewayService : IPaymentGatewayService 10 | { 11 | public Task CapturePaymentAsync(CapturePaymentRequest request) 12 | { 13 | return Task.FromResult(new CapturePaymentResult 14 | { 15 | ConfirmationNumber = Guid.NewGuid().ToString(), 16 | Status = CapturePaymentResultStatus.CaptureSuccessful 17 | }); 18 | } 19 | 20 | public Task PreAuthPaymentAsync(PreAuthPaymentRequest request) 21 | { 22 | return Task.FromResult(new PreAuthPaymentResult 23 | { 24 | HoldCode = Guid.NewGuid().ToString(), 25 | Status = PreAuthPaymentResultStatus.FundsOnHold 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/MockServices/MockTicketImageService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Web.Api.Services.TicketManagementService; 5 | 6 | namespace Relecloud.Web.Api.Services.MockServices 7 | { 8 | public class MockTicketImageService : ITicketImageService 9 | { 10 | public Task GetTicketImagesAsync(string imageName) 11 | { 12 | return Task.FromResult(Stream.Null); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/MockServices/MockTicketManagementService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.TicketManagement; 5 | using Relecloud.Web.Api.Services.TicketManagementService; 6 | 7 | namespace Relecloud.Web.Api.Services.MockServices 8 | { 9 | public class MockTicketManagementService : ITicketManagementService 10 | { 11 | public Task CountAvailableTicketsAsync(int concertId) 12 | { 13 | return Task.FromResult(new CountAvailableTicketsResult 14 | { 15 | CountOfAvailableTickets = 100, 16 | }); 17 | } 18 | 19 | public Task HaveTicketsBeenSoldAsync(int concertId) 20 | { 21 | return Task.FromResult(new HaveTicketsBeenSoldResult 22 | { 23 | HaveTicketsBeenSold = true, 24 | }); 25 | } 26 | 27 | public Task ReserveTicketsAsync(int concertId, string userId, int numberOfTickets, int customerId) 28 | { 29 | return Task.FromResult(new ReserveTicketsResult 30 | { 31 | Status = ReserveTicketsResultStatus.Success, 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/MockServices/MockTicketRenderingService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Web.Api.Services.TicketManagementService; 5 | 6 | namespace Relecloud.Web.Api.Services.MockServices 7 | { 8 | public class MockTicketRenderingService : ITicketRenderingService 9 | { 10 | public Task CreateTicketImageAsync(int ticketId) 11 | { 12 | return Task.CompletedTask; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/PaymentGatewayService/CapturePaymentRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api.Services.PaymentGatewayService 5 | { 6 | public class CapturePaymentRequest 7 | { 8 | public string HoldCode { get; set; } = string.Empty; 9 | public double TotalPrice { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/PaymentGatewayService/CapturePaymentResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.Services; 5 | 6 | namespace Relecloud.Web.Api.Services.PaymentGatewayService 7 | { 8 | public class CapturePaymentResult : IServiceProviderResult 9 | { 10 | public CapturePaymentResultStatus Status { get; set; } 11 | public string ConfirmationNumber { get; set; } = string.Empty; 12 | public string ErrorMessage { get; set; } = string.Empty; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/PaymentGatewayService/CapturePaymentResultStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api.Services.PaymentGatewayService 5 | { 6 | public enum CapturePaymentResultStatus 7 | { 8 | CaptureSuccessful = 0, 9 | InvalidHoldCode = 1, 10 | InvalidHoldAmount = 2, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/PaymentGatewayService/PreAuthPaymentRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.TicketManagement.Payment; 5 | 6 | namespace Relecloud.Web.Api.Services.PaymentGatewayService 7 | { 8 | public class PreAuthPaymentRequest 9 | { 10 | public double Amount { get; set; } 11 | public PaymentDetails? PaymentDetails { get; set; } 12 | public IDictionary? Tickets { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/PaymentGatewayService/PreAuthPaymentResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.Services; 5 | 6 | namespace Relecloud.Web.Api.Services.PaymentGatewayService 7 | { 8 | public partial class PreAuthPaymentResult : IServiceProviderResult 9 | { 10 | public string HoldCode { get; set; } = string.Empty; 11 | public PreAuthPaymentResultStatus Status { get; set; } 12 | public string ErrorMessage { get; set; } = string.Empty; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/PaymentGatewayService/PreAuthPaymentResultStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api.Services.PaymentGatewayService 5 | { 6 | public enum PreAuthPaymentResultStatus 7 | { 8 | FundsOnHold = 0, 9 | InsufficientFunds = 1, 10 | NotAValidCard = 3 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/TicketManagementService/FeatureDependentTicketRenderingServiceFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | 5 | using Microsoft.FeatureManagement; 6 | 7 | namespace Relecloud.Web.Api.Services.TicketManagementService 8 | { 9 | /// 10 | /// A ticket rendering service factory that creates 11 | /// instances based on feature flags. This factory type is used rather than registering 12 | /// a factory method with DI because checking feature flags is an async operation. 13 | /// 14 | public class FeatureDependentTicketRenderingServiceFactory : ITicketRenderingServiceFactory 15 | { 16 | private readonly IFeatureManager featureManager; 17 | private readonly IServiceProvider serviceProvider; 18 | 19 | public FeatureDependentTicketRenderingServiceFactory(IFeatureManager featureManager, IServiceProvider serviceProvider) 20 | { 21 | this.featureManager = featureManager; 22 | this.serviceProvider = serviceProvider; 23 | } 24 | 25 | /// 26 | /// Returns an instance based on feature flags. 27 | /// If "DistributedTicketRendering" is enabled, a 28 | /// is created, otherwise a is created. 29 | /// 30 | /// A new instance of 31 | /// or depending on the state of the 32 | /// DistributedTicketRendering feature flag. 33 | public async Task CreateAsync() => 34 | (await featureManager.IsEnabledAsync(FeatureFlags.DistributedTicketRendering)) 35 | ? serviceProvider.GetRequiredService() 36 | : serviceProvider.GetRequiredService(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/TicketManagementService/ITicketImageService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api.Services.TicketManagementService 5 | { 6 | public interface ITicketImageService 7 | { 8 | Task GetTicketImagesAsync(string imageName); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/TicketManagementService/ITicketManagementService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.TicketManagement; 5 | 6 | namespace Relecloud.Web.Api.Services.TicketManagementService 7 | { 8 | public interface ITicketManagementService 9 | { 10 | Task CountAvailableTicketsAsync(int concertId); 11 | Task HaveTicketsBeenSoldAsync(int concertId); 12 | Task ReserveTicketsAsync(int concertId, string userId, int numberOfTickets, int customerId); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/TicketManagementService/ITicketRenderingService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api.Services.TicketManagementService 5 | { 6 | public interface ITicketRenderingService 7 | { 8 | public Task CreateTicketImageAsync(int ticketId); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/TicketManagementService/ITicketRenderingServiceFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.Api.Services.TicketManagementService 5 | { 6 | // Reading a feature flag is an asynchronous operation, so it's not possible 7 | // to register an ITicketRenderingService provider method directly. Instead, 8 | // use a factory pattern to create the service asynchronously. 9 | 10 | /// 11 | /// Interface for generating instances based 12 | /// on current configuration. 13 | /// 14 | public interface ITicketRenderingServiceFactory 15 | { 16 | /// 17 | /// Creates a new instance. 18 | /// 19 | Task CreateAsync(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/Services/TicketManagementService/TicketImageService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Storage.Blobs; 5 | 6 | namespace Relecloud.Web.Api.Services.TicketManagementService 7 | { 8 | public class TicketImageService : ITicketImageService 9 | { 10 | private readonly ILogger logger; 11 | private readonly BlobContainerClient blobContainerClient; 12 | 13 | public TicketImageService(IConfiguration configuration, BlobServiceClient blobServiceClient, ILogger logger) 14 | { 15 | this.logger = logger; 16 | 17 | // It is best practice to create Azure SDK clients once and reuse them. 18 | // https://learn.microsoft.com/azure/storage/blobs/storage-blob-client-management#manage-client-objects 19 | // https://devblogs.microsoft.com/azure-sdk/lifetime-management-and-thread-safety-guarantees-of-azure-sdk-net-clients/ 20 | this.blobContainerClient = blobServiceClient.GetBlobContainerClient(configuration["App:StorageAccount:Container"]); 21 | } 22 | 23 | public Task GetTicketImagesAsync(string imageName) 24 | { 25 | try 26 | { 27 | this.logger.LogInformation("Retrieving image {ImageName} from blob storage container {ContainerName}.", imageName, blobContainerClient.Name); 28 | var blobClient = blobContainerClient.GetBlobClient(imageName); 29 | 30 | return blobClient.OpenReadAsync(); 31 | } 32 | catch (Exception ex) 33 | { 34 | this.logger.LogError(ex, "Unable to retrieve image {ImageName} from blob storage container {ContainerName}", imageName, blobContainerClient.Name); 35 | return Task.FromResult(Stream.Null); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "Api:MicrosoftEntraId": { 9 | "Instance": "https://login.microsoftonline.com/", 10 | "ClientId": "", 11 | "TenantId": "" 12 | }, 13 | "FeatureManagement": { 14 | "DistributedTicketRendering": true 15 | }, 16 | "AllowedHosts": "*" 17 | } 18 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Relecloud.Web.CallCenter.Controllers 7 | { 8 | public class HomeController : Controller 9 | { 10 | private readonly IConfiguration _config; 11 | 12 | public HomeController(IConfiguration config) 13 | { 14 | _config = config ?? throw new ArgumentNullException(nameof(config)); 15 | } 16 | 17 | public IActionResult Index() 18 | { 19 | return View(); 20 | } 21 | 22 | public IActionResult Error(string message) 23 | { 24 | ViewBag.Message = message; 25 | return View(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Controllers/ImageController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Relecloud.Web.CallCenter.Services; 7 | 8 | namespace Relecloud.Web.CallCenter.Controllers; 9 | 10 | [Route("webapi/[controller]")] 11 | [ApiController] 12 | public class ImageController : ControllerBase 13 | { 14 | private ITicketImageService ticketImageService; 15 | private ILogger logger; 16 | 17 | public ImageController(ITicketImageService ticketImageService, ILogger logger) 18 | { 19 | this.ticketImageService = ticketImageService; 20 | this.logger = logger; 21 | } 22 | 23 | [HttpGet("{imageName}")] 24 | [Authorize] 25 | public async Task GetTicketImage(string imageName) 26 | { 27 | try 28 | { 29 | var imageStream = await this.ticketImageService.GetTicketImagesAsync(imageName); 30 | 31 | return File(imageStream, "application/octet-stream"); 32 | } 33 | catch (Exception ex) 34 | { 35 | logger.LogError(ex, $"Unable to retrive image: {imageName}"); 36 | return Problem("Unable to get the image"); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Controllers/TicketController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Relecloud.Models.ConcertContext; 7 | using Relecloud.Models.Services; 8 | using Relecloud.Web.CallCenter.Infrastructure; 9 | using Relecloud.Web.CallCenter.ViewModels; 10 | 11 | namespace Relecloud.Web.CallCenter.Controllers 12 | { 13 | [Authorize] 14 | public class TicketController : Controller 15 | { 16 | #region Fields 17 | 18 | private readonly ILogger logger; 19 | private readonly IConcertContextService concertService; 20 | 21 | #endregion 22 | 23 | #region Constructors 24 | 25 | public TicketController(IConcertContextService concertService, ILogger logger) 26 | { 27 | this.concertService = concertService; 28 | this.logger = logger; 29 | } 30 | 31 | #endregion 32 | 33 | #region Index 34 | 35 | public async Task Index(int currentPage) 36 | { 37 | try 38 | { 39 | var userId = this.User.GetUniqueId(); 40 | var pagedResultModel = await this.concertService.GetAllTicketsAsync(userId, currentPage * TicketViewModel.DefaultPageSize, TicketViewModel.DefaultPageSize); 41 | 42 | return View(new TicketViewModel 43 | { 44 | CurrentPage = currentPage, 45 | TotalCount = pagedResultModel?.TotalCount ?? 0, 46 | Tickets = pagedResultModel?.PageOfData ?? new List() 47 | }); 48 | } 49 | catch (Exception ex) 50 | { 51 | logger.LogError(ex, "Unable to retrieve upcoming concerts"); 52 | return View(); 53 | } 54 | } 55 | 56 | #endregion 57 | } 58 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Infrastructure/CacheKeys.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.CallCenter.Infrastructure 5 | { 6 | public static class CacheKeys 7 | { 8 | public const string UpcomingConcerts = "UpcomingConcerts"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Infrastructure/RelecloudApiConfiguration.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json; 5 | 6 | namespace Relecloud.Web.CallCenter.Infrastructure 7 | { 8 | public class RelecloudApiConfiguration 9 | { 10 | public static JsonSerializerOptions GetSerializerOptions() 11 | { 12 | return new JsonSerializerOptions 13 | { 14 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 15 | }; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Infrastructure/Roles.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.CallCenter.Infrastructure 5 | { 6 | public static class Roles 7 | { 8 | public const string Administrator = "Administrator"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:49930", 7 | "sslPort": 44375 8 | } 9 | }, 10 | "profiles": { 11 | "Relecloud": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:7227;http://localhost:5227", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Services/ITicketImageService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.CallCenter.Services; 5 | 6 | public interface ITicketImageService 7 | { 8 | Task GetTicketImagesAsync(string imageName); 9 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Services/ITicketPurchaseService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.TicketManagement; 5 | 6 | namespace Relecloud.Web.CallCenter.Services 7 | { 8 | public interface ITicketPurchaseService 9 | { 10 | Task PurchaseTicketAsync(PurchaseTicketsRequest request); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Services/MockServices/MockConcertContextService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | using Relecloud.Models.Services; 6 | 7 | namespace Relecloud.Web.CallCenter.Services.MockServices 8 | { 9 | public class MockConcertContextService : IConcertContextService 10 | { 11 | public Task GetConcertByIdAsync(int id) 12 | { 13 | return Task.FromResult(null); 14 | } 15 | 16 | public Task> GetConcertsByIdAsync(ICollection ids) 17 | { 18 | return Task.FromResult>(Array.Empty()); 19 | } 20 | 21 | public Task> GetUpcomingConcertsAsync(int count) 22 | { 23 | return Task.FromResult>(Array.Empty()); 24 | } 25 | 26 | public Task CreateOrUpdateUserAsync(User user) 27 | { 28 | return Task.FromResult(new UpdateResult()); 29 | } 30 | 31 | public Task CreateConcertAsync(Concert newConcert) 32 | { 33 | return Task.FromResult(new CreateResult()); 34 | } 35 | 36 | public Task UpdateConcertAsync(Concert model) 37 | { 38 | return Task.FromResult(new UpdateResult()); 39 | } 40 | 41 | public Task DeleteConcertAsync(int id) 42 | { 43 | return Task.FromResult(new DeleteResult()); 44 | } 45 | 46 | public Task> GetAllTicketsAsync(string userId, int skip, int take) 47 | { 48 | return Task.FromResult(new PagedResult(new List(), 0)); 49 | } 50 | 51 | public Task GetTicketByIdAsync(int id) 52 | { 53 | return Task.FromResult(null); 54 | } 55 | 56 | public Task GetUserByIdAsync(string id) 57 | { 58 | return Task.FromResult(null); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Services/MockServices/MockConcertSearchService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.Search; 5 | using Relecloud.Models.Services; 6 | 7 | namespace Relecloud.Web.CallCenter.Services.MockServices 8 | { 9 | public class MockConcertSearchService : IConcertSearchService 10 | { 11 | public void Initialize() 12 | { 13 | } 14 | 15 | public Task> SearchAsync(SearchRequest request) 16 | { 17 | return Task.FromResult(new SearchResponse(request, Array.Empty(), Array.Empty())); 18 | } 19 | 20 | public Task> SuggestAsync(string query) 21 | { 22 | return Task.FromResult>(Array.Empty()); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Services/MockServices/MockTicketImageService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.CallCenter.Services.MockServices; 5 | 6 | public class MockTicketImageService : ITicketImageService 7 | { 8 | public Task GetTicketImagesAsync(string imageName) 9 | { 10 | return Task.FromResult(new MemoryStream() as Stream); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Services/MockServices/MockTicketPurchaseService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.TicketManagement; 5 | 6 | namespace Relecloud.Web.CallCenter.Services.MockServices 7 | { 8 | public class MockTicketPurchaseService : ITicketPurchaseService 9 | { 10 | public Task PurchaseTicketAsync(PurchaseTicketsRequest request) 11 | { 12 | return Task.FromResult(new PurchaseTicketsResult 13 | { 14 | Status = PurchaseTicketsResultStatus.UnableToProcess 15 | }); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Services/RelecloudApiServices/RelecloudApiOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.Web.CallCenter.Services.RelecloudApiServices 5 | { 6 | public class RelecloudApiOptions 7 | { 8 | public string? BaseUri { get; set; } 9 | public string? AttendeeScope { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Services/RelecloudApiServices/RelecloudApiTicketImageService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.Extensions.Options; 5 | using Microsoft.Identity.Web; 6 | using System.Net.Http.Headers; 7 | 8 | namespace Relecloud.Web.CallCenter.Services.RelecloudApiServices; 9 | 10 | public class RelecloudApiTicketImageService : ITicketImageService 11 | { 12 | private readonly HttpClient httpClient; 13 | private readonly IHttpContextAccessor httpContextAccessor; 14 | private readonly ITokenAcquisition tokenAcquisition; 15 | private readonly IOptions options; 16 | 17 | public RelecloudApiTicketImageService(IHttpContextAccessor httpContextAccessor, HttpClient httpClient, ITokenAcquisition tokenAcquisition, IOptions options) 18 | { 19 | this.httpContextAccessor = httpContextAccessor; 20 | this.httpClient = httpClient; 21 | this.tokenAcquisition = tokenAcquisition; 22 | this.options = options; 23 | } 24 | 25 | public async Task GetTicketImagesAsync(string imageName) 26 | { 27 | await PrepareAuthenticatedClient(); 28 | var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, $"api/Image/{imageName}"); 29 | var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); 30 | 31 | 32 | var responseMessage = await httpResponseMessage.Content.ReadAsStreamAsync(); 33 | 34 | return responseMessage; 35 | } 36 | 37 | 38 | private async Task PrepareAuthenticatedClient() 39 | { 40 | if (httpContextAccessor.HttpContext?.User?.Identity != null) 41 | { 42 | var scopes = new[] { options.Value.AttendeeScope ?? throw new ArgumentNullException(nameof(options.Value.AttendeeScope)) }; 43 | var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes); 44 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 45 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/ViewModels/CartViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | 6 | namespace Relecloud.Web.CallCenter.ViewModels 7 | { 8 | public class CartViewModel 9 | { 10 | public IDictionary Concerts { get; } 11 | public int TotalTickets { get; } 12 | public double TotalPrice { get; } 13 | 14 | public CartViewModel(IDictionary concerts) 15 | { 16 | this.Concerts = concerts; 17 | this.TotalTickets = this.Concerts.Sum(item => item.Value); 18 | this.TotalPrice = this.Concerts.Sum(item => item.Key.Price * item.Value); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/ViewModels/CheckoutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc.Rendering; 5 | using Relecloud.Models.TicketManagement.Payment; 6 | using System.ComponentModel.DataAnnotations; 7 | 8 | namespace Relecloud.Web.CallCenter.ViewModels 9 | { 10 | public class CheckoutViewModel 11 | { 12 | public CartViewModel? Cart { get; set; } 13 | 14 | [Required] 15 | public PaymentDetails? PaymentDetails { get; set; } 16 | 17 | public List GetCardTypeList() 18 | { 19 | return Enum.GetValues().Select(c => new SelectListItem(c.ToString(), c.ToString())).ToList(); 20 | } 21 | 22 | public List GetSampleSecurityCodes() 23 | { 24 | return new List 25 | { 26 | new SelectListItem("Valid Security Code","123"), 27 | new SelectListItem("Invalid Security Code","124"), 28 | }; 29 | } 30 | 31 | public List GetSampleCardNumbers() 32 | { 33 | return new List 34 | { 35 | new SelectListItem("Visa - Succeeds", "4242424242424242"), 36 | new SelectListItem("Visa - Insufficient Funds", "4000000000009995"), 37 | }; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/ViewModels/ConcertViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | 6 | namespace Relecloud.Web.CallCenter.ViewModels 7 | { 8 | public class ConcertViewModel 9 | { 10 | public Concert? Concert { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/ViewModels/TicketViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Relecloud.Models.ConcertContext; 5 | 6 | namespace Relecloud.Web.CallCenter.ViewModels 7 | { 8 | public class TicketViewModel 9 | { 10 | public const int DefaultPageSize = 5; 11 | 12 | public int TotalCount { get; set; } 13 | public int CurrentPage { get; set; } 14 | 15 | public ICollection Tickets { get; set; } = new List(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/Cart/Add.cshtml: -------------------------------------------------------------------------------- 1 | @model Relecloud.Models.ConcertContext.Concert 2 | @{ 3 | ViewData["Title"] = "Add To Cart"; 4 | } 5 |

@ViewData["Title"]

6 | 7 | @if(Model is null) 8 | { 9 |
Oops! Something went wrong. Please try again.
10 | return; 11 | } 12 | 13 |
14 |
15 | 28 | ticket(s) for @Model.Artist on @Model.StartTime.UtcDateTime.ToString() 29 |
30 |
31 | 32 | 33 |
34 |
-------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/Cart/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model Relecloud.Web.CallCenter.ViewModels.CartViewModel 2 | @{ 3 | ViewData["Title"] = "Your Cart"; 4 | } 5 |

@ViewData["Title"]

6 | @if (Model == null || !Model.Concerts.Any()) 7 | { 8 |
You haven't added any concerts to your cart yet, why don't you browse around the upcoming concerts?
9 | } 10 | else 11 | { 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @foreach (var concert in Model.Concerts.Keys) 22 | { 23 | 24 | 32 | 35 | 38 | 41 | 42 | } 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
ConcertPriceTicketsTotal
25 |
26 | 29 | @concert.Artist on @concert.StartTime.UtcDateTime.ToString() 30 |
31 |
33 | @concert.Price.ToString("c") 34 | 36 | @Model.Concerts[concert] 37 | 39 | @((concert.Price * Model.Concerts[concert]).ToString("c")) 40 |
@Model.TotalTickets@Model.TotalPrice.ToString("c")
52 | 53 | Checkout » 54 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/Concert/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model IList 2 | 3 | @{ 4 | ViewData["Title"] = "Upcoming Concerts"; 5 | } 6 | 7 | @if (User.IsInRole(Roles.Administrator)) 8 | { 9 | Create 10 | } 11 |

@ViewData["Title"]

12 | @if (Model == null || !Model.Any()) 13 | { 14 |
There are no upcoming concerts.
15 | } 16 | else 17 | { 18 |
19 | @foreach (var concert in Model) 20 | { 21 |
22 |

23 | @concert.Artist — @concert.Title 24 | @concert.Price.ToString("c") 25 |

26 |
@concert.Genre @concert.StartTime.UtcDateTime.ToString() » @concert.Location
27 |
28 | } 29 |
30 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/Concert/Search.cshtml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | 6 | namespace Relecloud.Web.Views.Concert 7 | { 8 | public class SearchModel : PageModel 9 | { 10 | public void OnGet() 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | ViewData["Title"] = "Home"; 4 | } 5 | 6 |
7 |
8 |

Relecloud Concerts

9 |

» Find a local concert for customers

10 |

» Securely purchase tickets

11 |

» Deliver world-class concert experience!

12 |
13 |
-------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | @if (ViewBag.Message != null) 8 | { 9 |
@ViewBag.Message
10 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/Shared/_Layout.cshtml.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | a { 11 | color: #0077cc; 12 | } 13 | 14 | .btn-primary { 15 | color: #fff; 16 | background-color: #1b6ec2; 17 | border-color: #1861ac; 18 | } 19 | 20 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link { 21 | color: #fff; 22 | background-color: #1b6ec2; 23 | border-color: #1861ac; 24 | } 25 | 26 | .border-top { 27 | border-top: 1px solid #e5e5e5; 28 | } 29 | .border-bottom { 30 | border-bottom: 1px solid #e5e5e5; 31 | } 32 | 33 | .box-shadow { 34 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 35 | } 36 | 37 | button.accept-policy { 38 | font-size: 1rem; 39 | line-height: inherit; 40 | } 41 | 42 | .footer { 43 | width: 100%; 44 | white-space: nowrap; 45 | line-height: 60px; 46 | } 47 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Relecloud.Web 2 | @using Relecloud.Models.ConcertContext 3 | @using Relecloud.Web.CallCenter.Infrastructure 4 | @using Relecloud.Web.CallCenter.ViewModels 5 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "MicrosoftEntraId": { 9 | "Instance": "https://login.microsoftonline.com/", 10 | "ClientId": "", 11 | "TenantId": "", 12 | "ClientSecret": "", 13 | "CallbackPath": "/signin-oidc", 14 | "SignedOutCallbackPath": "/signout-oidc" 15 | }, 16 | "GraphBeta": { 17 | "BaseUrl": "https://graph.microsoft.com/beta", 18 | "Scopes": "user.read" 19 | }, 20 | "App": { 21 | "RelecloudApi": { 22 | "BaseUri": "https://localhost:7242", 23 | "AttendeeScope": "" 24 | }, 25 | "SqlDatabase": { 26 | "ConnectionString": "" 27 | }, 28 | "RedisCache": { 29 | "ConnectionString": "" 30 | }, 31 | "StorageAccount": { 32 | "ConnectionString": "", 33 | "EventQueueName": "" 34 | }, 35 | "AzureSearch": { 36 | "ServiceName": "", 37 | "AdminKey": "" 38 | } 39 | }, 40 | "AllowedHosts": "*" 41 | } -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/src/Relecloud.Web.CallCenter/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/wwwroot/img/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/src/Relecloud.Web.CallCenter/wwwroot/img/banner.jpg -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your JavaScript code. 5 | $(document).ready(function () { 6 | $("#query").autocomplete({ 7 | source: function (request, response) { 8 | $.ajax({ 9 | type: "GET", 10 | url: "/concert/suggest", 11 | contentType: "application/json", 12 | data: { 13 | query: request.term 14 | } 15 | }) 16 | .done(function (data) { 17 | response(data); 18 | }); 19 | }, 20 | minLength: 3 21 | }); 22 | }); -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2021 Twitter, Inc. 4 | Copyright (c) 2011-2021 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /src/Relecloud.Web.CallCenter/wwwroot/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /tests/Relecloud.Messaging.Tests/AzureServiceBusMessageProcessorTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Messaging.ServiceBus; 5 | 6 | namespace Relecloud.TicketRenderer.Tests; 7 | 8 | public class AzureServiceBusMessageProcessorTests 9 | { 10 | [Fact] 11 | public async Task StopAsync_ShouldStopProcessor() 12 | { 13 | // Arrange 14 | var logger = Substitute.For>(); 15 | var processor = Substitute.For(); 16 | var ct = new CancellationToken(); 17 | var messageProcessor = new AzureServiceBusMessageProcessor(logger, processor); 18 | 19 | // Act 20 | await messageProcessor.StopAsync(ct); 21 | 22 | // Assert 23 | await processor.Received(1).StopProcessingAsync(ct); 24 | } 25 | 26 | [Fact] 27 | public async Task DisposeAsync_ShouldDisposeProcessor() 28 | { 29 | // Arrange 30 | var logger = Substitute.For>(); 31 | var processor = Substitute.For(); 32 | var messageProcessor = new AzureServiceBusMessageProcessor(logger, processor); 33 | 34 | // Act 35 | await messageProcessor.DisposeAsync(); 36 | 37 | // Assert 38 | // Can't intercept DisposeAsync() calls, so verify that the processor is closed instead 39 | // (which should happen as part of disposing it) 40 | await processor.Received(1).CloseAsync(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Relecloud.Messaging.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Microsoft.Extensions.Logging; 5 | global using NSubstitute; 6 | global using Relecloud.Messaging; 7 | global using Relecloud.Messaging.Messages; 8 | global using Relecloud.Messaging.ServiceBus; 9 | global using Relecloud.TicketRenderer.TestHelpers; 10 | global using Xunit; 11 | -------------------------------------------------------------------------------- /tests/Relecloud.Messaging.Tests/MessageBusOptionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Relecloud.TicketRenderer.Tests; 7 | 8 | public class MessageBusOptionsTests 9 | { 10 | [Fact] 11 | public void NamespaceIsRequired() 12 | { 13 | var options = new MessageBusOptions 14 | { 15 | RenderRequestQueueName = "TestRequestQueue", 16 | RenderCompleteQueueName = "TestReponseQueue" 17 | }; 18 | 19 | var context = new ValidationContext(options); 20 | var results = new List(); 21 | var isValid = Validator.TryValidateObject(options, context, results, true); 22 | 23 | Assert.False(isValid); 24 | Assert.Contains(results, r => r.MemberNames.Contains("Host")); 25 | } 26 | 27 | [Fact] 28 | public void RenderRequestQueueNameIsRequired() 29 | { 30 | var options = new MessageBusOptions 31 | { 32 | Host = "TestNamespace", 33 | RenderCompleteQueueName = "TestReponseQueue" 34 | }; 35 | 36 | var context = new ValidationContext(options); 37 | var results = new List(); 38 | var isValid = Validator.TryValidateObject(options, context, results, true); 39 | 40 | Assert.False(isValid); 41 | Assert.Contains(results, r => r.MemberNames.Contains("RenderRequestQueueName")); 42 | } 43 | 44 | [Fact] 45 | public void RenderedTicketQueueNameIsNotRequired() 46 | { 47 | var options = new MessageBusOptions 48 | { 49 | Host = "TestNamespace", 50 | RenderRequestQueueName = "TestRequestQueue" 51 | }; 52 | 53 | var context = new ValidationContext(options); 54 | var results = new List(); 55 | var isValid = Validator.TryValidateObject(options, context, results, true); 56 | 57 | Assert.True(isValid); 58 | Assert.Null(options.RenderCompleteQueueName); 59 | Assert.DoesNotContain(results, r => r.MemberNames.Contains("RenderedTicketQueueName")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Relecloud.Messaging.Tests/Relecloud.Messaging.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/ExpectedImages/test-ticket-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/tests/Relecloud.TestHelpers/ExpectedImages/test-ticket-linux.png -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/ExpectedImages/test-ticket-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/modern-web-app-pattern-dotnet/2c81915b2b5524f40d172608dfe42cf6dd6c7f1a/tests/Relecloud.TestHelpers/ExpectedImages/test-ticket-windows.png -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/Relecloud.TestHelpers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/RelecloudTestHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Reflection; 5 | using System.Runtime.InteropServices; 6 | using Xunit; 7 | 8 | namespace Relecloud.TicketRenderer.TestHelpers; 9 | 10 | public class RelecloudTestHelpers 11 | { 12 | public static Stream GetTestImageStream() 13 | { 14 | var resourceName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 15 | ? "Relecloud.TestHelpers.ExpectedImages.test-ticket-windows.png" 16 | : "Relecloud.TestHelpers.ExpectedImages.test-ticket-linux.png"; 17 | 18 | return Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName) 19 | ?? throw new InvalidOperationException($"Could not find embedded resource: {resourceName}."); 20 | } 21 | 22 | public static bool AssertStreamsEquivalent(Stream expected, Stream actual, string? debugOutFile = null) 23 | { 24 | var equivalent = true; 25 | 26 | if (expected.Length != actual.Length) 27 | { 28 | equivalent = false; 29 | } 30 | 31 | if (equivalent) 32 | { 33 | var expectedByte = expected.ReadByte(); 34 | var actualByte = actual.ReadByte(); 35 | while (expectedByte != -1 && actualByte != -1) 36 | { 37 | if (expectedByte != actualByte) 38 | { 39 | equivalent = false; 40 | } 41 | 42 | expectedByte = expected.ReadByte(); 43 | actualByte = actual.ReadByte(); 44 | } 45 | } 46 | 47 | if (!equivalent) 48 | { 49 | if (!string.IsNullOrEmpty(debugOutFile)) 50 | { 51 | actual.Position = 0; 52 | using var actualImage = File.Create(debugOutFile); 53 | actual.CopyTo(actualImage); 54 | actualImage.Flush(); 55 | } 56 | Assert.Fail($"The actual stream contents do not match the expected stream.{(string.IsNullOrEmpty(debugOutFile) ? string.Empty : " See " + debugOutFile)})"); 57 | } 58 | 59 | return equivalent; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/TestAzureResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure; 5 | using Azure.Core; 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | namespace Relecloud.TicketRenderer.TestHelpers 9 | { 10 | internal class TestAzureResponse(int status) : Response 11 | { 12 | public override int Status => status; 13 | 14 | public override bool IsError => status >= 400; 15 | 16 | public override string ReasonPhrase => string.Empty; 17 | 18 | public override Stream? ContentStream { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 19 | public override string ClientRequestId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 20 | 21 | public override void Dispose() { } 22 | 23 | protected override bool ContainsHeader(string name) => false; 24 | 25 | protected override IEnumerable EnumerateHeaders() => []; 26 | 27 | protected override bool TryGetHeader(string name, [NotNullWhen(true)] out string? value) 28 | { 29 | value = null; 30 | return false; 31 | } 32 | 33 | protected override bool TryGetHeaderValues(string name, [NotNullWhen(true)] out IEnumerable? values) 34 | { 35 | values = null; 36 | return false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/TestBlobClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure; 5 | using Azure.Storage.Blobs; 6 | using Azure.Storage.Blobs.Models; 7 | 8 | namespace Relecloud.TicketRenderer.TestHelpers; 9 | 10 | public class TestBlobClient : BlobClient 11 | { 12 | public IList Uploads { get; } = []; 13 | 14 | public override Task> UploadAsync(Stream content, bool overwrite = false, CancellationToken cancellationToken = default) 15 | { 16 | // Store the content as bytes rather than a stream since the stream will be disposed. 17 | var bytes = new byte[content.Length]; 18 | content.Read(bytes, 0, bytes.Length); 19 | Uploads.Add(bytes); 20 | return Task.FromResult(Response.FromValue(null!, new TestAzureResponse(200))); 21 | } 22 | } -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/TestServiceBusClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Messaging.ServiceBus; 5 | 6 | namespace Relecloud.TicketRenderer.TestHelpers; 7 | 8 | public sealed class TestServiceBusClient : ServiceBusClient 9 | { 10 | public TestServiceBusSender Sender { get; } 11 | 12 | public TestServiceBusReceiver Receiver { get;} 13 | 14 | public TestServiceBusProcessor Processor { get; } 15 | 16 | public TestServiceBusClient() 17 | { 18 | Sender = new TestServiceBusSender(); 19 | Receiver = new TestServiceBusReceiver(); 20 | Processor = new TestServiceBusProcessor(Receiver); 21 | } 22 | 23 | public override string FullyQualifiedNamespace => "TestNamespace"; 24 | 25 | public Task SimulateReceivedMessageAsync(T messageBody, CancellationToken ct) 26 | { 27 | throw new NotImplementedException(); 28 | } 29 | 30 | public override ServiceBusProcessor CreateProcessor(string queueName, ServiceBusProcessorOptions options) 31 | { 32 | return Processor; 33 | } 34 | 35 | public override ServiceBusSender CreateSender(string queueOrTopicName, ServiceBusSenderOptions options) 36 | { 37 | return Sender; 38 | } 39 | 40 | public override ValueTask DisposeAsync() => ValueTask.CompletedTask; 41 | } 42 | -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/TestServiceBusProcessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Messaging.ServiceBus; 5 | 6 | namespace Relecloud.TicketRenderer.TestHelpers; 7 | 8 | public class TestServiceBusProcessor(ServiceBusReceiver defaultReceiver) : ServiceBusProcessor 9 | { 10 | public int StartProcessingAsyncCallCount { get; private set; } 11 | public CancellationToken StartProcessing_CancellationToken { get; private set; } 12 | 13 | public override string FullyQualifiedNamespace => "TestNamespace"; 14 | 15 | public override string EntityPath => "TestPath"; 16 | 17 | public Task SimulateErrorAsync(ProcessErrorEventArgs args) => OnProcessErrorAsync(args); 18 | 19 | public Task SimulateMessageAsync(ProcessMessageEventArgs args) => OnProcessMessageAsync(args); 20 | 21 | public Task SimulateMessageAsync(T messageBody, CancellationToken ct) 22 | { 23 | var message = ServiceBusModelFactory.ServiceBusReceivedMessage(BinaryData.FromObjectAsJson(messageBody)); 24 | return SimulateMessageAsync(new ProcessMessageEventArgs(message, defaultReceiver, Guid.NewGuid().ToString(), ct)); 25 | } 26 | 27 | public Task SimulateErrorAsync(Exception exception, CancellationToken ct) => 28 | SimulateErrorAsync(new ProcessErrorEventArgs(exception, ServiceBusErrorSource.Receive, FullyQualifiedNamespace, EntityPath, Guid.NewGuid().ToString(), ct)); 29 | 30 | public override Task StartProcessingAsync(CancellationToken cancellationToken = default) 31 | { 32 | StartProcessingAsyncCallCount++; 33 | StartProcessing_CancellationToken = cancellationToken; 34 | return Task.CompletedTask; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/TestServiceBusReceiver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Messaging.ServiceBus; 5 | 6 | namespace Relecloud.TicketRenderer.TestHelpers; 7 | 8 | public class TestServiceBusReceiver : ServiceBusReceiver 9 | { 10 | public IList DeadLetters { get; } = []; 11 | 12 | public override string FullyQualifiedNamespace => "TestNamespace"; 13 | 14 | public override string EntityPath => "TestPath"; 15 | 16 | public override Task DeadLetterMessageAsync(ServiceBusReceivedMessage message, string deadLetterReason, string? deadLetterErrorDescription = null, CancellationToken cancellationToken = default) 17 | { 18 | DeadLetters.Add(message); 19 | return Task.CompletedTask; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Relecloud.TestHelpers/TestServiceBusSender.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Azure.Messaging.ServiceBus; 5 | 6 | namespace Relecloud.TicketRenderer.TestHelpers; 7 | 8 | public class TestServiceBusSender : ServiceBusSender 9 | { 10 | public IList SentMessages { get; } = []; 11 | 12 | public override Task SendMessageAsync(ServiceBusMessage message, CancellationToken cancellationToken = default) 13 | { 14 | SentMessages.Add(message); 15 | return Task.CompletedTask; 16 | } 17 | 18 | public override Task CloseAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; 19 | } 20 | -------------------------------------------------------------------------------- /tests/Relecloud.TicketRenderer.IntegrationTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Microsoft.AspNetCore.Mvc.Testing; 5 | global using NSubstitute; 6 | global using Relecloud.Models.ConcertContext; 7 | global using Relecloud.Messaging.Messages; 8 | global using Relecloud.TicketRenderer.Services; 9 | global using Relecloud.TicketRenderer.TestHelpers; 10 | global using Xunit; 11 | -------------------------------------------------------------------------------- /tests/Relecloud.TicketRenderer.IntegrationTests/Relecloud.TicketRenderer.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/Relecloud.TicketRenderer.Tests/AzureStorageOptionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Relecloud.TicketRenderer.Tests; 7 | 8 | public class AzureStorageOptionsTests 9 | { 10 | [Fact] 11 | public void UriIsRequired() 12 | { 13 | var options = new AzureStorageOptions 14 | { 15 | Container = "TestContainer" 16 | }; 17 | 18 | var context = new ValidationContext(options); 19 | var results = new List(); 20 | var isValid = Validator.TryValidateObject(options, context, results, true); 21 | 22 | Assert.False(isValid); 23 | Assert.Contains(results, r => r.MemberNames.Contains("Uri")); 24 | } 25 | 26 | [Fact] 27 | public void ContainerIsRequired() 28 | { 29 | var options = new AzureStorageOptions 30 | { 31 | Uri = "TestUri" 32 | }; 33 | 34 | var context = new ValidationContext(options); 35 | var results = new List(); 36 | var isValid = Validator.TryValidateObject(options, context, results, true); 37 | 38 | Assert.False(isValid); 39 | Assert.Contains(results, r => r.MemberNames.Contains("Container")); 40 | } 41 | } -------------------------------------------------------------------------------- /tests/Relecloud.TicketRenderer.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Microsoft.Extensions.Logging; 5 | global using Microsoft.Extensions.Options; 6 | global using NSubstitute; 7 | global using Relecloud.Messaging; 8 | global using Relecloud.Messaging.Messages; 9 | global using Relecloud.Models.ConcertContext; 10 | global using Relecloud.TicketRenderer.Models; 11 | global using Relecloud.TicketRenderer.Services; 12 | global using Relecloud.TicketRenderer.TestHelpers; 13 | global using Xunit; 14 | -------------------------------------------------------------------------------- /tests/Relecloud.TicketRenderer.Tests/Relecloud.TicketRenderer.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/Relecloud.TicketRenderer.Tests/ResilienceOptionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.TicketRenderer.Tests; 5 | 6 | public class ResilienceOptionsTests 7 | { 8 | [Fact] 9 | public void ValidateDefaults() 10 | { 11 | var options = new ResilienceOptions(); 12 | 13 | Assert.Equal(5, options.MaxRetries); 14 | Assert.Equal(0.8, options.BaseDelaySecondsBetweenRetries); 15 | Assert.Equal(60, options.MaxDelaySeconds); 16 | Assert.Equal(90, options.MaxNetworkTimeoutSeconds); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Relecloud.TicketRenderer.Tests/TestBarcodeGenerator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Relecloud.TicketRenderer.TestHelpers; 5 | 6 | public class TestBarcodeGenerator(int width) : IBarcodeGenerator 7 | { 8 | public IEnumerable GenerateBarcode(Ticket ticket) 9 | { 10 | for (var i = 0; i < width / 3 + 1; i++) 11 | { 12 | yield return 3; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Relecloud.Web.CallCenter.Api.Tests/FeatureDependentTicketRenderingServiceFactoryTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.FeatureManagement; 5 | 6 | namespace Relecloud.Web.CallCenter.Api.Tests; 7 | 8 | public class FeatureDependentTicketRenderingServiceFactoryTests 9 | { 10 | [InlineData(true)] 11 | [InlineData(false)] 12 | [Theory] 13 | public async Task CreateAsync_ReturnsExpectedService(bool distributedTicketRenderingEnabled) 14 | { 15 | // Arrange 16 | var featureManager = Substitute.For(); 17 | featureManager.IsEnabledAsync("DistributedTicketRendering").Returns(distributedTicketRenderingEnabled); 18 | 19 | var serviceCollection = new ServiceCollection(); 20 | var options = Substitute.For>(); 21 | options.Value.Returns(new MessageBusOptions { RenderRequestQueueName = "test-queue" }); 22 | serviceCollection.AddSingleton(new DistributedTicketRenderingService(null!, Substitute.For(), options, null!)); 23 | serviceCollection.AddSingleton(new LocalTicketRenderingService(null!, null!, null!)); 24 | var serviceProvider = serviceCollection.BuildServiceProvider(); 25 | 26 | var factory = new FeatureDependentTicketRenderingServiceFactory(featureManager, serviceProvider); 27 | 28 | // Act 29 | var service = await factory.CreateAsync(); 30 | 31 | // Assert 32 | if (distributedTicketRenderingEnabled) 33 | { 34 | Assert.IsType(service); 35 | } 36 | else 37 | { 38 | Assert.IsType(service); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Relecloud.Web.CallCenter.Api.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | global using Microsoft.Extensions.DependencyInjection; 5 | global using Microsoft.Extensions.Logging; 6 | global using Microsoft.Extensions.Options; 7 | global using NSubstitute; 8 | global using Relecloud.Messaging; 9 | global using Relecloud.Messaging.Messages; 10 | global using Relecloud.Models.ConcertContext; 11 | global using Relecloud.Web.Api.Services.SqlDatabaseConcertRepository; 12 | global using Relecloud.Web.Api.Services.TicketManagementService; 13 | global using Relecloud.Web.CallCenter.Api.Services.TicketManagementService; 14 | global using Xunit; 15 | -------------------------------------------------------------------------------- /tests/Relecloud.Web.CallCenter.Api.Tests/Relecloud.Web.CallCenter.Api.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Relecloud.Web.CallCenter.Api.Tests/TestHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Relecloud.Web.CallCenter.Api.Tests; 7 | 8 | internal static class TestHelpers 9 | { 10 | 11 | public static async Task CreateTestDatabaseAsync() 12 | { 13 | var contextOptions = new DbContextOptionsBuilder() 14 | .UseInMemoryDatabase($"TestDatabase-{Guid.NewGuid()}") 15 | .Options; 16 | var database = new ConcertDataContext(contextOptions); 17 | 18 | var concert = new Concert { Id = 1 }; 19 | var customer = new Customer { Id = 1 }; 20 | var user = new User { Id = "0" }; 21 | 22 | await database.Tickets.AddRangeAsync( 23 | new[] { 24 | new Ticket { Id = 10, Concert = concert, Customer = customer, User = user }, 25 | new Ticket { Id = 11, Concert = concert, Customer = customer, User = user }, 26 | new Ticket { Id = 12, Concert = concert, Customer = customer, User = user }, 27 | new Ticket { Id = 13, Concert = concert, Customer = customer, User = user } 28 | }); 29 | await database.SaveChangesAsync(); 30 | 31 | return database; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testscripts/README.md: -------------------------------------------------------------------------------- 1 | # Testing scripts 2 | These scripts are used by the engineering team to accelerate the testing process through deployment automation. 3 | 4 | ## Workflow 5 | 6 | 1. From terminal in the devcontainer start powershell 7 | 8 | ```sh 9 | pwsh 10 | ``` 11 | 12 | 1. Install the required PowerShell modules 13 | 14 | ```pwsh 15 | Install-Module Az 16 | ``` 17 | 18 | ```pwsh 19 | Import-Module Az 20 | ``` 21 | 22 | 1. Validate your connection settings 23 | 24 | ```pwsh 25 | Get-AzContext 26 | ``` 27 | 28 | ```pwsh 29 | azd config get defaults.subscription 30 | ``` 31 | 32 | * If you are not authenticated then run the following to set your account context. 33 | 34 | ```pwsh 35 | Connect-AzAccount 36 | ``` 37 | 38 | ```pwsh 39 | azd auth login 40 | ``` 41 | 42 | * If you need to change your default subscription. 43 | 44 | ```pwsh 45 | Set-AzContext -Subscription {your_subscription_id} 46 | ``` 47 | 48 | ```pwsh 49 | azd config set defaults.subscription {your_subscription_id} 50 | ``` 51 | 52 | 1. Start a provision 53 | 54 | > It is encouraged to use a distinct name for each deployment 55 | 56 | ```pwsh 57 | .\testscripts\setup.ps1 -NotIsolated -Development -CommonAppServicePlan -SingleLocation -Name reledev7 58 | ``` 59 | 60 | 61 | 62 | 1. Run a deployment 63 | 64 | ```pwsh 65 | azd deploy 66 | ``` 67 | 68 | 1. Clean up a provisioned environment 69 | 70 | > Find the full name of the application resource group to be supplied as the value for *ResourceGroup* param 71 | 72 | ```pwsh 73 | .\testscripts\cleanup.ps1 -ResourceGroup rg-reledev7-dev-westus3-application 74 | ``` -------------------------------------------------------------------------------- /testscripts/call-validate-deployment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is run by GitHub workflow and is part of the deployment lifecycle run when validatinng the deployment for the Relecloud web app. 4 | resourceGroupName=$((azd env get-values --output json) | jq -r .AZURE_RESOURCE_GROUP) 5 | echo "Calling validate-deployment.sh for group:'$resourceGroupName'..." 6 | ./testscripts/validate-deployment.sh -g $resourceGroupName --------------------------------------------------------------------------------