microsoft / FeatureManagement-Dotnet

Microsoft.FeatureManagement provides standardized APIs for enabling feature flags within applications. Utilize this library to secure a consistent experience when developing applications that use patterns such as beta access, rollout, dark deployments, and more.
MIT License
1.05k stars 114 forks source link

GetVariantAsync always returns null when loading from Azure App Configuration, works with appsettings.json #497

Closed AnyhowStep closed 1 month ago

AnyhowStep commented 1 month ago

When I use IsEnabledAsync(), it works as expected. If the flag is enabled, IsEnabledAsync returns true. If the flag is disabled, IsEnabledAsync returns false.

But I cannot get GetVariantAsync() to return a non-null value, no matter what I do.

Below is my config, code, expected output, and actual output.

Feature flag FLAG_X config,

{
    "id": "FLAG_X",
    "description": "",
    "enabled": true,
    "variants": [
        {
            "name": "A",
            "configuration_value": "A"
        },
        {
            "name": "B",
            "configuration_value": "B"
        },
        {
            "name": "C",
            "configuration_value": "C"
        }
    ],
    "allocation": {
        "percentile": [
            {
                "variant": "A",
                "from": 0,
                "to": 30
            },
            {
                "variant": "B",
                "from": 30,
                "to": 60
            },
            {
                "variant": "C",
                "from": 60,
                "to": 100
            }
        ],
        "default_when_enabled": "A",
        "default_when_disabled": "A"
    },
    "telemetry": {
        "enabled": true
    }
}

Feature Flag FLAG_Y config,

{
    "id": "FLAG_Y",
    "description": "",
    "enabled": true,
    "conditions": {
        "client_filters": []
    }
}

Feature Flag FLAG_Z config,

{
    "id": "FLAG_Z",
    "description": "",
    "enabled": true,
    "variants": [
        {
            "name": "Off",
            "configuration_value": false
        },
        {
            "name": "On",
            "configuration_value": true
        }
    ],
    "allocation": {
        "percentile": [
            {
                "variant": "Off",
                "from": 0,
                "to": 50
            },
            {
                "variant": "On",
                "from": 50,
                "to": 100
            }
        ],
        "default_when_enabled": "Off",
        "default_when_disabled": "Off"
    },
    "telemetry": {
        "enabled": false
    }
}

C# code,


    var builder = WebApplication.CreateBuilder(args);
    var configuration = builder.Configuration
        .AddAzureAppConfiguration(options =>
        {
            options
                .Connect("SOME CONNECTION STRING")
                .UseFeatureFlags();
            builder.Services.AddSingleton(options.GetRefresher());
        })
        .Build();

    IFeatureDefinitionProvider featureDefinitionProvider = new ConfigurationFeatureDefinitionProvider(configuration);

    IVariantFeatureManager featureManager = new FeatureManager(
        featureDefinitionProvider,
        new FeatureManagementOptions());

//...

    var t = new Task(async () =>
    {
        await foreach (var n in featureManager.GetFeatureNamesAsync())
        {
            Log.Information($"n: {n}");
            var variant = await featureManager.GetVariantAsync(n);
            var variant2 = await featureManager.GetVariantAsync(n, new TargetingContext()
            {
                UserId = "TestId",
                Groups = new string[] { },
            });
            var enabled = await featureManager.IsEnabledAsync(n);
            Log.Information($"{n} variant is null: {variant == null}");
            Log.Information($"{n} variant2 is null: {variant2 == null}");
            Log.Information($"{n} enabled: {enabled}");
        }
    });
    t.Start();

Expected output,

[01:26:10 INF] n: FLAG_X
[01:26:10 INF] FLAG_X variant is null: False
[01:26:10 INF] FLAG_X variant2 is null: False
[01:26:10 INF] FLAG_X enabled: True
[01:26:10 INF] n: FLAG_Y
[01:26:10 INF] FLAG_Y variant is null: True
[01:26:10 INF] FLAG_Y variant2 is null: True
[01:26:10 INF] FLAG_Y enabled: True
[01:26:10 INF] n: FLAG_Z
[01:26:10 INF] FLAG_Z variant is null: False
[01:26:10 INF] FLAG_Z variant2 is null: False
[01:26:10 INF] FLAG_Z enabled: True

Actual output,

[01:26:10 INF] n: FLAG_X
[01:26:10 INF] FLAG_X variant is null: True
[01:26:10 INF] FLAG_X variant2 is null: True
[01:26:10 INF] FLAG_X enabled: True
[01:26:10 INF] n: FLAG_Y
[01:26:10 INF] FLAG_Y variant is null: True
[01:26:10 INF] FLAG_Y variant2 is null: True
[01:26:10 INF] FLAG_Y enabled: True
[01:26:10 INF] n: FLAG_Z
[01:26:10 INF] FLAG_Z variant is null: True
[01:26:10 INF] FLAG_Z variant2 is null: True
[01:26:10 INF] FLAG_Z enabled: True
zhiyuanliang-ms commented 1 month ago

Hi, @AnyhowStep Which version of feature management package you are using?

I got the expected output on my side. I tried both DI and non-DI usages.

My code:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.FeatureFilters;

//
// Setup configuration
IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();

//
// DI usage
IServiceCollection services = new ServiceCollection();

services.AddSingleton(configuration)
        .AddFeatureManagement();

//
// Get the feature manager from application services
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
    IVariantFeatureManager featureManager = serviceProvider.GetRequiredService<IVariantFeatureManager>();

    await foreach (var n in featureManager.GetFeatureNamesAsync())
    {
        Console.WriteLine($"n: {n}");
        var variant = await featureManager.GetVariantAsync(n);
        var variant2 = await featureManager.GetVariantAsync(n, new TargetingContext()
        {
            UserId = "TestId",
            Groups = new string[] { },
        });
        var enabled = await featureManager.IsEnabledAsync(n);
        Console.WriteLine($"{n} GetVariant is null: {variant == null}");
        Console.WriteLine($"{n} GetVariant with context is null: {variant2 == null}");
        Console.WriteLine($"{n} enabled: {enabled}");
    }
}

//
// Non-DI usage
var featureDefinitionProvider = new ConfigurationFeatureDefinitionProvider(configuration);

IVariantFeatureManager featureManager2 = new FeatureManager(
    featureDefinitionProvider,
    new FeatureManagementOptions());

await foreach (var n in featureManager2.GetFeatureNamesAsync())
{
    Console.WriteLine($"n: {n}");
    var variant = await featureManager2.GetVariantAsync(n);
    var variant2 = await featureManager2.GetVariantAsync(n, new TargetingContext()
    {
        UserId = "TestId",
        Groups = new string[] { },
    });
    var enabled = await featureManager2.IsEnabledAsync(n);
    Console.WriteLine($"{n} GetVariant is null: {variant == null}");
    Console.WriteLine($"{n} GetVariant with context is null: {variant2 == null}");
    Console.WriteLine($"{n} enabled: {enabled}");
}

Feature flag in appsettings,json:

{
  "feature_management": {
    "feature_flags": [
      {
        "id": "FLAG_X",
        "description": "",
        "enabled": true,
        "variants": [
          {
            "name": "A",
            "configuration_value": "A"
          },
          {
            "name": "B",
            "configuration_value": "B"
          },
          {
            "name": "C",
            "configuration_value": "C"
          }
        ],
        "allocation": {
          "percentile": [
            {
              "variant": "A",
              "from": 0,
              "to": 30
            },
            {
              "variant": "B",
              "from": 30,
              "to": 60
            },
            {
              "variant": "C",
              "from": 60,
              "to": 100
            }
          ],
          "default_when_enabled": "A",
          "default_when_disabled": "A"
        },
        "telemetry": {
          "enabled": true
        }
      },
      {
        "id": "FLAG_Y",
        "description": "",
        "enabled": true,
        "conditions": {
          "client_filters": []
        }
      },
      {
        "id": "FLAG_Z",
        "description": "",
        "enabled": true,
        "variants": [
          {
            "name": "Off",
            "configuration_value": false
          },
          {
            "name": "On",
            "configuration_value": true
          }
        ],
        "allocation": {
          "percentile": [
            {
              "variant": "Off",
              "from": 0,
              "to": 50
            },
            {
              "variant": "On",
              "from": 50,
              "to": 100
            }
          ],
          "default_when_enabled": "Off",
          "default_when_disabled": "Off"
        },
        "telemetry": {
          "enabled": false
        }
      }
    ]
  }
}

My output: image

Could you provide more information so that I can debug for you? Maybe you can put your code in a public github repo, so that I can download it and run it?

AnyhowStep commented 1 month ago

I'm not using appsettings.json. It seems like loading variants from appsettings.json works.

But my current need is using Microsoft.Extensions.Configuration and AddAzureAppConfiguration().

The use-case is trying to load feature flag variants from an Azure App Configuration resource, so that we can adjust allocations and user/group overrides without having to restart the server application.

Could you check if that works for you?

[Edit] I've updated the ticket title to reflect that it works with appsettings.json

zhiyuanliang-ms commented 1 month ago

Hi, @AnyhowStep Sorry for the inconvenience. I think the reason of your issue is that you are not using the latest preview version of Microsoft.Extensions.Configuration.AzureAppConfiguration

At first, I used 7.3.0 and then I get the same output as yours. Then, I switched to 8.0.0-preview.3 and I got the expected result.

Could you try to upgrade the configuration provider pacakge and see whether this issuse still exists?

AnyhowStep commented 1 month ago

Which version of feature management package you are using?

Sorry I never answered. But, yes, I was on 7.3.0 of Microsoft.Extensions.Configuration.AzureAppConfiguration, and 4.0.0-preview4 of Microsoft.FeatureManagement.

And after updating to 8.0.0-preview.3, it works as expected. Thank you for solving this so quickly! =)