A Roslyn analyzer catching async deadlocks, mutable records, and other modern C# pitfalls.
Built-in CA rules and Roslynator cover style and basic correctness, but miss nuanced modern C# antipatterns. Task.Result in async contexts causes deadlocks. Records with mutable properties defeat their purpose. DateTime.Now breaks testability. These patterns cause real production issues — this analyzer catches them.
dotnet add package ModernDotNetAnalyzerThat's it. The analyzer integrates into your build process with zero configuration.
| ID | Category | Severity | Description |
|---|---|---|---|
| MDA001 | Reliability | Warning | Synchronous block on async code (Task.Result, .Wait(), .GetAwaiter().GetResult()) |
| MDA002 | Design | Warning | Record type has mutable property (uses set instead of init) |
| MDA003 | Design | Info | DateTime.Now/UtcNow used instead of TimeProvider |
| MDA004 | Reliability | Warning | CancellationToken not propagated to async call |
| MDA005 | Usage | Warning | IAsyncDisposable disposed synchronously (using instead of await using) |
| MDA006 | Usage | Warning | String interpolation in ILogger call instead of structured logging template |
// BAD - causes deadlock in async context
async Task ProcessAsync()
{
var result = GetDataAsync().Result; // MDA001
GetDataAsync().Wait(); // MDA001
GetDataAsync().GetAwaiter().GetResult(); // MDA001
}
// GOOD
async Task ProcessAsync()
{
var result = await GetDataAsync();
}// BAD - mutable property defeats record immutability
record Order
{
public string Status { get; set; } // MDA002
}
// GOOD
record Order
{
public string Status { get; init; }
}// BAD - not testable
var now = DateTime.Now; // MDA003
var utc = DateTime.UtcNow; // MDA003
// GOOD - use TimeProvider (introduced in .NET 8)
var now = timeProvider.GetLocalNow();// BAD - token not propagated
async Task FetchAsync(CancellationToken ct)
{
await Task.Delay(100); // MDA004 - should pass ct
}
// GOOD
async Task FetchAsync(CancellationToken ct)
{
await Task.Delay(100, ct);
}// BAD - synchronous disposal of async disposable
using var db = new MyDbContext(); // MDA005
// GOOD
await using var db = new MyDbContext();// BAD - defeats structured logging
logger.LogInformation($"User {name} logged in"); // MDA006
// GOOD - use message template
logger.LogInformation("User {Name} logged in", name);Severity levels are configurable via .editorconfig:
[*.cs]
# Disable a specific rule
dotnet_diagnostic.MDA003.severity = none
# Treat as error
dotnet_diagnostic.MDA001.severity = errordotnet build
dotnet test
dotnet pack src/ModernDotNetAnalyzer- Fork the repository
- Create a feature branch (
git checkout -b feat/new-analyzer) - Write the analyzer in
src/ModernDotNetAnalyzer/Analyzers/ - Add tests in
tests/ModernDotNetAnalyzer.Tests/ - Ensure all tests pass (
dotnet test) - Submit a pull request
- Create a new class inheriting from
DiagnosticAnalyzer - Define a
DiagnosticDescriptorwith the next available MDA ID - Register syntax/semantic actions in
Initialize() - Add the diagnostic ID to
DiagnosticIds.cs - Write comprehensive tests (positive + negative cases)