DateTimeOffset es el que conviene almacenar. Contiene el momento y su desplazamiento (offset) respecto a UTC, así que un valor que se vuelve a leer en otra zona horaria sigue identificando el mismo instante. La guía de Microsoft Learn para elegir entre los dos recomienda “considerar DateTimeOffset como el tipo de fecha y hora predeterminado para el desarrollo de aplicaciones”.

DateTime solo registra la hora que marca el reloj, más un indicador Kind que se pierde con facilidad en la serialización y en los viajes de ida y vuelta a la base de datos. Sigue siendo la opción adecuada para una hora de reloj que debe significar lo mismo en todas partes, como un recordatorio del almuerzo a las 12:00. Una vez que sabemos qué tipo almacenar, el artículo sobre cómo dar formato a fechas y horas en C# explica cómo mostrarlo.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

¿Qué es DateTimeOffset?

DateTimeOffset es una estructura que representa un único instante como una fecha y hora más su desplazamiento respecto a UTC.

Ese desplazamiento es lo que lo hace inequívoco. 2026-08-08 14:30 +02:00 y 2026-08-08 12:30 +00:00 son el mismo instante escrito de dos formas, y al compararlos resultan iguales, porque el tipo compara el momento en UTC.

Expone la mayoría de los miembros de DateTime (Year, Month, Day, Hour, AddDays(), Parse()), además de Offset, UtcDateTime y LocalDateTime.

Lo que no almacena es la zona horaria. Un desplazamiento de +02:00 lo comparten Atenas, El Cairo y Johannesburgo en enero, y cambia en cualquier zona que aplique el horario de verano. Cuando importa la zona en sí, almacena el identificador de la zona y resuélvelo con TimeZoneInfo.

DateTimeOffset.UtcNow y DateTimeOffset.Now son las dos formas que ofrece el tipo para obtener el momento actual.

La guía de Microsoft Learn para elegir entre los dos expresa la misma limitación con precisión: “Un valor DateTimeOffset no está vinculado a una zona horaria concreta, sino que puede proceder de diversas zonas horarias”.

Para empezar, vamos a crear un valor DateTimeOffset:

var dateTimeOffset = DateTimeOffset.Now;
Console.WriteLine($"DateTimeOffset: {dateTimeOffset}");

Aquí definimos la variable dateTimeOffset, le asignamos el valor actual de DateTimeOffset mediante la propiedad estática DateTimeOffset.Now y escribimos el valor en la consola:

DateTimeOffset: 8/14/2026 1:11:23 PM +02:00

Vemos el componente DateTime y, además, un Offset de +2:00. El Offset indica que el DateTime actual va 2 horas por delante de UTC.

El valor Offset no representa la zona horaria. El valor Offset sirve para determinar el subconjunto de zonas horarias en las que puede estar el valor DateTime. Si la zona horaria exacta es esencial, es importante almacenar la zona horaria real, porque un mismo Offset lo comparten varias zonas horarias.

Veámoslo con más detalle:

static List<TimeZoneInfo> GetTimeZoneFromOffset(TimeSpan offset) =>
    TimeZoneInfo.GetSystemTimeZones()
    .Where(tz => tz.BaseUtcOffset == offset)
    .ToList();

var timeZones = GetTimeZoneFromOffset(dateTimeOffset.Offset);
foreach (TimeZoneInfo timeZone in timeZones)
{
    Console.WriteLine($"Time Zone: {timeZone}");
} 

Aquí declaramos e implementamos el método estático GetTimeZoneFromOffset(), que recibe un parámetro de tipo TimeSpan. A partir de ese TimeSpan, comprobamos qué zonas horarias lo tienen como desplazamiento estándar (BaseUtcOffset) e imprimimos los resultados en la consola.

Veamos la salida:

Time Zone: (UTC+02:00) Athens, Bucharest
Time Zone: (UTC+02:00) Beirut
Time Zone: (UTC+02:00) Cairo
Time Zone: (UTC+02:00) Chisinau
Time Zone: (UTC+02:00) Gaza, Hebron
Time Zone: (UTC+02:00) Harare, Pretoria
Time Zone: (UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius
Time Zone: (UTC+02:00) Jerusalem
Time Zone: (UTC+02:00) Juba
Time Zone: (UTC+02:00) Kaliningrad
Time Zone: (UTC+02:00) Khartoum
Time Zone: (UTC+02:00) Tripoli
Time Zone: (UTC+02:00) Windhoek

Cuando la aplicación se estaba ejecutando, la zona horaria era (UTC +2:00) Harare, Pretoria. Según la salida, hay trece zonas horarias cuyo desplazamiento estándar es +2:00. BaseUtcOffset no tiene en cuenta el horario de verano, así que en esa fecha de agosto siete de ellas, incluidas Atenas y El Cairo, estaban en +03:00, mientras que faltan zonas que entonces estaban en +02:00, como Madrid y Berlín.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

DateTimeOffset vs DateTime: ¿cuál es la diferencia?

La principal diferencia entre las estructuras DateTimeOffset y DateTime está en si tienen en cuenta la zona horaria. Se considera que DateTimeOffset tiene en cuenta la zona horaria porque, además del componente DateTime, tiene un componente Offset que indica cuánto se diferencia DateTime de UTC.

Ese componente adicional cambia tres cosas con las que te vas a encontrar en producción. La igualdad y la ordenación comparan el instante UTC subyacente, así que dos valores escritos con desplazamientos distintos pueden ser iguales.

La serialización es más sencilla, porque el desplazamiento viaja dentro del propio valor. Un DateTime solo lleva un indicador Kind, que no registra más que si el valor está en UTC, es local o no está especificado, y una columna de fecha simple o una cadena de fecha sin desplazamiento no tiene dónde guardar ni siquiera eso.

Y la aritmética sigue siendo predecible: sumar horas a un DateTimeOffset mueve el instante y deja el desplazamiento como está, así que un cambio de horario de verano nunca altera el resultado sin avisar.

Vamos a imprimir los dos tipos en el mismo momento para ver qué conserva cada uno:

var dateTime = DateTime.Now;
Console.WriteLine($"DateTime: {dateTime}");

dateTimeOffset = DateTimeOffset.Now;
Console.WriteLine($"DateTimeOffset: {dateTimeOffset}"); 

Aquí imprimimos el valor de DateTime y el de DateTimeOffset al mismo tiempo. Los componentes de fecha y hora de ambos valores son idénticos. Además de esos componentes, el valor DateTimeOffset incluye el desplazamiento +02:00. Esto significa que el valor de DateTimeOffset va 2 horas por delante de UTC.

Como se considera que DateTime no tiene en cuenta la zona horaria, es fundamental tener mucho cuidado al manejar conversiones entre zonas horarias, porque no se gestionan automáticamente. Además, DateTime tiene una propiedad Kind de tipo DateTimeKind, mientras que DateTimeOffset no tiene una propiedad Kind:

var dateTimeUtc = DateTime.UtcNow;
Console.WriteLine($"DateTime Kind: {dateTimeUtc.Kind}");

var dateTimeLocal = DateTime.Now;
Console.WriteLine($"DateTime Kind: {dateTimeLocal.Kind}");

var dateTimeUnspecified = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified);
Console.WriteLine($"DateTime Kind: {dateTimeUnspecified.Kind}"); 

En este caso, vemos los valores posibles que puede tomar la propiedad Kind de los valores DateTime. La propiedad Kind es una enumeración que puede ser Utc, Local o Unspecified. Para ver la diferencia entre leer un valor en UTC y en hora local, consulta DateTime.Now vs DateTime.UtcNow.

DateTimeOffset no tiene una propiedad Kind, pero sí una propiedad DateTime. A través de esta propiedad DateTime de la estructura DateTimeOffset podemos acceder a la propiedad Kind.

La propiedad DateTime de un DateTimeOffset siempre tiene su Kind establecido en Unspecified.

Similitudes entre DateTimeOffset y DateTime

Aunque las estructuras DateTimeOffset y DateTime tienen diferencias, comparten similitudes fundamentales.

Independientemente de la información de zona horaria, tanto DateTimeOffset como DateTime tienen como función principal representar valores de fecha y hora. Por eso, DateTimeOffset y DateTime comparten miembros como Year, Month, Day, Hour, Minute, Second y Millisecond, además de algunos métodos comunes, como Parse(), TryParse(), ParseExact(), TryParseExact() y ToString(), entre otros. La comparación de dos instancias es uno de los miembros que comparten, y el artículo sobre cómo comparar valores DateTime explica las reglas relativas a la zona horaria que se aplican a ambos.

¿Cómo convertir DateTimeOffset en DateTime?

DateTimeOffset expone tres propiedades para esto, y elegir la equivocada es la vía más habitual por la que se cuelan los errores de zona horaria.

UtcDateTime devuelve el momento convertido a UTC, con Kind establecido en Utc. Es la que hay que usar al escribir en una base de datos o en una API, porque sigue siendo correcta dondequiera que se lea, y el artículo sobre cómo convertir un DateTime en una cadena ISO 8601 explica el formato serializado.

LocalDateTime convierte a la zona local de la máquina y establece Kind en Local. Es correcta para mostrar en una aplicación de escritorio, pero no para almacenar: el “local” al que se refiere es el del servidor, no el del usuario.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

DateTime devuelve la hora del reloj tal como está almacenada, descarta el desplazamiento y deja Kind como Unspecified. Por su nombre parece la opción obvia, pero casi siempre es la equivocada: el instante ya no se puede recuperar.

La conversión en sentido contrario también requiere cuidado, porque el desplazamiento es opcional. Al asignar un DateTime se usa su Kind, así que un valor con Kind igual a Unspecified adopta sin avisar el desplazamiento del servidor.

Si las escribimos una junto a otra, se ve que la diferencia entre las tres propiedades es, en parte, qué Kind se conserva:

var moment = DateTimeOffset.Now;

var forStorage = moment.UtcDateTime;      // Kind = Utc, safe to persist
var forDisplay = moment.LocalDateTime;    // Kind = Local, server's zone
var raw        = moment.DateTime;         // Kind = Unspecified, offset lost

UtcDateTime es la única que se puede persistir con seguridad; LocalDateTime y el DateTime en bruto descartan información en cuanto se ejecutan, solo que de formas distintas.

¿Cuál conviene usar: DateTimeOffset o DateTime?

Usa DateTimeOffset de forma predeterminada y recurre a DateTime cuando tengas un motivo concreto.

DateTimeOffset es la opción correcta siempre que el valor registre algo que ocurrió: entradas de auditoría, marcas de tiempo de pedidos, sobres de mensajes, cualquier cosa que una máquina registre y otra lea. La comparación y la ordenación siguen siendo correctas entre zonas sin que nadie tenga que acordarse de normalizar.

DateTime es la opción correcta para una hora de reloj que debe leerse igual en todas partes. Un recordatorio del almuerzo a las 12:00, una tienda que abre a las 09:00, una franja semanal recurrente: si les agregamos un desplazamiento, pasan a ser incorrectos para cualquiera que esté en otra zona.

Para una fecha sin hora, ninguno de los dos es la mejor respuesta: DateOnly existe para eso y elimina toda una categoría de errores relacionados con la medianoche.

Dos notas prácticas. TimeProvider es la forma moderna de obtener el momento actual en código testeable (consulta cómo probar código que depende del tiempo con TimeProvider) y sustituye a las llamadas directas a DateTimeOffset.Now. Y, sea cual sea el tipo que almacenemos, conviene usar siempre el mismo. Mezclar ambos tipos en columnas distintas es peor que cualquiera de las dos opciones.

CriterioDateTimeOffsetDateTime
Qué registraInstante + desplazamiento respecto a UTCHora del reloj + indicador Kind
Identifica un instante inequívocoSíSolo si se conserva Kind
Propiedad KindNo tiene; su .DateTime siempre es UnspecifiedUtc, Local o Unspecified
Se conserva al serializarEl desplazamiento forma parte del valorKind se pierde con frecuencia
Almacena la zona horariaNo, solo el desplazamientoNo
Comparación de dos valoresCorrecta entre zonasCorrecta solo dentro de una zona
Tamaño16 bytes8 bytes
Opción predeterminada para código nuevoSí, es la recomendación de MicrosoftPara horas de reloj y código heredado
Úsalo paraMarcas de tiempo de eventos, registros de auditoría, cualquier sistema distribuidoHorarios de apertura, horas locales recurrentes

Supongamos que tenemos una alarma que nos recuerda que es la hora del almuerzo a las 12:00. Querríamos que la alarma sonara a las 12:00 sin importar la zona horaria en la que estemos. En este caso, usar DateTime está justificado, porque la información de zona horaria no es importante.

Solemos usar DateTimeOffset en casos en los que se necesita información precisa sobre el instante en que ocurrió un evento concreto. Además, deberíamos plantearnos usar DateTimeOffset cuando trabajamos con sistemas distribuidos a los que se accede desde distintas zonas horarias.

Un caso real en el que DateTimeOffset sería la opción preferida es el registro de la fecha y hora de eventos o acciones dentro de un sistema distribuido. En un sistema distribuido, los usuarios están repartidos por todo el mundo y posiblemente en distintas zonas horarias. Por eso, al registrar la fecha y hora en que ocurren los eventos o acciones, es crucial ser precisos para poder informar correctamente sobre ellos.

Conclusión

En este artículo hemos visto las diferencias y similitudes entre las estructuras DateTimeOffset y DateTime en C#. Por último, hemos examinado algunos casos de uso habituales de DateTimeOffset y DateTime, guiándonos por sus diferencias, sus capacidades y los requisitos.

Probado con .NET 10.0.10.