Skip to content
Merged
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
8 changes: 7 additions & 1 deletion docs/Rules/MA0042.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public sealed class Sample

The rule does not report a diagnostic for `IDbContextFactory<TContext>.CreateDbContext()`. The `CreateDbContextAsync()` overload was introduced only for specific edge-case scenarios where the factory itself must perform asynchronous initialization, and is not intended as a general-purpose replacement. See [dotnet/efcore#26630](https://github.com/dotnet/efcore/issues/26630) for more details.

The rule will not report a diagnostic for a `using` statement on a `Stream` or `DbConnection` subclass that is directly instantiated with `new` when the concrete type does not override `DisposeAsync`. Both `Stream.DisposeAsync` and `DbConnection.DisposeAsync` merely call `Dispose()` synchronously by default, so switching to `await using` brings no benefit for such types. When the instance is obtained from a factory method rather than a direct `new` expression, the rule still reports a diagnostic because the runtime type may be a deeper subclass that does override `DisposeAsync`.
The rule will not report a diagnostic for a `using` statement on a `Stream`, `DbConnection`, or `DbCommand` subclass that is directly instantiated with `new` when the concrete type does not override `DisposeAsync`. `Stream.DisposeAsync`, `DbConnection.DisposeAsync`, and `DbCommand.DisposeAsync` merely call `Dispose()` synchronously by default, so switching to `await using` brings no benefit for such types. When the instance is obtained from a factory method rather than a direct `new` expression, the rule still reports a diagnostic because the runtime type may be a deeper subclass that does override `DisposeAsync`.

````csharp
public async Task Sample()
Expand All @@ -69,6 +69,12 @@ public async Task Sample()

// Diagnostic: obtained from a factory, runtime type may override DisposeAsync.
using var connection2 = CreateConnection();

// No diagnostic: SqlCommand does not override DisposeAsync.
using var command1 = new SqlCommand();

// Diagnostic: obtained from a factory, runtime type may override DisposeAsync.
using var command2 = CreateCommand();
}
````

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public Context(Compilation compilation)
ProcessSymbol = compilation.GetBestTypeByMetadataName("System.Diagnostics.Process");
StreamSymbol = compilation.GetBestTypeByMetadataName("System.IO.Stream");
DbConnectionSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbConnection");
DbCommandSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbCommand");
CancellationTokenSymbol = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken");
ObsoleteAttributeSymbol = compilation.GetBestTypeByMetadataName("System.ObsoleteAttribute");

Expand Down Expand Up @@ -126,6 +127,7 @@ public Context(Compilation compilation)
private ISymbol? StreamSymbol { get; }
private ISymbol? ProcessSymbol { get; }
private INamedTypeSymbol? DbConnectionSymbol { get; }
private INamedTypeSymbol? DbCommandSymbol { get; }
private ISymbol[] ConsoleErrorAndOutSymbols { get; }
private INamedTypeSymbol? CancellationTokenSymbol { get; }
private INamedTypeSymbol? ObsoleteAttributeSymbol { get; }
Expand Down Expand Up @@ -517,6 +519,17 @@ private bool CanBeAwaitUsing(IOperation operation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, DbConnectionSymbol);
}

// For DbCommand subclasses created directly (new T()), only report if the exact
// type being instantiated (or an intermediate subclass up to but not including
// DbCommand) actually overrides DisposeAsync. DbCommand.DisposeAsync just calls
// Dispose() synchronously, so it is not a meaningful async override.
if (DbCommandSymbol is not null && type.InheritsFrom(DbCommandSymbol))
{
var unwrapped = operation.UnwrapImplicitConversionOperations();
if (unwrapped is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, DbCommandSymbol);
}

return HasDisposeAsyncMethod(type);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2589,4 +2589,166 @@ class DerivedConnection : BaseConnection { }
""")
.ValidateAsync();
}

[Fact]
public async Task UsingNewDbCommandSubclass_NoDisposeAsyncOverride_NoDiagnostic()
{
await CreateProjectBuilder()
.WithTargetFramework(TargetFramework.Net8_0)
.WithSourceCode("""
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;

class Test
{
public async Task A()
{
using var command = new MyDbCommand();
}
}

class MyDbCommand : DbCommand
{
public override string CommandText { get => throw null; set => throw null; }
public override int CommandTimeout { get => throw null; set => throw null; }
public override CommandType CommandType { get => throw null; set => throw null; }
public override bool DesignTimeVisible { get => throw null; set => throw null; }
public override UpdateRowSource UpdatedRowSource { get => throw null; set => throw null; }
protected override DbConnection DbConnection { get => throw null; set => throw null; }
protected override DbParameterCollection DbParameterCollection => throw null;
protected override DbTransaction DbTransaction { get => throw null; set => throw null; }
public override void Cancel() => throw null;
public override int ExecuteNonQuery() => throw null;
public override object ExecuteScalar() => throw null;
public override void Prepare() => throw null;
protected override DbParameter CreateDbParameter() => throw null;
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw null;
}
""")
.ValidateAsync();
}

[Fact]
public async Task UsingFactoryMethod_DbCommandSubclass_NoDisposeAsyncOverride_Diagnostic()
{
await CreateProjectBuilder()
.WithTargetFramework(TargetFramework.Net8_0)
.WithSourceCode("""
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;

class Test
{
public async Task A()
{
[|using var command = CreateCommand();|]
}

private MyDbCommand CreateCommand() => throw null;
}

class MyDbCommand : DbCommand
{
public override string CommandText { get => throw null; set => throw null; }
public override int CommandTimeout { get => throw null; set => throw null; }
public override CommandType CommandType { get => throw null; set => throw null; }
public override bool DesignTimeVisible { get => throw null; set => throw null; }
public override UpdateRowSource UpdatedRowSource { get => throw null; set => throw null; }
protected override DbConnection DbConnection { get => throw null; set => throw null; }
protected override DbParameterCollection DbParameterCollection => throw null;
protected override DbTransaction DbTransaction { get => throw null; set => throw null; }
public override void Cancel() => throw null;
public override int ExecuteNonQuery() => throw null;
public override object ExecuteScalar() => throw null;
public override void Prepare() => throw null;
protected override DbParameter CreateDbParameter() => throw null;
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw null;
}
""")
.ValidateAsync();
}

[Fact]
public async Task UsingNewDbCommandSubclass_WithDisposeAsyncOverride_Diagnostic()
{
await CreateProjectBuilder()
.WithTargetFramework(TargetFramework.Net8_0)
.WithSourceCode("""
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;

class Test
{
public async Task A()
{
[|using var command = new MyDbCommand();|]
}
}

class MyDbCommand : DbCommand
{
public override string CommandText { get => throw null; set => throw null; }
public override int CommandTimeout { get => throw null; set => throw null; }
public override CommandType CommandType { get => throw null; set => throw null; }
public override bool DesignTimeVisible { get => throw null; set => throw null; }
public override UpdateRowSource UpdatedRowSource { get => throw null; set => throw null; }
protected override DbConnection DbConnection { get => throw null; set => throw null; }
protected override DbParameterCollection DbParameterCollection => throw null;
protected override DbTransaction DbTransaction { get => throw null; set => throw null; }
public override void Cancel() => throw null;
public override int ExecuteNonQuery() => throw null;
public override object ExecuteScalar() => throw null;
public override void Prepare() => throw null;
protected override DbParameter CreateDbParameter() => throw null;
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw null;
public override ValueTask DisposeAsync() => throw null;
}
""")
.ValidateAsync();
}

[Fact]
public async Task UsingNewDbCommandSubclass_DisposeAsyncOverriddenInIntermediateBase_Diagnostic()
{
await CreateProjectBuilder()
.WithTargetFramework(TargetFramework.Net8_0)
.WithSourceCode("""
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;

class Test
{
public async Task A()
{
[|using var command = new DerivedDbCommand();|]
}
}

class BaseDbCommand : DbCommand
{
public override string CommandText { get => throw null; set => throw null; }
public override int CommandTimeout { get => throw null; set => throw null; }
public override CommandType CommandType { get => throw null; set => throw null; }
public override bool DesignTimeVisible { get => throw null; set => throw null; }
public override UpdateRowSource UpdatedRowSource { get => throw null; set => throw null; }
protected override DbConnection DbConnection { get => throw null; set => throw null; }
protected override DbParameterCollection DbParameterCollection => throw null;
protected override DbTransaction DbTransaction { get => throw null; set => throw null; }
public override void Cancel() => throw null;
public override int ExecuteNonQuery() => throw null;
public override object ExecuteScalar() => throw null;
public override void Prepare() => throw null;
protected override DbParameter CreateDbParameter() => throw null;
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw null;
public override ValueTask DisposeAsync() => throw null;
}

class DerivedDbCommand : BaseDbCommand { }
""")
.ValidateAsync();
}
}
Loading