.NET DevOps Feature Flags Azure CI/CD Arquitetura

Feature flags no .NET com Microsoft.FeatureManagement: deploy seguro e rollout gradual

Guia completo de feature flags no .NET: Microsoft.FeatureManagement, targeting por usuário/tenant/percentual, feature flags remotas com Azure App.

N
Neryx Digital Architects
7 de novembro de 2025
12 min de leitura
220 profissionais leram
Categoria: Arquitetura Público: Times de plataforma e operação Etapa: Decisão

Feature flags (ou feature toggles) permitem que você faça deploy de código sem ativar funcionalidades, controle quem vê o quê e faça rollback instantâneo de uma feature sem novo deploy. São a base para trunk-based development, canary releases e A/B testing em ambientes .NET de produção.

Por que feature flags importam em produção

Sem feature flags, o fluxo comum é: branch de feature → PR longo → merge → deploy → torcer para não quebrar. Com feature flags:

  • Desenvolvedores fazem merge diariamente para main (trunk-based development)
  • Código novo está em produção, mas invisível para usuários
  • Ativação gradual: 1% → 10% → 50% → 100% dos usuários
  • Rollback em segundos — mude uma configuração, não faça redeploy
  • A/B testing com grupos específicos de usuários

Setup inicial: Microsoft.FeatureManagement

dotnet add package Microsoft.FeatureManagement.AspNetCore
// Program.cs
builder.Services.AddFeatureManagement();

// Com filtros customizados
builder.Services.AddFeatureManagement()
    .AddFeatureFilter<PercentageFilter>()       // % de usuários
    .AddFeatureFilter<TimeWindowFilter>()        // Janela de tempo
    .AddFeatureFilter<TargetingFilter>()         // Usuários/grupos específicos
    .AddFeatureFilter<TenantFeatureFilter>();    // Filtro customizado por tenant
// appsettings.json
{
  "FeatureManagement": {
    "NewDashboard": true,
    "ExperimentalSearch": false,
    "BetaCheckout": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": 10
          }
        }
      ]
    },
    "MaintenanceMode": {
      "EnabledFor": [
        {
          "Name": "TimeWindow",
          "Parameters": {
            "Start": "2026-05-01T02:00:00+00:00",
            "End": "2026-05-01T04:00:00+00:00"
          }
        }
      ]
    }
  }
}

Constantes de flags: evite strings mágicas

// src/Application/Features/FeatureFlags.cs
public static class FeatureFlags
{
    public const string NewDashboard = "NewDashboard";
    public const string ExperimentalSearch = "ExperimentalSearch";
    public const string BetaCheckout = "BetaCheckout";
    public const string MaintenanceMode = "MaintenanceMode";
    public const string NewPricingEngine = "NewPricingEngine";
    public const string AIRecommendations = "AIRecommendations";
}

Usando feature flags no código

Em controllers e Minimal APIs

// Controller
[ApiController]
[Route("api/[controller]")]
public class SearchController : ControllerBase
{
    private readonly IFeatureManager _featureManager;
    private readonly ISearchService _legacySearch;
    private readonly IExperimentalSearchService _experimentalSearch;

    public SearchController(
        IFeatureManager featureManager,
        ISearchService legacySearch,
        IExperimentalSearchService experimentalSearch)
    {
        _featureManager = featureManager;
        _legacySearch = legacySearch;
        _experimentalSearch = experimentalSearch;
    }

    [HttpGet]
    public async Task<IActionResult> Search([FromQuery] string query)
    {
        if (await _featureManager.IsEnabledAsync(FeatureFlags.ExperimentalSearch))
        {
            var result = await _experimentalSearch.SearchAsync(query);
            return Ok(result);
        }

        var legacyResult = await _legacySearch.SearchAsync(query);
        return Ok(legacyResult);
    }
}

// Minimal API
app.MapGet("/api/checkout", async (IFeatureManager features, ICheckoutService checkout) =>
{
    if (await features.IsEnabledAsync(FeatureFlags.BetaCheckout))
        return Results.Ok(await checkout.ProcessBetaAsync());

    return Results.Ok(await checkout.ProcessAsync());
});

Atributo de controller inteiro

// Bloqueia acesso ao controller inteiro se a flag estiver desabilitada
// Retorna 404 por padrão (configurável)
[FeatureGate(FeatureFlags.NewDashboard)]
[ApiController]
[Route("api/dashboard/v2")]
public class NewDashboardController : ControllerBase
{
    // Este controller só é acessível se NewDashboard == true
}

Em serviços de domínio

public class PricingService : IPricingService
{
    private readonly IFeatureManager _featureManager;
    private readonly ILegacyPricingEngine _legacyEngine;
    private readonly INewPricingEngine _newEngine;

    public PricingService(
        IFeatureManager featureManager,
        ILegacyPricingEngine legacyEngine,
        INewPricingEngine newEngine)
    {
        _featureManager = featureManager;
        _legacyEngine = legacyEngine;
        _newEngine = newEngine;
    }

    public async Task<decimal> CalculatePriceAsync(Product product, Customer customer)
    {
        if (await _featureManager.IsEnabledAsync(FeatureFlags.NewPricingEngine))
        {
            // Novo motor de preço (em rollout gradual)
            return await _newEngine.CalculateAsync(product, customer);
        }

        // Motor legado (comportamento padrão)
        return await _legacyEngine.CalculateAsync(product, customer);
    }
}

Em Razor Pages e Blazor

@inject IFeatureManager FeatureManager

@if (await FeatureManager.IsEnabledAsync(FeatureFlags.NewDashboard))
{
    <NewDashboardComponent />
}
else
{
    <LegacyDashboardComponent />
}

<!-- Tag helper (abordagem alternativa mais limpa) -->
<feature name="@FeatureFlags.AIRecommendations">
    <AIRecommendationsSection />
</feature>

Targeting: rollout para usuários e grupos específicos

// appsettings.json
{
  "FeatureManagement": {
    "BetaCheckout": {
      "EnabledFor": [
        {
          "Name": "Targeting",
          "Parameters": {
            "Audience": {
              "Users": [
                "danilo@neryx.com.br",
                "admin@empresa.com"
              ],
              "Groups": [
                {
                  "Name": "beta-testers",
                  "RolloutPercentage": 100
                },
                {
                  "Name": "All",
                  "RolloutPercentage": 5
                }
              ],
              "DefaultRolloutPercentage": 5
            }
          }
        }
      ]
    }
  }
}
// Registrar o contexto de targeting com o usuário atual
public class HttpContextTargetingContextAccessor : ITargetingContextAccessor
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public HttpContextTargetingContextAccessor(IHttpContextAccessor httpContextAccessor)
        => _httpContextAccessor = httpContextAccessor;

    public ValueTask<TargetingContext> GetContextAsync()
    {
        var user = _httpContextAccessor.HttpContext?.User;

        var userId = user?.FindFirst(ClaimTypes.Email)?.Value
            ?? user?.FindFirst(ClaimTypes.NameIdentifier)?.Value
            ?? "anonymous";

        // Grupos: roles do usuário + grupos customizados
        var groups = new List<string>();

        if (user?.IsInRole("BetaTester") == true)
            groups.Add("beta-testers");

        if (user?.IsInRole("Premium") == true)
            groups.Add("premium-users");

        return new ValueTask<TargetingContext>(new TargetingContext
        {
            UserId = userId,
            Groups = groups
        });
    }
}

// Registrar no DI
builder.Services.AddSingleton<ITargetingContextAccessor, HttpContextTargetingContextAccessor>();

Filtro customizado por tenant (multi-tenancy)

// Filtro que habilita feature para tenants específicos
[FilterAlias("Tenant")]
public class TenantFeatureFilter : IFeatureFilter
{
    private readonly ITenantContext _tenantContext;

    public TenantFeatureFilter(ITenantContext tenantContext)
        => _tenantContext = tenantContext;

    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
    {
        var settings = context.Parameters.Get<TenantFilterSettings>()
            ?? new TenantFilterSettings();

        var currentTenant = _tenantContext.Current?.Id;

        if (currentTenant is null)
            return Task.FromResult(false);

        // Habilitar para tenants específicos ou plano premium
        var isEnabled = settings.TenantIds?.Contains(currentTenant) == true
            || (settings.EnableForPremium && _tenantContext.Current?.Plan == "Premium");

        return Task.FromResult(isEnabled);
    }
}

public class TenantFilterSettings
{
    public string[]? TenantIds { get; set; }
    public bool EnableForPremium { get; set; }
}
// Configuração do filtro por tenant
{
  "FeatureManagement": {
    "AIRecommendations": {
      "EnabledFor": [
        {
          "Name": "Tenant",
          "Parameters": {
            "EnableForPremium": true,
            "TenantIds": ["tenant-abc", "tenant-xyz"]
          }
        }
      ]
    }
  }
}

Feature flags remotas com Azure App Configuration

O ponto crítico de feature flags é poder mudá-las sem redeploy. Para isso, use Azure App Configuration como fonte de verdade:

dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore
dotnet add package Microsoft.FeatureManagement
// Program.cs — integração com Azure App Configuration
builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(builder.Configuration["AzureAppConfig:ConnectionString"])
        // Recarrega configuração a cada 30 segundos
        .ConfigureRefresh(refresh =>
        {
            refresh.Register("FeatureManagement:Sentinel", refreshAll: true)
                   .SetCacheExpiration(TimeSpan.FromSeconds(30));
        })
        // Carrega feature flags do App Configuration
        .UseFeatureFlags(featureFlagOptions =>
        {
            featureFlagOptions.CacheExpirationInterval = TimeSpan.FromSeconds(30);
        });
});

builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement();

// Middleware para refresh automático
app.UseAzureAppConfiguration();

Com isso, você muda uma feature flag no portal do Azure (ou via CLI) e em até 30 segundos todos os pods em produção refletem a mudança — sem redeploy, sem downtime.

# Ativar feature via CLI do Azure
az appconfig feature enable \
  --name myapp-config \
  --feature BetaCheckout \
  --yes

# Rollout para 25% via CLI
az appconfig feature filter add \
  --name myapp-config \
  --feature BetaCheckout \
  --filter-name Percentage \
  --filter-parameters "Value=25"

# Rollback instantâneo
az appconfig feature disable \
  --name myapp-config \
  --feature BetaCheckout

Testando código com feature flags

// Teste com flag habilitada
public class PricingServiceTests
{
    [Fact]
    public async Task CalculatePrice_WithNewEngine_UsesNewAlgorithm()
    {
        // Arrange — simular flag habilitada
        var featureManager = new Mock<IFeatureManager>();
        featureManager
            .Setup(f => f.IsEnabledAsync(FeatureFlags.NewPricingEngine))
            .ReturnsAsync(true);

        var newEngine = new Mock<INewPricingEngine>();
        newEngine
            .Setup(e => e.CalculateAsync(It.IsAny<Product>(), It.IsAny<Customer>()))
            .ReturnsAsync(99.90m);

        var service = new PricingService(featureManager.Object, /* legacy mock */, newEngine.Object);

        // Act
        var price = await service.CalculatePriceAsync(new Product(), new Customer());

        // Assert
        price.Should().Be(99.90m);
        newEngine.Verify(e => e.CalculateAsync(It.IsAny<Product>(), It.IsAny<Customer>()), Times.Once);
    }

    [Fact]
    public async Task CalculatePrice_WithLegacyEngine_UsesLegacyAlgorithm()
    {
        // Arrange — simular flag desabilitada
        var featureManager = new Mock<IFeatureManager>();
        featureManager
            .Setup(f => f.IsEnabledAsync(FeatureFlags.NewPricingEngine))
            .ReturnsAsync(false);  // <-- flag off

        // ...verificar que legacyEngine foi chamado
    }
}

// Teste de integração com flag configurada no WebApplicationFactory
public class CheckoutApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public CheckoutApiTests(WebApplicationFactory<Program> factory)
        => _factory = factory;

    [Fact]
    public async Task BetaCheckout_WhenFlagEnabled_UsesNewFlow()
    {
        var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureAppConfiguration((ctx, config) =>
            {
                config.AddInMemoryCollection(new Dictionary<string, string>
                {
                    ["FeatureManagement:BetaCheckout"] = "true"
                });
            });
        }).CreateClient();

        var response = await client.PostAsJsonAsync("/api/checkout", new { });
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        // Verificar que resposta é do fluxo beta...
    }
}

Limpeza de flags obsoletas

Feature flags têm um ciclo de vida. Flags esquecidas geram dívida técnica. Adote um processo de limpeza:

// Documentar o ciclo de vida da flag em código
/// <summary>
/// Flag de rollout do novo motor de preços.
/// Status: Em rollout (30% dos usuários) — Criada: 2026-04-01
/// Remover após: Rollout completo em 2026-05-01
/// Owner: time-pricing@neryx.com.br
/// </summary>
public const string NewPricingEngine = "NewPricingEngine";

Crie um backlog de limpeza: quando uma flag chegar a 100% ou for descontinuada, abra uma tarefa para remover o código condicional e a própria flag. Flags permanentes são dívida técnica inevitável — minimize-as.

Padrão de decisão

Cenário Tipo de flag
Nova feature sendo desenvolvida Release toggle (temporária)
Rollout gradual para % de usuários Percentage + Targeting
Funcionalidade paga/premium Permission toggle (permanente por plano)
A/B testing Experiment toggle (temporária)
Manutenção programada TimeWindow (automática)
Kill switch de emergência Ops toggle (Azure App Config remote)

Feature flags são infraestrutura de entrega de software — não apenas uma técnica de desenvolvimento. Se você quer implementar trunk-based development e deploys seguros na sua equipe, fale com a Neryx.

Quer sair do modo reativo e priorizar o que mais importa?

O diagnóstico de maturidade ajuda a transformar sintomas operacionais em um plano mais claro de evolução.

Avaliar maturidade

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.