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
17 changes: 17 additions & 0 deletions docs/Rules/MA0042.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ 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`.

````csharp
public async Task Sample()
{
// No diagnostic: MemoryStream does not override DisposeAsync,
// so using vs await using makes no difference.
using var ms = new MemoryStream();

// No diagnostic: SqlConnection does not override DisposeAsync.
using var connection1 = new SqlConnection(connectionString);

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

## Additional resources

- [Enforcing asynchronous code good practices using a Roslyn analyzer](https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm)
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ public Context(Compilation compilation)
ConsoleErrorAndOutSymbols = [];
}

MemoryStreamSymbol = compilation.GetBestTypeByMetadataName("System.IO.MemoryStream");
ProcessSymbol = compilation.GetBestTypeByMetadataName("System.Diagnostics.Process");
StreamSymbol = compilation.GetBestTypeByMetadataName("System.IO.Stream");
DbConnectionSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbConnection");
CancellationTokenSymbol = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken");
ObsoleteAttributeSymbol = compilation.GetBestTypeByMetadataName("System.ObsoleteAttribute");

Expand Down Expand Up @@ -122,8 +123,9 @@ public Context(Compilation compilation)
_taskAwaiterLikeSymbols = [.. taskAwaiterLikeSymbols];
}

private ISymbol? MemoryStreamSymbol { get; }
private ISymbol? StreamSymbol { get; }
private ISymbol? ProcessSymbol { get; }
private INamedTypeSymbol? DbConnectionSymbol { get; }
private ISymbol[] ConsoleErrorAndOutSymbols { get; }
private INamedTypeSymbol? CancellationTokenSymbol { get; }
private INamedTypeSymbol? ObsoleteAttributeSymbol { get; }
Expand Down Expand Up @@ -454,14 +456,66 @@ private bool HasDisposeAsyncMethod(INamedTypeSymbol symbol)
return false;
}

/// <summary>
/// Checks whether any type in the hierarchy from <paramref name="symbol"/> up to (but NOT including)
/// <paramref name="baseTypeSymbol"/> declares or overrides a <c>DisposeAsync</c> method.
/// Used to detect whether a subclass has a meaningful (truly async) <c>DisposeAsync</c> override,
/// as opposed to relying on an inherited implementation that is not truly asynchronous.
/// </summary>
private bool HasDisposeAsyncMethodDeclaredInSubclass(INamedTypeSymbol symbol, INamedTypeSymbol baseTypeSymbol)
{
var current = symbol;
while (current is not null && !current.IsEqualTo(baseTypeSymbol))
{
foreach (var member in current.GetMembers("DisposeAsync").OfType<IMethodSymbol>())
{
if (member.Parameters.Length != 0)
continue;

if (member.IsGenericMethod)
continue;

if (member.IsStatic)
continue;

if (!member.ReturnType.IsEqualTo(ValueTaskSymbol))
continue;

return true;
}

current = current.BaseType;
}

return false;
}

private bool CanBeAwaitUsing(IOperation operation)
{
if (operation.GetActualType() is not INamedTypeSymbol type)
return false;

// using var ms = new MemoryStream();
if (operation is IObjectCreationOperation objectCreationOperation && objectCreationOperation.Type.IsEqualTo(MemoryStreamSymbol))
return false;
// For Stream subclasses (including MemoryStream) created directly (new T()), only report
// if the concrete type being instantiated (or an intermediate subclass up to but not
// including Stream) actually overrides DisposeAsync. Stream.DisposeAsync merely calls
// Dispose() synchronously by default, so it is not a meaningful async override.
if (StreamSymbol is INamedTypeSymbol streamSymbol && type.InheritsFrom(streamSymbol))
{
var unwrapped = operation.UnwrapImplicitConversionOperations();
if (unwrapped is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, streamSymbol);
}

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

return HasDisposeAsyncMethod(type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2112,6 +2112,78 @@ await CreateProjectBuilder()
.ValidateAsync();
}

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

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

private MyStream CreateStream() => throw null;
}

class MyStream : Stream
{
public override bool CanRead => throw null;
public override bool CanSeek => throw null;
public override bool CanWrite => throw null;
public override long Length => throw null;
public override long Position { get => throw null; set => throw null; }
public override void Flush() => throw null;
public override int Read(byte[] buffer, int offset, int count) => throw null;
public override long Seek(long offset, SeekOrigin origin) => throw null;
public override void SetLength(long value) => throw null;
public override void Write(byte[] buffer, int offset, int count) => throw null;
}
""")
.ValidateAsync();
}

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

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

class MyStream : Stream
{
public override bool CanRead => throw null;
public override bool CanSeek => throw null;
public override bool CanWrite => throw null;
public override long Length => throw null;
public override long Position { get => throw null; set => throw null; }
public override void Flush() => throw null;
public override int Read(byte[] buffer, int offset, int count) => throw null;
public override long Seek(long offset, SeekOrigin origin) => throw null;
public override void SetLength(long value) => throw null;
public override void Write(byte[] buffer, int offset, int count) => throw null;
public override ValueTask DisposeAsync() => throw null;
}
""")
.ValidateAsync();
}


[Fact]
public async Task SemaphoreSlim_Wait_NoDiagnostic()
{
Expand Down Expand Up @@ -2326,4 +2398,150 @@ public async Task A()
""")
.ValidateAsync();
}

[Fact]
public async Task UsingNewDbConnectionSubclass_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 conn = new MySqlConnection();
}
}

class MySqlConnection : DbConnection
{
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw null;
protected override DbCommand CreateDbCommand() => throw null;
public override void ChangeDatabase(string databaseName) => throw null;
public override void Close() => throw null;
public override void Open() => throw null;
public override string ConnectionString { get => throw null; set => throw null; }
public override string Database => throw null;
public override string DataSource => throw null;
public override string ServerVersion => throw null;
public override ConnectionState State => throw null;
}
""")
.ValidateAsync();
}

[Fact]
public async Task UsingFactoryMethod_DbConnectionSubclass_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 conn = CreateConnection();|]
}

private MySqlConnection CreateConnection() => throw null;
}

class MySqlConnection : DbConnection
{
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw null;
protected override DbCommand CreateDbCommand() => throw null;
public override void ChangeDatabase(string databaseName) => throw null;
public override void Close() => throw null;
public override void Open() => throw null;
public override string ConnectionString { get => throw null; set => throw null; }
public override string Database => throw null;
public override string DataSource => throw null;
public override string ServerVersion => throw null;
public override ConnectionState State => throw null;
}
""")
.ValidateAsync();
}

[Fact]
public async Task UsingNewDbConnectionSubclass_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 conn = new MySqlConnection();|]
}
}

class MySqlConnection : DbConnection
{
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw null;
protected override DbCommand CreateDbCommand() => throw null;
public override void ChangeDatabase(string databaseName) => throw null;
public override void Close() => throw null;
public override void Open() => throw null;
public override string ConnectionString { get => throw null; set => throw null; }
public override string Database => throw null;
public override string DataSource => throw null;
public override string ServerVersion => throw null;
public override ConnectionState State => throw null;
public override ValueTask DisposeAsync() => throw null;
}
""")
.ValidateAsync();
}

[Fact]
public async Task UsingNewDbConnectionSubclass_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 conn = new DerivedConnection();|]
}
}

class BaseConnection : DbConnection
{
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw null;
protected override DbCommand CreateDbCommand() => throw null;
public override void ChangeDatabase(string databaseName) => throw null;
public override void Close() => throw null;
public override void Open() => throw null;
public override string ConnectionString { get => throw null; set => throw null; }
public override string Database => throw null;
public override string DataSource => throw null;
public override string ServerVersion => throw null;
public override ConnectionState State => throw null;
public override ValueTask DisposeAsync() => throw null;
}

class DerivedConnection : BaseConnection { }
""")
.ValidateAsync();
}
}
Loading