Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -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
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -319,12 +320,98 @@ private List<KeyValuePair<string, string>> ProcessMicrosoftSchemaFeatureFlag(Fea
keyValues.Add(new KeyValuePair<string, string>($"{telemetryPath}:{FeatureManagementConstants.Metadata}:{FeatureManagementConstants.ETag}", setting.ETag.ToString()));

keyValues.Add(new KeyValuePair<string, string>($"{telemetryPath}:{FeatureManagementConstants.Enabled}", telemetry.Enabled.ToString()));

if (featureFlag.Allocation != null)
{
string allocationId = CalculateAllocationId(featureFlag);

if (allocationId != null)
{
keyValues.Add(new KeyValuePair<string, string>($"{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<string>();

// 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<FeaturePercentileAllocation> 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<FeatureVariant> 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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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}");
}
}
}
}
Loading
Loading