Implementando feature toogles com Flagr e C#

No artigo anterior nós discutimos o que são feature flags/feature toggles e como eles podem nos ajudar a controlar o impacto de alterações no sistema, bem como a conhecer melhor o comportamento de quem usa nossas soluções.

Agora é hora de você aprender a fazer isso na prática!

Vamos implementar uma aplicação console bastante simples (com um código que você pode reaproveitar facilmente no seu sistema). A única coisa que ela faz é recuperar o valor da flag no Flagr e selecionar uma entre duas features. Caso a flag não esteja configurada para aquela aplicação, utilizo um decorator para avisar o usuário que a flag não está configurada e executar uma feature padrão. Mas primeiro…

Não estresse o seu socket!

Você pode pensar que o modo mais fácil de fazer requisições http é utilizando um HttpClient. E você está correto. Porém, não utilizar instâncias reaproveitáveis do HttpClient pode trazer sérios problemas para a sua aplicação. Esse seria um tema para um outro artigo (e já fizeram um sobre o assunto: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/).

Por isso, para a nossa solução, vamos utilizar uma biblioteca, muito simples, de comunicação http. Ela nos oferece uma interface fluída pra escrita de código, possibilidade de testar as requisições e de quebra, nos dá transparência no controle de instâncias do HttpClient. Estou falando do Flurl. Você pode encontrar maiores detalhes no GitHub do projeto.

Basicamente o Flurl concede superpoderes às suas variáveis strings. Logo, através de interfaces fluentes, você consegue facilmente fazer qualquer requisição a serviços http

var person = await "https://api.com"
    .AppendPathSegment("person")
    .SetQueryParams(new { a = 1, b = 2 })
    .WithOAuthBearerToken("my_oauth_token")
    .PostJsonAsync(new
    {
        first_name = "Claire",
        last_name = "Underwood"
    })
    .ReceiveJson<Person>();

*snippet copiado de: https://flurl.dev/

A estratégia

Basicamente já sabemos que precisamos fazer uma requisição à api do Flagr, enviando um DTO com as informações da flag que nós queremos. E neste mesmo DTO, também devem ir as informações sobre o nosso contexto. Para esta aplicação o contexto será apenas o nome da aplicação – na verdade um marcador textual qualquer. No mundo real, você pode utilizar a interface IConfiguration – e suas múltiplas possibilidades – para obter esse valor.

De posse do response do Flagr, precisamos observar o valor “VariantKey”. Nele está contido o valor que configuramos como Variant e que foi resolvido de acordo com as regras do nosso segmento. Caso o nosso contexto não se encaixe em nenhum segmento, “VariantKey” estará vazio. E ao invés de externar uma exceção, vamos devolver um valor padrão para quem irá resolver qual feature deverá ser executada.

Para atender esse algoritmo, vamos precisar:

  • Uma classe para resolver as flags (FlagResolver)
  • Uma classe que faça a interpretação da flag e devolva a feature desejada (FeatureProvider)
  • Uma forma de identificar a flag que desejamos e seus possíveis retornos. Obviamente você pode utilizar classes para esse controle. Eu optei por utilizar enum pela sua simplicidade (FeatureFlag)
  • Três features: Duas que, de fato, executam o código e uma que implementa um decorator para a feature default.

O FlagResolver

A missão dessa classe é bastante simples: receber o identificador da flag e fazer a consulta na API do Flagr. Não vou listar o código dos request/response. Mas você pode encontrar o código completo no meu github.

    public class FlagResolver : IFlagResolver
    {
        public EvaluationResponse ResolveFlag<T>(object entityContext)
        {
            var request = GetRequest(typeof(T), entityContext);
            return GetFlag(request).Result;
        }
    (...)

O parâmetro genérico será o identificador da chave. Através do nome deste tipo é que nós identificaremos a flag no Flagr. Você poderia utilizar outras estratégias também, como utilizar attributes. Mas daí já é questão de gosto e decisão do time.

O parâmetro entityContext é um objeto contendo os dados de contexto da sua aplicação. Nós estamos recebendo um object porque o entityContext não tem um formato fixo, diferente dos objetos de request e response. E por falar em request:

private EvaluationRequest GetRequest(Type type, object entityContext) =>
    new EvaluationRequest
    {
         FlagKey = type.Name,
         EntityContext = entityContext,
    };

O código que cria o objeto de request apenas se encarrega de adicionar os valores da chave e do contexto. A requisição propriamente dita está no método GetFlag:

private async Task<EvaluationResponse> GetFlag(EvaluationRequest request)
{
    const string url = "http://localhost:18000/api";
    var response = await url
        .ConfigureRequest(settings =>
        {
            var jsonSettings = new JsonSerializerSettings
            {
                NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
                ObjectCreationHandling = ObjectCreationHandling.Replace
            };
            settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
        })
        .AppendPathSegment("v1")
        .AppendPathSegment("evaluation")
        .AllowAnyHttpStatus()
        .PostJsonAsync(request)
        .ReceiveJson<EvaluationResponse>()
        .ConfigureAwait(false);
    return response;
}

Os pontos de atenção desse código são

  • ConfigureRequest: as configurações feitas nesse método servem APENAS para esta requisição, não alterando as demais.
  • ReceiveJson<T>: Caso não seja possível converter o json recebido em T ou o conteúdo do response não seja json, uma exceção será lançada
  • AllowAnyHttpStatus: Quando esse método está presente na cadeia de chamadas, o Flurl não irá emitir uma exceção caso a API retorne um status code de erro. No nosso caso, não estamos utilizando essa opção. Ou seja: se o flagr estiver fora do ar, nós teremos uma exception.

FeatureProvider

Iniciamos nossa classe, carregando em um dictionary os possíveis retornos de flags do Flagr e funções call-back, que atual como fábricas dos objetos. Novamente, optei pelo caminho mais simples nesse ponto, principalmente porque não estou usando os injetores de dependência.

public FeatureProvider(IFlagResolver flagrResolver, string applicationName)
{
    _flagrResolver = flagrResolver;
    _features = new Dictionary<FeatureFlag, Func<IMyFeature>>
    {
        { FeatureFlag.Unknow, () => new FeatureUnknow(new FeatureOne()) },
        { FeatureFlag.Feature1, () => new FeatureOne() },
        { FeatureFlag.Feature2, () => new FeatureTwo() },
    };
    _applicationName = applicationName;
}

Unknow é o tipo retornado quando a pesquisa de contexto no Flagr não atende nenhum requisito. Neste caso, retorno a feature FeatureOne(), porém não sem adicionar um decorator que, no mundo real, poderia ser o responsável por logar o fato de não ter sido encontrado a flag para aquele contexto.

public IMyFeature GetFeature()
{
    var entityContext = new
    {
        ApplicationName = _applicationName,
    };
    
    var response = _flagrResolver.ResolveFlag<FeatureFlag>(entityContext);
    var flag = ParseFlag(response);

    var getFlag = _features.GetValueOrDefault(flag);
    return getFlag();
}

O método público GetFeature() começa por criar o contexto da aplicação. Como é um objeto sem interface definida, um anonymous object é suficiente. ResolveFlag retorna um response que é traduzido em um enum correspondente a feature configurada. E por fim, obtemos o método que instanciará as classes correspondentes à flag. Finalmente, temos o ponto de entrada do programa, onde a mágica acontece e ninguém vê:

public static void Main()
{
    Console.WriteLine("What feature is active?");
    IFlagResolver flagResolver = new FlagResolver();
    IFeatureProvider featureProvider = new FeatureProvider(flagResolver, "app1");
    var feature = featureProvider.GetFeature();
    feature.Execute();
}

Em negrito está o ponto em que definimos parte do contexto da nossa flag. Repito que essa informação poderia ser construída de forma dinâmica, com base em dados do usuário logado ou de arquivos de configuração, variáveis de ambiente e assim por diante.

As possíveis saídas, no console, para esta aplicação seriam:

Caso a entrada seja “app1”, o sistema escreve no terminal “You choose the Feature One”
Caso a entrada seja “app2”, o sistema escreve no terminal: “You choose the Feature Two”
Caso a entrada não tenha correspondência no Flagr, o sistema informa que
a aplicação não possui flag configurada e escreve o texto de app1

Bastante fácil, não é mesmo?

Palavras finais

Duas principais características de algoritmos como esse: Precisa ser simples, principalmente porque a tendência dele é ser apagado em breve do sistema e você não quer gastar muito tempo em um código que será apagado. Precisa ser reutilizável, afinal não há como saber qual será o próximo sistema a precisar do Flagr.

O código, que está disponível para download, ainda tem alguns problemas a serem resolvidos. Como o parsing do response para o valor de enum correto. Aliás algumas pessoas sequer gostam de incluir Enums em seus códigos. Portanto, pode ser uma dependência a ser retirada. Outro ponto importante é que você pode querer implementar algum mecanismo de retry (quem sabe usando o Polly?) além de um cache para a flag. Você não vai querer sempre pesquisar a flag e também não vai querer que o seu valor seja atualizado apenas quando a aplicação reiniciar.

Preciso frisar também que a Microsoft já possui também uma estratégia de feature toggle. Mas até onde li, é baseada em arquivos de configuração. Até a data da publicação desse artigo, desconhecia qualquer lib ou provider que contenha implementação do Flagr. Por isso essa abordagem mais “manual”. Quem sabe, talvez, a sua não seja a primeira?

Um grande abraço, pessoas, e até breve!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.