Commit 86902061 authored by Marc Gravell's avatar Marc Gravell

Add {Query|Read}FirstOrDefault methods with optimized paths; remove redundant...

Add {Query|Read}FirstOrDefault methods with optimized paths; remove redundant `return await` constructs (prefer to return the existing Task)
parent 337af552
......@@ -78,7 +78,7 @@ public static partial class SqlMapperExtensions
/// <param name="transaction">The transaction to run under, null (the defualt) if none</param>
/// <param name="commandTimeout">Number of seconds before command execution timeout</param>
/// <returns>Entity of T</returns>
public static async Task<IEnumerable<T>> GetAllAsync<T>(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
public static Task<IEnumerable<T>> GetAllAsync<T>(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
var type = typeof(T);
var cacheType = typeof(List<T>);
......@@ -95,9 +95,12 @@ public static partial class SqlMapperExtensions
if (!type.IsInterface())
{
return await connection.QueryAsync<T>(sql, null, transaction, commandTimeout);
return connection.QueryAsync<T>(sql, null, transaction, commandTimeout);
}
return GetAllAsyncImpl<T>(connection, transaction, commandTimeout, sql, type);
}
private static async Task<IEnumerable<T>> GetAllAsyncImpl<T>(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string sql, Type type) where T : class
{
var result = await connection.QueryAsync(sql);
var list = new List<T>();
foreach (IDictionary<string, object> res in result)
......@@ -124,7 +127,7 @@ public static partial class SqlMapperExtensions
/// <param name="commandTimeout">Number of seconds before command execution timeout</param>
/// <param name="sqlAdapter">The specific ISqlAdapter to use, auto-detected based on connection if null</param>
/// <returns>Identity of inserted entity</returns>
public static async Task<int> InsertAsync<T>(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null,
public static Task<int> InsertAsync<T>(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null,
int? commandTimeout = null, ISqlAdapter sqlAdapter = null) where T : class
{
var type = typeof(T);
......@@ -164,13 +167,13 @@ public static partial class SqlMapperExtensions
if (!isList) //single entity
{
return await sqlAdapter.InsertAsync(connection, transaction, commandTimeout, name, sbColumnList.ToString(),
return sqlAdapter.InsertAsync(connection, transaction, commandTimeout, name, sbColumnList.ToString(),
sbParameterList.ToString(), keyProperties, entityToInsert);
}
//insert list of entities
var cmd = $"INSERT INTO {name} ({sbColumnList}) values ({sbParameterList})";
return await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout);
return connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout);
}
/// <summary>
......
......@@ -130,10 +130,13 @@ public void Run(int iterations)
var mapperConnection = TestSuite.GetOpenConnection();
tests.Add(id => mapperConnection.Query<Post>("select * from Posts where Id = @Id", new { Id = id }, buffered: true).First(), "Mapper Query (buffered)");
tests.Add(id => mapperConnection.Query<Post>("select * from Posts where Id = @Id", new { Id = id }, buffered: false).First(), "Mapper Query (non-buffered)");
tests.Add(id => mapperConnection.QueryFirstOrDefault<Post>("select * from Posts where Id = @Id", new { Id = id }), "Mapper QueryFirstOrDefault");
var mapperConnection2 = TestSuite.GetOpenConnection();
tests.Add(id => mapperConnection2.Query("select * from Posts where Id = @Id", new { Id = id }, buffered: true).First(), "Dynamic Mapper Query (buffered)");
tests.Add(id => mapperConnection2.Query("select * from Posts where Id = @Id", new { Id = id }, buffered: false).First(), "Dynamic Mapper Query (non-buffered)");
tests.Add(id => mapperConnection2.QueryFirstOrDefault("select * from Posts where Id = @Id", new { Id = id }), "Dynamic Mapper QueryQueryFirstOrDefault");
// dapper.contrib
var mapperConnection3 = TestSuite.GetOpenConnection();
......
......@@ -156,6 +156,19 @@ public async Task TestMultiAsync()
}
}
[Fact]
public async Task TestMultiAsyncViaFirstOrDefault()
{
using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5"))
{
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(1);
multi.ReadAsync<int>().Result.Single().IsEqualTo(2);
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(3);
multi.ReadAsync<int>().Result.Single().IsEqualTo(4);
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(5);
}
}
[Fact]
public async Task TestMultiClosedConnAsync()
{
......@@ -166,6 +179,19 @@ public async Task TestMultiClosedConnAsync()
}
}
[Fact]
public async Task TestMultiClosedConnAsyncViaFirstOrDefault()
{
using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5;"))
{
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(1);
multi.ReadAsync<int>().Result.Single().IsEqualTo(2);
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(3);
multi.ReadAsync<int>().Result.Single().IsEqualTo(4);
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(5);
}
}
#if EXTERNALS
[Fact]
public async Task ExecuteReaderOpenAsync()
......
......@@ -1631,6 +1631,25 @@ public void TestChangingDefaultStringTypeMappingToAnsiString()
Dapper.SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String
}
[Fact]
public void TestChangingDefaultStringTypeMappingToAnsiStringFirstOrDefault()
{
var sql = "SELECT SQL_VARIANT_PROPERTY(CONVERT(sql_variant, @testParam),'BaseType') AS BaseType";
var param = new { testParam = "TestString" };
var result01 = connection.QueryFirstOrDefault<string>(sql, param);
result01.IsEqualTo("nvarchar");
Dapper.SqlMapper.PurgeQueryCache();
Dapper.SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString); // Change Default String Handling to AnsiString
var result02 = connection.QueryFirstOrDefault<string>(sql, param);
result02.IsEqualTo("varchar");
Dapper.SqlMapper.PurgeQueryCache();
Dapper.SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String
}
#if COREFX
class TransactedConnection : IDbConnection
{
......
......@@ -24,18 +24,27 @@ public static partial class SqlMapper
/// Execute a query asynchronously using .NET 4.5 Task.
/// </summary>
/// <remarks>Note: each row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public static async Task<IEnumerable<dynamic>> QueryAsync(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null)
public static Task<IEnumerable<dynamic>> QueryAsync(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null)
{
return await QueryAsync<DapperRow>(cnn, typeof(DapperRow), new CommandDefinition(sql, (object)param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken))).ConfigureAwait(false);
return QueryAsync<dynamic>(cnn, typeof(DapperRow), new CommandDefinition(sql, (object)param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken)));
}
/// <summary>
/// Execute a query asynchronously using .NET 4.5 Task.
/// </summary>
/// <remarks>Note: each row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public static async Task<IEnumerable<dynamic>> QueryAsync(this IDbConnection cnn, CommandDefinition command)
public static Task<IEnumerable<dynamic>> QueryAsync(this IDbConnection cnn, CommandDefinition command)
{
return await QueryAsync<DapperRow>(cnn, typeof(DapperRow), command).ConfigureAwait(false);
return QueryAsync<dynamic>(cnn, typeof(DapperRow), command);
}
/// <summary>
/// Execute a single-row query asynchronously using .NET 4.5 Task.
/// </summary>
/// <remarks>Note: the row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public static Task<dynamic> QueryFirstOrDefaultAsync(this IDbConnection cnn, CommandDefinition command)
{
return QueryFirstOrDefaultAsync<dynamic>(cnn, typeof(DapperRow), command);
}
/// <summary>
......@@ -46,6 +55,14 @@ public static Task<IEnumerable<T>> QueryAsync<T>(this IDbConnection cnn, string
return QueryAsync<T>(cnn, typeof(T), new CommandDefinition(sql, (object)param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken)));
}
/// <summary>
/// Execute a single-row query asynchronously using .NET 4.5 Task.
/// </summary>
public static Task<T> QueryFirstOrDefaultAsync<T>(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null)
{
return QueryFirstOrDefaultAsync<T>(cnn, typeof(T), new CommandDefinition(sql, (object)param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken)));
}
/// <summary>
/// Execute a query asynchronously using .NET 4.5 Task.
/// </summary>
......@@ -55,6 +72,15 @@ public static Task<IEnumerable<object>> QueryAsync(this IDbConnection cnn, Type
return QueryAsync<object>(cnn, type, new CommandDefinition(sql, (object)param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken)));
}
/// <summary>
/// Execute a single-row query asynchronously using .NET 4.5 Task.
/// </summary>
public static Task<object> QueryFirstOrDefaultAsync(this IDbConnection cnn, Type type, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return QueryFirstOrDefaultAsync<object>(cnn, type, new CommandDefinition(sql, (object)param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken)));
}
/// <summary>
/// Execute a query asynchronously using .NET 4.5 Task.
/// </summary>
......@@ -70,6 +96,13 @@ public static Task<IEnumerable<object>> QueryAsync(this IDbConnection cnn, Type
{
return QueryAsync<object>(cnn, type, command);
}
/// <summary>
/// Execute a single-row query asynchronously using .NET 4.5 Task.
/// </summary>
public static Task<object> QueryFirstOrDefaultAsync(this IDbConnection cnn, Type type, CommandDefinition command)
{
return QueryFirstOrDefaultAsync<object>(cnn, type, command);
}
private static async Task<IEnumerable<T>> QueryAsync<T>(this IDbConnection cnn, Type effectiveType, CommandDefinition command)
{
......@@ -133,6 +166,56 @@ private static async Task<IEnumerable<T>> QueryAsync<T>(this IDbConnection cnn,
}
}
}
private static async Task<T> QueryFirstOrDefaultAsync<T>(this IDbConnection cnn, Type effectiveType, CommandDefinition command)
{
object param = command.Parameters;
var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType(), null);
var info = GetCacheInfo(identity, param, command.AddToCache);
bool wasClosed = cnn.State == ConnectionState.Closed;
var cancel = command.CancellationToken;
using (var cmd = (DbCommand)command.SetupCommand(cnn, info.ParamReader))
{
DbDataReader reader = null;
try
{
if (wasClosed) await ((DbConnection)cnn).OpenAsync(cancel).ConfigureAwait(false);
reader = await cmd.ExecuteReaderAsync(wasClosed ? CommandBehavior.CloseConnection | CommandBehavior.SequentialAccess : CommandBehavior.SequentialAccess, cancel).ConfigureAwait(false);
T result = default(T);
if(await reader.ReadAsync(cancel).ConfigureAwait(false))
{
var tuple = info.Deserializer;
int hash = GetColumnHash(reader);
if (tuple.Func == null || tuple.Hash != hash)
{
tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false));
if (command.AddToCache) SetQueryCache(identity, info);
}
var func = tuple.Func;
object val = func(reader);
if (val == null || val is T)
{
result = (T)val;
}
else
{
var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
result = (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture);
}
while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { }
}
while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { }
return result;
}
finally
{
using (reader) { } // dispose if non-null
if (wasClosed) cnn.Close();
}
}
}
/// <summary>
/// Execute a command asynchronously using .NET 4.5 Task.
......
......@@ -32,6 +32,15 @@ public Task<IEnumerable<dynamic>> ReadAsync(bool buffered = true)
return ReadAsyncImpl<dynamic>(typeof(DapperRow), buffered);
}
/// <summary>
/// Read the first row of the next grid of results, returned as a dynamic object
/// </summary>
/// <remarks>Note: the row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public Task<dynamic> ReadFirstOrDefaultAsync()
{
return ReadFirstOrDefaultAsyncImpl<dynamic>(typeof(DapperRow));
}
/// <summary>
/// Read the next grid of results
/// </summary>
......@@ -40,6 +49,16 @@ public Task<IEnumerable<object>> ReadAsync(Type type, bool buffered = true)
if (type == null) throw new ArgumentNullException(nameof(type));
return ReadAsyncImpl<object>(type, buffered);
}
/// <summary>
/// Read the first row of the next grid of results
/// </summary>
public Task<object> ReadFirstOrDefaultAsync(Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ReadFirstOrDefaultAsyncImpl<object>(type);
}
/// <summary>
/// Read the next grid of results
/// </summary>
......@@ -48,6 +67,14 @@ public Task<IEnumerable<T>> ReadAsync<T>(bool buffered = true)
return ReadAsyncImpl<T>(typeof(T), buffered);
}
/// <summary>
/// Read the first row of the next grid of results
/// </summary>
public Task<T> ReadFirstOrDefaultAsync<T>()
{
return ReadFirstOrDefaultAsyncImpl<T>(typeof(T));
}
private async Task NextResultAsync()
{
if (await ((DbDataReader)reader).NextResultAsync(cancel).ConfigureAwait(false))
......@@ -94,6 +121,42 @@ private Task<IEnumerable<T>> ReadAsyncImpl<T>(Type type, bool buffered)
}
}
private Task<T> ReadFirstOrDefaultAsyncImpl<T>(Type type)
{
var dbReader = reader as DbDataReader;
if (dbReader != null) return ReadFirstOrDefaultAsyncImplImpl<T>(dbReader, type);
// no async API available; use non-async and fake it
return Task.FromResult<T>(ReadFirstOrDefaultImpl<T>(type));
}
private async Task<T> ReadFirstOrDefaultAsyncImplImpl<T>(DbDataReader reader, Type type)
{
if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed");
if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once");
IsConsumed = true;
T result = default(T);
if (await reader.ReadAsync(cancel).ConfigureAwait(false))
{
var typedIdentity = identity.ForGrid(type, gridIndex);
CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache);
var deserializer = cache.Deserializer;
int hash = GetColumnHash(reader);
if (deserializer.Func == null || deserializer.Hash != hash)
{
deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false));
cache.Deserializer = deserializer;
}
result = (T)deserializer.Func(reader);
while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { }
}
await NextResultAsync().ConfigureAwait(false);
return result;
}
private async Task<IEnumerable<T>> ReadBufferedAsync<T>(int index, Func<IDataReader, object> deserializer, Identity typedIdentity)
{
try
......
......@@ -39,6 +39,15 @@ public IEnumerable<dynamic> Read(bool buffered = true)
return ReadImpl<dynamic>(typeof(DapperRow), buffered);
}
/// <summary>
/// Read the first row of the next grid of results, returned as a dynamic object
/// </summary>
/// <remarks>Note: the row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public dynamic ReadFirstOrDefault()
{
return ReadFirstOrDefaultImpl<dynamic>(typeof(DapperRow));
}
/// <summary>
/// Read the next grid of results
/// </summary>
......@@ -47,6 +56,14 @@ public IEnumerable<T> Read<T>(bool buffered = true)
return ReadImpl<T>(typeof(T), buffered);
}
/// <summary>
/// Read the first row of the next grid of results
/// </summary>
public T ReadFirstOrDefault<T>()
{
return ReadFirstOrDefaultImpl<T>(typeof(T));
}
/// <summary>
/// Read the next grid of results
/// </summary>
......@@ -56,6 +73,15 @@ public IEnumerable<object> Read(Type type, bool buffered = true)
return ReadImpl<object>(type, buffered);
}
/// <summary>
/// Read the first row of the next grid of results
/// </summary>
public object ReadFirstOrDefault(Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ReadFirstOrDefaultImpl<object>(type);
}
private IEnumerable<T> ReadImpl<T>(Type type, bool buffered)
{
if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed");
......@@ -75,6 +101,32 @@ private IEnumerable<T> ReadImpl<T>(Type type, bool buffered)
return buffered ? result.ToList() : result;
}
private T ReadFirstOrDefaultImpl<T>(Type type)
{
if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed");
if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once");
IsConsumed = true;
T result = default(T);
if(reader.Read())
{
var typedIdentity = identity.ForGrid(type, gridIndex);
CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache);
var deserializer = cache.Deserializer;
int hash = GetColumnHash(reader);
if (deserializer.Func == null || deserializer.Hash != hash)
{
deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false));
cache.Deserializer = deserializer;
}
result = (T) deserializer.Func(reader);
while (reader.Read()) { }
}
NextResult();
return result;
}
private IEnumerable<TReturn> MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(Delegate func, string splitOn)
{
......
......@@ -579,7 +579,7 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinitio
}
/// <summary>
/// Return a list of dynamic objects, reader is closed after the call
/// Return a sequence of dynamic objects with properties matching the columns
/// </summary>
/// <remarks>Note: each row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)
......@@ -587,10 +587,18 @@ public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, obj
return Query<DapperRow>(cnn, sql, param as object, transaction, buffered, commandTimeout, commandType);
}
/// <summary>
/// Return a dynamic object with properties matching the columns
/// </summary>
/// <remarks>Note: the row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public static dynamic QueryFirstOrDefault(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null)
{
return QueryFirstOrDefault<DapperRow>(cnn, sql, param as object, transaction, commandTimeout, commandType);
}
/// <summary>
/// Executes a query, returning the data typed as per T
/// </summary>
/// <remarks>the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object</remarks>
/// <returns>A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is
/// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
/// </returns>
......@@ -604,7 +612,21 @@ public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, obj
}
/// <summary>
/// Executes a query, returning the data typed as per the Type suggested
/// Executes a single-row query, returning the data typed as per T
/// </summary>
/// <returns>A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is
/// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
/// </returns>
public static T QueryFirstOrDefault<T>(
this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null
)
{
var command = new CommandDefinition(sql, (object)param, transaction, commandTimeout, commandType, CommandFlags.None);
return QueryFirstOrDefaultImpl<T>(cnn, ref command, typeof(T));
}
/// <summary>
/// Executes a single-row query, returning the data typed as per the Type suggested
/// </summary>
/// <returns>A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is
/// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
......@@ -619,6 +641,20 @@ public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, obj
return command.Buffered ? data.ToList() : data;
}
/// <summary>
/// Executes a single-row query, returning the data typed as per the Type suggested
/// </summary>
/// <returns>A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is
/// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
/// </returns>
public static object QueryFirstOrDefault(
this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null
)
{
if (type == null) throw new ArgumentNullException(nameof(type));
var command = new CommandDefinition(sql, (object)param, transaction, commandTimeout, commandType, CommandFlags.None);
return QueryFirstOrDefaultImpl<object>(cnn, ref command, type);
}
/// <summary>
/// Executes a query, returning the data typed as per T
/// </summary>
/// <remarks>the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object</remarks>
......@@ -631,6 +667,17 @@ public static IEnumerable<T> Query<T>(this IDbConnection cnn, CommandDefinition
return command.Buffered ? data.ToList() : data;
}
/// <summary>
/// Executes a query, returning the data typed as per T
/// </summary>
/// <remarks>the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object</remarks>
/// <returns>A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is
/// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
/// </returns>
public static T QueryFirstOrDefault<T>(this IDbConnection cnn, CommandDefinition command)
{
return QueryFirstOrDefaultImpl<T>(cnn, ref command, typeof(T));
}
/// <summary>
......@@ -749,6 +796,73 @@ private static IEnumerable<T> QueryImpl<T>(this IDbConnection cnn, CommandDefini
}
}
private static T QueryFirstOrDefaultImpl<T>(this IDbConnection cnn, ref CommandDefinition command, Type effectiveType)
{
object param = command.Parameters;
var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType(), null);
var info = GetCacheInfo(identity, param, command.AddToCache);
IDbCommand cmd = null;
IDataReader reader = null;
bool wasClosed = cnn.State == ConnectionState.Closed;
try
{
cmd = command.SetupCommand(cnn, info.ParamReader);
if (wasClosed) cnn.Open();
reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection | CommandBehavior.SequentialAccess : CommandBehavior.SequentialAccess);
wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader
T result = default(T);
if (reader.Read() && reader.FieldCount != 0)
{
// with the CloseConnection flag, so the reader will deal with the connection; we
// still need something in the "finally" to ensure that broken SQL still results
// in the connection closing itself
var tuple = info.Deserializer;
int hash = GetColumnHash(reader);
if (tuple.Func == null || tuple.Hash != hash)
{
tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false));
if (command.AddToCache) SetQueryCache(identity, info);
}
var func = tuple.Func;
object val = func(reader);
if (val == null || val is T)
{
result = (T)val;
}
else {
var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
result = (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture);
}
while (reader.Read()) { }
}
while (reader.NextResult()) { }
// happy path; close the reader cleanly - no
// need for "Cancel" etc
reader.Dispose();
reader = null;
command.OnCompleted();
return result;
}
finally
{
if (reader != null)
{
if (!reader.IsClosed) try { cmd.Cancel(); }
catch { /* don't spoil the existing exception */ }
reader.Dispose();
}
if (wasClosed) cnn.Close();
cmd?.Dispose();
}
}
/// <summary>
/// Maps a query to objects
/// </summary>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment