Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
02ac2d2
feat: defined snapshot reference content type and Json Property
t-vvidyasaga Jul 29, 2025
69c2f64
added snapshot reference content type to content type extensions
t-vvidyasaga Aug 6, 2025
002db4e
updated the snapshot reference content type to include the charset
t-vvidyasaga Aug 6, 2025
011cb88
Implementing snapshot references
t-vvidyasaga Aug 14, 2025
1b137bb
Fixed the behavior of an edge case and moved file to a better locatio…
t-vvidyasaga Aug 16, 2025
540ef4f
created unit tests for Snapshot References
t-vvidyasaga Aug 16, 2025
68d80f5
Removed unnecessary comments from tests
t-vvidyasaga Aug 20, 2025
1a5345f
removed extra comments and cleaned up code
t-vvidyasaga Aug 20, 2025
bdc4ff6
modified test and added set up for testing snapshot references
t-vvidyasaga Aug 20, 2025
244042b
adding more integration tests for snapshot references
t-vvidyasaga Aug 20, 2025
be4794b
removed client and cancellation token from snapshot reference class a…
t-vvidyasaga Aug 21, 2025
3ceb262
updated namespaces for all files and created parser class
t-vvidyasaga Aug 21, 2025
a0c4193
additional file to update namespace
t-vvidyasaga Aug 21, 2025
6778996
Modified the code to directly use the content type instead of copying
t-vvidyasaga Aug 21, 2025
0b958e3
Used object initializer pattern and passed in cancellationTokens to a…
t-vvidyasaga Aug 26, 2025
44f84ce
fixed comment and updated TestContext format
t-vvidyasaga Aug 26, 2025
f8e6293
updated behavior to throw exception if snapshot name is null
t-vvidyasaga Aug 26, 2025
cdf559b
Moved the exception error messages from inline to ErrorMessages class…
t-vvidyasaga Aug 26, 2025
bded75d
Added Request tracing and case for snapshot reference is registered f…
t-vvidyasaga Aug 27, 2025
7bb3bbe
added test case to test adding snapshot reference to register but not…
t-vvidyasaga Aug 27, 2025
9357dab
updating request tracing to only tracking use of snapshot references
t-vvidyasaga Aug 27, 2025
f8033d9
removing request tracing logic for Snapshot References count
t-vvidyasaga Aug 27, 2025
6b12553
added comments and returning SnapshotReference type from Parse()
t-vvidyasaga Aug 27, 2025
b321889
removed second way of checking for snapshot references type and updat…
t-vvidyasaga Aug 28, 2025
ead364b
removed redundant error message
t-vvidyasaga Aug 28, 2025
712038e
removed unnecessary code
t-vvidyasaga Aug 28, 2025
8779fcf
fixed whitespace issues and made error message clearer
t-vvidyasaga Sep 4, 2025
b24f20c
updating naming and removing old telemetry code
t-vvidyasaga Sep 4, 2025
8aee706
updated namespace and directory to SnapshotReference
t-vvidyasaga Sep 4, 2025
aeb9fb7
Added new JsonFields type
t-vvidyasaga Sep 4, 2025
a96558d
fixed error regarding same namespace and type
t-vvidyasaga Sep 4, 2025
6b47242
Updated the Parse logic to handle all exceptions instead of LoadSnaps…
t-vvidyasaga Sep 5, 2025
003a1b8
updated comments to method
t-vvidyasaga Sep 5, 2025
532fee3
removed the update and reset snapshot reference request tracing metho…
t-vvidyasaga Sep 5, 2025
516009a
removed requestTracing for SnapshotReference from refresh
t-vvidyasaga Sep 5, 2025
d929178
reverting previous change
t-vvidyasaga Sep 5, 2025
fece7d8
Made these keyvault integration tests more resilient
t-vvidyasaga Sep 5, 2025
34019ee
Cleaning up code
t-vvidyasaga Sep 5, 2025
bdb3c15
More nit changes
t-vvidyasaga Sep 5, 2025
a1860a0
updated request tracing for snapshot references in refresh
t-vvidyasaga Sep 5, 2025
fe65c7e
Merge branch 'main' into t-vvidyasagar/snapshot-references
t-vvidyasaga Sep 5, 2025
4d18993
making the key vault integration test more resilient
t-vvidyasaga Sep 5, 2025
ad5ef58
correction to which test needed resilience
t-vvidyasaga Sep 5, 2025
4846750
merge from main
t-vvidyasaga Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//
using Azure;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models;
using Microsoft.Extensions.Diagnostics.HealthChecks;
Expand Down Expand Up @@ -874,6 +875,31 @@ await CallWithRequestTracing(async () =>

foreach (ConfigurationSetting setting in page.Values)
{
if (setting.ContentType == SnapshotReferenceConstants.ContentType)
{
// Track snapshot reference usage for telemetry
if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.UsesSnapshotReference = true;
}

SnapshotReference.SnapshotReference snapshotReference = SnapshotReferenceParser.Parse(setting);

Dictionary<string, ConfigurationSetting> resolvedSettings = await LoadSnapshotData(snapshotReference.SnapshotName, client, cancellationToken).ConfigureAwait(false);

if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.UsesSnapshotReference = false;
}

foreach (KeyValuePair<string, ConfigurationSetting> resolvedSetting in resolvedSettings)
{
data[resolvedSetting.Key] = resolvedSetting.Value;
}

continue;
}

data[setting.Key] = setting;

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

try
{
snapshot = await client.GetSnapshotAsync(loadOption.SnapshotName).ConfigureAwait(false);
}
catch (RequestFailedException rfe) when (rfe.Status == (int)HttpStatusCode.NotFound)
foreach (KeyValuePair<string, ConfigurationSetting> resolvedSetting in resolvedSettings)
{
throw new InvalidOperationException($"Could not find snapshot with name '{loadOption.SnapshotName}'.", rfe);
data[resolvedSetting.Key] = resolvedSetting.Value;
}
}
}

if (snapshot.SnapshotComposition != SnapshotComposition.Key)
{
throw new InvalidOperationException($"{nameof(snapshot.SnapshotComposition)} for the selected snapshot with name '{snapshot.Name}' must be 'key', found '{snapshot.SnapshotComposition}'.");
}
return data;
}

IAsyncEnumerable<ConfigurationSetting> settingsEnumerable = client.GetConfigurationSettingsForSnapshotAsync(
loadOption.SnapshotName,
cancellationToken);
private async Task<Dictionary<string, ConfigurationSetting>> LoadSnapshotData(string snapshotName, ConfigurationClient client, CancellationToken cancellationToken)
{
var resolvedSettings = new Dictionary<string, ConfigurationSetting>();

await CallWithRequestTracing(async () =>
{
await foreach (ConfigurationSetting setting in settingsEnumerable.ConfigureAwait(false))
{
data[setting.Key] = setting;
}
}).ConfigureAwait(false);
}
Debug.Assert(!string.IsNullOrWhiteSpace(snapshotName));

ConfigurationSnapshot snapshot = null;

try
{
await CallWithRequestTracing(async () => snapshot = await client.GetSnapshotAsync(snapshotName, cancellationToken: cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
}
catch (RequestFailedException rfe) when (rfe.Status == (int)HttpStatusCode.NotFound)
{

return data;
return resolvedSettings; // Return empty dictionary if snapshot not found
}

if (snapshot.SnapshotComposition != SnapshotComposition.Key)
{
throw new InvalidOperationException(string.Format(ErrorMessages.SnapshotInvalidComposition, nameof(snapshot.SnapshotComposition), snapshot.Name, snapshot.SnapshotComposition));
}

IAsyncEnumerable<ConfigurationSetting> settingsEnumerable = client.GetConfigurationSettingsForSnapshotAsync(
snapshotName,
cancellationToken);

await CallWithRequestTracing(async () =>
{
await foreach (ConfigurationSetting setting in settingsEnumerable.WithCancellation(cancellationToken).ConfigureAwait(false))
{
resolvedSettings[setting.Key] = setting;
}
}).ConfigureAwait(false);

return resolvedSettings;
}

private async Task<Dictionary<KeyValueIdentifier, ConfigurationSetting>> LoadKeyValuesRegisteredForRefresh(
Expand Down Expand Up @@ -969,7 +1012,33 @@ private async Task<Dictionary<KeyValueIdentifier, ConfigurationSetting>> LoadKey
if (watchedKv != null)
{
watchedIndividualKvs[watchedKeyLabel] = new ConfigurationSetting(watchedKv.Key, watchedKv.Value, watchedKv.Label, watchedKv.ETag);
existingSettings[watchedKey] = watchedKv;

if (watchedKv.ContentType == SnapshotReferenceConstants.ContentType)
{
// Track snapshot reference usage for telemetry
if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.UsesSnapshotReference = true;
}

SnapshotReference.SnapshotReference snapshotReference = SnapshotReferenceParser.Parse(watchedKv);

Dictionary<string, ConfigurationSetting> resolvedSettings = await LoadSnapshotData(snapshotReference.SnapshotName, client, cancellationToken).ConfigureAwait(false);

if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.UsesSnapshotReference = false;
}

foreach (KeyValuePair<string, ConfigurationSetting> resolvedSetting in resolvedSettings)
{
existingSettings[resolvedSetting.Key] = resolvedSetting.Value;
}
}
else
{
existingSettings[watchedKey] = watchedKv;
}
}
}

Expand Down Expand Up @@ -1034,7 +1103,8 @@ await CallWithRequestTracing(
logInfoBuilder.AppendLine(LogHelper.BuildKeyValueSettingUpdatedMessage(change.Key));
keyValueChanges.Add(change);

if (kvWatcher.RefreshAll)
// If the watcher is set to refresh all, or the content type matches the snapshot reference content type then refresh all
if (kvWatcher.RefreshAll || watchedKv.ContentType == SnapshotReferenceConstants.ContentType)
{
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ internal class ErrorMessages
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.";
public const string SnapshotReferenceInvalidFormat = "Invalid snapshot reference format for key '{0}' (label: '{1}').";
public const string SnapshotReferenceInvalidJsonProperty = "Invalid snapshot reference format for key '{0}' (label: '{1}'). The '{2}' property must be a string value, but found {3}.";
public const string SnapshotReferencePropertyMissing = "Invalid snapshot reference format for key '{0}' (label: '{1}'). The '{2}' property is required.";
public const string SnapshotInvalidComposition = "{0} for the selected snapshot with name '{1}' must be 'key', found '{2}'.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal class RequestTracingConstants
public const string LoadBalancingEnabledTag = "LB";
public const string AIConfigurationTag = "AI";
public const string AIChatCompletionConfigurationTag = "AICC";

public const string SnapshotReferenceTag = "SnapshotRef";
public const string SignalRUsedTag = "SignalR";
public const string FailoverRequestTag = "Failover";
public const string PushRefreshTag = "PushRefresh";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference;
using System.Net.Mime;
using System.Text;

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

/// <summary>
/// Flag to indicate whether any key-value uses snapshot references.
/// </summary>
public bool UsesSnapshotReference { get; set; } = false;

/// <summary>
/// Resets the AI configuration tracing flags.
/// </summary>
Expand Down Expand Up @@ -125,7 +131,8 @@ public bool UsesAnyTracingFeature()
return IsLoadBalancingEnabled ||
IsSignalRUsed ||
UsesAIConfiguration ||
UsesAIChatCompletionConfiguration;
UsesAIChatCompletionConfiguration ||
UsesSnapshotReference;
}

/// <summary>
Expand Down Expand Up @@ -176,6 +183,16 @@ public string CreateFeaturesString()
sb.Append(RequestTracingConstants.AIChatCompletionConfigurationTag);
}

if (UsesSnapshotReference)
{
if (sb.Length > 0)
{
sb.Append(RequestTracingConstants.Delimiter);
}

sb.Append(RequestTracingConstants.SnapshotReferenceTag);
}

return sb.ToString();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference
{
internal static class JsonFields
{
public const string SnapshotName = "snapshot_name";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference
{
internal class SnapshotReference
{
public string SnapshotName { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference
{
internal class SnapshotReferenceConstants
{
public const string ContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Azure.Data.AppConfiguration;
using System;
using System.Text.Json;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference
{
/// <summary>
/// Provides parsing functionality for snapshot reference configuration settings.
/// </summary>
internal static class SnapshotReferenceParser
{
/// <summary>
/// Parses a snapshot name from a configuration setting containing snapshot reference JSON.
/// </summary>
/// <param name="setting">The configuration setting containing the snapshot reference JSON.</param>
/// <returns>The snapshot reference containing a valid, non-empty snapshot name.</returns>
/// <exception cref="ArgumentNullException">Thrown when the setting is null.</exception>
/// <exception cref="FormatException">Thrown when the setting contains invalid JSON, invalid snapshot reference format, or empty/whitespace snapshot name.</exception>
public static SnapshotReference Parse(ConfigurationSetting setting)
{
if (setting == null)
{
throw new ArgumentNullException(nameof(setting));
}

if (string.IsNullOrWhiteSpace(setting.Value))
{
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidFormat, setting.Key, setting.Label));
}

try
{
var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(setting.Value));

if (reader.Read() && reader.TokenType != JsonTokenType.StartObject)
{
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidFormat, setting.Key, setting.Label));
}

while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
if (reader.TokenType != JsonTokenType.PropertyName)
{
continue;
}

if (reader.GetString() == JsonFields.SnapshotName)
{
if (reader.Read() && reader.TokenType == JsonTokenType.String)
{
string snapshotName = reader.GetString();
if (string.IsNullOrWhiteSpace(snapshotName))
{
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidFormat, setting.Key, setting.Label));
}

return new SnapshotReference { SnapshotName = snapshotName };
}

throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidJsonProperty, setting.Key, setting.Label, JsonFields.SnapshotName, reader.TokenType));
}

// Skip unknown properties
reader.Skip();
}

throw new FormatException(string.Format(ErrorMessages.SnapshotReferencePropertyMissing, setting.Key, setting.Label, JsonFields.SnapshotName));
}
catch (JsonException jsonEx)
{
throw new FormatException(string.Format(ErrorMessages.SnapshotReferenceInvalidFormat, setting.Key, setting.Label), jsonEx);
}
}
}
}
Loading
Loading