diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index e3bfbf55..4f474568 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -55,7 +55,7 @@ public sealed class Sample The rule does not report a diagnostic for `IDbContextFactory.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() @@ -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(); } ```` diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index f33fbcfc..ee512c6c 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -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"); @@ -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; } @@ -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); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index 0f78cf01..d54b3d43 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -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(); + } }