Trabajando con fechas UTC en C#

Parece algo trivial manejar las fechas y horarios en tu código a partir de DateTime o DateTimeOffset, pero pueden existir algunos casos donde el manejo de fechas puede traducirse en una pequeña pero molesta cantidad de errores o imprecisiones no deseadas, según el fin al que este destinado tu aplicación, y la geolocalización de los usuarios que la usen.

¿Alguna vez te has planteado en que zona horaria esta localizado el servidor que ejecuta tu código una vez lo despliegas a la nube? ¿En que podría afectar eso a la comparación de fechas para determinar aspectos sensibles de tu lógica? En esta entrada voy a profundizar en todo ello.

A continuación planteo un test sobre diferentes formatos de fechas almacenados como strings en bases de datos no relacionales que no admiten el tipo fecha, como DynamoDb.

Formatos

  • dd/MM/yyyy
  • MM/dd/yyyy
  • formato ISO 6801 (yyyy-MM-aaThh:mm:ss.fffff+hh:mm)

    Partiendo de que mi máquina para ejecutar el test, estaba configurada con zona horaria +02:00 respecto a UTC, y en lenguaje español con el formato de fecha que usualmente manejamos, los resultados son:

Problemas con el cambio de cultura

  • test1: Dado que se espera que la segunda cifra sea el mes en nuestra cultura, y este es 16, no se podrá hacer el parse, dando una excepción.
  • test2: En cambio en este caso, dado que la segunda cifra es 10, este formato si es válido y transformado a fecha.
  • test3: este problema en cambio no existe con el formato ISO 6801. DateTime o DateTimeOffset parse entienden este formato y toda la información que aporta, no dando lugar a ambigüedad entre los meses y los días, por lo tanto debería ser el formato elegido siempre que sea necesario almacenar fechas con tipo string.

Para solucionar un posible problema al transformar el string a fecha, podrías ignorar la cultura que tiene configurada la máquina que ejecuta el código, para ello aplica el CultureInfo.InvariantCulture que tienen los test4, 5 y 6. En este caso se puede observar como ahora la fecha en la cultura es-ES es la que falla, al considerar que la primera cifra es el mes, y este siendo 16, produce excepción.

Si guardas tus fechas con este formato (xx/xx/yyyy) te enfrentas a posibles problemas ante un DateTime.Parse aunque incluyas el invariantCulture.

Por lo tanto, siempre que trabajemos con fechas y queramos aislarnos de la cultura, si no trabajamos con ISO, el siguiente formato recomendado debería ser MM/dd/yyyy

Problemas con la zona horaria en la asignación de la variable

  • test5: al trabajar con formato MM/dd/yyyy, se convierte la fecha con horario 00:00:00 de la zona horaria de la maquina que ejecuta el código, +02:00 en este caso, lo que en UTC serán las 22:00:00 del día anterior.
  • test6: en este caso dado que el formato ISO ya determina que la hora es la 00:00:00 +00:00 (UTC), la fecha convertida será automáticamente convertida a local (02:00:00 + 02:00)

Si estas comparando fechas para determinar cuando un usuario hace una reserva, concreta una cita o si tiene permiso para ejecutar una determinada acción, es posible que otro problema surja con la zona horaria en la que se determina la fecha.

Una recomendación al respecto es que tu código de backend siempre trabaje con zonas horarias en UTC, para ello, en lugar de determinar una fecha como DateTime.Now utiliza DateTime.UtcNow. ¿Pero como se comporta el servidor al hacer un parse desde una fecha en formato string según el formato?

En los resultados anteriores a “test7” puedes observar que si trabajas con xx/xx/yyyy la fecha salvada será asignada con 00:00:00 para las horas, ¿pero de que zona horaria? de aquella donde esté la maquina que ejecuta el código, tal como se puede observar en los resultados de los test 6 y 7, por lo que si se vuelve a convertir a toString() esa fecha sin tener en cuenta el formato, podrían producirse errores no deseados al aplicar la lógica.

En cambio si usas formato ISO ya asume que las 00:00:00 son en UTC dado el +00:00 que incluye el formato, pero a la variable le será asignado 02:00:00 +02:00.

Para solucionar este problema, podemos aplicar el método ToUniversalTime(), que convertirá cualquier resultado de parse a horario adecuado UTC.

Problemas con la zona horaria en la lectura de la variable

  • test7: al trabajar con formato MM/dd/yyyy y convertir el resultado del parseo a UTC, se obtiene el valor del día anterior a las 22:00, puesto que el parse considera que el formato que se lee está en la zona horaria de la máquina que ejecuta el código, ante falta de información más precisa como la del formato ISO 6801.
  • test8: en este caso el formato ISO proporciona toda la información necesaria, y por tanto la fecha pasada a UTC es la que refleja el propio string.

Ante esta situación, la mejor solución es aplicar la sobrecarga DateTimeStyles.AssumeUniversal, de forma que el parse interpretará que la fecha leída, ante falta de información, está siendo leída desde la zona horaria UTC.

Conclusiones

Si tenemos necesidad de almacenar una fecha en una variable string o en una bbdd que no tiene tipado de fechas, lo mas adecuado es usar el formato ISO, tratando siempre las fechas en formato UTC.

En sus lecturas, el formato mas completo para trabajar siempre en UTC es añadiendo las sobrecargas CultureInfo.InvariantCulture y DateTimeStyles.AssumeUniversal, con posterior conversión a ToUniversalTime(), de forma que independientemente del formato de lectura, el resultado en la variable asignada sea una fecha asumiendo que siempre se ha trabajado con UTC para guardar el dato que se pretende leer.

Posteriormente ya en cliente, en frontend, se harán las conversiones a la zona horaria que tenga configurado dicho cliente para adaptar la fecha a su zona horaria como mejor convenga.

Y listo, espero que pese a que algunos de estos consejos puedan ser un poco básicos, sirva aunque sea a unos pocos para comprender por qué tener un especial cuidado al manipular fechas en código de backend, y las justificaciones de hacerlo siempre en UTC.

Un saludo.

Deja un comentario