diff --git a/build/install-dotnet.ps1 b/build/install-dotnet.ps1
index 11552a78..194dbf5a 100644
--- a/build/install-dotnet.ps1
+++ b/build/install-dotnet.ps1
@@ -1,8 +1,10 @@
-# Installs .NET 6 and .NET 7 for CI/CD environment
+# Installs .NET 6, .NET 7, and .NET 8 for CI/CD environment
# see: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script#examples
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 6.0
-&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 7.0
\ No newline at end of file
+&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 7.0
+
+&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 8.0
diff --git a/examples/ConfigStoreDemo/ConfigStoreDemo.csproj b/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
index 3b11aa18..caab5885 100644
--- a/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
+++ b/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
@@ -1,7 +1,7 @@
false
- net7.0
+ net8.0
diff --git a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
index dcff4691..ff4b0398 100644
--- a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
+++ b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
@@ -2,11 +2,11 @@
Exe
- net7.0
+ net8.0
-
+
diff --git a/examples/ConsoleApplication/ConsoleApplication.csproj b/examples/ConsoleApplication/ConsoleApplication.csproj
index 2bc2e9ec..be38de42 100644
--- a/examples/ConsoleApplication/ConsoleApplication.csproj
+++ b/examples/ConsoleApplication/ConsoleApplication.csproj
@@ -3,7 +3,7 @@
false
Exe
- net7.0
+ net8.0
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 5107c108..c081cacd 100644
--- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
@@ -2,7 +2,7 @@
- net6.0;net7.0
+ net6.0;net7.0;net8.0
Microsoft.Azure.AppConfiguration.AspNetCore allows developers to use Microsoft Azure App Configuration service as a configuration source in their applications. This package adds additional features for ASP.NET Core applications to the existing package Microsoft.Extensions.Configuration.AzureAppConfiguration.
true
false
@@ -21,7 +21,7 @@
- 7.1.0
+ 7.2.0
@@ -34,7 +34,8 @@
..\..\AzureAppConfigurationRules.ruleset
- True
+ true
+ true
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 2dbb3aac..90831b4d 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
@@ -2,7 +2,7 @@
- net6.0;net7.0
+ net6.0;net7.0;net8.0
Microsoft.Azure.AppConfiguration.Functions.Worker allows developers to use the Microsoft Azure App Configuration service as a configuration source in their applications. This package adds additional features to the existing package Microsoft.Extensions.Configuration.AzureAppConfiguration for .NET Azure Functions running in an isolated process.
true
false
@@ -24,7 +24,7 @@
- 7.1.0
+ 7.2.0
@@ -37,7 +37,8 @@
..\..\AzureAppConfigurationRules.ruleset
- True
+ true
+ true
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
index 21b5165f..dd5cddbc 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
@@ -29,22 +29,12 @@ public AzureKeyVaultKeyValueAdapter(AzureKeyVaultSecretProvider secretProvider)
/// returns the keyname and actual value
public async Task>> ProcessKeyValue(ConfigurationSetting setting, Logger logger, CancellationToken cancellationToken)
{
- KeyVaultSecretReference secretRef;
-
- // Content validation
- try
- {
- secretRef = JsonSerializer.Deserialize(setting.Value);
- }
- catch (JsonException e)
- {
- throw CreateKeyVaultReferenceException("Invalid Key Vault reference.", setting, e, null);
- }
+ string secretRefUri = ParseSecretReferenceUri(setting);
// Uri validation
- if (string.IsNullOrEmpty(secretRef.Uri) || !Uri.TryCreate(secretRef.Uri, UriKind.Absolute, out Uri secretUri) || !KeyVaultSecretIdentifier.TryCreate(secretUri, out KeyVaultSecretIdentifier secretIdentifier))
+ if (string.IsNullOrEmpty(secretRefUri) || !Uri.TryCreate(secretRefUri, UriKind.Absolute, out Uri secretUri) || !KeyVaultSecretIdentifier.TryCreate(secretUri, out KeyVaultSecretIdentifier secretIdentifier))
{
- throw CreateKeyVaultReferenceException("Invalid Key vault secret identifier.", setting, null, secretRef);
+ throw CreateKeyVaultReferenceException("Invalid Key vault secret identifier.", setting, null, secretRefUri);
}
string secret;
@@ -55,11 +45,11 @@ public async Task>> ProcessKeyValue(Con
}
catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false))
{
- throw CreateKeyVaultReferenceException(e.Message, setting, e, secretRef);
+ throw CreateKeyVaultReferenceException(e.Message, setting, e, secretRefUri);
}
catch (Exception e) when (e is RequestFailedException || ((e as AggregateException)?.InnerExceptions?.All(e => e is RequestFailedException) ?? false))
{
- throw CreateKeyVaultReferenceException("Key vault error.", setting, e, secretRef);
+ throw CreateKeyVaultReferenceException("Key vault error.", setting, e, secretRefUri);
}
return new KeyValuePair[]
@@ -68,7 +58,7 @@ public async Task>> ProcessKeyValue(Con
};
}
- KeyVaultReferenceException CreateKeyVaultReferenceException(string message, ConfigurationSetting setting, Exception inner, KeyVaultSecretReference secretRef = null)
+ KeyVaultReferenceException CreateKeyVaultReferenceException(string message, ConfigurationSetting setting, Exception inner, string secretRefUri = null)
{
return new KeyVaultReferenceException(message, inner)
{
@@ -76,7 +66,7 @@ KeyVaultReferenceException CreateKeyVaultReferenceException(string message, Conf
Label = setting.Label,
Etag = setting.ETag.ToString(),
ErrorCode = (inner as RequestFailedException)?.ErrorCode,
- SecretIdentifier = secretRef?.Uri
+ SecretIdentifier = secretRefUri
};
}
@@ -102,5 +92,50 @@ public bool NeedsRefresh()
{
return _secretProvider.ShouldRefreshKeyVaultSecrets();
}
+
+ private string ParseSecretReferenceUri(ConfigurationSetting setting)
+ {
+ string secretRefUri = null;
+
+ try
+ {
+ var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(setting.Value));
+
+ if (reader.Read() && reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, null, null);
+ }
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ if (reader.GetString() == KeyVaultConstants.SecretReferenceUriJsonPropertyName)
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ secretRefUri = reader.GetString();
+ }
+ else
+ {
+ throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, null, null);
+ }
+ }
+ else
+ {
+ reader.Skip();
+ }
+ }
+ }
+ catch (JsonException e)
+ {
+ throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, e, null);
+ }
+
+ return secretRefUri;
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs
index ec55e115..1309e58c 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs
@@ -6,5 +6,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault
internal class KeyVaultConstants
{
public const string ContentType = "application/vnd.microsoft.appconfig.keyvaultref+json";
+
+ public const string SecretReferenceUriJsonPropertyName = "uri";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultSecretReference.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultSecretReference.cs
deleted file mode 100644
index 40efc2b1..00000000
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultSecretReference.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT license.
-//
-using System.Text.Json.Serialization;
-
-namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault
-{
- internal class KeyVaultSecretReference
- {
- [JsonPropertyName("uri")]
- public string Uri { get; set; }
- }
-}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
index 0c19cccf..61b4bb3b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
@@ -7,5 +7,8 @@ internal class ErrorMessages
{
public const string CacheExpirationTimeTooShort = "The cache expiration time cannot be less than {0} milliseconds.";
public const string SecretRefreshIntervalTooShort = "The secret refresh interval cannot be less than {0} milliseconds.";
+ public const string FeatureFlagInvalidJsonProperty = "Invalid property '{0}' for feature flag. Key: '{1}'. Found type: '{2}'. Expected type: '{3}'.";
+ public const string FeatureFlagInvalidFormat = "Invalid json format for feature flag. Key: '{0}'";
+ public const string InvalidKeyVaultReference = "Invalid Key Vault reference.";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
index bc91de1e..7ac04ca9 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
@@ -31,29 +31,59 @@ public static bool TryCreatePushNotification(this EventGridEvent eventGridEvent,
if (Uri.TryCreate(eventGridEvent.Subject, UriKind.Absolute, out Uri resourceUri))
{
- JsonElement eventGridEventData;
+ string syncToken = null;
try
{
- eventGridEventData = JsonDocument.Parse(eventGridEvent.Data.ToString()).RootElement;
+ var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(eventGridEvent.Data.ToString()));
+
+ if (reader.Read() && reader.TokenType != JsonTokenType.StartObject)
+ {
+ return false;
+ }
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ if (reader.GetString() == SyncTokenPropertyName)
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ syncToken = reader.GetString();
+ }
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ reader.Skip();
+ }
+ }
}
catch (JsonException)
{
return false;
}
- if (eventGridEventData.ValueKind == JsonValueKind.Object &&
- eventGridEventData.TryGetProperty(SyncTokenPropertyName, out JsonElement syncTokenJson) &&
- syncTokenJson.ValueKind == JsonValueKind.String)
+ if (syncToken == null)
{
- pushNotification = new PushNotification()
- {
- SyncToken = syncTokenJson.GetString(),
- EventType = eventGridEvent.EventType,
- ResourceUri = resourceUri
- };
- return true;
+ return false;
}
+
+ pushNotification = new PushNotification()
+ {
+ SyncToken = syncToken,
+ EventType = eventGridEvent.EventType,
+ ResourceUri = resourceUri
+ };
+
+ return true;
}
return false;
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/Utf8JsonReaderExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/Utf8JsonReaderExtensions.cs
deleted file mode 100644
index f5e2a01e..00000000
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/Utf8JsonReaderExtensions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT license.
-//
-using System.Text.Json;
-
-namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
-{
- internal static class Utf8JsonReaderExtensions
- {
- public static string ReadAsString(this Utf8JsonReader reader)
- {
- if (reader.Read())
- {
- return reader.GetString();
- }
-
- return null;
- }
- }
-}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
index efd4023d..80aed990 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
@@ -2,16 +2,13 @@
// Licensed under the MIT license.
//
using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class ClientFilter
{
- [JsonPropertyName("name")]
public string Name { get; set; }
- [JsonPropertyName("parameters")]
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 6927d310..ec29c199 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs
@@ -2,16 +2,13 @@
// Licensed under the MIT license.
//
using System.Collections.Generic;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureConditions
{
- [JsonPropertyName("client_filters")]
public List ClientFilters { get; set; } = new List();
- [JsonPropertyName("requirement_type")]
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 3be1d3ce..a26fb6cc 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
@@ -1,19 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureFlag
{
- [JsonPropertyName("id")]
public string Id { get; set; }
- [JsonPropertyName("enabled")]
public bool Enabled { get; set; }
- [JsonPropertyName("conditions")]
public FeatureConditions Conditions { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
index b0e09723..e4775950 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
@@ -10,5 +10,13 @@ internal class FeatureManagementConstants
public const string SectionName = "FeatureManagement";
public const string EnabledFor = "EnabledFor";
public const string RequirementType = "RequirementType";
+
+ public const string EnabledJsonPropertyName = "enabled";
+ public const string IdJsonPropertyName = "id";
+ public const string ConditionsJsonPropertyName = "conditions";
+ public const string RequirementTypeJsonPropertyName = "requirement_type";
+ public const string ClientFiltersJsonPropertyName = "client_filters";
+ public const string NameJsonPropertyName = "name";
+ public const string ParametersJsonPropertyName = "parameters";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
index b5138a46..a4f64d6b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
@@ -22,58 +22,48 @@ public FeatureManagementKeyValueAdapter(FeatureFilterTracing featureFilterTracin
public Task>> ProcessKeyValue(ConfigurationSetting setting, Logger logger, CancellationToken cancellationToken)
{
- FeatureFlag featureFlag;
- try
- {
- featureFlag = JsonSerializer.Deserialize(setting.Value);
- }
- catch (JsonException e)
- {
- throw new FormatException(setting.Key, e);
- }
+ FeatureFlag featureFlag = ParseFeatureFlag(setting.Key, setting.Value);
var keyValues = new List>();
- if (featureFlag.Enabled)
+ if (!string.IsNullOrEmpty(featureFlag.Id))
{
- //if (featureFlag.Conditions?.ClientFilters == null)
- if (featureFlag.Conditions?.ClientFilters == null || !featureFlag.Conditions.ClientFilters.Any()) // workaround since we are not yet setting client filters to null
+ if (featureFlag.Enabled)
{
- //
- // Always on
- keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}", true.ToString()));
- }
- else
- {
- //
- // Conditionally on based on feature filters
- for (int i = 0; i < featureFlag.Conditions.ClientFilters.Count; i++)
+ if (featureFlag.Conditions?.ClientFilters == null || !featureFlag.Conditions.ClientFilters.Any())
+ {
+ keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}", true.ToString()));
+ }
+ else
{
- ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
+ for (int i = 0; i < featureFlag.Conditions.ClientFilters.Count; i++)
+ {
+ ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
- _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
+ _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
- keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.EnabledFor}:{i}:Name", clientFilter.Name));
+ keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.EnabledFor}:{i}:Name", clientFilter.Name));
- foreach (KeyValuePair kvp in new JsonFlattener().FlattenJson(clientFilter.Parameters))
- {
- keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.EnabledFor}:{i}:Parameters:{kvp.Key}", kvp.Value));
+ foreach (KeyValuePair kvp in new JsonFlattener().FlattenJson(clientFilter.Parameters))
+ {
+ keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.EnabledFor}:{i}:Parameters:{kvp.Key}", kvp.Value));
+ }
}
- }
- //
- // process RequirementType only when filters are not empty
- if (featureFlag.Conditions.RequirementType != null)
- {
- keyValues.Add(new KeyValuePair(
- $"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.RequirementType}",
- featureFlag.Conditions.RequirementType));
+ //
+ // process RequirementType only when filters are not empty
+ if (featureFlag.Conditions.RequirementType != null)
+ {
+ keyValues.Add(new KeyValuePair(
+ $"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.RequirementType}",
+ featureFlag.Conditions.RequirementType));
+ }
}
}
- }
- else
- {
- keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}", false.ToString()));
+ else
+ {
+ keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}", false.ToString()));
+ }
}
return Task.FromResult>>(keyValues);
@@ -96,5 +86,251 @@ public bool NeedsRefresh()
{
return false;
}
+
+ private FormatException CreateFeatureFlagFormatException(string jsonPropertyName, string settingKey, string foundJsonValueKind, string expectedJsonValueKind)
+ {
+ return new FormatException(string.Format(
+ ErrorMessages.FeatureFlagInvalidJsonProperty,
+ jsonPropertyName,
+ settingKey,
+ foundJsonValueKind,
+ expectedJsonValueKind));
+ }
+
+ private FeatureFlag ParseFeatureFlag(string settingKey, string settingValue)
+ {
+ FeatureFlag featureFlag = new FeatureFlag();
+
+ var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(settingValue));
+
+ try
+ {
+ if (reader.Read() && reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new FormatException(string.Format(ErrorMessages.FeatureFlagInvalidFormat, settingKey));
+ }
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string propertyName = reader.GetString();
+
+ switch (propertyName)
+ {
+ case FeatureManagementConstants.IdJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureFlag.Id = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.IdJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.EnabledJsonPropertyName:
+ {
+ if (reader.Read() && (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True))
+ {
+ featureFlag.Enabled = reader.GetBoolean();
+ }
+ else if (reader.TokenType == JsonTokenType.String && bool.TryParse(reader.GetString(), out bool enabled))
+ {
+ featureFlag.Enabled = enabled;
+ }
+ else
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.EnabledJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ $"{JsonTokenType.True}' or '{JsonTokenType.False}");
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.ConditionsJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ featureFlag.Conditions = ParseFeatureConditions(ref reader, settingKey);
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.ConditionsJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+ }
+ catch (JsonException e)
+ {
+ throw new FormatException(settingKey, e);
+ }
+
+ return featureFlag;
+ }
+
+ private FeatureConditions ParseFeatureConditions(ref Utf8JsonReader reader, string settingKey)
+ {
+ var featureConditions = new FeatureConditions();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string conditionsPropertyName = reader.GetString();
+
+ switch (conditionsPropertyName)
+ {
+ case FeatureManagementConstants.ClientFiltersJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.Null)
+ {
+ break;
+ }
+ else if (reader.TokenType == JsonTokenType.StartArray)
+ {
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ ClientFilter clientFilter = ParseClientFilter(ref reader, settingKey);
+
+ if (clientFilter.Name != null ||
+ (clientFilter.Parameters.ValueKind == JsonValueKind.Object &&
+ clientFilter.Parameters.EnumerateObject().Any()))
+ {
+ featureConditions.ClientFilters.Add(clientFilter);
+ }
+ }
+ }
+ }
+ else
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.ClientFiltersJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.RequirementTypeJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureConditions.RequirementType = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.RequirementTypeJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featureConditions;
+ }
+
+ private ClientFilter ParseClientFilter(ref Utf8JsonReader reader, string settingKey)
+ {
+ var clientFilter = new ClientFilter();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string clientFiltersPropertyName = reader.GetString();
+
+ switch (clientFiltersPropertyName)
+ {
+ case FeatureManagementConstants.NameJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ clientFilter.Name = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.NameJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.ParametersJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ clientFilter.Parameters = JsonDocument.ParseValue(ref reader).RootElement;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.ParametersJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return clientFilter;
+ }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
index 0a47923f..2c11d423 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
@@ -30,10 +30,15 @@ public Task>> ProcessKeyValue(Configura
}
string rootJson = $"{{\"{setting.Key}\":{setting.Value}}}";
- JsonElement jsonData;
+
+ List> keyValuePairs = new List>();
+
try
{
- jsonData = JsonSerializer.Deserialize(rootJson);
+ using (JsonDocument document = JsonDocument.Parse(rootJson))
+ {
+ keyValuePairs = new JsonFlattener().FlattenJson(document.RootElement);
+ }
}
catch (JsonException)
{
@@ -41,7 +46,7 @@ public Task>> ProcessKeyValue(Configura
return Task.FromResult>>(new[] { new KeyValuePair(setting.Key, setting.Value) });
}
- return Task.FromResult>>(new JsonFlattener().FlattenJson(jsonData));
+ return Task.FromResult>>(keyValuePairs);
}
public bool CanProcess(ConfigurationSetting setting)
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 6d6ddfd7..466c58be 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -14,7 +14,7 @@
-
+
@@ -34,7 +34,7 @@
- 7.1.0
+ 7.2.0
@@ -47,7 +47,8 @@
..\..\AzureAppConfigurationRules.ruleset
- True
+ true
+ true
diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
index ef89659c..a8cd3b51 100644
--- a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
+++ b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
@@ -1,7 +1,7 @@
- net6.0;net7.0
+ net6.0;net7.0;net8.0
8.0
false
true
@@ -10,11 +10,14 @@
-
-
-
-
-
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
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 0340afd5..f5af5a3f 100644
--- a/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj
+++ b/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj
@@ -1,7 +1,7 @@
- net6.0;net7.0
+ net6.0;net7.0;net8.0
8.0
false
true
@@ -10,11 +10,14 @@
-
-
-
-
-
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
index 4eb83a90..99348464 100644
--- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
@@ -9,7 +9,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
-using Microsoft.Extensions.Options;
using Moq;
using System;
using System.Collections.Generic;
@@ -85,7 +84,239 @@ public class FeatureManagementTests
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"));
- List _featureFlagCollection = new List
+ List _nullOrMissingConditionsFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "NullParameters",
+ value: @"
+ {
+ ""id"": ""NullParameters"",
+ ""description"": """",
+ ""display_name"": ""Null Parameters"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": [
+ {
+ ""name"": ""Filter"",
+ ""parameters"": null
+ }
+ ]
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "NullConditions",
+ value: @"
+ {
+ ""id"": ""NullConditions"",
+ ""description"": """",
+ ""display_name"": ""Null Conditions"",
+ ""enabled"": true,
+ ""conditions"": null
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "NullClientFilters",
+ value: @"
+ {
+ ""id"": ""NullClientFilters"",
+ ""description"": """",
+ ""display_name"": ""Null Client Filters"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": null
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "NoConditions",
+ value: @"
+ {
+ ""id"": ""NoConditions"",
+ ""description"": """",
+ ""display_name"": ""No Conditions"",
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "EmptyConditions",
+ value: @"
+ {
+ ""id"": ""EmptyConditions"",
+ ""description"": """",
+ ""display_name"": ""Empty Conditions"",
+ ""conditions"": {},
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "EmptyClientFilter",
+ value: @"
+ {
+ ""id"": ""EmptyClientFilter"",
+ ""description"": """",
+ ""display_name"": ""Empty Client Filter"",
+ ""conditions"": {
+ ""client_filters"": [
+ {}
+ ]
+ },
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
+ };
+
+ List _validFormatFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "AdditionalProperty",
+ value: @"
+ {
+ ""id"": ""AdditionalProperty"",
+ ""description"": ""Should not throw an exception, additional properties are skipped."",
+ ""ignored_object"": {
+ ""id"": false
+ },
+ ""enabled"": true,
+ ""conditions"": {}
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "DuplicateProperty",
+ value: @"
+ {
+ ""id"": ""DuplicateProperty"",
+ ""description"": ""Should not throw an exception, last of duplicate properties will win."",
+ ""enabled"": false,
+ ""enabled"": true,
+ ""conditions"": {}
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "AllowNullRequirementType",
+ value: @"
+ {
+ ""id"": ""AllowNullRequirementType"",
+ ""description"": ""Should not throw an exception, requirement type is allowed as null."",
+ ""enabled"": true,
+ ""conditions"": {
+ ""requirement_type"": null
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
+ };
+
+ List _invalidFormatFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingClosingBracket1",
+ value: @"
+ {
+ ""id"": ""MissingClosingBracket1"",
+ ""description"": ""Should throw an exception, invalid end of json."",
+ ""enabled"": true,
+ ""conditions"": {}
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingClosingBracket2",
+ value: @"
+ {
+ ""id"": ""MissingClosingBracket2"",
+ ""description"": ""Should throw an exception, invalid end of conditions object."",
+ ""conditions"": {,
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingClosingBracket3",
+ value: @"
+ {
+ ""id"": ""MissingClosingBracket3"",
+ ""description"": ""Should throw an exception, no closing bracket on client filters array."",
+ ""conditions"": {
+ ""client_filters"": [
+ },
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingOpeningBracket1",
+ value: @"
+ {
+ ""id"": ""MissingOpeningBracket1"",
+ ""description"": ""Should throw an exception, no opening bracket on conditions object."",
+ ""conditions"": },
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingOpeningBracket2",
+ value: @"
+ {
+ ""id"": ""MissingOpeningBracket2"",
+ ""description"": ""Should throw an exception, no opening bracket on client filters array."",
+ ""conditions"": {
+ ""client_filters"": ]
+ },
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
+ };
+
+ List _featureFlagCollection = new List
{
ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "App1_Feature1",
@@ -477,6 +708,124 @@ public void SelectFeatureFlags()
Assert.Null(config["FeatureManagement:App2_Feature2"]);
}
+ [Fact]
+ public void TestNullAndMissingValuesForConditions()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(new MockAsyncPageable(_nullOrMissingConditionsFeatureFlagCollection));
+
+ var testClient = mockClient.Object;
+
+ // Makes sure that adapter properly processes values and doesn't throw an exception
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.UseFeatureFlags(ff =>
+ {
+ ff.CacheExpirationInterval = cacheExpiration;
+ ff.Select(KeyFilter.Any);
+ });
+ })
+ .Build();
+
+ Assert.Null(config["FeatureManagement:NullConditions:EnabledFor"]);
+ Assert.Equal("Filter", config["FeatureManagement:NullParameters:EnabledFor:0:Name"]);
+ Assert.Null(config["FeatureManagement:NullParameters:EnabledFor:0:Parameters"]);
+ Assert.Null(config["FeatureManagement:NullClientFilters:EnabledFor"]);
+ Assert.Null(config["FeatureManagement:NoConditions:EnabledFor"]);
+ Assert.Null(config["FeatureManagement:EmptyConditions:EnabledFor"]);
+ Assert.Null(config["FeatureManagement:EmptyClientFilter:EnabledFor"]);
+ }
+
+ [Fact]
+ public void InvalidFeatureFlagFormatsThrowFormatException()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns((Func)GetTestKeys);
+
+ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct)
+ {
+ var copy = new List();
+ var newSetting = _invalidFormatFeatureFlagCollection.FirstOrDefault(s => s.Key == selector.KeyFilter);
+ if (newSetting != null)
+ copy.Add(TestHelpers.CloneSetting(newSetting));
+ return new MockAsyncPageable(copy);
+ };
+
+ var testClient = mockClient.Object;
+
+ foreach (ConfigurationSetting setting in _invalidFormatFeatureFlagCollection)
+ {
+ void action() => new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.Select("_");
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.UseFeatureFlags(ff =>
+ {
+ ff.CacheExpirationInterval = cacheExpiration;
+ ff.Select(setting.Key.Substring(FeatureManagementConstants.FeatureFlagMarker.Length));
+ });
+ })
+ .Build();
+
+ // Each of the feature flags should throw an exception
+ Assert.Throws(action);
+ }
+ }
+
+ [Fact]
+ public void AlternateValidFeatureFlagFormats()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns((Func)GetTestKeys);
+
+ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct)
+ {
+ var copy = new List();
+ var newSetting = _validFormatFeatureFlagCollection.FirstOrDefault(s => s.Key == selector.KeyFilter);
+ if (newSetting != null)
+ copy.Add(TestHelpers.CloneSetting(newSetting));
+ return new MockAsyncPageable(copy);
+ };
+
+ var testClient = mockClient.Object;
+
+ foreach (ConfigurationSetting setting in _validFormatFeatureFlagCollection)
+ {
+ string flagKey = setting.Key.Substring(FeatureManagementConstants.FeatureFlagMarker.Length);
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.Select("_");
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.UseFeatureFlags(ff =>
+ {
+ ff.CacheExpirationInterval = cacheExpiration;
+ ff.Select(flagKey);
+ });
+ })
+ .Build();
+
+ // None of the feature flags should throw an exception, and the flag should be loaded like normal
+ Assert.Equal("True", config[$"FeatureManagement:{flagKey}"]);
+ }
+ }
+
[Fact]
public void MultipleSelectsInSameUseFeatureFlags()
{
diff --git a/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs b/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
index 7723a612..91cb03a8 100644
--- a/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
+++ b/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
@@ -28,20 +28,15 @@ public void JsonContentTypeTests_CompareJsonSettingsBetweenAppConfigAndJsonFile(
var appconfigSettings = new ConfigurationBuilder()
.AddAzureAppConfiguration(options => options.ClientManager = mockClientManager)
- .Build()
- .AsEnumerable();
+ .Build();
var jsonSettings = new ConfigurationBuilder()
.AddJsonFile(jsonFilePath)
- .Build()
- .AsEnumerable();
-
- Assert.Equal(jsonSettings.Count(), appconfigSettings.Count());
+ .Build();
- foreach (KeyValuePair jsonSetting in jsonSettings)
+ foreach (KeyValuePair jsonSetting in jsonSettings.AsEnumerable())
{
- KeyValuePair appconfigSetting = appconfigSettings.SingleOrDefault(x => x.Key == jsonSetting.Key);
- Assert.Equal(jsonSetting, appconfigSetting);
+ Assert.Equal(jsonSettings.GetSection(jsonSetting.Key).Value, appconfigSettings.GetSection(jsonSetting.Key).Value);
}
}
diff --git a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
index 0cccf3cd..88c15ffb 100644
--- a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
+++ b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
@@ -12,6 +12,7 @@
using Moq;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
@@ -82,6 +83,91 @@ public class KeyVaultReferenceTests
contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
};
+ List _invalidJsonKvCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"MissingClosingBracket",
+ value: @"
+ {
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"MissingOpeningBracket",
+ value: @"
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"MissingUriInRootJson",
+ value: @"
+ {
+ {
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"UriValueInsideObject",
+ value: @"
+ {
+ {
+ ""uri"": {
+ ""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ }
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8")
+ };
+
+ List _validJsonKvCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"AdditionalProperty1",
+ value: @"
+ {
+ ""additional_property"":""additional_property"",
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"AdditionalProperty2",
+ value: @"
+ {
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret"",
+ ""additional_property"": {
+ ""inside_property"": ""inside_property""
+ }
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"DuplicateUri",
+ value: @"
+ {
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/certificates/TestCertificate"",
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8")
+ };
+
[Fact]
public void NotSecretIdentifierURI()
{
@@ -875,5 +961,95 @@ public void SecretsWithDifferentRefreshIntervals()
// Validate that 3 calls were made to fetch secrets from KeyVault because the secret cache had expired for only one secret.
mockSecretClient.Verify(client => client.GetSecretAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3));
}
+
+
+ [Fact]
+ public void ThrowsWhenInvalidKeyVaultSecretReferenceJson()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns((Func)GetTestKeys);
+
+ var mockSecretClient = new Mock(MockBehavior.Strict);
+ mockSecretClient.SetupGet(client => client.VaultUri).Returns(new Uri("https://keyvault-theclassics.vault.azure.net"));
+
+ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct)
+ {
+ var copy = new List();
+ var newSetting = _invalidJsonKvCollection.FirstOrDefault(s => s.Key == selector.KeyFilter);
+ if (newSetting != null)
+ copy.Add(TestHelpers.CloneSetting(newSetting));
+ return new MockAsyncPageable(copy);
+ };
+
+ var testClient = mockClient.Object;
+
+ foreach (ConfigurationSetting setting in _invalidJsonKvCollection)
+ {
+ void action() => new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.Select(setting.Key);
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.ConfigureKeyVault(kv =>
+ {
+ kv.Register(mockSecretClient.Object);
+ });
+ })
+ .Build();
+
+ // Each of the secret references should throw an exception when parsed
+ Assert.Throws(action);
+ }
+ }
+
+ [Fact]
+ public void AlternateValidKeyVaultSecretReferenceJsons()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns((Func)GetTestKeys);
+
+ var mockSecretClient = new Mock(MockBehavior.Strict);
+ mockSecretClient.SetupGet(client => client.VaultUri).Returns(new Uri("https://keyvault-theclassics.vault.azure.net"));
+ mockSecretClient.Setup(client => client.GetSecretAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns((string name, string version, CancellationToken cancellationToken) =>
+ Task.FromResult((Response)new MockResponse(new KeyVaultSecret(name, _secretValue))));
+
+ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct)
+ {
+ var copy = new List();
+ var newSetting = _validJsonKvCollection.FirstOrDefault(s => s.Key == selector.KeyFilter);
+ if (newSetting != null)
+ copy.Add(TestHelpers.CloneSetting(newSetting));
+ return new MockAsyncPageable(copy);
+ };
+
+ var testClient = mockClient.Object;
+
+ foreach (ConfigurationSetting setting in _validJsonKvCollection)
+ {
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.Select(setting.Key);
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.ConfigureKeyVault(kv =>
+ {
+ kv.Register(mockSecretClient.Object);
+ });
+ })
+ .Build();
+
+ // Each of the secret references should work as normal and use the uri
+ Assert.Equal(_secretValue, config[setting.Key]);
+ }
+ }
}
}
diff --git a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
index aa88fc86..c94e877f 100644
--- a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
@@ -170,10 +170,59 @@ public class PushRefreshTests
"Microsoft.AppConfiguration.KeyValueModified", "2",
BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;CRAle3342\"}")
)
+ },
+
+ // Test that last syncToken is used
+ {
+ "sn;BYRte4456",
+ new EventGridEvent(
+ "https://store2.resource.io/kv/searchQuery2",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
+ BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;CRAle3342\",\"syncToken\":\"sn;BYRte4456\"}")
+ )
}
};
- ConfigurationSetting FirstKeyValue => _kvCollection.First();
+ 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;Ttylmable",
+ new EventGridEvent(
+ "https://store1.resource.io/kv/searchQuery2",
+ "Microsoft.AppConfiguration.KeyValueDeleted", "2",
+ BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"fake_property\":{\"syncToken\":\"sn;Ttylmable\"}}")
+ )
+ },
+
+ {
+ "sn;CRAle3342",
+ new EventGridEvent(
+ "https://store2.resource.io/kv/searchQuery2",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
+ BinaryData.FromString("{\"fake_property\":{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;CRAle3342\"}}")
+ )
+ }
+ };
+
+ ConfigurationSetting FirstKeyValue => _kvCollection.First();
[Fact]
public void ValidatePushNotificationCreation()
@@ -191,6 +240,17 @@ public void ValidatePushNotificationCreation()
}
}
+ [Fact]
+ public void InvalidPushNotificationCreation()
+ {
+ foreach (KeyValuePair eventGridAndSync in _invalidFormatEventGridEvents)
+ {
+ EventGridEvent eventGridEvent = eventGridAndSync.Value;
+
+ Assert.False(eventGridEvent.TryCreatePushNotification(out PushNotification _));
+ }
+ }
+
[Fact]
public void ProcessPushNotificationThrowsArgumentExceptions()
{
diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/TestHelper.cs
index fe1685d9..477b4429 100644
--- a/tests/Tests.AzureAppConfiguration/TestHelper.cs
+++ b/tests/Tests.AzureAppConfiguration/TestHelper.cs
@@ -115,16 +115,22 @@ public static ConfigurationSetting CloneSetting(ConfigurationSetting setting)
public static List LoadJsonSettingsFromFile(string path)
{
List _kvCollection = new List();
- var valueArray = JsonSerializer.Deserialize(File.ReadAllText(path)).EnumerateArray();
- foreach (var setting in valueArray)
+
+ using (JsonDocument document = JsonDocument.Parse(File.ReadAllText(path)))
{
- ConfigurationSetting kv = ConfigurationModelFactory
- .ConfigurationSetting(
- key: setting.GetProperty("key").ToString(),
- value: setting.GetProperty("value").GetRawText(),
- contentType: setting.GetProperty("contentType").ToString());
- _kvCollection.Add(kv);
+ var valueArray = document.RootElement.EnumerateArray();
+
+ foreach (var setting in valueArray)
+ {
+ ConfigurationSetting kv = ConfigurationModelFactory
+ .ConfigurationSetting(
+ key: setting.GetProperty("key").ToString(),
+ value: setting.GetProperty("value").GetRawText(),
+ contentType: setting.GetProperty("contentType").ToString());
+ _kvCollection.Add(kv);
+ }
}
+
return _kvCollection;
}
diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
index 0dfcabbd..b0aef653 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
+++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
@@ -1,33 +1,33 @@
- net48;net6.0;net7.0
+ net48;net6.0;net7.0;net8.0
8.0
false
true
..\..\build\AzureAppConfiguration.snk
false
+ false
-
-
-
-
+
+
+
-
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
+
-
-
-
-
Always
diff --git a/tests/Tests.AzureAppConfiguration/Tests.cs b/tests/Tests.AzureAppConfiguration/Tests.cs
index b7e61978..c3b27124 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.cs
+++ b/tests/Tests.AzureAppConfiguration/Tests.cs
@@ -239,7 +239,7 @@ public void TestUserAgentHeader()
// 4. Contains the name and version of the App Configuration SDK package
// 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+(-preview(\.\d+)?)?,azsdk-net-Data.AppConfiguration/[.+\w-]+ \([.;\w\s]+\)$";
+ 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));