dotnet-quality

v2026.09.24

.NET code quality with Roslyn analyzers, StyleCop, and dotnet format. Covers linting, formatting, and best practices for C#/.NET. USE WHEN: user works with "C#", ".NET", "ASP.NET Core", asks about "Roslyn analyzers", "StyleCop", "dotnet format", ".NET linting", "C# best practices" DO NOT USE FOR: SonarQube - use `sonarqube` skill, testing - use .NET test skills, security - use `dotnet-security` skill

GitHub
Install command
npx skhub add claude-dev-suite/dotnet-quality
Markdown
SKILL.md

.NET Quality - Quick Reference

When NOT to Use This Skill

  • SonarQube setup - Use sonarqube skill
  • Security scanning - Use dotnet-security skill
  • Testing - Use .NET test skills

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: dotnet for comprehensive documentation.

Tool Overview

ToolFocusInstallation
dotnet formatFormatting + analyzersBuilt-in
Roslyn AnalyzersCode analysisNuGet
StyleCop.AnalyzersStyle rulesNuGet
RoslynatorRefactoringNuGet
SonarAnalyzer.CSharpSonarQube rulesNuGet

Roslyn Analyzers Setup

Install Analyzers

<!-- Directory.Build.props (solution-wide) -->
<Project>
  <PropertyGroup>
    <AnalysisLevel>latest-all</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
    <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Roslynator.Analyzers" Version="4.10.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>

.editorconfig

# .editorconfig
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.cs]
# Organize usings
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false

# this. qualification
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning

# Language keywords vs BCL types
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning

# var preferences
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion

# Expression-bodied members
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = false:suggestion
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_lambdas = true:suggestion

# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_pattern_matching_over_as_with_null_check = true:warning

# Null checking
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:warning
dotnet_style_coalesce_expression = true:warning
dotnet_style_null_propagation = true:warning

# Code style
csharp_prefer_braces = true:warning
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion

# Naming conventions
dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i

dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.capitalization = pascal_case

dotnet_naming_rule.private_field_should_be_camel_case_with_underscore.severity = warning
dotnet_naming_rule.private_field_should_be_camel_case_with_underscore.symbols = private_field
dotnet_naming_rule.private_field_should_be_camel_case_with_underscore.style = camel_case_underscore

dotnet_naming_symbols.private_field.applicable_kinds = field
dotnet_naming_symbols.private_field.applicable_accessibilities = private
dotnet_naming_style.camel_case_underscore.required_prefix = _
dotnet_naming_style.camel_case_underscore.capitalization = camel_case

# Analyzer severity
dotnet_diagnostic.CA1062.severity = warning  # Validate arguments
dotnet_diagnostic.CA1307.severity = warning  # Specify StringComparison
dotnet_diagnostic.CA1310.severity = warning  # Specify StringComparison for correctness
dotnet_diagnostic.CA2007.severity = none     # ConfigureAwait (not needed in ASP.NET Core)
dotnet_diagnostic.IDE0058.severity = none    # Expression value never used

# StyleCop
dotnet_diagnostic.SA1101.severity = none     # Prefix local calls with this
dotnet_diagnostic.SA1309.severity = none     # Field names must not begin with underscore
dotnet_diagnostic.SA1600.severity = none     # Elements should be documented
dotnet_diagnostic.SA1633.severity = none     # File should have header

dotnet format

Commands

# Check formatting
dotnet format --verify-no-changes

# Fix formatting
dotnet format

# Specific project
dotnet format ./src/MyProject

# Analyzers only
dotnet format analyzers

# Style only
dotnet format style

# Whitespace only
dotnet format whitespace

# With severity
dotnet format --severity warn

Common Analyzer Warnings

CA1062 - Validate Arguments

// BAD - No null check
public void Process(string input)
{
    Console.WriteLine(input.Length);  // CA1062
}

// GOOD - Null check
public void Process(string input)
{
    ArgumentNullException.ThrowIfNull(input);
    Console.WriteLine(input.Length);
}

// GOOD - Nullable reference type
public void Process(string? input)
{
    if (input is null) return;
    Console.WriteLine(input.Length);
}

CA1307/CA1310 - StringComparison

// BAD - Culture-dependent
if (name.Equals("admin"))  // CA1307

// GOOD - Explicit comparison
if (name.Equals("admin", StringComparison.OrdinalIgnoreCase))

// GOOD - For user-facing
if (name.Equals(otherName, StringComparison.CurrentCultureIgnoreCase))

CA2000 - Dispose Objects

// BAD - Not disposed
public void Process()
{
    var stream = new FileStream("file.txt", FileMode.Open);  // CA2000
    // forgot to dispose
}

// GOOD - Using statement
public void Process()
{
    using var stream = new FileStream("file.txt", FileMode.Open);
    // automatically disposed
}

IDE0090 - Use Target-typed new

// Before
List<string> items = new List<string>();

// After
List<string> items = new();

// Or with var
var items = new List<string>();

Common Code Smells & Fixes

1. God Class

// BAD - Does too much
public class OrderProcessor
{
    public void CreateOrder() { ... }
    public void SendEmail() { ... }
    public void GeneratePdf() { ... }
    public void CalculateTax() { ... }
}

// GOOD - Single responsibility
public class OrderService
{
    private readonly IEmailService _emailService;
    private readonly IPdfGenerator _pdfGenerator;
    private readonly ITaxCalculator _taxCalculator;

    public OrderService(
        IEmailService emailService,
        IPdfGenerator pdfGenerator,
        ITaxCalculator taxCalculator)
    {
        _emailService = emailService;
        _pdfGenerator = pdfGenerator;
        _taxCalculator = taxCalculator;
    }

    public async Task<Order> CreateOrderAsync(OrderRequest request)
    {
        var order = BuildOrder(request);
        order.Tax = _taxCalculator.Calculate(order);
        return order;
    }
}

2. Async/Await Pitfalls

// BAD - Blocking async
public void Process()
{
    var result = GetDataAsync().Result;  // Deadlock risk!
}

// BAD - Async void (except event handlers)
public async void ProcessAsync()  // Can't be awaited, exceptions lost
{
    await DoWorkAsync();
}

// GOOD - Async all the way
public async Task ProcessAsync()
{
    var result = await GetDataAsync();
}

// GOOD - When truly fire-and-forget
public void StartBackgroundWork()
{
    _ = Task.Run(async () =>
    {
        try { await DoWorkAsync(); }
        catch (Exception ex) { _logger.LogError(ex, "Background work failed"); }
    });
}

3. Nullable Reference Types

// Enable in .csproj
// <Nullable>enable</Nullable>

// BAD - Ignoring nullability
public string GetName(User user)
{
    return user.Name;  // Warning if Name is string?
}

// GOOD - Handle null
public string GetName(User user)
{
    return user.Name ?? "Unknown";
}

// GOOD - Return nullable
public string? GetName(User? user)
{
    return user?.Name;
}

4. Record Types for DTOs

// BAD - Mutable class with boilerplate
public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }

    // Equals, GetHashCode, ToString...
}

// GOOD - Immutable record
public record UserDto(int Id, string Name, string Email);

// With validation
public record UserDto
{
    public int Id { get; init; }
    public string Name { get; init; }
    public string Email { get; init; }

    public UserDto(int id, string name, string email)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(name);
        ArgumentException.ThrowIfNullOrWhiteSpace(email);

        Id = id;
        Name = name;
        Email = email;
    }
}

5. Pattern Matching

// BAD - Type checking with cast
if (shape is Circle)
{
    var circle = (Circle)shape;
    return circle.Radius * circle.Radius * Math.PI;
}

// GOOD - Pattern matching
if (shape is Circle circle)
{
    return circle.Radius * circle.Radius * Math.PI;
}

// GOOD - Switch expression
return shape switch
{
    Circle c => c.Radius * c.Radius * Math.PI,
    Rectangle r => r.Width * r.Height,
    _ => throw new ArgumentException("Unknown shape")
};

Pre-commit Setup

.pre-commit-config.yaml

repos:
  - repo: local
    hooks:
      - id: dotnet-format
        name: dotnet format
        entry: dotnet format --verify-no-changes
        language: system
        types: [c#]
        pass_filenames: false

      - id: dotnet-build
        name: dotnet build
        entry: dotnet build --no-restore -warnaserror
        language: system
        types: [c#]
        pass_filenames: false

Quality Metrics Targets

MetricTargetTool
Cyclomatic Complexity< 10Roslynator
Method Lines< 50StyleCop
Parameters< 5CA1026
Test Coverage> 80%coverlet
Maintainability Index> 60VS Metrics

CI/CD Integration

GitHub Actions

name: Quality
on: [push, pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Restore
        run: dotnet restore

      - name: Format check
        run: dotnet format --verify-no-changes

      - name: Build
        run: dotnet build --no-restore -warnaserror

      - name: Test
        run: dotnet test --no-build --collect:"XPlat Code Coverage"

      - name: Upload coverage
        uses: codecov/codecov-action@v4

VS Code / Rider Settings

// .vscode/settings.json
{
  "omnisharp.enableEditorConfigSupport": true,
  "omnisharp.enableRoslynAnalyzers": true,
  "[csharp]": {
    "editor.defaultFormatter": "ms-dotnettools.csharp",
    "editor.formatOnSave": true
  }
}

Anti-Patterns

Anti-PatternWhy It's BadCorrect Approach
.Result / .Wait()Deadlock riskUse async/await
async voidExceptions lostUse async Task
Ignoring CA warningsReal issues hiddenFix or suppress with reason
No nullable reference typesNullReferenceExceptionEnable <Nullable>enable</Nullable>
#pragma warning disableHides issuesFix or suppress specifically
Mutable DTOsUnexpected changesUse records or init-only

Quick Troubleshooting

IssueLikely CauseSolution
Analyzer not runningPackage not installedAdd to .csproj
Too many warningsFirst-time enableSuppress and fix incrementally
Format changes on buildDifferent settingsCommit .editorconfig
Nullable warnings everywhereLegacy codeEnable gradually per project
StyleCop conflictsDifferent conventionsConfigure in .editorconfig

Related Skills

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/quality/dotnet-quality

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1