Skip to content

ndcorder/ModernDotNetAnalyzer

Repository files navigation

ModernDotNetAnalyzer

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.

Installation

dotnet add package ModernDotNetAnalyzer

That's it. The analyzer integrates into your build process with zero configuration.

Diagnostics

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

Examples

MDA001: Async Deadlock Detection

// 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();
}

MDA002: Mutable Record Warning

// BAD - mutable property defeats record immutability
record Order
{
    public string Status { get; set; } // MDA002
}

// GOOD
record Order
{
    public string Status { get; init; }
}

MDA003: DateTime.Now Detection

// BAD - not testable
var now = DateTime.Now;    // MDA003
var utc = DateTime.UtcNow; // MDA003

// GOOD - use TimeProvider (introduced in .NET 8)
var now = timeProvider.GetLocalNow();

MDA004: CancellationToken Propagation

// 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);
}

MDA005: IAsyncDisposable Misuse

// BAD - synchronous disposal of async disposable
using var db = new MyDbContext(); // MDA005

// GOOD
await using var db = new MyDbContext();

MDA006: String Interpolation in Logging

// BAD - defeats structured logging
logger.LogInformation($"User {name} logged in"); // MDA006

// GOOD - use message template
logger.LogInformation("User {Name} logged in", name);

Configuration

Severity levels are configurable via .editorconfig:

[*.cs]
# Disable a specific rule
dotnet_diagnostic.MDA003.severity = none

# Treat as error
dotnet_diagnostic.MDA001.severity = error

Building from Source

dotnet build
dotnet test
dotnet pack src/ModernDotNetAnalyzer

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/new-analyzer)
  3. Write the analyzer in src/ModernDotNetAnalyzer/Analyzers/
  4. Add tests in tests/ModernDotNetAnalyzer.Tests/
  5. Ensure all tests pass (dotnet test)
  6. Submit a pull request

Adding a New Analyzer

  1. Create a new class inheriting from DiagnosticAnalyzer
  2. Define a DiagnosticDescriptor with the next available MDA ID
  3. Register syntax/semantic actions in Initialize()
  4. Add the diagnostic ID to DiagnosticIds.cs
  5. Write comprehensive tests (positive + negative cases)

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages