Como lidar com fusos horários diferentes?

É muito comum na engenharia de software termos a necessidade de trabalhar com data e hora. Praticamente todo sistema utiliza e armazena datas em algum cenário, como por exemplo:

  • Data em que um e-mail foi enviado
  • Data em que um chamado foi finalizado
  • Data de expiração de um acesso
  • Data em que um registro foi criado/alterado/excluído

O problema

Um erro que muitos desenvolvedores cometem é o de armazenar datas sem realizar a conversão para o horário global (UTC). Isso é errado, pois a data só estará correta para os usuários que estiverem no mesmo fuso horário da aplicação, impactando negativamente todos os demais usuários.

Imagine que uma aplicação está hospedada em um servidor em São Paulo e que uma ação foi executada às 23h. Usuários do Amazonas devem visualizar que a ação foi executada às 22h do horário local. A mesma lógica deve ser aplicada para usuários do Acre, que devem ser informados que uma ação ocorreu às 21h do horário local. Se sua aplicação possui clientes fora do país, a lógica é a mesma.

Outro erro que vários desenvolvedores cometem com frequência, é o de obter a data local e subtrair as horas de diferença para o horário global (UTC), conforme exemplificado abaixo:

DateTime hoje = DateTime.Now;
DateTime hojeUtc = DateTime.Now.AddHours(-3);

Isso funciona, mas não em todos os cenários. Quando o país adota o horário de verão, o relógio é adiantado em uma hora, e essa diferença para o horário global diminui na mesma proporção. Como essa subtração é feita na maioria das vezes diretamente no código fonte, é necessário um deploy da aplicação somente para realizar essa alteração.

A solução

Converter uma data para outros fusos não é difícil, desde que a data esteja em horário global (UTC).

Se você possui datas armazenadas e elas estão em data local, sua primeira tarefa deve ser o de realizar a conversão de todas essas datas para o horário global.

Feito isso, basta utilizar o método de extensão abaixo:

public static DateTimeOffset ConverterParaFusoDeBrazilia(this DateTimeOffset dataEmUtc)
{
    //Lista todos os fusos horários instalados na máquina 
    var fusos = TimeZoneInfo.GetSystemTimeZones();

    //Busca pelo fuso que você deseja converter a data. Neste exemplo, o fuso de Brasília 
    var fusoBrasilia = TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time");

    //Data convertida para o fuso desejado
    return TimeZoneInfo.ConvertTime(dataEmUtc, fusoBrasilia);
}

Pelo fato do .NET ser multiplataforma, precisamos garantir que esse código seja executado com sucesso em diferentes sistemas operacionais.

Executando o código com .NET 6

Execução no Windows (.NET 6): Net6-Windows


Execução em Linux (.NET 6): Net6-Linux

Executando o código com .NET 3.1

Execução no Windows (.NET 3.1): Net31-Windows


Execução em Linux (.NET 3.1): Net31-Linux

Nesta última execução tivemos um resultado inesperado: uma exception do tipo TimeZoneNotFoundException nos dizendo que o fuso horário que procuramos não foi encontrado no servidor.

Porque essa exception foi gerada? Como o mesmo código que funcionou no .NET 6 em diferentes sistemas operacionais, não funcionou no .NET 3.1 rodando em Linux? E porque funcionou no Windows?

A pegadinha

Esse erro aconteceu porque há 2 tipos de fuso horário:

  • Fusos fornecidos pela Microsoft para uso no Windows
  • Fusos fornecidos pela entidade IANA para uso em Linux. Esses fusos começaram a ser utilizados pelo .NET quando o .NET Core foi lançado e virou multiplataforma.

Isso trouxe um questionamento para os desenvolvedores sobre qual fuso horário utilizar. Como podemos utilizar fusos Linux se desenvolvemos em ambiente Windows e vice-versa? Como realizar um “de para” entre fusos?

Como vimos durante a execução do código, a partir do .NET 6 essa conversão acontece de forma nativa pelo framework, possibilitando o uso de fusos fornecidos pela Microsoft, mesmo que o código seja hospedado em um ambiente Linux.

Para projetos que ainda não utilizam o .NET 6 e precisam dessa compatibilidade, é necessário instalar um pacote chamado TimeZoneConverter que irá realizar o trabalho para nós.


dotnet add package TimeZoneConverter 

Feito isso, precisaremos substituir o código que busca pelo fuso horário:

public static DateTimeOffset ConverterParaFusoDeBrazilia(this DateTimeOffset dataEmUtc)
{
    //busca pelo fuso que você deseja e converte se necessário      
    var fusoBrasilia = TZConvert.GetTimeZoneInfo("E. South America Standard Time");

    //data convertida para o fuso desejado
    return TimeZoneInfo.ConvertTime(dataEmUtc, fusoBrasilia);
}

Executando o código novamente:

Execução no Windows com TimeZoneConverter (.NET 3.1): Net31-Windows-Tz


Execução em Linux com TimeZoneConverter (.NET 3.1): Net31-Linux-Tz

Conclusão

Saber armazenar e trabalhar de forma correta com datas é essencial na engenharia de software, para que seja possível garantir a mesma experiência para todos os usuários da sua aplicação ao redor do globo.

O .NET, que foi reescrito do zero, virou multiplataforma e está passando por novos desafios que seu antecessor não precisou se preocupar, portanto, precisamos estar cientes das características e comportamentos da versão que nossa aplicação está utilizando, e ficarmos atentos às melhorias que estão constantemente sendo implementadas em novas versões, ao mesmo tempo que avaliamos a viabilidade de realizar o upgrade de versão da aplicação.


Espero que tenham gostado do post!