Como lidar elegantemente com fusos horários

140

Eu tenho um site hospedado em um fuso horário diferente dos usuários que usam o aplicativo. Além disso, os usuários podem ter um fuso horário específico. Eu queria saber como outros usuários e aplicativos de SO abordam isso? A parte mais óbvia é que, dentro do banco de dados, as datas / horas são armazenadas no UTC. Quando no servidor, todas as datas / horas devem ser tratadas no UTC. No entanto, vejo três problemas que estou tentando superar:

  1. Obtendo a hora atual no UTC (resolvida facilmente com DateTime.UtcNow).

  2. Puxando data / hora do banco de dados e exibindo-as ao usuário. Há potencialmente muitas chamadas para imprimir datas em diferentes visualizações. Eu estava pensando em alguma camada entre a visualização e os controladores que poderiam resolver esse problema. Ou usando um método de extensão personalizado DateTime(veja abaixo). A principal desvantagem é que, em todo local do uso de data e hora em uma exibição, o método de extensão deve ser chamado!

    Isso também dificultaria o uso de algo como o JsonResult. Você não podia mais ligar facilmente Json(myEnumerable), teria que ser Json(myEnumerable.Select(transformAllDates)). Talvez o AutoMapper possa ajudar nessa situação?

  3. Obtendo entrada do usuário (Local para UTC). Por exemplo, POSTAR um formulário com uma data exigiria a conversão da data para UTC antes. A primeira coisa que vem à mente é criar um costume ModelBinder.

Aqui estão as extensões que pensei em usar nas visualizações:

public static class DateTimeExtensions
{
    public static DateTime UtcToLocal(this DateTime source, 
        TimeZoneInfo localTimeZone)
    {
        return TimeZoneInfo.ConvertTimeFromUtc(source, localTimeZone);
    }

    public static DateTime LocalToUtc(this DateTime source, 
        TimeZoneInfo localTimeZone)
    {
        source = DateTime.SpecifyKind(source, DateTimeKind.Unspecified);
        return TimeZoneInfo.ConvertTimeToUtc(source, localTimeZone);
    }
}

Eu acho que lidar com fusos horários seria algo tão comum, considerando que muitos aplicativos agora são baseados em nuvem, onde a hora local do servidor pode ser muito diferente do fuso horário esperado.

Isso já foi resolvido com elegância antes? Falta alguma coisa? Idéias e pensamentos são muito apreciados.

EDIT: Para esclarecer algumas confusões, pensei em adicionar mais alguns detalhes. O problema agora não é como armazenar os horários UTC no banco de dados, é mais sobre o processo de ir de UTC-> Local e Local-> UTC. Como o @Max Zerbini aponta, é obviamente inteligente colocar o código UTC-> Local na visualização, mas usar DateTimeExtensionsa resposta realmente? Ao obter informações do usuário, faz sentido aceitar datas como a hora local do usuário (já que isso seria o que JS usaria) e depois usar a ModelBinderpara transformar em UTC? O fuso horário do usuário é armazenado no banco de dados e é facilmente recuperado.

TheCloudlessSky
fonte
3
Você pode primeiro ter uma leitura através deste excelente post ... O horário de verão e fuso horário melhores práticas
dodgy_coder
@dodgy_coder - Esse sempre foi um ótimo link de recurso para fusos horários. No entanto, ele realmente não resolve nenhum dos meus problemas (especificamente referente ao MVC). Obrigado embora.
TheCloudlessSky
code.google.com/p/noda-time pode ser útil
Arnis Lapsa 28/09
Curioso em qual solução você optou. Enfrentando uma decisão semelhante. Boa pergunta.
22412 Sean
@Sean - A solução até agora não foi elegante (e é por isso que ainda não aceitei uma resposta). Tem havido muita sobrecarga manual de imprimir datas / horas e convertê-las manualmente de volta com um ModelBinder.
TheCloudlessSky

Respostas:

106

Não que isso seja uma recomendação, é mais o compartilhamento de um paradigma, mas a maneira mais agressiva que eu já vi de lidar com informações de fuso horário em um aplicativo Web (que não é exclusivo do ASP.NET MVC) foi a seguinte:

  • Todos os horários no servidor são UTC. Isso significa usar, como você disse DateTime.UtcNow,.

  • Tente confiar no cliente que passa as datas para o servidor o mínimo possível. Por exemplo, se você precisar de "agora", não crie uma data no cliente e passe-a para o servidor. Crie uma data no seu GET e passe-a para o ViewModel ou no POST DateTime.UtcNow.

Até agora, tarifa bastante padrão, mas é aqui que as coisas ficam 'interessantes'.

  • Se você precisar aceitar uma data do cliente, use o javascript para garantir que os dados que você está postando no servidor estejam no UTC. O cliente sabe em que fuso horário está e, com precisão razoável, pode converter os horários em UTC.

  • Ao renderizar visualizações, eles estavam usando o <time>elemento HTML5 , nunca renderizavam as datas diretamente no ViewModel. Foi implementado como HtmlHelperextensão, algo assim Html.Time(Model.when). Seria render <time datetime='[utctime]' data-date-format='[datetimeformat]'></time>.

    Eles usariam o javascript para traduzir a hora UTC na hora local do cliente. O script encontraria todos os <time>elementos e usaria a date-formatpropriedade data para formatar a data e preencher o conteúdo do elemento.

Dessa forma, eles nunca precisavam acompanhar, armazenar ou gerenciar o fuso horário de um cliente. O servidor não se importava em qual fuso horário o cliente estava, nem precisava fazer nenhuma conversão de fuso horário. Simplesmente cuspiu o UTC e deixou o cliente converter isso em algo razoável. O que é fácil no navegador, porque ele sabe em que fuso horário está. Se o cliente alterasse seu fuso horário, o aplicativo da Web se atualizaria automaticamente. A única coisa que eles armazenaram foi a sequência do formato datetime para o código do idioma do usuário.

Não estou dizendo que foi a melhor abordagem, mas era diferente que eu nunca tinha visto antes. Talvez você obtenha algumas idéias interessantes.

J. Holmes
fonte
Obrigado pela sua resposta. Ao obter a entrada de data do usuário (por exemplo, agendar um compromisso), essa entrada não é assumida no fuso horário atual? O que você acha do ModelBinder fazendo essa transformação antes de entrar na ação (veja meus comentários em @casperOne. Além disso, a <time>ideia é bem legal. A única desvantagem é que isso significa que eu preciso consultar todo o DOM para esses elementos, o que não é exatamente agradável apenas para transformar datas (e o JS desativado?)
.Obrigado
Geralmente, sim, a suposição é que estaria em um fuso horário local. Mas eles fizeram questão de garantir que, sempre que coletassem uma hora de um usuário, ela fosse transformada em UTC usando javascript antes de ser enviada ao servidor. A solução deles era muito pesada para o JS e não se degradava muito bem quando o JS estava desativado. Não pensei nisso o suficiente para ver se há algo inteligente nisso. Mas como eu disse, talvez isso lhe dê algumas idéias.
J. Holmes
2
Desde então, trabalhei em outro projeto que precisava de datas locais e esse foi o método que usei. Estou usando o moment.js para fazer a formatação de data / hora ... é legal . Obrigado!
TheCloudlessSky
Não existe uma maneira simples de definir no app.config / web.cofig do aplicativo um fuso horário do escopo do aplicativo e transformá-lo em todos os valores DateTime.Now em DateTime.UtcNow? (Eu quero evitar situações em que os programadores da empresa ainda usaria DateTime.Now por engano)
Uri Abramson
3
Uma desvantagem dessa abordagem que eu posso ver é quando você está usando o tempo para outras coisas, criando PDFs, e-mails e outras coisas, não há elemento de tempo para elas, então você ainda precisará convertê-las manualmente. Caso contrário, solução muito elegante
shenku
15

Depois de vários feedbacks, eis a minha solução final, que eu acho que é limpa e simples e cobre problemas de horário de verão.

1 - Lidamos com a conversão no nível do modelo. Então, na classe Model, escrevemos:

    public class Quote
    {
        ...
        public DateTime DateCreated
        {
            get { return CRM.Global.ToLocalTime(_DateCreated); }
            set { _DateCreated = value.ToUniversalTime(); }
        }
        private DateTime _DateCreated { get; set; }
        ...
    }

2 - Em um auxiliar global, criamos nossa função personalizada "ToLocalTime":

    public static DateTime ToLocalTime(DateTime utcDate)
    {
        var localTimeZoneId = "China Standard Time";
        var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById(localTimeZoneId);
        var localTime = TimeZoneInfo.ConvertTimeFromUtc(utcDate, localTimeZone);
        return localTime;
    }

3 - Podemos melhorar ainda mais, salvando o ID do fuso horário em cada perfil de usuário para que possamos recuperar da classe de usuário em vez de usar a constante "Hora padrão da China":

public class Contact
{
    ...
    public string TimeZone { get; set; }
    ...
}

4 - Aqui podemos obter a lista de fuso horário para mostrar ao usuário para selecionar em uma caixa suspensa:

public class ListHelper
{
    public IEnumerable<SelectListItem> GetTimeZoneList()
    {
        var list = from tz in TimeZoneInfo.GetSystemTimeZones()
                   select new SelectListItem { Value = tz.Id, Text = tz.DisplayName };

        return list;
    }
}

Então, agora às 9:25 na China, site hospedado nos EUA, data salva no UTC no banco de dados, eis o resultado final:

5/9/2013 6:25:58 PM (Server - in USA) 
5/10/2013 1:25:58 AM (Database - Converted UTC)
5/10/2013 9:25:58 AM (Local - in China)

EDITAR

Agradeço a Matt Johnson por apontar as partes fracas da solução original e desculpe-me por excluir a postagem original, mas houve problemas ao obter o formato correto de exibição de código ... o editor tem problemas em misturar "marcadores" com "pré-código", então eu removeu os bulbos e foi ok.

Nestor
fonte
Parece ser viável se alguém não possui arquitetura de n camadas ou as bibliotecas da Web são compartilhadas. Como podemos compartilhar a identificação de fuso horário na camada de dados.
Vikash Kumar
9

Na seção de eventos no sf4answers , os usuários inserem um endereço para um evento, uma data de início e uma data de término opcional. Esses horários são convertidos em um datetimeoffsetservidor SQL que responde pelo deslocamento do UTC.

Esse é o mesmo problema que você está enfrentando (embora você esteja adotando uma abordagem diferente, pois está usando DateTime.UtcNow); você tem um local e precisa traduzir um horário de um fuso horário para outro.

Há duas coisas principais que fiz que funcionaram para mim. Primeiro, use a DateTimeOffsetestrutura , sempre. É responsável pelo deslocamento da UTC e, se você pode obter essas informações do seu cliente, facilita a sua vida.

Segundo, ao executar as traduções, supondo que você conheça o local / fuso horário em que o cliente está, você pode usar o banco de dados de fuso horário público para converter um horário do UTC para outro fuso horário (ou triangular, se preferir, entre dois fusos horários). O melhor do banco de dados tz (às vezes chamado de banco de dados Olson ) é que ele explica as alterações nos fusos horários ao longo da história; obter uma compensação é uma função da data em que você deseja obter a compensação (basta olhar para o Energy Policy Act de 2005, que alterou as datas em que o horário de verão entra em vigor nos EUA ).

Com o banco de dados em mãos, você pode usar a API .NET do ZoneInfo (banco de dados tz / banco de dados Olson) . Observe que não há uma distribuição binária, você precisará baixar a versão mais recente e compilá-la.

No momento da redação deste artigo, ele atualmente analisa todos os arquivos na distribuição de dados mais recente (na verdade, eu o executei no arquivo ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz em 25 de setembro de 2011; em março de 2017, você poderá obtê-lo em https://iana.org/time-zones ou em ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz ).

Portanto, no sf4answers, depois de obter o endereço, ele é geocodificado em uma combinação de latitude / longitude e enviado a um serviço da web de terceiros para obter um fuso horário que corresponde a uma entrada no banco de dados tz. A partir daí, os horários de início e término são convertidos em DateTimeOffsetinstâncias com o deslocamento UTC adequado e, em seguida, armazenados no banco de dados.

Quanto a lidar com isso em SO e sites, depende do público e do que você está tentando exibir. Se você notar, a maioria dos sites sociais (e SO, e a seção de eventos em sf4answers) exibem eventos em tempo relativo ou, se um valor absoluto é usado, geralmente é UTC.

No entanto, se o seu público espera horários locais, o uso de DateTimeOffsetum método de extensão que leva o fuso horário para o qual converter seria ótimo; o tipo de dados SQL datetimeoffsetse traduziria em .NET DateTimeOffsetque você pode então chegar a tempo universal para o uso do GetUniversalTimemétodo . A partir daí, basta usar os métodos da ZoneInfoclasse para converter do UTC para o horário local (você precisará trabalhar um pouco para inseri-lo em um DateTimeOffset, mas é simples o suficiente).

Onde fazer a transformação? Esse é um custo que você terá que pagar em algum lugar , e não há o "melhor" caminho. Eu optaria pela visualização, porém, com o deslocamento do fuso horário como parte do modelo de visualização apresentado à visualização. Dessa forma, se os requisitos para a visualização forem alterados, você não precisará alterar seu modelo de visualização para acomodar a alteração. Você JsonResultsimplesmente conteria um modelo com o e o deslocamento.IEnumerable<T>

No lado da entrada, usando um fichário de modelo? Eu diria absolutamente de jeito nenhum. Você não pode garantir que todas as datas (agora ou no futuro) tenham que ser transformadas dessa maneira; deve ser uma função explícita do seu controlador executar esta ação. Novamente, se os requisitos mudarem, você não precisará ajustar uma ou várias ModelBinderinstâncias para ajustar sua lógica de negócios; e é lógica de negócios, o que significa que deve estar no controlador.

casperOne
fonte
Obrigado pela sua resposta detalhada. Espero não parecer rude, mas isso realmente não respondeu a nenhuma das minhas preocupações com o ASP.NET MVC: E a entrada do usuário (seria suficiente usar um fichário de modelo personalizado)? E o mais importante, que tal exibir essas datas (no fuso horário local do usuário)? Sinto que o método de extensão adicionará muito peso às minhas visualizações, o que parece desnecessário.
TheCloudlessSky
1
@TheCloudlessSky: veja os dois últimos parágrafos da minha resposta editada. Pessoalmente, acho que os detalhes de onde fazer as transformações são menores; o principal problema é a conversão e o armazenamento reais dos dados de data e hora (btw, eu não posso enfatizar o suficiente o uso datetimeoffsetno SQL Server e DateTimeOffsetno .NET aqui, eles realmente simplificam tremendamente as coisas) que eu acredito que o .NET não lida adequadamente pelas razões descritas acima. Se você tem uma data em Nova York que foi inserida em 2003 e deseja que ela seja traduzida para uma data em LA em 2011, o .NET falhará muito nesse caso.
precisa saber é o seguinte
O problema de não usar um fichário de modelo (ou qualquer camada antes da ação) é que todo controlador fica muito difícil de testar ao trabalhar com datas (todos eles dependem da conversão de datas). Isso permitiria que as datas dos testes de unidade fossem escritas em UTC. Meu aplicativo possui usuários associados a um perfil (pense em médicos / secretários associados à sua prática). O perfil contém as informações do fuso horário. Portanto, é muito fácil obter o fuso horário do perfil do usuário atual e transformar durante a ligação. Você tem outros argumentos contra isso? Obrigado pela sua contribuição!
TheCloudlessSky
O argumento contra isso é que seus testes não refletiriam com precisão os casos de teste. Sua entrada não estará no UTC; portanto, seus casos de teste não devem ser criados para usá-la. Ele deve usar datas do mundo real com locais e tudo (embora, se você usar, DateTimeOffsetvocê mitigue muito isso, IMO).
precisa saber é o seguinte
Sim - mas isso significa que todo teste que lida com datas deve ser responsável por isso. Toda ação que lida com tempos de data sempre será convertida em UTC. Para mim, este é um candidato privilegiado para o encadernador de modelos e, em seguida, teste-o .
TheCloudlessSky
5

Esta é apenas a minha opinião, acho que o aplicativo MVC deve separar o problema de apresentação de dados do poço do gerenciamento do modelo de dados. Um banco de dados pode armazenar dados no horário do servidor local, mas é um dever da camada de apresentação renderizar data e hora usando o fuso horário do usuário local. Isso me parece o mesmo problema que o I18N e o formato numérico para diferentes países. No seu caso, seu aplicativo deve detectar o Culturefuso horário e o usuário e alterar a Visualização, mostrando diferentes apresentações de texto, número e data e hora, mas os dados armazenados podem ter o mesmo formato.

Massimo Zerbini
fonte
Obrigado pela sua resposta. Sim, isso é o que eu basicamente descrevi, só estou me perguntando como isso pode ser conseguido com o MVC. O aplicativo armazena o fuso horário do usuário como parte da criação da conta (eles escolhem o fuso horário). O problema novamente vem com como renderizar as datas em cada modo de exibição sem (espero) ter que agrupá-las com os métodos que eu criei DateTimeExtensions.
TheCloudlessSky
Existem várias maneiras de fazer isso: uma é usar métodos auxiliares, como sugerido; outra, talvez mais complexa, mas elegante é o uso de um filtro que processa as solicitações e respostas e converte a data e hora. Outra maneira é desenvolver um atributo Display personalizado para anotar os campos do tipo DateTime do seu modelo de exibição.
Massimo Zerbini 28/09
Esses são os tipos de coisas que eu estava procurando. Se você olhar para o meu OP, eu também mencionei o uso do AutoMapper para fazer algum processamento antes de ir para o resultado do view / json, mas após a execução da ação.
TheCloudlessSky
1

Para saída, crie um modelo de exibição / editor como este

@inherits System.Web.Mvc.WebViewPage<System.DateTime>
@Html.Label(Model.ToLocalTime().ToLongTimeString()))

Você pode vinculá-los com base nos atributos do seu modelo se desejar que apenas determinados modelos usem esses modelos.

Veja aqui e aqui para obter mais detalhes sobre a criação de modelos de editor personalizados.

Como alternativa, como você deseja que ele funcione para entrada e saída, sugiro estender um controle ou até mesmo criar o seu. Dessa forma, você pode interceptar as entradas e saídas e converter o texto / valor conforme necessário.

Esperamos que este link o leve na direção certa, se você quiser seguir esse caminho.

De qualquer forma, se você quiser uma solução elegante, será um pouco de trabalho. Pelo lado positivo, depois de ter feito isso, você pode mantê-lo em sua biblioteca de códigos para uso futuro!

dnatoli
fonte
Eu acho que os modelos personalizados de exibição / editor e o fichário são a solução mais elegante, porque "funcionará" uma vez implementada. Nenhum desenvolvedor precisa saber nada de especial para obter as datas de trabalho direito é só usar DisplayFore EditorFore ele vai trabalhar o tempo todo. +1
Kevin Stricker
17
isso não renderizaria o tempo do servidor de aplicativos e não o tempo do navegador?
11743 Alex
0

Esta é provavelmente uma marreta para quebrar uma noz, mas você pode injetar uma camada entre as camadas UI e Business que converta de maneira transparente as datas para a hora local nos gráficos de objetos retornados e para o UTC nos parâmetros de entrada da data e hora.

Eu imagino que isso poderia ser conseguido usando PostSharp ou alguma inversão do contêiner de controle.

Pessoalmente, eu iria apenas converter explicitamente suas datas na interface do usuário ...

geoffreys
fonte