将.net DateTime的Dapper映射更改为使用DbType DateTime2,然后再次将其恢复原样

时间:2018-08-16 14:54:35

标签: c# sql-server datetime dapper datetime2

类似于问题How can I get Dapper to map .net DateTime to DateTime2;但我希望以后可以重新设置。

该问题的当前公认答案涉及更改Dapper源文件;但我使用的是NuGet软件包,因此对我不起作用。正如对已接受答案的第一条评论所指出的那样,这是不可逆的-“如果某些是DateTime而其他是DateTime2,该怎么办?” -这是我的情况:不同的查询需要不同的映射(每个查询只需一个即可。

我对同一问题使用higher-voted answer。但是,这种方法似乎是不可逆的。似乎保留第一个查询时设置的任何值,并且此后不可更改。

以下代码是MCVE。如果运行它,您会看到类型始终是 ,显示为“ datetime”,并且值的精确度永远不会超过毫秒(就像您期望的datetime一样);尽管尝试更改映射。然后,您必须注释掉对“ PerformDapperQuery()”的第一个调用,然后再次运行它:现在您将看到类型始终为“ ”,返回为“ datetime2”,并且值具有完整的7-精确到几分之一秒的数字精度(就像您期望的datetime2一样)。

public static void Main()
{
    // I know this is marked as obsolete, and I am open to suggestions for alternatives.
    // see https://github.com/StackExchange/Dapper/issues/798
#pragma warning disable 618
    var oldValue = SqlMapper.LookupDbType(typeof(DateTime), null, false, out var handler);
#pragma warning restore 618

    PerformDapperQuery();
    SqlMapper.AddTypeMap(typeof(DateTime), DbType.DateTime2);
    PerformDapperQuery();
    SqlMapper.AddTypeMap(typeof(DateTime), DbType.DateTime);
    PerformDapperQuery();
    SqlMapper.AddTypeMap(typeof(DateTime), oldValue);
    PerformDapperQuery();
 }

private static void PerformDapperQuery()
{
    using (var connection = new SqlConnection("server=localhost;Database=master;Integrated Security=SSPI;"))
    {
        var parameters = new { Param = DateTime.Now };
        using (var reader = connection.ExecuteReader(
            "SELECT sql_variant_property(@Param, 'BaseType'), CAST(@PARAM AS datetime2(7))", parameters))
        {
            Assert.That(reader.Read(), Is.True);
            string type = reader.GetString(0);
            DateTime value = reader.GetDateTime(1);
            Console.WriteLine($"Output: {type},{value:o}");
        }
    }
}

所以问题的第一部分是:如何多次更改Dapper的DateTime映射?问题的第二部分是我想恢复以前的映射。但正如您所看到的,LookupDbType被标记为过时,因此,如果有其他方法,我将很感兴趣。

解释缓存后的编辑是Damien_The_Unbeliever

我将上面的查询更改为$“ SELECT sql_variant_property(@Param,'BaseType'),CAST(@PARAM AS datetime2(7))-{DateTime.Now:o}”,因此它会有所不同每次,而且确实如此,这确实改变了行为。

之所以遇到这种情况,是因为我想添加一些内容来包装特定的Dapper查询,以使它们使用DateTime2而不是DateTime,所以我编写了此类:

internal sealed class DapperDateTime2MapperScope : IDisposable
{
    private readonly DbType? _predecessor;
    private bool _isDisposed;

    public DapperDateTime2MapperScope()
    {
        _predecessor = SqlMapperGetDbType();
        SqlMapper.AddTypeMap(typeof(DateTime), DbType.DateTime2);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!_isDisposed)
        {
            if (disposing)
            {
                if (_predecessor.HasValue)
                {
                    SqlMapper.AddTypeMap(typeof(DateTime), _predecessor.Value);
                }
            }

            _isDisposed = true;
        }
    }

    private DbType SqlMapperGetDbType()
    {
#pragma warning disable 618
        return SqlMapper.LookupDbType(typeof(DateTime), null, false, out var handler);
#pragma warning restore 618
    }
}

然后可以将其包装到using块中的Dapper查询,以使该查询使用DateTime2映射:

using (new DapperDateTime2MapperScope())
{
    -- Perform Dapper query here
}

然后我写了该类的单元测试-一个没有using的测试和一个带有using的测试;我发现单元测试彼此交互:它们可以单独工作,但是当所有测试运行时,一个或另一个测试将失败。原因(由于Damien的解释)是Dapper缓存查询。好消息是,我认为这很好-单元测试由于使用相同的查询而遭受了问题;但是在我的真实代码库中,如果我在此using中包装一个特定的查询,那么我将始终希望该查询使用该映射。因此,基本上这只是我的单元测试的问题,而不是真正使用该类的问题。

2 个答案:

答案 0 :(得分:1)

您的代码确实可以正确更改类型映射-但是dapper会积极地缓存查询。

如果您的实际查询在datetimedatetime2用例之间变化(我希望它们会有所不同),那应该没问题。否则,您可以自己清除查询缓存(但显然可能会带来其他的连锁反应):

        public static void Main()
        {
            // I know this is marked as obsolete, and I am open to suggestions for alternatives.
            // see https://github.com/StackExchange/Dapper/issues/798
#pragma warning disable 618
            var oldValue = SqlMapper.LookupDbType(typeof(DateTime), null, false, out var handler);
#pragma warning restore 618

            PerformDapperQuery();
            SqlMapper.AddTypeMap(typeof(DateTime), DbType.DateTime2);
            SqlMapper.PurgeQueryCache();
            PerformDapperQuery();
            SqlMapper.AddTypeMap(typeof(DateTime), DbType.DateTime);
            SqlMapper.PurgeQueryCache();
            PerformDapperQuery();
            SqlMapper.AddTypeMap(typeof(DateTime), oldValue);
            SqlMapper.PurgeQueryCache();
            PerformDapperQuery();
        }

对于您使用LookupDbType,我想您可以只使用GetDbType

答案 1 :(得分:-1)

您必须使用交易

        Func<DbCommand, string, object, DbParameter> CreateParameter = (cmd, parameterName, parameterValue) =>
        {
            var parameter = cmd.CreateParameter();
            parameter.ParameterName = parameterName;
            parameter.DbType = DbType.String;
            parameter.Direction = ParameterDirection.Input;
            parameter.Value = parameterValue;
            return parameter;
        };

        string sql = $"CREATE_RECORD @Application, @UpdatedTime; Select SCOPE_IDENTITY()";
        using (DbCommand command = CommandFactory.CreateCommand())
        {
            try
            {
                DbParameter[] parameters = new DbParameter[]
                {
                   CreateParameter(command, "@Application", escalation.Application),
                   CreateParameter(command, "@UpdatedTime", escalation.UpdatedTime)
                };

                command.CommandType = CommandType.Text;
                command.Connection = yourOpenConnection;
                command.Transaction = command.Connection.BeginTransaction(IsolationLevel.Serializable);
                command.Parameters.AddRange(parameters);
                int recID = (int)command.ExecuteScalar();
                command.Transaction.Commit();
                return recID;
            }
            catch (Exception ex)
            {
                command.Transaction.Rollback();
            }
        }
    }

重要的是

    command.Transaction = command.Connection.BeginTransaction(IsolationLevel.Serializable);
    //... 
    command.Transaction.Commit();
    //...
    command.Transaction.Rollback();

如果不提交,则所有查询都将被阻止以选择包含。如果您不想阻止此查询所涉及的数据库对象的读取,则可以使用 IsolationLevel.ReadCommited

IsolationLevel.Serializable 阻止与查询相关的任何访问,您的应用程序可能会创建难以解决的死锁。快照是一种很好的隔离模式,没有阻塞,但是需要更改访问selects中的元素的方式,并在每个查询上使用timestamp列,以了解查询是否脏了。

最后,函数CreateParameter只是在查询中创建参数的一种简单方法(这样可以避免 SQL注入)。