diff --git a/Aspire.sln b/Aspire.sln
index 4673be1ffb2..b6abcb7249a 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -665,6 +665,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.ContainerRegistry", "src\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj", "{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.AppService", "src\Aspire.Hosting.Azure.AppService\Aspire.Hosting.Azure.AppService.csproj", "{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzureAppService", "AzureAppService", "{2D9974C2-3AB2-FBFD-5156-080508BB7449}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAppService.ApiService", "playground\AzureAppService\AzureAppService.ApiService\AzureAppService.ApiService.csproj", "{A617DC84-65DA-41B5-B378-6C2F569CEE48}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAppService.AppHost", "playground\AzureAppService\AzureAppService.AppHost\AzureAppService.AppHost.csproj", "{2C879943-DF34-44FA-B2C3-29D97F24DD76}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -3891,6 +3899,42 @@ Global
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x64.Build.0 = Release|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.ActiveCfg = Release|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.Build.0 = Release|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x64.Build.0 = Debug|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x86.Build.0 = Debug|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x64.ActiveCfg = Release|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x64.Build.0 = Release|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x86.ActiveCfg = Release|Any CPU
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x86.Build.0 = Release|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Debug|x64.Build.0 = Debug|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Debug|x86.Build.0 = Debug|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Release|x64.ActiveCfg = Release|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Release|x64.Build.0 = Release|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Release|x86.ActiveCfg = Release|Any CPU
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48}.Release|x86.Build.0 = Release|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Debug|x64.Build.0 = Debug|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Debug|x86.Build.0 = Debug|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x64.ActiveCfg = Release|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x64.Build.0 = Release|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x86.ActiveCfg = Release|Any CPU
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -4208,7 +4252,11 @@ Global
{192747A2-9338-DECF-5C8C-28EB8E13829B} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{8FCA0CFA-7823-6A2F-342A-107A994915B0} = {C424395C-1235-41A4-BF55-07880A04368C}
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7} = {C424395C-1235-41A4-BF55-07880A04368C}
- {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3} = {77CFE74A-32EE-400C-8930-5025E8555256}
+ {5DDF8E89-FBBD-4A6F-BF32-7D2140724941} = {77CFE74A-32EE-400C-8930-5025E8555256}
+ {2D9974C2-3AB2-FBFD-5156-080508BB7449} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
+ {A617DC84-65DA-41B5-B378-6C2F569CEE48} = {2D9974C2-3AB2-FBFD-5156-080508BB7449}
+ {2C879943-DF34-44FA-B2C3-29D97F24DD76} = {2D9974C2-3AB2-FBFD-5156-080508BB7449}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {47DCFECF-5631-4BDE-A1EC-BE41E90F60C4}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b3825726499..ddb878f9ba9 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -32,6 +32,7 @@
+
diff --git a/playground/AzureAppService/.aspire/settings.json b/playground/AzureAppService/.aspire/settings.json
new file mode 100644
index 00000000000..21b430939bb
--- /dev/null
+++ b/playground/AzureAppService/.aspire/settings.json
@@ -0,0 +1,3 @@
+{
+ "appHostPath": "../AzureAppService.AppHost/AzureAppService.AppHost.csproj"
+}
\ No newline at end of file
diff --git a/playground/AzureAppService/AzureAppService.ApiService/AzureAppService.ApiService.csproj b/playground/AzureAppService/AzureAppService.ApiService/AzureAppService.ApiService.csproj
new file mode 100644
index 00000000000..84c50db2a4a
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.ApiService/AzureAppService.ApiService.csproj
@@ -0,0 +1,15 @@
+
+
+
+ $(DefaultTargetFramework)
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/playground/AzureAppService/AzureAppService.ApiService/Program.cs b/playground/AzureAppService/AzureAppService.ApiService/Program.cs
new file mode 100644
index 00000000000..b0cdbaa85d5
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.ApiService/Program.cs
@@ -0,0 +1,81 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Azure.Storage.Blobs;
+using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddCosmosDbContext("account", "db");
+builder.AddAzureBlobClient("blobs");
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+app.MapGet("/", () =>
+{
+ return Results.Content("""
+
+
+
+
+
+ """,
+ "text/html");
+});
+
+app.MapGet("/blobs", async (BlobServiceClient bsc) =>
+{
+ var container = bsc.GetBlobContainerClient("mycontainer");
+ await container.CreateIfNotExistsAsync();
+
+ var blobNameAndContent = Guid.NewGuid().ToString();
+ await container.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));
+
+ var blobs = container.GetBlobsAsync();
+
+ var blobNames = new List();
+
+ await foreach (var blob in blobs)
+ {
+ blobNames.Add(blob.Name);
+ }
+
+ return blobNames;
+});
+
+app.MapGet("/cosmos", async (TestCosmosContext context) =>
+{
+ await context.Database.EnsureCreatedAsync();
+
+ context.Entries.Add(new EntityFrameworkEntry());
+ await context.SaveChangesAsync();
+
+ return await context.Entries.ToListAsync();
+});
+
+app.Run();
+
+public class Entry
+{
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+}
+
+public class TestCosmosContext(DbContextOptions options) : DbContext(options)
+{
+ public DbSet Entries { get; set; }
+}
+
+public class EntityFrameworkEntry
+{
+ public Guid Id { get; set; } = Guid.NewGuid();
+}
+
diff --git a/playground/AzureAppService/AzureAppService.ApiService/Properties/launchSettings.json b/playground/AzureAppService/AzureAppService.ApiService/Properties/launchSettings.json
new file mode 100644
index 00000000000..de23e4696cf
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.ApiService/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5193",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/AzureAppService/AzureAppService.ApiService/appsettings.Development.json b/playground/AzureAppService/AzureAppService.ApiService/appsettings.Development.json
new file mode 100644
index 00000000000..0c208ae9181
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.ApiService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/AzureAppService/AzureAppService.ApiService/appsettings.json b/playground/AzureAppService/AzureAppService.ApiService/appsettings.json
new file mode 100644
index 00000000000..10f68b8c8b4
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/AzureAppService/AzureAppService.AppHost/AzureAppService.AppHost.csproj b/playground/AzureAppService/AzureAppService.AppHost/AzureAppService.AppHost.csproj
new file mode 100644
index 00000000000..77364f5bb2a
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/AzureAppService.AppHost.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ $(DefaultTargetFramework)
+ enable
+ enable
+ true
+ 9dc69458-f2b4-4306-9dc5-f7b8e398a3a9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/AzureAppService/AzureAppService.AppHost/Program.cs b/playground/AzureAppService/AzureAppService.AppHost/Program.cs
new file mode 100644
index 00000000000..7939e9fb629
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/Program.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.Azure;
+using Azure.Provisioning.Storage;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+builder.AddAppServiceEnvironment("infra");
+
+// Testing secret parameters
+var param = builder.AddParameter("secretparam", "fakeSecret", secret: true);
+
+// Testing kv secret refs
+var cosmosDb = builder.AddAzureCosmosDB("account")
+ .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent));
+
+cosmosDb.AddCosmosDatabase("db");
+
+// Testing managed identity
+var storage = builder.AddAzureStorage("storage")
+ .ConfigureInfrastructure(infra =>
+ {
+ var storage = infra.GetProvisionableResources().OfType().Single();
+ storage.AllowBlobPublicAccess = false;
+ })
+ .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent));
+var blobs = storage.AddBlobs("blobs");
+
+// Testing projects
+builder.AddProject("api")
+ .WithExternalHttpEndpoints()
+ .WithReference(blobs)
+ .WithRoleAssignments(storage, StorageBuiltInRole.StorageBlobDataContributor)
+ .WithReference(cosmosDb)
+ .WithEnvironment("VALUE", param)
+ .WithEnvironment(context =>
+ {
+ if (context.Resource.TryGetLastAnnotation(out var identity))
+ {
+ context.EnvironmentVariables["AZURE_PRINCIPAL_NAME"] = identity.IdentityResource.PrincipalName;
+ }
+ });
+
+#if !SKIP_DASHBOARD_REFERENCE
+// This project is only added in playground projects to support development/debugging
+// of the dashboard. It is not required in end developer code. Comment out this code
+// or build with `/p:SkipDashboardReference=true`, to test end developer
+// dashboard launch experience, Refer to Directory.Build.props for the path to
+// the dashboard binary (defaults to the Aspire.Dashboard bin output in the
+// artifacts dir).
+builder.AddProject(KnownResourceNames.AspireDashboard);
+#endif
+
+builder.Build().Run();
diff --git a/playground/AzureAppService/AzureAppService.AppHost/Properties/launchSettings.json b/playground/AzureAppService/AzureAppService.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000000..6f14f67dec0
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/Properties/launchSettings.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:15687;http://localhost:15688",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16167",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17317",
+ "ASPIRE_SHOW_DASHBOARD_RESOURCES": "true"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15688",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16167",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17318",
+ "ASPIRE_SHOW_DASHBOARD_RESOURCES": "true",
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
+ }
+ },
+ "generate-manifest": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "dotnetRunMessages": true,
+ "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json",
+ "applicationUrl": "http://localhost:15888",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16157"
+ }
+ }
+ }
+}
diff --git a/playground/AzureAppService/AzureAppService.AppHost/account.module.bicep b/playground/AzureAppService/AzureAppService.AppHost/account.module.bicep
new file mode 100644
index 00000000000..2aa3031838c
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/account.module.bicep
@@ -0,0 +1,39 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+resource account 'Microsoft.DocumentDB/databaseAccounts@2024-08-15' = {
+ name: take('account-${uniqueString(resourceGroup().id)}', 44)
+ location: location
+ properties: {
+ locations: [
+ {
+ locationName: location
+ failoverPriority: 0
+ }
+ ]
+ consistencyPolicy: {
+ defaultConsistencyLevel: 'Session'
+ }
+ databaseAccountOfferType: 'Standard'
+ disableLocalAuth: true
+ }
+ kind: 'GlobalDocumentDB'
+ tags: {
+ 'aspire-resource-name': 'account'
+ }
+}
+
+resource db 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-08-15' = {
+ name: 'db'
+ location: location
+ properties: {
+ resource: {
+ id: 'db'
+ }
+ }
+ parent: account
+}
+
+output connectionString string = account.properties.documentEndpoint
+
+output name string = account.name
\ No newline at end of file
diff --git a/playground/AzureAppService/AzureAppService.AppHost/api-identity.module.bicep b/playground/AzureAppService/AzureAppService.AppHost/api-identity.module.bicep
new file mode 100644
index 00000000000..0ff5902d5b0
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/api-identity.module.bicep
@@ -0,0 +1,15 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+resource api_identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
+ name: take('api_identity-${uniqueString(resourceGroup().id)}', 128)
+ location: location
+}
+
+output id string = api_identity.id
+
+output clientId string = api_identity.properties.clientId
+
+output principalId string = api_identity.properties.principalId
+
+output principalName string = api_identity.name
\ No newline at end of file
diff --git a/playground/AzureAppService/AzureAppService.AppHost/api-roles-account.module.bicep b/playground/AzureAppService/AzureAppService.AppHost/api-roles-account.module.bicep
new file mode 100644
index 00000000000..94e2505986b
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/api-roles-account.module.bicep
@@ -0,0 +1,25 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param account_outputs_name string
+
+param principalId string
+
+resource account 'Microsoft.DocumentDB/databaseAccounts@2024-08-15' existing = {
+ name: account_outputs_name
+}
+
+resource account_roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2024-08-15' existing = {
+ name: '00000000-0000-0000-0000-000000000002'
+ parent: account
+}
+
+resource account_roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-08-15' = {
+ name: guid(principalId, account_roleDefinition.id, account.id)
+ properties: {
+ principalId: principalId
+ roleDefinitionId: account_roleDefinition.id
+ scope: account.id
+ }
+ parent: account
+}
\ No newline at end of file
diff --git a/playground/AzureAppService/AzureAppService.AppHost/api-roles-storage.module.bicep b/playground/AzureAppService/AzureAppService.AppHost/api-roles-storage.module.bicep
new file mode 100644
index 00000000000..bab6fc34eb1
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/api-roles-storage.module.bicep
@@ -0,0 +1,20 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param storage_outputs_name string
+
+param principalId string
+
+resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' existing = {
+ name: storage_outputs_name
+}
+
+resource storage_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(storage.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: 'ServicePrincipal'
+ }
+ scope: storage
+}
\ No newline at end of file
diff --git a/playground/AzureAppService/AzureAppService.AppHost/api.module.bicep b/playground/AzureAppService/AzureAppService.AppHost/api.module.bicep
new file mode 100644
index 00000000000..c5b1d87d429
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/api.module.bicep
@@ -0,0 +1,90 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param infra_outputs_azure_container_registry_endpoint string
+
+param infra_outputs_planid string
+
+param infra_outputs_azure_container_registry_managed_identity_id string
+
+param infra_outputs_azure_container_registry_managed_identity_client_id string
+
+param api_containerimage string
+
+param api_containerport string
+
+param storage_outputs_blobendpoint string
+
+param account_outputs_connectionstring string
+
+@secure()
+param secretparam_value string
+
+param api_identity_outputs_principalname string
+
+param api_identity_outputs_id string
+
+param api_identity_outputs_clientid string
+
+resource webapp 'Microsoft.Web/sites@2024-04-01' = {
+ name: take('${toLower('api')}-${uniqueString(resourceGroup().id)}', 60)
+ location: location
+ properties: {
+ serverFarmId: infra_outputs_planid
+ keyVaultReferenceIdentity: api_identity_outputs_id
+ siteConfig: {
+ linuxFxVersion: 'DOCKER|${api_containerimage}'
+ acrUseManagedIdentityCreds: true
+ acrUserManagedIdentityID: infra_outputs_azure_container_registry_managed_identity_client_id
+ appSettings: [
+ {
+ name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES'
+ value: 'true'
+ }
+ {
+ name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES'
+ value: 'true'
+ }
+ {
+ name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY'
+ value: 'in_memory'
+ }
+ {
+ name: 'ASPNETCORE_FORWARDEDHEADERS_ENABLED'
+ value: 'true'
+ }
+ {
+ name: 'HTTP_PORTS'
+ value: api_containerport
+ }
+ {
+ name: 'ConnectionStrings__blobs'
+ value: storage_outputs_blobendpoint
+ }
+ {
+ name: 'ConnectionStrings__account'
+ value: account_outputs_connectionstring
+ }
+ {
+ name: 'VALUE'
+ value: secretparam_value
+ }
+ {
+ name: 'AZURE_PRINCIPAL_NAME'
+ value: api_identity_outputs_principalname
+ }
+ {
+ name: 'AZURE_CLIENT_ID'
+ value: api_identity_outputs_clientid
+ }
+ ]
+ }
+ }
+ identity: {
+ type: 'UserAssigned'
+ userAssignedIdentities: {
+ '${infra_outputs_azure_container_registry_managed_identity_id}': { }
+ '${api_identity_outputs_id}': { }
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/AzureAppService/AzureAppService.AppHost/appsettings.Development.json b/playground/AzureAppService/AzureAppService.AppHost/appsettings.Development.json
new file mode 100644
index 00000000000..0c208ae9181
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/AzureAppService/AzureAppService.AppHost/appsettings.json b/playground/AzureAppService/AzureAppService.AppHost/appsettings.json
new file mode 100644
index 00000000000..31c092aa450
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/playground/AzureAppService/AzureAppService.AppHost/aspire-manifest.json b/playground/AzureAppService/AzureAppService.AppHost/aspire-manifest.json
new file mode 100644
index 00000000000..2e8a59d0c58
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/aspire-manifest.json
@@ -0,0 +1,106 @@
+{
+ "$schema": "https://json.schemastore.org/aspire-8.0.json",
+ "resources": {
+ "infra": {
+ "type": "azure.bicep.v0",
+ "path": "infra.module.bicep",
+ "params": {
+ "userPrincipalId": ""
+ }
+ },
+ "secretparam": {
+ "type": "parameter.v0",
+ "value": "{secretparam.inputs.value}",
+ "inputs": {
+ "value": {
+ "type": "string",
+ "secret": true
+ }
+ }
+ },
+ "account": {
+ "type": "azure.bicep.v0",
+ "connectionString": "{account.outputs.connectionString}",
+ "path": "account.module.bicep"
+ },
+ "db": {
+ "type": "value.v0",
+ "connectionString": "AccountEndpoint={account.outputs.connectionString};Database=db"
+ },
+ "storage": {
+ "type": "azure.bicep.v0",
+ "path": "storage.module.bicep"
+ },
+ "blobs": {
+ "type": "value.v0",
+ "connectionString": "{storage.outputs.blobEndpoint}"
+ },
+ "api": {
+ "type": "project.v1",
+ "path": "../AzureAppService.ApiService/AzureAppService.ApiService.csproj",
+ "deployment": {
+ "type": "azure.bicep.v0",
+ "path": "api.module.bicep",
+ "params": {
+ "infra_outputs_azure_container_registry_endpoint": "{infra.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
+ "infra_outputs_planid": "{infra.outputs.planId}",
+ "infra_outputs_azure_container_registry_managed_identity_id": "{infra.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}",
+ "infra_outputs_azure_container_registry_managed_identity_client_id": "{infra.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID}",
+ "api_containerimage": "{api.containerImage}",
+ "api_containerport": "{api.containerPort}",
+ "storage_outputs_blobendpoint": "{storage.outputs.blobEndpoint}",
+ "account_outputs_connectionstring": "{account.outputs.connectionString}",
+ "secretparam_value": "{secretparam.value}",
+ "api_identity_outputs_principalname": "{api-identity.outputs.principalName}",
+ "api_identity_outputs_id": "{api-identity.outputs.id}",
+ "api_identity_outputs_clientid": "{api-identity.outputs.clientId}"
+ }
+ },
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "HTTP_PORTS": "{api.bindings.http.targetPort}",
+ "ConnectionStrings__blobs": "{blobs.connectionString}",
+ "ConnectionStrings__account": "{account.connectionString}",
+ "VALUE": "{secretparam.value}",
+ "AZURE_PRINCIPAL_NAME": "{api-identity.outputs.principalName}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "external": true
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http",
+ "external": true
+ }
+ }
+ },
+ "api-identity": {
+ "type": "azure.bicep.v0",
+ "path": "api-identity.module.bicep"
+ },
+ "api-roles-storage": {
+ "type": "azure.bicep.v0",
+ "path": "api-roles-storage.module.bicep",
+ "params": {
+ "storage_outputs_name": "{storage.outputs.name}",
+ "principalId": "{api-identity.outputs.principalId}"
+ }
+ },
+ "api-roles-account": {
+ "type": "azure.bicep.v0",
+ "path": "api-roles-account.module.bicep",
+ "params": {
+ "account_outputs_name": "{account.outputs.name}",
+ "principalId": "{api-identity.outputs.principalId}"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/AzureAppService/AzureAppService.AppHost/infra.module.bicep b/playground/AzureAppService/AzureAppService.AppHost/infra.module.bicep
new file mode 100644
index 00000000000..4cd6db10e5e
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/infra.module.bicep
@@ -0,0 +1,54 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param userPrincipalId string
+
+param tags object = { }
+
+resource infra_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
+ name: take('infra_mi-${uniqueString(resourceGroup().id)}', 128)
+ location: location
+ tags: tags
+}
+
+resource infra_acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
+ name: take('infraacr${uniqueString(resourceGroup().id)}', 50)
+ location: location
+ sku: {
+ name: 'Basic'
+ }
+ tags: tags
+}
+
+resource infra_acr_infra_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(infra_acr.id, infra_mi.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d'))
+ properties: {
+ principalId: infra_mi.properties.principalId
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
+ principalType: 'ServicePrincipal'
+ }
+ scope: infra_acr
+}
+
+resource infra_asplan 'Microsoft.Web/serverfarms@2024-04-01' = {
+ name: take('infraasplan-${uniqueString(resourceGroup().id)}', 60)
+ location: location
+ properties: {
+ reserved: true
+ }
+ kind: 'Linux'
+ sku: {
+ name: 'B1'
+ tier: 'Basic'
+ }
+}
+
+output planId string = infra_asplan.id
+
+output AZURE_CONTAINER_REGISTRY_NAME string = infra_acr.name
+
+output AZURE_CONTAINER_REGISTRY_ENDPOINT string = infra_acr.properties.loginServer
+
+output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = infra_mi.id
+
+output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID string = infra_mi.properties.clientId
\ No newline at end of file
diff --git a/playground/AzureAppService/AzureAppService.AppHost/storage.module.bicep b/playground/AzureAppService/AzureAppService.AppHost/storage.module.bicep
new file mode 100644
index 00000000000..9588f49e52a
--- /dev/null
+++ b/playground/AzureAppService/AzureAppService.AppHost/storage.module.bicep
@@ -0,0 +1,36 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
+ name: take('storage${uniqueString(resourceGroup().id)}', 24)
+ kind: 'StorageV2'
+ location: location
+ sku: {
+ name: 'Standard_GRS'
+ }
+ properties: {
+ accessTier: 'Hot'
+ allowBlobPublicAccess: false
+ allowSharedKeyAccess: false
+ minimumTlsVersion: 'TLS1_2'
+ networkAcls: {
+ defaultAction: 'Allow'
+ }
+ }
+ tags: {
+ 'aspire-resource-name': 'storage'
+ }
+}
+
+resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
+ name: 'default'
+ parent: storage
+}
+
+output blobEndpoint string = storage.properties.primaryEndpoints.blob
+
+output queueEndpoint string = storage.properties.primaryEndpoints.queue
+
+output tableEndpoint string = storage.properties.primaryEndpoints.table
+
+output name string = storage.name
\ No newline at end of file
diff --git a/src/Aspire.Hosting.Azure.AppContainers/ContainerAppContext.cs b/src/Aspire.Hosting.Azure.AppContainers/ContainerAppContext.cs
index 06a0abff8d5..a97a1bfb28e 100644
--- a/src/Aspire.Hosting.Azure.AppContainers/ContainerAppContext.cs
+++ b/src/Aspire.Hosting.Azure.AppContainers/ContainerAppContext.cs
@@ -628,26 +628,7 @@ private BicepValue AllocateKeyVaultSecretUriReference(BicepSecretOutputR
private BicepValue AllocateKeyVaultSecretUriReference(IAzureKeyVaultSecretReference secretOutputReference)
{
- if (!KeyVaultRefs.TryGetValue(secretOutputReference.Resource.Name, out var kv))
- {
- // We resolve the keyvault that represents the storage for secret outputs
- var parameter = AllocateParameter(secretOutputReference.Resource.NameOutputReference);
- kv = KeyVaultService.FromExisting($"{parameter.BicepIdentifier}_kv");
- kv.Name = parameter;
-
- KeyVaultRefs[secretOutputReference.Resource.Name] = kv;
- }
-
- if (!KeyVaultSecretRefs.TryGetValue(secretOutputReference.ValueExpression, out var secret))
- {
- // Now we resolve the secret
- var secretBicepIdentifier = Infrastructure.NormalizeBicepIdentifier($"{kv.BicepIdentifier}_{secretOutputReference.SecretName}");
- secret = KeyVaultSecret.FromExisting(secretBicepIdentifier);
- secret.Name = secretOutputReference.SecretName;
- secret.Parent = kv;
-
- KeyVaultSecretRefs[secretOutputReference.ValueExpression] = secret;
- }
+ var secret = secretOutputReference.AsKeyVaultSecret(Infra);
return secret.Properties.SecretUri;
}
diff --git a/src/Aspire.Hosting.Azure.AppContainers/IContainerRegistry.cs b/src/Aspire.Hosting.Azure.AppContainers/IContainerRegistry.cs
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj b/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj
new file mode 100644
index 00000000000..b004368b6a6
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj
@@ -0,0 +1,23 @@
+
+
+
+ $(DefaultTargetFramework)
+ true
+ aspire appservice integration hosting azure
+ Azure app service resource types for .NET Aspire.
+ $(SharedDir)Azure_256x.png
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceComputeResourceExtensions.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceComputeResourceExtensions.cs
new file mode 100644
index 00000000000..3e5c83eadc2
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceComputeResourceExtensions.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure;
+using Azure.Provisioning.AppService;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for publishing compute resources as Azure App Service websites.
+///
+public static class AzureAppServiceComputeResourceExtensions
+{
+ ///
+ /// Publishes the specified compute resource as an Azure App Service.
+ ///
+ /// The type of the compute resource.
+ /// The compute resource builder.
+ /// The configuration action for the App Service WebSite resource.
+ /// The updated compute resource builder.
+ ///
+ ///
+ ///
+ /// builder.AddProject<Projects.Api>("name").PublishAsAzureAppServiceWebsite((infrastructure, app) =>
+ /// {
+ /// // Configure the App Service WebSite resource here
+ /// });
+ ///
+ ///
+ ///
+ public static IResourceBuilder PublishAsAzureAppServiceWebsite(this IResourceBuilder builder, Action configure)
+#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ where T : IComputeResource
+#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configure);
+
+ if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
+ {
+ return builder;
+ }
+
+ return builder.WithAnnotation(new AzureAppServiceWebsiteCustomizationAnnotation(configure));
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentContext.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentContext.cs
new file mode 100644
index 00000000000..efff206e3d8
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentContext.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure.AppService;
+using Microsoft.Extensions.Logging;
+
+namespace Aspire.Hosting.Azure;
+
+internal sealed class AzureAppServiceEnvironmentContext(
+ ILogger logger,
+ DistributedApplicationExecutionContext executionContext,
+ AzureAppServiceEnvironmentResource environment)
+{
+ public ILogger Logger => logger;
+
+ public DistributedApplicationExecutionContext ExecutionContext => executionContext;
+
+ public AzureAppServiceEnvironmentResource Environment => environment;
+
+ private readonly Dictionary _appServices = [];
+
+ public AzureAppServiceWebsiteContext GetAppServiceContext(IResource resource)
+ {
+ if (!_appServices.TryGetValue(resource, out var context))
+ {
+ throw new InvalidOperationException($"App Service context not found for resource {resource.Name}.");
+ }
+
+ return context;
+ }
+
+ public async Task CreateAppServiceAsync(IResource resource, AzureProvisioningOptions provisioningOptions, CancellationToken cancellationToken)
+ {
+ if (!_appServices.TryGetValue(resource, out var context))
+ {
+ _appServices[resource] = context = new AzureAppServiceWebsiteContext(resource, this);
+ await context.ProcessAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ var provisioningResource = new AzureProvisioningResource(resource.Name, context.BuildWebSite)
+ {
+ ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions
+ };
+
+ return provisioningResource;
+ }
+}
\ No newline at end of file
diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs
new file mode 100644
index 00000000000..39f4eff4c81
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs
@@ -0,0 +1,130 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure;
+using Aspire.Hosting.Azure.AppService;
+using Aspire.Hosting.Lifecycle;
+using Azure.Provisioning;
+using Azure.Provisioning.AppService;
+using Azure.Provisioning.ContainerRegistry;
+using Azure.Provisioning.Expressions;
+using Azure.Provisioning.Roles;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Hosting;
+
+///
+/// Extensions for adding Azure App Service Environment resources to a distributed application builder.
+///
+public static partial class AzureAppServiceEnvironmentExtensions
+{
+ ///
+ /// Adds a azure app service environment resource to the distributed application builder.
+ ///
+ /// The distributed application builder.
+ /// The name of the resource.
+ ///
+ public static IResourceBuilder AddAppServiceEnvironment(this IDistributedApplicationBuilder builder, string name)
+ {
+ builder.AddAzureProvisioning();
+ builder.Services.Configure(options => options.SupportsTargetedRoleAssignments = true);
+
+ if (builder.ExecutionContext.IsPublishMode)
+ {
+ builder.Services.TryAddLifecycleHook();
+ }
+
+ var resource = new AzureAppServiceEnvironmentResource(name, static infra =>
+ {
+ var prefix = infra.AspireResource.Name;
+ var resource = infra.AspireResource;
+
+ // This tells azd to avoid creating infrastructure
+ var userPrincipalId = new ProvisioningParameter(AzureBicepResource.KnownParameters.UserPrincipalId, typeof(string));
+ infra.Add(userPrincipalId);
+
+ var tags = new ProvisioningParameter("tags", typeof(object))
+ {
+ Value = new BicepDictionary()
+ };
+
+ infra.Add(tags);
+
+ var identity = new UserAssignedIdentity(Infrastructure.NormalizeBicepIdentifier($"{prefix}-mi"))
+ {
+ Tags = tags
+ };
+
+ infra.Add(identity);
+
+ ContainerRegistryService? containerRegistry = null;
+ if (resource.TryGetLastAnnotation(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource registry)
+ {
+ containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
+ }
+ else
+ {
+ containerRegistry = new ContainerRegistryService(Infrastructure.NormalizeBicepIdentifier($"{prefix}_acr"))
+ {
+ Sku = new() { Name = ContainerRegistrySkuName.Basic },
+ Tags = tags
+ };
+ }
+
+ infra.Add(containerRegistry);
+
+ var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, identity);
+
+ // There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
+ pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, identity.Id, pullRa.RoleDefinitionId);
+ infra.Add(pullRa);
+
+ var plan = new AppServicePlan(Infrastructure.NormalizeBicepIdentifier($"{prefix}-asplan"))
+ {
+ Sku = new AppServiceSkuDescription
+ {
+ Name = "B1",
+ Tier = "Basic"
+ },
+ Kind = "Linux",
+ IsReserved = true
+ };
+
+ infra.Add(plan);
+
+ infra.Add(new ProvisioningOutput("planId", typeof(string))
+ {
+ Value = plan.Id
+ });
+
+ infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
+ {
+ Value = containerRegistry.Name
+ });
+
+ // AZD looks for this output to find the container registry endpoint
+ infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
+ {
+ Value = containerRegistry.LoginServer
+ });
+
+ infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
+ {
+ Value = identity.Id
+ });
+
+ infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID", typeof(string))
+ {
+ Value = identity.ClientId
+ });
+ });
+
+ if (!builder.ExecutionContext.IsPublishMode)
+ {
+ return builder.CreateResourceBuilder(resource);
+ }
+
+ return builder.AddResource(resource);
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs
new file mode 100644
index 00000000000..e3984cd743c
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.Azure.AppService;
+
+///
+/// Represents an Azure App Service Environment resource.
+///
+/// The name of the Azure App Service Environment.
+/// The callback to configure the Azure infrastructure for this resource.
+public class AzureAppServiceEnvironmentResource(string name, Action configureInfrastructure) :
+ AzureProvisioningResource(name, configureInfrastructure),
+#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ IComputeEnvironmentResource,
+ IAzureContainerRegistry
+#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+{
+ // We don't want these to be public if we end up with an app service
+ // per compute resource.
+ internal BicepOutputReference PlanIdOutputReference => new("planId", this);
+ internal BicepOutputReference ContainerRegistryUrl => new("AZURE_CONTAINER_REGISTRY_ENDPOINT", this);
+ internal BicepOutputReference ContainerRegistryName => new("AZURE_CONTAINER_REGISTRY_NAME", this);
+ internal BicepOutputReference ContainerRegistryManagedIdentityId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", this);
+ internal BicepOutputReference ContainerRegistryClientId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID", this);
+
+ ReferenceExpression IAzureContainerRegistry.ManagedIdentityId =>
+ ReferenceExpression.Create($"{ContainerRegistryManagedIdentityId}");
+
+ ReferenceExpression IContainerRegistry.Name =>
+ ReferenceExpression.Create($"{ContainerRegistryName}");
+
+ ReferenceExpression IContainerRegistry.Endpoint =>
+ ReferenceExpression.Create($"{ContainerRegistryUrl}");
+}
\ No newline at end of file
diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs
new file mode 100644
index 00000000000..db5fa1e203f
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Lifecycle;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Aspire.Hosting.Azure.AppService;
+
+internal sealed class AzureAppServiceInfrastructure(
+ ILogger logger,
+ IOptions provisioningOptions,
+ DistributedApplicationExecutionContext executionContext) :
+ IDistributedApplicationLifecycleHook
+{
+ public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
+ {
+ if (!executionContext.IsPublishMode)
+ {
+ return;
+ }
+
+ var appServiceEnvironment = appModel.Resources.OfType().FirstOrDefault()
+ ?? throw new InvalidOperationException("AppServiceEnvironmentResource not found.");
+
+ var appServiceEnvironmentContext = new AzureAppServiceEnvironmentContext(
+ logger,
+ executionContext,
+ appServiceEnvironment);
+
+ foreach (var resource in appModel.Resources)
+ {
+ if (resource.TryGetLastAnnotation(out var lastAnnotation) && lastAnnotation == ManifestPublishingCallbackAnnotation.Ignore)
+ {
+ continue;
+ }
+
+ // We only support project resources for now.
+ if (resource is not ProjectResource)
+ {
+ continue;
+ }
+
+ var website = await appServiceEnvironmentContext.CreateAppServiceAsync(resource, provisioningOptions.Value, cancellationToken).ConfigureAwait(false);
+
+#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ resource.Annotations.Add(new DeploymentTargetAnnotation(website)
+ {
+ ContainerRegistry = appServiceEnvironment,
+ ComputeEnvironment = appServiceEnvironment
+ });
+#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
+ }
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebsiteContext.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebsiteContext.cs
new file mode 100644
index 00000000000..bb8ed7f68c9
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebsiteContext.cs
@@ -0,0 +1,346 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+using System.Globalization;
+using Aspire.Hosting.ApplicationModel;
+using Azure.Provisioning;
+using Azure.Provisioning.AppService;
+using Azure.Provisioning.Expressions;
+using Azure.Provisioning.Resources;
+
+namespace Aspire.Hosting.Azure.AppService;
+
+internal sealed class AzureAppServiceWebsiteContext(
+ IResource resource,
+ AzureAppServiceEnvironmentContext environmentContext)
+{
+ public IResource Resource => resource;
+
+ record struct EndpointMapping(string Scheme, BicepValue Host, int Port, int? TargetPort, bool IsHttpIngress, bool External);
+
+ private readonly Dictionary _endpointMapping = [];
+
+ // Resolved environment variables and command line args
+ // These contain the values that need to be further transformed into
+ // bicep compatible values
+ public Dictionary EnvironmentVariables { get; } = [];
+ public List