diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..233e6128
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,39 @@
+# editorconfig.org
+
+# top-most EditorConfig file
+root = true
+
+## Default settings ##
+[*]
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+## Formatting rule ##
+# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055
+dotnet_diagnostic.IDE0055.severity = error
+
+# 'Using' directive preferences
+dotnet_sort_system_directives_first = false
+
+# New line preferences
+dotnet_diagnostic.IDE2002.severity = error
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
+dotnet_diagnostic.IDE2004.severity = error
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
+dotnet_diagnostic.IDE2005.severity = error
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false
+dotnet_diagnostic.IDE2006.severity = error
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false
+dotnet_diagnostic.IDE2000.severity = error
+dotnet_style_allow_multiple_blank_lines_experimental = false
+dotnet_diagnostic.IDE2003.severity = error
+dotnet_style_allow_statement_immediately_after_block_experimental = false
+
+[*.csproj]
+indent_size = 2
+charset = utf-8
+
+[*.json]
+indent_size = 2
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..af9b1d22
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,53 @@
+name: AppConfiguration-DotnetProvider CI
+
+on:
+ push:
+ branches:
+ - main
+ - preview
+ - release/*
+ pull_request:
+ branches:
+ - main
+ - preview
+ - release/*
+
+permissions:
+ security-events: write
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Install .NET
+ run: pwsh build/install-dotnet.ps1 -RestoreOnly
+
+ - name: Restore
+ run: pwsh build.ps1 -RestoreOnly
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: 'csharp'
+
+ - name: Dotnet Build
+ run: pwsh build.ps1
+
+ - name: Dotnet Pack
+ run: pwsh pack.ps1
+
+ - name: Dotnet Test
+ run: pwsh test.ps1
+
+ - name: Publish Test Results
+ uses: actions/upload-artifact@v4
+ with:
+ name: Unit Test Results
+ path: ${{ github.workspace }}/tests/**/*.trx
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
diff --git a/.pipelines/OneBranch.Official.yml b/.pipelines/OneBranch.Official.yml
deleted file mode 100644
index 1297da99..00000000
--- a/.pipelines/OneBranch.Official.yml
+++ /dev/null
@@ -1,175 +0,0 @@
-trigger: none
-
-parameters: # parameters are shown up in ADO UI in a build queue time
-- name: 'debug'
- displayName: 'Enable debug output'
- type: boolean
- default: false
-
-variables:
- CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task
- system.debug: ${{ parameters.debug }}
- ENABLE_PRS_DELAYSIGN: 1
- ROOT: $(Build.SourcesDirectory)
- REPOROOT: $(Build.SourcesDirectory)
- OUTPUTROOT: $(REPOROOT)\out
- CDP_USER_SOURCE_FOLDER_CONTAINER_PATH: $(Build.SourcesDirectory)
- CDP_DEFINITION_BUILD_COUNT_DAY: $[counter(format('{0:yyyyMMdd}', pipeline.startTime), 1)]
- CDP_DEFINITION_BUILD_COUNT_MONTH: $[counter(format('{0:yyyyMM}', pipeline.startTime), 1)]
- CDP_DEFINITION_BUILD_COUNT_YEAR: $[counter(format('{0:yyyy}', pipeline.startTime), 1)]
- NUGET_XMLDOC_MODE: none
-
- # Docker image which is used to build the project
- WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2019/vse2022:latest'
-
-resources:
- repositories:
- - repository: templates
- type: git
- name: OneBranch.Pipelines/GovernedTemplates
- ref: refs/heads/main
-
-extends:
- template: v2/OneBranch.Official.CrossPlat.yml@templates
- parameters:
- cloudvault:
- enabled: false
- globalSdl:
- tsa:
- enabled: false # onebranch publish all sdl results to TSA. If TSA is disabled all SDL tools will forced into 'break' build mode.
- # credscan:
- # suppressionsFile: $(Build.SourcesDirectory)\.config\CredScanSuppressions.json
- binskim:
- break: true # always break the build on binskim issues in addition to TSA upload
- policheck:
- break: true # always break the build on policheck issues. You can disable it by setting to 'false'
- # baseline:
- # baselineFile: $(Build.SourcesDirectory)\.gdn\global.gdnbaselines
- cg:
- failOnAlert: false
-
- stages:
- - stage: build
- jobs:
- - job: main
- pool:
- type: windows
-
- variables:
- ob_outputDirectory: '$(REPOROOT)\out' # this directory is uploaded to pipeline artifacts, reddog and cloudvault
- ob_sdl_binskim_break: true
- ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/master') }}: # conditionally enable symbolsPublishing for master branch only
- ob_symbolsPublishing_enabled: true
- # ob_sdl_baseline_baselineFile: $(Build.SourcesDirectory)\.gdn\build.official.gdnbaselines
- # ob_sdl_codeSignValidation_excludes: -|**\*.js # Example -|**\*.js;-|**\Test*\**
- ob_artifactBaseName: 'drop'
- ob_sdl_cg_failOnAlert: false
-
- steps:
- - task: CmdLine@2
- displayName: 'Install .NET'
- inputs:
- script: $(Build.SourcesDirectory)\build\CallPowerShell.cmd build/install-dotnet.ps1 -RestoreOnly
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CmdLine@2
- displayName: 'Restore'
- inputs:
- script: $(Build.SourcesDirectory)\build\CallPowerShell.cmd build.ps1 -RestoreOnly
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: onebranch.pipeline.version@1 # generates automatic version
- displayName: 'Setup BuildNumber'
- inputs:
- system: 'BuildRevision'
- major: '1'
- minor: '0'
- name: 'Azconfig-DotnetProvider'
- # exclude_commit: true
-
-
-
- - task: CmdLine@2
- displayName: 'Dotnet Build'
- inputs:
- script: $(Build.SourcesDirectory)\build\CallPowerShell.cmd build.ps1
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CopyFiles@2
- inputs:
- SourceFolder: '$(Build.SourcesDirectory)\buildlogs'
- Contents: |
- **/*
- TargetFolder: '$(Build.SourcesDirectory)\out\outputs\build\Build Logs'
-
-
-
- - task: onebranch.pipeline.signing@1
- displayName: 'Signing'
- inputs:
- command: 'sign'
- signing_environment: 'azure-ado'
- signing_profile: 'external_distribution '
- files_to_sign: '*/bin/Release/**/*'
- search_root: '$(Build.SourcesDirectory)\src'
-
-
- - task: CopyFiles@2
- inputs:
- SourceFolder: '$(Build.SourcesDirectory)\src'
- Contents: |
- */bin/Release/**/*
- TargetFolder: '$(Build.SourcesDirectory)\out\outputs\build\Binaries'
-
- - task: CmdLine@2
- displayName: 'Dotnet Pack'
- inputs:
- script: $(Build.SourcesDirectory)\build\CallPowerShell.cmd pack.ps1
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CopyFiles@2
- inputs:
- SourceFolder: '$(Build.SourcesDirectory)\buildlogs'
- Contents: |
- **/*
- TargetFolder: '$(Build.SourcesDirectory)\out\outputs\package\Build Logs'
-
-
-
- - task: onebranch.pipeline.signing@1
- displayName: 'Signing'
- inputs:
- command: 'sign'
- signing_environment: 'azure-ado'
- signing_profile: 'external_distribution '
- files_to_sign: '*/bin/PackageOutput/**/*.nupkg'
- search_root: '$(Build.SourcesDirectory)\src'
-
-
- - task: CopyFiles@2
- inputs:
- SourceFolder: '$(Build.SourcesDirectory)\src'
- Contents: |
- */bin/PackageOutput/**/*.nupkg
- TargetFolder: '$(Build.SourcesDirectory)\out\outputs\package\Packages'
-
- - task: CmdLine@2
- displayName: 'Dotnet Test'
- inputs:
- script: '$(Build.SourcesDirectory)\build/CallPowerShell.cmd test.ps1|| exit /b 0'
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CopyFiles@2
- inputs:
- SourceFolder: '$(Build.SourcesDirectory)\tests'
- Contents: '**/*.trx'
- TargetFolder: '$(Build.SourcesDirectory)\out\outputs\test\_post_command__run_log_alerts_schedular_tests\_testresults'
-
- - task: PublishTestResults@2
- displayName: 'Unit Tests'
- inputs:
- testResultsFormat: 'vstest'
- testResultsFiles: '**/*.trx'
- searchFolder: ''
- failTaskOnFailedTests: True
- testRunTitle: Unit Tests
\ No newline at end of file
diff --git a/.pipelines/windows-buddy.yml b/.pipelines/windows-buddy.yml
deleted file mode 100644
index b9dcd260..00000000
--- a/.pipelines/windows-buddy.yml
+++ /dev/null
@@ -1,125 +0,0 @@
-
-pr:
-- main
-- preview
-- release/*
-
-trigger:
-- none
-
-parameters: # parameters are shown up in ADO UI in a build queue time
-- name: 'debug'
- displayName: 'Enable debug output'
- type: boolean
- default: false
-
-jobs:
-- job: main
- pool:
- type: windows
- isCustom: true
- name: Azure Pipelines
- vmImage: 'windows-latest'
-
- variables:
- Codeql.Enabled: true
-
- steps:
- - task: AntiMalware@4
- inputs:
- InputType: 'Basic'
- ScanType: 'CustomScan'
- FileDirPath: '$(Build.StagingDirectory)'
- TreatSignatureUpdateFailureAs: 'Warning'
- SignatureFreshness: 'UpToDate'
- TreatStaleSignatureAs: 'Error'
-
- - task: CredScan@3
-
- - task: nuget-security-analysis@0
-
- - task: CmdLine@2
- displayName: 'Install .NET'
- inputs:
- script: build\CallPowerShell.cmd build/install-dotnet.ps1 -RestoreOnly
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CmdLine@2
- displayName: 'Restore'
- inputs:
- script: $(Build.SourcesDirectory)\build\CallPowerShell.cmd build.ps1 -RestoreOnly
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CodeQL3000Init@0
- displayName: 'Initialize CodeQL'
-
- - task: CmdLine@2
- displayName: 'Dotnet Build'
- inputs:
- script: $(Build.SourcesDirectory)\build\CallPowerShell.cmd build.ps1
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CopyFiles@2
- inputs:
- SourceFolder: '$(Build.SourcesDirectory)\buildlogs'
- Contents: |
- **/*
- TargetFolder: '$(Build.SourcesDirectory)\out\outputs\build\Build Logs'
-
- - task: CmdLine@2
- displayName: 'Dotnet Pack'
- inputs:
- script: $(Build.SourcesDirectory)\build\CallPowerShell.cmd pack.ps1
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CopyFiles@2
- inputs:
- SourceFolder: '$(Build.SourcesDirectory)\buildlogs'
- Contents: |
- **/*
- TargetFolder: '$(Build.SourcesDirectory)\out\outputs\package\Build Logs'
-
- - task: CmdLine@2
- displayName: 'Dotnet Test'
- inputs:
- script: '$(Build.SourcesDirectory)\build/CallPowerShell.cmd test.ps1|| exit /b 0'
- workingDirectory: '$(Build.SourcesDirectory)'
-
- - task: CopyFiles@2
- inputs:
- SourceFolder: '$(Build.SourcesDirectory)\tests'
- Contents: '**/*.trx'
- TargetFolder: '$(Build.SourcesDirectory)\out\outputs\test\_post_command__run_log_alerts_schedular_tests\_testresults'
-
- - task: PublishTestResults@2
- displayName: 'Unit Tests'
- inputs:
- testResultsFormat: 'vstest'
- testResultsFiles: '**/*.trx'
- searchFolder: ''
- failTaskOnFailedTests: True
- testRunTitle: Unit Tests
-
- - task: ComponentGovernanceComponentDetection@0
- displayName: "Component Detection"
- inputs:
- scanType: 'Register'
- verbosity: 'Verbose'
- alertWarningLevel: 'High'
-
- - task: BinSkim@4
- inputs:
- InputType: 'Basic'
- Function: 'analyze'
- TargetPattern: 'guardianGlob'
- AnalyzeTargetGlob: '$(Build.SourcesDirectory)\**.dll;$(Build.SourcesDirectory)\**.exe;'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifacts'
- inputs:
- targetPath: '$(Build.SourcesDirectory)\out\outputs'
- artifact: 'drop'
- publishLocation: 'pipeline'
-
- - task: CodeQL3000Finalize@0
- displayName: 'Finalize CodeQL'
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..e1220a8d
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+ True
+
+
+
\ No newline at end of file
diff --git a/examples/ConfigStoreDemo/ConfigStoreDemo.csproj b/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
index caab5885..a4845d9f 100644
--- a/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
+++ b/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
@@ -1,17 +1,22 @@
-
+
+
false
net8.0
+
+
+
Always
+
diff --git a/examples/ConfigStoreDemo/Pages/About.cshtml.cs b/examples/ConfigStoreDemo/Pages/About.cshtml.cs
index dbbdbbf1..ab5a5b85 100644
--- a/examples/ConfigStoreDemo/Pages/About.cshtml.cs
+++ b/examples/ConfigStoreDemo/Pages/About.cshtml.cs
@@ -1,10 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Examples.ConfigStoreDemo.Pages
diff --git a/examples/ConfigStoreDemo/Pages/Contact.cshtml.cs b/examples/ConfigStoreDemo/Pages/Contact.cshtml.cs
index 846a4cbc..64453907 100644
--- a/examples/ConfigStoreDemo/Pages/Contact.cshtml.cs
+++ b/examples/ConfigStoreDemo/Pages/Contact.cshtml.cs
@@ -1,10 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Examples.ConfigStoreDemo.Pages
diff --git a/examples/ConfigStoreDemo/Pages/Error.cshtml.cs b/examples/ConfigStoreDemo/Pages/Error.cshtml.cs
index a75e869d..61725493 100644
--- a/examples/ConfigStoreDemo/Pages/Error.cshtml.cs
+++ b/examples/ConfigStoreDemo/Pages/Error.cshtml.cs
@@ -1,12 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.RazorPages;
+using System.Diagnostics;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Examples.ConfigStoreDemo.Pages
{
diff --git a/examples/ConsoleApplication/ConsoleApplication.csproj b/examples/ConsoleApplication/ConsoleApplication.csproj
index bd4756fa..0b63a9e4 100644
--- a/examples/ConsoleApplication/ConsoleApplication.csproj
+++ b/examples/ConsoleApplication/ConsoleApplication.csproj
@@ -1,4 +1,4 @@
-
+
false
diff --git a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
index cb429e03..a5d4dee4 100644
--- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
@@ -1,4 +1,4 @@
-
+
@@ -21,7 +21,7 @@
- 8.0.0-preview.3
+ 8.1.0-preview
diff --git a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
index 67df4997..4354b77e 100644
--- a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
@@ -1,4 +1,4 @@
-
+
@@ -24,7 +24,7 @@
- 8.0.0-preview.3
+ 8.1.0-preview
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AssemblyInfo.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AssemblyInfo.cs
index ded5fff4..f9522baf 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AssemblyInfo.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AssemblyInfo.cs
@@ -30,4 +30,4 @@
"c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654" +
"753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46" +
"ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484c" +
-"f7045cc7")]
\ No newline at end of file
+"f7045cc7")]
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs
index 2bdacb5a..cca16df8 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs
@@ -14,7 +14,17 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
///
public class AzureAppConfigurationKeyVaultOptions
{
+ // 6 retries is the highest number that will make the total retry time comfortably fall under the default startup timeout of 100 seconds.
+ // This allows the provider to throw a KeyVaultReferenceException with all relevant information and halt startup instead of timing out.
+ private const int KeyVaultMaxRetries = 6;
+
internal TokenCredential Credential;
+ internal SecretClientOptions ClientOptions = new SecretClientOptions
+ {
+ Retry = {
+ MaxRetries = KeyVaultMaxRetries
+ }
+ };
internal List SecretClients = new List();
internal Func> SecretResolver;
internal Dictionary SecretRefreshIntervals = new Dictionary();
@@ -31,6 +41,17 @@ public AzureAppConfigurationKeyVaultOptions SetCredential(TokenCredential creden
return this;
}
+ ///
+ /// Configures the client options used when connecting to key vaults that have no registered .
+ /// The client options will not affect instances registered via .
+ ///
+ /// A callback used to configure secret client options.
+ public AzureAppConfigurationKeyVaultOptions ConfigureClientOptions(Action configure)
+ {
+ configure?.Invoke(ClientOptions);
+ return this;
+ }
+
///
/// Registers the specified instance to use to resolve key vault references for secrets from associated key vault.
///
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index 69b6b399..b5dd42f1 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -126,6 +126,7 @@ public AzureAppConfigurationProvider(IConfigurationClientManager configClientMan
requestTracingDisabled = Environment.GetEnvironmentVariable(RequestTracingConstants.RequestTracingDisabledEnvironmentVariable);
}
catch (SecurityException) { }
+
_requestTracingEnabled = bool.TryParse(requestTracingDisabled, out bool tracingDisabled) ? !tracingDisabled : true;
if (_requestTracingEnabled)
@@ -209,7 +210,7 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
//
// Filter clients based on their backoff status
- clients = clients.Where(client =>
+ clients = clients.Where(client =>
{
Uri endpoint = _configClientManager.GetEndpointForClient(client);
@@ -321,7 +322,8 @@ await CallWithRequestTracing(
refreshAll = true;
break;
}
- } else
+ }
+ else
{
logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString()));
}
@@ -388,7 +390,15 @@ await CallWithRequestTracing(
// Invalidate the cached Key Vault secret (if any) for this ConfigurationSetting
foreach (IKeyValueAdapter adapter in _options.Adapters)
{
- adapter.OnChangeDetected(change.Current);
+ // If the current setting is null, try to pass the previous setting instead
+ if (change.Current != null)
+ {
+ adapter.OnChangeDetected(change.Current);
+ }
+ else if (change.Previous != null)
+ {
+ adapter.OnChangeDetected(change.Previous);
+ }
}
}
}
@@ -669,17 +679,6 @@ private async Task TryInitializeAsync(IEnumerable cli
throw;
}
- catch (KeyVaultReferenceException exception)
- {
- if (IsFailOverable(exception))
- {
- startupExceptions.Add(exception);
-
- return false;
- }
-
- throw;
- }
catch (AggregateException exception)
{
if (exception.InnerExceptions?.Any(e => e is OperationCanceledException) ?? false)
@@ -698,7 +697,7 @@ private async Task TryInitializeAsync(IEnumerable cli
return false;
}
-
+
throw;
}
@@ -968,7 +967,8 @@ private void SetRequestTracingOptions()
IsKeyVaultConfigured = _options.IsKeyVaultConfigured,
IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured,
ReplicaCount = _options.Endpoints?.Count() - 1 ?? _options.ConnectionStrings?.Count() - 1 ?? 0,
- FeatureFlagTracing = _options.FeatureFlagTracing
+ FeatureFlagTracing = _options.FeatureFlagTracing,
+ IsLoadBalancingEnabled = _options.LoadBalancingEnabled
};
}
@@ -1003,6 +1003,11 @@ private async Task ExecuteWithFailOverPolicyAsync(
Func> funcToExecute,
CancellationToken cancellationToken = default)
{
+ if (_requestTracingEnabled && _requestTracingOptions != null)
+ {
+ _requestTracingOptions.IsFailoverRequest = false;
+ }
+
if (_options.LoadBalancingEnabled && _lastSuccessfulEndpoint != null && clients.Count() > 1)
{
int nextClientIndex = 0;
@@ -1057,15 +1062,6 @@ private async Task ExecuteWithFailOverPolicyAsync(
throw;
}
}
- catch (KeyVaultReferenceException kvre)
- {
- if (!IsFailOverable(kvre) || !clientEnumerator.MoveNext())
- {
- backoffAllClients = true;
-
- throw;
- }
- }
catch (AggregateException ae)
{
if (!IsFailOverable(ae) || !clientEnumerator.MoveNext())
@@ -1105,6 +1101,11 @@ private async Task ExecuteWithFailOverPolicyAsync(
}
previousEndpoint = currentEndpoint;
+
+ if (_requestTracingEnabled && _requestTracingOptions != null)
+ {
+ _requestTracingOptions.IsFailoverRequest = true;
+ }
}
}
@@ -1132,7 +1133,9 @@ private bool IsFailOverable(RequestFailedException rfe)
{
if (rfe.Status == HttpStatusCodes.TooManyRequests ||
rfe.Status == (int)HttpStatusCode.RequestTimeout ||
- rfe.Status >= (int)HttpStatusCode.InternalServerError)
+ rfe.Status >= (int)HttpStatusCode.InternalServerError ||
+ rfe.Status == (int)HttpStatusCode.Forbidden ||
+ rfe.Status == (int)HttpStatusCode.Unauthorized)
{
return true;
}
@@ -1154,20 +1157,6 @@ innerException is SocketException ||
innerException is IOException;
}
- private bool IsFailOverable(KeyVaultReferenceException kvre)
- {
- if (kvre.InnerException is RequestFailedException rfe && IsFailOverable(rfe))
- {
- return true;
- }
- else if (kvre.InnerException is AggregateException ae && IsFailOverable(ae))
- {
- return true;
- }
-
- return false;
- }
-
private async Task> MapConfigurationSettings(Dictionary data)
{
Dictionary mappedData = new Dictionary(StringComparer.OrdinalIgnoreCase);
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationRefreshOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationRefreshOptions.cs
index 32ff2291..f3fb6c4a 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationRefreshOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationRefreshOptions.cs
@@ -14,7 +14,7 @@ public class AzureAppConfigurationRefreshOptions
{
internal TimeSpan RefreshInterval { get; private set; } = RefreshConstants.DefaultRefreshInterval;
internal ISet RefreshRegistrations = new HashSet();
-
+
///
/// Register the specified individual key-value to be refreshed when the configuration provider's triggers a refresh.
/// The instance can be obtained by calling .
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
index 446fa714..dee62006 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
@@ -12,7 +12,8 @@ internal class AzureAppConfigurationSource : IConfigurationSource
public AzureAppConfigurationSource(Action optionsInitializer, bool optional = false)
{
- _optionsProvider = () => {
+ _optionsProvider = () =>
+ {
var options = new AzureAppConfigurationOptions();
optionsInitializer(options);
return options;
@@ -37,18 +38,18 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
else if (options.ConnectionStrings != null)
{
clientManager = new ConfigurationClientManager(
- options.ConnectionStrings,
- options.ClientOptions,
- options.ReplicaDiscoveryEnabled,
+ options.ConnectionStrings,
+ options.ClientOptions,
+ options.ReplicaDiscoveryEnabled,
options.LoadBalancingEnabled);
}
else if (options.Endpoints != null && options.Credential != null)
{
clientManager = new ConfigurationClientManager(
- options.Endpoints,
- options.Credential,
- options.ClientOptions,
- options.ReplicaDiscoveryEnabled,
+ options.Endpoints,
+ options.Credential,
+ options.ClientOptions,
+ options.ReplicaDiscoveryEnabled,
options.LoadBalancingEnabled);
}
else
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
index 8d0ec3b5..a272b413 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
@@ -84,7 +84,15 @@ public void OnChangeDetected(ConfigurationSetting setting = null)
}
else
{
- _secretProvider.RemoveSecretFromCache(setting.Key);
+ if (CanProcess(setting))
+ {
+ string secretRefUri = ParseSecretReferenceUri(setting);
+
+ if (!string.IsNullOrEmpty(secretRefUri) && Uri.TryCreate(secretRefUri, UriKind.Absolute, out Uri secretUri) && KeyVaultSecretIdentifier.TryCreate(secretUri, out KeyVaultSecretIdentifier secretIdentifier))
+ {
+ _secretProvider.RemoveSecretFromCache(secretIdentifier.SourceId);
+ }
+ }
}
}
@@ -143,4 +151,4 @@ private string ParseSecretReferenceUri(ConfigurationSetting setting)
return secretRefUri;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs
index 6c8f4ec2..57505ff9 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs
@@ -15,14 +15,14 @@ internal class AzureKeyVaultSecretProvider
{
private readonly AzureAppConfigurationKeyVaultOptions _keyVaultOptions;
private readonly IDictionary _secretClients;
- private readonly Dictionary _cachedKeyVaultSecrets;
- private string _nextRefreshKey;
+ private readonly Dictionary _cachedKeyVaultSecrets;
+ private Uri _nextRefreshSourceId;
private DateTimeOffset? _nextRefreshTime;
public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVaultOptions = null)
{
_keyVaultOptions = keyVaultOptions ?? new AzureAppConfigurationKeyVaultOptions();
- _cachedKeyVaultSecrets = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _cachedKeyVaultSecrets = new Dictionary();
_secretClients = new Dictionary(StringComparer.OrdinalIgnoreCase);
if (_keyVaultOptions.SecretClients != null)
@@ -39,7 +39,7 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi
{
string secretValue = null;
- if (_cachedKeyVaultSecrets.TryGetValue(key, out CachedKeyVaultSecret cachedSecret) &&
+ if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedSecret) &&
(!cachedSecret.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedSecret.RefreshAt.Value))
{
return cachedSecret.SecretValue;
@@ -68,12 +68,12 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi
secretValue = await _keyVaultOptions.SecretResolver(secretIdentifier.SourceId).ConfigureAwait(false);
}
- cachedSecret = new CachedKeyVaultSecret(secretValue);
+ cachedSecret = new CachedKeyVaultSecret(secretValue, secretIdentifier.SourceId);
success = true;
}
finally
{
- SetSecretInCache(key, cachedSecret, success);
+ SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success);
}
return secretValue;
@@ -86,16 +86,34 @@ public bool ShouldRefreshKeyVaultSecrets()
public void ClearCache()
{
- _cachedKeyVaultSecrets.Clear();
- _nextRefreshKey = null;
- _nextRefreshTime = null;
+ var sourceIdsToRemove = new List();
+
+ var utcNow = DateTimeOffset.UtcNow;
+
+ foreach (KeyValuePair secret in _cachedKeyVaultSecrets)
+ {
+ if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow)
+ {
+ sourceIdsToRemove.Add(secret.Key);
+ }
+ }
+
+ foreach (Uri sourceId in sourceIdsToRemove)
+ {
+ _cachedKeyVaultSecrets.Remove(sourceId);
+ }
+
+ if (_cachedKeyVaultSecrets.Any())
+ {
+ UpdateNextRefreshableSecretFromCache();
+ }
}
- public void RemoveSecretFromCache(string key)
+ public void RemoveSecretFromCache(Uri sourceId)
{
- _cachedKeyVaultSecrets.Remove(key);
+ _cachedKeyVaultSecrets.Remove(sourceId);
- if (key == _nextRefreshKey)
+ if (sourceId == _nextRefreshSourceId)
{
UpdateNextRefreshableSecretFromCache();
}
@@ -115,12 +133,17 @@ private SecretClient GetSecretClient(Uri secretUri)
return null;
}
- client = new SecretClient(new Uri(secretUri.GetLeftPart(UriPartial.Authority)), _keyVaultOptions.Credential);
+ client = new SecretClient(
+ new Uri(secretUri.GetLeftPart(UriPartial.Authority)),
+ _keyVaultOptions.Credential,
+ _keyVaultOptions.ClientOptions);
+
_secretClients.Add(keyVaultId, client);
+
return client;
}
- private void SetSecretInCache(string key, CachedKeyVaultSecret cachedSecret, bool success = true)
+ private void SetSecretInCache(Uri sourceId, string key, CachedKeyVaultSecret cachedSecret, bool success = true)
{
if (cachedSecret == null)
{
@@ -128,31 +151,31 @@ private void SetSecretInCache(string key, CachedKeyVaultSecret cachedSecret, boo
}
UpdateCacheExpirationTimeForSecret(key, cachedSecret, success);
- _cachedKeyVaultSecrets[key] = cachedSecret;
+ _cachedKeyVaultSecrets[sourceId] = cachedSecret;
- if (key == _nextRefreshKey)
+ if (sourceId == _nextRefreshSourceId)
{
UpdateNextRefreshableSecretFromCache();
}
else if ((cachedSecret.RefreshAt.HasValue && _nextRefreshTime.HasValue && cachedSecret.RefreshAt.Value < _nextRefreshTime.Value)
|| (cachedSecret.RefreshAt.HasValue && !_nextRefreshTime.HasValue))
{
- _nextRefreshKey = key;
+ _nextRefreshSourceId = sourceId;
_nextRefreshTime = cachedSecret.RefreshAt.Value;
}
}
private void UpdateNextRefreshableSecretFromCache()
{
- _nextRefreshKey = null;
+ _nextRefreshSourceId = null;
_nextRefreshTime = DateTimeOffset.MaxValue;
- foreach (KeyValuePair secret in _cachedKeyVaultSecrets)
+ foreach (KeyValuePair secret in _cachedKeyVaultSecrets)
{
if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < _nextRefreshTime)
{
_nextRefreshTime = secret.Value.RefreshAt;
- _nextRefreshKey = secret.Key;
+ _nextRefreshSourceId = secret.Key;
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/CachedKeyVaultSecret.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/CachedKeyVaultSecret.cs
index 1b813d36..ab09311e 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/CachedKeyVaultSecret.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/CachedKeyVaultSecret.cs
@@ -22,11 +22,23 @@ internal class CachedKeyVaultSecret
///
public int RefreshAttempts { get; set; }
- public CachedKeyVaultSecret(string secretValue = null, DateTimeOffset? refreshAt = null, int refreshAttempts = 0)
+ ///
+ /// The last time this secret was reloaded from Key Vault.
+ ///
+ public DateTimeOffset LastRefreshTime { get; set; }
+
+ ///
+ /// The source for this secret.
+ ///
+ public Uri SourceId { get; }
+
+ public CachedKeyVaultSecret(string secretValue = null, Uri sourceId = null, DateTimeOffset? refreshAt = null, int refreshAttempts = 0)
{
SecretValue = secretValue;
RefreshAt = refreshAt;
+ LastRefreshTime = DateTimeOffset.UtcNow;
RefreshAttempts = refreshAttempts;
+ SourceId = sourceId;
}
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs
index 0a80932c..a0215ca3 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs
@@ -161,7 +161,7 @@ public void RefreshClients()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
- if (_replicaDiscoveryEnabled &&
+ if (_replicaDiscoveryEnabled &&
now >= _lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval)
{
_lastFallbackClientRefreshAttempt = now;
@@ -277,9 +277,9 @@ private async Task RefreshFallbackClients(CancellationToken cancellationToken)
// Honor with the DNS based service discovery protocol, but shuffle the results first to ensure hosts can be picked randomly,
// Srv lookup does retrieve trailing dot in the host name, just trim it.
- IEnumerable OrderedHosts = srvTargetHosts.Any() ?
- srvTargetHosts.ToList().Shuffle().SortSrvRecords().Select(r => $"{r.Target.Value.TrimEnd('.')}") :
- Enumerable.Empty();
+ IEnumerable OrderedHosts = srvTargetHosts.Any()
+ ? srvTargetHosts.ToList().Shuffle().SortSrvRecords().Select(r => $"{r.Target.Value.TrimEnd('.')}")
+ : Enumerable.Empty();
foreach (string host in OrderedHosts)
{
@@ -289,9 +289,9 @@ private async Task RefreshFallbackClients(CancellationToken cancellationToken)
{
var targetEndpoint = new Uri($"https://{host}");
- var configClient = _credential == null ?
- new ConfigurationClient(ConnectionStringUtils.Build(targetEndpoint, _id, _secret), _clientOptions) :
- new ConfigurationClient(targetEndpoint, _credential, _clientOptions);
+ var configClient = _credential == null
+ ? new ConfigurationClient(ConnectionStringUtils.Build(targetEndpoint, _id, _secret), _clientOptions)
+ : new ConfigurationClient(targetEndpoint, _credential, _clientOptions);
newDynamicClients.Add(new ConfigurationClientWrapper(targetEndpoint, configClient));
}
@@ -335,7 +335,7 @@ internal bool IsValidEndpoint(string hostName)
public void Dispose()
{
- if (!_isDisposed)
+ if (!_isDisposed)
{
_cancellationTokenSource.Cancel();
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/HttpStatusCodes.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/HttpStatusCodes.cs
index 76de0b48..823895bd 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/HttpStatusCodes.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/HttpStatusCodes.cs
@@ -9,4 +9,4 @@ internal class HttpStatusCodes
// This constant is necessary because System.Net.HttpStatusCode.TooManyRequests is only available in netstandard2.1 and higher.
public static readonly int TooManyRequests = 429;
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RefreshConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RefreshConstants.cs
index 965e380a..2c1616fe 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RefreshConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RefreshConstants.cs
@@ -16,7 +16,7 @@ internal class RefreshConstants
public static readonly TimeSpan MinimumFeatureFlagRefreshInterval = TimeSpan.FromSeconds(1);
// Key Vault secrets
- public static readonly TimeSpan MinimumSecretRefreshInterval = TimeSpan.FromSeconds(1);
+ public static readonly TimeSpan MinimumSecretRefreshInterval = TimeSpan.FromMinutes(1);
// Backoff during refresh failures
public static readonly TimeSpan DefaultMinBackoff = TimeSpan.FromSeconds(30);
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
index 9e542785..15e862b6 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
@@ -10,7 +10,7 @@ internal class RequestTracingConstants
public const string AzureWebAppEnvironmentVariable = "WEBSITE_SITE_NAME";
public const string ContainerAppEnvironmentVariable = "CONTAINER_APP_NAME";
public const string KubernetesEnvironmentVariable = "KUBERNETES_PORT";
-
+
public const string AspNetCoreEnvironmentVariable = "ASPNETCORE_ENVIRONMENT";
public const string DotNetCoreEnvironmentVariable = "DOTNET_ENVIRONMENT";
public const string DevelopmentEnvironmentName = "Development";
@@ -22,7 +22,6 @@ internal class RequestTracingConstants
public const string RequestTypeKey = "RequestType";
public const string HostTypeKey = "Host";
- public const string FilterTypeKey = "Filter";
public const string EnvironmentKey = "Env";
public const string FeatureManagementVersionKey = "FMVer";
public const string FeatureManagementAspNetCoreVersionKey = "FMANCVer";
@@ -30,12 +29,21 @@ internal class RequestTracingConstants
public const string KeyVaultConfiguredTag = "UsesKeyVault";
public const string KeyVaultRefreshConfiguredTag = "RefreshesKeyVault";
public const string ReplicaCountKey = "ReplicaCount";
+ public const string FeaturesKey = "Features";
+ public const string LoadBalancingEnabledTag = "LB";
+ public const string SignalRUsedTag = "SignalR";
+ public const string FailoverRequestTag = "Failover";
+
+ public const string FeatureFlagFilterTypeKey = "Filter";
+ public const string CustomFilter = "CSTM";
+ public const string PercentageFilter = "PRCNT";
+ public const string TimeWindowFilter = "TIME";
+ public const string TargetingFilter = "TRGT";
+ public const string FeatureFlagFeaturesKey = "FFFeatures";
public const string FeatureFlagUsesTelemetryTag = "Telemetry";
public const string FeatureFlagUsesSeedTag = "Seed";
public const string FeatureFlagMaxVariantsKey = "MaxVariants";
public const string FeatureFlagUsesVariantConfigurationReferenceTag = "ConfigRef";
- public const string FeatureFlagFeaturesKey = "FFFeatures";
- public const string SignalRUsedTag = "UsesSignalR";
public const string DiagnosticHeaderActivityName = "Azure.CustomDiagnosticHeaders";
public const string CorrelationContextHeader = "Correlation-Context";
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Exceptions/KeyVaultReferenceException.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Exceptions/KeyVaultReferenceException.cs
index 58046c93..7d549a06 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Exceptions/KeyVaultReferenceException.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Exceptions/KeyVaultReferenceException.cs
@@ -20,7 +20,7 @@ public class KeyVaultReferenceException : Exception
/// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. ///
public KeyVaultReferenceException(string message,
Exception inner)
- :base(string.Empty, inner)
+ : base(string.Empty, inner)
{
_message = message;
}
@@ -29,7 +29,7 @@ public KeyVaultReferenceException(string message,
///Gets a message that describes the current exception.
///Returns The error message that explains the reason for the exception, or an empty string("").
///
- public override string Message => $"{_message} ErrorCode:'{ErrorCode}' Key:'{Key}' Label:'{Label}' Etag:'{Etag}' SecretIdentifier:'{SecretIdentifier}'";
+ public override string Message => $"{_message} ErrorCode:'{ErrorCode}' Key:'{Key}' Label:'{Label}' Etag:'{Etag}' SecretIdentifier:'{SecretIdentifier}'";
///
/// The key of the Key Vault reference that caused the exception.
@@ -56,5 +56,4 @@ public KeyVaultReferenceException(string message,
///
public string ErrorCode { get; set; }
}
-
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/BytesExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/BytesExtensions.cs
index 0be4bdf4..3c5266ec 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/BytesExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/BytesExtensions.cs
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using System.Text;
using System;
+using System.Text;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
{
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs
index 0e378f44..d479ad6b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs
@@ -36,6 +36,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli
return new KeyValueChange
{
ChangeType = KeyValueChangeType.Modified,
+ Previous = setting,
Current = response.Value,
Key = setting.Key,
Label = setting.Label
@@ -47,6 +48,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli
return new KeyValueChange
{
ChangeType = KeyValueChangeType.Deleted,
+ Previous = setting,
Current = null,
Key = setting.Key,
Label = setting.Label
@@ -56,6 +58,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli
return new KeyValueChange
{
ChangeType = KeyValueChangeType.None,
+ Previous = setting,
Current = setting,
Key = setting.Key,
Label = setting.Label
@@ -116,7 +119,7 @@ public static async Task> GetKeyValueChangeCollectio
await TracingUtils.CallWithRequestTracing(options.RequestTracingEnabled, RequestType.Watch, options.RequestTracingOptions,
async () =>
{
- await foreach(ConfigurationSetting setting in client.GetConfigurationSettingsAsync(selector, cancellationToken).ConfigureAwait(false))
+ await foreach (ConfigurationSetting setting in client.GetConfigurationSettingsAsync(selector, cancellationToken).ConfigureAwait(false))
{
if (!eTagMap.TryGetValue(setting.Key, out ETag etag) || !etag.Equals(setting.ETag))
{
@@ -158,6 +161,7 @@ await TracingUtils.CallWithRequestTracing(options.RequestTracingEnabled, Request
ChangeType = KeyValueChangeType.Modified,
Key = setting.Key,
Label = options.Label.NormalizeNull(),
+ Previous = null,
Current = setting
});
string key = setting.Key.Substring(FeatureManagementConstants.FeatureFlagMarker.Length);
@@ -176,6 +180,7 @@ await TracingUtils.CallWithRequestTracing(options.RequestTracingEnabled, Request
ChangeType = KeyValueChangeType.Deleted,
Key = kvp.Key,
Label = options.Label.NormalizeNull(),
+ Previous = null,
Current = null
});
string key = kvp.Key.Substring(FeatureManagementConstants.FeatureFlagMarker.Length);
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
index 7ac04ca9..ee904324 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
@@ -89,4 +89,4 @@ public static bool TryCreatePushNotification(this EventGridEvent eventGridEvent,
return false;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ListExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ListExtensions.cs
index 5579553c..93722539 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ListExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ListExtensions.cs
@@ -65,4 +65,4 @@ public static void AppendUnique(this List items, T item)
items.Add(item);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/StringExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/StringExtensions.cs
index 7bcf7212..8b2c488d 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/StringExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/StringExtensions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
+using System;
+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
{
internal static class LabelFilters
@@ -16,5 +18,12 @@ public static string NormalizeNull(this string s)
{
return s == LabelFilters.Null ? null : s;
}
+
+ public static string ToBase64String(this string s)
+ {
+ byte[] bytes = System.Text.Encoding.UTF8.GetBytes(s);
+
+ return Convert.ToBase64String(bytes);
+ }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
index 80aed990..12145cd7 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
@@ -11,4 +11,4 @@ internal class ClientFilter
public JsonElement Parameters { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs
index ec29c199..d1c23003 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs
@@ -11,4 +11,4 @@ internal class FeatureConditions
public string RequirementType { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
index 7fe6245e..31af50a6 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
@@ -19,4 +19,4 @@ internal class FeatureFlag
public FeatureTelemetry Telemetry { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagOptions.cs
index 11f2fde1..1e8beae6 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagOptions.cs
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -24,10 +24,10 @@ public class FeatureFlagOptions
///
/// The time after which feature flags can be refreshed. Must be greater than or equal to 1 second.
///
- internal TimeSpan RefreshInterval
+ internal TimeSpan RefreshInterval
{
get { return _refreshInterval; }
- set { _refreshInterval = value; }
+ set { _refreshInterval = value; }
}
///
@@ -38,7 +38,7 @@ internal TimeSpan RefreshInterval
///
/// The time after which the cached values of the feature flags expire. Must be greater than or equal to 1 second.
///
- [Obsolete("The " + nameof(CacheExpirationInterval) + " property is deprecated and will be removed in a future release. " +
+ [Obsolete("The " + nameof(CacheExpirationInterval) + " property is deprecated and will be removed in a future release. " +
"Please use the new " + nameof(SetRefreshInterval) + " method instead. " +
"Note that the usage has changed, but the functionality remains the same.")]
public TimeSpan CacheExpirationInterval
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs
index c1e4aaa5..f48b5220 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs
@@ -14,11 +14,6 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage
///
internal class FeatureFlagTracing
{
- private const string CustomFilter = "CSTM";
- private const string PercentageFilter = "PRCNT";
- private const string TimeWindowFilter = "TIME";
- private const string TargetingFilter = "TRGT";
-
// Built-in Feature Filter Names
private readonly List PercentageFilterNames = new List { "Percentage", "Microsoft.Percentage", "PercentageFilter", "Microsoft.PercentageFilter" };
private readonly List TimeWindowFilterNames = new List { "TimeWindow", "Microsoft.TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindowFilter" };
@@ -98,7 +93,7 @@ public string CreateFiltersString()
if (UsesCustomFilter)
{
- sb.Append(CustomFilter);
+ sb.Append(RequestTracingConstants.CustomFilter);
}
if (UsesPercentageFilter)
@@ -108,7 +103,7 @@ public string CreateFiltersString()
sb.Append(RequestTracingConstants.Delimiter);
}
- sb.Append(PercentageFilter);
+ sb.Append(RequestTracingConstants.PercentageFilter);
}
if (UsesTimeWindowFilter)
@@ -118,7 +113,7 @@ public string CreateFiltersString()
sb.Append(RequestTracingConstants.Delimiter);
}
- sb.Append(TimeWindowFilter);
+ sb.Append(RequestTracingConstants.TimeWindowFilter);
}
if (UsesTargetingFilter)
@@ -128,14 +123,23 @@ public string CreateFiltersString()
sb.Append(RequestTracingConstants.Delimiter);
}
- sb.Append(TargetingFilter);
+ sb.Append(RequestTracingConstants.TargetingFilter);
}
return sb.ToString();
}
+ ///
+ /// Returns a formatted string containing code names, indicating which tracing features are used by feature flags.
+ ///
+ /// Formatted string like: "Seed+ConfigRef+Telemetry". If no tracing features are used, empty string will be returned.
public string CreateFeaturesString()
{
+ if (!UsesAnyTracingFeature())
+ {
+ return string.Empty;
+ }
+
var sb = new StringBuilder();
if (UsesSeed)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
index c6d86d84..aa573a1e 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
@@ -44,6 +44,7 @@ internal class FeatureManagementConstants
public const string ETag = "ETag";
public const string FeatureFlagId = "FeatureFlagId";
public const string FeatureFlagReference = "FeatureFlagReference";
+ public const string AllocationId = "AllocationId";
// Dotnet schema keys
public const string DotnetSchemaSectionName = "FeatureManagement";
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
index b562a904..c29827b9 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
@@ -5,6 +5,7 @@
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
@@ -117,7 +118,7 @@ private List> ProcessDotnetSchemaFeatureFlag(Featur
{
keyValues.Add(new KeyValuePair($"{featureFlagPath}", false.ToString()));
}
-
+
return keyValues;
}
@@ -319,12 +320,98 @@ private List> ProcessMicrosoftSchemaFeatureFlag(Fea
keyValues.Add(new KeyValuePair($"{telemetryPath}:{FeatureManagementConstants.Metadata}:{FeatureManagementConstants.ETag}", setting.ETag.ToString()));
keyValues.Add(new KeyValuePair($"{telemetryPath}:{FeatureManagementConstants.Enabled}", telemetry.Enabled.ToString()));
+
+ if (featureFlag.Allocation != null)
+ {
+ string allocationId = CalculateAllocationId(featureFlag);
+
+ if (allocationId != null)
+ {
+ keyValues.Add(new KeyValuePair($"{telemetryPath}:{FeatureManagementConstants.Metadata}:{FeatureManagementConstants.AllocationId}", allocationId));
+ }
+ }
}
}
return keyValues;
}
+ private string CalculateAllocationId(FeatureFlag flag)
+ {
+ Debug.Assert(flag.Allocation != null);
+
+ StringBuilder inputBuilder = new StringBuilder();
+
+ // Seed
+ inputBuilder.Append($"seed={flag.Allocation.Seed ?? string.Empty}");
+
+ var allocatedVariants = new HashSet();
+
+ // DefaultWhenEnabled
+ if (flag.Allocation.DefaultWhenEnabled != null)
+ {
+ allocatedVariants.Add(flag.Allocation.DefaultWhenEnabled);
+ }
+
+ inputBuilder.Append($"\ndefault_when_enabled={flag.Allocation.DefaultWhenEnabled ?? string.Empty}");
+
+ // Percentiles
+ inputBuilder.Append("\npercentiles=");
+
+ if (flag.Allocation.Percentile != null && flag.Allocation.Percentile.Any())
+ {
+ IEnumerable sortedPercentiles = flag.Allocation.Percentile
+ .Where(p => p.From != p.To)
+ .OrderBy(p => p.From)
+ .ToList();
+
+ allocatedVariants.UnionWith(sortedPercentiles.Select(p => p.Variant));
+
+ inputBuilder.Append(string.Join(";", sortedPercentiles.Select(p => $"{p.From},{p.Variant.ToBase64String()},{p.To}")));
+ }
+
+ // If there's no custom seed and no variants allocated, stop now and return null
+ if (flag.Allocation.Seed == null &&
+ !allocatedVariants.Any())
+ {
+ return null;
+ }
+
+ // Variants
+ inputBuilder.Append("\nvariants=");
+
+ if (allocatedVariants.Any() && flag.Variants != null && flag.Variants.Any())
+ {
+ IEnumerable sortedVariants = flag.Variants
+ .Where(variant => allocatedVariants.Contains(variant.Name))
+ .OrderBy(variant => variant.Name)
+ .ToList();
+
+ inputBuilder.Append(string.Join(";", sortedVariants.Select(v =>
+ {
+ var variantValue = string.Empty;
+
+ if (v.ConfigurationValue.ValueKind != JsonValueKind.Null && v.ConfigurationValue.ValueKind != JsonValueKind.Undefined)
+ {
+ variantValue = v.ConfigurationValue.SerializeWithSortedKeys();
+ }
+
+ return $"{v.Name.ToBase64String()},{(variantValue)}";
+ })));
+ }
+
+ // Example input string
+ // input == "seed=123abc\ndefault_when_enabled=Control\npercentiles=0,Blshdk,20;20,Test,100\nvariants=TdLa,standard;Qfcd,special"
+ string input = inputBuilder.ToString();
+
+ using (SHA256 sha256 = SHA256.Create())
+ {
+ byte[] truncatedHash = new byte[15];
+ Array.Copy(sha256.ComputeHash(Encoding.UTF8.GetBytes(input)), truncatedHash, 15);
+ return truncatedHash.ToBase64Url();
+ }
+ }
+
private FormatException CreateFeatureFlagFormatException(string jsonPropertyName, string settingKey, string foundJsonValueKind, string expectedJsonValueKind)
{
return new FormatException(string.Format(
@@ -1076,8 +1163,8 @@ private FeaturePercentileAllocation ParseFeaturePercentileAllocation(ref Utf8Jso
case FeatureManagementConstants.From:
{
- if (reader.Read() &&
- ((reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out int from)) ||
+ if (reader.Read() &&
+ ((reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out int from)) ||
(reader.TokenType == JsonTokenType.String && int.TryParse(reader.GetString(), out from))))
{
featurePercentileAllocation.From = from;
@@ -1185,7 +1272,6 @@ private FeatureVariant ParseFeatureVariant(ref Utf8JsonReader reader, string set
break;
}
-
case FeatureManagementConstants.StatusOverride:
{
if (reader.Read() && reader.TokenType == JsonTokenType.String)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/JsonElementExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/JsonElementExtensions.cs
new file mode 100644
index 00000000..fc7f8b26
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/JsonElementExtensions.cs
@@ -0,0 +1,91 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+
+namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
+{
+ internal static class JsonElementExtensions
+ {
+ public static string SerializeWithSortedKeys(this JsonElement rootElement)
+ {
+ using var stream = new MemoryStream();
+
+ using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }))
+ {
+ WriteElementWithSortedKeys(rootElement, writer);
+ }
+
+ return Encoding.UTF8.GetString(stream.ToArray());
+ }
+
+ private static void WriteElementWithSortedKeys(JsonElement element, Utf8JsonWriter writer)
+ {
+ switch (element.ValueKind)
+ {
+ case JsonValueKind.Object:
+ writer.WriteStartObject();
+
+ foreach (JsonProperty property in element.EnumerateObject().OrderBy(p => p.Name))
+ {
+ writer.WritePropertyName(property.Name);
+ WriteElementWithSortedKeys(property.Value, writer);
+ }
+
+ writer.WriteEndObject();
+ break;
+
+ case JsonValueKind.Array:
+ writer.WriteStartArray();
+
+ foreach (JsonElement item in element.EnumerateArray())
+ {
+ WriteElementWithSortedKeys(item, writer);
+ }
+
+ writer.WriteEndArray();
+ break;
+
+ case JsonValueKind.String:
+ writer.WriteStringValue(element.GetString());
+ break;
+
+ case JsonValueKind.Number:
+ if (element.TryGetInt32(out int intValue))
+ {
+ writer.WriteNumberValue(intValue);
+ }
+ else if (element.TryGetInt64(out long longValue))
+ {
+ writer.WriteNumberValue(longValue);
+ }
+ else if (element.TryGetDecimal(out decimal decimalValue))
+ {
+ writer.WriteNumberValue(element.GetDecimal());
+ }
+ else if (element.TryGetDouble(out double doubleValue))
+ {
+ writer.WriteNumberValue(element.GetDouble());
+ }
+
+ break;
+
+ case JsonValueKind.True:
+ writer.WriteBooleanValue(true);
+ break;
+
+ case JsonValueKind.False:
+ writer.WriteBooleanValue(false);
+ break;
+
+ case JsonValueKind.Null:
+ writer.WriteNullValue();
+ break;
+
+ default:
+ throw new InvalidOperationException($"Unsupported JsonValueKind: {element.ValueKind}");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/JsonFlattener.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/JsonFlattener.cs
index 9974a933..fa5983c1 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/JsonFlattener.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/JsonFlattener.cs
@@ -36,6 +36,7 @@ private void VisitJsonElement(JsonElement element)
{
VisitJsonProperty(property);
}
+
break;
case JsonValueKind.Array:
@@ -45,6 +46,7 @@ private void VisitJsonElement(JsonElement element)
VisitJsonElement(element[index]);
ExitContext();
}
+
break;
case JsonValueKind.String:
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
index 574f64fa..b4448e32 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
@@ -16,7 +16,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
internal class JsonKeyValueAdapter : IKeyValueAdapter
{
- private static readonly IEnumerable ExcludedJsonContentTypes = new[]
+ private static readonly IEnumerable ExcludedJsonContentTypes = new[]
{
FeatureManagementConstants.ContentType,
KeyVaultConstants.ContentType
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs
index 7d41e107..2286016d 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs
@@ -21,5 +21,7 @@ internal struct KeyValueChange
public string Label { get; set; }
public ConfigurationSetting Current { get; set; }
+
+ public ConfigurationSetting Previous { get; set; }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
index 31e1e854..aeedd6e6 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -1,4 +1,5 @@
-
+
+
@@ -34,7 +35,7 @@
- 8.0.0-preview.3
+ 8.1.0-preview
@@ -50,5 +51,5 @@
true
true
-
+
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/PushNotification.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/PushNotification.cs
index e23d8bb4..1690645d 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/PushNotification.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/PushNotification.cs
@@ -24,6 +24,5 @@ public class PushNotification
/// The Type of Event which triggered the .
///
public string EventType { get; set; }
-
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
index bbf9f667..7b06535b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
//
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
+using System.Text;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
@@ -51,5 +52,55 @@ internal class RequestTracingOptions
/// Flag to indicate whether Microsoft.AspNetCore.SignalR assembly is present in the application.
///
public bool IsSignalRUsed { get; set; } = false;
+
+ ///
+ /// Flag to indicate whether load balancing is enabled.
+ ///
+ public bool IsLoadBalancingEnabled { get; set; } = false;
+
+ ///
+ /// Flag to indicate whether the request is triggered by a failover.
+ ///
+ public bool IsFailoverRequest { get; set; } = false;
+
+ ///
+ /// Checks whether any tracing feature is used.
+ ///
+ /// True if any tracing feature is used, otherwise false.
+ public bool UsesAnyTracingFeature()
+ {
+ return IsLoadBalancingEnabled || IsSignalRUsed;
+ }
+
+ ///
+ /// Returns a formatted string containing code names, indicating which tracing features are used by the application.
+ ///
+ /// Formatted string like: "LB+SignalR". If no tracing features are used, empty string will be returned.
+ public string CreateFeaturesString()
+ {
+ if (!UsesAnyTracingFeature())
+ {
+ return string.Empty;
+ }
+
+ var sb = new StringBuilder();
+
+ if (IsLoadBalancingEnabled)
+ {
+ sb.Append(RequestTracingConstants.LoadBalancingEnabledTag);
+ }
+
+ if (IsSignalRUsed)
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append(RequestTracingConstants.Delimiter);
+ }
+
+ sb.Append(RequestTracingConstants.SignalRUsedTag);
+ }
+
+ return sb.ToString();
+ }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/SrvLookupClient.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/SrvLookupClient.cs
index bcbdff75..74bad0b5 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/SrvLookupClient.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/SrvLookupClient.cs
@@ -1,12 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
+using DnsClient;
+using DnsClient.Protocol;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using DnsClient;
-using DnsClient.Protocol;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
@@ -26,7 +26,7 @@ public SrvLookupClient()
public async Task> QueryAsync(string host, CancellationToken cancellationToken)
{
string originSrvDns = $"{TcpOrigin}.{host}";
-
+
IEnumerable originRecords = await InternalQueryAsync(originSrvDns, cancellationToken).ConfigureAwait(false);
if (originRecords == null || originRecords.Count() == 0)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
index 6d5ae208..b1b2b196 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
@@ -138,7 +138,7 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re
{
IList> correlationContextKeyValues = new List>();
IList correlationContextTags = new List();
-
+
correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.RequestTypeKey, Enum.GetName(typeof(RequestType), requestType)));
if (requestTracingOptions.ReplicaCount > 0)
@@ -158,7 +158,7 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re
if (requestTracingOptions.FeatureFlagTracing.UsesAnyFeatureFilter())
{
- correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FilterTypeKey, requestTracingOptions.FeatureFlagTracing.CreateFiltersString()));
+ correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeatureFlagFilterTypeKey, requestTracingOptions.FeatureFlagTracing.CreateFiltersString()));
}
if (requestTracingOptions.FeatureFlagTracing.MaxVariants > 0)
@@ -181,6 +181,11 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re
correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeatureManagementAspNetCoreVersionKey, requestTracingOptions.FeatureManagementAspNetCoreVersion));
}
+ if (requestTracingOptions.UsesAnyTracingFeature())
+ {
+ correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeaturesKey, requestTracingOptions.CreateFeaturesString()));
+ }
+
if (requestTracingOptions.IsKeyVaultConfigured)
{
correlationContextTags.Add(RequestTracingConstants.KeyVaultConfiguredTag);
@@ -191,14 +196,14 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re
correlationContextTags.Add(RequestTracingConstants.KeyVaultRefreshConfiguredTag);
}
- if (requestTracingOptions.IsSignalRUsed)
+ if (requestTracingOptions.IsFailoverRequest)
{
- correlationContextTags.Add(RequestTracingConstants.SignalRUsedTag);
+ correlationContextTags.Add(RequestTracingConstants.FailoverRequestTag);
}
var sb = new StringBuilder();
- foreach (KeyValuePair kvp in correlationContextKeyValues)
+ foreach (KeyValuePair kvp in correlationContextKeyValues)
{
if (sb.Length > 0)
{
diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/AzureAppConfigurationExtensionsTests.cs b/tests/Tests.AzureAppConfiguration.AspNetCore/AzureAppConfigurationExtensionsTests.cs
index 5c093d1a..2d4870f2 100644
--- a/tests/Tests.AzureAppConfiguration.AspNetCore/AzureAppConfigurationExtensionsTests.cs
+++ b/tests/Tests.AzureAppConfiguration.AspNetCore/AzureAppConfigurationExtensionsTests.cs
@@ -1,4 +1,7 @@
-using Microsoft.AspNetCore.Builder;
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using System;
diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/RefreshMiddlewareTests.cs b/tests/Tests.AzureAppConfiguration.AspNetCore/RefreshMiddlewareTests.cs
index 934cd997..de752f03 100644
--- a/tests/Tests.AzureAppConfiguration.AspNetCore/RefreshMiddlewareTests.cs
+++ b/tests/Tests.AzureAppConfiguration.AspNetCore/RefreshMiddlewareTests.cs
@@ -9,9 +9,7 @@
using Moq;
using System.Collections.Generic;
using System.Linq;
-using System.Threading;
using Xunit;
-using static Tests.AzureAppConfiguration.AspNetCore.TestHelper;
namespace Tests.AzureAppConfiguration.AspNetCore
{
diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/TestHelper.cs b/tests/Tests.AzureAppConfiguration.AspNetCore/TestHelper.cs
index a67e8127..500f3aa9 100644
--- a/tests/Tests.AzureAppConfiguration.AspNetCore/TestHelper.cs
+++ b/tests/Tests.AzureAppConfiguration.AspNetCore/TestHelper.cs
@@ -1,4 +1,7 @@
-using Azure;
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure;
using Azure.Data.AppConfiguration;
using Moq;
using System;
diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
index 5d0a8d25..bdd1236b 100644
--- a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
+++ b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
@@ -1,4 +1,4 @@
-
+
net6.0;net8.0
@@ -24,4 +24,5 @@
+
diff --git a/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj b/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj
index 6ad45820..6f9f3b96 100644
--- a/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj
+++ b/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj
@@ -1,4 +1,4 @@
-
+
net6.0;net8.0
diff --git a/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockRequest.cs b/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockRequest.cs
index 443c8acb..d5ad0815 100644
--- a/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockRequest.cs
+++ b/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockRequest.cs
@@ -32,6 +32,7 @@ public override RequestContent Content
{
_headers.Remove("Content-Length");
}
+
base.Content = value;
}
}
diff --git a/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/TaskExtensions.cs b/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/TaskExtensions.cs
index 37d5d55f..ace8ebe2 100644
--- a/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/TaskExtensions.cs
+++ b/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/TaskExtensions.cs
@@ -27,7 +27,6 @@ public static Task TimeoutAfterDefault(this Task task,
return task.TimeoutAfter(DefaultTimeout, filePath, lineNumber);
}
-
public static async Task TimeoutAfter(this Task task, TimeSpan timeout,
[CallerFilePath] string filePath = null,
[CallerLineNumber] int lineNumber = default)
diff --git a/tests/Tests.AzureAppConfiguration/ConnectTests.cs b/tests/Tests.AzureAppConfiguration/ConnectTests.cs
index 22a1507a..6a8ddc8d 100644
--- a/tests/Tests.AzureAppConfiguration/ConnectTests.cs
+++ b/tests/Tests.AzureAppConfiguration/ConnectTests.cs
@@ -7,7 +7,6 @@
using Moq;
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
@@ -48,7 +47,6 @@ public void ConnectTests_ThrowsIfConnectNotInvoked()
Assert.Throws(action);
}
-
[Fact]
public void ConnectTests_UsesParametersFromLatestConnectCall()
{
diff --git a/tests/Tests.AzureAppConfiguration/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
index 4ddbd58f..86ea96b9 100644
--- a/tests/Tests.AzureAppConfiguration/FailoverTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
@@ -72,7 +72,7 @@ public async Task FailOverTests_ReturnsAllClientsIfAllBackedOff()
});
options.ReplicaDiscoveryEnabled = false;
-
+
refresher = options.GetRefresher();
});
@@ -210,7 +210,7 @@ public async Task FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
// Wait for client 1 backoff to end
Thread.Sleep(2500);
-
+
await refresher.RefreshAsync();
// The first client should have been called now with refresh after the backoff time ends
@@ -228,9 +228,9 @@ public void FailOverTests_AutoFailover()
mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Throws(new RequestFailedException(503, "Request failed."));
mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Throws(new RequestFailedException(503, "Request failed."));
+ .Throws(new RequestFailedException(403, "Forbidden."));
mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Throws(new RequestFailedException(503, "Request failed."));
+ .Throws(new RequestFailedException(401, "Unauthorized."));
mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true);
var mockClient2 = new Mock();
@@ -337,55 +337,5 @@ public void FailOverTests_GetNoDynamicClient()
// Only contains the client that passed while constructing the ConfigurationClientManager
Assert.Single(clients);
}
-
- [Fact]
- public void FailOverTests_FailOverOnKeyVaultReferenceException()
- {
- // Arrange
- IConfigurationRefresher refresher = null;
- var mockResponse = new Mock();
-
- var mockClient1 = new Mock();
- mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
- .Throws(new KeyVaultReferenceException("Key vault reference failed.", new RequestFailedException(503, "Request failed.")));
- mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Throws(new KeyVaultReferenceException("Key vault reference failed.", new RequestFailedException(503, "Request failed.")));
- mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Throws(new KeyVaultReferenceException("Key vault reference failed.", new RequestFailedException(503, "Request failed.")));
- mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true);
-
- var mockClient2 = new Mock();
- mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
- .Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
- mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
- mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
- mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
-
- ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object);
- ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object);
-
- var clientList = new List() { cw1, cw2 };
- var configClientManager = new ConfigurationClientManager(clientList);
-
- var config = new ConfigurationBuilder()
- .AddAzureAppConfiguration(options =>
- {
- options.ClientManager = configClientManager;
- options.Select("TestKey*");
- options.ConfigureRefresh(refreshOptions =>
- {
- refreshOptions.Register("TestKey1", "label")
- .SetRefreshInterval(TimeSpan.FromSeconds(1));
- });
-
- refresher = options.GetRefresher();
- })
- .Build();
-
- // The build should be successful since one client was backed off and it failed over to the second client.
- Assert.Equal("TestValue1", config["TestKey1"]);
- }
}
}
diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
index 8e871ef2..7e49e8ab 100644
--- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
@@ -320,7 +320,7 @@ public class FeatureManagementTests
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
};
- List _featureFlagCollection = new List
+ List _featureFlagCollection = new List
{
ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "App1_Feature1",
@@ -622,6 +622,114 @@ public class FeatureManagementTests
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
};
+ List _allocationIdFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "TelemetryVariant",
+ value: @"
+ {
+ ""id"": ""TelemetryVariant"",
+ ""enabled"": true,
+ ""variants"": [
+ {
+ ""name"": ""True_Override"",
+ ""configuration_value"": ""default"",
+ ""status_override"": ""Disabled""
+ }
+ ],
+ ""allocation"": {
+ ""default_when_enabled"": ""True_Override""
+ },
+ ""telemetry"": {
+ ""enabled"": ""true""
+ }
+ }
+ ",
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("cmwBRcIAq1jUyKL3Kj8bvf9jtxBrFg-R-ayExStMC90")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "TelemetryVariantPercentile",
+ value: @"
+ {
+ ""id"": ""TelemetryVariantPercentile"",
+ ""enabled"": true,
+ ""variants"": [
+ {
+ ""name"": ""True_Override"",
+ ""configuration_value"": {
+ ""someOtherKey"": {
+ ""someSubKey"": ""someSubValue""
+ },
+ ""someKey4"": [3, 1, 4, true],
+ ""someKey"": ""someValue"",
+ ""someKey3"": 3.14,
+ ""someKey2"": 3
+ }
+ }
+ ],
+ ""allocation"": {
+ ""default_when_enabled"": ""True_Override"",
+ ""percentile"": [
+ {
+ ""variant"": ""True_Override"",
+ ""from"": 0,
+ ""to"": 100
+ }
+ ]
+ },
+ ""telemetry"": {
+ ""enabled"": ""true""
+ }
+ }
+ ",
+ label: "label",
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("cmwBRcIAq1jUyKL3Kj8bvf9jtxBrFg-R-ayExStMC90")),
+
+ // Quote of the day test
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "Greeting",
+ value: @"
+ {
+ ""id"": ""Greeting"",
+ ""description"": """",
+ ""enabled"": true,
+ ""variants"": [
+ {
+ ""name"": ""On"",
+ ""configuration_value"": true
+ },
+ {
+ ""name"": ""Off"",
+ ""configuration_value"": false
+ }
+ ],
+ ""allocation"": {
+ ""percentile"": [
+ {
+ ""variant"": ""On"",
+ ""from"": 0,
+ ""to"": 50
+ },
+ {
+ ""variant"": ""Off"",
+ ""from"": 50,
+ ""to"": 100
+ }
+ ],
+ ""default_when_enabled"": ""Off"",
+ ""default_when_disabled"": ""Off""
+ },
+ ""telemetry"": {
+ ""enabled"": true
+ }
+ }
+ ",
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("8kS3pc_cQmWnfLY9LQ1cd-RfR6_nQqH6sgdlL9eCgek")),
+ };
+
TimeSpan RefreshInterval = TimeSpan.FromSeconds(1);
[Fact]
@@ -1624,6 +1732,7 @@ public async Task ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefre
{
informationalInvocation += s;
}
+
if (args.Level == EventLevel.Verbose)
{
verboseInvocation += s;
@@ -1806,6 +1915,7 @@ public async Task MapTransformFeatureFlagWithRefresh()
}
";
}
+
return new ValueTask(setting);
});
refresher = options.GetRefresher();
@@ -1970,6 +2080,64 @@ public void WithTelemetry()
Assert.Equal("Tag2Value", config["feature_management:feature_flags:1:telemetry:metadata:Tags.Tag1"]);
}
+ [Fact]
+ public void WithAllocationId()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(new MockAsyncPageable(_allocationIdFeatureFlagCollection));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Connect(TestHelpers.PrimaryConfigStoreEndpoint, new DefaultAzureCredential());
+ options.UseFeatureFlags();
+ })
+ .Build();
+
+ byte[] featureFlagIdHash;
+
+ using (HashAlgorithm hashAlgorithm = SHA256.Create())
+ {
+ featureFlagIdHash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes($"{FeatureManagementConstants.FeatureFlagMarker}TelemetryVariant\n"));
+ }
+
+ string featureFlagId = Convert.ToBase64String(featureFlagIdHash)
+ .TrimEnd('=')
+ .Replace('+', '-')
+ .Replace('/', '_');
+
+ // Validate TelemetryVariant
+ Assert.Equal("True", config["feature_management:feature_flags:0:telemetry:enabled"]);
+ Assert.Equal("TelemetryVariant", config["feature_management:feature_flags:0:id"]);
+
+ Assert.Equal(featureFlagId, config["feature_management:feature_flags:0:telemetry:metadata:FeatureFlagId"]);
+
+ Assert.Equal($"{TestHelpers.PrimaryConfigStoreEndpoint}kv/{FeatureManagementConstants.FeatureFlagMarker}TelemetryVariant", config["feature_management:feature_flags:0:telemetry:metadata:FeatureFlagReference"]);
+
+ Assert.Equal("MExY1waco2tqen4EcJKK", config["feature_management:feature_flags:0:telemetry:metadata:AllocationId"]);
+
+ // Validate TelemetryVariantPercentile
+ Assert.Equal("True", config["feature_management:feature_flags:1:telemetry:enabled"]);
+ Assert.Equal("TelemetryVariantPercentile", config["feature_management:feature_flags:1:id"]);
+
+ Assert.Equal($"{TestHelpers.PrimaryConfigStoreEndpoint}kv/{FeatureManagementConstants.FeatureFlagMarker}TelemetryVariantPercentile?label=label", config["feature_management:feature_flags:1:telemetry:metadata:FeatureFlagReference"]);
+
+ Assert.Equal("YsdJ4pQpmhYa8KEhRLUn", config["feature_management:feature_flags:1:telemetry:metadata:AllocationId"]);
+
+ // Validate Greeting
+ Assert.Equal("True", config["feature_management:feature_flags:2:telemetry:enabled"]);
+ Assert.Equal("Greeting", config["feature_management:feature_flags:2:id"]);
+
+ Assert.Equal("63pHsrNKDSi5Zfe_FvZPSegwbsEo5TS96hf4k7cc4Zw", config["feature_management:feature_flags:2:telemetry:metadata:FeatureFlagId"]);
+
+ Assert.Equal($"{TestHelpers.PrimaryConfigStoreEndpoint}kv/{FeatureManagementConstants.FeatureFlagMarker}Greeting", config["feature_management:feature_flags:2:telemetry:metadata:FeatureFlagReference"]);
+
+ Assert.Equal("L0m7_ulkdsaQmz6dSw4r", config["feature_management:feature_flags:2:telemetry:metadata:AllocationId"]);
+ }
[Fact]
public void WithRequirementType()
diff --git a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
index 6108519c..06c88040 100644
--- a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
+++ b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
@@ -186,7 +186,7 @@ public void NotSecretIdentifierURI()
{
configuration = builder.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
}).Build();
});
@@ -212,7 +212,7 @@ public void UseSecret()
var configuration = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
})
.Build();
@@ -237,7 +237,7 @@ public void UseCertificate()
var configuration = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
})
.Build();
@@ -262,7 +262,7 @@ public void ThrowsWhenSecretNotFound()
{
new ConfigurationBuilder().AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
}).Build();
});
@@ -287,7 +287,7 @@ public void DisabledSecretIdentifier()
{
new ConfigurationBuilder().AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
}).Build();
});
@@ -309,7 +309,7 @@ public void WrongContentType()
var configuration = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
})
.Build();
@@ -364,14 +364,13 @@ public void CancellationToken()
{
startupOptions.Timeout = TimeSpan.FromSeconds(5);
});
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
})
.Build();
});
}
-
[Fact]
public void HasNoAccessToKeyVault()
{
@@ -389,7 +388,7 @@ public void HasNoAccessToKeyVault()
{
new ConfigurationBuilder().AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
})
.Build();
@@ -418,7 +417,7 @@ public void RegisterMultipleClients()
var configuration = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient1.Object)
.Register(mockSecretClient2.Object));
})
@@ -441,7 +440,7 @@ public void ServerRequestIsMadeWhenDefaultCredentialIsSet()
{
new ConfigurationBuilder().AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.SetCredential(new DefaultAzureCredential()));
})
.Build();
@@ -469,7 +468,7 @@ public void ThrowsWhenNoMatchingSecretClientIsFound()
new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient1.Object).Register(mockSecretClient2.Object));
})
.Build();
@@ -492,7 +491,7 @@ public void ThrowsWhenConfigureKeyVaultIsMissing()
new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
})
.Build();
});
@@ -516,7 +515,7 @@ public void DoesNotThrowKeyVaultExceptionWhenProviderIsOptional()
new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.Adapters = new List { mockKeyValueAdapter.Object };
}, optional: true)
.Build();
@@ -533,7 +532,7 @@ public void CallsSecretResolverCallbackWhenNoMatchingSecretClientIsFound()
IConfiguration config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.SetSecretResolver((secretUri) =>
@@ -559,7 +558,7 @@ public void ThrowsWhenBothDefaultCredentialAndSecretResolverCallbackAreSet()
{
new ConfigurationBuilder().AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.SetSecretResolver((secretUri) =>
@@ -589,7 +588,7 @@ public void ThrowsWhenSecretResolverIsNull()
{
new ConfigurationBuilder().AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.SetSecretResolver(null);
@@ -610,7 +609,7 @@ public void LastKeyVaultOptionsWinWithMultipleConfigureKeyVaultCalls()
IConfiguration config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.SetCredential(new DefaultAzureCredential());
@@ -645,7 +644,7 @@ public void DontUseSecretResolverCallbackWhenMatchingSecretClientIsPresent()
var configuration = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.SetSecretResolver((secretUri) =>
@@ -673,7 +672,7 @@ public void ThrowsWhenSecretRefreshIntervalIsTooShort()
{
new ConfigurationBuilder().AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.SetSecretRefreshInterval(_kv.Key, TimeSpan.FromMilliseconds(10));
@@ -722,7 +721,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
@@ -760,7 +759,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
public async Task CachedSecretIsInvalidatedWhenRefreshAllIsTrue()
{
IConfigurationRefresher refresher = null;
- TimeSpan refreshInterval = TimeSpan.FromSeconds(1);
+ TimeSpan refreshInterval = TimeSpan.FromSeconds(60);
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -795,7 +794,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.Register(mockSecretClient.Object);
@@ -832,7 +831,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
public async Task SecretIsReloadedFromKeyVaultWhenCacheExpires()
{
IConfigurationRefresher refresher = null;
- TimeSpan refreshInterval = TimeSpan.FromSeconds(1);
+ TimeSpan refreshInterval = TimeSpan.FromSeconds(60);
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -848,7 +847,7 @@ public async Task SecretIsReloadedFromKeyVaultWhenCacheExpires()
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.Register(mockSecretClient.Object);
@@ -875,7 +874,7 @@ public async Task SecretIsReloadedFromKeyVaultWhenCacheExpires()
public async Task SecretsWithDefaultRefreshInterval()
{
IConfigurationRefresher refresher = null;
- TimeSpan shortRefreshInterval = TimeSpan.FromSeconds(1);
+ TimeSpan shortRefreshInterval = TimeSpan.FromSeconds(60);
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -891,7 +890,7 @@ public async Task SecretsWithDefaultRefreshInterval()
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.Register(mockSecretClient.Object);
@@ -920,7 +919,7 @@ public async Task SecretsWithDefaultRefreshInterval()
public async Task SecretsWithDifferentRefreshIntervals()
{
IConfigurationRefresher refresher = null;
- TimeSpan shortRefreshInterval = TimeSpan.FromSeconds(1);
+ TimeSpan shortRefreshInterval = TimeSpan.FromSeconds(60);
TimeSpan longRefreshInterval = TimeSpan.FromDays(1);
var mockResponse = new Mock();
@@ -937,7 +936,7 @@ public async Task SecretsWithDifferentRefreshIntervals()
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.ConfigureKeyVault(kv =>
{
kv.Register(mockSecretClient.Object);
@@ -963,7 +962,6 @@ public async Task SecretsWithDifferentRefreshIntervals()
mockSecretClient.Verify(client => client.GetSecretAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3));
}
-
[Fact]
public void ThrowsWhenInvalidKeyVaultSecretReferenceJson()
{
diff --git a/tests/Tests.AzureAppConfiguration/LoggingTests.cs b/tests/Tests.AzureAppConfiguration/LoggingTests.cs
index 2c614acf..547c65bd 100644
--- a/tests/Tests.AzureAppConfiguration/LoggingTests.cs
+++ b/tests/Tests.AzureAppConfiguration/LoggingTests.cs
@@ -9,7 +9,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
-using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Moq;
using System;
using System.Collections.Generic;
@@ -132,7 +131,7 @@ public async Task ValidateUnauthorizedExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
-
+
Thread.Sleep(RefreshInterval);
await refresher.TryRefreshAsync();
@@ -505,6 +504,7 @@ public async Task ValidateCorrectKeyValueLoggedDuringRefresh()
{
informationalInvocation += s;
}
+
if (args.Level == EventLevel.Verbose)
{
verboseInvocation += s;
@@ -558,6 +558,7 @@ public async Task ValidateCorrectKeyVaultSecretLoggedDuringRefresh()
{
informationalInvocation += s;
}
+
if (args.Level == EventLevel.Verbose)
{
verboseInvocation += s;
@@ -581,7 +582,7 @@ public async Task ValidateCorrectKeyVaultSecretLoggedDuringRefresh()
refreshOptions.Register("TestKey1", "label", true)
.SetRefreshInterval(RefreshInterval);
});
- options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object).SetSecretRefreshInterval(RefreshInterval));
+ options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object).SetSecretRefreshInterval(TimeSpan.FromSeconds(60)));
refresher = options.GetRefresher();
})
.Build();
diff --git a/tests/Tests.AzureAppConfiguration/MapTests.cs b/tests/Tests.AzureAppConfiguration/MapTests.cs
index 95e13525..623ae477 100644
--- a/tests/Tests.AzureAppConfiguration/MapTests.cs
+++ b/tests/Tests.AzureAppConfiguration/MapTests.cs
@@ -1,19 +1,22 @@
-using Microsoft.Extensions.Configuration.AzureAppConfiguration;
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure;
+using Azure.Core.Diagnostics;
+using Azure.Core.Testing;
+using Azure.Data.AppConfiguration;
+using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
+using Moq;
using System;
using System.Collections.Generic;
-using Xunit;
-using Azure.Data.AppConfiguration;
-using Azure;
-using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
+using System.Diagnostics.Tracing;
using System.Linq;
-using Azure.Core.Testing;
-using Moq;
using System.Threading;
using System.Threading.Tasks;
-using Azure.Security.KeyVault.Secrets;
-using Azure.Core.Diagnostics;
-using System.Diagnostics.Tracing;
+using Xunit;
namespace Tests.AzureAppConfiguration
{
@@ -71,6 +74,7 @@ public void MapTransformKeyValue()
{
setting.Value += " mapped";
}
+
return new ValueTask(setting);
}).Map((setting) =>
{
@@ -82,6 +86,7 @@ public void MapTransformKeyValue()
{
setting.Value += " second";
}
+
return new ValueTask(setting);
});
})
@@ -113,16 +118,17 @@ public void MapTransformKeyVaultValueBeforeAdapters()
options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object));
options.Map((setting) =>
{
- if (setting.ContentType != KeyVaultConstants.ContentType + "; charset=utf-8")
- {
- setting.Value = @"
+ if (setting.ContentType != KeyVaultConstants.ContentType + "; charset=utf-8")
+ {
+ setting.Value = @"
{
""uri"":""https://keyvault-theclassics.vault.azure.net/certificates/TestCertificate""
}";
- setting.ContentType = KeyVaultConstants.ContentType + "; charset=utf-8";
- }
- return new ValueTask(setting);
- });
+ setting.ContentType = KeyVaultConstants.ContentType + "; charset=utf-8";
+ }
+
+ return new ValueTask(setting);
+ });
})
.Build();
@@ -152,6 +158,7 @@ public async Task MapTransformWithRefresh()
{
setting.Value += " mapped";
}
+
return new ValueTask(setting);
}).Map((setting) =>
{
@@ -163,6 +170,7 @@ public async Task MapTransformWithRefresh()
{
setting.Value += " second";
}
+
return new ValueTask(setting);
});
@@ -205,6 +213,7 @@ public async Task MapTransformSettingKeyWithRefresh()
{
setting.Key = "newTestKey1";
}
+
return new ValueTask(setting);
}).Map((setting) =>
{
@@ -212,6 +221,7 @@ public async Task MapTransformSettingKeyWithRefresh()
{
setting.Value += " changed";
}
+
return new ValueTask(setting);
});
refresher = options.GetRefresher();
@@ -255,6 +265,7 @@ public async Task MapTransformSettingLabelWithRefresh()
{
setting.Label = "newLabel";
}
+
return new ValueTask(setting);
}).Map((setting) =>
{
@@ -262,6 +273,7 @@ public async Task MapTransformSettingLabelWithRefresh()
{
setting.Value += " changed";
}
+
return new ValueTask(setting);
});
refresher = options.GetRefresher();
@@ -303,6 +315,7 @@ public async Task MapTransformSettingCreateDuplicateKeyWithRefresh()
{
setting.Key = "TestKey2";
}
+
return new ValueTask(setting);
}).Map((setting) =>
{
@@ -310,6 +323,7 @@ public async Task MapTransformSettingCreateDuplicateKeyWithRefresh()
{
setting.Value += " changed";
}
+
return new ValueTask(setting);
});
refresher = options.GetRefresher();
@@ -356,6 +370,7 @@ public async Task MapCreateNewSettingWithRefresh()
eTag: new ETag("changed"),
contentType: "text");
}
+
return new ValueTask(setting);
});
refresher = options.GetRefresher();
@@ -395,13 +410,14 @@ public void MapResolveKeyVaultReferenceThrowsExceptionInAdapter()
.AddAzureAppConfiguration(options =>
{
options.ClientManager = mockClientManager;
- options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object).SetSecretRefreshInterval(TimeSpan.FromSeconds(1)));
+ options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object).SetSecretRefreshInterval(TimeSpan.FromSeconds(60)));
options.Map((setting) =>
{
if (setting.ContentType == KeyVaultConstants.ContentType + "; charset=utf-8")
{
setting.Value = _secretValue;
}
+
return new ValueTask(setting);
});
refresher = options.GetRefresher();
@@ -426,12 +442,11 @@ public void MapAsyncResolveKeyVaultReference()
.Returns((string name, string version, CancellationToken cancellationToken) =>
Task.FromResult((Response)new MockResponse(new KeyVaultSecret(name, _secretValue))));
-
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
options.ClientManager = mockClientManager;
- options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object).SetSecretRefreshInterval(TimeSpan.FromSeconds(1)));
+ options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object).SetSecretRefreshInterval(TimeSpan.FromSeconds(60)));
options.Map(async (setting) =>
{
if (setting.ContentType == KeyVaultConstants.ContentType + "; charset=utf-8")
@@ -440,6 +455,7 @@ public void MapAsyncResolveKeyVaultReference()
setting.Value = secret.Value;
setting.ContentType = "text";
}
+
return setting;
});
refresher = options.GetRefresher();
@@ -466,6 +482,7 @@ public async Task MapTransformSettingKeyWithLogAndRefresh()
{
informationalInvocation += s;
}
+
if (args.Level == EventLevel.Verbose)
{
verboseInvocation += s;
@@ -487,6 +504,7 @@ public async Task MapTransformSettingKeyWithLogAndRefresh()
{
setting.Key = "newTestKey1";
}
+
return new ValueTask(setting);
}).Map((setting) =>
{
@@ -494,6 +512,7 @@ public async Task MapTransformSettingKeyWithLogAndRefresh()
{
setting.Value += " changed";
}
+
return new ValueTask(setting);
});
refresher = options.GetRefresher();
diff --git a/tests/Tests.AzureAppConfiguration/MockedConfigurationClientManager.cs b/tests/Tests.AzureAppConfiguration/MockedConfigurationClientManager.cs
index 7e6c03e1..0216d1be 100644
--- a/tests/Tests.AzureAppConfiguration/MockedConfigurationClientManager.cs
+++ b/tests/Tests.AzureAppConfiguration/MockedConfigurationClientManager.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using System;
@@ -63,7 +62,8 @@ public IEnumerable GetClients()
{
var result = new List();
- foreach (var client in _clients) {
+ foreach (var client in _clients)
+ {
result.Add(client.Client);
}
diff --git a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
index 1b3291b2..c4c7c38c 100644
--- a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
@@ -19,58 +19,58 @@
namespace Tests.AzureAppConfiguration
{
public class PushRefreshTests
- {
+ {
static readonly Uri PrimaryResourceUri = new Uri(TestHelpers.PrimaryConfigStoreEndpoint.ToString() + "/kv/searchQuery1");
static readonly Uri SecondaryResourceUri = new Uri(TestHelpers.SecondaryConfigStoreEndpoint.ToString() + "/kv/searchQuery2");
List _kvCollection = new List
- {
- ConfigurationModelFactory.ConfigurationSetting(
- key: "TestKey1",
- label: "label",
- value: "TestValue1",
- eTag: new ETag("0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63"),
- contentType: "text"),
-
- ConfigurationModelFactory.ConfigurationSetting(
- key: "TestKey2",
- label: "label",
- value: "TestValue2",
- eTag: new ETag("31c38369-831f-4bf1-b9ad-79db56c8b989"),
- contentType: "text"),
-
- ConfigurationModelFactory.ConfigurationSetting(
- key: "TestKey3",
- label: "label",
- value: "TestValue3",
- eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"),
- contentType: "text"),
-
- ConfigurationModelFactory.ConfigurationSetting(
- key: "TestKeyWithMultipleLabels",
- label: "label1",
- value: "TestValueForLabel1",
- eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"),
- contentType: "text"),
-
- ConfigurationModelFactory.ConfigurationSetting(
- key: "TestKeyWithMultipleLabels",
- label: "label2",
- value: "TestValueForLabel2",
- eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"),
- contentType: "text")
- };
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: "TestKey1",
+ label: "label",
+ value: "TestValue1",
+ eTag: new ETag("0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63"),
+ contentType: "text"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: "TestKey2",
+ label: "label",
+ value: "TestValue2",
+ eTag: new ETag("31c38369-831f-4bf1-b9ad-79db56c8b989"),
+ contentType: "text"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: "TestKey3",
+ label: "label",
+ value: "TestValue3",
+ eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"),
+ contentType: "text"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: "TestKeyWithMultipleLabels",
+ label: "label1",
+ value: "TestValueForLabel1",
+ eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"),
+ contentType: "text"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: "TestKeyWithMultipleLabels",
+ label: "label2",
+ value: "TestValueForLabel2",
+ eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"),
+ contentType: "text")
+ };
List _pushNotificationList = new List
{
new PushNotification {
ResourceUri = PrimaryResourceUri,
- EventType = "eventType.KeyValueModified",
+ EventType = "eventType.KeyValueModified",
SyncToken = "SyncToken1;sn=001",
},
new PushNotification {
ResourceUri = PrimaryResourceUri,
- EventType = "eventType.KeyValueModified",
+ EventType = "eventType.KeyValueModified",
SyncToken = "SyncToken2",
},
new PushNotification {
@@ -114,12 +114,12 @@ public class PushRefreshTests
},
new PushNotification {
ResourceUri = SecondaryResourceUri,
- EventType = null,
+ EventType = null,
SyncToken = "SyncToken2"
},
new PushNotification {
ResourceUri = PrimaryResourceUri,
- EventType = "eventType.KeyValueDeleted",
+ EventType = "eventType.KeyValueDeleted",
SyncToken = null
},
new PushNotification {
@@ -134,13 +134,13 @@ public class PushRefreshTests
}
};
- Dictionary _eventGridEvents = new Dictionary
- {
+ Dictionary _eventGridEvents = new Dictionary
+ {
{
"sn;Vxujfidne",
new EventGridEvent(
"https://store1.resource.io/kv/searchQuery1",
- "Microsoft.AppConfiguration.KeyValueModified", "2",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;Vxujfidne\"}")
)
},
@@ -158,7 +158,7 @@ public class PushRefreshTests
"sn;Ttylmable",
new EventGridEvent(
"https://store1.resource.io/kv/searchQuery2",
- "Microsoft.AppConfiguration.KeyValueDeleted", "2",
+ "Microsoft.AppConfiguration.KeyValueDeleted", "2",
BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;Ttylmable\"}")
)
},
@@ -167,7 +167,7 @@ public class PushRefreshTests
"sn;CRAle3342",
new EventGridEvent(
"https://store2.resource.io/kv/searchQuery2",
- "Microsoft.AppConfiguration.KeyValueModified", "2",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;CRAle3342\"}")
)
},
@@ -185,25 +185,25 @@ public class PushRefreshTests
Dictionary _invalidFormatEventGridEvents = new Dictionary
{
- {
- "sn;Vxujfidne",
- new EventGridEvent(
- "https://store1.resource.io/kv/searchQuery1",
- "Microsoft.AppConfiguration.KeyValueModified", "2",
- BinaryData.FromString("\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;Vxujfidne\"}")
- )
- },
-
- {
- "sn;AxRty78B",
- new EventGridEvent(
- "https://store1.resource.io/kv/searchQuery1",
- "Microsoft.AppConfiguration.KeyValueModified", "2",
- BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;Vxujfidne\"")
- )
- },
-
- {
+ {
+ "sn;Vxujfidne",
+ new EventGridEvent(
+ "https://store1.resource.io/kv/searchQuery1",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
+ BinaryData.FromString("\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;Vxujfidne\"}")
+ )
+ },
+
+ {
+ "sn;AxRty78B",
+ new EventGridEvent(
+ "https://store1.resource.io/kv/searchQuery1",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
+ BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;Vxujfidne\"")
+ )
+ },
+
+ {
"sn;Ttylmable",
new EventGridEvent(
"https://store1.resource.io/kv/searchQuery2",
@@ -227,18 +227,18 @@ public class PushRefreshTests
[Fact]
public void ValidatePushNotificationCreation()
{
- foreach (KeyValuePair eventGridAndSync in _eventGridEvents)
+ foreach (KeyValuePair eventGridAndSync in _eventGridEvents)
{
- string syncToken = eventGridAndSync.Key;
- EventGridEvent eventGridEvent = eventGridAndSync.Value;
+ string syncToken = eventGridAndSync.Key;
+ EventGridEvent eventGridEvent = eventGridAndSync.Value;
- Assert.True(eventGridEvent.TryCreatePushNotification(out PushNotification pushNotification));
+ Assert.True(eventGridEvent.TryCreatePushNotification(out PushNotification pushNotification));
Assert.NotNull(pushNotification);
Assert.Equal(eventGridEvent.EventType, pushNotification.EventType);
Assert.Equal(eventGridEvent.Subject, pushNotification.ResourceUri.OriginalString);
Assert.Equal(syncToken, pushNotification.SyncToken);
}
- }
+ }
[Fact]
public void InvalidPushNotificationCreation()
@@ -252,145 +252,143 @@ public void InvalidPushNotificationCreation()
}
[Fact]
- public void ProcessPushNotificationThrowsArgumentExceptions()
+ public void ProcessPushNotificationThrowsArgumentExceptions()
{
- var mockResponse = new Mock();
- var mockClient = GetMockConfigurationClient();
-
- IConfigurationRefresher refresher = null;
-
- var config = new ConfigurationBuilder()
- .AddAzureAppConfiguration(options =>
- {
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
- options.Select("*");
- options.ConfigureRefresh(refreshOptions =>
- {
- refreshOptions.Register("TestKey1", "label")
- .SetRefreshInterval(TimeSpan.FromDays(30));
- });
- refresher = options.GetRefresher();
- })
- .Build();
-
- foreach (PushNotification invalidPushNotification in _invalidPushNotificationList)
+ var mockResponse = new Mock();
+ var mockClient = GetMockConfigurationClient();
+
+ IConfigurationRefresher refresher = null;
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select("*");
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.Register("TestKey1", "label")
+ .SetRefreshInterval(TimeSpan.FromDays(30));
+ });
+ refresher = options.GetRefresher();
+ })
+ .Build();
+
+ foreach (PushNotification invalidPushNotification in _invalidPushNotificationList)
{
- Action action = () => refresher.ProcessPushNotification(invalidPushNotification);
- Assert.Throws(action);
- }
-
- PushNotification nullPushNotification = null;
-
- Action nullAction = () => refresher.ProcessPushNotification(nullPushNotification);
- Assert.Throws(nullAction);
- }
-
- [Fact]
- public async Task SyncTokenUpdatesCorrectNumberOfTimes()
- {
- // Arrange
- var mockResponse = new Mock();
- var mockClient = GetMockConfigurationClient();
-
- IConfigurationRefresher refresher = null;
- var clientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
-
- var config = new ConfigurationBuilder()
- .AddAzureAppConfiguration(options =>
- {
- options.ClientManager = clientManager;
- options.Select("*");
- options.ConfigureRefresh(refreshOptions =>
- {
- refreshOptions.Register("TestKey1", "label")
- .SetRefreshInterval(TimeSpan.FromDays(30));
- });
- refresher = options.GetRefresher();
- })
- .Build();
-
- foreach (PushNotification pushNotification in _pushNotificationList)
- {
- refresher.ProcessPushNotification(pushNotification, TimeSpan.FromSeconds(0));
- await refresher.RefreshAsync();
- }
-
- var validNotificationKVWatcherCount = 8;
- var validEndpointCount = 4;
-
- mockClient.Verify(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(validNotificationKVWatcherCount));
- Assert.Equal(_pushNotificationList.Count, clientManager.UpdateSyncTokenCalled);
- mockClient.Verify(c => c.UpdateSyncToken(It.IsAny()), Times.Exactly(validEndpointCount));
- }
-
- [Fact]
- public async Task RefreshAsyncUpdatesConfig()
- {
- // Arrange
- var mockResponse = new Mock();
- var mockClient = GetMockConfigurationClient();
-
- IConfigurationRefresher refresher = null;
-
- var config = new ConfigurationBuilder()
- .AddAzureAppConfiguration(options =>
- {
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);;
- options.Select("*");
- options.ConfigureRefresh(refreshOptions =>
- {
- refreshOptions.Register("TestKey1", "label")
- .SetRefreshInterval(TimeSpan.FromDays(30));
- });
- refresher = options.GetRefresher();
- })
- .Build();
-
-
- Assert.Equal("TestValue1", config["TestKey1"]);
- FirstKeyValue.Value = "newValue1";
-
- refresher.ProcessPushNotification(_pushNotificationList.First(), TimeSpan.FromSeconds(0));
- await refresher.RefreshAsync();
-
- Assert.Equal("newValue1", config["TestKey1"]);
- }
-
- private Mock GetMockConfigurationClient()
- {
- var mockResponse = new Mock();
- var mockClient = new Mock(MockBehavior.Strict);
-
- Response GetTestKey(string key, string label, CancellationToken cancellationToken)
- {
- return Response.FromValue(TestHelpers.CloneSetting(_kvCollection.FirstOrDefault(s => s.Key == key && s.Label == label)), mockResponse.Object);
- }
-
- Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken)
- {
- var newSetting = _kvCollection.FirstOrDefault(s => (s.Key == setting.Key && s.Label == setting.Label));
- var unchanged = (newSetting.Key == setting.Key && newSetting.Label == setting.Label && newSetting.Value == setting.Value);
- var response = new MockResponse(unchanged ? 304 : 200);
- return Response.FromValue(newSetting, response);
- }
-
- // We don't actually select KV based on SettingSelector, we just return a deep copy of _kvCollection
- mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
- .Returns(() =>
- {
- return new MockAsyncPageable(_kvCollection.Select(setting => TestHelpers.CloneSetting(setting)).ToList());
- });
-
- mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .ReturnsAsync((Func>)GetTestKey);
-
- mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .ReturnsAsync((Func>)GetIfChanged);
-
- mockClient.Setup(c => c.UpdateSyncToken(It.IsAny()));
-
- return mockClient;
- }
- }
+ Action action = () => refresher.ProcessPushNotification(invalidPushNotification);
+ Assert.Throws(action);
+ }
+
+ PushNotification nullPushNotification = null;
+
+ Action nullAction = () => refresher.ProcessPushNotification(nullPushNotification);
+ Assert.Throws(nullAction);
+ }
+ [Fact]
+ public async Task SyncTokenUpdatesCorrectNumberOfTimes()
+ {
+ // Arrange
+ var mockResponse = new Mock();
+ var mockClient = GetMockConfigurationClient();
+
+ IConfigurationRefresher refresher = null;
+ var clientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = clientManager;
+ options.Select("*");
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.Register("TestKey1", "label")
+ .SetRefreshInterval(TimeSpan.FromDays(30));
+ });
+ refresher = options.GetRefresher();
+ })
+ .Build();
+
+ foreach (PushNotification pushNotification in _pushNotificationList)
+ {
+ refresher.ProcessPushNotification(pushNotification, TimeSpan.FromSeconds(0));
+ await refresher.RefreshAsync();
+ }
+
+ var validNotificationKVWatcherCount = 8;
+ var validEndpointCount = 4;
+
+ mockClient.Verify(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(validNotificationKVWatcherCount));
+ Assert.Equal(_pushNotificationList.Count, clientManager.UpdateSyncTokenCalled);
+ mockClient.Verify(c => c.UpdateSyncToken(It.IsAny()), Times.Exactly(validEndpointCount));
+ }
+
+ [Fact]
+ public async Task RefreshAsyncUpdatesConfig()
+ {
+ // Arrange
+ var mockResponse = new Mock();
+ var mockClient = GetMockConfigurationClient();
+
+ IConfigurationRefresher refresher = null;
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); ;
+ options.Select("*");
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.Register("TestKey1", "label")
+ .SetRefreshInterval(TimeSpan.FromDays(30));
+ });
+ refresher = options.GetRefresher();
+ })
+ .Build();
+
+ Assert.Equal("TestValue1", config["TestKey1"]);
+ FirstKeyValue.Value = "newValue1";
+
+ refresher.ProcessPushNotification(_pushNotificationList.First(), TimeSpan.FromSeconds(0));
+ await refresher.RefreshAsync();
+
+ Assert.Equal("newValue1", config["TestKey1"]);
+ }
+
+ private Mock GetMockConfigurationClient()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ Response GetTestKey(string key, string label, CancellationToken cancellationToken)
+ {
+ return Response.FromValue(TestHelpers.CloneSetting(_kvCollection.FirstOrDefault(s => s.Key == key && s.Label == label)), mockResponse.Object);
+ }
+
+ Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken)
+ {
+ var newSetting = _kvCollection.FirstOrDefault(s => (s.Key == setting.Key && s.Label == setting.Label));
+ var unchanged = (newSetting.Key == setting.Key && newSetting.Label == setting.Label && newSetting.Value == setting.Value);
+ var response = new MockResponse(unchanged ? 304 : 200);
+ return Response.FromValue(newSetting, response);
+ }
+
+ // We don't actually select KV based on SettingSelector, we just return a deep copy of _kvCollection
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(() =>
+ {
+ return new MockAsyncPageable(_kvCollection.Select(setting => TestHelpers.CloneSetting(setting)).ToList());
+ });
+
+ mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync((Func>)GetTestKey);
+
+ mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync((Func>)GetIfChanged);
+
+ mockClient.Setup(c => c.UpdateSyncToken(It.IsAny()));
+
+ return mockClient;
+ }
+ }
}
diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs
index 1faae290..6edc1a9a 100644
--- a/tests/Tests.AzureAppConfiguration/RefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/RefreshTests.cs
@@ -83,10 +83,10 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Load all settings except the one registered for refresh - this test is to ensure that it will be loaded later
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(keyValueCollection.Where(s => s.Key != "TestKey1" && s.Label != "label").ToList()));
-
+
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((Func>)GetTestKey);
-
+
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((Func>)GetIfChanged);
@@ -101,7 +101,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
});
})
.Build();
-
+
Assert.Equal("TestValue1", config["TestKey1"]);
}
@@ -1049,9 +1049,9 @@ public void RefreshTests_RefreshIsCancelled()
Thread.Sleep(1500);
using var cancellationSource = new CancellationTokenSource();
- cancellationSource.Cancel();
+ cancellationSource.Cancel();
Action action = () => refresher.RefreshAsync(cancellationSource.Token).Wait();
- var exception = Assert.Throws(action);
+ var exception = Assert.Throws(action);
Assert.IsType(exception.InnerException);
Assert.Equal("TestValue1", config["TestKey1"]);
}
@@ -1133,7 +1133,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((Func>)GetTestKey);
-
+
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((Func>)GetIfChanged);
@@ -1169,10 +1169,10 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns((Func)GetTestKeys);
-
+
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((Func>)GetTestKey);
-
+
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny