Dates

Escopo: C#. Idiomas específicos deste ecossistema.

O DateTime do C# guarda, além da data e da hora, um campo Kind que diz se aquele instante é local, é UTC (Coordinated Universal Time · Tempo Universal Coordenado) ou é indefinido. O problema é que a maior parte das formas de criar um DateTime deixa esse campo como indefinido, e o valor passa a depender de quem o lê. Use DateTimeOffset, que carrega o deslocamento em relação ao UTC dentro do próprio valor. Guarde tudo em UTC, que é a referência comum entre servidores, e converta para o timezone (fuso horário) do usuário só na hora de exibir.

Conceitos fundamentais

ConceitoO que é
DateTime (data e hora)Tipo de data/hora cujo campo Kind pode ser Local, Utc ou Unspecified; ambíguo por padrão
DateTimeOffset (data, hora e deslocamento)Tipo que carrega o offset junto com o instante; elimina ambiguidade entre fusos
UTC (Coordinated Universal Time · Tempo Universal Coordenado)Referência global de tempo sem fuso; padrão para armazenamento e troca entre serviços
Kind (tipo de instante)Campo de DateTime que indica se o valor é local, UTC ou indefinido
TimeZoneInfo (informação de fuso horário)Tipo que descreve um fuso e suas regras; usado para converter UTC em hora local
DateOnly / TimeOnly (apenas data / apenas hora)Tipos do .NET 6+ para representar apenas data ou apenas hora, sem componente residual
ISO 8601 (padrão internacional de formato de data)Formato textual YYYY-MM-DDTHH:mm:ssZ para troca interoperável de instantes

Marcar o instante em UTC

DateTime.Now devolve a hora do relógio da máquina onde o código roda. Dois servidores em regiões diferentes, ou dois containers com fuso diferente, gravam horas diferentes para o mesmo acontecimento, e comparar esses valores depois produz respostas erradas. DateTimeOffset.UtcNow devolve sempre o mesmo instante, com o deslocamento explícito no valor.

❌ Ruim: hora local do servidor, Kind implícito
var createdAt = DateTime.Now;
// Kind: Local (depende do timezone do servidor)
// Comparações entre servidores em timezones diferentes produzem resultados errados
✅ Bom: UTC explícito, comparável em qualquer ambiente
var createdAt = DateTimeOffset.UtcNow;
// Offset: +00:00 (inequívoco, portável)
// Serializa como "2026-04-19T14:00:00+00:00"

DateTimeOffset carrega o fuso junto do valor

Um DateTime com Kind indefinido no contrato da API entrega ao cliente um "2026-04-19T14:00:00" que não diz de que fuso aquilo veio. Cada lado interpreta com a própria convenção, e a diferença aparece como um pedido criado três horas no futuro. DateTimeOffset serializa "2026-04-19T14:00:00+00:00", e o +00:00 responde a pergunta sem depender de combinação prévia.

❌ Ruim: DateTime sem Kind perde contexto de timezone
public record OrderResponse
{
    public required Guid Id { get; init; }
    public required DateTime CreatedAt { get; init; }
    // Kind: Unspecified: impossível saber se é UTC ou local sem convenção externa
}
✅ Bom: DateTimeOffset carrega o offset, sem ambiguidade
public record OrderResponse
{
    public required Guid Id { get; init; }
    public required DateTimeOffset CreatedAt { get; init; }
    // "2026-04-19T14:00:00+00:00" (timezone embutida no valor)
}

DateOnly e TimeOnly

Uma data de nascimento não tem hora. Guardá-la num DateTime acrescenta um 00:00:00 que ninguém pediu, e esse zero vira problema quando o valor é convertido de fuso: a meia-noite de 21 de agosto em São Paulo é o dia 20 em UTC, e a data de nascimento anda um dia para trás. DateOnly e TimeOnly (.NET 6+) guardam só o que interessa e não têm o que deslocar.

❌ Ruim: DateTime para data pura, hora fantasma causa bugs
public record CustomerRequest
{
    public required string Name { get; init; }
    public required DateTime BirthDate { get; init; }
    // "1990-08-21T00:00:00": hora zero sem sentido, sujeita a deslocamento de timezone
}
✅ Bom: DateOnly para data, TimeOnly para hora, intenção clara
public record CustomerRequest
{
    public required string Name { get; init; }
    public required DateOnly BirthDate { get; init; }
    // "1990-08-21" (só data, sem componente de hora)
}

public record ScheduleRequest
{
    public required DateOnly Date { get; init; }
    public required TimeOnly StartTime { get; init; }
    // "09:30:00" (só hora, sem data acoplada)
}

Gravar e ler no banco sem perder o fuso

O EF Core grava o DateTime conforme o Kind que ele encontrar. Quando o Kind é indefinido, o valor vai para o banco sem conversão e volta indefinido na leitura: a informação de fuso não estava lá para se perder. Declarar a propriedade como DateTimeOffset resolve na origem. O EF mapeia para datetimeoffset no SQL Server e timestamptz no PostgreSQL, e o deslocamento sobrevive à ida e à volta sem configuração extra.

❌ Ruim: DateTime sem Kind, round-trip ambíguo com o banco
public class Order
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; } // Kind: Unspecified após leitura do banco
}
✅ Bom: DateTimeOffset no modelo, EF preserva offset no banco
public class Order
{
    public Guid Id { get; set; }
    public DateTimeOffset CreatedAt { get; set; }
    // SQL Server: datetimeoffset, PostgreSQL: timestamptz
    // Offset preservado no round-trip, sem configuração extra
}

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.