Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion Dapper/WrappedReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,28 @@ public override long GetChars(int i, long fieldoffset, char[]? buffer, int buffe
#if NET5_0_OR_GREATER
public override Task CloseAsync() => _reader.CloseAsync();

public override ValueTask DisposeAsync() => _reader.DisposeAsync();
public override async ValueTask DisposeAsync()
{
var cmd = _cmd;
try
{
await _reader.DisposeAsync().ConfigureAwait(false);
}
finally
{
_reader = DisposedReader.Instance;
_cmd = null!;
// the command may support async disposal even though it is held as IDbCommand
if (cmd is DbCommand typed)
{
await typed.DisposeAsync().ConfigureAwait(false);
}
else
{
cmd?.Dispose();
}
}
}

public override Task<ReadOnlyCollection<DbColumn>> GetColumnSchemaAsync(CancellationToken cancellationToken = default) => _reader.GetColumnSchemaAsync(cancellationToken);

Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<PackageVersion Include="RepoDb.SqlServer" Version="1.13.1" />
<PackageVersion Include="ServiceStack.OrmLite.SqlServer" Version="10.0.6" />
<PackageVersion Include="Snowflake.Data" Version="5.5.0" />
<PackageVersion Include="pengdows.crud.fakeDb" Version="2.0.5" />
<PackageVersion Include="SqlMarshal" Version="0.5.0" />
<PackageVersion Include="SubSonic" Version="3.0.0.4" />
<PackageVersion Include="Susanoo.SqlServer" Version="1.2.4.2" />
Expand Down
1 change: 1 addition & 0 deletions tests/Dapper.Tests/Dapper.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net481'">
<PackageReference Include="pengdows.crud.fakeDb" />
<PackageReference Include="Npgsql" />
<PackageReference Include="Snowflake.Data" />
</ItemGroup>
Expand Down
129 changes: 129 additions & 0 deletions tests/Dapper.Tests/FakeDbTests.Issue2185.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#if !NET481
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using pengdows.crud.fakeDb;
using Xunit;

namespace Dapper.Tests;

public sealed class FakeDbIssue2185Tests
{
[Fact]
public async Task ExecuteReaderAsyncDisposeAsyncDisposesCommand()
{
using var connection = new fakeDbConnection(new FakeDataStore());
connection.EnqueueReaderResult(new[] { new Dictionary<string, object?> { ["Value"] = 1 } });
connection.Open();

await using (var reader = await connection.ExecuteReaderAsync("select 1"))
{
Assert.True(await reader.ReadAsync());
}

Assert.NotNull(connection.LastCreatedCommand);
Assert.True(connection.LastCreatedCommand!.WasDisposed);
}

[Fact]
public async Task DisposeAsyncUsesAsyncDisposalOfDbCommand()
{
using var connection = new fakeDbConnection(new FakeDataStore());
connection.EnqueueReaderResult(new[] { new Dictionary<string, object?> { ["Value"] = 1 } });
connection.Open();
var cmd = new RecordingCommand(connection.CreateCommand());
var inner = await cmd.InnerCommand.ExecuteReaderAsync();

await using (new DbWrappedReader(cmd, inner)) { }

Assert.Equal(new[] { "DisposeAsync" }, cmd.Calls);
}

[Fact]
public async Task DisposeAsyncStillDisposesCommandWhenReaderDisposalThrows()
{
var cmd = new RecordingCommand(new fakeDbConnection(new FakeDataStore()).CreateCommand());
var wrapped = new DbWrappedReader(cmd, new ThrowingReader());

await Assert.ThrowsAsync<InvalidOperationException>(async () => await wrapped.DisposeAsync());

Assert.Equal(new[] { "DisposeAsync" }, cmd.Calls);
}

private sealed class ThrowingReader : DbDataReader
{
public override System.Threading.Tasks.ValueTask DisposeAsync() => throw new InvalidOperationException();
public override int Depth => 0;
public override int FieldCount => 0;
public override bool HasRows => false;
public override bool IsClosed => false;
public override int RecordsAffected => 0;
public override object this[int ordinal] => throw new NotSupportedException();
public override object this[string name] => throw new NotSupportedException();
public override bool GetBoolean(int ordinal) => throw new NotSupportedException();
public override byte GetByte(int ordinal) => throw new NotSupportedException();
public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) => throw new NotSupportedException();
public override char GetChar(int ordinal) => throw new NotSupportedException();
public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) => throw new NotSupportedException();
public override string GetDataTypeName(int ordinal) => throw new NotSupportedException();
public override DateTime GetDateTime(int ordinal) => throw new NotSupportedException();
public override decimal GetDecimal(int ordinal) => throw new NotSupportedException();
public override double GetDouble(int ordinal) => throw new NotSupportedException();
public override System.Collections.IEnumerator GetEnumerator() => throw new NotSupportedException();
public override Type GetFieldType(int ordinal) => throw new NotSupportedException();
public override float GetFloat(int ordinal) => throw new NotSupportedException();
public override Guid GetGuid(int ordinal) => throw new NotSupportedException();
public override short GetInt16(int ordinal) => throw new NotSupportedException();
public override int GetInt32(int ordinal) => throw new NotSupportedException();
public override long GetInt64(int ordinal) => throw new NotSupportedException();
public override string GetName(int ordinal) => throw new NotSupportedException();
public override int GetOrdinal(string name) => throw new NotSupportedException();
public override string GetString(int ordinal) => throw new NotSupportedException();
public override object GetValue(int ordinal) => throw new NotSupportedException();
public override int GetValues(object[] values) => throw new NotSupportedException();
public override bool IsDBNull(int ordinal) => throw new NotSupportedException();
public override bool NextResult() => false;
public override bool Read() => false;
}

// Delegating command that records which disposal path was taken.
private sealed class RecordingCommand : DbCommand
{
public RecordingCommand(DbCommand inner) => InnerCommand = inner;
public DbCommand InnerCommand { get; }
public List<string> Calls { get; } = new();

public override System.Threading.Tasks.ValueTask DisposeAsync()
{
Calls.Add("DisposeAsync");
return InnerCommand.DisposeAsync();
}

protected override void Dispose(bool disposing)
{
if (disposing) Calls.Add("Dispose");
InnerCommand.Dispose();
base.Dispose(disposing);
}

[System.Diagnostics.CodeAnalysis.AllowNull]
public override string CommandText { get => InnerCommand.CommandText; set => InnerCommand.CommandText = value; }
public override int CommandTimeout { get => InnerCommand.CommandTimeout; set => InnerCommand.CommandTimeout = value; }
public override CommandType CommandType { get => InnerCommand.CommandType; set => InnerCommand.CommandType = value; }
public override bool DesignTimeVisible { get => InnerCommand.DesignTimeVisible; set => InnerCommand.DesignTimeVisible = value; }
public override UpdateRowSource UpdatedRowSource { get => InnerCommand.UpdatedRowSource; set => InnerCommand.UpdatedRowSource = value; }
protected override DbConnection? DbConnection { get => InnerCommand.Connection; set => InnerCommand.Connection = value; }
protected override DbParameterCollection DbParameterCollection => InnerCommand.Parameters;
protected override DbTransaction? DbTransaction { get => InnerCommand.Transaction; set => InnerCommand.Transaction = value; }
public override void Cancel() => InnerCommand.Cancel();
public override int ExecuteNonQuery() => InnerCommand.ExecuteNonQuery();
public override object? ExecuteScalar() => InnerCommand.ExecuteScalar();
public override void Prepare() => InnerCommand.Prepare();
protected override DbParameter CreateDbParameter() => InnerCommand.CreateParameter();
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => InnerCommand.ExecuteReader(behavior);
}
}
#endif