├── .azdo └── pipelines │ ├── README.md │ └── azure-dev.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── azure-dev.yml ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── ApiSamples.sln ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Diagrams ├── APIM-GQL-Samples.drawio └── APIM-GQL-Samples.drawio.png ├── LICENSE.md ├── README.md ├── azure.yaml ├── infra ├── abbreviations.json ├── app │ ├── database.bicep │ ├── starwars-rest-api.bicep │ ├── starwars-syngql-api.bicep │ ├── todo-graphql-api.bicep │ ├── todo-react-rest.bicep │ └── todo-rest-api.bicep ├── core │ ├── cache │ │ └── redis.bicep │ ├── database │ │ └── sqlserver.bicep │ ├── gateway │ │ ├── api-management.bicep │ │ ├── default-policy.xml │ │ ├── graphql-api.bicep │ │ ├── rest-api.bicep │ │ ├── synthetic-graphql-api.bicep │ │ └── synthetic-graphql-resolver.bicep │ ├── host │ │ ├── appservice.bicep │ │ ├── appserviceplan.bicep │ │ └── staticwebapp.bicep │ └── monitor │ │ ├── applicationinsights-dashboard.bicep │ │ ├── applicationinsights.bicep │ │ ├── loganalytics.bicep │ │ └── monitoring.bicep ├── main.bicep └── main.parameters.json └── src ├── ApiManagement ├── StarWarsRestApi │ ├── policy.xml │ └── swagger.json ├── StarWarsSynGQLApi │ ├── policy.xml │ ├── resolvers │ │ ├── film-all.xml │ │ ├── person-all.xml │ │ ├── planet-all.xml │ │ ├── query-characters.xml │ │ ├── query-films.xml │ │ ├── query-getcharacterbyid.xml │ │ ├── query-getfilmbyid.xml │ │ ├── query-getplanetbyid.xml │ │ ├── query-getspeciesbyid.xml │ │ ├── query-getstarshipbyid.xml │ │ ├── query-getvehiclebyid.xml │ │ ├── query-planets.xml │ │ ├── query-species.xml │ │ ├── query-starships.xml │ │ ├── query-vehicles.xml │ │ ├── species-all.xml │ │ ├── starship-all.xml │ │ └── vehicle-all.xml │ └── schema.graphql ├── TodoGraphQLApi │ ├── policy.xml │ └── schema.graphql └── TodoRestApi │ ├── policy.xml │ └── swagger.json ├── StarWars.Data ├── Models │ ├── BaseModel.cs │ ├── BaseVehicle.cs │ ├── Film.cs │ ├── Person.cs │ ├── Planet.cs │ ├── Species.cs │ ├── Starship.cs │ └── Vehicle.cs ├── Serialization │ └── DataModelSerializers.cs ├── StarWars.Data.csproj └── StarWarsData.cs ├── StarWars.RestApi ├── Controllers │ ├── FilmController.cs │ ├── PersonController.cs │ ├── PlanetController.cs │ ├── SpeciesController.cs │ ├── StarshipController.cs │ └── VehicleController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Serialization │ └── DateOnlyJsonConverter.cs ├── StarWars.RestApi.csproj ├── appsettings.Development.json └── appsettings.json ├── Todo.Data ├── IDatabaseInitializer.cs ├── Todo.Data.csproj ├── TodoBaseModel.cs ├── TodoDbContext.cs ├── TodoItem.cs └── TodoList.cs ├── Todo.GraphQLApi ├── GraphQL │ ├── DTO │ │ ├── TodoItem.cs │ │ └── TodoList.cs │ ├── GraphQLExtensions.cs │ ├── InputTypes.cs │ ├── MappingProfile.cs │ ├── Mutation.cs │ ├── Query.cs │ ├── Services │ │ ├── TodoDataService.cs │ │ └── TodoServiceException.cs │ └── TodoListNode.cs ├── Program.cs ├── Properties │ ├── ModuleInfo.cs │ └── launchSettings.json ├── Todo.GraphQLApi.csproj ├── appsettings.Development.json └── appsettings.json ├── Todo.RestApi ├── Controllers │ └── ListsController.cs ├── Extensions │ ├── ServiceExceptionFilter.cs │ └── Utils.cs ├── Models │ ├── CreateUpdateTodoItem.cs │ ├── CreateUpdateTodoList.cs │ └── Page.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── DTO │ │ ├── TodoItem.cs │ │ └── TodoList.cs │ ├── Exceptions.cs │ ├── ITodoDataService.cs │ ├── MappingProfile.cs │ └── TodoDataService.cs ├── Todo.RestApi.csproj ├── appsettings.Development.json └── appsettings.json └── todo.rest.web ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── web.config ├── src ├── @types │ └── window.d.ts ├── App.css ├── App.tsx ├── actions │ ├── actionCreators.ts │ ├── common.ts │ ├── itemActions.ts │ └── listActions.ts ├── components │ ├── telemetry.tsx │ ├── telemetryContext.ts │ ├── todoContext.ts │ ├── todoItemDetailPane.tsx │ ├── todoItemListPane.tsx │ └── todoListMenu.tsx ├── config │ └── index.ts ├── index.css ├── index.tsx ├── layout │ ├── header.tsx │ ├── layout.tsx │ └── sidebar.tsx ├── models │ ├── applicationState.ts │ ├── index.ts │ ├── todoItem.ts │ └── todoList.ts ├── pages │ └── homePage.tsx ├── react-app-env.d.ts ├── reducers │ ├── index.ts │ ├── listsReducer.ts │ ├── selectedItemReducer.ts │ └── selectedListReducer.ts ├── reportWebVitals.ts ├── services │ ├── itemService.ts │ ├── listService.ts │ ├── restService.ts │ └── telemetryService.ts ├── setupTests.ts └── ux │ ├── styles.ts │ └── theme.ts ├── tools ├── .gitignore ├── entrypoint.js └── entrypoint.sh └── tsconfig.json /.azdo/pipelines/README.md: -------------------------------------------------------------------------------- 1 | # Azure DevOps Pipeline Configuration 2 | 3 | This document will show you how to configure an Azure DevOps pipeline that uses the Azure Developer CLI. This can be configured by running the `azd pipeline config --provider azdo` command. 4 | 5 | You will find a default Azure DevOps pipeline file in `./.azdo/pipelines/azure-dev.yml`. It will provision your Azure resources and deploy your code upon pushes and pull requests. 6 | 7 | You are welcome to use that file as-is or modify it to suit your needs. 8 | 9 | ## Create or Use Existing Azure DevOps Organization 10 | 11 | To run a pipeline in Azure DevOps, you'll first need an Azure DevOps organization. You must create an organization using the Azure DevOps portal here: https://dev.azure.com. 12 | 13 | Once you have that organization, copy and paste it below, then run the commands to set those environment variables. 14 | 15 | ```bash 16 | export AZURE_DEVOPS_ORG_NAME="" 17 | ``` 18 | 19 | This can also be set as an Azure Developer CLI environment via the command: 20 | 21 | ```bash 22 | azd env set AZURE_DEVOPS_ORG_NAME "" 23 | ``` 24 | > AZURE_DEVOPS_ORG_NAME: The name of the Azure DevOps organization that you just created or existing one that you want to use. 25 | 26 | ## Create a Personal Access Token 27 | 28 | The Azure Developer CLI relies on an Azure DevOps Personal Access Token (PAT) to configure an Azure DevOps project. The Azure Developer CLI will prompt you to create a PAT and provide [documentation on the PAT creation process](https://aka.ms/azure-dev/azdo-pat). 29 | 30 | 31 | ```bash 32 | export AZURE_DEVOPS_EXT_PAT= 33 | ``` 34 | > AZURE_DEVOPS_EXT_PAT: The Azure DevOps Personal Access Token that you just created or existing one that you want to use. 35 | 36 | ## Invoke the Pipeline configure command 37 | 38 | By running `azd pipeline config --provider azdo` you can instruct the Azure Developer CLI to configure an Azure DevOps Project and Repository with a deployment Pipeline. 39 | 40 | ## Conclusion 41 | 42 | That is everything you need to have in place to get the Azure DevOps pipeline running. You can verify that it is working by going to the Azure DevOps portal (https://dev.azure.com) and finding the project you just created. 43 | 44 | 45 | -------------------------------------------------------------------------------- /.azdo/pipelines/azure-dev.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | 4 | pool: 5 | vmImage: ubuntu-latest 6 | 7 | container: mcr.microsoft.com/azure-dev-cli-apps:latest 8 | 9 | steps: 10 | - pwsh: | 11 | azd config set auth.useAzCliAuth "true" 12 | displayName: Configure AZD to Use AZ CLI Authentication. 13 | - task: AzureCLI@2 14 | displayName: Azure Dev Provision 15 | inputs: 16 | azureSubscription: azconnection 17 | scriptType: bash 18 | scriptLocation: inlineScript 19 | inlineScript: | 20 | azd provision --no-prompt 21 | env: 22 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 23 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 24 | AZURE_LOCATION: $(AZURE_LOCATION) 25 | 26 | - task: AzureCLI@2 27 | displayName: Azure Dev Deploy 28 | inputs: 29 | azureSubscription: azconnection 30 | scriptType: bash 31 | scriptLocation: inlineScript 32 | inlineScript: | 33 | azd deploy --no-prompt 34 | env: 35 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 36 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 37 | AZURE_LOCATION: $(AZURE_LOCATION) -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT=bullseye 2 | FROM --platform=amd64 mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} 3 | RUN export DEBIAN_FRONTEND=noninteractive \ 4 | && apt-get update && apt-get install -y xdg-utils \ 5 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 6 | RUN curl -fsSL https://aka.ms/install-azd.sh | bash -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure Developer CLI", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "args": { 6 | "VARIANT": "bullseye" 7 | } 8 | }, 9 | "customizations": { 10 | "vscode": { 11 | "extensions": [ 12 | "ms-azuretools.azure-dev", 13 | "ms-vscode.azure-account", 14 | "ms-azurecache.vscode-azurecache", 15 | "ms-azuretools.vscode-azureresourcegroups", 16 | "ms-azuretools.vscode-azureappservice", 17 | "ms-azuretools.vscode-azurestaticwebapps", 18 | "ms-azuretools.vscode-apimanagement", 19 | "ms-azuretools.vscode-bicep", 20 | "ms-azuretools.vscode-docker", 21 | "ms-mssql.mssql", 22 | "ms-vscode.vscode-node-azure-pack", 23 | "VisualStudioOnlineApplicationInsights.application-insights", 24 | "ms-dotnettools.csharp", 25 | "ms-dotnettools.vscode-dotnet-runtime", 26 | "mikestead.dotenv" 27 | ] 28 | } 29 | }, 30 | "features": { 31 | "ghcr.io/devcontainers/features/docker-from-docker:1": { 32 | "version": "20.10" 33 | }, 34 | "ghcr.io/devcontainers/features/dotnet:1": { 35 | "version": "6.0" 36 | }, 37 | "ghcr.io/devcontainers/features/github-cli:1": { 38 | "version": "2" 39 | }, 40 | "ghcr.io/devcontainers/features/node:1": { 41 | "version": "16", 42 | "nodeGypDependencies": false 43 | } 44 | }, 45 | "forwardPorts": [3000, 3100], 46 | "postCreateCommand": "", 47 | "remoteUser": "vscode", 48 | "hostRequirements": { 49 | "memory": "8gb" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /.github/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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | azd up 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: 5 | - main 6 | 7 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication 8 | permissions: 9 | id-token: write 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | container: 16 | image: mcr.microsoft.com/azure-dev-cli-apps:latest 17 | env: 18 | AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} 19 | AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 20 | AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 21 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Log in with Azure (Federated Credentials) 27 | if: ${{ env.AZURE_CLIENT_ID != '' }} 28 | run: | 29 | azd login ` 30 | --client-id "$Env:AZURE_CLIENT_ID" ` 31 | --federated-credential-provider "github" ` 32 | --tenant-id "$Env:AZURE_TENANT_ID" 33 | shell: pwsh 34 | 35 | - name: Log in with Azure (Client Credentials) 36 | if: ${{ env.AZURE_CREDENTIALS != '' }} 37 | run: | 38 | $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; 39 | Write-Host "::add-mask::$($info.clientSecret)" 40 | azd login ` 41 | --client-id "$($info.clientId)" ` 42 | --client-secret "$($info.clientSecret)" ` 43 | --tenant-id "$($info.tenantId)" 44 | shell: pwsh 45 | env: 46 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 47 | 48 | - name: Azure Dev Provision 49 | run: azd provision --no-prompt 50 | env: 51 | AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} 52 | AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} 53 | AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 54 | 55 | - name: Azure Dev Deploy 56 | run: azd deploy --no-prompt 57 | env: 58 | AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} 59 | AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} 60 | AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.azure-dev", 4 | "ms-vscode.azure-account", 5 | "ms-azuretools.vscode-azureresourcegroups", 6 | "ms-azuretools.vscode-azureappservice", 7 | "ms-azuretools.vscode-apimanagement", 8 | "ms-azuretools.vscode-cosmosdb", 9 | "ms-azuretools.vscode-bicep", 10 | "VisualStudioOnlineApplicationInsights.application-insights", 11 | "ms-dotnettools.csharp", 12 | "mikestead.dotenv" 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Todo REST App", 6 | "request": "launch", 7 | "type": "msedge", 8 | "webRoot": "${workspaceFolder}/src/todo.rest.web/src", 9 | "url": "http://localhost:3000", 10 | "sourceMapPathOverrides": { 11 | "webpack:///src/*": "${webRoot}/*" 12 | }, 13 | }, 14 | { 15 | // Use IntelliSense to find out which attributes exist for C# debugging 16 | // Use hover for the description of the existing attributes 17 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 18 | "name": "Debug Todo REST API", 19 | "type": "coreclr", 20 | "request": "launch", 21 | "preLaunchTask": "Build API", 22 | // If you have changed target frameworks, make sure to update the program path. 23 | "program": "${workspaceFolder}/src/Todo.RestApi/bin/Debug/net6.0/Todo.RestApi.dll", 24 | "args": [], 25 | "cwd": "${workspaceFolder}/src/Todo.RestApi", 26 | "stopAtEntry": false, 27 | "env": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | }, 30 | "envFile": "${input:dotEnvFilePath}" 31 | }, 32 | { 33 | "name": ".NET Core Attach", 34 | "type": "coreclr", 35 | "request": "attach" 36 | } 37 | ], 38 | "inputs": [ 39 | { 40 | "id": "dotEnvFilePath", 41 | "type": "command", 42 | "command": "azure-dev.commands.getDotEnvFilePath" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /ApiSamples.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarWars.Data", "src\StarWars.Data\StarWars.Data.csproj", "{36DEE7C6-C9E4-4DD4-BFFC-A4C36AA691FE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarWars.RestApi", "src\StarWars.RestApi\StarWars.RestApi.csproj", "{E431C58C-18A4-4D67-B6B8-C61D48586B2A}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.Data", "src\Todo.Data\Todo.Data.csproj", "{78B0B8B1-9C97-444D-A223-5B16C0587E28}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.RestApi", "src\Todo.RestApi\Todo.RestApi.csproj", "{36714EE8-6D41-4B80-A567-D6ADDB00A6DA}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.GraphQLApi", "src\Todo.GraphQLApi\Todo.GraphQLApi.csproj", "{9E758950-45A0-45E1-90C9-497B21E00E39}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {36DEE7C6-C9E4-4DD4-BFFC-A4C36AA691FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {36DEE7C6-C9E4-4DD4-BFFC-A4C36AA691FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {36DEE7C6-C9E4-4DD4-BFFC-A4C36AA691FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {36DEE7C6-C9E4-4DD4-BFFC-A4C36AA691FE}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {E431C58C-18A4-4D67-B6B8-C61D48586B2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {E431C58C-18A4-4D67-B6B8-C61D48586B2A}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {E431C58C-18A4-4D67-B6B8-C61D48586B2A}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {E431C58C-18A4-4D67-B6B8-C61D48586B2A}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {78B0B8B1-9C97-444D-A223-5B16C0587E28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {78B0B8B1-9C97-444D-A223-5B16C0587E28}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {78B0B8B1-9C97-444D-A223-5B16C0587E28}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {78B0B8B1-9C97-444D-A223-5B16C0587E28}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {36714EE8-6D41-4B80-A567-D6ADDB00A6DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {36714EE8-6D41-4B80-A567-D6ADDB00A6DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {36714EE8-6D41-4B80-A567-D6ADDB00A6DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {36714EE8-6D41-4B80-A567-D6ADDB00A6DA}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {9E758950-45A0-45E1-90C9-497B21E00E39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {9E758950-45A0-45E1-90C9-497B21E00E39}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {9E758950-45A0-45E1-90C9-497B21E00E39}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {9E758950-45A0-45E1-90C9-497B21E00E39}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {BE240592-0EC7-4523-967B-AFBF2920BC8E} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## api-management-sample-apis Changelog 2 | 3 | 4 | # 1.0.0 (2023-03-01) 5 | 6 | *Features* 7 | * First Release! 8 | * Added the Star Wars and TodoList API REST API definitions. 9 | 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to api-management-sample-apis 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 of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/azure-samples/api-management-sample-apis/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/azure-samples/api-management-sample-apis/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /Diagrams/APIM-GQL-Samples.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/api-management-sample-apis/d317a2342a63d951005ac56ce719a9e86809dcca/Diagrams/APIM-GQL-Samples.drawio.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: api-samples 4 | metadata: 5 | template: api-management-api-samples@1.0.0 6 | services: 7 | starwars-rest: 8 | project: ./src/StarWars.RestApi 9 | language: csharp 10 | host: appservice 11 | todo-rest: 12 | project: ./src/Todo.RestApi 13 | language: csharp 14 | host: appservice 15 | todo-react-rest: 16 | project: ./src/todo.rest.web 17 | dist: build 18 | language: js 19 | host: staticwebapp -------------------------------------------------------------------------------- /infra/app/database.bicep: -------------------------------------------------------------------------------- 1 | @description('The name of the SQL server host') 2 | param name string 3 | 4 | @description('The location to create host resources in') 5 | param location string = resourceGroup().location 6 | 7 | @description('The tags to apply to resources') 8 | param tags object = {} 9 | 10 | @description('The name of the database to create') 11 | param databaseName string = '' 12 | 13 | @secure() 14 | @description('SQL Server administrator password') 15 | param sqlAdminPassword string 16 | 17 | var actualDatabaseName = !empty(databaseName) ? databaseName : 'Samples' 18 | 19 | module sqlserver '../core/database/sqlserver.bicep' = { 20 | name: 'sqlserver' 21 | params: { 22 | name: name 23 | location: location 24 | tags: tags 25 | databaseName: actualDatabaseName 26 | sqlAdminPassword: sqlAdminPassword 27 | } 28 | } 29 | 30 | output connectionString string = sqlserver.outputs.connectionString 31 | output databaseName string = sqlserver.outputs.databaseName 32 | -------------------------------------------------------------------------------- /infra/app/starwars-rest-api.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param tags object = {} 4 | 5 | param applicationInsightsName string = '' 6 | param appServicePlanId string 7 | param apiManagementServiceName string = '' 8 | param apiManagementLoggerName string = '' 9 | param path string = 'starwars-rest' 10 | 11 | module apiService '../core/host/appservice.bicep' = { 12 | name: name 13 | params: { 14 | name: name 15 | location: location 16 | tags: tags 17 | appCommandLine: '' 18 | applicationInsightsName: applicationInsightsName 19 | appServicePlanId: appServicePlanId 20 | appSettings: {} 21 | runtimeName: 'dotnetcore' 22 | runtimeVersion: '6.0' 23 | scmDoBuildDuringDeployment: false 24 | } 25 | } 26 | 27 | module restApiDefinition '../core/gateway/rest-api.bicep' = if (!empty(apiManagementServiceName)) { 28 | name: 'starwars-rest-api-definition' 29 | params: { 30 | name: 'starwars-rest' 31 | apimServiceName: apiManagementServiceName 32 | apimLoggerName: apiManagementLoggerName 33 | path: path 34 | serviceUrl: apiService.outputs.uri 35 | policy: loadTextContent('../../src/ApiManagement/StarWarsRestApi/policy.xml') 36 | definition: loadTextContent('../../src/ApiManagement/StarWarsRestApi/swagger.json') 37 | } 38 | } 39 | 40 | output serviceUri string = apiService.outputs.uri 41 | output gatewayUri string = restApiDefinition.outputs.serviceUrl 42 | -------------------------------------------------------------------------------- /infra/app/starwars-syngql-api.bicep: -------------------------------------------------------------------------------- 1 | param serviceUri string 2 | param path string = 'starwars-syngql' 3 | 4 | param apiManagementServiceName string 5 | param apiManagementLoggerName string = '' 6 | 7 | var resolvers = [ 8 | { 9 | name: 'query-characters', type: 'Query', field: 'characters' 10 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-characters.xml') 11 | } 12 | { 13 | name: 'query-films', type: 'Query', field: 'films' 14 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-films.xml') 15 | } 16 | { 17 | name: 'query-planets', type: 'Query', field: 'planets' 18 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-planets.xml') 19 | } 20 | { 21 | name: 'query-species', type: 'Query', field: 'species' 22 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-species.xml') 23 | } 24 | { 25 | name: 'query-starships', type: 'Query', field: 'starships' 26 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-starships.xml') 27 | } 28 | { 29 | name: 'query-vehicles', type: 'Query', field: 'vehicles' 30 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-vehicles.xml') 31 | } 32 | { 33 | name: 'query-getcharacterbyid', type: 'Query', field: 'getCharacterById' 34 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getCharacterById.xml') 35 | } 36 | { 37 | name: 'query-getfilmbyid', type: 'Query', field: 'getFilmById' 38 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getFilmById.xml') 39 | } 40 | { 41 | name: 'query-getplanetbyid', type: 'Query', field: 'getPlanetById' 42 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getPlanetById.xml') 43 | } 44 | { 45 | name: 'query-getspeciesbyid', type: 'Query', field: 'getSpeciesById' 46 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getSpeciesById.xml') 47 | } 48 | { 49 | name: 'query-getstarshipbyid', type: 'Query', field: 'getStarshipById' 50 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getStarshipById.xml') 51 | } 52 | { 53 | name: 'query-getvehiclebyid', type: 'Query', field: 'getVehicleById' 54 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getVehicleById.xml') 55 | } 56 | { 57 | name: 'film-all', type: 'Film', field: '*' 58 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/film-all.xml') 59 | } 60 | { 61 | name: 'person-all', type: 'Person', field: '*' 62 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/person-all.xml') 63 | } 64 | { 65 | name: 'planet-all', type: 'Planet', field: '*' 66 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/planet-all.xml') 67 | } 68 | { 69 | name: 'species-all', type: 'Species', field: '*' 70 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/species-all.xml') 71 | } 72 | { 73 | name: 'starship-all', type: 'Starship', field: '*' 74 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/starship-all.xml') 75 | } 76 | { 77 | name: 'vehicle-all', type: 'Vehicle', field: '*' 78 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/resolvers/vehicle-all.xml') 79 | } 80 | ] 81 | 82 | module graphqlApiDefinition '../core/gateway/synthetic-graphql-api.bicep' = { 83 | name: 'starwars-syngql-api-definition' 84 | params: { 85 | name: 'starwars-syngql' 86 | apimServiceName: apiManagementServiceName 87 | apimLoggerName: apiManagementLoggerName 88 | path: path 89 | policy: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/policy.xml') 90 | schema: loadTextContent('../../src/ApiManagement/StarWarsSynGQLApi/schema.graphql') 91 | namedValues: [ 92 | { name: 'starwarsapi', value: '${serviceUri}/api' } 93 | ] 94 | resolvers: resolvers 95 | } 96 | } 97 | 98 | output gatewayUri string = graphqlApiDefinition.outputs.serviceUrl 99 | -------------------------------------------------------------------------------- /infra/app/todo-graphql-api.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param tags object = {} 4 | param appSettings object = {} 5 | param connectionStrings object = {} 6 | 7 | param applicationInsightsName string = '' 8 | param appServicePlanId string 9 | param apiManagementServiceName string = '' 10 | param apiManagementLoggerName string = '' 11 | param path string = 'todo-graphql' 12 | 13 | module apiService '../core/host/appservice.bicep' = { 14 | name: name 15 | params: { 16 | name: name 17 | location: location 18 | tags: tags 19 | appCommandLine: '' 20 | applicationInsightsName: applicationInsightsName 21 | appServicePlanId: appServicePlanId 22 | appSettings: appSettings 23 | connectionStrings: connectionStrings 24 | runtimeName: 'dotnetcore' 25 | runtimeVersion: '6.0' 26 | scmDoBuildDuringDeployment: false 27 | useManagedIdentity: true 28 | } 29 | } 30 | 31 | module graphqlApiDefinition '../core/gateway/graphql-api.bicep' = if (!empty(apiManagementServiceName)) { 32 | name: 'todo-graphql-api-definition' 33 | params: { 34 | name: 'todo-graphql' 35 | apimServiceName: apiManagementServiceName 36 | apimLoggerName: apiManagementLoggerName 37 | path: path 38 | serviceUrl: apiService.outputs.uri 39 | policy: loadTextContent('../../src/ApiManagement/TodoGraphQLApi/policy.xml') 40 | schema: loadTextContent('../../src/ApiManagement/TodoGraphQLApi/schema.graphql') 41 | } 42 | } 43 | 44 | output serviceUri string = apiService.outputs.uri 45 | output gatewayUri string = graphqlApiDefinition.outputs.serviceUrl 46 | output servicePrincipalId string = apiService.outputs.servicePrincipalId 47 | -------------------------------------------------------------------------------- /infra/app/todo-react-rest.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param serviceName string = 'todo-react-web' 6 | 7 | module web '../core/host/staticwebapp.bicep' = { 8 | name: '${serviceName}-staticwebapp-module' 9 | params: { 10 | name: name 11 | location: location 12 | tags: tags 13 | } 14 | } 15 | 16 | output SERVICE_WEB_NAME string = web.outputs.name 17 | output SERVICE_WEB_URI string = web.outputs.uri 18 | -------------------------------------------------------------------------------- /infra/app/todo-rest-api.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param tags object = {} 4 | param appSettings object = {} 5 | param connectionStrings object = {} 6 | 7 | param applicationInsightsName string = '' 8 | param appServicePlanId string 9 | param apiManagementServiceName string = '' 10 | param apiManagementLoggerName string = '' 11 | param path string = 'todo-rest' 12 | 13 | module apiService '../core/host/appservice.bicep' = { 14 | name: name 15 | params: { 16 | name: name 17 | location: location 18 | tags: tags 19 | appCommandLine: '' 20 | applicationInsightsName: applicationInsightsName 21 | appServicePlanId: appServicePlanId 22 | appSettings: appSettings 23 | connectionStrings: connectionStrings 24 | runtimeName: 'dotnetcore' 25 | runtimeVersion: '6.0' 26 | scmDoBuildDuringDeployment: false 27 | useManagedIdentity: true 28 | } 29 | } 30 | 31 | module restApiDefinition '../core/gateway/rest-api.bicep' = if (!empty(apiManagementServiceName)) { 32 | name: 'todo-rest-api-definition' 33 | params: { 34 | name: 'todo-rest' 35 | apimServiceName: apiManagementServiceName 36 | apimLoggerName: apiManagementLoggerName 37 | path: path 38 | serviceUrl: apiService.outputs.uri 39 | policy: loadTextContent('../../src/ApiManagement/TodoRestApi/policy.xml') 40 | definition: loadTextContent('../../src/ApiManagement/TodoRestApi/swagger.json') 41 | } 42 | } 43 | 44 | output serviceUri string = apiService.outputs.uri 45 | output gatewayUri string = restApiDefinition.outputs.serviceUrl 46 | output servicePrincipalId string = apiService.outputs.servicePrincipalId 47 | -------------------------------------------------------------------------------- /infra/core/cache/redis.bicep: -------------------------------------------------------------------------------- 1 | @description('The location to deploy our resources to. Default is location of resource group') 2 | param location string = resourceGroup().location 3 | 4 | @description('The name of the Azure Cache for Redis instance to deploy') 5 | param name string 6 | 7 | @description('The tags to apply to the created resources') 8 | param tags object = {} 9 | 10 | @description('The pricing tier of the new Azure Cache for Redis instance') 11 | @allowed([ 'Basic', 'Standard', 'Premium' ]) 12 | param sku string = 'Basic' 13 | 14 | @description('Specify the size of the new Azure Redis Cache instance. Valid values: for C (Basic/Standard) family (0, 1, 2, 3, 4, 5, 6), for P (Premium) family (1, 2, 3, 4)') 15 | @minValue(0) 16 | @maxValue(6) 17 | param capacity int = 1 18 | 19 | var skuFamily = (sku == 'Premium') ? 'P' : 'C' 20 | 21 | resource redisCache 'Microsoft.Cache/redis@2022-06-01' = { 22 | name: name 23 | location: location 24 | tags: tags 25 | properties: { 26 | enableNonSslPort: false 27 | minimumTlsVersion: '1.2' 28 | sku: { 29 | capacity: capacity 30 | family: skuFamily 31 | name: sku 32 | } 33 | } 34 | } 35 | 36 | output cacheName string = redisCache.name 37 | output hostName string = redisCache.properties.hostName 38 | -------------------------------------------------------------------------------- /infra/core/database/sqlserver.bicep: -------------------------------------------------------------------------------- 1 | @description('The name of the SQL server host') 2 | param name string 3 | 4 | @description('The location to create host resources in') 5 | param location string = resourceGroup().location 6 | 7 | @description('The tags to apply to resources') 8 | param tags object = {} 9 | 10 | @description('The name of the database to create') 11 | param databaseName string = '' 12 | 13 | @secure() 14 | @description('SQL Server administrator password') 15 | param sqlAdminPassword string 16 | 17 | @description('SQL Server administrator password') 18 | param sqlAdminUserName string = 'appadmin' 19 | 20 | resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { 21 | name: name 22 | location: location 23 | tags: tags 24 | properties: { 25 | version: '12.0' 26 | minimalTlsVersion: '1.2' 27 | publicNetworkAccess: 'Enabled' 28 | administratorLogin: sqlAdminUserName 29 | administratorLoginPassword: sqlAdminPassword 30 | } 31 | 32 | resource database 'databases' = { 33 | name: databaseName 34 | location: location 35 | } 36 | 37 | resource firewall 'firewallRules' = { 38 | name: 'Azure Services' 39 | properties: { 40 | startIpAddress: '0.0.0.1' 41 | endIpAddress: '255.255.255.254' 42 | } 43 | } 44 | } 45 | 46 | output connectionString string = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${sqlAdminUserName}' 47 | output databaseName string = sqlServer::database.name 48 | -------------------------------------------------------------------------------- /infra/core/gateway/api-management.bicep: -------------------------------------------------------------------------------- 1 | @description('The name of the API Management service') 2 | param name string 3 | 4 | @description('The region where the API Management service should be deployed') 5 | param location string = resourceGroup().location 6 | 7 | @description('The tags that should be applied to the API Management service') 8 | param tags object = {} 9 | 10 | @description('The named values that should be installed for this API Management service') 11 | param namedValues array = [] 12 | 13 | @description('The email address of the owner of the service') 14 | @minLength(1) 15 | param publisherEmail string = 'noreply@microsoft.com' 16 | 17 | @description('The name of the owner of the service') 18 | @minLength(1) 19 | param publisherName string = 'n/a' 20 | 21 | @description('The pricing tier of this API Management service') 22 | @allowed([ 'Consumption', 'Developer', 'Standard', 'Premium', 'BasicV2', 'StandardV2']) 23 | param sku string = 'BasicV2' 24 | 25 | @description('The instance size of this API Management service.') 26 | @allowed([ 0, 1, 2 ]) 27 | param skuCount int = 0 28 | 29 | @description('Azure Application Insights Name') 30 | param applicationInsightsName string 31 | 32 | @description('Azure Cache for Redis Service Name') 33 | param redisCacheServiceName string = '' 34 | 35 | var redisConnectionString = !empty(redisCacheServiceName) ? '${redisCache.properties.hostName},password=${redisCache.listKeys().primaryKey},ssl=True,abortConnect=False' : '' 36 | var redisHostName = !empty(redisCacheServiceName) ? '${redisCache.properties.hostName}' : '' 37 | 38 | resource apimService 'Microsoft.ApiManagement/service@2023-03-01-preview' = { 39 | name: name 40 | location: location 41 | tags: tags 42 | sku: { 43 | name: sku 44 | // Consumptions requires 0, Developer 1, everything else > 0 45 | capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : (skuCount == 0) ? 1 : skuCount) 46 | } 47 | properties: { 48 | publisherEmail: publisherEmail 49 | publisherName: publisherName 50 | } 51 | } 52 | 53 | resource apimLogger 'Microsoft.ApiManagement/service/loggers@2023-03-01-preview' = if (!empty(applicationInsightsName)) { 54 | name: 'app-insights-logger' 55 | parent: apimService 56 | properties: { 57 | credentials: { 58 | instrumentationKey: applicationInsights.properties.InstrumentationKey 59 | } 60 | description: 'Logger to Azure Application Insights' 61 | isBuffered: false 62 | loggerType: 'applicationInsights' 63 | resourceId: applicationInsights.id 64 | } 65 | } 66 | 67 | resource apimCache 'Microsoft.ApiManagement/service/caches@2023-03-01-preview' = if (!empty(redisCacheServiceName)) { 68 | name: 'redis-cache' 69 | parent: apimService 70 | properties: { 71 | connectionString: redisConnectionString 72 | useFromLocation: 'default' 73 | description: redisHostName 74 | } 75 | } 76 | 77 | resource apimNamedValue 'Microsoft.ApiManagement/service/namedValues@2023-03-01-preview' = [for nv in namedValues: { 78 | name: nv.key 79 | parent: apimService 80 | properties: { 81 | displayName: nv.key 82 | secret: contains(nv, 'secret') ? nv.secret : false 83 | value: nv.value 84 | } 85 | }] 86 | 87 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { 88 | name: applicationInsightsName 89 | } 90 | 91 | resource redisCache 'Microsoft.Cache/redis@2022-06-01' existing = if (!empty(redisCacheServiceName)) { 92 | name: redisCacheServiceName 93 | } 94 | 95 | output serviceName string = apimService.name 96 | output loggerName string = !empty(applicationInsightsName) ? apimLogger.name : '' 97 | output uri string = apimService.properties.gatewayUrl 98 | -------------------------------------------------------------------------------- /infra/core/gateway/default-policy.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /infra/core/gateway/graphql-api.bicep: -------------------------------------------------------------------------------- 1 | @description('The display name of the API') 2 | param name string 3 | 4 | @description('The name of the API Management service') 5 | param apimServiceName string 6 | 7 | @description('The name of the API Management logger to use (or blank to disable)') 8 | param apimLoggerName string 9 | 10 | @description('The path that will be exposed by the API Management service') 11 | param path string = 'graphql' 12 | 13 | @description('The URL of the backend service to proxy the request to') 14 | param serviceUrl string 15 | 16 | @description('The policy to configure. If blank, a default policy will be used.') 17 | param policy string = '' 18 | 19 | @description('The GraphQL schema to install.') 20 | param schema string 21 | 22 | @description('The number of bytes in the request/response body to record for diagnostic purposes') 23 | param logBytes int = 8192 24 | 25 | var logSettings = { 26 | headers: [ 'Content-type', 'User-agent' ] 27 | body: { bytes: logBytes } 28 | } 29 | 30 | resource apimService 'Microsoft.ApiManagement/service@2022-08-01' existing = { 31 | name: apimServiceName 32 | } 33 | 34 | resource apimLogger 'Microsoft.ApiManagement/service/loggers@2022-08-01' existing = if (!empty(apimLoggerName)) { 35 | name: apimLoggerName 36 | parent: apimService 37 | } 38 | 39 | var realPolicy = empty(policy) ? loadTextContent('./default-policy.xml') : policy 40 | 41 | resource graphqlApi 'Microsoft.ApiManagement/service/apis@2022-08-01' = { 42 | name: name 43 | parent: apimService 44 | properties: { 45 | path: path 46 | apiType: 'graphql' 47 | displayName: name 48 | protocols: [ 'https', 'wss' ] 49 | serviceUrl: serviceUrl 50 | subscriptionRequired: false 51 | type: 'graphql' 52 | } 53 | } 54 | 55 | resource graphqlSchema 'Microsoft.ApiManagement/service/apis/schemas@2022-08-01' = { 56 | name: 'graphql' 57 | parent: graphqlApi 58 | properties: { 59 | contentType: 'application/vnd.ms-azure-apim.graphql.schema' 60 | document: { 61 | value: schema 62 | } 63 | } 64 | } 65 | 66 | resource graphqlPolicy 'Microsoft.ApiManagement/service/apis/policies@2022-08-01' = { 67 | name: 'policy' 68 | parent: graphqlApi 69 | properties: { 70 | format: 'rawxml' 71 | value: realPolicy 72 | } 73 | } 74 | 75 | resource diagnosticsPolicy 'Microsoft.ApiManagement/service/apis/diagnostics@2022-08-01' = if (!empty(apimLoggerName)) { 76 | name: 'applicationinsights' 77 | parent: graphqlApi 78 | properties: { 79 | alwaysLog: 'allErrors' 80 | httpCorrelationProtocol: 'W3C' 81 | logClientIp: true 82 | loggerId: apimLogger.id 83 | metrics: true 84 | verbosity: 'verbose' 85 | sampling: { 86 | samplingType: 'fixed' 87 | percentage: 100 88 | } 89 | frontend: { 90 | request: logSettings 91 | response: logSettings 92 | } 93 | backend: { 94 | request: logSettings 95 | response: logSettings 96 | } 97 | } 98 | } 99 | 100 | output serviceUrl string = '${apimService.properties.gatewayUrl}/${path}' 101 | -------------------------------------------------------------------------------- /infra/core/gateway/rest-api.bicep: -------------------------------------------------------------------------------- 1 | @description('The display name of the API') 2 | param name string 3 | 4 | @description('The name of the API Management service') 5 | param apimServiceName string 6 | 7 | @description('The name of the API Management logger to use (or blank to disable)') 8 | param apimLoggerName string 9 | 10 | @description('The path that will be exposed by the API Management service') 11 | param path string = 'graphql' 12 | 13 | @description('The URL of the backend service to proxy the request to') 14 | param serviceUrl string 15 | 16 | @description('The policy to configure. If blank, a default policy will be used.') 17 | param policy string = '' 18 | 19 | @description('The OpenAPI description of the API') 20 | param definition string 21 | 22 | @description('The named values that need to be defined prior to the policy being uploaded') 23 | param namedValues array = [] 24 | 25 | @description('The number of bytes of the request/response body to record for diagnostic purposes') 26 | param logBytes int = 8192 27 | 28 | var logSettings = { 29 | headers: [ 'Content-type', 'User-agent' ] 30 | body: { bytes: logBytes } 31 | } 32 | 33 | resource apimService 'Microsoft.ApiManagement/service@2022-08-01' existing = { 34 | name: apimServiceName 35 | } 36 | 37 | resource apimLogger 'Microsoft.ApiManagement/service/loggers@2022-08-01' existing = if (!empty(apimLoggerName)) { 38 | name: apimLoggerName 39 | parent: apimService 40 | } 41 | 42 | var realPolicy = empty(policy) ? loadTextContent('./default-policy.xml') : policy 43 | 44 | resource restApi 'Microsoft.ApiManagement/service/apis@2022-08-01' = { 45 | name: name 46 | parent: apimService 47 | properties: { 48 | displayName: name 49 | path: path 50 | protocols: [ 'https' ] 51 | subscriptionRequired: false 52 | type: 'http' 53 | format: 'openapi' 54 | serviceUrl: serviceUrl 55 | value: definition 56 | } 57 | } 58 | 59 | resource apimNamedValue 'Microsoft.ApiManagement/service/namedValues@2022-08-01' = [for nv in namedValues: { 60 | name: nv.key 61 | parent: apimService 62 | properties: { 63 | displayName: nv.key 64 | secret: contains(nv, 'secret') ? nv.secret : false 65 | value: nv.value 66 | } 67 | }] 68 | 69 | resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2022-08-01' = { 70 | name: 'policy' 71 | parent: restApi 72 | properties: { 73 | format: 'rawxml' 74 | value: realPolicy 75 | } 76 | dependsOn: [ 77 | apimNamedValue 78 | ] 79 | } 80 | 81 | resource diagnosticsPolicy 'Microsoft.ApiManagement/service/apis/diagnostics@2022-08-01' = if (!empty(apimLoggerName)) { 82 | name: 'applicationinsights' 83 | parent: restApi 84 | properties: { 85 | alwaysLog: 'allErrors' 86 | httpCorrelationProtocol: 'W3C' 87 | logClientIp: true 88 | loggerId: apimLogger.id 89 | metrics: true 90 | verbosity: 'verbose' 91 | sampling: { 92 | samplingType: 'fixed' 93 | percentage: 100 94 | } 95 | frontend: { 96 | request: logSettings 97 | response: logSettings 98 | } 99 | backend: { 100 | request: logSettings 101 | response: logSettings 102 | } 103 | } 104 | } 105 | 106 | output serviceUrl string = '${apimService.properties.gatewayUrl}/${path}' 107 | -------------------------------------------------------------------------------- /infra/core/gateway/synthetic-graphql-api.bicep: -------------------------------------------------------------------------------- 1 | @description('The display name of the API') 2 | param name string 3 | 4 | @description('The name of the API Management service') 5 | param apimServiceName string 6 | 7 | @description('The name of the API Management logger to use (or blank to disable)') 8 | param apimLoggerName string 9 | 10 | @description('The path that will be exposed by the API Management service') 11 | param path string = 'graphql' 12 | 13 | @description('The policy to configure. If blank, a default policy will be used.') 14 | param policy string = '' 15 | 16 | @description('The GraphQL schema to install.') 17 | param schema string 18 | 19 | @description('The list of resolvers') 20 | param resolvers array 21 | 22 | @description('The list of named values to install') 23 | param namedValues array 24 | 25 | @description('The number of bytes in the request/response body to record for diagnostic purposes') 26 | param logBytes int = 8192 27 | 28 | var logSettings = { 29 | headers: [ 'Content-type', 'User-agent' ] 30 | body: { bytes: logBytes } 31 | } 32 | 33 | resource apimService 'Microsoft.ApiManagement/service@2022-08-01' existing = { 34 | name: apimServiceName 35 | } 36 | 37 | resource apimLogger 'Microsoft.ApiManagement/service/loggers@2022-08-01' existing = if (!empty(apimLoggerName)) { 38 | name: apimLoggerName 39 | parent: apimService 40 | } 41 | 42 | var realPolicy = empty(policy) ? loadTextContent('./default-policy.xml') : policy 43 | 44 | resource graphqlApi 'Microsoft.ApiManagement/service/apis@2022-08-01' = { 45 | name: name 46 | parent: apimService 47 | properties: { 48 | path: path 49 | apiType: 'graphql' 50 | displayName: name 51 | protocols: [ 'https', 'wss' ] 52 | subscriptionRequired: false 53 | type: 'graphql' 54 | } 55 | } 56 | 57 | resource apiNamedValues 'Microsoft.ApiManagement/service/namedValues@2022-08-01' = [for item in namedValues: { 58 | name: item.name 59 | parent: apimService 60 | properties: { 61 | displayName: item.name 62 | value: item.value 63 | } 64 | }] 65 | 66 | resource graphqlSchema 'Microsoft.ApiManagement/service/apis/schemas@2022-08-01' = { 67 | name: 'graphql' 68 | parent: graphqlApi 69 | properties: { 70 | contentType: 'application/vnd.ms-azure-apim.graphql.schema' 71 | document: { 72 | value: schema 73 | } 74 | } 75 | } 76 | 77 | resource graphqlPolicy 'Microsoft.ApiManagement/service/apis/policies@2022-08-01' = { 78 | name: 'policy' 79 | parent: graphqlApi 80 | properties: { 81 | format: 'rawxml' 82 | value: realPolicy 83 | } 84 | dependsOn: [ 85 | apiNamedValues 86 | ] 87 | } 88 | 89 | module graphqlResolver 'synthetic-graphql-resolver.bicep' = [for item in resolvers: { 90 | name: 'graphql-resolver-${item.name}' 91 | params: { 92 | apimServiceName: apimServiceName 93 | graphqlApiName: graphqlApi.name 94 | resolverName: item.name 95 | schemaType: item.type 96 | schemaField: item.field 97 | resolverPolicy: item.policy 98 | } 99 | dependsOn: [ apiNamedValues, graphqlSchema, graphqlPolicy ] 100 | }] 101 | 102 | resource diagnosticsPolicy 'Microsoft.ApiManagement/service/apis/diagnostics@2022-08-01' = if (!empty(apimLoggerName)) { 103 | name: 'applicationinsights' 104 | parent: graphqlApi 105 | properties: { 106 | alwaysLog: 'allErrors' 107 | httpCorrelationProtocol: 'W3C' 108 | logClientIp: true 109 | loggerId: apimLogger.id 110 | metrics: true 111 | verbosity: 'verbose' 112 | sampling: { 113 | samplingType: 'fixed' 114 | percentage: 100 115 | } 116 | frontend: { 117 | request: logSettings 118 | response: logSettings 119 | } 120 | backend: { 121 | request: logSettings 122 | response: logSettings 123 | } 124 | } 125 | } 126 | 127 | output serviceUrl string = '${apimService.properties.gatewayUrl}/${path}' 128 | -------------------------------------------------------------------------------- /infra/core/gateway/synthetic-graphql-resolver.bicep: -------------------------------------------------------------------------------- 1 | param apimServiceName string 2 | param graphqlApiName string 3 | param resolverName string 4 | param schemaType string 5 | param schemaField string 6 | param resolverPolicy string 7 | 8 | resource apimService 'Microsoft.ApiManagement/service@2022-08-01' existing = { 9 | name: apimServiceName 10 | } 11 | 12 | resource graphqlApi 'Microsoft.ApiManagement/service/apis@2022-08-01' existing = { 13 | name: graphqlApiName 14 | parent: apimService 15 | } 16 | 17 | resource graphqlResolver 'Microsoft.ApiManagement/service/apis/resolvers@2022-08-01' = { 18 | name: resolverName 19 | parent: graphqlApi 20 | properties: { 21 | displayName: resolverName 22 | path: '${schemaType}/${schemaField}' 23 | description: 'GraphQL Resolver for ${schemaType}/${schemaField}' 24 | } 25 | } 26 | 27 | resource graphqlResolverPolicy 'Microsoft.ApiManagement/service/apis/resolvers/policies@2022-08-01' = { 28 | name: 'policy' 29 | parent: graphqlResolver 30 | properties: { 31 | format: 'rawxml' 32 | value: resolverPolicy 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /infra/core/host/appservice.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param applicationInsightsName string = '' 6 | param appServicePlanId string 7 | 8 | @allowed(['dotnet', 'dotnetcore', 'dotnet-isolated', 'node']) 9 | param runtimeName string 10 | param runtimeVersion string 11 | param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' 12 | param kind string = 'app,linux' 13 | param useManagedIdentity bool = true 14 | 15 | // Microsoft.Web/sites/config 16 | param allowedOrigins array = [] 17 | param alwaysOn bool = true 18 | param appCommandLine string = '' 19 | param appSettings object = {} 20 | param connectionStrings object = {} 21 | param clientAffinityEnabled bool = false 22 | param enableOryxBuild bool = contains(kind, 'linux') 23 | param functionAppScaleLimit int = -1 24 | param linuxFxVersion string = runtimeNameAndVersion 25 | param minimumElasticInstanceCount int = -1 26 | param numberOfWorkers int = -1 27 | param scmDoBuildDuringDeployment bool = false 28 | param use32BitWorkerProcess bool = false 29 | 30 | var portalOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ] 31 | 32 | resource appService 'Microsoft.Web/sites@2022-03-01' = { 33 | name: name 34 | location: location 35 | tags: tags 36 | kind: kind 37 | properties: { 38 | serverFarmId: appServicePlanId 39 | siteConfig: { 40 | linuxFxVersion: linuxFxVersion 41 | alwaysOn: alwaysOn 42 | ftpsState: 'FtpsOnly' 43 | appCommandLine: appCommandLine 44 | numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null 45 | minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null 46 | use32BitWorkerProcess: use32BitWorkerProcess 47 | functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null 48 | cors: { 49 | allowedOrigins: union(portalOrigins, allowedOrigins) 50 | } 51 | } 52 | clientAffinityEnabled: clientAffinityEnabled 53 | httpsOnly: true 54 | } 55 | identity: { type: useManagedIdentity ? 'SystemAssigned' : 'None' } 56 | 57 | resource configAppSettings 'config' = { 58 | name: 'appsettings' 59 | properties: union(appSettings, 60 | { 61 | SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) 62 | ENABLE_ORYX_BUILD: string(enableOryxBuild) 63 | }, 64 | !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}) 65 | } 66 | 67 | resource configConnectionStrings 'config' = { 68 | name: 'connectionstrings' 69 | properties: connectionStrings 70 | } 71 | 72 | resource configLogs 'config' = { 73 | name: 'logs' 74 | properties: { 75 | applicationLogs: { fileSystem: { level: 'Verbose' } } 76 | detailedErrorMessages: { enabled: true } 77 | failedRequestsTracing: { enabled: true } 78 | httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } 79 | } 80 | dependsOn: [ 81 | configAppSettings 82 | ] 83 | } 84 | } 85 | 86 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { 87 | name: applicationInsightsName 88 | } 89 | 90 | output name string = appService.name 91 | output uri string = 'https://${appService.properties.defaultHostName}' 92 | output servicePrincipalId string = useManagedIdentity ? appService.identity.principalId : '' 93 | -------------------------------------------------------------------------------- /infra/core/host/appserviceplan.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param kind string = '' 6 | param reserved bool = true 7 | param sku object 8 | 9 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { 10 | name: name 11 | location: location 12 | tags: tags 13 | sku: sku 14 | kind: kind 15 | properties: { 16 | reserved: reserved 17 | } 18 | } 19 | 20 | output id string = appServicePlan.id 21 | -------------------------------------------------------------------------------- /infra/core/host/staticwebapp.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param sku object = { 6 | name: 'Free' 7 | tier: 'Free' 8 | } 9 | 10 | resource web 'Microsoft.Web/staticSites@2022-03-01' = { 11 | name: name 12 | location: location 13 | tags: tags 14 | sku: sku 15 | properties: { 16 | provider: 'Custom' 17 | } 18 | } 19 | 20 | output name string = web.name 21 | output uri string = 'https://${web.properties.defaultHostname}' 22 | -------------------------------------------------------------------------------- /infra/core/monitor/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param dashboardName string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param logAnalyticsWorkspaceId string 7 | 8 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | kind: 'web' 13 | properties: { 14 | Application_Type: 'web' 15 | WorkspaceResourceId: logAnalyticsWorkspaceId 16 | } 17 | } 18 | 19 | module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = { 20 | name: 'application-insights-dashboard' 21 | params: { 22 | name: dashboardName 23 | location: location 24 | applicationInsightsName: applicationInsights.name 25 | } 26 | } 27 | 28 | output connectionString string = applicationInsights.properties.ConnectionString 29 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 30 | output name string = applicationInsights.name 31 | -------------------------------------------------------------------------------- /infra/core/monitor/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 6 | name: name 7 | location: location 8 | tags: tags 9 | properties: any({ 10 | retentionInDays: 30 11 | features: { 12 | searchVersion: 1 13 | } 14 | sku: { 15 | name: 'PerGB2018' 16 | } 17 | }) 18 | } 19 | 20 | output id string = logAnalytics.id 21 | output name string = logAnalytics.name 22 | -------------------------------------------------------------------------------- /infra/core/monitor/monitoring.bicep: -------------------------------------------------------------------------------- 1 | param logAnalyticsName string 2 | param applicationInsightsName string 3 | param applicationInsightsDashboardName string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | module logAnalytics 'loganalytics.bicep' = { 8 | name: 'loganalytics' 9 | params: { 10 | name: logAnalyticsName 11 | location: location 12 | tags: tags 13 | } 14 | } 15 | 16 | module applicationInsights 'applicationinsights.bicep' = { 17 | name: 'applicationinsights' 18 | params: { 19 | name: applicationInsightsName 20 | location: location 21 | tags: tags 22 | dashboardName: applicationInsightsDashboardName 23 | logAnalyticsWorkspaceId: logAnalytics.outputs.id 24 | } 25 | } 26 | 27 | output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString 28 | output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey 29 | output applicationInsightsName string = applicationInsights.outputs.name 30 | output logAnalyticsWorkspaceId string = logAnalytics.outputs.id 31 | output logAnalyticsWorkspaceName string = logAnalytics.outputs.name 32 | -------------------------------------------------------------------------------- /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 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "sqlAdminPassword": { 12 | "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} sqlAdminPassword)" 13 | }, 14 | "appInsightsLocationName": { 15 | "value": "southcentralus" 16 | }, 17 | "staticSitesLocationName": { 18 | "value": "centralus" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsRestApi/policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Accept 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/film-all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/film/{context.GraphQL.Parent["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/person-all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/person/{context.GraphQL.Parent["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/planet-all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/planet/{context.GraphQL.Parent["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-characters.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/person") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-films.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/film") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getcharacterbyid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/person/{context.GraphQL.Arguments["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getfilmbyid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/film/{context.GraphQL.Arguments["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getplanetbyid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/planet/{context.GraphQL.Arguments["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getspeciesbyid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/species/{context.GraphQL.Arguments["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getstarshipbyid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/starship/{context.GraphQL.Arguments["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-getvehiclebyid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/vehicle/{context.GraphQL.Arguments["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-planets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/planet") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-species.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/species") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-starships.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/starship") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/query-vehicles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/vehicle") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/species-all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/species/{context.GraphQL.Parent["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/starship-all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/starship/{context.GraphQL.Parent["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/StarWarsSynGQLApi/resolvers/vehicle-all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET 4 | @($"{{starwarsapi}}/vehicle/{context.GraphQL.Parent["id"]}") 5 | 6 | -------------------------------------------------------------------------------- /src/ApiManagement/TodoGraphQLApi/policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ApiManagement/TodoRestApi/policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | * 6 | 7 | 8 | GET 9 | POST 10 | PUT 11 | DELETE 12 | 13 | 14 |
*
15 |
16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
-------------------------------------------------------------------------------- /src/StarWars.Data/Models/BaseModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json.Serialization; 5 | 6 | namespace StarWars.Data.Models; 7 | 8 | /// 9 | /// The common properties for all the models. 10 | /// 11 | public abstract class BaseModel 12 | { 13 | /// 14 | /// Creates a new model, based on a type and ID. 15 | /// 16 | /// The type of the model. 17 | /// The ID of the model. 18 | protected BaseModel(string modelType, int id) 19 | { 20 | ModelType = modelType; 21 | Id = id; 22 | } 23 | 24 | /// 25 | /// The model type (film, character, etc.) 26 | /// 27 | [JsonIgnore] 28 | public string ModelType { get; } 29 | 30 | /// 31 | /// The ID of the model. 32 | /// 33 | [JsonPropertyName("id")] 34 | public int Id { get; } 35 | } 36 | -------------------------------------------------------------------------------- /src/StarWars.Data/Models/BaseVehicle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using StarWars.Data.Serialization; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace StarWars.Data.Models; 8 | 9 | /// 10 | /// A base set of features for a vehicle. 11 | /// 12 | public abstract class BaseVehicle : BaseModel 13 | { 14 | /// 15 | /// Creates a new object. 16 | /// 17 | /// The model type. 18 | /// The ID of the vehicle. 19 | /// The name of the vehicle. 20 | /// The model of the vehicle. 21 | /// The manufacturer of the vehicle. 22 | /// The maximum length of time that this starship can provide consumables for its entire crew without having to resupply. 23 | protected BaseVehicle(string modelType, int id, string name, string model, string manufacturer, string consumables) 24 | : base(modelType, id) 25 | { 26 | Name = name; 27 | Model = model; 28 | Manufacturer = manufacturer; 29 | Consumables = consumables; 30 | } 31 | 32 | /// 33 | /// The name of the vehicle. 34 | /// 35 | [JsonPropertyName("name")] 36 | public string Name { get; set; } 37 | 38 | /// 39 | /// The model or official name of this vehicle. 40 | /// 41 | [JsonPropertyName("model")] 42 | public string Model { get; set; } 43 | 44 | /// 45 | /// The manufacturer of this vehicle. 46 | /// 47 | [JsonPropertyName("manufacturer")] 48 | public string Manufacturer { get; set; } 49 | 50 | /// 51 | /// The cost of this starship new, in galactic credits. 52 | /// 53 | [JsonPropertyName("cost_in_credits")] 54 | public long? CostInCredits { get; set; } 55 | 56 | /// 57 | /// The length of this starship in meters. 58 | /// 59 | [JsonPropertyName("length")] 60 | public double? Length { get; set; } 61 | 62 | /// 63 | /// The maximum speed of this starship in atmosphere. n/a if this starship is incapable of atmosphering flight. 64 | /// 65 | [JsonPropertyName("max_atmosphering_speed")] 66 | public int? MaxAtmospheringSpeed { get; set; } 67 | 68 | /// 69 | /// The number of personnel needed to run or pilot this starship. 70 | /// 71 | [JsonPropertyName("crew")] 72 | public int? Crew { get; set; } 73 | 74 | /// 75 | /// The number of non-essential people this starship can transport. 76 | /// 77 | [JsonPropertyName("passengers")] 78 | public int? Passengers { get; set; } 79 | 80 | /// 81 | /// The maximum number of kilograms that this starship can transport. 82 | /// 83 | [JsonPropertyName("cargo_capacity")] 84 | public long? CargoCapacity { get; set; } 85 | 86 | /// 87 | /// The maximum length of time that this starship can provide consumables for its entire crew without having to resupply. 88 | /// 89 | [JsonPropertyName("consumables")] 90 | public string Consumables { get; set; } 91 | 92 | /// 93 | /// A list of Person URL Resources that this starship has been piloted by. 94 | /// 95 | [JsonPropertyName("pilots")] 96 | [JsonConverter(typeof(PersonListConverter))] 97 | public IList Pilots { get; set; } = new List(); 98 | 99 | /// 100 | /// A list of Film resources that this starship has appeared in. 101 | /// 102 | [JsonPropertyName("films")] 103 | [JsonConverter(typeof(FilmListConverter))] 104 | public IList Films { get; set; } = new List(); 105 | } 106 | -------------------------------------------------------------------------------- /src/StarWars.Data/Models/Film.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using StarWars.Data.Serialization; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace StarWars.Data.Models; 8 | 9 | /// 10 | /// A Star Wars Film 11 | /// 12 | public class Film : BaseModel, IEquatable 13 | { 14 | private const string modelType = "film"; 15 | 16 | /// 17 | /// Creates a complete episode with all information. 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | public Film(int episodeId, string title, string openingCrawl, string director, string producer, DateOnly releaseDate) : base(modelType, episodeId) 26 | { 27 | Title = title; 28 | OpeningCrawl = openingCrawl; 29 | Director = director; 30 | Producer = producer; 31 | ReleaseDate = releaseDate; 32 | } 33 | 34 | /// 35 | /// The title of this film. 36 | /// 37 | [JsonPropertyName("title")] 38 | public string Title { get; set; } = string.Empty; 39 | 40 | /// 41 | /// The opening crawl text at the beginning of this film. 42 | /// 43 | [JsonPropertyName("opening_crawl")] 44 | public string OpeningCrawl { get; set; } = string.Empty; 45 | 46 | /// 47 | /// The director of this film. 48 | /// 49 | [JsonPropertyName("director")] 50 | public string Director { get; set; } = string.Empty; 51 | 52 | /// 53 | /// The producer(s) of this film. 54 | /// 55 | [JsonPropertyName("producer")] 56 | public string Producer { get; set; } = string.Empty; 57 | 58 | /// 59 | /// The release date at original creator country. 60 | /// 61 | [JsonPropertyName("release_date")] 62 | public DateOnly ReleaseDate { get; set; } = DateOnly.MinValue; 63 | 64 | /// 65 | /// The list of characters features within this film. 66 | /// 67 | [JsonPropertyName("characters")] 68 | [JsonConverter(typeof(PersonListConverter))] 69 | public IList Characters { get; set; } = new List(); 70 | 71 | /// 72 | /// The list of planets featured within this film. 73 | /// 74 | [JsonPropertyName("planets")] 75 | [JsonConverter(typeof(PlanetListConverter))] 76 | public IList Planets { get; set; } = new List(); 77 | 78 | /// 79 | /// The list of species featured within this film. 80 | /// 81 | [JsonPropertyName("species")] 82 | [JsonConverter(typeof(SpeciesListConverter))] 83 | public IList Species { get; set; } = new List(); 84 | 85 | /// 86 | /// The list of starships featured within this film. 87 | /// 88 | [JsonPropertyName("starships")] 89 | [JsonConverter(typeof(StarshipListConverter))] 90 | public IList Starships { get; set; } = new List(); 91 | 92 | /// 93 | /// The list of vehicles featured within this film. 94 | /// 95 | [JsonPropertyName("vehicles")] 96 | [JsonConverter(typeof(VehicleListConverter))] 97 | public IList Vehicles { get; set; } = new List(); 98 | 99 | #region IEquatable 100 | /// 101 | /// Indicates whether the current object is equal to another object of the same type. 102 | /// 103 | /// An object to compare with this object. 104 | /// true if the current object is equal to the parameter; otherwise, false. 105 | public bool Equals(Film? other) 106 | => other != null && other.Id == Id; 107 | 108 | /// 109 | /// Indicates whether the current object is equal to another object of the same type. 110 | /// 111 | /// An object to compare with this object. 112 | /// true if the current object is equal to the parameter; otherwise, false. 113 | public override bool Equals(object? obj) 114 | => obj is Film other && Equals(other); 115 | 116 | /// 117 | /// Returns the hash code for this instance. 118 | /// 119 | /// The hash code for this instance. 120 | public override int GetHashCode() 121 | => HashCode.Combine(Id, Title); 122 | #endregion 123 | } 124 | -------------------------------------------------------------------------------- /src/StarWars.Data/Models/Species.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using StarWars.Data.Serialization; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace StarWars.Data.Models; 8 | 9 | /// 10 | /// A species within the Star Wars Universe 11 | /// 12 | public class Species : BaseModel 13 | { 14 | private const string modelType = "species"; 15 | 16 | /// 17 | /// Creates a new record. 18 | /// 19 | /// The ID of the species. 20 | /// The name of this species. 21 | /// The classification of this species. 22 | /// The designation of this species. 23 | /// The average height of this person in centimeters. 24 | /// The average lifespan of this species in years. 25 | /// A comma-seperated string of common hair colors for this species, none if this species does not typically have hair. 26 | /// A comma-seperated string of common skin colors for this species 27 | /// A comma-seperated string of common eye colors for this species 28 | /// The language commonly spoken by this species. 29 | public Species(int speciesId, string name, string classification, string designation, string averageHeight, string averageLifespan, string hairColors, string skinColors, string eyeColors, string language) 30 | : base(modelType, speciesId) 31 | { 32 | Name = name; 33 | Classification = classification; 34 | Designation = designation; 35 | AverageHeight = averageHeight; 36 | AverageLifespan = averageLifespan; 37 | HairColors = hairColors; 38 | SkinColors = skinColors; 39 | EyeColors = eyeColors; 40 | Language = language; 41 | } 42 | 43 | /// 44 | /// The name of this species. 45 | /// 46 | [JsonPropertyName("name")] 47 | public string Name { get; set; } 48 | 49 | /// 50 | /// The classification of this species. 51 | /// 52 | [JsonPropertyName("classification")] 53 | public string Classification { get; set; } 54 | 55 | /// 56 | /// The designation of this species. 57 | /// 58 | [JsonPropertyName("designation")] 59 | public string Designation { get; set; } 60 | 61 | /// 62 | /// The average height of this species in centimeters. 63 | /// 64 | [JsonPropertyName("average_height")] 65 | public string AverageHeight { get; set; } 66 | 67 | /// 68 | /// The average lifespan of this species in years 69 | /// 70 | [JsonPropertyName("average_lifespan")] 71 | public string AverageLifespan { get; set; } 72 | 73 | /// 74 | /// A comma-separated string of common hair colors for this species. 75 | /// 76 | [JsonPropertyName("hair_colors")] 77 | public string HairColors { get; set; } 78 | 79 | /// 80 | /// A comma-separated string of common skin colors for this species. 81 | /// 82 | [JsonPropertyName("skin_colors")] 83 | public string SkinColors { get; set; } 84 | 85 | /// 86 | /// A comma-separated string of common eye colors for this species. 87 | /// 88 | [JsonPropertyName("eye_colors")] 89 | public string EyeColors { get; set; } 90 | 91 | /// 92 | /// The language spoken by this species. 93 | /// 94 | [JsonPropertyName("language")] 95 | public string Language { get; set; } 96 | 97 | /// 98 | /// The URL of the planet resource that this species inhavits. 99 | /// 100 | [JsonPropertyName("homeworld")] 101 | [JsonConverter(typeof(PlanetConverter))] 102 | public Planet? Homeworld { get; set; } 103 | 104 | /// 105 | /// The list of film resources that this person has been in. 106 | /// 107 | [JsonPropertyName("films")] 108 | [JsonConverter(typeof(FilmListConverter))] 109 | public IList Films { get; set; } = new List(); 110 | 111 | #region IEquatable 112 | /// 113 | /// Indicates whether the current object is equal to another object of the same type. 114 | /// 115 | /// An object to compare with this object. 116 | /// true if the current object is equal to the parameter; otherwise, false. 117 | public bool Equals(Species? other) 118 | => other != null && other.Id == Id; 119 | 120 | /// 121 | /// Indicates whether the current object is equal to another object of the same type. 122 | /// 123 | /// An object to compare with this object. 124 | /// true if the current object is equal to the parameter; otherwise, false. 125 | public override bool Equals(object? obj) 126 | => obj is Species other && Equals(other); 127 | 128 | /// 129 | /// Returns the hash code for this instance. 130 | /// 131 | /// The hash code for this instance. 132 | public override int GetHashCode() 133 | => HashCode.Combine(Id, Name); 134 | #endregion 135 | } 136 | -------------------------------------------------------------------------------- /src/StarWars.Data/Models/Starship.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json.Serialization; 5 | 6 | namespace StarWars.Data.Models; 7 | 8 | public class Starship : BaseVehicle, IEquatable 9 | { 10 | private const string modelType = "starship"; 11 | 12 | /// 13 | /// Create a new record. 14 | /// 15 | /// The ID of the starship. 16 | /// The name of the starship. 17 | /// The class of this starship. 18 | /// The model or official name of this starship. 19 | /// The manufacturer of this starship. Comma seperated if more than one. 20 | /// The length of this starship in meters. 21 | /// The maximum length of time that this starship can provide consumables for its entire crew without having to resupply. 22 | public Starship(int starshipId, string name, string starshipClass, string model, string manufacturer, double length, string consumables) 23 | : base(modelType, starshipId, name, model, manufacturer, consumables) 24 | { 25 | Length = length; 26 | StarshipClass = starshipClass; 27 | } 28 | 29 | /// 30 | /// Create a new record. 31 | /// 32 | /// The ID of the starship. 33 | /// The name of the starship. 34 | /// The class of this starship. 35 | /// The model or official name of this starship. 36 | /// The manufacturer of this starship. Comma seperated if more than one. 37 | /// The cost of this starship new, in galactic credits. 38 | /// The length of this starship in meters. 39 | /// The maximum speed of this starship in atmosphere. n/a if this starship is incapable of atmosphering flight. 40 | /// The number of personnel needed to run or pilot this starship. 41 | /// The number of non-essential people this starship can transport. 42 | /// The maximum number of kilograms that this starship can transport. 43 | /// The maximum length of time that this starship can provide consumables for its entire crew without having to resupply. 44 | /// The class of this starships hyperdrive. 45 | /// The Maximum number of Megalights this starship can travel in a standard hour. 46 | public Starship(int starshipId, string name, string starshipClass, string model, string manufacturer, long? cost, double length, 47 | int? speed, int? crew, int? passengers, long? cargoCapacity, string consumables, double? hyperdriveRating, int? mglt) 48 | : this(starshipId, name, starshipClass, model, manufacturer, length, consumables) 49 | { 50 | CostInCredits = cost; 51 | MaxAtmospheringSpeed = speed; 52 | Crew = crew; 53 | Passengers = passengers; 54 | CargoCapacity = cargoCapacity; 55 | HyperdriveRating = hyperdriveRating; 56 | MegalightsPerHour = mglt; 57 | } 58 | 59 | /// 60 | /// The class of this starship, such as Starfighter or Deep Space Mobile Battlestation. 61 | /// 62 | [JsonPropertyName("starship_class")] 63 | public string StarshipClass { get; set; } 64 | 65 | /// 66 | /// The class of this starships hyperdrive. 67 | /// 68 | [JsonPropertyName("hyperdrive_rating")] 69 | public double? HyperdriveRating { get; set; } 70 | 71 | /// 72 | /// The Maximum number of Megalights this starship can travel in a standard hour. A Megalight is a standard unit of distance and 73 | /// has never been defined before within the Star Wars universe. This figure is only really useful for measuring the difference 74 | /// in speed of starships. We can assume it is similar to AU, the distance between our Sun (Sol) and Earth. 75 | /// 76 | [JsonPropertyName("MGLT")] 77 | public int? MegalightsPerHour { get; set; } 78 | 79 | #region IEquatable 80 | /// 81 | /// Indicates whether the current object is equal to another object of the same type. 82 | /// 83 | /// An object to compare with this object. 84 | /// true if the current object is equal to the parameter; otherwise, false. 85 | public bool Equals(Starship? other) 86 | => other != null && other.Id == Id; 87 | 88 | /// 89 | /// Indicates whether the current object is equal to another object of the same type. 90 | /// 91 | /// An object to compare with this object. 92 | /// true if the current object is equal to the parameter; otherwise, false. 93 | public override bool Equals(object? obj) 94 | => obj is Starship other && Equals(other); 95 | 96 | /// 97 | /// Returns the hash code for this instance. 98 | /// 99 | /// The hash code for this instance. 100 | public override int GetHashCode() 101 | => HashCode.Combine(Id, Name); 102 | #endregion 103 | } 104 | -------------------------------------------------------------------------------- /src/StarWars.Data/Models/Vehicle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json.Serialization; 5 | 6 | namespace StarWars.Data.Models; 7 | 8 | /// 9 | /// A vehicle. 10 | /// 11 | public class Vehicle : BaseVehicle, IEquatable 12 | { 13 | private const string modelType = "vehicle"; 14 | 15 | /// 16 | /// Creates a new object. 17 | /// 18 | /// The ID of the vehicle. 19 | /// The name of the vehicle. 20 | /// The model of the vehicle. 21 | /// The class of this vehicle. 22 | /// The manufacturer of the vehicle. 23 | /// The maximum length of time that this starship can provide consumables for its entire crew without having to resupply. 24 | public Vehicle(int vehicleId, string name, string model, string vehicleClass, string manufacturer, string consumables) 25 | : base(modelType, vehicleId, name, model, manufacturer, consumables) 26 | { 27 | VehicleClass = vehicleClass; 28 | } 29 | 30 | /// 31 | /// Creates a new object. 32 | /// 33 | /// The ID of the vehicle. 34 | /// The name of the vehicle. 35 | /// The model of the vehicle. 36 | /// The class of this vehicle. 37 | /// The manufacturer of the vehicle. 38 | /// The cost of this starship new, in galactic credits. 39 | /// The length of this starship in meters. 40 | /// The number of personnel needed to run or pilot this starship. 41 | /// The number of non-essential people this starship can transport. 42 | /// The maximum speed of this starship in atmosphere. n/a if this starship is incapable of atmosphering flight. 43 | /// The maximum number of kilograms that this starship can transport. 44 | /// The maximum length of time that this starship can provide consumables for its entire crew without having to resupply. 45 | public Vehicle(int vehicleId, string name, string model, string vehicleClass, string manufacturer, long? cost, double? length, int? crew, int? passengers, int? speed, long? cargoCapacity, string consumables) 46 | : this(vehicleId, name, model, vehicleClass, manufacturer, consumables) 47 | { 48 | CostInCredits = cost; 49 | Length = length; 50 | Crew = crew; 51 | Passengers = passengers; 52 | MaxAtmospheringSpeed = speed; 53 | CargoCapacity = cargoCapacity; 54 | } 55 | 56 | /// 57 | /// The class of this vehicle, such as Wheeled. 58 | /// 59 | [JsonPropertyName("vehicle_class")] 60 | public string VehicleClass { get; set; } 61 | 62 | #region IEquatable 63 | /// 64 | /// Indicates whether the current object is equal to another object of the same type. 65 | /// 66 | /// An object to compare with this object. 67 | /// true if the current object is equal to the parameter; otherwise, false. 68 | public bool Equals(Vehicle? other) 69 | => other != null && other.Id == Id; 70 | 71 | /// 72 | /// Indicates whether the current object is equal to another object of the same type. 73 | /// 74 | /// An object to compare with this object. 75 | /// true if the current object is equal to the parameter; otherwise, false. 76 | public override bool Equals(object? obj) 77 | => obj is Vehicle other && Equals(other); 78 | 79 | /// 80 | /// Returns the hash code for this instance. 81 | /// 82 | /// The hash code for this instance. 83 | public override int GetHashCode() 84 | => HashCode.Combine(Id, Name); 85 | #endregion 86 | } -------------------------------------------------------------------------------- /src/StarWars.Data/Serialization/DataModelSerializers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using StarWars.Data.Models; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | 8 | namespace StarWars.Data.Serialization; 9 | 10 | public static class DataModelHelpers 11 | { 12 | public static void WriteBaseModelValue(this Utf8JsonWriter writer, BaseModel value, JsonSerializerOptions options) 13 | { 14 | writer.WriteStartObject(); 15 | writer.WritePropertyName("id"); 16 | writer.WriteNumberValue(value.Id); 17 | writer.WriteEndObject(); 18 | } 19 | 20 | public static void WriteBaseModelArray(this Utf8JsonWriter writer, IEnumerable value, JsonSerializerOptions options) 21 | { 22 | writer.WriteStartArray(); 23 | foreach (var item in value) 24 | { 25 | writer.WriteBaseModelValue(item, options); 26 | } 27 | writer.WriteEndArray(); 28 | } 29 | } 30 | 31 | public class FilmListConverter : JsonConverter> 32 | { 33 | public override IList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 34 | => throw new NotImplementedException(); 35 | 36 | public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) 37 | => writer.WriteBaseModelArray(value, options); 38 | } 39 | 40 | public class PersonListConverter : JsonConverter> 41 | { 42 | public override IList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 43 | => throw new NotImplementedException(); 44 | 45 | public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) 46 | => writer.WriteBaseModelArray(value, options); 47 | } 48 | 49 | public class PlanetConverter : JsonConverter 50 | { 51 | public override Planet? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 52 | => throw new NotImplementedException(); 53 | 54 | public override void Write(Utf8JsonWriter writer, Planet value, JsonSerializerOptions options) 55 | => writer.WriteBaseModelValue(value, options); 56 | } 57 | 58 | public class PlanetListConverter : JsonConverter> 59 | { 60 | public override IList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 61 | => throw new NotImplementedException(); 62 | 63 | public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) 64 | => writer.WriteBaseModelArray(value, options); 65 | } 66 | 67 | public class SpeciesConverter : JsonConverter 68 | { 69 | public override Species? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 70 | => throw new NotImplementedException(); 71 | 72 | public override void Write(Utf8JsonWriter writer, Species value, JsonSerializerOptions options) 73 | => writer.WriteBaseModelValue(value, options); 74 | } 75 | 76 | public class SpeciesListConverter : JsonConverter> 77 | { 78 | public override IList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 79 | => throw new NotImplementedException(); 80 | 81 | public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) 82 | => writer.WriteBaseModelArray(value, options); 83 | } 84 | 85 | public class StarshipListConverter : JsonConverter> 86 | { 87 | public override IList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 88 | => throw new NotImplementedException(); 89 | 90 | public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) 91 | => writer.WriteBaseModelArray(value, options); 92 | } 93 | 94 | public class VehicleListConverter : JsonConverter> 95 | { 96 | public override IList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 97 | => throw new NotImplementedException(); 98 | 99 | public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) 100 | => writer.WriteBaseModelArray(value, options); 101 | } 102 | 103 | -------------------------------------------------------------------------------- /src/StarWars.Data/StarWars.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/Controllers/FilmController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using StarWars.Data; 6 | using StarWars.Data.Models; 7 | 8 | namespace StarWars.RestApi.Controllers; 9 | 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class FilmController : ControllerBase 13 | { 14 | private readonly StarWarsData datamodel; 15 | 16 | public FilmController(StarWarsData datamodel) 17 | { 18 | this.datamodel = datamodel; 19 | } 20 | 21 | [HttpGet] 22 | [Produces("application/json")] 23 | [ProducesResponseType(200, Type = typeof(IEnumerable))] 24 | public IEnumerable GetAllFilms() 25 | { 26 | return datamodel.Films.Values; 27 | } 28 | 29 | [HttpGet("{id}")] 30 | [Produces("application/json")] 31 | [ProducesResponseType(200, Type=typeof(Film))] 32 | [ProducesResponseType(404)] 33 | public IActionResult GetFilmById([FromRoute] int id) 34 | { 35 | if (datamodel.Films.ContainsKey(id)) 36 | { 37 | return Ok(datamodel.Films[id]); 38 | } 39 | return NotFound(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/Controllers/PersonController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using StarWars.Data; 6 | using StarWars.Data.Models; 7 | 8 | namespace StarWars.RestApi.Controllers; 9 | 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class PersonController : ControllerBase 13 | { 14 | private readonly StarWarsData datamodel; 15 | 16 | public PersonController(StarWarsData datamodel) 17 | { 18 | this.datamodel = datamodel; 19 | } 20 | 21 | [HttpGet] 22 | [Produces("application/json")] 23 | [ProducesResponseType(200, Type = typeof(IEnumerable))] 24 | public IEnumerable Get() 25 | { 26 | return datamodel.People.Values; 27 | } 28 | 29 | [HttpGet("{id}")] 30 | [Produces("application/json")] 31 | [ProducesResponseType(200, Type = typeof(Person))] 32 | [ProducesResponseType(404)] 33 | public IActionResult Get(int id) 34 | { 35 | if (datamodel.People.ContainsKey(id)) 36 | { 37 | return Ok(datamodel.People[id]); 38 | } 39 | return NotFound(); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/Controllers/PlanetController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using StarWars.Data; 6 | using StarWars.Data.Models; 7 | 8 | namespace StarWars.RestApi.Controllers; 9 | 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class PlanetController : ControllerBase 13 | { 14 | private readonly StarWarsData datamodel; 15 | 16 | public PlanetController(StarWarsData datamodel) 17 | { 18 | this.datamodel = datamodel; 19 | } 20 | 21 | [HttpGet] 22 | [Produces("application/json")] 23 | [ProducesResponseType(200, Type = typeof(IEnumerable))] 24 | public IEnumerable GetAllPlanets() 25 | { 26 | return datamodel.Planets.Values; 27 | } 28 | 29 | [HttpGet("{id}")] 30 | [Produces("application/json")] 31 | [ProducesResponseType(200, Type = typeof(Planet))] 32 | [ProducesResponseType(404)] 33 | public IActionResult GetPlanetById([FromRoute] int id) 34 | { 35 | if (datamodel.Planets.ContainsKey(id)) 36 | { 37 | return Ok(datamodel.Planets[id]); 38 | } 39 | return NotFound(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/Controllers/SpeciesController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using StarWars.Data; 6 | using StarWars.Data.Models; 7 | 8 | namespace StarWars.RestApi.Controllers; 9 | 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class SpeciesController : ControllerBase 13 | { 14 | private readonly StarWarsData datamodel; 15 | 16 | public SpeciesController(StarWarsData datamodel) 17 | { 18 | this.datamodel = datamodel; 19 | } 20 | 21 | [HttpGet] 22 | [Produces("application/json")] 23 | [ProducesResponseType(200, Type = typeof(IEnumerable))] 24 | public IEnumerable GetAllSpeciess() 25 | { 26 | return datamodel.Species.Values; 27 | } 28 | 29 | [HttpGet("{id}")] 30 | [Produces("application/json")] 31 | [ProducesResponseType(200, Type = typeof(Species))] 32 | [ProducesResponseType(404)] 33 | public IActionResult GetSpeciesById([FromRoute] int id) 34 | { 35 | if (datamodel.Species.ContainsKey(id)) 36 | { 37 | return Ok(datamodel.Species[id]); 38 | } 39 | return NotFound(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/Controllers/StarshipController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using StarWars.Data; 6 | using StarWars.Data.Models; 7 | 8 | namespace StarWars.RestApi.Controllers; 9 | 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class StarshipController : ControllerBase 13 | { 14 | private readonly StarWarsData datamodel; 15 | 16 | public StarshipController(StarWarsData datamodel) 17 | { 18 | this.datamodel = datamodel; 19 | } 20 | 21 | [HttpGet] 22 | [Produces("application/json")] 23 | [ProducesResponseType(200, Type = typeof(IEnumerable))] 24 | public IEnumerable GetAllStarships() 25 | { 26 | return datamodel.Starships.Values; 27 | } 28 | 29 | [HttpGet("{id}")] 30 | [Produces("application/json")] 31 | [ProducesResponseType(200, Type = typeof(Starship))] 32 | [ProducesResponseType(404)] 33 | public IActionResult GetStarshipById([FromRoute] int id) 34 | { 35 | if (datamodel.Starships.ContainsKey(id)) 36 | { 37 | return Ok(datamodel.Starships[id]); 38 | } 39 | return NotFound(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/Controllers/VehicleController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using StarWars.Data; 6 | using StarWars.Data.Models; 7 | 8 | namespace StarWars.RestApi.Controllers; 9 | 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class VehicleController : ControllerBase 13 | { 14 | private readonly StarWarsData datamodel; 15 | 16 | public VehicleController(StarWarsData datamodel) 17 | { 18 | this.datamodel = datamodel; 19 | } 20 | 21 | [HttpGet] 22 | [Produces("application/json")] 23 | [ProducesResponseType(200, Type = typeof(IEnumerable))] 24 | public IEnumerable GetAllVehicles() 25 | { 26 | return datamodel.Vehicles.Values; 27 | } 28 | 29 | [HttpGet("{id}")] 30 | [Produces("application/json")] 31 | [ProducesResponseType(200, Type = typeof(Vehicle))] 32 | [ProducesResponseType(404)] 33 | public IActionResult GetVehicleById([FromRoute] int id) 34 | { 35 | if (datamodel.Vehicles.ContainsKey(id)) 36 | { 37 | return Ok(datamodel.Vehicles[id]); 38 | } 39 | return NotFound(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using StarWars.Data; 5 | using StarWars.RestApi.Serialization; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | /* 10 | ** Application Insights logging 11 | */ 12 | builder.Services.AddApplicationInsightsTelemetry(); 13 | 14 | /* 15 | ** CORS 16 | */ 17 | builder.Services.AddCors(options => 18 | { 19 | options.AddDefaultPolicy(policy => 20 | { 21 | policy.AllowAnyOrigin(); 22 | policy.AllowAnyHeader(); 23 | policy.AllowAnyMethod(); 24 | }); 25 | }); 26 | 27 | /* 28 | ** Add the data model to the service builder so it can be accessed everywhere. 29 | */ 30 | builder.Services.AddSingleton(); 31 | 32 | /* 33 | ** Add all the controllers to the service builder. 34 | */ 35 | builder.Services.AddHttpContextAccessor(); 36 | builder.Services 37 | .AddControllers() 38 | .AddJsonOptions(options => options.JsonSerializerOptions.AddDateOnlyConverters()); 39 | 40 | /* 41 | ** Add the Swagger generator. 42 | */ 43 | builder.Services.AddEndpointsApiExplorer(); 44 | builder.Services.AddSwaggerGen(); 45 | 46 | /************************************************************************************************ 47 | ** 48 | ** HTTP PIPELINE BUILDER 49 | */ 50 | var app = builder.Build(); 51 | 52 | /* 53 | ** CORS 54 | */ 55 | app.UseCors(); 56 | 57 | /* 58 | ** Add Swagger support. 59 | */ 60 | app.UseSwagger(); 61 | app.UseSwaggerUI(); 62 | 63 | /* 64 | ** Controllers. 65 | */ 66 | app.UseHttpsRedirection(); 67 | app.UseAuthorization(); 68 | app.MapControllers(); 69 | 70 | /************************************************************************************************ 71 | ** 72 | ** Run the application. 73 | */ 74 | app.Run(); 75 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/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:53378", 8 | "sslPort": 44363 9 | } 10 | }, 11 | "profiles": { 12 | "StarWars.RestApi": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7264;http://localhost:5131", 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/StarWars.RestApi/Serialization/DateOnlyJsonConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Text.Json.Serialization; 5 | using System.Text.Json; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace StarWars.RestApi.Serialization; 9 | 10 | /// 11 | /// Serializer / Deserializer for DateOnly. 12 | /// 13 | /// 14 | public class DateOnlyJsonConverter : JsonConverter 15 | { 16 | public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 17 | { 18 | if (reader.TryGetDateTime(out var dt)) 19 | { 20 | return DateOnly.FromDateTime(dt); 21 | } 22 | var value = reader.GetString(); 23 | if (value == null) 24 | { 25 | return default; 26 | } 27 | var match = new Regex("^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)(T|\\s|\\z)").Match(value); 28 | return match.Success ? new DateOnly(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value)) : default; 29 | } 30 | 31 | public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) 32 | => writer.WriteStringValue(value.ToString("yyyy-MM-dd")); 33 | } 34 | 35 | /// 36 | /// Serializer / Deserializer for DateOnly?. 37 | /// 38 | /// 39 | public class NullableDateOnlyJsonConverter : JsonConverter 40 | { 41 | public override DateOnly? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 42 | { 43 | if (reader.TryGetDateTime(out var dt)) 44 | { 45 | return DateOnly.FromDateTime(dt); 46 | } 47 | var value = reader.GetString(); 48 | if (value == null) 49 | { 50 | return default; 51 | } 52 | var match = new Regex("^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)(T|\\s|\\z)").Match(value); 53 | return match.Success 54 | ? new DateOnly(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value)) 55 | : default; 56 | } 57 | 58 | public override void Write(Utf8JsonWriter writer, DateOnly? value, JsonSerializerOptions options) 59 | => writer.WriteStringValue(value?.ToString("yyyy-MM-dd")); 60 | } 61 | 62 | /// 63 | /// Extension methods to add DateOnly converters. 64 | /// 65 | public static class DateOnlyConverterExtensions 66 | { 67 | public static void AddDateOnlyConverters(this JsonSerializerOptions options) 68 | { 69 | options.Converters.Add(new DateOnlyJsonConverter()); 70 | options.Converters.Add(new NullableDateOnlyJsonConverter()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/StarWars.RestApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 22a114c0-4f84-4183-bdef-4937eebf38a5 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/StarWars.RestApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "", 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Todo.Data/IDatabaseInitializer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Todo.Data; 5 | 6 | /// 7 | /// Interface to handle database initialization. 8 | /// 9 | public interface IDatabaseInitializer 10 | { 11 | /// 12 | /// Initializes the database. 13 | /// 14 | /// A to observe. 15 | /// A that resolves when the operation is complete. 16 | Task InitializeDatabaseAsync(CancellationToken cancellationToken = default); 17 | } 18 | -------------------------------------------------------------------------------- /src/Todo.Data/Todo.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Todo.Data/TodoBaseModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Todo.Data; 7 | 8 | public abstract class TodoBaseModel 9 | { 10 | public TodoBaseModel(string name) 11 | { 12 | Name = name; 13 | } 14 | 15 | /// 16 | /// The ID of the entity. 17 | /// 18 | [Key] 19 | public Guid? Id { get; set; } 20 | 21 | /// 22 | /// The name of the entity 23 | /// 24 | public string Name { get; set; } 25 | 26 | /// 27 | /// The description of the entity 28 | /// 29 | public string? Description { get; set; } 30 | 31 | /// 32 | /// The date that the entity was created. 33 | /// 34 | public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.UtcNow; 35 | 36 | /// 37 | /// The date that the entity was last updated. 38 | /// 39 | public DateTimeOffset UpdatedDate { get; set; } = DateTimeOffset.UtcNow; 40 | } 41 | -------------------------------------------------------------------------------- /src/Todo.Data/TodoDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Todo.Data; 7 | 8 | /// 9 | /// The database context for an Azure SQL database. 10 | /// 11 | public class TodoDbContext : DbContext, IDatabaseInitializer 12 | { 13 | /// 14 | /// Creates a new . 15 | /// 16 | /// The database context options. 17 | public TodoDbContext(DbContextOptions options) : base(options) 18 | { 19 | } 20 | 21 | /// 22 | /// Create the model for the database. 23 | /// 24 | /// 25 | protected override void OnModelCreating(ModelBuilder modelBuilder) 26 | { 27 | modelBuilder.ConfigureModel(); 28 | modelBuilder.ConfigureModel(); 29 | 30 | modelBuilder.Entity() 31 | .HasOne(m => m.List) 32 | .WithMany(m => m.Items) 33 | .HasForeignKey(m => m.ListId) 34 | .OnDelete(DeleteBehavior.Cascade); 35 | 36 | base.OnModelCreating(modelBuilder); 37 | } 38 | 39 | /// 40 | /// The data set for the items. 41 | /// 42 | public DbSet TodoItems => Set(); 43 | 44 | /// 45 | /// The data set for the lists. 46 | /// 47 | public DbSet TodoLists => Set(); 48 | 49 | /// 50 | /// Initializes the database. 51 | /// 52 | /// A to observe. 53 | /// A that resolves when the operation is complete. 54 | public async Task InitializeDatabaseAsync(CancellationToken cancellationToken = default) 55 | { 56 | await Database.EnsureCreatedAsync(cancellationToken).ConfigureAwait(false); 57 | } 58 | } 59 | 60 | internal static class ModelBuilderExtensions 61 | { 62 | internal static void ConfigureModel(this ModelBuilder modelBuilder) where T : TodoBaseModel 63 | { 64 | modelBuilder.Entity().HasKey(m => m.Id); 65 | modelBuilder.Entity().Property(m => m.CreatedDate).ValueGeneratedOnAdd(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Todo.Data/TodoItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Todo.Data; 5 | 6 | public enum TodoItemState 7 | { 8 | Todo, 9 | InProgress, 10 | Done 11 | } 12 | 13 | /// 14 | /// The model definition for the TodoItem. 15 | /// 16 | public class TodoItem : TodoBaseModel 17 | { 18 | public TodoItem(Guid listId, string name) : base(name) 19 | { 20 | ListId = listId; 21 | } 22 | 23 | /// 24 | /// The list. 25 | /// 26 | public TodoList? List { get; set; } 27 | 28 | /// 29 | /// The ID of the list. 30 | /// 31 | public Guid ListId { get; set; } 32 | 33 | public TodoItemState State { get; set; } = TodoItemState.Todo; 34 | public DateTimeOffset? DueDate { get; set; } 35 | public DateTimeOffset? CompletedDate { get; set; } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Todo.Data/TodoList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Todo.Data; 5 | 6 | public class TodoList : TodoBaseModel 7 | { 8 | public TodoList(string name) : base(name) 9 | { 10 | } 11 | 12 | /// 13 | /// The list of items in the dataset. 14 | /// 15 | public List? Items { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/DTO/TodoItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Todo.Data; 5 | 6 | namespace Todo.GraphQLApi.GraphQL.DTO; 7 | 8 | public class TodoItem 9 | { 10 | /// 11 | /// The ID of the entity. 12 | /// 13 | [ID] 14 | public Guid Id { get; set; } = Guid.Empty; 15 | 16 | /// 17 | /// The name of the entity 18 | /// 19 | public string Name { get; set; } = string.Empty; 20 | 21 | /// 22 | /// The description of the entity 23 | /// 24 | public string? Description { get; set; } 25 | 26 | /// 27 | /// The date that the entity was created. 28 | /// 29 | public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.UtcNow; 30 | 31 | /// 32 | /// The date that the entity was last updated. 33 | /// 34 | public DateTimeOffset UpdatedDate { get; set; } = DateTimeOffset.UtcNow; 35 | 36 | /// 37 | /// The ID of the list. 38 | /// 39 | [ID] 40 | public Guid ListId { get; set; } 41 | 42 | /// 43 | /// The current state of the item. 44 | /// 45 | public TodoItemState State { get; set; } = TodoItemState.Todo; 46 | 47 | /// 48 | /// The due date for the item. 49 | /// 50 | public DateTimeOffset? DueDate { get; set; } 51 | 52 | /// 53 | /// The completed date for the item. 54 | /// 55 | public DateTimeOffset? CompletedDate { get; set; } 56 | } 57 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/DTO/TodoList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Todo.GraphQLApi.GraphQL.DTO; 5 | 6 | public class TodoList 7 | { 8 | /// 9 | /// The ID of the entity. 10 | /// 11 | [ID] 12 | public Guid Id { get; set; } = Guid.Empty; 13 | 14 | /// 15 | /// The name of the entity 16 | /// 17 | public string Name { get; set; } = string.Empty; 18 | 19 | /// 20 | /// The description of the entity 21 | /// 22 | public string? Description { get; set; } 23 | 24 | /// 25 | /// The date that the entity was created. 26 | /// 27 | public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.UtcNow; 28 | 29 | /// 30 | /// The date that the entity was last updated. 31 | /// 32 | public DateTimeOffset UpdatedDate { get; set; } = DateTimeOffset.UtcNow; 33 | } 34 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/GraphQLExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using HotChocolate.Types.Pagination; 5 | using Todo.GraphQLApi.GraphQL.Services; 6 | 7 | namespace Todo.GraphQLApi.GraphQL; 8 | 9 | public static class GraphQLExtensions 10 | { 11 | /// 12 | /// Adds the appropriate GraphQL Services to the collection. 13 | /// 14 | /// 15 | public static void AddGraphQLService(this IServiceCollection services) 16 | { 17 | var pagingOptions = new PagingOptions { MaxPageSize = 100, DefaultPageSize = 50 }; 18 | 19 | services.AddGraphQLServer() 20 | .RegisterService() 21 | .AddTypes() 22 | .AddMutationConventions() 23 | .AddGlobalObjectIdentification() 24 | .SetPagingOptions(pagingOptions) 25 | .AddFiltering() 26 | .AddSorting(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/InputTypes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Todo.Data; 5 | 6 | namespace Todo.GraphQLApi.GraphQL; 7 | 8 | /// 9 | /// The input type for saving a TodoList. 10 | /// 11 | public class SaveTodoListInput 12 | { 13 | [ID] public Guid? Id { get; set; } 14 | public string Name { get; set; } = string.Empty; 15 | public string? Description { get; set; } 16 | 17 | public TodoList CopyTo(TodoList target) 18 | { 19 | bool isUpdated = false; 20 | 21 | if (target.Name != Name) { isUpdated = true; target.Name = Name; } 22 | if (target.Description != Description) { isUpdated = true; target.Description = Description; } 23 | 24 | if (isUpdated) 25 | target.UpdatedDate = DateTimeOffset.UtcNow; 26 | return target; 27 | } 28 | } 29 | 30 | /// 31 | /// The input type for saving a TodoItem. 32 | /// 33 | public class SaveTodoItemInput 34 | { 35 | [ID] public Guid? Id { get; set; } 36 | [ID] public Guid ListId { get; set; } = Guid.Empty; 37 | public string Name { get; set; } = string.Empty; 38 | public TodoItemState State { get; set; } = TodoItemState.Todo; 39 | public string? Description { get; set; } 40 | public DateTimeOffset? DueDate { get; set; } 41 | public DateTimeOffset? CompletedDate { get; set; } 42 | 43 | public TodoItem CopyTo(TodoItem target) 44 | { 45 | bool isUpdated = false; 46 | 47 | if (target.ListId != ListId) { isUpdated = true; target.ListId = ListId; } 48 | if (target.Name != Name) { isUpdated = true; target.Name = Name; } 49 | if (target.State != State) { isUpdated = true; target.State = State; } 50 | if (target.Description != Description) { isUpdated = true; target.Description = Description; } 51 | if (target.DueDate != DueDate) { isUpdated = true; target.DueDate = DueDate; } 52 | if (target.CompletedDate != CompletedDate) { isUpdated = true; target.CompletedDate = CompletedDate; } 53 | 54 | if (isUpdated) 55 | target.UpdatedDate = DateTimeOffset.UtcNow; 56 | return target; 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using AutoMapper; 5 | 6 | namespace Todo.GraphQLApi.GraphQL; 7 | 8 | /// 9 | /// The profile for mapping the database models 10 | /// to the GraphQL DTOs. 11 | /// 12 | public class MappingProfile : Profile 13 | { 14 | public MappingProfile() 15 | { 16 | CreateMap(); 17 | CreateMap(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/Mutation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Todo.GraphQLApi.GraphQL.Services; 5 | 6 | namespace Todo.GraphQLApi.GraphQL; 7 | 8 | [MutationType] 9 | public static class Mutation 10 | { 11 | [UseMutationConvention(PayloadFieldName = "success")] 12 | public static Task DeleteTodoItemAsync(TodoDataService service, [ID] Guid id, CancellationToken cancellationToken = default) 13 | => service.DeleteTodoItemAsync(id, cancellationToken); 14 | 15 | [UseMutationConvention(PayloadFieldName = "success")] 16 | public static Task DeleteTodoListAsync(TodoDataService service, [ID] Guid id, CancellationToken cancellationToken = default) 17 | => service.DeleteTodoListAsync(id, cancellationToken); 18 | 19 | [Error(typeof(TodoServiceException))] 20 | public static Task SaveTodoItemAsync(TodoDataService service, SaveTodoItemInput input, CancellationToken cancellationToken = default) 21 | => service.SaveTodoItemAsync(input, cancellationToken); 22 | 23 | [Error(typeof(TodoServiceException))] 24 | public static Task SaveTodoListAsync(TodoDataService service, SaveTodoListInput input, CancellationToken cancellationToken = default) 25 | => service.SaveTodoListAsync(input, cancellationToken); 26 | } 27 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/Query.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Todo.GraphQLApi.GraphQL.Services; 5 | 6 | namespace Todo.GraphQLApi.GraphQL; 7 | 8 | [QueryType] 9 | public static class Query 10 | { 11 | [NodeResolver] 12 | public static Task GetTodoItemByIdAsync(TodoDataService service, Guid id, CancellationToken cancellationToken = default) 13 | => service.GetTodoItemByIdAsync(id, cancellationToken); 14 | 15 | [NodeResolver] 16 | public static Task GetTodoListByIdAsync(TodoDataService service, Guid id, CancellationToken cancellationToken = default) 17 | => service.GetTodoListByIdAsync(id, cancellationToken); 18 | 19 | [UsePaging] 20 | [UseFiltering] 21 | [UseSorting] 22 | public static IQueryable GetTodoLists(TodoDataService service) 23 | => service.GetTodoLists(); 24 | } 25 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/Services/TodoServiceException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.Serialization; 5 | 6 | namespace Todo.GraphQLApi.GraphQL.Services; 7 | 8 | /// 9 | /// An exception thrown when there is an error with the . 10 | /// 11 | public class TodoServiceException : Exception 12 | { 13 | public TodoServiceException() 14 | { 15 | } 16 | 17 | public TodoServiceException(string? message) : base(message) 18 | { 19 | } 20 | 21 | public TodoServiceException(string? message, Exception? innerException) : base(message, innerException) 22 | { 23 | } 24 | 25 | protected TodoServiceException(SerializationInfo info, StreamingContext context) : base(info, context) 26 | { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/GraphQL/TodoListNode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Todo.Data; 5 | using Todo.GraphQLApi.GraphQL.Services; 6 | 7 | namespace Todo.GraphQLApi.GraphQL; 8 | 9 | [ExtendObjectType] 10 | public class TodoListNode 11 | { 12 | [UsePaging] 13 | [UseFiltering] 14 | [UseSorting] 15 | public static IQueryable GetItems(TodoDataService service, [Parent] DTO.TodoList list, TodoItemState? state) 16 | => service.GetTodoItems(list.Id, state); 17 | } 18 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Todo.Data; 6 | using Todo.GraphQLApi.GraphQL; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | /****************************************************************************************** 11 | ** 12 | ** Add services to the container 13 | */ 14 | 15 | /* 16 | ** Application Insights logging 17 | */ 18 | builder.Services.AddApplicationInsightsTelemetry(); 19 | 20 | /* 21 | ** CORS 22 | */ 23 | builder.Services.AddCors(options => 24 | { 25 | options.AddDefaultPolicy(policy => 26 | { 27 | policy.AllowAnyOrigin(); 28 | policy.AllowAnyHeader(); 29 | policy.AllowAnyMethod(); 30 | }); 31 | }); 32 | 33 | /* 34 | ** Entity Framework Core Setup. 35 | */ 36 | var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); 37 | if (connectionString == null) 38 | { 39 | throw new ApplicationException("DefaultConnection is not set"); 40 | } 41 | builder.Services.AddPooledDbContextFactory(options => 42 | { 43 | options.UseSqlServer(connectionString); 44 | }); 45 | builder.Services.AddAutoMapper(typeof(MappingProfile)); 46 | 47 | /* 48 | ** GraphQL Services 49 | */ 50 | builder.Services.AddGraphQLService(); 51 | 52 | /****************************************************************************************** 53 | ** 54 | ** Configure the HTTP Pipeline. 55 | */ 56 | var app = builder.Build(); 57 | 58 | /* 59 | ** Database Initialization 60 | */ 61 | using (var scope = app.Services.CreateScope()) 62 | { 63 | var contextFactory = scope.ServiceProvider.GetRequiredService>(); 64 | var context = contextFactory.CreateDbContext(); 65 | if (context is IDatabaseInitializer initializer) 66 | { 67 | await initializer.InitializeDatabaseAsync(); 68 | } 69 | } 70 | 71 | /* 72 | ** CORS 73 | */ 74 | app.UseCors(); 75 | 76 | /* 77 | ** Controllers. 78 | */ 79 | app.UseHttpsRedirection(); 80 | app.MapGraphQL(); 81 | 82 | /* 83 | ** Redirect index page to GraphQL console 84 | */ 85 | app.MapGet("/", (HttpResponse response) => response.Redirect("/graphql")); 86 | 87 | /************************************************************************************************ 88 | ** 89 | ** Run the application. 90 | */ 91 | app.Run(); 92 | 93 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/Properties/ModuleInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using HotChocolate; 5 | 6 | [assembly: Module("Types")] 7 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/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:7118", 8 | "sslPort": 44351 9 | } 10 | }, 11 | "profiles": { 12 | "Todo.GraphQLApi": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "graphql", 17 | "applicationUrl": "https://localhost:7012;http://localhost:5015", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "graphql", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/Todo.GraphQLApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 11.0 8 | 5978435e-39a4-40d7-a56e-81d6e1851cae 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Todo.GraphQLApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=SampleApis;Trusted_Connection=True" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Information", 9 | "Microsoft.AspNetCore": "Warning" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Extensions/ServiceExceptionFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | using Todo.RestApi.Services; 7 | 8 | namespace Todo.RestApi.Extensions; 9 | 10 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 11 | public class ServiceExceptionFilterAttribute : ExceptionFilterAttribute 12 | { 13 | public override void OnException(ExceptionContext context) 14 | { 15 | if (context.Exception is MalformedInputException) 16 | { 17 | context.Result = new BadRequestResult(); 18 | } 19 | 20 | if (context.Exception is EntityExistsException entity_exists) 21 | { 22 | context.Result = entity_exists.Entity != null ? new ConflictObjectResult(entity_exists.Entity) : new ConflictResult(); 23 | } 24 | 25 | if (context.Exception is EntityMissingException) 26 | { 27 | context.Result = new NotFoundResult(); 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Extensions/Utils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.AspNetCore.Http.Extensions; 5 | using System.Web; 6 | using Todo.Data; 7 | using Todo.RestApi.Services; 8 | 9 | namespace Todo.RestApi.Extensions; 10 | 11 | public static class Utils 12 | { 13 | private const int DefaultBatchSize = 20; 14 | private const int MaxBatchSize = 100; 15 | 16 | 17 | /// 18 | /// Gets this request, but without the $skip and $top parameters. 19 | /// 20 | /// 21 | public static string? GetBaseUri(HttpRequest? request) 22 | { 23 | if (request == null) 24 | { 25 | return null; 26 | } 27 | 28 | var baseUri = new UriBuilder(request.GetDisplayUrl()); 29 | if (request.QueryString.HasValue) 30 | { 31 | var query = HttpUtility.ParseQueryString(request.QueryString.Value ?? string.Empty); 32 | query.Remove("$skip"); 33 | query.Remove("$top"); 34 | baseUri.Query = query.ToString(); 35 | } 36 | return baseUri.ToString(); 37 | } 38 | 39 | /// 40 | /// Gets the appropriate batch size. 41 | /// 42 | /// The customer provided batch size. 43 | /// The actual batch size to use. 44 | public static int GetBatchSize(int? batchSize) 45 | { 46 | if (batchSize == null || batchSize < 1) 47 | { 48 | return DefaultBatchSize; 49 | } 50 | else if (batchSize < MaxBatchSize) 51 | { 52 | return (int)batchSize; 53 | } 54 | else 55 | { 56 | return MaxBatchSize; 57 | } 58 | } 59 | 60 | /// 61 | /// Converts a string into a Guid, throwing if the ID is invalid. 62 | /// 63 | /// The ID to transform. 64 | /// The transformed ID. 65 | /// if the ID is malformed. 66 | public static Guid ParseGuid(string id) 67 | { 68 | if (Guid.TryParse(id, out Guid result) && result != Guid.Empty) 69 | { 70 | return result; 71 | } 72 | throw new MalformedInputException($"The ID \'{id}\' is not a valid GUID."); 73 | } 74 | 75 | /// 76 | /// Converts a State string into a TodoItemState. 77 | /// 78 | /// 79 | /// 80 | /// 81 | public static bool ParseState(string? input, out TodoItemState state) 82 | { 83 | if (string.IsNullOrEmpty(input) || input.Equals("todo", StringComparison.OrdinalIgnoreCase)) 84 | { 85 | state = TodoItemState.Todo; 86 | return true; 87 | } 88 | if (input.Equals("inprogress", StringComparison.OrdinalIgnoreCase) || input.Equals("in_progress", StringComparison.OrdinalIgnoreCase)) 89 | { 90 | state = TodoItemState.InProgress; 91 | return true; 92 | } 93 | if (input.Equals("done", StringComparison.OrdinalIgnoreCase)) 94 | { 95 | state = TodoItemState.Done; 96 | return true; 97 | } 98 | state = TodoItemState.Todo; 99 | return false; 100 | } 101 | 102 | /// 103 | /// Determine if skip and top are valid. 104 | /// 105 | /// 106 | /// 107 | /// 108 | public static void ValidatePaging(int? skip, int? batchSize) 109 | { 110 | if (skip.HasValue && skip.Value < 0) 111 | { 112 | throw new MalformedInputException($"The value of '$skip' is invalid."); 113 | } 114 | if (batchSize.HasValue && (batchSize.Value < 1 || batchSize > MaxBatchSize)) 115 | { 116 | throw new MalformedInputException($"The value of '$top' is invalid."); 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Models/CreateUpdateTodoItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Todo.Data; 5 | 6 | namespace Todo.RestApi.Models; 7 | 8 | /// 9 | /// The input record for creating or updating a . 10 | /// 11 | /// The replacement name for the entity. 12 | /// The updated state for the entity. 13 | /// The updated due date for the entity. 14 | /// The updated completion date for the entity. 15 | /// The updated description for the entity. 16 | public record CreateUpdateTodoItem( 17 | string Name, 18 | string? State, 19 | DateTimeOffset? DueDate, 20 | DateTimeOffset? CompletedDate, 21 | string? Description 22 | ); 23 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Models/CreateUpdateTodoList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Todo.RestApi.Models; 5 | 6 | /// 7 | /// The input record for updating or creating a TodoList. 8 | /// 9 | /// 10 | /// 11 | public record CreateUpdateTodoList( 12 | string Name, 13 | string? Description = null 14 | ); -------------------------------------------------------------------------------- /src/Todo.RestApi/Models/Page.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Todo.RestApi.Models; 5 | 6 | /// 7 | /// A type representing a page of items. 8 | /// 9 | /// The type of the items. 10 | public class Page 11 | { 12 | public Page(IEnumerable? items, bool hasMoreItems, Uri? nextLink) 13 | { 14 | Items = items; 15 | HasMoreItems = hasMoreItems; 16 | NextLink = HasMoreItems ? nextLink : null; 17 | } 18 | 19 | /// 20 | /// The list of items in this result. 21 | /// 22 | public IEnumerable? Items { get; set; } 23 | 24 | /// 25 | /// If true, more items can be retrieved. 26 | /// 27 | public bool HasMoreItems { get; set; } 28 | 29 | /// 30 | /// If there are more items, then the link to the next page of items. 31 | /// 32 | public Uri? NextLink { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | using Todo.Data; 8 | using Todo.RestApi.Services; 9 | 10 | var builder = WebApplication.CreateBuilder(args); 11 | 12 | /****************************************************************************************** 13 | ** 14 | ** Add services to the container 15 | */ 16 | 17 | /* 18 | ** Application Insights logging 19 | */ 20 | builder.Services.AddApplicationInsightsTelemetry(); 21 | 22 | /* 23 | ** CORS 24 | */ 25 | builder.Services.AddCors(options => 26 | { 27 | options.AddDefaultPolicy(policy => 28 | { 29 | policy.AllowAnyOrigin(); 30 | policy.AllowAnyHeader(); 31 | policy.AllowAnyMethod(); 32 | }); 33 | }); 34 | 35 | /* 36 | ** Entity Framework Core Setup. 37 | */ 38 | var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); 39 | if (connectionString == null) 40 | { 41 | throw new ApplicationException("DefaultConnection is not set"); 42 | } 43 | builder.Services.AddDbContext(options => 44 | { 45 | options.UseSqlServer(connectionString); 46 | }); 47 | 48 | /* 49 | ** Automapper 50 | */ 51 | builder.Services.AddAutoMapper(typeof(MappingProfile)); 52 | 53 | /* 54 | ** The Data Service. 55 | */ 56 | builder.Services.AddScoped(); 57 | 58 | /* 59 | ** Controllers. 60 | */ 61 | builder.Services.AddHttpContextAccessor(); 62 | builder.Services.AddControllers().AddJsonOptions(options => 63 | { 64 | options.JsonSerializerOptions.WriteIndented = true; 65 | options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; 66 | options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); 67 | }); 68 | 69 | /* 70 | ** Swagger / OpenAPI. 71 | */ 72 | builder.Services.AddEndpointsApiExplorer(); 73 | builder.Services.AddSwaggerGen(); 74 | 75 | /****************************************************************************************** 76 | ** 77 | ** Configure the HTTP Pipeline. 78 | */ 79 | var app = builder.Build(); 80 | 81 | /* 82 | ** Database Initialization 83 | */ 84 | using (var scope = app.Services.CreateScope()) 85 | { 86 | var context = scope.ServiceProvider.GetRequiredService(); 87 | if (context is IDatabaseInitializer initializer) 88 | { 89 | await initializer.InitializeDatabaseAsync(); 90 | } 91 | } 92 | 93 | /* 94 | ** CORS 95 | */ 96 | app.UseCors(); 97 | 98 | /* 99 | ** Add Swagger support. 100 | */ 101 | app.UseSwagger(); 102 | app.UseSwaggerUI(); 103 | 104 | /* 105 | ** Controllers. 106 | */ 107 | app.UseHttpsRedirection(); 108 | app.UseAuthorization(); 109 | app.MapControllers(); 110 | 111 | /************************************************************************************************ 112 | ** 113 | ** Run the application. 114 | */ 115 | app.Run(); 116 | -------------------------------------------------------------------------------- /src/Todo.RestApi/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:38052", 8 | "sslPort": 44375 9 | } 10 | }, 11 | "profiles": { 12 | "Todo.RestApi": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7143;http://localhost:5139", 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/Todo.RestApi/Services/DTO/TodoItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | using Todo.Data; 6 | 7 | namespace Todo.RestApi.Services.DTO; 8 | 9 | public class TodoItem 10 | { 11 | [Required] 12 | public Guid? Id { get; set; } 13 | [Required] 14 | public Guid? ListId { get; set; } 15 | [Required] 16 | public string? Name { get; set; } 17 | public string? Description { get; set; } 18 | [Required] 19 | public TodoItemState State { get; set; } = TodoItemState.Todo; 20 | public DateTimeOffset? DueDate { get; set; } 21 | public DateTimeOffset? CompletedDate { get; set; } 22 | [Required] 23 | public DateTimeOffset? CreatedDate { get; set; } 24 | [Required] 25 | public DateTimeOffset? UpdatedDate { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Services/DTO/TodoList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Todo.RestApi.Services.DTO; 7 | 8 | public class TodoList 9 | { 10 | [Required] 11 | public Guid? Id { get; set; } 12 | [Required] 13 | public string? Name { get; set; } 14 | public string? Description { get; set; } 15 | [Required] 16 | public DateTimeOffset? CreatedDate { get; set; } 17 | [Required] 18 | public DateTimeOffset? UpdatedDate { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Services/Exceptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.Serialization; 5 | 6 | namespace Todo.RestApi.Services; 7 | 8 | /// 9 | /// The core exception for the service. 10 | /// 11 | public class DataServiceException : Exception 12 | { 13 | public DataServiceException() 14 | { 15 | } 16 | 17 | public DataServiceException(string? message) : base(message) 18 | { 19 | } 20 | 21 | public DataServiceException(string? message, Exception? innerException) : base(message, innerException) 22 | { 23 | } 24 | 25 | protected DataServiceException(SerializationInfo info, StreamingContext context) : base(info, context) 26 | { 27 | } 28 | } 29 | 30 | /// 31 | /// Exception that is thrown when the input from the user is mal-formed. 32 | /// 33 | public class MalformedInputException : DataServiceException 34 | { 35 | public MalformedInputException() 36 | { 37 | } 38 | 39 | public MalformedInputException(string? message) : base(message) 40 | { 41 | } 42 | 43 | public MalformedInputException(string? message, Exception? innerException) : base(message, innerException) 44 | { 45 | } 46 | 47 | protected MalformedInputException(SerializationInfo info, StreamingContext context) : base(info, context) 48 | { 49 | } 50 | } 51 | 52 | /// 53 | /// Exception that is thrown when an entity is expected, but does not exist. 54 | /// 55 | public class EntityMissingException : DataServiceException 56 | { 57 | public EntityMissingException() 58 | { 59 | } 60 | 61 | public EntityMissingException(string? message) : base(message) 62 | { 63 | } 64 | 65 | public EntityMissingException(string? message, Exception? innerException) : base(message, innerException) 66 | { 67 | } 68 | 69 | protected EntityMissingException(SerializationInfo info, StreamingContext context) : base(info, context) 70 | { 71 | } 72 | } 73 | 74 | /// 75 | /// Exception that is thrown when an entity does not exist, but is expected. 76 | /// 77 | public class EntityExistsException : DataServiceException 78 | { 79 | public EntityExistsException() 80 | { 81 | } 82 | 83 | public EntityExistsException(string? message) : base(message) 84 | { 85 | } 86 | 87 | public EntityExistsException(string? message, Exception? innerException) : base(message, innerException) 88 | { 89 | } 90 | 91 | protected EntityExistsException(SerializationInfo info, StreamingContext context) : base(info, context) 92 | { 93 | } 94 | 95 | /// 96 | /// Can be optionally set to the entity that exists. 97 | /// 98 | public object? Entity { get; set; } = null; 99 | } 100 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Services/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All Rights Reserved. 2 | // Licensed under the MIT License. 3 | 4 | using AutoMapper; 5 | 6 | namespace Todo.RestApi.Services; 7 | 8 | public class MappingProfile : Profile 9 | { 10 | public MappingProfile() 11 | { 12 | CreateMap(); 13 | CreateMap(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Todo.RestApi/Todo.RestApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | a8e44c19-9160-4468-ba9b-9af38e6a7910 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Todo.RestApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Todo.RestApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=SampleApis;Trusted_Connection=True" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Information", 9 | "Microsoft.AspNetCore": "Warning" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/todo.rest.web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .eslintcache 25 | 26 | env-config.js 27 | -------------------------------------------------------------------------------- /src/todo.rest.web/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /src/todo.rest.web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo.rest.web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "envconfig": "node tools/entrypoint.js -e .env -o ./public/env-config.js", 7 | "prestart": "run-s envconfig lint", 8 | "start": "react-scripts start", 9 | "prebuild": "run-s envconfig lint", 10 | "build": "react-scripts build", 11 | "pretest": "run-s envconfig", 12 | "test": "react-scripts test", 13 | "eject": "react-scripts eject", 14 | "lint": "eslint ./src --ext .ts,.tsx" 15 | }, 16 | "dependencies": { 17 | "@fluentui/react": "^8.105.8", 18 | "@microsoft/applicationinsights-react-js": "^3.4.1", 19 | "@microsoft/applicationinsights-web": "^2.8.10", 20 | "axios": "^1.3.3", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-router-dom": "^6.8.1", 24 | "web-vitals": "^2.1.4" 25 | }, 26 | "devDependencies": { 27 | "@testing-library/jest-dom": "^5.16.5", 28 | "@testing-library/react": "^13.4.0", 29 | "@testing-library/user-event": "^14.4.3", 30 | "@types/jest": "^29.4.0", 31 | "@types/node": "^18.13.0", 32 | "@types/react": "^18.0.28", 33 | "@types/react-dom": "^18.0.11", 34 | "@types/react-router-dom": "^5.3.3", 35 | "@typescript-eslint/eslint-plugin": "^5.52.0", 36 | "@typescript-eslint/parser": "^5.52.0", 37 | "dotenv": "^16.0.3", 38 | "eslint": "^8.34.0", 39 | "eslint-config-react-app": "^7.0.1", 40 | "immer": "^9.0.19", 41 | "npm-run-all": "^4.1.5", 42 | "react-scripts": "^5.0.1", 43 | "typescript": "^4.9.5" 44 | }, 45 | "eslintConfig": { 46 | "extends": [ 47 | "react-app", 48 | "eslint:recommended", 49 | "plugin:@typescript-eslint/recommended" 50 | ] 51 | }, 52 | "browserslist": { 53 | "production": [ 54 | ">0.2%", 55 | "not dead", 56 | "not op_mini all" 57 | ], 58 | "development": [ 59 | "last 1 chrome version", 60 | "last 1 firefox version", 61 | "last 1 safari version" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/todo.rest.web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/api-management-sample-apis/d317a2342a63d951005ac56ce719a9e86809dcca/src/todo.rest.web/public/favicon.ico -------------------------------------------------------------------------------- /src/todo.rest.web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 21 | 22 | 23 | AzDev Todo 24 | 25 | 26 | 27 |
28 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/todo.rest.web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Todo (REST)", 3 | "name": "Todo Ap (REST)", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#0392ff", 14 | "background_color": "#fcfcfc" 15 | } -------------------------------------------------------------------------------- /src/todo.rest.web/public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/todo.rest.web/src/@types/window.d.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | 3 | declare global { 4 | interface Window { 5 | ENV_CONFIG: { 6 | TODO_REACT_REST_API_BASE_URL: string; 7 | TODO_REACT_REST_APPLICATIONINSIGHTS_CONNECTION_STRING: string; 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/todo.rest.web/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100vh; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/todo.rest.web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer, FC } from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import Layout from './layout/layout'; 4 | import './App.css'; 5 | import { DarkTheme } from './ux/theme'; 6 | import { AppContext, ApplicationState, getDefaultState } from './models/applicationState'; 7 | import appReducer from './reducers'; 8 | import { TodoContext } from './components/todoContext'; 9 | import { initializeIcons } from '@fluentui/react/lib/Icons'; 10 | import { ThemeProvider } from '@fluentui/react'; 11 | import Telemetry from './components/telemetry'; 12 | 13 | export const App: FC = () => { 14 | const defaultState: ApplicationState = getDefaultState(); 15 | const [applicationState, dispatch] = useReducer(appReducer, defaultState); 16 | const initialContext: AppContext = { state: applicationState, dispatch: dispatch } 17 | 18 | initializeIcons(); 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; -------------------------------------------------------------------------------- /src/todo.rest.web/src/actions/actionCreators.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Dispatch } from "react"; 3 | 4 | export interface Action { 5 | type: T 6 | } 7 | 8 | export interface AnyAction extends Action { 9 | [extraProps: string]: any 10 | } 11 | 12 | export interface ActionCreator { 13 | (...args: P): A 14 | } 15 | 16 | export interface ActionCreatorsMapObject { 17 | [key: string]: ActionCreator 18 | } 19 | 20 | export type ActionMethod = (dispatch: Dispatch) => Promise; 21 | 22 | export interface PayloadAction extends Action { 23 | payload: TPayload; 24 | } 25 | 26 | export function createAction>(type: TAction["type"]): () => Action { 27 | return () => ({ 28 | type, 29 | }); 30 | } 31 | 32 | export function createPayloadAction>(type: TAction["type"]): (payload: TAction["payload"]) => PayloadAction { 33 | return (payload: TAction["payload"]) => ({ 34 | type, 35 | payload, 36 | }); 37 | } 38 | 39 | export type BoundActionMethod = (...args: A[]) => Promise; 40 | export type BoundActionsMapObject = { [key: string]: BoundActionMethod } 41 | 42 | function bindActionCreator(actionCreator: ActionCreator, dispatch: Dispatch): BoundActionMethod { 43 | return async function (this: any, ...args: any[]) { 44 | const actionMethod = actionCreator.apply(this, args) as any as ActionMethod; 45 | return await actionMethod(dispatch); 46 | } 47 | } 48 | 49 | export function bindActionCreators( 50 | actionCreators: ActionCreator | ActionCreatorsMapObject, 51 | dispatch: Dispatch 52 | ): BoundActionsMapObject | BoundActionMethod { 53 | if (typeof actionCreators === 'function') { 54 | return bindActionCreator(actionCreators, dispatch) 55 | } 56 | 57 | if (typeof actionCreators !== 'object' || actionCreators === null) { 58 | throw new Error('bindActionCreators expected an object or a function, did you write "import ActionCreators from" instead of "import * as ActionCreators from"?') 59 | } 60 | 61 | const boundActionCreators: ActionCreatorsMapObject = {} 62 | for (const key in actionCreators) { 63 | const actionCreator = actionCreators[key] 64 | if (typeof actionCreator === 'function') { 65 | boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) 66 | } 67 | } 68 | return boundActionCreators 69 | } -------------------------------------------------------------------------------- /src/todo.rest.web/src/actions/common.ts: -------------------------------------------------------------------------------- 1 | import * as itemActions from './itemActions'; 2 | import * as listActions from './listActions'; 3 | 4 | export enum ActionTypes { 5 | LOAD_TODO_LISTS = "LOAD_TODO_LISTS", 6 | LOAD_TODO_LIST = "LOAD_TODO_LIST", 7 | SELECT_TODO_LIST = "SELECT_TODO_LIST", 8 | SAVE_TODO_LIST = "SAVE_TODO_LIST", 9 | DELETE_TODO_LIST = "DELETE_TODO_LIST", 10 | LOAD_TODO_ITEMS = "LOAD_TODO_ITEMS", 11 | LOAD_TODO_ITEM = "LOAD_TODO_ITEM", 12 | SELECT_TODO_ITEM = "SELECT_TODO_ITEM", 13 | SAVE_TODO_ITEM = "SAVE_TODO_ITEM", 14 | DELETE_TODO_ITEM = "DELETE_TODO_ITEM" 15 | } 16 | 17 | export type TodoActions = 18 | itemActions.ListItemsAction | 19 | itemActions.SelectItemAction | 20 | itemActions.LoadItemAction | 21 | itemActions.SaveItemAction | 22 | itemActions.DeleteItemAction | 23 | listActions.ListListsAction | 24 | listActions.SelectListAction | 25 | listActions.LoadListAction | 26 | listActions.SaveListAction | 27 | listActions.DeleteListAction; -------------------------------------------------------------------------------- /src/todo.rest.web/src/actions/itemActions.ts: -------------------------------------------------------------------------------- 1 | import { QueryOptions } from "@testing-library/react"; 2 | import { Dispatch } from "react"; 3 | import { TodoItem } from "../models"; 4 | import { ItemService } from "../services/itemService"; 5 | import { ActionTypes } from "./common"; 6 | import config from "../config" 7 | import { ActionMethod, createPayloadAction, PayloadAction } from "./actionCreators"; 8 | 9 | const itemsPrefix = (listId: string) => `${config.api.listsPrefix}/${listId}/items`; 10 | 11 | export interface ItemActions { 12 | list(listId: string, options?: QueryOptions): Promise 13 | select(item?: TodoItem): Promise 14 | load(listId: string, id: string): Promise 15 | save(listId: string, Item: TodoItem): Promise 16 | remove(listId: string, Item: TodoItem): Promise 17 | } 18 | 19 | export const list = (listId: string, options?: QueryOptions): ActionMethod => async (dispatch: Dispatch) => { 20 | const itemService = new ItemService(config.api.baseUrl, itemsPrefix(listId)); 21 | const items = await itemService.getList(options); 22 | 23 | dispatch(listItemsAction(items)); 24 | 25 | return items; 26 | } 27 | 28 | export const select = (item?: TodoItem): ActionMethod => async (dispatch: Dispatch) => { 29 | dispatch(selectItemAction(item)); 30 | 31 | return Promise.resolve(item); 32 | } 33 | 34 | export const load = (listId: string, id: string): ActionMethod => async (dispatch: Dispatch) => { 35 | const itemService = new ItemService(config.api.baseUrl, itemsPrefix(listId)); 36 | const item = await itemService.get(id); 37 | 38 | dispatch(loadItemAction(item)); 39 | 40 | return item; 41 | } 42 | 43 | export const save = (listId: string, item: TodoItem): ActionMethod => async (dispatch: Dispatch) => { 44 | const itemService = new ItemService(config.api.baseUrl, itemsPrefix(listId)); 45 | const newItem = await itemService.save(item); 46 | 47 | dispatch(saveItemAction(newItem)); 48 | 49 | return newItem; 50 | } 51 | 52 | export const remove = (listId: string, item: TodoItem): ActionMethod => async (dispatch: Dispatch) => { 53 | const itemService = new ItemService(config.api.baseUrl, itemsPrefix(listId)); 54 | if (item.id) { 55 | await itemService.delete(item.id); 56 | dispatch(deleteItemAction(item.id)); 57 | } 58 | } 59 | 60 | export interface ListItemsAction extends PayloadAction { 61 | type: ActionTypes.LOAD_TODO_ITEMS 62 | } 63 | 64 | export interface SelectItemAction extends PayloadAction { 65 | type: ActionTypes.SELECT_TODO_ITEM 66 | } 67 | 68 | export interface LoadItemAction extends PayloadAction { 69 | type: ActionTypes.LOAD_TODO_ITEM 70 | } 71 | 72 | export interface SaveItemAction extends PayloadAction { 73 | type: ActionTypes.SAVE_TODO_ITEM 74 | } 75 | 76 | export interface DeleteItemAction extends PayloadAction { 77 | type: ActionTypes.DELETE_TODO_ITEM 78 | } 79 | 80 | const listItemsAction = createPayloadAction(ActionTypes.LOAD_TODO_ITEMS); 81 | const selectItemAction = createPayloadAction(ActionTypes.SELECT_TODO_ITEM); 82 | const loadItemAction = createPayloadAction(ActionTypes.LOAD_TODO_ITEM); 83 | const saveItemAction = createPayloadAction(ActionTypes.SAVE_TODO_ITEM); 84 | const deleteItemAction = createPayloadAction(ActionTypes.DELETE_TODO_ITEM); -------------------------------------------------------------------------------- /src/todo.rest.web/src/actions/listActions.ts: -------------------------------------------------------------------------------- 1 | import { QueryOptions } from "@testing-library/react"; 2 | import { Dispatch } from "react"; 3 | import { TodoList } from "../models"; 4 | import { ListService } from "../services/listService"; 5 | import { ActionTypes } from "./common"; 6 | import config from "../config" 7 | import { trackEvent } from "../services/telemetryService"; 8 | import { ActionMethod, createPayloadAction, PayloadAction } from "./actionCreators"; 9 | 10 | const listService = new ListService(config.api.baseUrl, config.api.listsPrefix); 11 | 12 | export interface ListActions { 13 | list(options?: QueryOptions): Promise 14 | load(id: string): Promise 15 | select(list: TodoList): Promise 16 | save(list: TodoList): Promise 17 | remove(id: string): Promise 18 | } 19 | 20 | export const list = (options?: QueryOptions): ActionMethod => async (dispatch: Dispatch) => { 21 | const lists = await listService.getList(options); 22 | 23 | dispatch(listListsAction(lists)); 24 | 25 | return lists; 26 | } 27 | 28 | export const select = (list: TodoList): ActionMethod => (dispatch: Dispatch) => { 29 | dispatch(selectListAction(list)); 30 | 31 | return Promise.resolve(list); 32 | } 33 | 34 | export const load = (id: string): ActionMethod => async (dispatch: Dispatch) => { 35 | const list = await listService.get(id); 36 | 37 | dispatch(loadListAction(list)); 38 | 39 | return list; 40 | } 41 | 42 | export const save = (list: TodoList): ActionMethod => async (dispatch: Dispatch) => { 43 | const newList = await listService.save(list); 44 | 45 | dispatch(saveListAction(newList)); 46 | 47 | trackEvent(ActionTypes.SAVE_TODO_LIST.toString()); 48 | 49 | return newList; 50 | } 51 | 52 | export const remove = (id: string): ActionMethod => async (dispatch: Dispatch) => { 53 | await listService.delete(id); 54 | 55 | dispatch(deleteListAction(id)); 56 | } 57 | 58 | export interface ListListsAction extends PayloadAction { 59 | type: ActionTypes.LOAD_TODO_LISTS 60 | } 61 | 62 | export interface SelectListAction extends PayloadAction { 63 | type: ActionTypes.SELECT_TODO_LIST 64 | } 65 | 66 | export interface LoadListAction extends PayloadAction { 67 | type: ActionTypes.LOAD_TODO_LIST 68 | } 69 | 70 | export interface SaveListAction extends PayloadAction { 71 | type: ActionTypes.SAVE_TODO_LIST 72 | } 73 | 74 | export interface DeleteListAction extends PayloadAction { 75 | type: ActionTypes.DELETE_TODO_LIST 76 | } 77 | 78 | const listListsAction = createPayloadAction(ActionTypes.LOAD_TODO_LISTS); 79 | const selectListAction = createPayloadAction(ActionTypes.SELECT_TODO_LIST); 80 | const loadListAction = createPayloadAction(ActionTypes.LOAD_TODO_LIST); 81 | const saveListAction = createPayloadAction(ActionTypes.SAVE_TODO_LIST); 82 | const deleteListAction = createPayloadAction(ActionTypes.DELETE_TODO_LIST); -------------------------------------------------------------------------------- /src/todo.rest.web/src/components/telemetry.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactElement, useEffect, ComponentType, ComponentClass, PropsWithChildren } from 'react'; 2 | import { TelemetryProvider } from './telemetryContext'; 3 | import { reactPlugin, getApplicationInsights } from '../services/telemetryService'; 4 | import { withAITracking } from '@microsoft/applicationinsights-react-js'; 5 | 6 | type TelemetryProps = PropsWithChildren; 7 | 8 | const Telemetry: FC = (props: TelemetryProps): ReactElement => { 9 | 10 | useEffect(() => { 11 | getApplicationInsights(); 12 | }, []); 13 | 14 | return ( 15 | 16 | {props.children} 17 | 18 | ); 19 | } 20 | 21 | export default Telemetry; 22 | export const withApplicationInsights = (component: ComponentType, componentName: string): ComponentClass, unknown> => withAITracking(reactPlugin, component, componentName); -------------------------------------------------------------------------------- /src/todo.rest.web/src/components/telemetryContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { reactPlugin } from '../services/telemetryService'; 3 | 4 | const TelemetryContext = createContext(reactPlugin); 5 | 6 | export const TelemetryProvider = TelemetryContext.Provider; 7 | export const TelemetryConsumer = TelemetryContext.Consumer; 8 | export default TelemetryContext; -------------------------------------------------------------------------------- /src/todo.rest.web/src/components/todoContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { AppContext, getDefaultState } from "../models/applicationState"; 3 | 4 | const initialState = getDefaultState(); 5 | const dispatch = () => { return }; 6 | 7 | export const TodoContext = createContext({ state: initialState, dispatch: dispatch }); -------------------------------------------------------------------------------- /src/todo.rest.web/src/components/todoItemDetailPane.tsx: -------------------------------------------------------------------------------- 1 | import { Text, DatePicker, Stack, TextField, PrimaryButton, DefaultButton, Dropdown, IDropdownOption, FontIcon } from '@fluentui/react'; 2 | import React, { useEffect, useState, FC, ReactElement, MouseEvent, FormEvent } from 'react'; 3 | import { TodoItem, TodoItemState } from '../models'; 4 | import { stackGaps, stackItemMargin, stackItemPadding, titleStackStyles } from '../ux/styles'; 5 | 6 | interface TodoItemDetailPaneProps { 7 | item?: TodoItem; 8 | onEdit: (item: TodoItem) => void 9 | onCancel: () => void 10 | } 11 | 12 | export const TodoItemDetailPane: FC = (props: TodoItemDetailPaneProps): ReactElement => { 13 | const [name, setName] = useState(props.item?.name || ''); 14 | const [description, setDescription] = useState(props.item?.description); 15 | const [dueDate, setDueDate] = useState(props.item?.dueDate); 16 | const [state, setState] = useState(props.item?.state || TodoItemState.Todo); 17 | 18 | useEffect(() => { 19 | setName(props.item?.name || ''); 20 | setDescription(props.item?.description); 21 | setDueDate(props.item?.dueDate ? new Date(props.item?.dueDate) : undefined); 22 | setState(props.item?.state || TodoItemState.Todo); 23 | }, [props.item]); 24 | 25 | const saveTodoItem = (evt: MouseEvent) => { 26 | evt.preventDefault(); 27 | 28 | if (!props.item?.id) { 29 | return; 30 | } 31 | 32 | const todoItem: TodoItem = { 33 | id: props.item.id, 34 | listId: props.item.listId, 35 | name: name, 36 | description: description, 37 | dueDate: dueDate, 38 | state: state, 39 | }; 40 | 41 | props.onEdit(todoItem); 42 | }; 43 | 44 | const cancelEdit = (evt: MouseEvent) => { 45 | props.onCancel(); 46 | } 47 | 48 | const onStateChange = (evt: FormEvent, value?: IDropdownOption) => { 49 | if (value) { 50 | setState(value.key as TodoItemState); 51 | } 52 | } 53 | 54 | const onDueDateChange = (date: Date | null | undefined) => { 55 | setDueDate(date || undefined); 56 | } 57 | 58 | const todoStateOptions: IDropdownOption[] = [ 59 | { key: TodoItemState.Todo, text: 'To Do' }, 60 | { key: TodoItemState.InProgress, text: 'In Progress' }, 61 | { key: TodoItemState.Done, text: 'Done' }, 62 | ]; 63 | 64 | return ( 65 | 66 | {props.item && 67 | <> 68 | 69 | {name} 70 | {description} 71 | 72 | 73 | setName(value || '')} /> 74 | setDescription(value)} /> 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | } 86 | {!props.item && 87 | 88 | 89 | Select an item to edit 90 | } 91 | 92 | ); 93 | } 94 | 95 | export default TodoItemDetailPane; -------------------------------------------------------------------------------- /src/todo.rest.web/src/components/todoListMenu.tsx: -------------------------------------------------------------------------------- 1 | import { IIconProps, INavLink, INavLinkGroup, Nav, Stack, TextField } from '@fluentui/react'; 2 | import React, { FC, ReactElement, useState, FormEvent, MouseEvent } from 'react'; 3 | import { useNavigate } from 'react-router'; 4 | import { TodoList } from '../models/todoList'; 5 | import { stackItemPadding } from '../ux/styles'; 6 | 7 | interface TodoListMenuProps { 8 | selectedList?: TodoList 9 | lists?: TodoList[] 10 | onCreate: (list: TodoList) => void 11 | } 12 | 13 | const iconProps: IIconProps = { 14 | iconName: 'AddToShoppingList' 15 | } 16 | 17 | const TodoListMenu: FC = (props: TodoListMenuProps): ReactElement => { 18 | const navigate = useNavigate(); 19 | const [newListName, setNewListName] = useState(''); 20 | 21 | const onNavLinkClick = (evt?: MouseEvent, item?: INavLink) => { 22 | evt?.preventDefault(); 23 | 24 | if (!item) { 25 | return; 26 | } 27 | 28 | navigate(`/lists/${item.key}`); 29 | } 30 | 31 | const createNavGroups = (lists: TodoList[]): INavLinkGroup[] => { 32 | const links = lists.map(list => ({ 33 | key: list.id, 34 | name: list.name, 35 | url: `/lists/${list.id}`, 36 | links: [], 37 | isExpanded: props.selectedList ? list.id === props.selectedList.id : false 38 | })); 39 | 40 | return [{ 41 | links: links 42 | }] 43 | } 44 | 45 | const onNewListNameChange = (evt: FormEvent, value?: string) => { 46 | setNewListName(value || ''); 47 | } 48 | 49 | const onFormSubmit = async (evt: FormEvent) => { 50 | evt.preventDefault(); 51 | 52 | if (newListName) { 53 | const list: TodoList = { 54 | name: newListName 55 | }; 56 | 57 | props.onCreate(list); 58 | setNewListName(''); 59 | } 60 | } 61 | 62 | return ( 63 | 64 | 65 |