From 7f3fc997086d4cb43341eb6bed89e8a64eef9295 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 17 Mar 2025 17:50:04 +0800 Subject: [PATCH 01/18] client integration for Azure App Config --- Aspire.sln | 48 ++++++++++++++++++ Directory.Packages.props | 1 + .../AppHost/AppHost.csproj | 23 +++++++++ .../AzureAppConfiguration/AppHost/Program.cs | 12 +++++ .../AppHost/Properties/launchSettings.json | 29 +++++++++++ .../AppHost/appsettings.Development.json | 8 +++ .../AppHost/appsettings.json | 16 ++++++ .../WorkerService/Program.cs | 24 +++++++++ .../Properties/launchSettings.json | 12 +++++ .../WorkerService/Worker.cs | 35 +++++++++++++ .../WorkerService/WorkerService.csproj | 18 +++++++ .../appsettings.Development.json | 8 +++ .../WorkerService/appsettings.json | 8 +++ playground/Directory.Packages.props | 3 ++ .../Aspire.Azure.Data.AppConfiguration.csproj | 19 +++++++ .../AspireAppConfigurationExtensions.cs | 50 +++++++++++++++++++ .../AzureDataAppConfigurationSettings.cs | 22 ++++++++ 17 files changed, 336 insertions(+) create mode 100644 playground/AzureAppConfiguration/AppHost/AppHost.csproj create mode 100644 playground/AzureAppConfiguration/AppHost/Program.cs create mode 100644 playground/AzureAppConfiguration/AppHost/Properties/launchSettings.json create mode 100644 playground/AzureAppConfiguration/AppHost/appsettings.Development.json create mode 100644 playground/AzureAppConfiguration/AppHost/appsettings.json create mode 100644 playground/AzureAppConfiguration/WorkerService/Program.cs create mode 100644 playground/AzureAppConfiguration/WorkerService/Properties/launchSettings.json create mode 100644 playground/AzureAppConfiguration/WorkerService/Worker.cs create mode 100644 playground/AzureAppConfiguration/WorkerService/WorkerService.csproj create mode 100644 playground/AzureAppConfiguration/WorkerService/appsettings.Development.json create mode 100644 playground/AzureAppConfiguration/WorkerService/appsettings.json create mode 100644 src/Components/Aspire.Azure.Data.AppConfiguration/Aspire.Azure.Data.AppConfiguration.csproj create mode 100644 src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs create mode 100644 src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs diff --git a/Aspire.sln b/Aspire.sln index ed0c202bf94..73365b4f113 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -665,6 +665,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Docker.Tests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Kubernetes.Tests", "tests\Aspire.Hosting.Kubernetes.Tests\Aspire.Hosting.Kubernetes.Tests.csproj", "{582B06FD-CC56-4C58-8138-D92F8FE62BBD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Data.AppConfiguration", "src\Components\Aspire.Azure.Data.AppConfiguration\Aspire.Azure.Data.AppConfiguration.csproj", "{34E74A65-3891-49AC-9C3E-8D049E078D4D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzureAppConfiguration", "AzureAppConfiguration", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "playground\AzureAppConfiguration\AppHost\AppHost.csproj", "{426E8FEA-7077-4433-959C-E5B1E5F26CC0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkerService", "playground\AzureAppConfiguration\WorkerService\WorkerService.csproj", "{5B26B478-95AF-9F3E-D5E7-D7535855470F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -3891,6 +3899,42 @@ Global {582B06FD-CC56-4C58-8138-D92F8FE62BBD}.Release|x64.Build.0 = Release|Any CPU {582B06FD-CC56-4C58-8138-D92F8FE62BBD}.Release|x86.ActiveCfg = Release|Any CPU {582B06FD-CC56-4C58-8138-D92F8FE62BBD}.Release|x86.Build.0 = Release|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Debug|x64.ActiveCfg = Debug|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Debug|x64.Build.0 = Debug|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Debug|x86.ActiveCfg = Debug|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Debug|x86.Build.0 = Debug|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Release|Any CPU.Build.0 = Release|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Release|x64.ActiveCfg = Release|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Release|x64.Build.0 = Release|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Release|x86.ActiveCfg = Release|Any CPU + {34E74A65-3891-49AC-9C3E-8D049E078D4D}.Release|x86.Build.0 = Release|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Debug|x64.Build.0 = Debug|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Debug|x86.Build.0 = Debug|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Release|Any CPU.Build.0 = Release|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Release|x64.ActiveCfg = Release|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Release|x64.Build.0 = Release|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Release|x86.ActiveCfg = Release|Any CPU + {426E8FEA-7077-4433-959C-E5B1E5F26CC0}.Release|x86.Build.0 = Release|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Debug|x64.Build.0 = Debug|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Debug|x86.Build.0 = Debug|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|Any CPU.Build.0 = Release|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|x64.ActiveCfg = Release|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|x64.Build.0 = Release|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|x86.ActiveCfg = Release|Any CPU + {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -4210,6 +4254,10 @@ Global {3AE2ED5B-4EC7-4E6D-A61D-C68B837E5FA7} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47} {43B560D6-F158-4A4C-8E43-981056EB9038} = {830A89EC-4029-4753-B25A-068BAE37DEC7} {582B06FD-CC56-4C58-8138-D92F8FE62BBD} = {830A89EC-4029-4753-B25A-068BAE37DEC7} + {34E74A65-3891-49AC-9C3E-8D049E078D4D} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} + {426E8FEA-7077-4433-959C-E5B1E5F26CC0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {5B26B478-95AF-9F3E-D5E7-D7535855470F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {47DCFECF-5631-4BDE-A1EC-BE41E90F60C4} diff --git a/Directory.Packages.props b/Directory.Packages.props index c2b67df6a7f..12c463dc3da 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,6 +30,7 @@ + diff --git a/playground/AzureAppConfiguration/AppHost/AppHost.csproj b/playground/AzureAppConfiguration/AppHost/AppHost.csproj new file mode 100644 index 00000000000..808aadd7893 --- /dev/null +++ b/playground/AzureAppConfiguration/AppHost/AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net9.0 + enable + enable + true + 6624a704-3e83-4d16-826d-964ae9acb07c + + + + + + + + + + + + diff --git a/playground/AzureAppConfiguration/AppHost/Program.cs b/playground/AzureAppConfiguration/AppHost/Program.cs new file mode 100644 index 00000000000..6d5c7fefa99 --- /dev/null +++ b/playground/AzureAppConfiguration/AppHost/Program.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +var builder = DistributedApplication.CreateBuilder(args); + +var appConfig = builder + .AddAzureAppConfiguration("appconfig"); + +builder.AddProject("workerservice") + .WithReference(appConfig); + +builder.Build().Run(); diff --git a/playground/AzureAppConfiguration/AppHost/Properties/launchSettings.json b/playground/AzureAppConfiguration/AppHost/Properties/launchSettings.json new file mode 100644 index 00000000000..aed4d5d94cf --- /dev/null +++ b/playground/AzureAppConfiguration/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:17091;http://localhost:15133", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21146", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22084" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15133", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19209", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20168" + } + } + } +} diff --git a/playground/AzureAppConfiguration/AppHost/appsettings.Development.json b/playground/AzureAppConfiguration/AppHost/appsettings.Development.json new file mode 100644 index 00000000000..0c208ae9181 --- /dev/null +++ b/playground/AzureAppConfiguration/AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/AzureAppConfiguration/AppHost/appsettings.json b/playground/AzureAppConfiguration/AppHost/appsettings.json new file mode 100644 index 00000000000..962fd14ad40 --- /dev/null +++ b/playground/AzureAppConfiguration/AppHost/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + }, + "Azure": { + "CredentialSource": "AzureCli", + "SubscriptionId": "", + "AllowResourceGroupCreation": true, + "ResourceGroup": "", + "Location": "" + } +} diff --git a/playground/AzureAppConfiguration/WorkerService/Program.cs b/playground/AzureAppConfiguration/WorkerService/Program.cs new file mode 100644 index 00000000000..9b570999389 --- /dev/null +++ b/playground/AzureAppConfiguration/WorkerService/Program.cs @@ -0,0 +1,24 @@ +using Aspire.Azure.Data.AppConfiguration; +using Azure.Identity; +using Microsoft.FeatureManagement; +using WorkerService; + +var builder = Host.CreateApplicationBuilder(args); + +builder.AddServiceDefaults(); +builder.Services.AddHostedService(); +builder.Configuration.AddAzureAppConfiguration( + "appconfig", + configureSettings: settings => settings.Credential = new AzureCliCredential(), + configureOptions: options => { + options.UseFeatureFlags(); + options.ConfigureRefresh(refresh => + { + refresh.RegisterAll(); + }); + builder.Services.AddSingleton(options.GetRefresher()); + }); +builder.Services.AddFeatureManagement(); + +var host = builder.Build(); +host.Run(); diff --git a/playground/AzureAppConfiguration/WorkerService/Properties/launchSettings.json b/playground/AzureAppConfiguration/WorkerService/Properties/launchSettings.json new file mode 100644 index 00000000000..153357d5e1c --- /dev/null +++ b/playground/AzureAppConfiguration/WorkerService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "WorkerService": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/playground/AzureAppConfiguration/WorkerService/Worker.cs b/playground/AzureAppConfiguration/WorkerService/Worker.cs new file mode 100644 index 00000000000..de69640b7f0 --- /dev/null +++ b/playground/AzureAppConfiguration/WorkerService/Worker.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Microsoft.FeatureManagement; + +namespace WorkerService; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IConfigurationRefresher _refresher; + private readonly IVariantFeatureManager _featureManager; + + public Worker(ILogger logger, IConfiguration configuration, IConfigurationRefresher refresher, IVariantFeatureManager featureManager) + { + _logger = logger; + _configuration = configuration; + _refresher = refresher; + _featureManager = featureManager; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await _refresher.TryRefreshAsync(stoppingToken); + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + _logger.LogInformation("Message from App Config: {message}", _configuration["message"]); + _logger.LogInformation("Beta feature flag is enabled: {enabled}", await _featureManager.IsEnabledAsync("Beta", stoppingToken)); + } + await Task.Delay(10000, stoppingToken); + } + } +} diff --git a/playground/AzureAppConfiguration/WorkerService/WorkerService.csproj b/playground/AzureAppConfiguration/WorkerService/WorkerService.csproj new file mode 100644 index 00000000000..945d7063366 --- /dev/null +++ b/playground/AzureAppConfiguration/WorkerService/WorkerService.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + dotnet-WorkerService-a204435a-f29d-40a7-b698-e883da9db8da + + + + + + + + + + + diff --git a/playground/AzureAppConfiguration/WorkerService/appsettings.Development.json b/playground/AzureAppConfiguration/WorkerService/appsettings.Development.json new file mode 100644 index 00000000000..b2dcdb67421 --- /dev/null +++ b/playground/AzureAppConfiguration/WorkerService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/playground/AzureAppConfiguration/WorkerService/appsettings.json b/playground/AzureAppConfiguration/WorkerService/appsettings.json new file mode 100644 index 00000000000..b2dcdb67421 --- /dev/null +++ b/playground/AzureAppConfiguration/WorkerService/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/playground/Directory.Packages.props b/playground/Directory.Packages.props index b4a3cd073f3..ac081d09be1 100644 --- a/playground/Directory.Packages.props +++ b/playground/Directory.Packages.props @@ -1,4 +1,7 @@ + + + \ No newline at end of file diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/Aspire.Azure.Data.AppConfiguration.csproj b/src/Components/Aspire.Azure.Data.AppConfiguration/Aspire.Azure.Data.AppConfiguration.csproj new file mode 100644 index 00000000000..bcadae4b744 --- /dev/null +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/Aspire.Azure.Data.AppConfiguration.csproj @@ -0,0 +1,19 @@ + + + + $(DefaultTargetFramework) + true + $(ComponentAzurePackageTags) configuration appconfiguration + A client for Azure App Configuration that integrates with Aspire. + $(SharedDir)AzureAppConfig_256x.png + $(NoWarn);SYSLIB1100;SYSLIB1101 + + false + + + + + + + + diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs new file mode 100644 index 00000000000..987209ae8d8 --- /dev/null +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Identity; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Microsoft.Extensions.Configuration; + +namespace Aspire.Azure.Data.AppConfiguration; + +/// +/// Provides extension methods for registering and configuring Azure App Configuration in a .NET Aspire application. +/// +public static class AspireAppConfigurationExtensions +{ + /// + /// Adds the Azure App Configuration to be configuration values in the . + /// + /// The to add the secrets to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + public static IConfigurationBuilder AddAzureAppConfiguration( + this IConfigurationManager configuration, + string connectionName, + Action? configureSettings = null, + Action? configureOptions = null) + { + ArgumentNullException.ThrowIfNull(configuration); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + var settings = new AzureDataAppConfigurationSettings(); + + if (configuration.GetConnectionString(connectionName) is string connectionString) + { + if (!string.IsNullOrEmpty(connectionString) && + Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + settings.Endpoint = uri; + } + } + + configureSettings?.Invoke(settings); + + return configuration.AddAzureAppConfiguration(options => + { + options.Connect(settings.Endpoint, settings.Credential ?? new DefaultAzureCredential()); + configureOptions?.Invoke(options); + }); + } +} diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs new file mode 100644 index 00000000000..a38b1a64721 --- /dev/null +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Core; + +namespace Aspire.Azure.Data.AppConfiguration; + +/// +/// Provides the client configuration settings for connecting to Azure App Configuration. +/// +public sealed class AzureDataAppConfigurationSettings +{ + /// + /// A to the App Configuration store on which the client operates. + /// + public Uri? Endpoint { get; set; } + + /// + /// Gets or sets the credential used to authenticate to the Azure App Configuration. + /// + public TokenCredential? Credential { get; set; } +} From aaac4cc1c7481e725c149256409844a249584f49 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 27 Mar 2025 17:26:03 +0800 Subject: [PATCH 02/18] update --- Directory.Packages.props | 1 + .../WorkerService/Program.cs | 1 - .../WorkerService/WorkerService.csproj | 2 +- .../AppConfigurationComponent.cs | 58 +++++++ .../Aspire.Azure.Data.AppConfiguration.csproj | 11 ++ .../AspireAppConfigurationExtensions.cs | 59 +++++++- .../AssemblyInfo.cs | 14 ++ .../AzureAppConfigurationHealthCheck.cs | 33 ++++ .../AzureDataAppConfigurationSettings.cs | 19 ++- .../ConfigurationSchema.json | 142 ++++++++++++++++++ .../README.md | 11 ++ src/Components/Aspire_Components_Progress.md | 1 + 12 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 src/Components/Aspire.Azure.Data.AppConfiguration/AppConfigurationComponent.cs create mode 100644 src/Components/Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs create mode 100644 src/Components/Aspire.Azure.Data.AppConfiguration/AzureAppConfigurationHealthCheck.cs create mode 100644 src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json create mode 100644 src/Components/Aspire.Azure.Data.AppConfiguration/README.md diff --git a/Directory.Packages.props b/Directory.Packages.props index 12c463dc3da..8d5be39577d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/playground/AzureAppConfiguration/WorkerService/Program.cs b/playground/AzureAppConfiguration/WorkerService/Program.cs index 9b570999389..29e1e6222d0 100644 --- a/playground/AzureAppConfiguration/WorkerService/Program.cs +++ b/playground/AzureAppConfiguration/WorkerService/Program.cs @@ -1,4 +1,3 @@ -using Aspire.Azure.Data.AppConfiguration; using Azure.Identity; using Microsoft.FeatureManagement; using WorkerService; diff --git a/playground/AzureAppConfiguration/WorkerService/WorkerService.csproj b/playground/AzureAppConfiguration/WorkerService/WorkerService.csproj index 945d7063366..0f1e48c81ff 100644 --- a/playground/AzureAppConfiguration/WorkerService/WorkerService.csproj +++ b/playground/AzureAppConfiguration/WorkerService/WorkerService.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AppConfigurationComponent.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AppConfigurationComponent.cs new file mode 100644 index 00000000000..cedc1dc9fb5 --- /dev/null +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AppConfigurationComponent.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Azure.Common; +using Azure.Core; +using Azure.Core.Extensions; +using Azure.Data.AppConfiguration; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Aspire.Azure.Data.AppConfiguration; +internal sealed class AppConfigurationComponent : AzureComponent +{ + protected override IAzureClientBuilder AddClient( + AzureClientFactoryBuilder azureFactoryBuilder, AzureDataAppConfigurationSettings settings, + string connectionName, string configurationSectionName) + { + return azureFactoryBuilder.AddClient((options, cred, _) => + { + if (settings.Endpoint is null) + { + throw new InvalidOperationException($"Endpoint is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'Endpoint' key in the '{configurationSectionName}' configuration section."); + } + + return new ConfigurationClient(settings.Endpoint, cred, options); + }); + } + + protected override IHealthCheck CreateHealthCheck(ConfigurationClient client, AzureDataAppConfigurationSettings settings) + { + return new AzureAppConfigurationHealthCheck(client); + } + + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) + { +#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works + clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 + } + + protected override void BindSettingsToConfiguration(AzureDataAppConfigurationSettings settings, IConfiguration configuration) + { + configuration.Bind(settings); + } + + protected override bool GetHealthCheckEnabled(AzureDataAppConfigurationSettings settings) + => !settings.DisableHealthChecks; + + protected override TokenCredential? GetTokenCredential(AzureDataAppConfigurationSettings settings) + => settings.Credential; + + protected override bool GetMetricsEnabled(AzureDataAppConfigurationSettings settings) + => false; + + protected override bool GetTracingEnabled(AzureDataAppConfigurationSettings settings) + => !settings.DisableTracing; +} diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/Aspire.Azure.Data.AppConfiguration.csproj b/src/Components/Aspire.Azure.Data.AppConfiguration/Aspire.Azure.Data.AppConfiguration.csproj index bcadae4b744..16964cb4bff 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/Aspire.Azure.Data.AppConfiguration.csproj +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/Aspire.Azure.Data.AppConfiguration.csproj @@ -12,8 +12,19 @@ + + + + + + + + + + + diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs index 987209ae8d8..538cc42681e 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs @@ -1,17 +1,67 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Azure.Data.AppConfiguration; +using Azure.Core.Extensions; +using Azure.Data.AppConfiguration; using Azure.Identity; using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; -namespace Aspire.Azure.Data.AppConfiguration; +namespace Microsoft.Extensions.Hosting; /// /// Provides extension methods for registering and configuring Azure App Configuration in a .NET Aspire application. /// public static class AspireAppConfigurationExtensions { + internal const string DefaultConfigSectionName = "Aspire:Azure:Data:AppConfiguration"; + + /// + /// Registers as a singleton in the services provided by the . + /// Enables retries, corresponding health check, logging and telemetry. + /// + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Data:AppConfiguration" section. + /// Thrown when mandatory is not provided. + public static void AddAzureAppConfigurationClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + new AppConfigurationComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); + } + + /// + /// Registers as a singleton for given in the services provided by the . + /// Enables retries, corresponding health check, logging and telemetry. + /// + /// The to read config from and add services to. + /// The name of the component, which is used as the of the service and also to retrieve the connection information from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Data:AppConfiguration" section. + /// Thrown when mandatory is not provided. + public static void AddKeyedAzureAppConfigurationClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + new AppConfigurationComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); + } + /// /// Adds the Azure App Configuration to be configuration values in the . /// @@ -19,6 +69,8 @@ public static class AspireAppConfigurationExtensions /// A name used to retrieve the connection string from the ConnectionStrings configuration section. /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Data:AppConfiguration" section. + /// Thrown when mandatory is not provided. public static IConfigurationBuilder AddAzureAppConfiguration( this IConfigurationManager configuration, string connectionName, @@ -41,6 +93,11 @@ public static IConfigurationBuilder AddAzureAppConfiguration( configureSettings?.Invoke(settings); + if (settings.Endpoint is null) + { + throw new InvalidOperationException($"Endpoint is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'Endpoint' key in the '{DefaultConfigSectionName}' configuration section."); + } + return configuration.AddAzureAppConfiguration(options => { options.Connect(settings.Endpoint, settings.Credential ?? new DefaultAzureCredential()); diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs new file mode 100644 index 00000000000..967122a56f4 --- /dev/null +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire; +using Aspire.Azure.Data.AppConfiguration; +using Azure.Data.AppConfiguration; + +[assembly: ConfigurationSchema("Aspire:Azure:Data:AppConfiguration", typeof(AzureDataAppConfigurationSettings))] +[assembly: ConfigurationSchema("Aspire:Azure:Data:AppConfiguration:ClientOptions", typeof(ConfigurationClientOptions), exclusionPaths: ["Default"])] + +[assembly: LoggingCategories( + "Azure", + "Azure.Core", + "Azure.Identity")] diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AzureAppConfigurationHealthCheck.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureAppConfigurationHealthCheck.cs new file mode 100644 index 00000000000..8b6821a59a7 --- /dev/null +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureAppConfigurationHealthCheck.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 Azure.Data.AppConfiguration; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Aspire.Azure.Data.AppConfiguration; + +internal sealed class AzureAppConfigurationHealthCheck : IHealthCheck +{ + private readonly ConfigurationClient _client; + + public AzureAppConfigurationHealthCheck(ConfigurationClient client) + { + ArgumentNullException.ThrowIfNull(client); + _client = client; + } + + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + _ = await _client.GetConfigurationSettingAsync("*", null, cancellationToken).ConfigureAwait(false); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } +} diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs index a38b1a64721..34e02887ded 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs @@ -11,7 +11,8 @@ namespace Aspire.Azure.Data.AppConfiguration; public sealed class AzureDataAppConfigurationSettings { /// - /// A to the App Configuration store on which the client operates. + /// A to the App Config store on which the client operates. Appears as "Endpoint" in the Azure portal. + /// This is likely to be similar to "https://{store_name}.azconfig.io". /// public Uri? Endpoint { get; set; } @@ -19,4 +20,20 @@ public sealed class AzureDataAppConfigurationSettings /// Gets or sets the credential used to authenticate to the Azure App Configuration. /// public TokenCredential? Credential { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the App Configuration health check is disabled or not. + /// + /// + /// The default value is . + /// + public bool DisableHealthChecks { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not. + /// + /// + /// The default value is . + /// + public bool DisableTracing { get; set; } } diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json b/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json new file mode 100644 index 00000000000..06b37e0f284 --- /dev/null +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json @@ -0,0 +1,142 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Azure": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Azure.Core": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Azure.Identity": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + }, + "type": "object", + "properties": { + "Aspire": { + "type": "object", + "properties": { + "Azure": { + "type": "object", + "properties": { + "Data": { + "type": "object", + "properties": { + "AppConfiguration": { + "type": "object", + "properties": { + "ClientOptions": { + "type": "object", + "properties": { + "Diagnostics": { + "type": "object", + "properties": { + "ApplicationId": { + "type": "string", + "description": "Gets or sets the value sent as the first part of \"User-Agent\" headers for all requests issues by this client. Defaults to 'Azure.Core.DiagnosticsOptions.DefaultApplicationId'." + }, + "DefaultApplicationId": { + "type": "string", + "description": "Gets or sets the default application id. Default application id would be set on all instances." + }, + "IsDistributedTracingEnabled": { + "type": "boolean", + "description": "Gets or sets value indicating whether distributed tracing activities ('System.Diagnostics.Activity') are going to be created for the clients methods calls and HTTP calls." + }, + "IsLoggingContentEnabled": { + "type": "boolean", + "description": "Gets or sets value indicating if request or response content should be logged." + }, + "IsLoggingEnabled": { + "type": "boolean", + "description": "Get or sets value indicating whether HTTP pipeline logging is enabled." + }, + "IsTelemetryEnabled": { + "type": "boolean", + "description": "Gets or sets value indicating whether the \"User-Agent\" header containing 'Azure.Core.DiagnosticsOptions.ApplicationId', client library package name and version, 'System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription' and 'System.Runtime.InteropServices.RuntimeInformation.OSDescription' should be sent. The default value can be controlled process wide by setting AZURE_TELEMETRY_DISABLED to true, false, 1 or 0." + }, + "LoggedContentSizeLimit": { + "type": "integer", + "description": "Gets or sets value indicating maximum size of content to log in bytes. Defaults to 4096." + }, + "LoggedHeaderNames": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets a list of header names that are not redacted during logging." + }, + "LoggedQueryParameters": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Gets a list of query parameter names that are not redacted during logging." + } + }, + "description": "Gets the client diagnostic options." + }, + "Retry": { + "type": "object", + "properties": { + "Delay": { + "type": "string", + "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$", + "description": "The delay between retry attempts for a fixed approach or the delay on which to base calculations for a backoff-based approach. If the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value." + }, + "MaxDelay": { + "type": "string", + "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$", + "description": "The maximum permissible delay between retry attempts when the service does not provide a Retry-After response header. If the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value." + }, + "MaxRetries": { + "type": "integer", + "description": "The maximum number of retry attempts before giving up." + }, + "Mode": { + "enum": [ + "Fixed", + "Exponential" + ], + "description": "The approach to use for calculating retry delays." + }, + "NetworkTimeout": { + "type": "string", + "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$", + "description": "The timeout applied to an individual network operations." + } + }, + "description": "Gets the client retry options." + } + }, + "description": "Options that allow you to configure the requests sent to App Configuration." + }, + "DisableHealthChecks": { + "type": "boolean", + "description": "Gets or sets a boolean value that indicates whether the Key Vault health check is disabled or not.", + "default": false + }, + "DisableTracing": { + "type": "boolean", + "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.", + "default": false + }, + "Endpoint": { + "type": "string", + "format": "uri", + "description": "A 'System.Uri' to the App Config store on which the client operates. Appears as \"Endpoint\" in the Azure portal." + } + }, + "description": "Provides the client configuration settings for connecting to Azure App Configuration." + } + } + } + } + } + } + } + } +} diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/README.md b/src/Components/Aspire.Azure.Data.AppConfiguration/README.md new file mode 100644 index 00000000000..3050f5469c5 --- /dev/null +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/README.md @@ -0,0 +1,11 @@ +# Aspire.Azure.Data.AppConfiguration + +Retrieves configuration settings from Azure App Configuration to use in your application. Registers a [ConfigurationClient](https://learn.microsoft.com/dotnet/api/azure.data.appconfiguration.configurationclient) in the DI container for connecting to Azure App Configuration. Enables corresponding health checks, logging and telemetry. + +## Getting started + +### Prerequisites + +- Azure subscription - [create one for free](https://azure.microsoft.com/free/) +- Azure App Configuration - [create one](https://learn.microsoft.com/azure/azure-app-configuration/quickstart-azure-app-configuration-create). + diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md index 246caf788e1..32e36887a62 100644 --- a/src/Components/Aspire_Components_Progress.md +++ b/src/Components/Aspire_Components_Progress.md @@ -12,6 +12,7 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As | Microsoft.EntityFrameworkCore.SqlServer | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | MongoDB.Driver | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | Azure.AI.OpenAI | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| Azure.Data.AppConfiguration | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | Azure.Data.Tables | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | Azure.Messaging.EventHubs | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | | Azure.Messaging.WebPubSub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | From 14bbfc0866ad220d9faa12be7009f172b96eba26 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 27 Mar 2025 22:35:05 +0800 Subject: [PATCH 03/18] add readme --- .../README.md | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/README.md b/src/Components/Aspire.Azure.Data.AppConfiguration/README.md index 3050f5469c5..12288cd553e 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/README.md +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/README.md @@ -9,3 +9,187 @@ Retrieves configuration settings from Azure App Configuration to use in your app - Azure subscription - [create one for free](https://azure.microsoft.com/free/) - Azure App Configuration - [create one](https://learn.microsoft.com/azure/azure-app-configuration/quickstart-azure-app-configuration-create). +### Install the package + +Install the .NET Aspire Azure App Configuration library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package Aspire.Azure.Data.AppConfiguration +``` + +## Usage examples + +### Add App Configuration to configuration + +In the _Program.cs_ file of your project, call the `builder.Configuration.AddAzureAppConfiguration` extension method to add key-values from Azure App Configuration to the application's Configuration. The method takes a connection name parameter. + +```csharp +builder.Configuration.AddAzureAppConfiguration("appConfig"); +``` + +You can then retrieve a key-value through normal `IConfiguration` APIS. For example, to retrieve a key-value from a Web API controller: + +```csharp +public MyController(IConfiguration configuration) +{ + string someValue = configuration["someKey"]; +} +``` + +#### Use feature flags + +To use feature flags, install the Feature Management library: + +```dotnetcli +dotnet add package Microsoft.FeatureManagement +``` + +App Configuration will not load feature flags by default. To load feature flags, you can pass the `Action configureOptions` delegate when calling `builder.Configuration.AddAzureAppConfiguration`. + +```csharp +builder.Configuration.AddAzureAppConfiguration("appConfig", configureOptions: options => options.UseFeatureFlags()); + +// Register feature management services +builder.Services.AddFeatureManagement(); +``` + +You can then use `IVariantFeatureManager` to evaluate feature flags in your application: + +```csharp +private readonly IVariantFeatureManager _featureManager; + +public MyController(IVariantFeatureManager featureManager) +{ + _featureManager = featureManager; +} + +[HttpGet] +public async Task Get() +{ + if (await _featureManager.IsEnabledAsync("NewFeature")) + { + return Ok("New feature is enabled!"); + } + + return Ok("Using standard implementation."); +} +``` + +For information about using the Feature Management library, please go to the [documentation](https://learn.microsoft.com/azure/azure-app-configuration/feature-management-dotnet-reference). + +### Use the ConfigurationClient + +Alternatively, you can use a `ConfigurationClient` to retrieve the configuration settings on demand. In the _Program.cs_ file of your project, call the `AddAzureAppConfigurationClient` extension method to register a `ConfigurationClient` for use via the dependency injection container. The method takes a connection name parameter. + +```csharp +builder.AddAzureAppConfigurationClient("appConfig"); +``` + +You can then retrieve the `ConfigurationClient` instance using dependency injection. For example, to retrieve the client from a Web API controller: + +```csharp +private readonly ConfigurationClient _client; + +public MyController(ConfigurationClient client) +{ + _client = client; +} +``` + +See the [Azure.Data.AppConfiguration documentation](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/appconfiguration/Azure.Data.AppConfiguration/README.md) for examples on using the `ConfigurationClient`. + +## Configuration + +The .NET Aspire Azure App Configuration library provides multiple options to configure the Azure App Configuration connection based on the requirements and conventions of your project. Note that the App Config `Endpoint` is required to be supplied. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureAppConfigurationClient()`: + +```csharp +builder.AddAzureAppConfigurationClient("appConfigConnectionName"); +``` + +And then the App Config endpoint will be retrieved from the `ConnectionStrings` configuration section. The App Config store URI which works with the `AzureDataAppConfigurationSettings.Credential` property to establish a connection. If no credential is configured, the [DefaultAzureCredential](https://learn.microsoft.com/dotnet/api/azure.identity.defaultazurecredential) is used. + +```json +{ + "ConnectionStrings": { + "appConfigConnectionName": "https://{store_name}.azconfig.io" + } +} +``` + +### Use configuration providers + +The .NET Aspire Azure App Configuration library supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `AzureDataAppConfigurationSettings` and `ConfigurationClientOptions` from configuration by using the `Aspire:Azure:Data:AppConfiguration` key. Example `appsettings.json` that configures some of the options: + +```json +{ + "Aspire": { + "Azure": { + "Data": { + "AppConfiguration": { + "DisableHealthChecks": true, + "DisableTracing": false, + "ClientOptions": { + "Diagnostics": { + "ApplicationId": "myapp" + } + } + } + } + } + } +} +``` + +### Use inline delegates + +You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: + +```csharp +builder.AddAzureAppConfigurationClient("appConfig", configureSettings: settings => settings.DisableHealthChecks = true); +``` + +You can also setup the [ConfigurationClientOptions](https://learn.microsoft.com/dotnet/api/azure.data.appconfiguration.configurationclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureAppConfigurationClient` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: + +```csharp +builder.AddAzureAppConfigurationClient("appConfig", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); +``` + +## AppHost extensions + +In your AppHost project, install the Aspire Azure App Configuration Hosting library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package Aspire.Hosting.Azure.AppConfiguration +``` + +Then, in the _Program.cs_ file of `AppHost`, add a App Configuration connection and consume the connection using the following methods: + +```csharp +// Service registration +var appConfig = builder.ExecutionContext.IsPublishMode + ? builder.AddAzureApppConfiguration("appConfig") + : builder.AddConnectionString("appConfig"); + +// Service consumption +var myService = builder.AddProject() + .WithReference(appConfig); +``` + +The `AddAzureAppConfiguration` method adds an Azure App Configuration resource to the builder. Or `AddConnectionString` can be used to read connection information from the AppHost's configuration under the `ConnectionStrings:appConfig` config key. The `WithReference` method passes that connection information into a connection string named `appConfig` in the `MyService` project. In the _Program.cs_ file of `MyService`, the connection can be consumed using: + +```csharp +builder.AddAzureAppConfigurationClient("appConfig"); +``` + +## Additional documentation + +* https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/appconfiguration/Azure.Data.AppConfiguration/README.md +* https://github.com/dotnet/aspire/tree/main/src/Components/README.md + +## Feedback & contributing + +https://github.com/dotnet/aspire From d347f91bea139119a04cb4acf273f2985dec2bbf Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 28 Mar 2025 14:44:20 +0800 Subject: [PATCH 04/18] update configuration schema --- .../ConfigurationSchema.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json b/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json index 06b37e0f284..6d8a90680e5 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json @@ -112,11 +112,11 @@ "description": "Gets the client retry options." } }, - "description": "Options that allow you to configure the requests sent to App Configuration." + "description": "Options that allow users to configure the requests sent to the App Configuration service." }, "DisableHealthChecks": { "type": "boolean", - "description": "Gets or sets a boolean value that indicates whether the Key Vault health check is disabled or not.", + "description": "Gets or sets a boolean value that indicates whether the App Configuration health check is disabled or not.", "default": false }, "DisableTracing": { @@ -127,7 +127,7 @@ "Endpoint": { "type": "string", "format": "uri", - "description": "A 'System.Uri' to the App Config store on which the client operates. Appears as \"Endpoint\" in the Azure portal." + "description": "A 'System.Uri' to the App Config store on which the client operates. Appears as \"Endpoint\" in the Azure portal. This is likely to be similar to \"https://{store_name}.azconfig.io\"." } }, "description": "Provides the client configuration settings for connecting to Azure App Configuration." From 803de742e7ce2d0012df76188e845189a06bd9d6 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 31 Mar 2025 13:46:29 +0800 Subject: [PATCH 05/18] add conformance test --- .../AspireAppConfigurationExtensions.cs | 2 +- ...e.Azure.Data.AppConfiguration.Tests.csproj | 14 +++ .../ConfigurationTests.cs | 21 ++++ .../ConformanceTests.cs | 95 +++++++++++++++++++ 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj create mode 100644 tests/Aspire.Azure.Data.AppConfiguration.Tests/ConfigurationTests.cs create mode 100644 tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs index 538cc42681e..5ff39f92c0e 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs @@ -85,7 +85,7 @@ public static IConfigurationBuilder AddAzureAppConfiguration( if (configuration.GetConnectionString(connectionName) is string connectionString) { if (!string.IsNullOrEmpty(connectionString) && - Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) { settings.Endpoint = uri; } diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj b/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj new file mode 100644 index 00000000000..34ea56565dc --- /dev/null +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj @@ -0,0 +1,14 @@ + + + + $(DefaultTargetFramework) + + + + + + + + + + \ No newline at end of file diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConfigurationTests.cs b/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConfigurationTests.cs new file mode 100644 index 00000000000..094fecb72d4 --- /dev/null +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConfigurationTests.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Aspire.Azure.Data.AppConfiguration.Tests; + +public class ConfigurationTests +{ + [Fact] + public void EndpointUriIsNullByDefault() + => Assert.Null(new AzureDataAppConfigurationSettings().Endpoint); + + [Fact] + public void HealthCheckIsEnabledByDefault() + => Assert.False(new AzureDataAppConfigurationSettings().DisableHealthChecks); + + [Fact] + public void TracingIsEnabledByDefault() + => Assert.False(new AzureDataAppConfigurationSettings().DisableTracing); +} diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs new file mode 100644 index 00000000000..ae987e1daab --- /dev/null +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.ConformanceTests; +using Azure.Data.AppConfiguration; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Azure.Data.AppConfiguration.Tests; + +public class ConformanceTests : ConformanceTests +{ + public const string Endpoint = "https://aspiretests.azconfig.io/"; + + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + protected override string ActivitySourceName => "Azure.Data.AppConfiguration.ConfigurationClient"; + + protected override string[] RequiredLogCategories => new string[] { "Azure.Core" }; + + protected override bool SupportsKeyedRegistrations => true; + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "Azure": { + "Data": { + "AppConfiguration": { + "Endpoint": "http://YOUR_URI", + "DisableHealthChecks": true, + "DisableTracing": false, + "ClientOptions": { + "DisableChallengeResourceVerification": true, + "Retry": { + "Mode": "Exponential", + "Delay": "00:03" + } + } + } + } + } + } + } + """; + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[] + { + new(CreateConfigKey("Aspire:Azure:Data:AppConfiguraton", key, "Endpoint"), Endpoint), + new(CreateConfigKey("Aspire:Azure:Data:AppConfiguraton", key, "ClientOptions:Retry:MaxRetries"), "0") + }); + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + if (key is null) + { + builder.AddAzureAppConfigurationClient("appConfig", configure); + } + else + { + builder.AddKeyedAzureAppConfigurationClient(key, configure); + } + } + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "Azure": { "Data":{ "AppConfiguration": { "Endpoint": "YOUR_URI"}}}}}""", "Value does not match format \"uri\""), + ("""{"Aspire": { "Azure": { "Data":{ "AppConfiguration": { "Endpoint": "http://YOUR_URI", "DisableHealthChecks": "true"}}}}}""", "Value is \"string\" but should be \"boolean\""), + ("""{"Aspire": { "Azure": { "Data":{ "AppConfiguration": { "Endpoint": "http://YOUR_URI", "ClientOptions": {"Retry": {"Mode": "Fast"}}}}}}}""", "Value should match one of the values specified by the enum"), + ("""{"Aspire": { "Azure": { "Data":{ "AppConfiguration": { "Endpoint": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "The string value is not a match for the indicated regular expression") + }; + + protected override void SetHealthCheck(AzureDataAppConfigurationSettings options, bool enabled) + => throw new NotImplementedException(); + + protected override void SetMetrics(AzureDataAppConfigurationSettings options, bool enabled) + => throw new NotImplementedException(); + + protected override void SetTracing(AzureDataAppConfigurationSettings options, bool enabled) + => options.DisableTracing = !enabled; + + protected override void TriggerActivity(ConfigurationClient service) + => service.GetConfigurationSettingAsync("*", null); + + [Fact] + public void TracingEnablesTheRightActivitySource() + => RemoteExecutor.Invoke(() => ActivitySourceTest(key: null)).Dispose(); + + [Fact] + public void TracingEnablesTheRightActivitySource_Keyed() + => RemoteExecutor.Invoke(() => ActivitySourceTest(key: "key")).Dispose(); +} From 59b1dc2d6748cea5373103478bdabf9ffd5c033a Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 31 Mar 2025 16:35:35 +0800 Subject: [PATCH 06/18] fix project path --- Aspire.sln | 15 +++++++++++++++ .../AspireAppConfigurationExtensions.cs | 10 +++++----- .../AzureDataAppConfigurationSettings.cs | 12 +++++++++++- ...spire.Azure.Data.AppConfiguration.Tests.csproj | 2 +- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Aspire.sln b/Aspire.sln index 73365b4f113..a880109408a 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -673,6 +673,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "playground\Azure EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkerService", "playground\AzureAppConfiguration\WorkerService\WorkerService.csproj", "{5B26B478-95AF-9F3E-D5E7-D7535855470F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Data.AppConfiguration.Tests", "tests\Aspire.Azure.Data.AppConfiguration.Tests\Aspire.Azure.Data.AppConfiguration.Tests.csproj", "{4C902423-42B5-40E9-82FA-36D2439EE631}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -3935,6 +3937,18 @@ Global {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|x64.Build.0 = Release|Any CPU {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|x86.ActiveCfg = Release|Any CPU {5B26B478-95AF-9F3E-D5E7-D7535855470F}.Release|x86.Build.0 = Release|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Debug|x64.Build.0 = Debug|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Debug|x86.ActiveCfg = Debug|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Debug|x86.Build.0 = Debug|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Release|Any CPU.Build.0 = Release|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Release|x64.ActiveCfg = Release|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Release|x64.Build.0 = Release|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Release|x86.ActiveCfg = Release|Any CPU + {4C902423-42B5-40E9-82FA-36D2439EE631}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -4258,6 +4272,7 @@ Global {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {426E8FEA-7077-4433-959C-E5B1E5F26CC0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {5B26B478-95AF-9F3E-D5E7-D7535855470F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {4C902423-42B5-40E9-82FA-36D2439EE631} = {C424395C-1235-41A4-BF55-07880A04368C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {47DCFECF-5631-4BDE-A1EC-BE41E90F60C4} diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs index 5ff39f92c0e..7c3884b770f 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Configuration.AzureAppConfiguration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Aspire.Azure.Common; namespace Microsoft.Extensions.Hosting; @@ -80,15 +81,14 @@ public static IConfigurationBuilder AddAzureAppConfiguration( ArgumentNullException.ThrowIfNull(configuration); ArgumentException.ThrowIfNullOrEmpty(connectionName); + var configSection = configuration.GetSection(DefaultConfigSectionName); + var settings = new AzureDataAppConfigurationSettings(); + configSection.Bind(settings); if (configuration.GetConnectionString(connectionName) is string connectionString) { - if (!string.IsNullOrEmpty(connectionString) && - Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) - { - settings.Endpoint = uri; - } + ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); } configureSettings?.Invoke(settings); diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs index 34e02887ded..7c31238f87a 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureDataAppConfigurationSettings.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Azure.Common; using Azure.Core; namespace Aspire.Azure.Data.AppConfiguration; @@ -8,7 +9,7 @@ namespace Aspire.Azure.Data.AppConfiguration; /// /// Provides the client configuration settings for connecting to Azure App Configuration. /// -public sealed class AzureDataAppConfigurationSettings +public sealed class AzureDataAppConfigurationSettings : IConnectionStringSettings { /// /// A to the App Config store on which the client operates. Appears as "Endpoint" in the Azure portal. @@ -36,4 +37,13 @@ public sealed class AzureDataAppConfigurationSettings /// The default value is . /// public bool DisableTracing { get; set; } + + void IConnectionStringSettings.ParseConnectionString(string? connectionString) + { + if (!string.IsNullOrEmpty(connectionString) && + Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + Endpoint = uri; + } + } } diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj b/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj index 34ea56565dc..0b2fcda4796 100644 --- a/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj @@ -7,7 +7,7 @@ - + From 0b0baf934a3e40a5b9d5deef6adb382dfd9da40d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 31 Mar 2025 17:25:08 +0800 Subject: [PATCH 07/18] update --- src/Components/Common/AzureComponent.cs | 1 + .../ConformanceTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Components/Common/AzureComponent.cs b/src/Components/Common/AzureComponent.cs index aeb7c5c82bc..6834995f8aa 100644 --- a/src/Components/Common/AzureComponent.cs +++ b/src/Components/Common/AzureComponent.cs @@ -49,6 +49,7 @@ internal TSettings AddClient( string connectionName, string? serviceKey) { + var test = builder.Configuration; var configSection = builder.Configuration.GetSection(configurationSectionName); var settings = new TSettings(); diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs index ae987e1daab..f9672f5b82f 100644 --- a/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs @@ -49,8 +49,8 @@ public class ConformanceTests : ConformanceTests configuration.AddInMemoryCollection(new KeyValuePair[] { - new(CreateConfigKey("Aspire:Azure:Data:AppConfiguraton", key, "Endpoint"), Endpoint), - new(CreateConfigKey("Aspire:Azure:Data:AppConfiguraton", key, "ClientOptions:Retry:MaxRetries"), "0") + new(CreateConfigKey("Aspire:Azure:Data:AppConfiguration", key, "Endpoint"), Endpoint), + new(CreateConfigKey("Aspire:Azure:Data:AppConfiguration", key, "ClientOptions:Retry:MaxRetries"), "0") }); protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) From 24a3b56e15fd17a56aa0daa121a2db66e3ff1b2b Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 31 Mar 2025 17:38:11 +0800 Subject: [PATCH 08/18] fix testcases --- .../ConformanceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs index f9672f5b82f..47289e904a7 100644 --- a/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/ConformanceTests.cs @@ -83,7 +83,7 @@ protected override void SetTracing(AzureDataAppConfigurationSettings options, bo => options.DisableTracing = !enabled; protected override void TriggerActivity(ConfigurationClient service) - => service.GetConfigurationSettingAsync("*", null); + => service.GetConfigurationSetting("*", null); [Fact] public void TracingEnablesTheRightActivitySource() From b98b94da0e771c5447d522fa2e21e729af79ecd9 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Mon, 31 Mar 2025 22:36:22 +0800 Subject: [PATCH 09/18] finished unit test --- .../AspireAppConfigurationExtensions.cs | 14 +- ...e.Azure.Data.AppConfiguration.Tests.csproj | 2 + .../AspireAppConfigurationExtensionsTests.cs | 186 ++++++++++++++++++ .../DataAppConfigurationPublicApiTests.cs | 95 +++++++++ 4 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 tests/Aspire.Azure.Data.AppConfiguration.Tests/AspireAppConfigurationExtensionsTests.cs create mode 100644 tests/Aspire.Azure.Data.AppConfiguration.Tests/DataAppConfigurationPublicApiTests.cs diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs index 7c3884b770f..8d0d68b29b0 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AspireAppConfigurationExtensions.cs @@ -64,29 +64,29 @@ public static void AddKeyedAzureAppConfigurationClient( } /// - /// Adds the Azure App Configuration to be configuration values in the . + /// Adds the Azure App Configuration to be configuration values in the . /// - /// The to add the secrets to. + /// The to add the secrets to. /// A name used to retrieve the connection string from the ConnectionStrings configuration section. /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Azure:Data:AppConfiguration" section. /// Thrown when mandatory is not provided. public static IConfigurationBuilder AddAzureAppConfiguration( - this IConfigurationManager configuration, + this IConfigurationManager configurationManager, string connectionName, Action? configureSettings = null, Action? configureOptions = null) { - ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configurationManager); ArgumentException.ThrowIfNullOrEmpty(connectionName); - var configSection = configuration.GetSection(DefaultConfigSectionName); + var configSection = configurationManager.GetSection(DefaultConfigSectionName); var settings = new AzureDataAppConfigurationSettings(); configSection.Bind(settings); - if (configuration.GetConnectionString(connectionName) is string connectionString) + if (configurationManager.GetConnectionString(connectionName) is string connectionString) { ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); } @@ -98,7 +98,7 @@ public static IConfigurationBuilder AddAzureAppConfiguration( throw new InvalidOperationException($"Endpoint is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'Endpoint' key in the '{DefaultConfigSectionName}' configuration section."); } - return configuration.AddAzureAppConfiguration(options => + return configurationManager.AddAzureAppConfiguration(options => { options.Connect(settings.Endpoint, settings.Credential ?? new DefaultAzureCredential()); configureOptions?.Invoke(options); diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj b/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj index 0b2fcda4796..b5313fd1982 100644 --- a/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj @@ -9,6 +9,8 @@ + + \ No newline at end of file diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/AspireAppConfigurationExtensionsTests.cs b/tests/Aspire.Azure.Data.AppConfiguration.Tests/AspireAppConfigurationExtensionsTests.cs new file mode 100644 index 00000000000..c8c957d8cbd --- /dev/null +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/AspireAppConfigurationExtensionsTests.cs @@ -0,0 +1,186 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Azure.Data.AppConfiguration; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Azure.Data.AppConfiguration.Tests; + +public class AspireAppConfigurationExtensionsTests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AppConfigEndpointCanBeSetInCode(bool useKeyed) + { + var endpoint = new Uri(ConformanceTests.Endpoint); + var mockTransport = new MockTransport(CreateResponse("""{}""")); + + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:appConfig", "https://unused.azconfig.io/") + ]); + + if (useKeyed) + { + builder.AddKeyedAzureAppConfigurationClient( + "appConfig", + settings => settings.Endpoint = endpoint, + clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + + } + else + { + builder.AddAzureAppConfigurationClient( + "appConfig", + settings => settings.Endpoint = endpoint, + clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + } + + using var host = builder.Build(); + var client = useKeyed ? + host.Services.GetRequiredKeyedService("appConfig") : + host.Services.GetRequiredService(); + + // ConfigurationClient doesn't have a public property to get the endpoint, so we can't verify it directly. + // Make a request to trigger the transport and record the URI + client.GetConfigurationSetting("test-key"); + + Assert.NotEmpty(mockTransport.Requests); + var request = mockTransport.Requests[0]; + Assert.StartsWith(endpoint.ToString(), request.Uri.ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConnectionNameWinsOverConfigSection(bool useKeyed) + { + var mockTransport = new MockTransport(CreateResponse("""{}""")); + + var builder = Host.CreateEmptyApplicationBuilder(null); + + var key = useKeyed ? "secrets" : null; + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair(ConformanceTests.CreateConfigKey("Aspire:Azure:Data:AppConfiguration", key, "Endpoint"), "unused"), + new KeyValuePair("ConnectionStrings:appConfig", ConformanceTests.Endpoint) + ]); + + if (useKeyed) + { + builder.AddKeyedAzureAppConfigurationClient( + "appConfig", + configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + + } + else + { + builder.AddAzureAppConfigurationClient( + "appConfig", + configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + } + + using var host = builder.Build(); + var client = useKeyed ? + host.Services.GetRequiredKeyedService("appConfig") : + host.Services.GetRequiredService(); + + // ConfigurationClient doesn't have a public property to get the endpoint, so we can't verify it directly. + // Make a request to trigger the transport and record the URI + client.GetConfigurationSetting("test-key"); + + Assert.NotEmpty(mockTransport.Requests); + var request = mockTransport.Requests[0]; + var endpoint = new Uri(ConformanceTests.Endpoint); + Assert.StartsWith(endpoint.ToString(), request.Uri.ToString()); + } + + [Fact] + public void AddsAppConfigurationKeyValuesToConfig() + { + var mockTransport = new MockTransport(CreateResponse(""" + { + "items": [ + { + "key": "test-key-1", + "value": "test-value-1" + }, + { + "key": "test-key-2", + "value": "test-value-2" + } + ] + } + """)); + + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:appConfig", ConformanceTests.Endpoint) + ]); + + builder.Configuration.AddAzureAppConfiguration( + "appConfig", + configureOptions: options => options.ConfigureClientOptions( + clientOptions => clientOptions.Transport = mockTransport)); + + Assert.Equal("test-value-1", builder.Configuration["test-key-1"]); + Assert.Equal("test-value-2", builder.Configuration["test-key-2"]); + } + + [Fact] + public void CanAddMultipleKeyedServices() + { + var mockTransport = new MockTransport( + CreateResponse("""{}"""), + CreateResponse("""{}"""), + CreateResponse("""{}""")); + + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:appConfig1", "https://aspiretests1.vault.azconfig.io/"), + new KeyValuePair("ConnectionStrings:appConfig2", "https://aspiretests2.vault.azconfig.io/"), + new KeyValuePair("ConnectionStrings:appConfig3", "https://aspiretests3.vault.azconfig.io/") + ]); + + builder.AddAzureAppConfigurationClient("appConfig1", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + builder.AddKeyedAzureAppConfigurationClient("appConfig2", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + builder.AddKeyedAzureAppConfigurationClient("appConfig3", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + + using var host = builder.Build(); + + // Unkeyed services don't work with keyed services. See https://github.com/dotnet/aspire/issues/3890 + //var client1 = host.Services.GetRequiredService(); + var client2 = host.Services.GetRequiredKeyedService("appConfig2"); + var client3 = host.Services.GetRequiredKeyedService("appConfig3"); + + //Assert.NotSame(client1, client2); + //Assert.NotSame(client1, client3); + Assert.NotSame(client2, client3); + + // ConfigurationClient doesn't have a public property to get the endpoint, so we can't verify it directly. + // Make a request to trigger the transport and record the URI + //client1.GetConfigurationSetting("test-key"); + //Assert.StartsWith("https://aspiretests1.vault.azconfig.io/", mockTransport.Requests.Last().Uri.ToString()); + client2.GetConfigurationSetting("test-key"); + Assert.StartsWith("https://aspiretests2.vault.azconfig.io/", mockTransport.Requests.Last().Uri.ToString()); + client3.GetConfigurationSetting("test-key"); + Assert.StartsWith("https://aspiretests3.vault.azconfig.io/", mockTransport.Requests.Last().Uri.ToString()); + } + + private static MockResponse CreateResponse(string content) + { + var buffer = Encoding.UTF8.GetBytes(content); + var response = new MockResponse(200) + { + ClientRequestId = Guid.NewGuid().ToString(), + ContentStream = new MemoryStream(buffer), + }; + + return response; + } +} diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/DataAppConfigurationPublicApiTests.cs b/tests/Aspire.Azure.Data.AppConfiguration.Tests/DataAppConfigurationPublicApiTests.cs new file mode 100644 index 00000000000..fb9c2c2e10d --- /dev/null +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/DataAppConfigurationPublicApiTests.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Azure.Data.AppConfiguration.Tests; + +public class DataAppConfigurationPublicApiTests +{ + [Fact] + public void AddAzureKeyVaultClientShouldThrowWhenBuilderIsNull() + { + IHostApplicationBuilder builder = null!; + const string connectionName = "appConfig"; + + var action = () => builder.AddAzureAppConfigurationClient(connectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddAzureAppConfigurationClientShouldThrowWhenConnectionNameIsNullOrEmpty(bool isNull) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + var connectionName = isNull ? null! : string.Empty; + + var action = () => builder.AddAzureAppConfigurationClient(connectionName); + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + Assert.Equal(nameof(connectionName), exception.ParamName); + } + + [Fact] + public void AddKeyedAzureAppConfigurationClientShouldThrowWhenBuilderIsNull() + { + IHostApplicationBuilder builder = null!; + const string name = "appConfig"; + + var action = () => builder.AddKeyedAzureAppConfigurationClient(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddKeyedAzureAppConfigurationClientShouldThrowWhenNameIsNullOrEmpty(bool isNull) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + var name = isNull ? null! : string.Empty; + + var action = () => builder.AddKeyedAzureAppConfigurationClient(name); + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } + + [Fact] + public void AddAzureAppConfigurationShouldThrowWhenConfigurationManagerIsNull() + { + IConfigurationManager configurationManager = null!; + const string connectionName = "appConfig"; + + var action = () => configurationManager.AddAzureAppConfiguration(connectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configurationManager), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddAzureAppConfigurationShouldThrowWhenConnectionNameIsNullOrEmpty(bool isNull) + { + var configurationManager = new ConfigurationManager(); + var connectionName = isNull ? null! : string.Empty; + + var action = () => configurationManager.AddAzureAppConfiguration(connectionName); + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + Assert.Equal(nameof(connectionName), exception.ParamName); + } +} From d628c7a5b2a8a57fe4163675a7280908b1a7372e Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 1 Apr 2025 13:06:25 +0800 Subject: [PATCH 10/18] use empty token credential in the testcase --- .../AspireAppConfigurationExtensionsTests.cs | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/AspireAppConfigurationExtensionsTests.cs b/tests/Aspire.Azure.Data.AppConfiguration.Tests/AspireAppConfigurationExtensionsTests.cs index c8c957d8cbd..3426ac1f9ac 100644 --- a/tests/Aspire.Azure.Data.AppConfiguration.Tests/AspireAppConfigurationExtensionsTests.cs +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/AspireAppConfigurationExtensionsTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Azure.Core; using Azure.Data.AppConfiguration; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; @@ -30,7 +31,10 @@ public void AppConfigEndpointCanBeSetInCode(bool useKeyed) { builder.AddKeyedAzureAppConfigurationClient( "appConfig", - settings => settings.Endpoint = endpoint, + settings => { + settings.Endpoint = endpoint; + settings.Credential = new EmptyTokenCredential(); + }, clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); } @@ -38,7 +42,10 @@ public void AppConfigEndpointCanBeSetInCode(bool useKeyed) { builder.AddAzureAppConfigurationClient( "appConfig", - settings => settings.Endpoint = endpoint, + settings => { + settings.Endpoint = endpoint; + settings.Credential = new EmptyTokenCredential(); + }, clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); } @@ -75,14 +82,16 @@ public void ConnectionNameWinsOverConfigSection(bool useKeyed) { builder.AddKeyedAzureAppConfigurationClient( "appConfig", - configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + settings => settings.Credential = new EmptyTokenCredential(), + clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); } else { builder.AddAzureAppConfigurationClient( "appConfig", - configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + settings => settings.Credential = new EmptyTokenCredential(), + clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); } using var host = builder.Build(); @@ -125,6 +134,7 @@ public void AddsAppConfigurationKeyValuesToConfig() builder.Configuration.AddAzureAppConfiguration( "appConfig", + settings => settings.Credential = new EmptyTokenCredential(), configureOptions: options => options.ConfigureClientOptions( clientOptions => clientOptions.Transport = mockTransport)); @@ -147,9 +157,9 @@ public void CanAddMultipleKeyedServices() new KeyValuePair("ConnectionStrings:appConfig3", "https://aspiretests3.vault.azconfig.io/") ]); - builder.AddAzureAppConfigurationClient("appConfig1", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); - builder.AddKeyedAzureAppConfigurationClient("appConfig2", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); - builder.AddKeyedAzureAppConfigurationClient("appConfig3", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + builder.AddAzureAppConfigurationClient("appConfig1", settings => settings.Credential = new EmptyTokenCredential(), clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + builder.AddKeyedAzureAppConfigurationClient("appConfig2", settings => settings.Credential = new EmptyTokenCredential(), clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); + builder.AddKeyedAzureAppConfigurationClient("appConfig3", settings => settings.Credential = new EmptyTokenCredential(), clientBuilder => clientBuilder.ConfigureOptions(options => options.Transport = mockTransport)); using var host = builder.Build(); @@ -183,4 +193,17 @@ private static MockResponse CreateResponse(string content) return response; } + + internal sealed class EmptyTokenCredential : TokenCredential + { + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new AccessToken(string.Empty, DateTimeOffset.MaxValue); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(new AccessToken(string.Empty, DateTimeOffset.MaxValue)); + } + } } From 316eb9d1652d47b3874ebbcdec841aae1632c0f9 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 1 Apr 2025 14:56:28 +0800 Subject: [PATCH 11/18] update example --- playground/AzureAppConfiguration/AppHost/appsettings.json | 3 +-- playground/AzureAppConfiguration/WorkerService/Program.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/playground/AzureAppConfiguration/AppHost/appsettings.json b/playground/AzureAppConfiguration/AppHost/appsettings.json index 962fd14ad40..57ec55662a3 100644 --- a/playground/AzureAppConfiguration/AppHost/appsettings.json +++ b/playground/AzureAppConfiguration/AppHost/appsettings.json @@ -7,10 +7,9 @@ } }, "Azure": { - "CredentialSource": "AzureCli", "SubscriptionId": "", "AllowResourceGroupCreation": true, "ResourceGroup": "", - "Location": "" + "Location": "" } } diff --git a/playground/AzureAppConfiguration/WorkerService/Program.cs b/playground/AzureAppConfiguration/WorkerService/Program.cs index 29e1e6222d0..0d7dcba2b7a 100644 --- a/playground/AzureAppConfiguration/WorkerService/Program.cs +++ b/playground/AzureAppConfiguration/WorkerService/Program.cs @@ -1,14 +1,13 @@ -using Azure.Identity; using Microsoft.FeatureManagement; using WorkerService; var builder = Host.CreateApplicationBuilder(args); builder.AddServiceDefaults(); +builder.AddAzureAppConfigurationClient("appConfig"); builder.Services.AddHostedService(); builder.Configuration.AddAzureAppConfiguration( "appconfig", - configureSettings: settings => settings.Credential = new AzureCliCredential(), configureOptions: options => { options.UseFeatureFlags(); options.ConfigureRefresh(refresh => From 2cf4ec8635e77c0f089aa6dfe3b33c81aeb20306 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 1 Apr 2025 15:18:11 +0800 Subject: [PATCH 12/18] remove dead code --- src/Components/Common/AzureComponent.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Common/AzureComponent.cs b/src/Components/Common/AzureComponent.cs index f734bed0ed0..0dcd6485495 100644 --- a/src/Components/Common/AzureComponent.cs +++ b/src/Components/Common/AzureComponent.cs @@ -49,7 +49,6 @@ internal TSettings AddClient( string connectionName, string? serviceKey) { - var test = builder.Configuration; var configSection = builder.Configuration.GetSection(configurationSectionName); var settings = new TSettings(); From 9ff35723aeb8faf9781f157ab46260d877dd364a Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 1 Apr 2025 15:21:50 +0800 Subject: [PATCH 13/18] fix typo in readme --- src/Components/Aspire.Azure.Data.AppConfiguration/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/README.md b/src/Components/Aspire.Azure.Data.AppConfiguration/README.md index 12288cd553e..1481e280188 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/README.md +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/README.md @@ -171,7 +171,7 @@ Then, in the _Program.cs_ file of `AppHost`, add a App Configuration connection ```csharp // Service registration var appConfig = builder.ExecutionContext.IsPublishMode - ? builder.AddAzureApppConfiguration("appConfig") + ? builder.AddAzureAppConfiguration("appConfig") : builder.AddConnectionString("appConfig"); // Service consumption From f5a57327e28ddaee567a166de5677b5712bb5809 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 1 Apr 2025 15:39:22 +0800 Subject: [PATCH 14/18] update test project --- .../Aspire.Azure.Data.AppConfiguration.Tests.csproj | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj b/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj index b5313fd1982..dc252aa4c7c 100644 --- a/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj +++ b/tests/Aspire.Azure.Data.AppConfiguration.Tests/Aspire.Azure.Data.AppConfiguration.Tests.csproj @@ -6,11 +6,16 @@ + + + + + + + - - \ No newline at end of file From 3a893f727751994a2751a5d895ab4cb588d4bf86 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 2 Apr 2025 11:07:28 +0800 Subject: [PATCH 15/18] improve health check --- .../AzureAppConfigurationHealthCheck.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AzureAppConfigurationHealthCheck.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureAppConfigurationHealthCheck.cs index 8b6821a59a7..76c9e52791c 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/AzureAppConfigurationHealthCheck.cs +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AzureAppConfigurationHealthCheck.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Azure; using Azure.Data.AppConfiguration; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -21,11 +22,20 @@ public async Task CheckHealthAsync(HealthCheckContext context { try { - _ = await _client.GetConfigurationSettingAsync("*", null, cancellationToken).ConfigureAwait(false); + var selector = new SettingSelector + { + KeyFilter = "__UNEXISTED_KEY__", + LabelFilter = null + }; + AsyncPageable pageableSettings = _client.GetConfigurationSettingsAsync(selector, cancellationToken); + await foreach (var page in pageableSettings.AsPages().ConfigureAwait(false)) + { + _ = page.GetRawResponse(); // If healthy, the response should be 200 and with empty content + } return HealthCheckResult.Healthy(); } - catch (Exception ex) + catch (RequestFailedException ex) { return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); } From 2e4382f19323758b13dc9665924159162b3faf08 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sat, 5 Apr 2025 16:28:35 +0800 Subject: [PATCH 16/18] update Directory.Packages --- Directory.Packages.props | 1 + playground/Directory.Packages.props | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8524a059518..66fca7a0a09 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -87,6 +87,7 @@ + diff --git a/playground/Directory.Packages.props b/playground/Directory.Packages.props index ac081d09be1..b4a3cd073f3 100644 --- a/playground/Directory.Packages.props +++ b/playground/Directory.Packages.props @@ -1,7 +1,4 @@ - - - \ No newline at end of file From f808da423324e05117bb50fadc48b1d1ab062ca3 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sun, 6 Apr 2025 01:19:06 +0800 Subject: [PATCH 17/18] add configuration provider logging category --- .../Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs b/src/Components/Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs index 967122a56f4..7e07a0171cc 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/AssemblyInfo.cs @@ -11,4 +11,5 @@ [assembly: LoggingCategories( "Azure", "Azure.Core", - "Azure.Identity")] + "Azure.Identity", + "Microsoft.Extensions.Configuration.AzureAppConfiguration.Refresh")] From b15edf876af0f2b51cb9db80da9ea7e043cc85d8 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 8 Apr 2025 01:01:56 +0800 Subject: [PATCH 18/18] update configurationschema --- .../ConfigurationSchema.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json b/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json index 6d8a90680e5..7823e7410ca 100644 --- a/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json +++ b/src/Components/Aspire.Azure.Data.AppConfiguration/ConfigurationSchema.json @@ -10,6 +10,9 @@ }, "Azure.Identity": { "$ref": "#/definitions/logLevelThreshold" + }, + "Microsoft.Extensions.Configuration.AzureAppConfiguration.Refresh": { + "$ref": "#/definitions/logLevelThreshold" } } }