diff --git a/.gitignore b/.gitignore
index f452f0fdcc7..79d0b07c380 100644
--- a/.gitignore
+++ b/.gitignore
@@ -147,4 +147,4 @@ node_modules/
*.svclog
# Python virtual environments
-.venv
\ No newline at end of file
+.venv
diff --git a/Aspire.sln b/Aspire.sln
index 77e05ffe136..29e219afabc 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -1,3 +1,4 @@
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
@@ -583,6 +584,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Dapr.Tests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.AWS.Tests", "tests\Aspire.Hosting.AWS.Tests\Aspire.Hosting.AWS.Tests.csproj", "{6F71BC73-B703-4E64-98E0-801781302E7A}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzureFunctionsEndToEnd", "AzureFunctionsEndToEnd", "{305D5B56-8782-493C-BD44-9860F8616D92}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureFunctionsEndToEnd.ApiService", "playground\AzureFunctionsEndToEnd\AzureFunctionsEndToEnd.ApiService\AzureFunctionsEndToEnd.ApiService.csproj", "{659AF918-57A4-4616-B3D0-24FCE38DF12A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureFunctionsEndToEnd.AppHost", "playground\AzureFunctionsEndToEnd\AzureFunctionsEndToEnd.AppHost\AzureFunctionsEndToEnd.AppHost.csproj", "{13025E2D-2E2B-4319-8754-0B12F324283E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureFunctionsEndToEnd.Functions", "playground\AzureFunctionsEndToEnd\AzureFunctionsEndToEnd.Functions\AzureFunctionsEndToEnd.Functions.csproj", "{79D2E40E-95EC-4BAF-8382-E5669B8E6E42}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.Functions", "src\Aspire.Hosting.Azure.Functions\Aspire.Hosting.Azure.Functions.csproj", "{A8FFEB1F-B128-48D0-A114-5C94FF770551}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "waitfor", "waitfor", "{3FF3F00C-95C0-46FC-B2BE-A3920C71E393}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WaitForSandbox.AppHost", "playground\waitfor\WaitForSandbox.AppHost\WaitForSandbox.AppHost.csproj", "{415E011A-1C56-41A1-BAEB-CA5D5CED1A57}"
@@ -1547,6 +1558,22 @@ Global
{6F71BC73-B703-4E64-98E0-801781302E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F71BC73-B703-4E64-98E0-801781302E7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F71BC73-B703-4E64-98E0-801781302E7A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {659AF918-57A4-4616-B3D0-24FCE38DF12A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {659AF918-57A4-4616-B3D0-24FCE38DF12A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {659AF918-57A4-4616-B3D0-24FCE38DF12A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {659AF918-57A4-4616-B3D0-24FCE38DF12A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {13025E2D-2E2B-4319-8754-0B12F324283E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {13025E2D-2E2B-4319-8754-0B12F324283E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {13025E2D-2E2B-4319-8754-0B12F324283E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {13025E2D-2E2B-4319-8754-0B12F324283E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {79D2E40E-95EC-4BAF-8382-E5669B8E6E42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {79D2E40E-95EC-4BAF-8382-E5669B8E6E42}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {79D2E40E-95EC-4BAF-8382-E5669B8E6E42}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {79D2E40E-95EC-4BAF-8382-E5669B8E6E42}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A8FFEB1F-B128-48D0-A114-5C94FF770551}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A8FFEB1F-B128-48D0-A114-5C94FF770551}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A8FFEB1F-B128-48D0-A114-5C94FF770551}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A8FFEB1F-B128-48D0-A114-5C94FF770551}.Release|Any CPU.Build.0 = Release|Any CPU
{415E011A-1C56-41A1-BAEB-CA5D5CED1A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{415E011A-1C56-41A1-BAEB-CA5D5CED1A57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{415E011A-1C56-41A1-BAEB-CA5D5CED1A57}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1844,6 +1871,11 @@ Global
{091EA540-355B-4763-9980-5F83F0BB6F11} = {15966C27-17FA-4A46-A172-55985411540A}
{C60C5CFA-5B6D-4432-BFCD-54D1BEEC7DBE} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{6F71BC73-B703-4E64-98E0-801781302E7A} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
+ {305D5B56-8782-493C-BD44-9860F8616D92} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
+ {659AF918-57A4-4616-B3D0-24FCE38DF12A} = {305D5B56-8782-493C-BD44-9860F8616D92}
+ {13025E2D-2E2B-4319-8754-0B12F324283E} = {305D5B56-8782-493C-BD44-9860F8616D92}
+ {79D2E40E-95EC-4BAF-8382-E5669B8E6E42} = {305D5B56-8782-493C-BD44-9860F8616D92}
+ {A8FFEB1F-B128-48D0-A114-5C94FF770551} = {77CFE74A-32EE-400C-8930-5025E8555256}
{3FF3F00C-95C0-46FC-B2BE-A3920C71E393} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{415E011A-1C56-41A1-BAEB-CA5D5CED1A57} = {3FF3F00C-95C0-46FC-B2BE-A3920C71E393}
{C554C480-3DA7-4D62-A09A-3F3F743D7A66} = {3FF3F00C-95C0-46FC-B2BE-A3920C71E393}
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 97c52f795e2..0b0b9392ba0 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -24,4 +24,21 @@
+
+
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1dabf4af9c5..c3958449ef6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -167,9 +167,18 @@
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/AzureFunctionsEndToEnd.ApiService.csproj b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/AzureFunctionsEndToEnd.ApiService.csproj
new file mode 100644
index 00000000000..9f7f87bb363
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/AzureFunctionsEndToEnd.ApiService.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs
new file mode 100644
index 00000000000..38d5b8317f7
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs
@@ -0,0 +1,67 @@
+using System.Security.Cryptography;
+using System.Text;
+#if !SKIP_EVENTHUBS_EMULATION
+using Azure.Messaging.EventHubs;
+using Azure.Messaging.EventHubs.Producer;
+#endif
+using Azure.Storage.Blobs;
+using Azure.Storage.Queues;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add service defaults & Aspire components.
+builder.AddServiceDefaults();
+builder.AddAzureQueueClient("queue");
+builder.AddAzureBlobClient("blob");
+#if !SKIP_EVENTHUBS_EMULATION
+builder.AddAzureEventHubProducerClient("eventhubs", static settings => settings.EventHubName = "myhub");
+#endif
+
+var app = builder.Build();
+
+app.MapGet("/publish/asq", async (QueueServiceClient client, CancellationToken cancellationToken) =>
+{
+ var queue = client.GetQueueClient("queue");
+ await queue.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
+ var data = Convert.ToBase64String(Encoding.UTF8.GetBytes("Hello, World!"));
+ await queue.SendMessageAsync(data, cancellationToken: cancellationToken);
+ return Results.Ok("Message sent to Azure Storage Queue.");
+});
+
+static string RandomString(int length)
+{
+ const string chars = "abcdefghijklmnopqrstuvwxyz";
+ return RandomNumberGenerator.GetString(chars, length);
+}
+
+app.MapGet("/publish/blob", async (BlobServiceClient client, CancellationToken cancellationToken, int length = 20) =>
+{
+ var container = client.GetBlobContainerClient("blobs");
+ await container.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
+
+ var entry = new { Id = Guid.NewGuid(), Text = RandomString(length) };
+ var blob = container.GetBlobClient(entry.Id.ToString());
+
+ await blob.UploadAsync(new BinaryData(entry));
+
+ return Results.Ok("String uploaded to Azure Storage Blobs.");
+});
+
+#if !SKIP_EVENTHUBS_EMULATION
+app.MapGet("/publish/eventhubs", async (EventHubProducerClient client, CancellationToken cancellationToken, int length = 20) =>
+{
+ var data = new BinaryData(Encoding.UTF8.GetBytes(RandomString(length)));
+ await client.SendAsync([new EventData(data)]);
+ return Results.Ok("Message sent to Azure EventHubs.");
+});
+#endif
+
+app.MapGet("/", async (HttpClient client) =>
+{
+ var stream = await client.GetStreamAsync("http://funcapp/api/weatherforecast");
+ return Results.Stream(stream, "application/json");
+});
+
+app.MapDefaultEndpoints();
+
+app.Run();
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Properties/launchSettings.json b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Properties/launchSettings.json
new file mode 100644
index 00000000000..96710726e07
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "publish/asq",
+ "applicationUrl": "http://localhost:5313",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "publish/asq",
+ "applicationUrl": "https://localhost:7314;http://localhost:5313",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/appsettings.Development.json b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/appsettings.Development.json
new file mode 100644
index 00000000000..0c208ae9181
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/appsettings.json b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/appsettings.json
new file mode 100644
index 00000000000..10f68b8c8b4
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/AzureFunctionsEndToEnd.AppHost.csproj b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/AzureFunctionsEndToEnd.AppHost.csproj
new file mode 100644
index 00000000000..f33cab6be79
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/AzureFunctionsEndToEnd.AppHost.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ d824db17-effc-4f8c-aa80-f0ae6aba93eb
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs
new file mode 100644
index 00000000000..be5ba8e4b4c
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs
@@ -0,0 +1,29 @@
+using Aspire.Hosting.Azure;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var storage = builder.AddAzureStorage("storage").RunAsEmulator();
+var queue = storage.AddQueues("queue");
+var blob = storage.AddBlobs("blob");
+
+#if !SKIP_EVENTHUBS_EMULATION
+var eventHubs = builder.AddAzureEventHubs("eventhubs").RunAsEmulator().AddEventHub("myhub");
+#endif
+
+var funcApp = builder.AddAzureFunctionsProject("funcapp")
+ .WithExternalHttpEndpoints()
+#if !SKIP_EVENTHUBS_EMULATION
+ .WithReference(eventHubs)
+#endif
+ .WithReference(blob)
+ .WithReference(queue);
+
+builder.AddProject("apiservice")
+#if !SKIP_EVENTHUBS_EMULATION
+ .WithReference(eventHubs)
+#endif
+ .WithReference(queue)
+ .WithReference(blob)
+ .WithReference(funcApp);
+
+builder.Build().Run();
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Properties/launchSettings.json b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000000..d06eb6fd9fc
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17244;http://localhost:15054",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21003",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22110"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15054",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19010",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20125"
+ }
+ }
+ }
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/appsettings.Development.json b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/appsettings.Development.json
new file mode 100644
index 00000000000..0c208ae9181
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/appsettings.json b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/appsettings.json
new file mode 100644
index 00000000000..31c092aa450
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/.dockerignore b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/.dockerignore
new file mode 100644
index 00000000000..1927772bc2e
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/.dockerignore
@@ -0,0 +1 @@
+local.settings.json
\ No newline at end of file
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/AzureFunctionsEndToEnd.Functions.csproj b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/AzureFunctionsEndToEnd.Functions.csproj
new file mode 100644
index 00000000000..850bd534c5a
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/AzureFunctionsEndToEnd.Functions.csproj
@@ -0,0 +1,42 @@
+
+
+ net8.0
+ v4
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
+
+
+
+
+
+ func
+ start --csharp --verbose
+
+
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Dockerfile b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Dockerfile
new file mode 100644
index 00000000000..f5bedbc00ee
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Dockerfile
@@ -0,0 +1,14 @@
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS installer-env
+
+COPY . /src/dotnet-function-app
+RUN cd /src/dotnet-function-app/AzureFunctionsEndToEnd.Functions && \
+mkdir -p /home/site/wwwroot && \
+dotnet publish *.csproj --output /home/site/wwwroot
+
+# To enable ssh & remote debugging on app service change the base image to the one below
+# FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0-appservice
+FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0
+ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
+ AzureFunctionsJobHost__Logging__Console__IsEnabled=true
+
+COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/HostBuilderExtensions.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/HostBuilderExtensions.cs
new file mode 100644
index 00000000000..0f30915d12c
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/HostBuilderExtensions.cs
@@ -0,0 +1,59 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Microsoft.Extensions.Hosting;
+
+public static class HostBuilderExtensions
+{
+ ///
+ /// The `AddServiceDefaults` method that Aspire provides out of the box is designed
+ /// to target IHostApplicationBuilder. Function Apps still use HostBuilder so we implement
+ /// a dupe of the `AddServiceDefaults` implementation here to get things working properly.
+ /// Long-term, Functions apps should use `IHostApplicationBuilder`.
+ ///
+ public static HostBuilder AddServiceDefaults(this HostBuilder builder)
+ {
+ builder.ConfigureLogging(x =>
+ {
+ x.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+ });
+
+ builder.ConfigureServices((context, services) =>
+ {
+ services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+ });
+
+ builder.ConfigureServices(services =>
+ {
+ services.AddServiceDiscovery();
+
+ services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ });
+ });
+
+ return builder;
+ }
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs
new file mode 100644
index 00000000000..f8a2a5ef5d0
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs
@@ -0,0 +1,16 @@
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.Logging;
+
+namespace AzureFunctionsEndToEnd.Functions;
+
+public class MyAzureBlobTrigger(ILogger logger)
+{
+ [Function(nameof(MyAzureBlobTrigger))]
+ [BlobOutput("test-files/{name}.txt", Connection = "blob")]
+ public string Run([BlobTrigger("blobs/{name}", Connection = "blob")] string triggerString)
+ {
+ logger.LogInformation("C# blob trigger function invoked with {message}...", triggerString);
+ return triggerString.ToUpper();
+ }
+}
+
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs
new file mode 100644
index 00000000000..0ea035d98c5
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs
@@ -0,0 +1,15 @@
+using Azure.Storage.Queues.Models;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.Logging;
+
+namespace AzureFunctionsEndToEnd.Functions;
+
+public class MyAzureQueueTrigger(ILogger logger)
+{
+ [Function(nameof(MyAzureQueueTrigger))]
+ public void Run([QueueTrigger("queue", Connection = "queue")] QueueMessage message)
+ {
+ logger.LogInformation("C# Queue trigger function processed: {Text}", message.MessageText);
+ }
+}
+
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyEventHubTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyEventHubTrigger.cs
new file mode 100644
index 00000000000..8cab0867ef9
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyEventHubTrigger.cs
@@ -0,0 +1,15 @@
+#if !SKIP_EVENTHUBS_EMULATION
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.Logging;
+
+namespace AspirePlusFunctions.Functions;
+
+public class MyEventHubTrigger(ILogger logger)
+{
+ [Function(nameof(MyEventHubTrigger))]
+ public void Run([EventHubTrigger("myhub", Connection = "eventhubs")] string[] input)
+ {
+ logger.LogInformation("C# EventHub trigger function processed: {Count} messages", input.Length);
+ }
+}
+#endif
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs
new file mode 100644
index 00000000000..ee0140cd12a
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.Logging;
+
+namespace AzureFunctionsEndToEnd.Functions;
+
+public class MyHttpTrigger(ILogger logger)
+{
+ [Function("weatherforecast")]
+ public IResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
+ {
+ logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ var forecast = Enumerable.Range(1, 5).Select(index =>
+ new WeatherForecast
+ (
+ DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
+ Random.Shared.Next(-20, 55),
+ "Warm and sunny in Azure Functions"
+ ))
+ .ToArray();
+
+ return Results.Ok(forecast);
+ }
+}
+
+public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
+{
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs
new file mode 100644
index 00000000000..e509752d83d
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs
@@ -0,0 +1,17 @@
+using Microsoft.Azure.Functions.Worker.OpenTelemetry;
+using Microsoft.Extensions.Hosting;
+using OpenTelemetry;
+using Microsoft.Extensions.DependencyInjection;
+
+var host = new HostBuilder()
+ .AddServiceDefaults()
+ .ConfigureFunctionsWebApplication()
+ .ConfigureServices(services =>
+ {
+ services.AddOpenTelemetry()
+ .UseFunctionsWorkerDefaults()
+ .UseOtlpExporter();
+ })
+ .Build();
+
+host.Run();
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Properties/launchSettings.json b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Properties/launchSettings.json
new file mode 100644
index 00000000000..696ecb572c1
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "AzureFunctionsEndToEnd_Functions": {
+ "commandName": "Project",
+ "launchBrowser": false
+ }
+ }
+}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/host.json b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/host.json
new file mode 100644
index 00000000000..aa5fcc18c65
--- /dev/null
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/host.json
@@ -0,0 +1,13 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ },
+ "enableLiveMetricsFilters": true
+ }
+ },
+ "telemetryMode": "openTelemetry"
+}
\ No newline at end of file
diff --git a/playground/Directory.Build.targets b/playground/Directory.Build.targets
index 4ffec86c99f..66767645cf9 100644
--- a/playground/Directory.Build.targets
+++ b/playground/Directory.Build.targets
@@ -3,8 +3,11 @@
+ building on CI -->
true
+
+ true
@@ -14,7 +17,8 @@
-
- SKIP_DASHBOARD_REFERENCE;$(DefineConstants)
+
+ SKIP_DASHBOARD_REFERENCE;$(DefineConstants)
+ SKIP_EVENTHUBS_EMULATION;$(DefineConstants)
diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs
index ec2a63bb600..7ff43dc6ae3 100644
--- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs
+++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs
@@ -16,7 +16,8 @@ namespace Aspire.Hosting.Azure;
public class AzureEventHubsResource(string name, Action configureConstruct) :
AzureConstructResource(name, configureConstruct),
IResourceWithConnectionString,
- IResourceWithEndpoints
+ IResourceWithEndpoints,
+ IResourceWithAzureFunctionsConfig
{
internal List<(string Name, Action, ResourceModuleConstruct, EventHub>? Configure)> Hubs { get; } = [];
@@ -39,4 +40,16 @@ public class AzureEventHubsResource(string name, Action
IsEmulator
? ReferenceExpression.Create($"Endpoint=sb://{EmulatorEndpoint.Property(EndpointProperty.Host)}:{EmulatorEndpoint.Property(EndpointProperty.Port)};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;")
: ReferenceExpression.Create($"{EventHubsEndpoint}");
+
+ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary target, string connectionName)
+ {
+ if (IsEmulator)
+ {
+ target[connectionName] = ConnectionStringExpression;
+ }
+ else
+ {
+ target[$"{connectionName}__fullyQualifiedNamespace"] = EventHubsEndpoint;
+ }
+ }
}
diff --git a/src/Aspire.Hosting.Azure.Functions/Aspire.Hosting.Azure.Functions.csproj b/src/Aspire.Hosting.Azure.Functions/Aspire.Hosting.Azure.Functions.csproj
new file mode 100644
index 00000000000..34262d21de0
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/Aspire.Hosting.Azure.Functions.csproj
@@ -0,0 +1,19 @@
+
+
+
+ $(NetCurrent)
+ true
+ aspire hosting azure functions
+ Azure Functions resource types for .NET Aspire.
+ $(SharedDir)Azure_256x.png
+
+
+
+ 0
+
+
+
+
+
+
+
diff --git a/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs
new file mode 100644
index 00000000000..61a18d80be2
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs
@@ -0,0 +1,145 @@
+// 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;
+
+///
+/// Extension methods for .
+///
+public static class AzureFunctionsProjectResourceExtensions
+{
+ ///
+ /// Adds an Azure Functions project to the distributed application.
+ ///
+ /// The type of the project metadata, which must implement and have a parameterless constructor.
+ /// The to which the Azure Functions project will be added.
+ /// The name to be associated with the Azure Functions project. This name will be used for service discovery when referenced in a dependency.
+ /// An for the added Azure Functions project resource.
+ public static IResourceBuilder AddAzureFunctionsProject(this IDistributedApplicationBuilder builder, string name) where TProject : IProjectMetadata, new()
+ {
+ var resource = new AzureFunctionsProjectResource(name);
+
+ // Add the default storage resource if it doesn't already exist.
+ var storage = builder.Resources.OfType().FirstOrDefault(r => r.Name == "azure-functions-default-storage");
+
+ if (storage is null)
+ {
+ storage = builder.AddAzureStorage("azure-functions-default-storage").RunAsEmulator().Resource;
+
+ builder.Eventing.Subscribe((data, token) =>
+ {
+ var removeStorage = true;
+ // Look at all of the resources and if none of them use the default storage, then we can remove it.
+ // This is because we're unable to cleanly add a resource to the builder from within a callback.
+ foreach (var item in data.Model.Resources.OfType())
+ {
+ if (item.HostStorage == storage)
+ {
+ removeStorage = false;
+ }
+
+ // Before the resource starts, we want to apply the azure functions specific environment variables.
+ // we look at all of the environment variables and apply the configuration for any resources that implement IResourceWithAzureFunctionsConfig.
+ item.Annotations.Add(new EnvironmentCallbackAnnotation(static context =>
+ {
+ var functionsConfigMapping = new Dictionary();
+ var valuesToRemove = new List();
+
+ foreach (var (envName, val) in context.EnvironmentVariables)
+ {
+ var (name, config) = val switch
+ {
+ IResourceWithAzureFunctionsConfig c => (c.Name, c),
+ ConnectionStringReference conn when conn.Resource is IResourceWithAzureFunctionsConfig c => (conn.ConnectionName ?? c.Name, c),
+ _ => ("", null)
+ };
+
+ if (config is not null)
+ {
+ valuesToRemove.Add(envName);
+ functionsConfigMapping[name] = config;
+ }
+ }
+
+ // REVIEW: We need to remove the existing values before adding the new ones as there's a conflict with the connection strings.
+ // we don't want to do this because it'll stop the aspire components from working in functions projects.
+ foreach (var envName in valuesToRemove)
+ {
+ context.EnvironmentVariables.Remove(envName);
+ }
+
+ foreach (var (name, config) in functionsConfigMapping)
+ {
+ config.ApplyAzureFunctionsConfiguration(context.EnvironmentVariables, name);
+ }
+
+ return Task.CompletedTask;
+ }));
+ }
+
+ if (removeStorage)
+ {
+ data.Model.Resources.Remove(storage);
+ }
+
+ return Task.CompletedTask;
+ });
+ }
+
+ resource.HostStorage = storage;
+
+ return builder.AddResource(resource)
+ .WithAnnotation(new TProject())
+ .WithEnvironment(context =>
+ {
+ context.EnvironmentVariables["OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES"] = "true";
+ context.EnvironmentVariables["OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES"] = "true";
+ context.EnvironmentVariables["OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY"] = "in_memory";
+ context.EnvironmentVariables["ASPNETCORE_FORWARDEDHEADERS_ENABLED"] = "true";
+ context.EnvironmentVariables["FUNCTIONS_WORKER_RUNTIME"] = "dotnet-isolated";
+
+ // Set the storage connection string.
+ ((IResourceWithAzureFunctionsConfig)resource.HostStorage).ApplyAzureFunctionsConfiguration(context.EnvironmentVariables, "Storage");
+ })
+ .WithArgs(context =>
+ {
+ var http = resource.GetEndpoint("http");
+ context.Args.Add("--port");
+ context.Args.Add(http.Property(EndpointProperty.TargetPort));
+ })
+ .WithOtlpExporter()
+ .WithHttpEndpoint()
+ .WithManifestPublishingCallback(async (context) =>
+ {
+ context.Writer.WriteString("type", "function.v0");
+ context.Writer.WriteString("path", context.GetManifestRelativePath(new TProject().ProjectPath));
+ await context.WriteEnvironmentVariablesAsync(resource).ConfigureAwait(false);
+ context.Writer.WriteStartObject("bindings");
+ foreach (var s in new string[] { "http", "https" })
+ {
+ context.Writer.WriteStartObject(s);
+ context.Writer.WriteString("scheme", s);
+ context.Writer.WriteString("protocol", "tcp");
+ context.Writer.WriteString("transport", "http");
+ context.Writer.WriteBoolean("external", true);
+ context.Writer.WriteEndObject();
+ }
+
+ context.Writer.WriteEndObject();
+ });
+ }
+
+ ///
+ /// Configures the Azure Functions project resource to use the specified Azure Storage resource as its host storage.
+ ///
+ /// The resource builder for the Azure Functions project resource.
+ /// The resource builder for the Azure Storage resource to be used as host storage.
+ /// The resource builder for the Azure Functions project resource, configured with the specified host storage.
+ public static IResourceBuilder WithHostStorage(this IResourceBuilder builder, IResourceBuilder storage)
+ {
+ builder.Resource.HostStorage = storage.Resource;
+ return builder;
+ }
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/AzureFunctionsResource.cs b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsResource.cs
new file mode 100644
index 00000000000..1c57a53d904
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsResource.cs
@@ -0,0 +1,20 @@
+// 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;
+
+///
+/// Represents an Azure Functions project resource within the Aspire hosting environment.
+///
+///
+/// This class is used to define and manage the configuration of an Azure Functions project,
+/// including its associated host storage. We create a strongly-typed resource for the Azure Functions
+/// to support Functions-specific customizations, like the mapping of connection strings and configurations
+/// for host storage.
+/// ///
+public class AzureFunctionsProjectResource(string name) : ProjectResource(name)
+{
+ internal AzureStorageResource? HostStorage { get; set; }
+}
diff --git a/src/Aspire.Hosting.Azure.Functions/PublicAPI.Shipped.txt b/src/Aspire.Hosting.Azure.Functions/PublicAPI.Shipped.txt
new file mode 100644
index 00000000000..7dc5c58110b
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Aspire.Hosting.Azure.Functions/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Azure.Functions/PublicAPI.Unshipped.txt
new file mode 100644
index 00000000000..4a6ca950364
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/PublicAPI.Unshipped.txt
@@ -0,0 +1,6 @@
+#nullable enable
+Aspire.Hosting.Azure.AzureFunctionsProjectResource
+Aspire.Hosting.Azure.AzureFunctionsProjectResource.AzureFunctionsProjectResource(string! name) -> void
+Aspire.Hosting.Azure.AzureFunctionsProjectResourceExtensions
+static Aspire.Hosting.Azure.AzureFunctionsProjectResourceExtensions.AddAzureFunctionsProject(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.Azure.AzureFunctionsProjectResourceExtensions.WithHostStorage(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! storage) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
diff --git a/src/Aspire.Hosting.Azure.Functions/README.md b/src/Aspire.Hosting.Azure.Functions/README.md
new file mode 100644
index 00000000000..206256dfdaf
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Functions/README.md
@@ -0,0 +1,107 @@
+# Aspire.Hosting.Azure.Functions library
+
+Provides methods to the .NET Aspire hosting model for Azure functions.
+
+## Getting started
+
+### Prerequisites
+
+* A .NET Aspire project based on the starter template.
+* A .NET-based Azure Functions worker project.
+
+### Install the package
+
+In your AppHost project, install the .NET Aspire Azure Functions Hosting library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Hosting.Azure.Functions
+```
+
+## Usage example
+
+Add the following `PropertyGroup` in your .NET-based Azure Functions project.
+
+```xml
+
+ func
+ start --csharp
+
+```
+
+The Aspire Azure Functions integration does not currently support ports configured in the launch profile of the Functions application.
+Remove the `commandLineArgs` property in the default `launchSettings.json` file:
+
+```diff
+{
+ "profiles": {
+ "Company.FunctionApp": {
+ "commandName": "Project",
+- "commandLineArgs": "--port 7071",
+ "launchBrowser": false
+ }
+ }
+}
+```
+
+Add a reference to the .NET-based Azure Functions project in your `AppHost` project.
+
+```dotnetcli
+dotnet add reference ..\Company.FunctionApp\Company.FunctionApp.csproj
+```
+
+In the _Program.cs_ file of `AppHost`, use the `AddAzureFunctionsProject` to configure the Functions project resource.
+
+```csharp
+using Aspire.Hosting;
+using Aspire.Hosting.Azure;
+using Aspire.Hosting.Azure.Functions;
+
+var builder = new DistributedApplicationBuilder();
+
+var storage = builder.AddAzureStorage("storage").RunAsEmulator();
+var queue = storage.AddQueues("queue");
+var blob = storage.AddBlobs("blob");
+
+builder.AddAzureFunctionsProject("my-functions-project")
+ .WithReference(queue)
+ .WithReference(blob);
+
+var app = builder.Build();
+
+app.Run();
+```
+
+## Current Limitations
+
+The Azure Functions integration currently only support Azure Storage Queues, Azure Storage Blobs, and Azure Event Hubs as resource references.
+
+The Azure Functions integration does not currently support OpenTelemetry from the locally running Azure Functions host.
+
+Due to a [current bug in the Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools/issues/3594), the Functions host may fail
+to find the target project to build:
+
+> Can't determine Project to build. Expected 1 .csproj or .fsproj but found 2
+
+To work around this issue, run the following commands in the Functions project directory:
+
+```dotnetcli
+cd Company.FunctionApp
+rm bin/ obj/
+func start --csharp
+```
+
+Then, update the `RunArguments` in the project file as follows:
+
+```diff
+
+ func
+- start --csharp
++ start --no-build --csharp
+
+```
+
+Stop the local Functions host running in `Company.FunctionApp` and re-run the Aspire AppHost.
+
+## Feedback & contributing
+
+https://github.com/dotnet/aspire
diff --git a/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs
index c8edef404e2..9c7591d3af5 100644
--- a/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs
+++ b/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs
@@ -12,7 +12,8 @@ namespace Aspire.Hosting.Azure;
/// The that the resource is stored in.
public class AzureBlobStorageResource(string name, AzureStorageResource storage) : Resource(name),
IResourceWithConnectionString,
- IResourceWithParent
+ IResourceWithParent,
+ IResourceWithAzureFunctionsConfig
{
///
/// Gets the parent AzureStorageResource of this AzureBlobStorageResource.
@@ -24,4 +25,18 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage)
///
public ReferenceExpression ConnectionStringExpression =>
Parent.GetBlobConnectionString();
+
+ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary target, string connectionName)
+ {
+ if (Parent.IsEmulator)
+ {
+ target[connectionName] = Parent.GetEmulatorConnectionString();
+ }
+ else
+ {
+ // Blob and Queue services are required to make blob triggers work.
+ target[$"{connectionName}__blobServiceUri"] = Parent.BlobEndpoint;
+ target[$"{connectionName}__queueServiceUri"] = Parent.QueueEndpoint;
+ }
+ }
}
diff --git a/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageResource.cs
index 549dac5725a..1b6adfe65fc 100644
--- a/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageResource.cs
+++ b/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageResource.cs
@@ -12,7 +12,8 @@ namespace Aspire.Hosting.Azure;
/// The that the resource is stored in.
public class AzureQueueStorageResource(string name, AzureStorageResource storage) : Resource(name),
IResourceWithConnectionString,
- IResourceWithParent
+ IResourceWithParent,
+ IResourceWithAzureFunctionsConfig
{
///
/// Gets the parent AzureStorageResource of this AzureQueueStorageResource.
@@ -24,4 +25,16 @@ public class AzureQueueStorageResource(string name, AzureStorageResource storage
///
public ReferenceExpression ConnectionStringExpression =>
Parent.GetQueueConnectionString();
+
+ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary target, string connectionName)
+ {
+ if (Parent.IsEmulator)
+ {
+ target[connectionName] = Parent.GetEmulatorConnectionString();
+ }
+ else
+ {
+ target[$"{connectionName}__queueServiceUri"] = Parent.QueueEndpoint;
+ }
+ }
}
diff --git a/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs
index 09fe2d345df..c809b8ac1af 100644
--- a/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs
+++ b/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs
@@ -12,7 +12,8 @@ namespace Aspire.Hosting.Azure;
/// Callback to populate the construct with Azure resources.
public class AzureStorageResource(string name, Action configureConstruct) :
AzureConstructResource(name, configureConstruct),
- IResourceWithEndpoints
+ IResourceWithEndpoints,
+ IResourceWithAzureFunctionsConfig
{
private EndpointReference EmulatorBlobEndpoint => new(this, "blob");
private EndpointReference EmulatorQueueEndpoint => new(this, "queue");
@@ -38,6 +39,14 @@ public class AzureStorageResource(string name, Action c
///
public bool IsEmulator => this.IsContainer();
+ ///
+ /// Gets the connection string for the Azure Storage emulator.
+ ///
+ ///
+ internal ReferenceExpression GetEmulatorConnectionString() => IsEmulator
+ ? ReferenceExpression.Create($"{AzureStorageEmulatorConnectionString.Create(blobPort: EmulatorBlobEndpoint.Port, queuePort: EmulatorQueueEndpoint.Port, tablePort: EmulatorTableEndpoint.Port)}")
+ : throw new InvalidOperationException("The Azure Storage resource is not running in the local emulator.");
+
internal ReferenceExpression GetTableConnectionString() => IsEmulator
? ReferenceExpression.Create($"{AzureStorageEmulatorConnectionString.Create(tablePort: EmulatorTableEndpoint.Port)}")
: ReferenceExpression.Create($"{TableEndpoint}");
@@ -49,4 +58,17 @@ internal ReferenceExpression GetQueueConnectionString() => IsEmulator
internal ReferenceExpression GetBlobConnectionString() => IsEmulator
? ReferenceExpression.Create($"{AzureStorageEmulatorConnectionString.Create(blobPort: EmulatorBlobEndpoint.Port)}")
: ReferenceExpression.Create($"{BlobEndpoint}");
+
+ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary target, string connectionName)
+ {
+ if (IsEmulator)
+ {
+ target[connectionName] = GetEmulatorConnectionString();
+ }
+ else
+ {
+ target[$"{connectionName}__blobServiceUri"] = BlobEndpoint;
+ target[$"{connectionName}__queueServiceUri"] = QueueEndpoint;
+ }
+ }
}
diff --git a/src/Aspire.Hosting.Azure/IResourceWithAzureFunctionsConfig.cs b/src/Aspire.Hosting.Azure/IResourceWithAzureFunctionsConfig.cs
new file mode 100644
index 00000000000..ec70998e8e7
--- /dev/null
+++ b/src/Aspire.Hosting.Azure/IResourceWithAzureFunctionsConfig.cs
@@ -0,0 +1,19 @@
+// 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;
+
+///
+/// Represents an resource that can provide configuration for Azure Functions.
+///
+public interface IResourceWithAzureFunctionsConfig : IResource
+{
+ ///
+ /// Applies the Azure Functions configuration to the target dictionary.
+ ///
+ /// The dictionary to which the Azure Functions configuration will be applied.
+ /// The name of the connection key to be used for the given configuration.
+ void ApplyAzureFunctionsConfiguration(IDictionary target, string connectionName);
+}
diff --git a/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt
index 245ea64008f..b9b544922b3 100644
--- a/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt
+++ b/src/Aspire.Hosting.Azure/PublicAPI.Unshipped.txt
@@ -1,4 +1,6 @@
#nullable enable
static Aspire.Hosting.AzureBicepResourceExtensions.WithParameter(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.EndpointReference! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
static Aspire.Hosting.AzureBicepResourceExtensions.WithParameter(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.ReferenceExpression! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+Aspire.Hosting.Azure.IResourceWithAzureFunctionsConfig
+Aspire.Hosting.Azure.IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(System.Collections.Generic.IDictionary! target, string! connectionName) -> void
static Aspire.Hosting.AzureConstructResourceExtensions.ConfigureConstruct(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Action! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
diff --git a/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs b/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs
index e1f1ba6d39e..7bd9472fa85 100644
--- a/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs
+++ b/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs
@@ -17,6 +17,11 @@ public class ConnectionStringReference(IResourceWithConnectionString resource, b
///
public bool Optional { get; } = optional;
+ ///
+ /// The name of the connection key.
+ ///
+ public string? ConnectionName { get; set; }
+
string IManifestExpressionProvider.ValueExpression => Resource.ValueExpression;
IEnumerable
+
+
+ SKIP_EVENTHUBS_EMULATION;$(DefineConstants)
@@ -24,6 +31,9 @@
picked up by msbuild by default -->
+
+
+
@@ -35,6 +45,7 @@
+
@@ -60,6 +71,9 @@
+
+
+
diff --git a/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs b/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs
index 7de2d3f5a10..49a917fd0f2 100644
--- a/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs
+++ b/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs
@@ -3,6 +3,7 @@
using Aspire.Hosting;
using Aspire.Hosting.Tests.Utils;
+using Aspire.Components.Common.Tests;
using SamplesIntegrationTests;
using SamplesIntegrationTests.Infrastructure;
using Xunit;
@@ -54,6 +55,74 @@ await WaitForAllTextAsync(app,
await app.StopAsync();
}
+ [Fact]
+ [ActiveIssue("https://github.com/dotnet/aspire/issues/5564", typeof(PlatformDetection), nameof(PlatformDetection.IsRunningOnCI))]
+ [RequiresDocker]
+ [RequiresTools(["func"])]
+ public async Task AzureFunctionsTest()
+ {
+ var appHostPath = Directory.GetFiles(AppContext.BaseDirectory, "AzureFunctionsEndToEnd.AppHost.dll").Single();
+ var appHost = await DistributedApplicationTestFactory.CreateAsync(appHostPath, _testOutput);
+ await using var app = await appHost.BuildAsync();
+
+ await app.StartAsync();
+ await app.WaitForResources().WaitAsync(TimeSpan.FromMinutes(2));
+
+ // Wait for the blobTrigger to be discovered as an indication that the host and worker
+ // has been successfully launched
+ await WaitForAllTextAsync(app,
+ [
+ "MyAzureBlobTrigger: blobTrigger"
+ ],
+ resourceName: "funcapp",
+ timeoutSecs: 160);
+
+ // Assert that HTTP triggers work correctly
+ await app.CreateHttpClient("funcapp").GetAsync("/api/weatherforecast");
+ await WaitForAllTextAsync(app,
+ [
+ "Executing HTTP request:",
+ "api/weatherforecast"
+ ],
+ resourceName: "funcapp",
+ timeoutSecs: 160);
+
+ // Assert that Azure Storage Queue triggers work correctly
+ await app.CreateHttpClient("apiservice").GetAsync("/publish/asq");
+ await WaitForAllTextAsync(app,
+ [
+ "Executed 'Functions.MyAzureQueueTrigger'"
+ ],
+ resourceName: "funcapp",
+ timeoutSecs: 160);
+
+ // Assert that Azure Storage Blob triggers work correctly
+ await app.CreateHttpClient("apiservice").GetAsync("/publish/blob");
+ await WaitForAllTextAsync(app,
+ [
+ "Executed 'Functions.MyAzureBlobTrigger'"
+ ],
+ resourceName: "funcapp",
+ timeoutSecs: 160);
+
+ // Assert that EventHubs triggers work correctly
+#if !SKIP_EVENTHUBS_EMULATION
+ await app.CreateHttpClient("apiservice").GetAsync("/publish/eventhubs");
+ await WaitForAllTextAsync(app,
+ [
+ "Executed 'Functions.MyEventHubTrigger'"
+ ],
+ resourceName: "funcapp",
+ timeoutSecs: 160);
+#endif
+
+ // TODO: The following line is commented out because the test fails due to an erroneous log in the Functions App
+ // resource that happens after the Functions host has been built. The error log shows up after the Functions
+ // worker extension has been built and before the host has launched.
+ // app.EnsureNoErrorsLogged();
+ await app.StopAsync();
+ }
+
internal static Task WaitForAllTextAsync(DistributedApplication app, IEnumerable logTexts, string? resourceName = null, int timeoutSecs = -1)
{
CancellationTokenSource cts = new();
diff --git a/tests/Shared/RepoTesting/Directory.Packages.Helix.props b/tests/Shared/RepoTesting/Directory.Packages.Helix.props
index e0d771d335e..173a1ce6788 100644
--- a/tests/Shared/RepoTesting/Directory.Packages.Helix.props
+++ b/tests/Shared/RepoTesting/Directory.Packages.Helix.props
@@ -23,6 +23,7 @@
+
diff --git a/tests/helix/send-to-helix-buildonhelixtests.targets b/tests/helix/send-to-helix-buildonhelixtests.targets
index 9029c46e586..751bb8ed0d9 100644
--- a/tests/helix/send-to-helix-buildonhelixtests.targets
+++ b/tests/helix/send-to-helix-buildonhelixtests.targets
@@ -4,6 +4,7 @@
$(BuildHelixWorkItemsDependsOn);BuildHelixWorkItemsForBuildOnHelixTests
true
true
+ true
$(TestArchiveTestsDirForBuildOnHelixTests)**/*.zip
diff --git a/tests/helix/send-to-helix-inner.proj b/tests/helix/send-to-helix-inner.proj
index ebcc638a185..781f6615663 100644
--- a/tests/helix/send-to-helix-inner.proj
+++ b/tests/helix/send-to-helix-inner.proj
@@ -26,6 +26,9 @@
<_CreateDotNetDevCertsDirectory>$(ArtifactsObjDir)create-dotnet-devcert
<_SupportDataStagingDir>$([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'support-data'))
+ <_AzureFunctionsCliUrl Condition="'$(OS)' == 'Windows_NT'">https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.5907/Azure.Functions.Cli.min.win-x64_net8.4.0.5907.zip
+ <_AzureFunctionsCliUrl Condition="'$(OS)' != 'Windows_NT'">https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.5907/Azure.Functions.Cli.linux-x64_net8.4.0.5907.zip
+
_StageDotNetCoverageTool;_StageCreateDotNetDevCertScripts
@@ -108,6 +111,15 @@
+
+
+
+
+
+
+
+
+