web-dev-qa-db-fra.com

Obtenez DateTime en UTC avec Dapper

J'utilise Dapper pour mapper mes entités à SQL Server CE. Si j'enregistre un DateTime avec Kind=Utc, quand je le relis, j'obtiens un DateTime avec Kind=Unspecified, ce qui entraîne toutes sortes de problèmes.

Exemple:

var f = new Foo { Id = 42, ModificationDate = DateTime.UtcNow };
Console.WriteLine("{0} ({1})", f.ModificationDate, f.ModificationDate.Kind);
connection.Execute("insert into Foo(Id, ModificationDate) values(@Id, @ModificationDate)", f);
var f2 = connection.Query<Foo>("select * from Foo where Id = @Id", f).Single();
Console.WriteLine("{0} ({1})", f2.ModificationDate, f2.ModificationDate.Kind);

Ce code donne la sortie suivante:

20/09/2012 10:04:16 (Utc)
20/09/2012 10:04:16 (Unspecified)

Je sais que je devrais utiliser un DateTimeOffset, mais malheureusement, SQL CE ne prend pas en charge ce type.

Y at-il un travail autour? Puis-je dire à Dapper de supposer que toutes les dates ont DateTimeKind.Utc? Et plus généralement, quelles sont mes options pour personnaliser le mappage?


EDIT: Ma solution de contournement actuelle consiste à corriger les dates après que Dapper a matérialisé le résultat, mais ça sent un peu ...

var results = _connection.Query<Foo>(sql, param).Select(PatchDate);

...

static Foo PatchDate(Foo f)
{
    if (f.ModificationDate.Kind == DateTimeKind.Unspecified)
        f.ModificationDate = DateTime.SpecifyKind(f.ModificationDate, DateTimeKind.Utc);
    return f;
}
56
Thomas Levesque

Ajout de cette réponse pour toute autre personne qui recherche une solution simple. Cela est désormais possible avec l'ajout de SqlMapper.TypeHandler dans Dapper.

Ajoutez cette classe pour convertir la valeur de la base de données en un datetime avec le type spécifié en UTC.

public class DateTimeHandler : SqlMapper.TypeHandler<DateTime>
{
    public override void SetValue(IDbDataParameter parameter, DateTime value)
    {
        parameter.Value = value;
    }

    public override DateTime Parse(object value)
    {
        return DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc);
    }
}

Ensuite, dans mon fichier Global.asax de mon API Web, j'ajoute le gestionnaire de type à dapper.

SqlMapper.AddTypeHandler(new DateTimeHandler());

Si vous devez vous assurer que vous insérez toujours des dates au format UTC, vous pouvez utiliser la méthode SetValue:

parameter.Value = DateTime.SpecifyKind(value, DateTimeKind.Utc);
62
Matt Jenkins

Regardé dans le code Dapper. À moins que le mien ne soit obsolète, pour les types de valeur comme datetime (qui est mappé sur DbType.DateTime), dapper effectue simplement une conversion simple à partir de l'objet IDataReader.

Pseudo: return return (DateTime) IDataReader.GetValue (0);

C'est le cas spécifique de Datetime sur un tas de code générique et de lambdas.

AFAIK, SQL datetime ne stocke jamais l'offset/le fuseau horaire, donc le type dira toujours "Non spécifié" à n'importe quelle heure que vous stockez et récupérez.

Donc, pour le faire propre, vous pouvez toucher des éléments internes élégants:

ce qui est pénible car vous devez toucher une grande méthode de génération d'IL (le désérialiseur DataRow) et mettre un cas if pour DateTime.

OR

il suffit de mettre un setter sur les accessoires DateTime où UTC est un problème (ce qui est un peu contre POCO mais est relativement sain d'esprit):

class Foo
{
    private DateTime _modificationDate;
    public DateTime ModificationDate
    {
        get { return _modificationDate; }
        set { _modificationDate = DateTime.SpecifyKind(value, DateTimeKind.Utc); }
    }
    //Ifs optional? since it's always going to be a UTC date, and any DB call will return unspecified anyways
}
27
Vivek

Je voulais juste mettre ma solution complète ici pour intégrer de façon transparente DateTimeOffset/DateTimeOffset? champs/propriétés avec une base de données MySQL 5.7 (qui ne prend pas en charge DbType.DateTimeOffset) - basé sur la réponse de @ matt-jenkins ci-dessus:

public static class DapperExtensions
{
    class DateTimeOffsetTypeHandler : SqlMapper.TypeHandler<DateTimeOffset>
    {
        public override void SetValue(IDbDataParameter parameter, DateTimeOffset value)
        {
            switch (parameter.DbType)
            {
                case DbType.DateTime:
                case DbType.DateTime2:
                case DbType.AnsiString: // Seems to be some MySQL type mapping here
                    parameter.Value = value.UtcDateTime;
                    break;
                case DbType.DateTimeOffset:
                    parameter.Value = value;
                    break;
                default:
                    throw new InvalidOperationException("DateTimeOffset must be assigned to a DbType.DateTime SQL field.");
            }
        }

        public override DateTimeOffset Parse(object value)
        {
            switch (value)
            {
                case DateTime time:
                    return new DateTimeOffset(DateTime.SpecifyKind(time, DateTimeKind.Utc), TimeSpan.Zero);
                case DateTimeOffset dto:
                    return dto;
                default:
                    throw new InvalidOperationException("Must be DateTime or DateTimeOffset object to be mapped.");
            }
        }
    }


    private static int DateTimeOffsetMapperInstalled = 0;

    public static void InstallDateTimeOffsetMapper()
    {
        // Assumes SqlMapper.ResetTypeHandlers() is never called.
        if (Interlocked.CompareExchange(ref DateTimeOffsetMapperInstalled, 1, 0) == 0)
        {
            // First remove the default type map between typeof(DateTimeOffset) => DbType.DateTimeOffset (not valid for MySQL)
            SqlMapper.RemoveTypeMap(typeof(DateTimeOffset));
            SqlMapper.RemoveTypeMap(typeof(DateTimeOffset?));

            // This handles nullable value types automatically e.g. DateTimeOffset?
            SqlMapper.AddTypeHandler(typeof(DateTimeOffset), new DateTimeOffsetTypeHandler());
        }
    }
}
3
jamespconnor

Si vous utilisez Dapper à partir de la source (pas nuget), vous pouvez modifier le code pour forcer toujours DateTimeKind d'UTC. Une option plus configurable peut consister à créer un nouvel attribut pour les valeurs de propriété DateTime qui vous permet de spécifier le type date-heure comme indice pour dapper. Dapper peut rechercher les propriétés DateTime avec cet attribut et, lorsqu'il le trouve, peut l'utiliser pour spécifier le type DateTime lors du mappage ORM. Cela pourrait être une fonctionnalité intéressante pour Core Dapper car vous n'êtes pas le seul à avoir ce problème :)

1
user2368632