Skip to content

Commit e7ca517

Browse files
authored
Adding Snapshot References Feature (#689)
* feat: defined snapshot reference content type and Json Property * added snapshot reference content type to content type extensions * updated the snapshot reference content type to include the charset * Implementing snapshot references * Fixed the behavior of an edge case and moved file to a better location in repo * created unit tests for Snapshot References * Removed unnecessary comments from tests * removed extra comments and cleaned up code * modified test and added set up for testing snapshot references * adding more integration tests for snapshot references * removed client and cancellation token from snapshot reference class and updated all affected code * updated namespaces for all files and created parser class * additional file to update namespace * Modified the code to directly use the content type instead of copying * Used object initializer pattern and passed in cancellationTokens to async methods * fixed comment and updated TestContext format * updated behavior to throw exception if snapshot name is null * Moved the exception error messages from inline to ErrorMessages class and renamed parsing method to Parse() * Added Request tracing and case for snapshot reference is registered for refresh by not called in select * added test case to test adding snapshot reference to register but not part of select * updating request tracing to only tracking use of snapshot references * removing request tracing logic for Snapshot References count * added comments and returning SnapshotReference type from Parse() * removed second way of checking for snapshot references type and updated test cases * removed redundant error message * removed unnecessary code * fixed whitespace issues and made error message clearer * updating naming and removing old telemetry code * updated namespace and directory to SnapshotReference * Added new JsonFields type * fixed error regarding same namespace and type * Updated the Parse logic to handle all exceptions instead of LoadSnapshotData * updated comments to method * removed the update and reset snapshot reference request tracing methods as they were unnecessary * removed requestTracing for SnapshotReference from refresh * reverting previous change * Made these keyvault integration tests more resilient * Cleaning up code * More nit changes * updated request tracing for snapshot references in refresh * making the key vault integration test more resilient * correction to which test needed resilience
1 parent c17524a commit e7ca517

File tree

10 files changed

+1001
-42
lines changed

10 files changed

+1001
-42
lines changed

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs

Lines changed: 95 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//
44
using Azure;
55
using Azure.Data.AppConfiguration;
6+
using Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference;
67
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
78
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models;
89
using Microsoft.Extensions.Diagnostics.HealthChecks;
@@ -874,6 +875,31 @@ await CallWithRequestTracing(async () =>
874875

875876
foreach (ConfigurationSetting setting in page.Values)
876877
{
878+
if (setting.ContentType == SnapshotReferenceConstants.ContentType)
879+
{
880+
// Track snapshot reference usage for telemetry
881+
if (_requestTracingEnabled && _requestTracingOptions != null)
882+
{
883+
_requestTracingOptions.UsesSnapshotReference = true;
884+
}
885+
886+
SnapshotReference.SnapshotReference snapshotReference = SnapshotReferenceParser.Parse(setting);
887+
888+
Dictionary<string, ConfigurationSetting> resolvedSettings = await LoadSnapshotData(snapshotReference.SnapshotName, client, cancellationToken).ConfigureAwait(false);
889+
890+
if (_requestTracingEnabled && _requestTracingOptions != null)
891+
{
892+
_requestTracingOptions.UsesSnapshotReference = false;
893+
}
894+
895+
foreach (KeyValuePair<string, ConfigurationSetting> resolvedSetting in resolvedSettings)
896+
{
897+
data[resolvedSetting.Key] = resolvedSetting.Value;
898+
}
899+
900+
continue;
901+
}
902+
877903
data[setting.Key] = setting;
878904

879905
if (loadOption.IsFeatureFlagSelector)
@@ -899,37 +925,54 @@ await CallWithRequestTracing(async () =>
899925
}
900926
else
901927
{
902-
ConfigurationSnapshot snapshot;
928+
Dictionary<string, ConfigurationSetting> resolvedSettings = await LoadSnapshotData(loadOption.SnapshotName, client, cancellationToken).ConfigureAwait(false);
903929

904-
try
905-
{
906-
snapshot = await client.GetSnapshotAsync(loadOption.SnapshotName).ConfigureAwait(false);
907-
}
908-
catch (RequestFailedException rfe) when (rfe.Status == (int)HttpStatusCode.NotFound)
930+
foreach (KeyValuePair<string, ConfigurationSetting> resolvedSetting in resolvedSettings)
909931
{
910-
throw new InvalidOperationException($"Could not find snapshot with name '{loadOption.SnapshotName}'.", rfe);
932+
data[resolvedSetting.Key] = resolvedSetting.Value;
911933
}
934+
}
935+
}
912936

913-
if (snapshot.SnapshotComposition != SnapshotComposition.Key)
914-
{
915-
throw new InvalidOperationException($"{nameof(snapshot.SnapshotComposition)} for the selected snapshot with name '{snapshot.Name}' must be 'key', found '{snapshot.SnapshotComposition}'.");
916-
}
937+
return data;
938+
}
917939

918-
IAsyncEnumerable<ConfigurationSetting> settingsEnumerable = client.GetConfigurationSettingsForSnapshotAsync(
919-
loadOption.SnapshotName,
920-
cancellationToken);
940+
private async Task<Dictionary<string, ConfigurationSetting>> LoadSnapshotData(string snapshotName, ConfigurationClient client, CancellationToken cancellationToken)
941+
{
942+
var resolvedSettings = new Dictionary<string, ConfigurationSetting>();
921943

922-
await CallWithRequestTracing(async () =>
923-
{
924-
await foreach (ConfigurationSetting setting in settingsEnumerable.ConfigureAwait(false))
925-
{
926-
data[setting.Key] = setting;
927-
}
928-
}).ConfigureAwait(false);
929-
}
944+
Debug.Assert(!string.IsNullOrWhiteSpace(snapshotName));
945+
946+
ConfigurationSnapshot snapshot = null;
947+
948+
try
949+
{
950+
await CallWithRequestTracing(async () => snapshot = await client.GetSnapshotAsync(snapshotName, cancellationToken: cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
930951
}
952+
catch (RequestFailedException rfe) when (rfe.Status == (int)HttpStatusCode.NotFound)
953+
{
931954

932-
return data;
955+
return resolvedSettings; // Return empty dictionary if snapshot not found
956+
}
957+
958+
if (snapshot.SnapshotComposition != SnapshotComposition.Key)
959+
{
960+
throw new InvalidOperationException(string.Format(ErrorMessages.SnapshotInvalidComposition, nameof(snapshot.SnapshotComposition), snapshot.Name, snapshot.SnapshotComposition));
961+
}
962+
963+
IAsyncEnumerable<ConfigurationSetting> settingsEnumerable = client.GetConfigurationSettingsForSnapshotAsync(
964+
snapshotName,
965+
cancellationToken);
966+
967+
await CallWithRequestTracing(async () =>
968+
{
969+
await foreach (ConfigurationSetting setting in settingsEnumerable.WithCancellation(cancellationToken).ConfigureAwait(false))
970+
{
971+
resolvedSettings[setting.Key] = setting;
972+
}
973+
}).ConfigureAwait(false);
974+
975+
return resolvedSettings;
933976
}
934977

935978
private async Task<Dictionary<KeyValueIdentifier, ConfigurationSetting>> LoadKeyValuesRegisteredForRefresh(
@@ -969,7 +1012,33 @@ private async Task<Dictionary<KeyValueIdentifier, ConfigurationSetting>> LoadKey
9691012
if (watchedKv != null)
9701013
{
9711014
watchedIndividualKvs[watchedKeyLabel] = new ConfigurationSetting(watchedKv.Key, watchedKv.Value, watchedKv.Label, watchedKv.ETag);
972-
existingSettings[watchedKey] = watchedKv;
1015+
1016+
if (watchedKv.ContentType == SnapshotReferenceConstants.ContentType)
1017+
{
1018+
// Track snapshot reference usage for telemetry
1019+
if (_requestTracingEnabled && _requestTracingOptions != null)
1020+
{
1021+
_requestTracingOptions.UsesSnapshotReference = true;
1022+
}
1023+
1024+
SnapshotReference.SnapshotReference snapshotReference = SnapshotReferenceParser.Parse(watchedKv);
1025+
1026+
Dictionary<string, ConfigurationSetting> resolvedSettings = await LoadSnapshotData(snapshotReference.SnapshotName, client, cancellationToken).ConfigureAwait(false);
1027+
1028+
if (_requestTracingEnabled && _requestTracingOptions != null)
1029+
{
1030+
_requestTracingOptions.UsesSnapshotReference = false;
1031+
}
1032+
1033+
foreach (KeyValuePair<string, ConfigurationSetting> resolvedSetting in resolvedSettings)
1034+
{
1035+
existingSettings[resolvedSetting.Key] = resolvedSetting.Value;
1036+
}
1037+
}
1038+
else
1039+
{
1040+
existingSettings[watchedKey] = watchedKv;
1041+
}
9731042
}
9741043
}
9751044

@@ -1034,7 +1103,8 @@ await CallWithRequestTracing(
10341103
logInfoBuilder.AppendLine(LogHelper.BuildKeyValueSettingUpdatedMessage(change.Key));
10351104
keyValueChanges.Add(change);
10361105

1037-
if (kvWatcher.RefreshAll)
1106+
// If the watcher is set to refresh all, or the content type matches the snapshot reference content type then refresh all
1107+
if (kvWatcher.RefreshAll || watchedKv.ContentType == SnapshotReferenceConstants.ContentType)
10381108
{
10391109
return true;
10401110
}

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ internal class ErrorMessages
1010
public const string FeatureFlagInvalidJsonProperty = "Invalid property '{0}' for feature flag. Key: '{1}'. Found type: '{2}'. Expected type: '{3}'.";
1111
public const string FeatureFlagInvalidFormat = "Invalid json format for feature flag. Key: '{0}'.";
1212
public const string InvalidKeyVaultReference = "Invalid Key Vault reference.";
13+
public const string SnapshotReferenceInvalidFormat = "Invalid snapshot reference format for key '{0}' (label: '{1}').";
14+
public const string SnapshotReferenceInvalidJsonProperty = "Invalid snapshot reference format for key '{0}' (label: '{1}'). The '{2}' property must be a string value, but found {3}.";
15+
public const string SnapshotReferencePropertyMissing = "Invalid snapshot reference format for key '{0}' (label: '{1}'). The '{2}' property is required.";
16+
public const string SnapshotInvalidComposition = "{0} for the selected snapshot with name '{1}' must be 'key', found '{2}'.";
1317
}
1418
}

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal class RequestTracingConstants
3333
public const string LoadBalancingEnabledTag = "LB";
3434
public const string AIConfigurationTag = "AI";
3535
public const string AIChatCompletionConfigurationTag = "AICC";
36-
36+
public const string SnapshotReferenceTag = "SnapshotRef";
3737
public const string SignalRUsedTag = "SignalR";
3838
public const string FailoverRequestTag = "Failover";
3939
public const string PushRefreshTag = "PushRefresh";

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//
44
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
55
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
6+
using Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference;
67
using System.Net.Mime;
78
using System.Text;
89

@@ -87,6 +88,11 @@ internal class RequestTracingOptions
8788
/// </summary>
8889
public bool UsesAIChatCompletionConfiguration { get; set; } = false;
8990

91+
/// <summary>
92+
/// Flag to indicate whether any key-value uses snapshot references.
93+
/// </summary>
94+
public bool UsesSnapshotReference { get; set; } = false;
95+
9096
/// <summary>
9197
/// Resets the AI configuration tracing flags.
9298
/// </summary>
@@ -125,7 +131,8 @@ public bool UsesAnyTracingFeature()
125131
return IsLoadBalancingEnabled ||
126132
IsSignalRUsed ||
127133
UsesAIConfiguration ||
128-
UsesAIChatCompletionConfiguration;
134+
UsesAIChatCompletionConfiguration ||
135+
UsesSnapshotReference;
129136
}
130137

131138
/// <summary>
@@ -176,6 +183,16 @@ public string CreateFeaturesString()
176183
sb.Append(RequestTracingConstants.AIChatCompletionConfigurationTag);
177184
}
178185

186+
if (UsesSnapshotReference)
187+
{
188+
if (sb.Length > 0)
189+
{
190+
sb.Append(RequestTracingConstants.Delimiter);
191+
}
192+
193+
sb.Append(RequestTracingConstants.SnapshotReferenceTag);
194+
}
195+
179196
return sb.ToString();
180197
}
181198
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference
5+
{
6+
internal static class JsonFields
7+
{
8+
public const string SnapshotName = "snapshot_name";
9+
}
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
5+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference
6+
{
7+
internal class SnapshotReference
8+
{
9+
public string SnapshotName { get; set; }
10+
}
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference
5+
{
6+
internal class SnapshotReferenceConstants
7+
{
8+
public const string ContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8";
9+
}
10+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Azure.Data.AppConfiguration;
5+
using System;
6+
using System.Text.Json;
7+
8+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference
9+
{
10+
/// <summary>
11+
/// Provides parsing functionality for snapshot reference configuration settings.
12+
/// </summary>
13+
internal static class SnapshotReferenceParser
14+
{
15+
/// <summary>
16+
/// Parses a snapshot name from a configuration setting containing snapshot reference JSON.
17+
/// </summary>
18+
/// <param name="setting">The configuration setting containing the snapshot reference JSON.</param>
19+
/// <returns>The snapshot reference containing a valid, non-empty snapshot name.</returns>
20+
/// <exception cref="ArgumentNullException">Thrown when the setting is null.</exception>
21+
/// <exception cref="FormatException">Thrown when the setting contains invalid JSON, invalid snapshot reference format, or empty/whitespace snapshot name.</exception>
22+
public static SnapshotReference Parse(ConfigurationSetting setting)
23+
{
24+
if (setting == null)
25+
{
26+
throw new ArgumentNullException(nameof(setting));
27+
}
28+
29+
if (string.IsNullOrWhiteSpace(setting.Value))
30+
{
31+
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidFormat, setting.Key, setting.Label));
32+
}
33+
34+
try
35+
{
36+
var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(setting.Value));
37+
38+
if (reader.Read() && reader.TokenType != JsonTokenType.StartObject)
39+
{
40+
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidFormat, setting.Key, setting.Label));
41+
}
42+
43+
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
44+
{
45+
if (reader.TokenType != JsonTokenType.PropertyName)
46+
{
47+
continue;
48+
}
49+
50+
if (reader.GetString() == JsonFields.SnapshotName)
51+
{
52+
if (reader.Read() && reader.TokenType == JsonTokenType.String)
53+
{
54+
string snapshotName = reader.GetString();
55+
if (string.IsNullOrWhiteSpace(snapshotName))
56+
{
57+
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidFormat, setting.Key, setting.Label));
58+
}
59+
60+
return new SnapshotReference { SnapshotName = snapshotName };
61+
}
62+
63+
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidJsonProperty, setting.Key, setting.Label, JsonFields.SnapshotName, reader.TokenType));
64+
}
65+
66+
// Skip unknown properties
67+
reader.Skip();
68+
}
69+
70+
throw new FormatException(string.Format(ErrorMessages.SnapshotReferencePropertyMissing, setting.Key, setting.Label, JsonFields.SnapshotName));
71+
}
72+
catch (JsonException jsonEx)
73+
{
74+
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidFormat, setting.Key, setting.Label), jsonEx);
75+
}
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)