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(), It.IsAny(), It.IsAny())) .ReturnsAsync((Func>)GetIfChanged); diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/TestHelper.cs index 477b4429..bc7989b2 100644 --- a/tests/Tests.AzureAppConfiguration/TestHelper.cs +++ b/tests/Tests.AzureAppConfiguration/TestHelper.cs @@ -84,8 +84,10 @@ static public void SerializeSetting(ref Utf8JsonWriter json, ConfigurationSettin { json.WriteString(tag.Key, tag.Value); } + json.WriteEndObject(); } + if (setting.ETag != default) json.WriteString("etag", setting.ETag.ToString()); if (setting.LastModified.HasValue) @@ -103,6 +105,7 @@ static public void SerializeBatch(ref Utf8JsonWriter json, ConfigurationSetting[ { SerializeSetting(ref json, item); } + json.WriteEndArray(); json.WriteEndObject(); } diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index e628ecca..49c58ec6 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -1,4 +1,4 @@ - + net48;net6.0;net8.0 @@ -39,4 +39,5 @@ Always + diff --git a/tests/Tests.AzureAppConfiguration/Tests.cs b/tests/Tests.AzureAppConfiguration/Tests.cs index c3b27124..30010f7d 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.cs +++ b/tests/Tests.AzureAppConfiguration/Tests.cs @@ -240,7 +240,7 @@ public void TestUserAgentHeader() // 5. Contains the runtime information (target framework, OS description etc.) in the format set by the SDK // 6. Does not contain any additional components string userAgentRegex = @"^Microsoft\.Extensions\.Configuration\.AzureAppConfiguration/\d+\.\d+\.\d+(\+[a-z0-9]+)?(-preview(\.\d+)?)?,azsdk-net-Data.AppConfiguration/[.+\w-]+ \([.;\w\s]+\)$"; - + var response = new MockResponse(200); response.SetContent(SerializationHelpers.Serialize(_kvCollectionPageOne.ToArray(), TestHelpers.SerializeBatch));