未验证 提交 d91d9046 编写于 作者: M Marc Gravell 提交者: GitHub

reinstate support for `IDataReader` (#1913)

* if we stop supporting DbDataReader, someone is going to complain; I'd
rather wrap it for now (to make the lib work), and add an analyzer
in AOT that shouts at them for using IDbConnection

also: compiler nits

* release notes
上级 677fee27
......@@ -1136,7 +1136,7 @@ private static async Task<DbDataReader> ExecuteWrappedReaderImplAsync(IDbConnect
var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, commandBehavior, command.CancellationToken).ConfigureAwait(false);
wasClosed = false;
disposeCommand = false;
return WrappedReader.Create(cmd, reader);
return DbWrappedReader.Create(cmd, reader);
}
finally
{
......
......@@ -14,7 +14,7 @@ public static partial class SqlMapper
/// <param name="reader">The data reader to parse results from.</param>
public static IEnumerable<T> Parse<T>(this IDataReader reader)
{
var dbReader = GetDbDataReader(reader, false);
var dbReader = GetDbDataReader(reader);
if (dbReader.Read())
{
var effectiveType = typeof(T);
......@@ -42,7 +42,7 @@ public static IEnumerable<T> Parse<T>(this IDataReader reader)
/// <param name="type">The type to parse from the <paramref name="reader"/>.</param>
public static IEnumerable<object> Parse(this IDataReader reader, Type type)
{
var dbReader = GetDbDataReader(reader, false);
var dbReader = GetDbDataReader(reader);
if (dbReader.Read())
{
var deser = GetDeserializer(type, dbReader, 0, -1, false);
......@@ -59,7 +59,7 @@ public static IEnumerable<object> Parse(this IDataReader reader, Type type)
/// <param name="reader">The data reader to parse results from.</param>
public static IEnumerable<dynamic> Parse(this IDataReader reader)
{
var dbReader = GetDbDataReader(reader, false);
var dbReader = GetDbDataReader(reader);
if (dbReader.Read())
{
var deser = GetDapperRowDeserializer(dbReader, 0, -1, false);
......@@ -86,7 +86,7 @@ public static IEnumerable<dynamic> Parse(this IDataReader reader)
public static Func<IDataReader, object> GetRowParser(this IDataReader reader, Type type,
int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false)
{
return WrapObjectReader(GetDeserializer(type, GetDbDataReader(reader, false), startIndex, length, returnNullIfFirstMissing));
return WrapObjectReader(GetDeserializer(type, GetDbDataReader(reader), startIndex, length, returnNullIfFirstMissing));
}
/// <summary>
......@@ -113,12 +113,12 @@ public static IEnumerable<dynamic> Parse(this IDataReader reader)
int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false)
{
concreteType ??= typeof(T);
var func = GetDeserializer(concreteType, GetDbDataReader(reader, false), startIndex, length, returnNullIfFirstMissing);
var func = GetDeserializer(concreteType, GetDbDataReader(reader), startIndex, length, returnNullIfFirstMissing);
return Wrap(func);
// this is just to be very clear about what we're capturing
static Func<IDataReader, T> Wrap(Func<DbDataReader, object> func)
=> reader => (T)func(GetDbDataReader(reader, false));
=> reader => (T)func(GetDbDataReader(reader));
}
/// <summary>
......
......@@ -58,7 +58,7 @@ private static void OnQueryCachePurged()
handler?.Invoke(null, EventArgs.Empty);
}
private static readonly System.Collections.Concurrent.ConcurrentDictionary<Identity, CacheInfo> _queryCache = new System.Collections.Concurrent.ConcurrentDictionary<Identity, CacheInfo>();
private static readonly System.Collections.Concurrent.ConcurrentDictionary<Identity, CacheInfo> _queryCache = new();
private static void SetQueryCache(Identity key, CacheInfo value)
{
if (Interlocked.Increment(ref collect) == COLLECT_PER_ITEMS)
......@@ -188,11 +188,11 @@ public TypeMapEntry(DbType dbType, TypeMapEntryFlags flags)
public override bool Equals(object obj) => obj is TypeMapEntry other && Equals(other);
public bool Equals(TypeMapEntry other) => other.DbType == DbType && other.Flags == Flags;
public static readonly TypeMapEntry
DoNotSet = new TypeMapEntry((DbType)(-2), TypeMapEntryFlags.None),
DecimalFieldValue = new TypeMapEntry(DbType.Decimal, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue);
DoNotSet = new((DbType)(-2), TypeMapEntryFlags.None),
DecimalFieldValue = new(DbType.Decimal, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue);
public static implicit operator TypeMapEntry(DbType dbType)
=> new TypeMapEntry(dbType, TypeMapEntryFlags.SetType);
=> new(dbType, TypeMapEntryFlags.SetType);
}
static SqlMapper()
......@@ -677,7 +677,7 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, string sql, obje
{
var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered);
var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out IDbCommand dbcmd);
return WrappedReader.Create(dbcmd, reader);
return DbWrappedReader.Create(dbcmd, reader);
}
/// <summary>
......@@ -693,7 +693,7 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, string sql, obje
public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinition command)
{
var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out IDbCommand dbcmd);
return WrappedReader.Create(dbcmd, reader);
return DbWrappedReader.Create(dbcmd, reader);
}
/// <summary>
......@@ -710,7 +710,7 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinitio
public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior)
{
var reader = ExecuteReaderImpl(cnn, ref command, commandBehavior, out IDbCommand dbcmd);
return WrappedReader.Create(dbcmd, reader);
return DbWrappedReader.Create(dbcmd, reader);
}
/// <summary>
......@@ -1842,13 +1842,13 @@ private static void PassByPosition(IDbCommand cmd)
{
if (cmd.Parameters.Count == 0) return;
Dictionary<string, IDbDataParameter> parameters = new Dictionary<string, IDbDataParameter>(StringComparer.Ordinal);
Dictionary<string, IDbDataParameter> parameters = new(StringComparer.Ordinal);
foreach (IDbDataParameter param in cmd.Parameters)
{
if (!string.IsNullOrEmpty(param.ParameterName)) parameters[param.ParameterName] = param;
}
HashSet<string> consumed = new HashSet<string>(StringComparer.Ordinal);
var consumed = new HashSet<string>(StringComparer.Ordinal);
bool firstMatch = true;
int index = 0; // use this to spoof names; in most pseudo-positional cases, the name is ignored, however:
// for "snowflake", the name needs to be incremental i.e. "1", "2", "3"
......@@ -1884,22 +1884,9 @@ private static void PassByPosition(IDbCommand cmd)
});
}
static DbDataReader GetDbDataReader(IDataReader reader, bool disposeOnFail = true)
static DbDataReader GetDbDataReader(IDataReader reader)
{
return reader as DbDataReader ?? Throw(reader, disposeOnFail);
static DbDataReader Throw(IDataReader reader, bool disposeOnFail)
{
if (reader is null)
{
throw new ArgumentNullException(nameof(reader));
}
if (disposeOnFail)
{
reader.Dispose(); // don't leak
}
// in reality, all providers have satisfied this since forever; we should have made Dapper target DbConnection, oops!
throw new NotSupportedException("The provided reader is required to be a DbDataReader, and is not");
}
return reader as DbDataReader ?? new WrappedBasicReader(reader);
}
private static Func<DbDataReader, object> GetDeserializer(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
......@@ -2171,7 +2158,7 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
}
var tmp = listParam.Value = SanitizeParameterValue(item);
if (tmp != null && !(tmp is DBNull))
if (tmp != null && tmp is not DBNull)
lastValue = tmp; // only interested in non-trivial values for padding
if (DynamicParameters.ShouldSetDbType(dbType) && listParam.DbType != dbType.GetValueOrDefault())
......@@ -2277,7 +2264,7 @@ private static bool TryStringSplit(ref IEnumerable list, int splitAt, string nam
private static bool TryStringSplit<T>(ref IEnumerable<T> list, int splitAt, string namePrefix, IDbCommand command, string colType, bool byPosition,
Action<StringBuilder, T> append)
{
if (!(list is ICollection<T> typed))
if (list is not ICollection<T> typed)
{
typed = list.ToList();
list = typed; // because we still need to be able to iterate it, even if we fail here
......@@ -2371,9 +2358,9 @@ private static IEnumerable<PropertyInfo> FilterParameters(IEnumerable<PropertyIn
}
// look for ? / @ / : *by itself*
private static readonly Regex smellsLikeOleDb = new Regex(@"(?<![\p{L}\p{N}@_])[?@:](?![\p{L}\p{N}@_])", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled),
literalTokens = new Regex(@"(?<![\p{L}\p{N}_])\{=([\p{L}\p{N}_]+)\}", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled),
pseudoPositional = new Regex(@"\?([\p{L}_][\p{L}\p{N}_]*)\?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex smellsLikeOleDb = new(@"(?<![\p{L}\p{N}@_])[?@:](?![\p{L}\p{N}@_])", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled),
literalTokens = new(@"(?<![\p{L}\p{N}_])\{=([\p{L}\p{N}_]+)\}", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled),
pseudoPositional = new(@"\?([\p{L}_][\p{L}\p{N}_]*)\?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
/// <summary>
/// Replace all literal tokens with their text form.
......@@ -2483,7 +2470,7 @@ internal static IList<LiteralToken> GetLiteralTokens(string sql)
var matches = literalTokens.Matches(sql);
var found = new HashSet<string>(StringComparer.Ordinal);
List<LiteralToken> list = new List<LiteralToken>(matches.Count);
var list = new List<LiteralToken>(matches.Count);
foreach (Match match in matches)
{
string token = match.Value;
......@@ -3091,7 +3078,7 @@ private static DbDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefi
return factory(index);
}
// cache of ReadViaGetFieldValueFactory<T> for per-value T
static readonly Hashtable s_ReadViaGetFieldValueCache = new Hashtable();
static readonly Hashtable s_ReadViaGetFieldValueCache = new();
static Func<DbDataReader, object> UnderlyingReadViaGetFieldValueFactory<T>(int index)
=> reader => reader.IsDBNull(index) ? null : reader.GetFieldValue<T>(index);
......@@ -3165,7 +3152,7 @@ public static ITypeMap GetTypeMap(Type type)
}
// use Hashtable to get free lockless reading
private static readonly Hashtable _typeMaps = new Hashtable();
private static readonly Hashtable _typeMaps = new();
/// <summary>
/// Set custom mapping for type deserializers
......@@ -3211,7 +3198,7 @@ public static void SetTypeMap(Type type, ITypeMap map)
Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false
)
{
return WrapObjectReader(GetTypeDeserializer(type, GetDbDataReader(reader, false), startBound, length, returnNullIfFirstMissing));
return WrapObjectReader(GetTypeDeserializer(type, GetDbDataReader(reader), startBound, length, returnNullIfFirstMissing));
}
private static Func<IDataReader, object> WrapObjectReader(Func<DbDataReader, object> dbReader)
......@@ -3684,10 +3671,7 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind
Type numericType = Enum.GetUnderlyingType(unboxType);
if (colType == typeof(string))
{
if (stringEnumLocal == null)
{
stringEnumLocal = il.DeclareLocal(typeof(string));
}
stringEnumLocal ??= il.DeclareLocal(typeof(string));
il.Emit(OpCodes.Castclass, typeof(string)); // stack is now [...][string]
il.Emit(OpCodes.Stloc, stringEnumLocal); // stack is now [...]
il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [...][enum-type-token]
......
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
......@@ -11,7 +13,7 @@ namespace Dapper
{
internal sealed class DisposedReader : DbDataReader
{
internal static readonly DisposedReader Instance = new DisposedReader();
internal static readonly DisposedReader Instance = new();
private DisposedReader() { }
public override int Depth => 0;
public override int FieldCount => 0;
......@@ -76,7 +78,7 @@ private async static Task<T> ThrowDisposedAsync<T>()
public override object this[string name] => ThrowDisposed<object>();
}
internal static class WrappedReader
internal sealed class DbWrappedReader : DbDataReader, IWrappedDataReader
{
// the purpose of wrapping here is to allow closing a reader to *also* close
// the command, without having to explicitly hand the command back to the
......@@ -89,9 +91,7 @@ public static DbDataReader Create(IDbCommand cmd, DbDataReader reader)
cmd.Dispose();
return null; // GIGO
}
}
internal sealed class DbWrappedReader : DbDataReader, IWrappedDataReader
{
private DbDataReader _reader;
private IDbCommand _cmd;
......@@ -200,5 +200,167 @@ protected override void Dispose(bool disposing)
public override Task<bool> ReadAsync(CancellationToken cancellationToken) => _reader.ReadAsync(cancellationToken);
public override int VisibleFieldCount => _reader.VisibleFieldCount;
protected override DbDataReader GetDbDataReader(int ordinal) => _reader.GetData(ordinal);
#if NET5_0_OR_GREATER
public override Task CloseAsync() => _reader.CloseAsync();
public override ValueTask DisposeAsync() => _reader.DisposeAsync();
public override Task<ReadOnlyCollection<DbColumn>> GetColumnSchemaAsync(CancellationToken cancellationToken = default) => _reader.GetColumnSchemaAsync(cancellationToken);
public override Task<DataTable> GetSchemaTableAsync(CancellationToken cancellationToken = default) => base.GetSchemaTableAsync(cancellationToken);
#endif
}
internal sealed class WrappedBasicReader : DbDataReader
{
private IDataReader _reader;
public WrappedBasicReader(IDataReader reader)
{
Debug.Assert(reader is not DbDataReader); // or we wouldn't be here!
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
}
public override bool HasRows => true; // have to assume that we do
public override void Close() => _reader.Close();
public override DataTable GetSchemaTable() => _reader.GetSchemaTable();
#if PLAT_NO_REMOTING
[Obsolete("This Remoting API is not supported and throws PlatformNotSupportedException.", DiagnosticId = "SYSLIB0010", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
#endif
public override object InitializeLifetimeService() => throw new NotSupportedException();
public override int Depth => _reader.Depth;
public override bool IsClosed => _reader.IsClosed;
public override bool NextResult() => _reader.NextResult();
public override bool Read() => _reader.Read();
public override int RecordsAffected => _reader.RecordsAffected;
protected override void Dispose(bool disposing)
{
if (disposing)
{
_reader.Close();
_reader.Dispose();
_reader = DisposedReader.Instance; // all future ops are no-ops
}
}
public override int FieldCount => _reader.FieldCount;
public override bool GetBoolean(int i) => _reader.GetBoolean(i);
public override byte GetByte(int i) => _reader.GetByte(i);
public override long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) =>
_reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length);
public override char GetChar(int i) => _reader.GetChar(i);
public override long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) =>
_reader.GetChars(i, fieldoffset, buffer, bufferoffset, length);
public override string GetDataTypeName(int i) => _reader.GetDataTypeName(i);
public override DateTime GetDateTime(int i) => _reader.GetDateTime(i);
public override decimal GetDecimal(int i) => _reader.GetDecimal(i);
public override double GetDouble(int i) => _reader.GetDouble(i);
public override Type GetFieldType(int i) => _reader.GetFieldType(i);
public override float GetFloat(int i) => _reader.GetFloat(i);
public override Guid GetGuid(int i) => _reader.GetGuid(i);
public override short GetInt16(int i) => _reader.GetInt16(i);
public override int GetInt32(int i) => _reader.GetInt32(i);
public override long GetInt64(int i) => _reader.GetInt64(i);
public override string GetName(int i) => _reader.GetName(i);
public override int GetOrdinal(string name) => _reader.GetOrdinal(name);
public override string GetString(int i) => _reader.GetString(i);
public override object GetValue(int i) => _reader.GetValue(i);
public override int GetValues(object[] values) => _reader.GetValues(values);
public override bool IsDBNull(int i) => _reader.IsDBNull(i);
public override object this[string name] => _reader[name];
public override object this[int i] => _reader[i];
public override T GetFieldValue<T>(int ordinal)
{
var value = _reader.GetValue(ordinal);
if (value is DBNull)
{
value = null;
}
return (T)value;
}
public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(GetFieldValue<T>(ordinal));
}
public override IEnumerator GetEnumerator() => _reader is IEnumerable e ? e.GetEnumerator()
: throw new NotImplementedException();
public override Type GetProviderSpecificFieldType(int ordinal) => _reader.GetFieldType(ordinal);
public override object GetProviderSpecificValue(int ordinal) => _reader.GetValue(ordinal);
public override int GetProviderSpecificValues(object[] values) => _reader.GetValues(values);
public override Stream GetStream(int ordinal) => throw new NotSupportedException();
public override TextReader GetTextReader(int ordinal) => throw new NotSupportedException();
public override Task<bool> IsDBNullAsync(int ordinal, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(_reader.IsDBNull(ordinal));
}
public override Task<bool> NextResultAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(_reader.NextResult());
}
public override Task<bool> ReadAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(_reader.Read());
}
public override int VisibleFieldCount => _reader.FieldCount;
protected override DbDataReader GetDbDataReader(int ordinal) => throw new NotSupportedException();
#if NET5_0_OR_GREATER
public override Task CloseAsync()
{
_reader.Close();
return Task.CompletedTask;
}
public override ValueTask DisposeAsync()
{
_reader.Dispose();
return default;
}
public override Task<ReadOnlyCollection<DbColumn>> GetColumnSchemaAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public override Task<DataTable> GetSchemaTableAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(_reader.GetSchemaTable());
}
#endif
}
}
......@@ -22,6 +22,8 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command
### unreleased
- reinstate fallback support for `IDataReader`, and implement missing `DbDataReader` async APIs
(note: new PRs will not be merged until they add release note wording here)
### 2.0.138
......
......@@ -96,7 +96,7 @@ public void TestTreatIntAsABool()
[Fact]
public void DiscriminatedUnion_IDataReader()
{
List<Discriminated_BaseType> result = new List<Discriminated_BaseType>();
var result = new List<Discriminated_BaseType>();
using (var reader = connection.ExecuteReader(@"
select 'abc' as Name, 1 as Type, 3.0 as Value
union all
......@@ -137,7 +137,7 @@ union all
[Fact]
public void DiscriminatedUnion_DbDataReader()
{
List<Discriminated_BaseType> result = new List<Discriminated_BaseType>();
var result = new List<Discriminated_BaseType>();
using (var reader = Assert.IsAssignableFrom<DbDataReader>(connection.ExecuteReader(@"
select 'abc' as Name, 1 as Type, 3.0 as Value
union all
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册