diff --git a/.all-contributorsrc b/.all-contributorsrc
index 0f9e3035..c29451ca 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -224,6 +224,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "michaelmsonne",
+ "name": "Michael Morten Sonne | Microsoft MVP",
+ "avatar_url": "https://avatars.githubusercontent.com/u/49366751?v=4",
+ "profile": "https://blog.sonnes.cloud",
+ "contributions": [
+ "bug"
+ ]
}
],
"commitConvention": "angular",
diff --git a/README.md b/README.md
index e0e3bcf6..6adb8e06 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
  [](https://github.com/microsoft/AzUrlShortener/actions/workflows/build.yml)  [](https://discord.gg/6zA3jKw)
-[](#contributors-)
+[](#contributors-)
@@ -116,6 +116,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 ddematheu2 🐛 💻 |
 David Oliva Paredes 💻 |
+  Michael Morten Sonne | Microsoft MVP 🐛 |
diff --git a/doc/faq.md b/doc/faq.md
index efd319d5..77565e15 100644
--- a/doc/faq.md
+++ b/doc/faq.md
@@ -10,6 +10,7 @@
- [How does it work?](./how-it-works.md)
- [Security Considerations](./security-considerations.md)
- [How to make the api public](./how-to-set-api-public.md)
+- [How to deploy to App Service (budget friendlier version)](./how-deploy-budget-solution.md)
## How to run AzUrlShortener locally
diff --git a/doc/how-deploy-budget-solution.md b/doc/how-deploy-budget-solution.md
new file mode 100644
index 00000000..9d2527e1
--- /dev/null
+++ b/doc/how-deploy-budget-solution.md
@@ -0,0 +1,100 @@
+
+
+# Deploy the budget solution
+
+This guide explains how to deploy the budget-friendly version of AzUrlShortener. This deployment uses an Azure App Service for the admin website, an Azure Functions app for redirects, and an API container running as a private sidecar in the admin App Service.
+
+## Prerequisites
+
+Install the following tools before you begin:
+
+- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)
+- [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd)
+- Git, if you are cloning the repository
+
+You also need:
+
+- An Azure subscription with permission to create resource groups and the resources in this deployment.
+- A local copy of your fork or clone of the AzUrlShortener repository.
+- The custom domain you will use for short URLs, including `https://`.
+- The default redirect URL, including `https://`.
+
+## First deployment
+
+1. Sign in to Azure with both the Azure CLI and Azure Developer CLI:
+
+ ```bash
+ az login
+ azd auth login
+ ```
+
+ Use the same account, or an account with access to the target subscription.
+
+1. Change to the budget deployment directory:
+
+ ```bash
+ cd src/budget-deployment
+ ```
+
+ If you cloned the repository elsewhere, replace the path with your local repository path.
+
+1. Provision the Azure resources and deploy the application:
+
+ ```bash
+ azd up
+ ```
+
+ During this command, `azd` asks you to select an Azure subscription and location, and to provide values for:
+
+ - **Environment name**: Used in the resource group name. For example, an environment named `budget-prod` creates the resource group `rg-budget-prod`.
+ - **Custom domain**: The complete domain for your short URLs, such as `https://c5m.ca`.
+ - **Default redirect URL**: The destination used when a vanity URL does not exist or no vanity is supplied.
+
+ `azd up` creates the resource group and required storage, App Service, Functions, and container registry resources. After provisioning, the post-provision hook builds the API image in Azure Container Registry and restarts the admin App Service so it can load the sidecar.
+
+1. Open the URLs printed by `azd` when deployment completes. The admin website URL is the App Service URL, and the redirect service URL is the Azure Functions URL.
+
+## Update an existing deployment
+
+After making application changes, sign in and move to the deployment directory:
+
+```bash
+az login
+azd auth login
+cd /home/frank/gh/AzUrlShortener/src/budget-deployment
+```
+
+Deploy only the admin website and its related application files with:
+
+```bash
+azd deploy admin
+```
+
+To deploy all application services instead, use:
+
+```bash
+azd deploy
+```
+
+Use `azd provision` when the infrastructure definition has changed and the Azure resources need to be updated.
+
+## Troubleshooting the API sidecar
+
+The API sidecar image is normally built automatically by the `azd up` post-provision hook. If the image is missing or the hook did not complete, run the hook manually from `src/budget-deployment`:
+
+```bash
+sh hooks/deploy-api-sidecar.sh
+```
+
+The script builds the image with Azure Container Registry Tasks, so Docker does not need to be running locally. It then restarts the admin App Service.
+
+If the admin website still has the old sidecar after the image is available, restart it manually:
+
+```bash
+az webapp restart \
+ --name \
+ --resource-group .
+ The API sidecar is private and is reachable only by the admin application through `http://localhost:8081`; it is not exposed directly to the internet.
+
+
+Replace `` and `` with the values from the deployment output or the Azure portal. The API sidecar is private and is reachable only by the admin application through `http://localhost:8081`; it is not exposed directly to the internet.
\ No newline at end of file
diff --git a/prompt.txt b/prompt.txt
new file mode 100644
index 00000000..f2354469
--- /dev/null
+++ b/prompt.txt
@@ -0,0 +1,11 @@
+In this folder `src/budget-deployment` I have the deployment IaC to deploy my entire solution and it works. But right now it deploys the web site admin: `../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj`
+
+with it's api: `../Api/Cloud5mins.ShortenerTools.Api.csproj` in two individual app service.
+
+It would be better if they were deploy in the same App Service using sidecar.
+
+I would like to have the "api" NOT reachable from the internet. Like with a docker compose setup when a backend container is only accessible by the frontend container.
+
+Here admin is the frontend and api is the backend
+
+I saw that post that explains a few things about sidecar deployments in Azure App Service https://azure.github.io/AppService/2025/03/06/Sidecars-Deep-Dive-Part1.html
\ No newline at end of file
diff --git a/src/.dockerignore b/src/.dockerignore
new file mode 100644
index 00000000..2c5d0f71
--- /dev/null
+++ b/src/.dockerignore
@@ -0,0 +1,13 @@
+# Exclude build output and other large/unnecessary directories from the
+# Docker build context (used by both local 'docker build' and 'az acr build').
+**/bin/
+**/obj/
+**/.git/
+**/.azure/
+**/node_modules/
+# Exclude other deployment configs – only src/ content is needed
+budget-deployment/
+AppHost/
+infra/
+tools/
+*.md
diff --git a/src/.gitignore b/src/.gitignore
index 8e843802..1c037d0f 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -1 +1,2 @@
.azure
+publish/
diff --git a/src/Api/Cloud5mins.ShortenerTools.Api.csproj b/src/Api/Cloud5mins.ShortenerTools.Api.csproj
index 19e21d56..e492bbf0 100644
--- a/src/Api/Cloud5mins.ShortenerTools.Api.csproj
+++ b/src/Api/Cloud5mins.ShortenerTools.Api.csproj
@@ -1,10 +1,11 @@
-
+
net10.0
enable
enable
true
+ 500bdb23-5095-412f-b1e9-421be27fb629
diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile
new file mode 100644
index 00000000..43ad5950
--- /dev/null
+++ b/src/Api/Dockerfile
@@ -0,0 +1,32 @@
+# Build stage – uses the full SDK image
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+# Copy NuGet central version management before restore
+COPY ["Directory.Packages.props", "./"]
+
+# Copy project files and restore (layer-cache friendly)
+COPY ["Api/Cloud5mins.ShortenerTools.Api.csproj", "Api/"]
+COPY ["Core/Cloud5mins.ShortenerTools.Core.csproj", "Core/"]
+COPY ["ServiceDefaults/Cloud5mins.ShortenerTools.ServiceDefaults.csproj", "ServiceDefaults/"]
+RUN dotnet restore "Api/Cloud5mins.ShortenerTools.Api.csproj"
+
+# Copy everything and publish
+COPY . .
+RUN dotnet publish "Api/Cloud5mins.ShortenerTools.Api.csproj" \
+ -c Release \
+ -o /app/publish \
+ /p:UseAppHost=false
+
+# Runtime stage – lightweight ASP.NET image
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
+WORKDIR /app
+
+# Listen on HTTP only; App Service routes external traffic to the admin container.
+# Use a dedicated internal port so the main admin app (which listens on 8080)
+# can coexist with the sidecar container in the same site unit.
+ENV ASPNETCORE_HTTP_PORTS=8081
+EXPOSE 8081
+
+COPY --from=build /app/publish .
+ENTRYPOINT ["dotnet", "Cloud5mins.ShortenerTools.Api.dll"]
diff --git a/src/Api/Program.cs b/src/Api/Program.cs
index 3a93d85a..de3bb9a0 100644
--- a/src/Api/Program.cs
+++ b/src/Api/Program.cs
@@ -6,7 +6,7 @@
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
-builder.AddAzureTableClient("strTables");
+builder.AddAzureTableServiceClient("strTables");
builder.Services.AddTransient(sp =>
{
diff --git a/src/Api/ShortenerEnpoints.cs b/src/Api/ShortenerEnpoints.cs
index d5e702b3..b56c996a 100644
--- a/src/Api/ShortenerEnpoints.cs
+++ b/src/Api/ShortenerEnpoints.cs
@@ -10,8 +10,7 @@ public static class ShortenerEnpoints
{
public static void MapShortenerEnpoints(this IEndpointRouteBuilder app)
{
- var endpoints = app.MapGroup("api")
- .WithOpenApi();
+ var endpoints = app.MapGroup("api");
// GETS
diff --git a/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj b/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj
index 0f8183cf..4801274d 100644
--- a/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj
+++ b/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj
@@ -1,4 +1,4 @@
-
+
Exe
diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs
index 073ad77f..5d7a1a9f 100644
--- a/src/AppHost/Program.cs
+++ b/src/AppHost/Program.cs
@@ -6,13 +6,20 @@
var customDomain = builder.AddParameter("CustomDomain");
var defaultRedirectUrl = builder.AddParameter("DefaultRedirectUrl");
+// To use an existing storage account, you can provide the name and resource group of the existing storage account.
+// var existingStorageName = builder.AddParameter("existingStorageName");
+// var existingStorageResourceGroup = builder.AddParameter("existingStorageResourceGroup");
+
var urlStorage = builder.AddAzureStorage("url-data");
+ // .AsExisting(existingStorageName, existingStorageResourceGroup);
if (builder.Environment.IsDevelopment())
{
urlStorage.RunAsEmulator();
}
+
+
var strTables = urlStorage.AddTables("strTables");
var azFuncLight = builder.AddAzureFunctionsProject("azfunc-light")
diff --git a/src/AppHost/infra/admin.tmpl.yaml b/src/AppHost/infra/admin.tmpl.yaml
new file mode 100644
index 00000000..4a85ff1d
--- /dev/null
+++ b/src/AppHost/infra/admin.tmpl.yaml
@@ -0,0 +1,48 @@
+api-version: 2024-02-02-preview
+location: {{ .Env.AZURE_LOCATION }}
+identity:
+ type: UserAssigned
+ userAssignedIdentities:
+ ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}"
+ : {}
+properties:
+ environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }}
+ configuration:
+ activeRevisionsMode: single
+ runtime:
+ dotnet:
+ autoConfigureDataProtection: true
+ ingress:
+ external: true
+ targetPort: {{ targetPortOrDefault 8080 }}
+ transport: http
+ allowInsecure: false
+ registries:
+ - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }}
+ identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}
+ template:
+ containers:
+ - image: {{ .Image }}
+ name: admin
+ env:
+ - name: AZURE_CLIENT_ID
+ value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }}
+ - name: API_HTTP
+ value: http://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }}
+ - name: API_HTTPS
+ value: https://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }}
+ - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED
+ value: "true"
+ - name: HTTP_PORTS
+ value: '{{ targetPortOrDefault 0 }}'
+ - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY
+ value: in_memory
+ - name: services__api__http__0
+ value: http://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }}
+ - name: services__api__https__0
+ value: https://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }}
+ scale:
+ minReplicas: 1
+tags:
+ azd-service-name: admin
+ aspire-resource-name: admin
diff --git a/src/AppHost/infra/api.tmpl.yaml b/src/AppHost/infra/api.tmpl.yaml
new file mode 100644
index 00000000..d7e65517
--- /dev/null
+++ b/src/AppHost/infra/api.tmpl.yaml
@@ -0,0 +1,51 @@
+api-version: 2024-02-02-preview
+location: {{ .Env.AZURE_LOCATION }}
+identity:
+ type: UserAssigned
+ userAssignedIdentities:
+ ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}"
+ : {}
+properties:
+ environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }}
+ configuration:
+ activeRevisionsMode: single
+ runtime:
+ dotnet:
+ autoConfigureDataProtection: true
+ ingress:
+ external: false
+ targetPort: {{ targetPortOrDefault 8080 }}
+ transport: http
+ allowInsecure: true
+ registries:
+ - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }}
+ identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}
+ secrets:
+ - name: connectionstrings--strtables
+ value: '{{ .Env.URL_DATA_TABLEENDPOINT }}'
+ template:
+ containers:
+ - image: {{ .Image }}
+ name: api
+ env:
+ - name: AZURE_CLIENT_ID
+ value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }}
+ - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED
+ value: "true"
+ - name: CustomDomain
+ value: '{{ parameter "CustomDomain" }}'
+ - name: DefaultRedirectUrl
+ value: '{{ parameter "DefaultRedirectUrl" }}'
+ - name: HTTP_PORTS
+ value: '{{ targetPortOrDefault 0 }}'
+ - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY
+ value: in_memory
+ - name: STRTABLES_URI
+ value: '{{ .Env.URL_DATA_TABLEENDPOINT }}'
+ - name: ConnectionStrings__strTables
+ secretRef: connectionstrings--strtables
+ scale:
+ minReplicas: 1
+tags:
+ azd-service-name: api
+ aspire-resource-name: api
diff --git a/src/AppHost/infra/azfunc-light.tmpl.yaml b/src/AppHost/infra/azfunc-light.tmpl.yaml
new file mode 100644
index 00000000..f48e7e30
--- /dev/null
+++ b/src/AppHost/infra/azfunc-light.tmpl.yaml
@@ -0,0 +1,66 @@
+api-version: 2024-02-02-preview
+location: {{ .Env.AZURE_LOCATION }}
+identity:
+ type: UserAssigned
+ userAssignedIdentities:
+ ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}"
+ : {}
+properties:
+ environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }}
+ configuration:
+ activeRevisionsMode: single
+ runtime:
+ dotnet:
+ autoConfigureDataProtection: true
+ ingress:
+ external: true
+ targetPort: 8080
+ transport: http
+ allowInsecure: false
+ registries:
+ - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }}
+ identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}
+ template:
+ containers:
+ - image: {{ .Image }}
+ name: azfunc-light
+ env:
+ - name: AZURE_CLIENT_ID
+ value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }}
+ - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED
+ value: "true"
+ - name: ASPNETCORE_URLS
+ value: http://+:8080
+ - name: Aspire__Azure__Data__Tables__AzureWebJobsStorage__ServiceUri
+ value: '{{ .Env.FUNCSTORAGEA17CA_TABLEENDPOINT }}'
+ - name: Aspire__Azure__Data__Tables__strTables__ServiceUri
+ value: '{{ .Env.URL_DATA_TABLEENDPOINT }}'
+ - name: Aspire__Azure__Storage__Blobs__AzureWebJobsStorage__ServiceUri
+ value: '{{ .Env.FUNCSTORAGEA17CA_BLOBENDPOINT }}'
+ - name: Aspire__Azure__Storage__Files__DataLake__AzureWebJobsStorage__ServiceUri
+ value: '{{ .Env.FUNCSTORAGEA17CA_DATALAKEENDPOINT }}'
+ - name: Aspire__Azure__Storage__Queues__AzureWebJobsStorage__ServiceUri
+ value: '{{ .Env.FUNCSTORAGEA17CA_QUEUEENDPOINT }}'
+ - name: AzureFunctionsJobHost__telemetryMode
+ value: OpenTelemetry
+ - name: AzureWebJobsStorage__blobServiceUri
+ value: '{{ .Env.FUNCSTORAGEA17CA_BLOBENDPOINT }}'
+ - name: AzureWebJobsStorage__dataLakeServiceUri
+ value: '{{ .Env.FUNCSTORAGEA17CA_DATALAKEENDPOINT }}'
+ - name: AzureWebJobsStorage__queueServiceUri
+ value: '{{ .Env.FUNCSTORAGEA17CA_QUEUEENDPOINT }}'
+ - name: AzureWebJobsStorage__tableServiceUri
+ value: '{{ .Env.FUNCSTORAGEA17CA_TABLEENDPOINT }}'
+ - name: DefaultRedirectUrl
+ value: '{{ parameter "DefaultRedirectUrl" }}'
+ - name: FUNCTIONS_WORKER_RUNTIME
+ value: dotnet-isolated
+ - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY
+ value: in_memory
+ - name: strTables__tableServiceUri
+ value: '{{ .Env.URL_DATA_TABLEENDPOINT }}'
+ scale:
+ minReplicas: 1
+tags:
+ azd-service-name: azfunc-light
+ aspire-resource-name: azfunc-light
diff --git a/src/AzUrlShortener.sln b/src/AzUrlShortener.sln
index a000c143..1e6500c2 100644
--- a/src/AzUrlShortener.sln
+++ b/src/AzUrlShortener.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
@@ -18,35 +18,87 @@ EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x64.Build.0 = Debug|Any CPU
+ {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x86.Build.0 = Debug|Any CPU
{99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|Any CPU.Build.0 = Release|Any CPU
+ {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x64.ActiveCfg = Release|Any CPU
+ {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x64.Build.0 = Release|Any CPU
+ {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x86.ActiveCfg = Release|Any CPU
+ {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x86.Build.0 = Release|Any CPU
{1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x64.Build.0 = Debug|Any CPU
+ {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x86.Build.0 = Debug|Any CPU
{1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x64.ActiveCfg = Release|Any CPU
+ {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x64.Build.0 = Release|Any CPU
+ {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x86.ActiveCfg = Release|Any CPU
+ {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x86.Build.0 = Release|Any CPU
{3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x64.Build.0 = Debug|Any CPU
+ {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x86.Build.0 = Debug|Any CPU
{3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x64.ActiveCfg = Release|Any CPU
+ {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x64.Build.0 = Release|Any CPU
+ {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x86.ActiveCfg = Release|Any CPU
+ {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x86.Build.0 = Release|Any CPU
{6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x64.Build.0 = Debug|Any CPU
+ {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x86.Build.0 = Debug|Any CPU
{6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x64.ActiveCfg = Release|Any CPU
+ {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x64.Build.0 = Release|Any CPU
+ {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x86.ActiveCfg = Release|Any CPU
+ {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x86.Build.0 = Release|Any CPU
{85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x64.Build.0 = Debug|Any CPU
+ {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x86.Build.0 = Debug|Any CPU
{85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x64.ActiveCfg = Release|Any CPU
+ {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x64.Build.0 = Release|Any CPU
+ {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x86.ActiveCfg = Release|Any CPU
+ {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x86.Build.0 = Release|Any CPU
{88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x64.Build.0 = Debug|Any CPU
+ {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x86.Build.0 = Debug|Any CPU
{88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x64.ActiveCfg = Release|Any CPU
+ {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x64.Build.0 = Release|Any CPU
+ {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x86.ActiveCfg = Release|Any CPU
+ {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
diff --git a/src/Core/Domain/ClickDateList.cs b/src/Core/Domain/ClickDateList.cs
index 0cf13b43..27208e57 100644
--- a/src/Core/Domain/ClickDateList.cs
+++ b/src/Core/Domain/ClickDateList.cs
@@ -2,11 +2,10 @@ namespace Cloud5mins.ShortenerTools.Core.Domain
{
public class ClickDateList
{
- public List Items { get; set; }
- public string Url { get; set; }
+ public List Items { get; set; } = new();
+ public string Url { get; set; } = string.Empty;
public ClickDateList()
{
- Url = string.Empty;
}
public ClickDateList(List list)
{
diff --git a/src/Core/Domain/ClickStatsEntity.cs b/src/Core/Domain/ClickStatsEntity.cs
index da0a7943..94a59d17 100644
--- a/src/Core/Domain/ClickStatsEntity.cs
+++ b/src/Core/Domain/ClickStatsEntity.cs
@@ -5,9 +5,9 @@ namespace Cloud5mins.ShortenerTools.Core.Domain;
public class ClickStatsEntity : ITableEntity
{
- public string Datetime { get; set; }
- public string PartitionKey { get; set; }
- public string RowKey { get; set; }
+ public string Datetime { get; set; } = string.Empty;
+ public string PartitionKey { get; set; } = string.Empty;
+ public string RowKey { get; set; } = string.Empty;
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
diff --git a/src/Core/Domain/ClickStatsEntityList.cs b/src/Core/Domain/ClickStatsEntityList.cs
index b3b25852..ca0b8c15 100644
--- a/src/Core/Domain/ClickStatsEntityList.cs
+++ b/src/Core/Domain/ClickStatsEntityList.cs
@@ -2,7 +2,7 @@ namespace Cloud5mins.ShortenerTools.Core.Domain
{
public class ClickStatsEntityList
{
- public List ClickStatsList { get; set; }
+ public List ClickStatsList { get; set; } = new();
public ClickStatsEntityList() { }
public ClickStatsEntityList(List list)
diff --git a/src/Core/Domain/DetailledBadRequest.cs b/src/Core/Domain/DetailledBadRequest.cs
index c66c7d3d..26e9e32c 100644
--- a/src/Core/Domain/DetailledBadRequest.cs
+++ b/src/Core/Domain/DetailledBadRequest.cs
@@ -2,6 +2,6 @@ namespace Cloud5mins.ShortenerTools.Core.Domain
{
public class DetailedBadRequest
{
- public string Message { get; set; }
+ public string Message { get; set; } = string.Empty;
}
}
\ No newline at end of file
diff --git a/src/Core/Domain/NextId.cs b/src/Core/Domain/NextId.cs
index 8edc0f5f..19e7c9b3 100644
--- a/src/Core/Domain/NextId.cs
+++ b/src/Core/Domain/NextId.cs
@@ -6,8 +6,8 @@ namespace Cloud5mins.ShortenerTools.Core.Domain;
public class NextId : ITableEntity
{
public int Id { get; set; }
- public string PartitionKey { get; set; }
- public string RowKey { get; set; }
+ public string PartitionKey { get; set; } = string.Empty;
+ public string RowKey { get; set; } = string.Empty;
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
}
diff --git a/src/Core/Domain/ShortUrlEntity.cs b/src/Core/Domain/ShortUrlEntity.cs
index 4841bc5d..b355246a 100644
--- a/src/Core/Domain/ShortUrlEntity.cs
+++ b/src/Core/Domain/ShortUrlEntity.cs
@@ -2,16 +2,14 @@
using Azure.Data.Tables;
using System.Runtime.Serialization;
using System.Text.Json;
-using Azure;
-using Azure.Data.Tables;
namespace Cloud5mins.ShortenerTools.Core.Domain
{
public class ShortUrlEntity : ITableEntity
{
- public string Url { get; set; }
- private string _activeUrl { get; set; }
+ public string Url { get; set; } = string.Empty;
+ private string _activeUrl { get; set; } = string.Empty;
public string ActiveUrl
{
@@ -24,16 +22,16 @@ public string ActiveUrl
}
- public string Title { get; set; }
+ public string Title { get; set; } = string.Empty;
- public string ShortUrl { get; set; }
+ public string ShortUrl { get; set; } = string.Empty;
public int Clicks { get; set; }
public bool? IsArchived { get; set; }
- public string SchedulesPropertyRaw { get; set; }
+ public string SchedulesPropertyRaw { get; set; } = string.Empty;
- private List _schedules { get; set; }
+ private List _schedules { get; set; } = new();
//[IgnoreProperty]
[IgnoreDataMember]
@@ -49,7 +47,7 @@ public List Schedules
}
else
{
- _schedules = JsonSerializer.Deserialize(SchedulesPropertyRaw).ToList();
+ _schedules = JsonSerializer.Deserialize(SchedulesPropertyRaw)?.ToList() ?? new List();
}
}
return _schedules;
@@ -60,12 +58,12 @@ public List Schedules
}
}
- public string PartitionKey { get; set; }
- public string RowKey { get; set; }
+ public string PartitionKey { get; set; } = string.Empty;
+ public string RowKey { get; set; } = string.Empty;
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
- public string CreatedDate { get; set; }
+ public string CreatedDate { get; set; } = string.Empty;
@@ -76,17 +74,17 @@ public ShortUrlEntity(string longUrl, string endUrl)
Initialize(longUrl, endUrl, string.Empty, null);
}
- public ShortUrlEntity(string longUrl, string endUrl, Schedule[] schedules)
+ public ShortUrlEntity(string longUrl, string endUrl, Schedule[]? schedules)
{
Initialize(longUrl, endUrl, string.Empty, schedules);
}
- public ShortUrlEntity(string longUrl, string endUrl, string title, Schedule[] schedules)
+ public ShortUrlEntity(string longUrl, string endUrl, string title, Schedule[]? schedules)
{
Initialize(longUrl, endUrl, title, schedules);
}
- private void Initialize(string longUrl, string endUrl, string title, Schedule[] schedules)
+ private void Initialize(string longUrl, string endUrl, string title, Schedule[]? schedules)
{
PartitionKey = endUrl.First().ToString();
RowKey = endUrl;
diff --git a/src/Core/Domain/ShortenerSettings.cs b/src/Core/Domain/ShortenerSettings.cs
index b702f8d4..ceb52e15 100644
--- a/src/Core/Domain/ShortenerSettings.cs
+++ b/src/Core/Domain/ShortenerSettings.cs
@@ -2,8 +2,8 @@ namespace Cloud5mins.ShortenerTools.Core.Domain
{
public class ShortenerSettings
{
- public string DefaultRedirectUrl { get; set; }
- public string CustomDomain { get; set; }
- public string DataStorage { get; set; }
+ public string DefaultRedirectUrl { get; set; } = string.Empty;
+ public string CustomDomain { get; set; } = string.Empty;
+ public string DataStorage { get; set; } = string.Empty;
}
}
\ No newline at end of file
diff --git a/src/Core/Messages/ListResponse.cs b/src/Core/Messages/ListResponse.cs
index a20619fc..66556c7e 100644
--- a/src/Core/Messages/ListResponse.cs
+++ b/src/Core/Messages/ListResponse.cs
@@ -4,7 +4,7 @@ namespace Cloud5mins.ShortenerTools.Core.Messages
{
public class ListResponse
{
- public List UrlList { get; set; }
+ public List UrlList { get; set; } = new();
public ListResponse() { }
public ListResponse(List list)
diff --git a/src/Core/Messages/ShortRequest.cs b/src/Core/Messages/ShortRequest.cs
index c366338d..2cf5092c 100644
--- a/src/Core/Messages/ShortRequest.cs
+++ b/src/Core/Messages/ShortRequest.cs
@@ -4,12 +4,12 @@ namespace Cloud5mins.ShortenerTools.Core.Messages
{
public class ShortRequest
{
- public string Vanity { get; set; }
+ public string Vanity { get; set; } = string.Empty;
- public string Url { get; set; }
+ public string Url { get; set; } = string.Empty;
- public string Title { get; set; }
+ public string Title { get; set; } = string.Empty;
- public Schedule[] Schedules { get; set; }
+ public Schedule[] Schedules { get; set; } = Array.Empty();
}
}
\ No newline at end of file
diff --git a/src/Core/Messages/ShortResponse.cs b/src/Core/Messages/ShortResponse.cs
index a4cf2df6..7a475688 100644
--- a/src/Core/Messages/ShortResponse.cs
+++ b/src/Core/Messages/ShortResponse.cs
@@ -2,9 +2,9 @@ namespace Cloud5mins.ShortenerTools.Core.Messages
{
public class ShortResponse
{
- public string ShortUrl { get; set; }
- public string LongUrl { get; set; }
- public string Title { get; set; }
+ public string ShortUrl { get; set; } = string.Empty;
+ public string LongUrl { get; set; } = string.Empty;
+ public string Title { get; set; } = string.Empty;
public ShortResponse() { }
public ShortResponse(string host, string longUrl, string endUrl, string title)
diff --git a/src/Core/Messages/ShortUrlRequest.cs b/src/Core/Messages/ShortUrlRequest.cs
index 0300e83e..32f76d30 100644
--- a/src/Core/Messages/ShortUrlRequest.cs
+++ b/src/Core/Messages/ShortUrlRequest.cs
@@ -7,7 +7,7 @@ namespace Cloud5mins.ShortenerTools.Core.Messages
{
public class ShortUrlRequest
{
- private string _vanity;
+ private string _vanity = string.Empty;
public string? Title { get; set; }
@@ -24,9 +24,9 @@ public string Vanity
}
[Required]
- public string Url { get; set; }
+ public string Url { get; set; } = string.Empty;
- private List _schedules;
+ private List _schedules = new();
public List Schedules
{
diff --git a/src/Core/Service/AzStrorageTablesService.cs b/src/Core/Service/AzStrorageTablesService.cs
index 4d2ea59e..d78e0f47 100644
--- a/src/Core/Service/AzStrorageTablesService.cs
+++ b/src/Core/Service/AzStrorageTablesService.cs
@@ -111,7 +111,7 @@ public async Task UpdateShortUrlEntity(ShortUrlEntity urlEntity)
public async Task GetShortUrlEntityByVanity(string vanity)
{
var tblUrls = GetUrlsTable();
- ShortUrlEntity shortUrlEntity = null;
+ ShortUrlEntity? shortUrlEntity = null;
var result = tblUrls.QueryAsync(e => e.RowKey == vanity);
await foreach (var entity in result)
@@ -126,14 +126,13 @@ public async Task GetShortUrlEntity(ShortUrlEntity row)
{
TableClient tblUrls = GetUrlsTable();
var response = await tblUrls.GetEntityAsync(row.PartitionKey, row.RowKey);
- ShortUrlEntity eShortUrl = response.Value as ShortUrlEntity;
- return eShortUrl;
+ return response.Value;
}
public async Task IfShortUrlEntityExistByVanity(string vanity)
{
- ShortUrlEntity shortUrlEntity = await GetShortUrlEntityByVanity(vanity);
+ ShortUrlEntity? shortUrlEntity = await GetShortUrlEntityByVanity(vanity);
return (shortUrlEntity != null);
}
@@ -155,11 +154,11 @@ public async Task ArchiveShortUrlEntity(ShortUrlEntity urlEntity
- public async Task> GetAllStatsByVanity(string vanity, string startDate, string endDate)
+ public async Task> GetAllStatsByVanity(string vanity, string? startDate, string? endDate)
{
var tblStats = GetStatsTable();
- var sDate = DateOnly.ParseExact(startDate, "yyyy-MM-dd", CultureInfo.InvariantCulture);
- var eDate = DateOnly.ParseExact(endDate, "yyyy-MM-dd", CultureInfo.InvariantCulture);
+ var sDate = DateOnly.ParseExact(startDate ?? DateOnly.MinValue.ToString("yyyy-MM-dd"), "yyyy-MM-dd", CultureInfo.InvariantCulture);
+ var eDate = DateOnly.ParseExact(endDate ?? DateOnly.MaxValue.ToString("yyyy-MM-dd"), "yyyy-MM-dd", CultureInfo.InvariantCulture);
var lstUrlStats = new List();
AsyncPageable queryResult;
diff --git a/src/Core/Service/Utility.cs b/src/Core/Service/Utility.cs
index 68b14daa..a3c23e03 100644
--- a/src/Core/Service/Utility.cs
+++ b/src/Core/Service/Utility.cs
@@ -94,8 +94,8 @@ public async static Task ExtractUrlsDataFromCSV(string fileFullName)
{
data.NextId = new NextId
{
- PartitionKey = csv.GetField("PartitionKey"),
- RowKey = csv.GetField("RowKey"),
+ PartitionKey = csv.GetField("PartitionKey") ?? String.Empty,
+ RowKey = csv.GetField("RowKey") ?? String.Empty,
Id = csv.GetField("Id")
};
}
@@ -103,11 +103,11 @@ public async static Task ExtractUrlsDataFromCSV(string fileFullName)
{
var record = new ShortUrlEntity
{
- PartitionKey = csv.GetField("PartitionKey"),
- RowKey = csv.GetField("RowKey"),
+ PartitionKey = csv.GetField("PartitionKey") ?? String.Empty,
+ RowKey = csv.GetField("RowKey") ?? String.Empty,
Clicks = csv.GetField("Clicks") ?? 0,
Title = csv.GetField("Title") ?? String.Empty,
- Url = csv.GetField("Url"),
+ Url = csv.GetField("Url") ?? String.Empty,
SchedulesPropertyRaw = csv.GetField("SchedulesPropertyRaw") ?? String.Empty,
IsArchived = csv.GetField("IsArchived") ?? false
};
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index b23f817d..42224a2b 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -1,39 +1,32 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj b/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj
index d6e1e138..ced86731 100644
--- a/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj
+++ b/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
v4
@@ -6,6 +6,7 @@
enable
enable
true
+ 713be2c6-6051-4bcf-ac1c-2ab228a0e42d
diff --git a/src/FunctionsLight/Program.cs b/src/FunctionsLight/Program.cs
index 6e68fedc..d236cb08 100644
--- a/src/FunctionsLight/Program.cs
+++ b/src/FunctionsLight/Program.cs
@@ -3,7 +3,7 @@
var builder = FunctionsApplication.CreateBuilder(args);
-builder.AddAzureTableClient("strTables");
+builder.AddAzureTableServiceClient("strTables");
builder.ConfigureFunctionsWebApplication();
diff --git a/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj b/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj
index 25d231f4..d332ee3a 100644
--- a/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj
+++ b/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj
@@ -1,14 +1,15 @@
-
+
net10.0
enable
enable
true
+ 5a6db536-4d9e-4bfd-8006-85420b82f175
-
+
diff --git a/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor b/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor
index 20a76e93..907a5cf2 100644
--- a/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor
+++ b/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor
@@ -1,25 +1,28 @@
@inherits LayoutComponentBase
-
- Tiny Blazor Admin
-
-
-
-
-
- @Body
-
-
-
-
-
-
-
+
+ Tiny Blazor Admin
+
+
+
+
+
+ @Body
+
+
+
+
+
+
+ Documentation and demos
+
+
+
+ About Blazor
+
+
+
@@ -27,13 +30,13 @@
- An unhandled error has occurred.
-
Reload
-
🗙
+ An unhandled error has occurred.
+
Reload
+
🗙
@code {
- public DesignThemeModes Mode { get; set; }
- public OfficeColor? OfficeColor { get; set; }
+ public DesignThemeModes Mode { get; set; }
+ public OfficeColor? OfficeColor { get; set; }
}
\ No newline at end of file
diff --git a/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor b/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor
index 74f1dd42..10fa37af 100644
--- a/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor
+++ b/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor
@@ -84,9 +84,8 @@
IQueryable? filteredUrlList => urlList?.Where(x => x.RowKey.Contains(vanityFilter, StringComparison.CurrentCultureIgnoreCase) && x.Title.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase) );
PaginationState pagination = new PaginationState { ItemsPerPage = 20 };
- ShortUrlRequest shortUrlRequest;
- ShortUrlEntity editedUrl;
- public List gridData { get; set; }
+ ShortUrlRequest shortUrlRequest = new();
+ public List gridData { get; set; } = new();
GridSort sortByClicks = GridSort.ByDescending(p => p.Clicks);
GridSort sortBySchedules = GridSort.ByDescending(p => p.Schedules.Count);
@@ -156,7 +155,6 @@
}
}
- [Inject] public IJSRuntime JsRuntime { get; set; }
public async Task CopyToClipboardAsync(string url)
{
await JSRuntime.InvokeVoidAsync("clipboardCopy.copyText", url);
diff --git a/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor b/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor
index c4348957..439341f6 100644
--- a/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor
+++ b/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor
@@ -33,7 +33,7 @@
@code {
[Parameter]
- public Schedule? schedule { get; set; }
+ public Schedule schedule { get; set; } = new();
private DateTime startDate;
private DateTime startTime;
diff --git a/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor b/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor
index 45428eea..dea0dda7 100644
--- a/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor
+++ b/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor
@@ -37,7 +37,7 @@
@code {
[Parameter]
- public List schedules { get; set; }
+ public List schedules { get; set; } = new();
private void AddScheduleClick(){
diff --git a/src/TinyBlazorAdmin/UrlManagerClient.cs b/src/TinyBlazorAdmin/UrlManagerClient.cs
index a2593c57..28a9f887 100644
--- a/src/TinyBlazorAdmin/UrlManagerClient.cs
+++ b/src/TinyBlazorAdmin/UrlManagerClient.cs
@@ -9,7 +9,7 @@ public class UrlManagerClient(HttpClient httpClient)
public async Task?> GetUrls()
{
- IQueryable urlList = null;
+ IQueryable? urlList = null;
try{
using var response = await httpClient.GetAsync("/api/UrlList");
if(response.IsSuccessStatusCode){
diff --git a/src/azure.yaml b/src/azure.yaml
index 929a3b2b..f0ab76bb 100644
--- a/src/azure.yaml
+++ b/src/azure.yaml
@@ -1,6 +1,6 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
-name: src
+name: azurlshortener
services:
app:
language: dotnet
diff --git a/src/budget-deployment/azure.yaml b/src/budget-deployment/azure.yaml
new file mode 100644
index 00000000..ed36068a
--- /dev/null
+++ b/src/budget-deployment/azure.yaml
@@ -0,0 +1,32 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+
+
+name: budget-azUrl
+infra:
+ provider: bicep
+ path: ./infra
+services:
+ azfunc-light:
+ project: ../FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj
+ language: dotnet
+ host: function
+
+ # The API is no longer a standalone App Service.
+ # It runs as a private sidecar container on the admin App Service
+ # (only reachable via http://localhost:8081 from within the same site unit).
+ # The sidecar image is built and pushed by the postprovision hook below.
+
+ admin:
+ project: ../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj
+ language: dotnet
+ host: appservice
+
+hooks:
+ # After infrastructure is provisioned, build the API container image with
+ # ACR Tasks (no local Docker daemon required) and restart the admin site so
+ # App Service pulls the fresh image for the sidecar container.
+ postprovision:
+ posix:
+ shell: sh
+ run: hooks/deploy-api-sidecar.sh
+ interactive: false
\ No newline at end of file
diff --git a/src/budget-deployment/hooks/deploy-api-sidecar.sh b/src/budget-deployment/hooks/deploy-api-sidecar.sh
new file mode 100755
index 00000000..e9873756
--- /dev/null
+++ b/src/budget-deployment/hooks/deploy-api-sidecar.sh
@@ -0,0 +1,112 @@
+#!/bin/sh
+# deploy-api-sidecar.sh
+#
+# Builds the API container image using ACR Tasks (no local Docker daemon needed),
+# pushes it to the project's ACR, then restarts the admin App Service so that
+# App Service pulls the fresh image for the API sidecar container.
+#
+# Environment variables provided by azd:
+# AZURE_CONTAINER_REGISTRY_NAME – ACR name (output from Bicep)
+# AZURE_ADMIN_APP_NAME – Admin App Service name (output from Bicep)
+# AZURE_RESOURCE_GROUP – Resource group name (set by azd)
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# Docker build context = src/ (solution root, two levels up from this script)
+SRC_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
+
+# azd may not export these variables into the shell inside a hook, even when the
+# environment file exists. Load the environment file explicitly from the project
+# root's .azure directory, not from the hooks folder.
+ENV_FILE=""
+if [ -n "${AZURE_ENV_NAME:-}" ] && [ -f "$SCRIPT_DIR/../.azure/${AZURE_ENV_NAME}/.env" ]; then
+ ENV_FILE="$SCRIPT_DIR/../.azure/${AZURE_ENV_NAME}/.env"
+fi
+
+if [ -z "$ENV_FILE" ]; then
+ for candidate in "$SCRIPT_DIR/../.azure"/*/.env; do
+ if [ -f "$candidate" ]; then
+ ENV_FILE="$candidate"
+ break
+ fi
+ done
+fi
+
+if [ -n "$ENV_FILE" ]; then
+ set -a
+ . "$ENV_FILE"
+ set +a
+fi
+
+# Fallback for azd hook shells that don't export env vars automatically.
+if [ -z "$AZURE_RESOURCE_GROUP" ] && [ -n "$AZURE_ENV_NAME" ]; then
+ # This project names the resource group as rg-
+ AZURE_RESOURCE_GROUP="rg-${AZURE_ENV_NAME}"
+fi
+
+if [ -z "$AZURE_RESOURCE_GROUP" ]; then
+ AZURE_RESOURCE_GROUP="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_RESOURCE_GROUP=/{print $2}' | tr -d '"' || true)"
+fi
+
+if [ -z "$AZURE_ADMIN_APP_NAME" ]; then
+ AZURE_ADMIN_APP_NAME="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_ADMIN_APP_NAME=/{print $2}' | tr -d '"' || true)"
+fi
+
+if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then
+ AZURE_CONTAINER_REGISTRY_NAME="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_CONTAINER_REGISTRY_NAME=/{print $2}' | tr -d '"' || true)"
+fi
+
+if [ -z "$AZURE_RESOURCE_GROUP" ]; then
+ echo "ERROR: AZURE_RESOURCE_GROUP is not set. Run 'azd provision' first."
+ exit 1
+fi
+
+if [ -z "$AZURE_ADMIN_APP_NAME" ]; then
+ echo "ERROR: AZURE_ADMIN_APP_NAME is not set. Run 'azd provision' first."
+ exit 1
+fi
+
+if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then
+ AZURE_CONTAINER_REGISTRY_NAME="$(az acr list -g "$AZURE_RESOURCE_GROUP" --query "[0].name" -o tsv 2>/dev/null || true)"
+fi
+
+if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then
+ echo "ERROR: AZURE_CONTAINER_REGISTRY_NAME is not set and no registry was found in resource group '$AZURE_RESOURCE_GROUP'. Run 'azd provision' first."
+ exit 1
+fi
+
+DOCKERFILE_PATH="Api/Dockerfile"
+if [ ! -f "$SRC_DIR/$DOCKERFILE_PATH" ]; then
+ echo "ERROR: Dockerfile not found at '$SRC_DIR/$DOCKERFILE_PATH'."
+ exit 1
+fi
+
+echo ""
+echo "==> Building and pushing API sidecar image to ACR: $AZURE_CONTAINER_REGISTRY_NAME"
+echo " Build context: $SRC_DIR"
+echo " Dockerfile: $DOCKERFILE_PATH"
+echo ""
+
+# 'az acr build' runs the Docker build inside Azure (no local Docker required).
+# Ensure the current working directory matches the build context so the relative
+# Dockerfile path resolves consistently regardless of the hook's shell location.
+(
+ cd "$SRC_DIR"
+ az acr build \
+ --registry "$AZURE_CONTAINER_REGISTRY_NAME" \
+ --image "api:latest" \
+ --file "$DOCKERFILE_PATH" \
+ .
+)
+
+echo ""
+echo "==> Restarting admin site '$AZURE_ADMIN_APP_NAME' to pull the new API sidecar image..."
+az webapp restart \
+ --name "$AZURE_ADMIN_APP_NAME" \
+ --resource-group "$AZURE_RESOURCE_GROUP"
+
+echo ""
+echo "==> API sidecar deployment complete."
+echo " The API is accessible only via http://localhost:8081 from within the"
+echo " site unit and is NOT reachable from the internet."
diff --git a/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep b/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep
new file mode 100644
index 00000000..a3d4d0de
--- /dev/null
+++ b/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep
@@ -0,0 +1,52 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param funcstoragea17ca_outputs_name string
+
+param principalType string
+
+param principalId string
+
+resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' existing = {
+ name: funcstoragea17ca_outputs_name
+}
+
+resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
+ principalType: principalType
+ }
+ scope: funcstoragea17ca
+}
+
+resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
+ principalType: principalType
+ }
+ scope: funcstoragea17ca
+}
+
+resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
+ principalType: principalType
+ }
+ scope: funcstoragea17ca
+}
+
+resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')
+ principalType: principalType
+ }
+ scope: funcstoragea17ca
+}
\ No newline at end of file
diff --git a/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep b/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep
new file mode 100644
index 00000000..335395d2
--- /dev/null
+++ b/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep
@@ -0,0 +1,35 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = {
+ name: take('funcstoragea17ca${uniqueString(resourceGroup().id)}', 24)
+ kind: 'StorageV2'
+ location: location
+ sku: {
+ name: 'Standard_GRS'
+ }
+ properties: {
+ accessTier: 'Hot'
+ allowSharedKeyAccess: false
+ isHnsEnabled: false
+ minimumTlsVersion: 'TLS1_2'
+ networkAcls: {
+ defaultAction: 'Allow'
+ }
+ }
+ tags: {
+ 'aspire-resource-name': 'funcstoragea17ca'
+ }
+}
+
+output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob
+
+output dataLakeEndpoint string = funcstoragea17ca.properties.primaryEndpoints.dfs
+
+output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue
+
+output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table
+
+output name string = funcstoragea17ca.name
+
+output id string = funcstoragea17ca.id
\ No newline at end of file
diff --git a/src/budget-deployment/infra/main.bicep b/src/budget-deployment/infra/main.bicep
new file mode 100644
index 00000000..75d5f1d8
--- /dev/null
+++ b/src/budget-deployment/infra/main.bicep
@@ -0,0 +1,93 @@
+targetScope = 'subscription'
+
+@minLength(1)
+@maxLength(64)
+@description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-')
+param environmentName string
+
+@minLength(1)
+@description('The location used for all deployed resources')
+param location string
+
+@description('Id of the user or app to assign application roles')
+param principalId string = ''
+
+param CustomDomain string
+param DefaultRedirectUrl string
+
+var tags = {
+ 'azd-env-name': environmentName
+}
+
+resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = {
+ name: 'rg-${environmentName}'
+ location: location
+ tags: tags
+}
+
+module resources './resources.bicep' = {
+ scope: rg
+ name: 'resources'
+ params: {
+ location: location
+ tags: tags
+ principalId: principalId
+ CustomDomain: CustomDomain
+ DefaultRedirectUrl: DefaultRedirectUrl
+ urlDataTableEndpoint: url_data.outputs.tableEndpoint
+ funcStorageBlobEndpoint: funcstoragea17ca.outputs.blobEndpoint
+ funcStorageQueueEndpoint: funcstoragea17ca.outputs.queueEndpoint
+ funcStorageTableEndpoint: funcstoragea17ca.outputs.tableEndpoint
+ }
+}
+
+module funcstoragea17ca './funcstoragea17ca/funcstoragea17ca.module.bicep' = {
+ name: 'funcstoragea17ca'
+ scope: rg
+ params: {
+ location: location
+ }
+}
+
+module funcstoragea17ca_roles './funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep' = {
+ name: 'funcstoragea17ca-roles'
+ scope: rg
+ params: {
+ funcstoragea17ca_outputs_name: funcstoragea17ca.outputs.name
+ location: location
+ principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID
+ principalType: 'ServicePrincipal'
+ }
+}
+
+module url_data './url-data/url-data.module.bicep' = {
+ name: 'url-data'
+ scope: rg
+ params: {
+ location: location
+ }
+}
+
+module url_data_roles './url-data-roles/url-data-roles.module.bicep' = {
+ name: 'url-data-roles'
+ scope: rg
+ params: {
+ location: location
+ principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID
+ principalType: 'ServicePrincipal'
+ url_data_outputs_name: url_data.outputs.name
+ }
+}
+
+output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID
+output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME
+output AZURE_APP_SERVICE_PLAN_NAME string = resources.outputs.AZURE_APP_SERVICE_PLAN_NAME
+output AZURE_FUNCTION_APP_NAME string = resources.outputs.AZURE_FUNCTION_APP_NAME
+output AZURE_ADMIN_APP_NAME string = resources.outputs.AZURE_ADMIN_APP_NAME
+output AZURE_CONTAINER_REGISTRY_NAME string = resources.outputs.AZURE_CONTAINER_REGISTRY_NAME
+output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT
+output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint
+output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint
+output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint
+output FUNCSTORAGEA17CA_TABLEENDPOINT string = funcstoragea17ca.outputs.tableEndpoint
+output URL_DATA_TABLEENDPOINT string = url_data.outputs.tableEndpoint
diff --git a/src/budget-deployment/infra/main.parameters.json b/src/budget-deployment/infra/main.parameters.json
new file mode 100644
index 00000000..396f07bc
--- /dev/null
+++ b/src/budget-deployment/infra/main.parameters.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "principalId": {
+ "value": "${AZURE_PRINCIPAL_ID}"
+ },
+ "CustomDomain": {
+ "value": "${AZURE_CUSTOM_DOMAIN}"
+ },
+ "DefaultRedirectUrl": {
+ "value": "${AZURE_DEFAULT_REDIRECT_URL}"
+ },
+ "environmentName": {
+ "value": "${AZURE_ENV_NAME}"
+ },
+ "location": {
+ "value": "${AZURE_LOCATION}"
+ }
+ }
+}
diff --git a/src/budget-deployment/infra/resources.bicep b/src/budget-deployment/infra/resources.bicep
new file mode 100644
index 00000000..a9533435
--- /dev/null
+++ b/src/budget-deployment/infra/resources.bicep
@@ -0,0 +1,271 @@
+@description('The location used for all deployed resources')
+param location string = resourceGroup().location
+
+@description('Id of the user or app to assign application roles')
+param principalId string = ''
+
+@description('Tags that will be applied to all resources')
+param tags object = {}
+
+@description('Custom domain passed to API service')
+param CustomDomain string
+
+@description('Default redirect URL passed to API and function services')
+param DefaultRedirectUrl string
+
+@description('Table service endpoint of the url-data storage account (for URL shortener data)')
+param urlDataTableEndpoint string
+
+@description('Blob service endpoint of the func storage account (AzureWebJobsStorage)')
+param funcStorageBlobEndpoint string
+
+@description('Queue service endpoint of the func storage account (AzureWebJobsStorage)')
+param funcStorageQueueEndpoint string
+
+@description('Table service endpoint of the func storage account (AzureWebJobsStorage)')
+param funcStorageTableEndpoint string
+
+var resourceToken = uniqueString(resourceGroup().id)
+
+resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
+ name: 'mi-${resourceToken}'
+ location: location
+ tags: tags
+}
+
+resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
+ name: 'asp-${resourceToken}'
+ location: location
+ sku: {
+ name: 'B1'
+ tier: 'Basic'
+ size: 'B1'
+ family: 'B'
+ capacity: 1
+ }
+ // Linux is required for App Service sidecar containers
+ kind: 'linux'
+ properties: {
+ reserved: true
+ }
+ tags: union(tags, {
+ 'aspire-resource-name': 'budget-appservice-plan'
+ })
+}
+
+// Container registry to host the API sidecar image
+resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
+ name: 'acr${resourceToken}'
+ location: location
+ sku: {
+ name: 'Basic'
+ }
+ properties: {
+ adminUserEnabled: false
+ }
+ tags: tags
+}
+
+// Allow the managed identity to pull images from ACR
+resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ // AcrPull built-in role: 7f951dda-4ed3-4680-a7ca-43fe172d538d
+ name: guid(containerRegistry.id, managedIdentity.id, '7f951dda-4ed3-4680-a7ca-43fe172d538d')
+ scope: containerRegistry
+ properties: {
+ principalId: managedIdentity.properties.principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource functionConsumptionPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
+ name: 'asp-func-${resourceToken}'
+ location: location
+ sku: {
+ name: 'Y1'
+ tier: 'Dynamic'
+ size: 'Y1'
+ family: 'Y'
+ capacity: 0
+ }
+ kind: 'functionapp'
+ properties: {
+ reserved: false
+ }
+ tags: union(tags, {
+ 'aspire-resource-name': 'budget-functions-plan'
+ })
+}
+
+resource functionSite 'Microsoft.Web/sites@2023-12-01' = {
+ name: 'func-${resourceToken}'
+ location: location
+ kind: 'functionapp'
+ identity: {
+ type: 'UserAssigned'
+ userAssignedIdentities: {
+ '${managedIdentity.id}': {}
+ }
+ }
+ properties: {
+ serverFarmId: functionConsumptionPlan.id
+ httpsOnly: true
+ siteConfig: {
+ ftpsState: 'Disabled'
+ minTlsVersion: '1.2'
+ appSettings: [
+ {
+ name: 'FUNCTIONS_EXTENSION_VERSION'
+ value: '~4'
+ }
+ {
+ name: 'FUNCTIONS_WORKER_RUNTIME'
+ value: 'dotnet-isolated'
+ }
+ {
+ name: 'AZURE_CLIENT_ID'
+ value: managedIdentity.properties.clientId
+ }
+ {
+ name: 'DefaultRedirectUrl'
+ value: DefaultRedirectUrl
+ }
+ // AzureWebJobsStorage uses identity-based access (no connection string)
+ {
+ name: 'AzureWebJobsStorage__blobServiceUri'
+ value: funcStorageBlobEndpoint
+ }
+ {
+ name: 'AzureWebJobsStorage__queueServiceUri'
+ value: funcStorageQueueEndpoint
+ }
+ {
+ name: 'AzureWebJobsStorage__tableServiceUri'
+ value: funcStorageTableEndpoint
+ }
+ {
+ name: 'AzureWebJobsStorage__credential'
+ value: 'managedidentity'
+ }
+ {
+ name: 'AzureWebJobsStorage__clientId'
+ value: managedIdentity.properties.clientId
+ }
+ // strTables connection used by AddAzureTableClient("strTables")
+ {
+ name: 'ConnectionStrings__strTables'
+ value: urlDataTableEndpoint
+ }
+ ]
+ }
+ }
+ tags: union(tags, {
+ 'azd-service-name': 'azfunc-light'
+ 'aspire-resource-name': 'azfunc-light'
+ })
+}
+
+// apiSite removed – the API is now deployed as a sidecar container on adminSite.
+// See the apiSidecar sitecontainer resource below.
+
+// Admin site – Linux code-based app (main container receives all external traffic).
+// The API runs as a sidecar container on the same site unit and is only
+// reachable via localhost:8080 (never from the internet).
+resource adminSite 'Microsoft.Web/sites@2023-12-01' = {
+ name: 'admin-${resourceToken}'
+ location: location
+ kind: 'app,linux'
+ identity: {
+ type: 'UserAssigned'
+ userAssignedIdentities: {
+ '${managedIdentity.id}': {}
+ }
+ }
+ properties: {
+ serverFarmId: appServicePlan.id
+ httpsOnly: true
+ siteConfig: {
+ // Linux App Service must be explicitly configured for the .NET runtime;
+ // otherwise it defaults to the generic PHP container and the admin app
+ // never receives the published Blazor app bundle.
+ linuxFxVersion: 'DOTNETCORE|10.0'
+ ftpsState: 'Disabled'
+ minTlsVersion: '1.2'
+ appSettings: [
+ // ── Admin settings ──────────────────────────────────────────────────
+ {
+ name: 'AZURE_CLIENT_ID'
+ value: managedIdentity.properties.clientId
+ }
+ // Aspire service-discovery: route API calls to the sidecar on localhost.
+ // The main admin app listens on 8080, so the private sidecar needs its
+ // own internal port to avoid a bind conflict in the shared site unit.
+ {
+ name: 'services__api__http__0'
+ value: 'http://localhost:8081'
+ }
+ // ── API settings (inherited by the sidecar via
+ // inheritAppSettingsAndConnectionStrings: true) ──────────────────
+ {
+ name: 'CustomDomain'
+ value: CustomDomain
+ }
+ {
+ name: 'DefaultRedirectUrl'
+ value: DefaultRedirectUrl
+ }
+ // strTables connection used by AddAzureTableClient("strTables")
+ {
+ name: 'ConnectionStrings__strTables'
+ value: urlDataTableEndpoint
+ }
+ ]
+ }
+ }
+ tags: union(tags, {
+ 'azd-service-name': 'admin'
+ 'aspire-resource-name': 'admin'
+ })
+}
+
+// API sidecar container – shares the same network namespace as adminSite.
+// App Service only routes inbound internet traffic to the main (admin) container;
+// this container is exclusively reachable via http://localhost:8081 from within
+// the site unit.
+resource apiSidecar 'Microsoft.Web/sites/sitecontainers@2024-04-01' = {
+ parent: adminSite
+ name: 'api'
+ properties: {
+ // Image is pushed to ACR by the postprovision hook.
+ // 'latest' is refreshed by restarting the site after each push.
+ image: '${containerRegistry.properties.loginServer}/api:latest'
+ isMain: false
+ targetPort: '8081'
+ // Pull image using the site's user-assigned managed identity.
+ // App Service passes all app settings from the main site into the sidecar
+ // unless a specific override is defined.
+ authType: 'UserAssigned'
+ userManagedIdentityClientId: managedIdentity.properties.clientId
+ }
+ dependsOn: [
+ acrPullRole
+ ]
+}
+
+resource principalRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) {
+ name: guid(resourceGroup().id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c'))
+ scope: resourceGroup()
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
+ }
+}
+
+output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId
+output MANAGED_IDENTITY_NAME string = managedIdentity.name
+output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId
+output AZURE_APP_SERVICE_PLAN_NAME string = appServicePlan.name
+output AZURE_FUNCTION_APP_NAME string = functionSite.name
+output AZURE_ADMIN_APP_NAME string = adminSite.name
+output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.name
+output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer
diff --git a/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep b/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep
new file mode 100644
index 00000000..e029cdc0
--- /dev/null
+++ b/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep
@@ -0,0 +1,42 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param url_data_outputs_name string
+
+param principalType string
+
+param principalId string
+
+resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' existing = {
+ name: url_data_outputs_name
+}
+
+resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
+ principalType: principalType
+ }
+ scope: url_data
+}
+
+resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
+ principalType: principalType
+ }
+ scope: url_data
+}
+
+resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
+ principalType: principalType
+ }
+ scope: url_data
+}
\ No newline at end of file
diff --git a/src/budget-deployment/infra/url-data/url-data.module.bicep b/src/budget-deployment/infra/url-data/url-data.module.bicep
new file mode 100644
index 00000000..6da8ab17
--- /dev/null
+++ b/src/budget-deployment/infra/url-data/url-data.module.bicep
@@ -0,0 +1,35 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = {
+ name: take('urldata${uniqueString(resourceGroup().id)}', 24)
+ kind: 'StorageV2'
+ location: location
+ sku: {
+ name: 'Standard_GRS'
+ }
+ properties: {
+ accessTier: 'Hot'
+ allowSharedKeyAccess: false
+ isHnsEnabled: false
+ minimumTlsVersion: 'TLS1_2'
+ networkAcls: {
+ defaultAction: 'Allow'
+ }
+ }
+ tags: {
+ 'aspire-resource-name': 'url-data'
+ }
+}
+
+output blobEndpoint string = url_data.properties.primaryEndpoints.blob
+
+output dataLakeEndpoint string = url_data.properties.primaryEndpoints.dfs
+
+output queueEndpoint string = url_data.properties.primaryEndpoints.queue
+
+output tableEndpoint string = url_data.properties.primaryEndpoints.table
+
+output name string = url_data.name
+
+output id string = url_data.id
\ No newline at end of file
diff --git a/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep b/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep
new file mode 100644
index 00000000..a3d4d0de
--- /dev/null
+++ b/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep
@@ -0,0 +1,52 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param funcstoragea17ca_outputs_name string
+
+param principalType string
+
+param principalId string
+
+resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' existing = {
+ name: funcstoragea17ca_outputs_name
+}
+
+resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
+ principalType: principalType
+ }
+ scope: funcstoragea17ca
+}
+
+resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
+ principalType: principalType
+ }
+ scope: funcstoragea17ca
+}
+
+resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
+ principalType: principalType
+ }
+ scope: funcstoragea17ca
+}
+
+resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')
+ principalType: principalType
+ }
+ scope: funcstoragea17ca
+}
\ No newline at end of file
diff --git a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep
index 01fb9a78..335395d2 100644
--- a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep
+++ b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep
@@ -1,10 +1,6 @@
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-param principalId string
-
-param principalType string
-
resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = {
name: take('funcstoragea17ca${uniqueString(resourceGroup().id)}', 24)
kind: 'StorageV2'
@@ -15,6 +11,7 @@ resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = {
properties: {
accessTier: 'Hot'
allowSharedKeyAccess: false
+ isHnsEnabled: false
minimumTlsVersion: 'TLS1_2'
networkAcls: {
defaultAction: 'Allow'
@@ -25,53 +22,14 @@ resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = {
}
}
-resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
- name: 'default'
- parent: funcstoragea17ca
-}
-
-resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
- properties: {
- principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
- principalType: principalType
- }
- scope: funcstoragea17ca
-}
-
-resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
- properties: {
- principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
- principalType: principalType
- }
- scope: funcstoragea17ca
-}
+output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob
-resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
- properties: {
- principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
- principalType: principalType
- }
- scope: funcstoragea17ca
-}
+output dataLakeEndpoint string = funcstoragea17ca.properties.primaryEndpoints.dfs
-resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab'))
- properties: {
- principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')
- principalType: principalType
- }
- scope: funcstoragea17ca
-}
+output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue
-output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob
+output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table
-output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue
+output name string = funcstoragea17ca.name
-output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table
\ No newline at end of file
+output id string = funcstoragea17ca.id
\ No newline at end of file
diff --git a/src/infra/main.bicep b/src/infra/main.bicep
index b43172b8..25faaf92 100644
--- a/src/infra/main.bicep
+++ b/src/infra/main.bicep
@@ -39,6 +39,14 @@ module funcstoragea17ca 'funcstoragea17ca/funcstoragea17ca.module.bicep' = {
scope: rg
params: {
location: location
+ }
+}
+module funcstoragea17ca_roles 'funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep' = {
+ name: 'funcstoragea17ca-roles'
+ scope: rg
+ params: {
+ funcstoragea17ca_outputs_name: funcstoragea17ca.outputs.name
+ location: location
principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID
principalType: 'ServicePrincipal'
}
@@ -46,10 +54,18 @@ module funcstoragea17ca 'funcstoragea17ca/funcstoragea17ca.module.bicep' = {
module url_data 'url-data/url-data.module.bicep' = {
name: 'url-data'
scope: rg
+ params: {
+ location: location
+ }
+}
+module url_data_roles 'url-data-roles/url-data-roles.module.bicep' = {
+ name: 'url-data-roles'
+ scope: rg
params: {
location: location
principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID
principalType: 'ServicePrincipal'
+ url_data_outputs_name: url_data.outputs.name
}
}
@@ -63,6 +79,7 @@ output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CO
output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID
output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN
output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint
+output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint
output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint
output FUNCSTORAGEA17CA_TABLEENDPOINT string = funcstoragea17ca.outputs.tableEndpoint
output URL_DATA_TABLEENDPOINT string = url_data.outputs.tableEndpoint
diff --git a/src/infra/main.json b/src/infra/main.json
new file mode 100644
index 00000000..91a34417
--- /dev/null
+++ b/src/infra/main.json
@@ -0,0 +1,685 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.45.15.27210",
+ "templateHash": "11546803755779573508"
+ }
+ },
+ "parameters": {
+ "environmentName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "metadata": {
+ "description": "Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-"
+ }
+ },
+ "location": {
+ "type": "string",
+ "minLength": 1,
+ "metadata": {
+ "description": "The location used for all deployed resources"
+ }
+ },
+ "principalId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Id of the user or app to assign application roles"
+ }
+ },
+ "CustomDomain": {
+ "type": "string"
+ },
+ "DefaultRedirectUrl": {
+ "type": "string"
+ }
+ },
+ "variables": {
+ "tags": {
+ "azd-env-name": "[parameters('environmentName')]"
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "apiVersion": "2022-09-01",
+ "name": "[format('rg-{0}', parameters('environmentName'))]",
+ "location": "[parameters('location')]",
+ "tags": "[variables('tags')]"
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "resources",
+ "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "tags": {
+ "value": "[variables('tags')]"
+ },
+ "principalId": {
+ "value": "[parameters('principalId')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.45.15.27210",
+ "templateHash": "17949110531045892075"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().location]",
+ "metadata": {
+ "description": "The location used for all deployed resources"
+ }
+ },
+ "principalId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Id of the user or app to assign application roles"
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {},
+ "metadata": {
+ "description": "Tags that will be applied to all resources"
+ }
+ }
+ },
+ "variables": {
+ "resourceToken": "[uniqueString(resourceGroup().id)]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.App/managedEnvironments/dotNetComponents",
+ "apiVersion": "2024-02-02-preview",
+ "name": "[format('{0}/{1}', format('cae-{0}', variables('resourceToken')), 'aspire-dashboard')]",
+ "properties": {
+ "componentType": "AspireDashboard"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "apiVersion": "2023-01-31",
+ "name": "[format('mi-{0}', variables('resourceToken'))]",
+ "location": "[parameters('location')]",
+ "tags": "[parameters('tags')]"
+ },
+ {
+ "type": "Microsoft.ContainerRegistry/registries",
+ "apiVersion": "2023-07-01",
+ "name": "[replace(format('acr-{0}', variables('resourceToken')), '-', '')]",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "Basic"
+ },
+ "tags": "[parameters('tags')]"
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', ''))]",
+ "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', '')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d'))]",
+ "properties": {
+ "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').principalId]",
+ "principalType": "ServicePrincipal",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', ''))]",
+ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.OperationalInsights/workspaces",
+ "apiVersion": "2022-10-01",
+ "name": "[format('law-{0}', variables('resourceToken'))]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "sku": {
+ "name": "PerGB2018"
+ }
+ },
+ "tags": "[parameters('tags')]"
+ },
+ {
+ "type": "Microsoft.App/managedEnvironments",
+ "apiVersion": "2024-02-02-preview",
+ "name": "[format('cae-{0}', variables('resourceToken'))]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "workloadProfiles": [
+ {
+ "workloadProfileType": "Consumption",
+ "name": "consumption"
+ }
+ ],
+ "appLogsConfiguration": {
+ "destination": "log-analytics",
+ "logAnalyticsConfiguration": {
+ "customerId": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken'))), '2022-10-01').customerId]",
+ "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken'))), '2022-10-01').primarySharedKey]"
+ }
+ }
+ },
+ "tags": "[parameters('tags')]",
+ "dependsOn": [
+ "[resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken')))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "MANAGED_IDENTITY_CLIENT_ID": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').clientId]"
+ },
+ "MANAGED_IDENTITY_NAME": {
+ "type": "string",
+ "value": "[format('mi-{0}', variables('resourceToken'))]"
+ },
+ "MANAGED_IDENTITY_PRINCIPAL_ID": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').principalId]"
+ },
+ "AZURE_LOG_ANALYTICS_WORKSPACE_NAME": {
+ "type": "string",
+ "value": "[format('law-{0}', variables('resourceToken'))]"
+ },
+ "AZURE_LOG_ANALYTICS_WORKSPACE_ID": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken')))]"
+ },
+ "AZURE_CONTAINER_REGISTRY_ENDPOINT": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', '')), '2023-07-01').loginServer]"
+ },
+ "AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken')))]"
+ },
+ "AZURE_CONTAINER_REGISTRY_NAME": {
+ "type": "string",
+ "value": "[replace(format('acr-{0}', variables('resourceToken')), '-', '')]"
+ },
+ "AZURE_CONTAINER_APPS_ENVIRONMENT_NAME": {
+ "type": "string",
+ "value": "[format('cae-{0}', variables('resourceToken'))]"
+ },
+ "AZURE_CONTAINER_APPS_ENVIRONMENT_ID": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken')))]"
+ },
+ "AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken'))), '2024-02-02-preview').defaultDomain]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "funcstoragea17ca",
+ "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.45.15.27210",
+ "templateHash": "5132421617093321654"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().location]",
+ "metadata": {
+ "description": "The location for the resource(s) to be deployed."
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Storage/storageAccounts",
+ "apiVersion": "2024-01-01",
+ "name": "[take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)]",
+ "kind": "StorageV2",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "Standard_GRS"
+ },
+ "properties": {
+ "accessTier": "Hot",
+ "allowSharedKeyAccess": false,
+ "isHnsEnabled": false,
+ "minimumTlsVersion": "TLS1_2",
+ "networkAcls": {
+ "defaultAction": "Allow"
+ }
+ },
+ "tags": {
+ "aspire-resource-name": "funcstoragea17ca"
+ }
+ }
+ ],
+ "outputs": {
+ "blobEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.blob]"
+ },
+ "dataLakeEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.dfs]"
+ },
+ "queueEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.queue]"
+ },
+ "tableEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.table]"
+ },
+ "name": {
+ "type": "string",
+ "value": "[take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)]"
+ },
+ "id": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24))]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "funcstoragea17ca-roles",
+ "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "funcstoragea17ca_outputs_name": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.name.value]"
+ },
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "principalId": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_PRINCIPAL_ID.value]"
+ },
+ "principalType": {
+ "value": "ServicePrincipal"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.45.15.27210",
+ "templateHash": "6014125912618429667"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().location]",
+ "metadata": {
+ "description": "The location for the resource(s) to be deployed."
+ }
+ },
+ "funcstoragea17ca_outputs_name": {
+ "type": "string"
+ },
+ "principalType": {
+ "type": "string"
+ },
+ "principalId": {
+ "type": "string"
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]",
+ "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))]",
+ "properties": {
+ "principalId": "[parameters('principalId')]",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]",
+ "principalType": "[parameters('principalType')]"
+ }
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]",
+ "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))]",
+ "properties": {
+ "principalId": "[parameters('principalId')]",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]",
+ "principalType": "[parameters('principalType')]"
+ }
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]",
+ "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))]",
+ "properties": {
+ "principalId": "[parameters('principalId')]",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]",
+ "principalType": "[parameters('principalType')]"
+ }
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]",
+ "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab'))]",
+ "properties": {
+ "principalId": "[parameters('principalId')]",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]",
+ "principalType": "[parameters('principalType')]"
+ }
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca')]",
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources')]",
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "url-data",
+ "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.45.15.27210",
+ "templateHash": "10282375894748324618"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().location]",
+ "metadata": {
+ "description": "The location for the resource(s) to be deployed."
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Storage/storageAccounts",
+ "apiVersion": "2024-01-01",
+ "name": "[take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)]",
+ "kind": "StorageV2",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "Standard_GRS"
+ },
+ "properties": {
+ "accessTier": "Hot",
+ "allowSharedKeyAccess": false,
+ "isHnsEnabled": false,
+ "minimumTlsVersion": "TLS1_2",
+ "networkAcls": {
+ "defaultAction": "Allow"
+ }
+ },
+ "tags": {
+ "aspire-resource-name": "url-data"
+ }
+ }
+ ],
+ "outputs": {
+ "blobEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.blob]"
+ },
+ "dataLakeEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.dfs]"
+ },
+ "queueEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.queue]"
+ },
+ "tableEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.table]"
+ },
+ "name": {
+ "type": "string",
+ "value": "[take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)]"
+ },
+ "id": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24))]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "url-data-roles",
+ "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "principalId": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_PRINCIPAL_ID.value]"
+ },
+ "principalType": {
+ "value": "ServicePrincipal"
+ },
+ "url_data_outputs_name": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data'), '2025-04-01').outputs.name.value]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.45.15.27210",
+ "templateHash": "5468789998190738231"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().location]",
+ "metadata": {
+ "description": "The location for the resource(s) to be deployed."
+ }
+ },
+ "url_data_outputs_name": {
+ "type": "string"
+ },
+ "principalType": {
+ "type": "string"
+ },
+ "principalId": {
+ "type": "string"
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]",
+ "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))]",
+ "properties": {
+ "principalId": "[parameters('principalId')]",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]",
+ "principalType": "[parameters('principalType')]"
+ }
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]",
+ "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))]",
+ "properties": {
+ "principalId": "[parameters('principalId')]",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]",
+ "principalType": "[parameters('principalType')]"
+ }
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]",
+ "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))]",
+ "properties": {
+ "principalId": "[parameters('principalId')]",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]",
+ "principalType": "[parameters('principalType')]"
+ }
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources')]",
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]",
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data')]"
+ ]
+ }
+ ],
+ "outputs": {
+ "MANAGED_IDENTITY_CLIENT_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_CLIENT_ID.value]"
+ },
+ "MANAGED_IDENTITY_NAME": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_NAME.value]"
+ },
+ "AZURE_LOG_ANALYTICS_WORKSPACE_NAME": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME.value]"
+ },
+ "AZURE_CONTAINER_REGISTRY_ENDPOINT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT.value]"
+ },
+ "AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID.value]"
+ },
+ "AZURE_CONTAINER_REGISTRY_NAME": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_NAME.value]"
+ },
+ "AZURE_CONTAINER_APPS_ENVIRONMENT_NAME": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME.value]"
+ },
+ "AZURE_CONTAINER_APPS_ENVIRONMENT_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID.value]"
+ },
+ "AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN.value]"
+ },
+ "FUNCSTORAGEA17CA_BLOBENDPOINT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.blobEndpoint.value]"
+ },
+ "FUNCSTORAGEA17CA_DATALAKEENDPOINT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.dataLakeEndpoint.value]"
+ },
+ "FUNCSTORAGEA17CA_QUEUEENDPOINT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.queueEndpoint.value]"
+ },
+ "FUNCSTORAGEA17CA_TABLEENDPOINT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.tableEndpoint.value]"
+ },
+ "URL_DATA_TABLEENDPOINT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data'), '2025-04-01').outputs.tableEndpoint.value]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/infra/resources.bicep b/src/infra/resources.bicep
index 6bedbd68..61533054 100644
--- a/src/infra/resources.bicep
+++ b/src/infra/resources.bicep
@@ -72,15 +72,6 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-p
}
-resource explicitContributorUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(containerAppEnvironment.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c'))
- scope: containerAppEnvironment
- properties: {
- principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
- }
-}
-
output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId
output MANAGED_IDENTITY_NAME string = managedIdentity.name
output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId
diff --git a/src/infra/url-data-roles/url-data-roles.module.bicep b/src/infra/url-data-roles/url-data-roles.module.bicep
new file mode 100644
index 00000000..e029cdc0
--- /dev/null
+++ b/src/infra/url-data-roles/url-data-roles.module.bicep
@@ -0,0 +1,42 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param url_data_outputs_name string
+
+param principalType string
+
+param principalId string
+
+resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' existing = {
+ name: url_data_outputs_name
+}
+
+resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
+ principalType: principalType
+ }
+ scope: url_data
+}
+
+resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
+ principalType: principalType
+ }
+ scope: url_data
+}
+
+resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
+ properties: {
+ principalId: principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
+ principalType: principalType
+ }
+ scope: url_data
+}
\ No newline at end of file
diff --git a/src/infra/url-data/url-data.module.bicep b/src/infra/url-data/url-data.module.bicep
index 248c278c..6da8ab17 100644
--- a/src/infra/url-data/url-data.module.bicep
+++ b/src/infra/url-data/url-data.module.bicep
@@ -1,10 +1,6 @@
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location
-param principalId string
-
-param principalType string
-
resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = {
name: take('urldata${uniqueString(resourceGroup().id)}', 24)
kind: 'StorageV2'
@@ -15,6 +11,7 @@ resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = {
properties: {
accessTier: 'Hot'
allowSharedKeyAccess: false
+ isHnsEnabled: false
minimumTlsVersion: 'TLS1_2'
networkAcls: {
defaultAction: 'Allow'
@@ -25,43 +22,14 @@ resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = {
}
}
-resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
- name: 'default'
- parent: url_data
-}
+output blobEndpoint string = url_data.properties.primaryEndpoints.blob
-resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
- properties: {
- principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
- principalType: principalType
- }
- scope: url_data
-}
+output dataLakeEndpoint string = url_data.properties.primaryEndpoints.dfs
-resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
- properties: {
- principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
- principalType: principalType
- }
- scope: url_data
-}
-
-resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
- properties: {
- principalId: principalId
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
- principalType: principalType
- }
- scope: url_data
-}
+output queueEndpoint string = url_data.properties.primaryEndpoints.queue
-output blobEndpoint string = url_data.properties.primaryEndpoints.blob
+output tableEndpoint string = url_data.properties.primaryEndpoints.table
-output queueEndpoint string = url_data.properties.primaryEndpoints.queue
+output name string = url_data.name
-output tableEndpoint string = url_data.properties.primaryEndpoints.table
\ No newline at end of file
+output id string = url_data.id
\ No newline at end of file
diff --git a/src/next-steps.md b/src/next-steps.md
index 4a558546..21e65176 100644
--- a/src/next-steps.md
+++ b/src/next-steps.md
@@ -17,10 +17,11 @@ To troubleshoot any issues, see [troubleshooting](#troubleshooting).
### Configure CI/CD pipeline
-1. Create a workflow pipeline file locally. The following starters are available:
- - [Deploy with GitHub Actions](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.github/workflows/azure-dev.yml)
- - [Deploy with Azure Pipelines](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.azdo/pipelines/azure-dev.yml)
-2. Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step.
+Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step.
+
+- Deploying with `GitHub Actions`: Select `GitHub` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration.
+
+- Deploying with `Azure DevOps Pipeline`: Select `Azure DevOps` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration.
## What was added
@@ -34,7 +35,7 @@ To describe the infrastructure and application, an `azure.yaml` was added with t
This file contains a single service, which references your project's App Host. When needed, `azd` generates the required infrastructure as code in memory and uses it.
-If you would like to see or modify the infrastructure that `azd` uses, run `azd infra synth` to persist it to disk.
+If you would like to see or modify the infrastructure that `azd` uses, run `azd infra gen` to generate it to disk.
If you do this, some additional directories will be created:
@@ -46,9 +47,7 @@ If you do this, some additional directories will be created:
In addition, for each project resource referenced by your app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. This file contains the infrastructure as code for running the project on Azure Container Apps.
-*Note*: Once you have synthesized your infrastructure to disk, changes made to your App Host will not be reflected in the infrastructure. You can re-generate the infrastructure by running `azd infra synth` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra synth` to overwrite the files without prompting.
-
-*Note*: `azd infra synth` is currently an alpha feature and must be explicitly enabled by running `azd config set alpha.infraSynth on`. You only need to do this once.
+*Note*: Once you have generated your infrastructure to disk, those files are the source of truth for azd. Any changes made to `azure.yaml` or your App Host will not be reflected in the infrastructure until you regenerate it with `azd infra gen` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra gen` to overwrite the files without prompting.
## Billing