Problématique : dans le cadre de la conception de flux d’intégrations*, comment gérer les données qui représentent des dates ?
Ou comment réagir quand on vous assigne un bug qui parle de dates qui ne correspondent pas (décalage d’un jour, d’une heure, ou autre).
*Flux faisant transiter de la donnée d’un système source vers un système cible, via un éventuel pivot.
Définitions
Interface / Interface d’intégration : représente à la fois le protocole utilisé pour faire transiter la donnée, ainsi que le format et les spécificités de la donnée.
Date / DateOnly : représente une date sans horaire. Nous prenons le parti de représenter une DateOnly de la même façon qu’une DateTime, avec l’heure à minuit.
DateTime : représente une date et heure, précise à la seconde (en réalité, on peut traiter cette donnée avec plus de précision, mais cela n’a aucun intérêt ici)
Système source : système qui contient la donnée existante, qui devra être récupérée.
Système cible : système qui doit accueillir de la nouvelle donnée.
Pivot : représentation de la donnée dans un format pratique, qui n’est ni le système source, ni le système cible. On doit pouvoir passer du système source au pivot, ainsi que du pivot au système cible.

L’importance du Pivot : transformer l’hétérogène en un standard UTC fiable.
Un peu de contexte
Pour cet article, nous allons nous focaliser sur un système d’échange de données assez simple, sans problématique de Time Zone. Nous avons besoin de remplir un système cible de gestion d’événement. Ce système cible requiert une donnée telle qu’un événement :
- EventName string(50)
- EventDate Date(yyyyMMdd)
J’ai volontairement pris une notation telle que l’on peut la retrouver dans le monde de l’entreprise, quelque chose de chaotique. On remarque avec cette spécification que la donnée EventDate représente la Date avec une string dans un format spécifique. La première chose à faire ici est, évidemment, de demander sur quelle TimeZone est géré ce parsing de date. Sachant que nous sommes en France, la réponse sera probablement l’un de ces choix :
- Heure de Paris
- UTC. Universal Time Coordinated
À noter que l’heure de Paris correspond à UTC+1 en hiver et UTC+2 en été.
Le système source, lui, va prendre plusieurs formes. Nous allons en fait avoir 2 systèmes sources. C’est là qu’est la spécificité de notre scénario. En effet, intégrer de la donnée avec un seul flux ne pose pas le problème que nous essayons de résoudre. Nous allons détailler les détails du système source plus tard.
Quid de notre système à nous ? (Pivot) Nous décidons de gérer notre système avec ce format intermédiaire qui représente un évènement :
- Name string(250)
- CreationDateUtc DateTime
Ici, nous avons choisi de ne pas précéder chacun de nos noms de donnée du mot Event, pour une raison de nomenclature propre à notre entreprise. Cela n’a aucune importance ici. Notons que le Name a une plus grande capacité, ce qui semble inutile, car le système cible n’en prend que 50. Nous avons fait ce choix pour la raison suivante. Après avoir fait des ateliers avec les parties prenantes, nous nous rendons compte que le système source va gérer des noms jusqu’à 250 caractères. Il y aura un choix à faire au moment de l’intégration dans le système cible, mais cela ne fait pas partie de notre programme du jour !
De même, on note un peu de changement sur la donnée de la date CreationDateUtc. Déjà, on voit que notre type est bien un DateTime. J’entends par là que l’on stocke l’ensemble de la date et de l’heure de l’événement. Nous gardons la précision maximale, quitte à la perdre ensuite dans le système cible. Si l’on prend pour exemple un stockage sur du SQL Server, on peut imaginer un DateTime2(2) par exemple. Dernier point, le plus important, on va toujours garder nos dates en UTC. Dans notre système, on doit donc s’assurer que toute donnée entrante est convertie en UTC, et toute donnée sortante est bien marquée comme étant UTC, pour être ensuite adaptée au système cible. Cela paraissait évident, n’est-ce pas ? Voilà une donnée bien représentée. Si vous construisez un modèle de données avec des dates, précisez la TimeZone ! Ne pas le faire est selon moi une erreur, et pas des moindres.
Nous allons donc, pour notre système, utiliser de l’UTC pour les dates.
Explorer le système source
Dans notre exemple, nous avons 2 systèmes sources, l’un aura le kind disponible, l’autre non.
C’est quoi le kind ? Je parle du kind de datetime, donc du type de datetime, est-ce qu’il est local ou Utc ?
Le kind est disponible
Pour cet exemple, nous voulons intégrer des événements auxquels un utilisateur a souscrit. Souscrit à quoi ? Un abonnement, un service, peu importe. Gardons le focus !
Nous avons récupéré subscriptionDate du système source et nous voulons intégrer cette date dans notre système pivot.
Pour convertir notre date en Utc, c’est très simple, il suffit d’appeler la method ToUniversalTime(). Que le kind soit local ou Utc, la method renvoie une date en Utc.
En revanche, il faut tout de même faire attention, car si le kind est undefined, on ne peut rien faire.
Il faut penser à mettre une guard pour éviter que le système ne plante dans le futur. Vous ne pouvez probablement pas utiliser un test unitaire ici, car vous ne maitrisez pas forcément la source de la donnée.
Il est éventuellement possible d’utiliser un Debug.Assert(), mais il faut que le système soit correctement testé sur un environnement en mode debug avec des tests end to end. Ce qui doit être une chose rare.
Au final, il suffit de rajouter ces 3 lignes de code dans votre système source et c’est tout.


« Guard clause » et cycle de vie
Le kind n’est pas disponible
Pour notre 2ᵉ système source, cela se complique. On veut ici intégrer des événements du même style que l’exemple précédent mais depuis un autre système.
Ici, on parle de registrationDate, pour changer un peu !
Nous commençons naïvement par faire la même implémentation que précédemment, sauf que cette fois-ci, la guard ne passe pas et le système throw une exception. La date n’a pas de kind ! Donc le kind est Unspecified.
Pourquoi est-ce dangereux ?
C’est ici que le piège se referme. Si vous laissez passer une date Unspecified, le framework va « deviner » pour vous, souvent en se basant sur l’horloge locale du serveur :

le piège du Kind Unspecified
Ici, il va falloir faire votre propre investigation pour trouver quel est le vrai kind de cette date. Je ne peux pas vraiment vous donner de recette toute faite. Trouvez d’abord la source de la donnée. Si c’est un système standard, vous pouvez vous référer à la documentation. Si vous avez accès au système qui crée cette date, vous pouvez aller voir ce qu’il s’y passe. Pensez aussi à demander autour de vous.
Une fois que c’est fait, vous savez maintenant si votre date est en Utc ou en local. On va donc rajouter une étape dans notre process, c’est d’initialiser le kind tout simplement.

Et j’ai même envie de vous dire, ce n’est même pas utile. Il y a une subtilité dont je souhaite vous parler. Sachez qu’un appel à la method ToUniversalTime()
se comporte exactement de la même façon pour le kind Local que Unspecified. Exactement de la même manière. La raison est simple, c’est comme ça que le code du framework fonctionne, possiblement pour des raisons de compatibilité. Pour ma part je n’ai fait qu’analyser le code décompilé. Concrètement le code qui nous intéresse ressemble à ça (code du framework décompilé) :
Vous l’aurez deviné, la date en cache est en local. Le code est bien plus archaïque alors je vous l’épargne.
Autre point, pensez à changer votre condition, maintenant assurez-vous que le kind ne change pas (comme avant), en inversant la condition.
Cet exemple vaut si l’on sait que le kind est local. Sinon, on peut directement se passer de la dernière ligne, ou bien spécifier le kind.
Pour finir, la chose la plus importante à faire ici est d’expliquer ce que vous avez trouvé. N’hésitez pas à rajouter un commentaire bien verbeux, pour une fois que c’est une bonne chose. Vous pouvez aussi le documenter dans le message de commit, mais pour le coup, je pense qu’il est nécessaire de placer un commentaire. Cette situation m’est arrivée chez un client et c’est effectivement un des seuls commentaires que j’ai écrit dans mon code.
Le commentaire + la guard, sont 2 outils qui protègeront la validité de la donnée dans le futur.
Retenez bien que ToUniversalTime() suppose que la date est Locale si elle est Unspecified. C’est ce qui crée souvent le bug du « décalage d’une heure » (selon l’heure d’été/hiver du serveur).
Anecdote :
J’ai récemment rencontré un problème de kind unspecified chez un client. La donnée source provient d’un dynamics Crm de Microsoft, un logiciel qui, à mon goût, défie l’ensemble des bons patterns de développement. Un très bon cas d’usage pour cet article !
J’ai tout d’abord repéré l’entity Crm en question. J’ai récupéré sa configuration et fait ce constat :
- Un input est stocké tel quel. Il est lu et affiché tel que stocké. Il est exposé via l’API Crm tel quel. Cela fonctionne bien dans l’écosystème Crm. Je sais que la timezone utilisée est celle de Paris.
Une autre entity que j’ai utilisé avait un comportement différent :
- Un input est stocké en considérant que la timezone de l’utilisateur. L’affichage est adapté à la timezone de l’utilisateur. La donnée est stockée en Utc. L’API Crm expose la donnée en Utc.
Selon le cas, j’ai donc pu m’adapter et aussi coller un bon gros pavé en commentaire pour expliquer tout cela.
Exemple d’implémentation du Pivot
On peut faire beaucoup de validation et d’affinage au niveau du pivot. En fait c’est notre système parfait si l’on veut.
On n’est ni dépendant du système source, ni du système cible. On peut donc utiliser tous les conseils que j’ai donné. On veut nommer au mieux nos properties et nos champs en base de donnée. Le Utc à la fin est très utile. On peut aussi rajouter de la validation comme on l’a vu, en testant le kind de la date. On va aussi mettre des commentaires Xml pour indiquer le kind que l’on veut avoir.
Maintenant, pour rendre notre système fiable, on stocke nos dates en Utc, ok. Mais surtout, on veut récupérer la donnée depuis la base de donnée avec le bon kind. Tout dépend de votre adapter, je vais ici donner un exemple pratique avec Entity Framework.
Si vous avez un système un peu trop velu et seulement quelques dates à gérer dans votre coin, vous pouvez simplement spécifier le kind dans vos getters, c’est assez simple à faire.
Si vous souhaitez une solution totalement fiable, vous pouvez implémenter un interceptor via l’interface IMaterializationInterceptor.
Le principe est simple. au moment où EF récupère de la donnée, vous l’interceptez et vous pouvez la modifier. On veut pour notre part, filtrer les dates et leur spécifier le kind Utc
On n’oublie pas de l’enregistrer dans la DI :
Pratique non ?
Un disclaimer tout de même, je ne montre qu’un exemple facile et fiable. Ce n’est pas forcément la solution pour votre système. Il y a des tas de choses à considérer comme l’impact en performances. N’hésitez pas à considérer toutes les solutions, comme les Value Converters.
Pour ceux qui aiment se faire mal à la tête, vous avez peut-être envie d’en savoir plus sur les DateTimeOffset. Cela méritera un article complet. Je ne peux que vous conseiller de vous renseigner sur ce type avant de l’utiliser. Une règle simple peut être, si vous n’avez qu’une seule timezone à gérer, hors Utc, utilisez le DateTime.
À toi de jouer !
J’espère que vous l’avez compris, ce n’est pas une méthode à employer aveuglément. Il y a plein de façons de faire. Avant de copier-coller des morceaux de code dans vos applications, comprenez bien ce que vous faites. J’ai fait exprès de prendre un exemple très simple. On voit que malgré ça, on se pose beaucoup de questions et qu’à force de dérouler le problème, on y trouve plein de pièges. La date c’est compliqué. Retenez ça, la date c’est vraiment compliqué.
Prenez donc le temps pour gérer cela, ce qui compte c’est la réflexion et le cheminement que vous faites. J’ai envie de dire, si vous partez sur un flux d’intégration et que vous ne vous êtes pas pétés les dents sur les dates c’est que vous vous êtes trompé.
De façon générale, un bon développeur sait que lorsqu’une problématique est jugée trop simple, c’est qu’elle est incomprise.
Bonus : le feature flag
Si comme moi, vous avez dû gérer la problématique des dates en cours de route sur un système déjà actif en production, vous pouvez éventuellement avoir besoin d’un feature flag. Pour ma part j’avais de multiples sources et je n’étais pas sûr de pouvoir faire mon évolution d’un seul coup. J’ai des contraintes de validation de code, de déploiement et j’en passe.
Dans un système contraint, votre meilleur allié c’est le feature flag. Gardez votre système intact, et rajoutez votre nouvelle validation de kind sous feature flag. Autant dans votre pivot que dans vos sources. De cette façon, vous pouvez activer le feature flag une fois tous vos systèmes migrés. Et en bonus, vous pouvez tester dans des environnements qui ne sont pas la production.

