Für Entwickler, Architekten, Projektleiter und...

Read more

In der Welt der Softwareentwicklung ist die...

Read more

QUIBIQ spendet für den guten Zweck – und für...

Read more

Eine bestimmte Antwort auf einen HTTP Request zu...

Read more

In einer Welt, die von stetigem Wandel geprägt...

Read more

In einem unserer Kundenprojekte, war das Ziel eine...

Read more

QUIBIQ Hamburg wird mit dem Hamburger...

Read more

Zwei Tage lang wurde vom 14.-15.11 wieder das...

Read more

Was ist ein Excel-Plugin – und wann ist es...

Read more

Wir expandieren, bringen Kunden und Talente besser...

Read more

How-to: C# Roslyn Analyzer für async void

In C# ist es möglich in einer Methodendeklaration async und void in Kombination zu verwenden und im Fall von UI Events ist es sogar nicht anders möglich. Async void kann das Programm jedoch sehr leicht zum Absturz bringen, sollte eine Exception geworfen werden, weil diese nicht einfach gefangen werden kann.

Dieses kurze Programm sorgt dafür, dass die gesamte Anwendung abstürzt, obwohl es mit einem try-catch umschlossen ist.

public class Program
{
    public static async Task Main()
    {
       
try
       
{
           
DangerousMethod();
           
await Task.Delay(TimeSpan.FromMilliseconds(2000));
        }
       
catch(Exception ex)
        {
           
Console.WriteLine(ex.Message);
        }

       
Console.WriteLine("Continue work.");
    }

   
public static async void DangerousMethod()
    {
       
await Task.Delay(TimeSpan.FromMilliseconds(1000));

       
throw new Exception("Uh oh!");
    }
}

In der Konsole erscheint nur folgende Meldung:

Unhandled exception. System.Exception: Uh oh!
   at AsyncVoidRoslyn.Program.DangerousMethod() in C: \AsyncVoidRoslyn\Program.cs:line 30
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_1(Object state)
   at System.Threading.QueueUserWorkItemCallbackDefaultContext.Execute()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

Keine der gewünschten Konsolenausgaben erscheint und das Programm wird beendet.
Das kann zu einem Fehler führen, der nur sehr schwer zu finden ist.

Wenn nun eine Codebase existiert, die von vielen Entwicklern über eine lange Zeit aufgebaut wurde kann es dazu führen, dass eine void Methode mit einem async erweitert wurde, ohne den Rückgabewert auf Task anzupassen.

Jetzt kann man natürlich hingehen und alle async void auf async Task umschreiben. Und wenn man konsequent sein will, muss man Task immer weiter nach oben durchziehen und erst dann fällt auf wie hoch die technischen Schulden eigentlich sind.

Um diese technische Schuld frühzeitig zu bekämpfen kann ein selbst geschriebener Roslyn Analyzer verwendet werden.
 

Was ist ein Roslyn Analyzer?

Roslyn ist ein Compiler für .NET und bietet eine API an, um mit diesem Compiler zu sprechen. Damit kann .NET Code analysiert werden. Das eigene Parsen einer .cs-Datei wird dadurch unteranderem erspart und es ist möglich mehr Kontext aus den Codeelementen zu erhalten.

Mit einem Roslyn Analyzer kann Code analysiert werden, um mögliche Probleme zu finden, Designentscheidung durchzusetzen oder Coding Guidelines zu erzwingen, um damit die Codequalität nahhaltig zu verbessern.

Die Analyzer können für einzelne Projekte installiert werden, wodurch in der Codebase für alle Entwickler immer die gleichen Fehler angezeigt werden.

Ein Roslyn Analyzer, der bei .NET dabei ist, wäre das Durchsetzen der Regel, dass niemals var verwendet werden darf.

Das kann von Coding Guideline zu Coding Guideline natürlich anders sein.

Solche Analyzer können selbst entwickelt werden. Das sehen wir an dem zuvor genannten Beispiel:

Async void Methoden sollen nicht erlaubt sein.

Wie wird ein Roslyn Analyzer entwickelt?

Zuerst muss eine Klassenbibliothek mit dem Zielframework .netstandard2.0 erstellt werden. Diese braucht Referenzen auf die Packages Microsoft.CodeAnalysis.Common und Microsoft.CodeAnalysis.CSharp.

Wir beginnen nun mit einer neuen Klasse, in der wir die Analyse durchführen. Diese muss mit vom Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer erben und mit dem Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzerAttribut versehen werden.

 

[DiagnosticAnalyzer(LanguageNames.CSharp)]public class AsyncVoidAnalyzer : DiagnosticAnalyzer

{

   // Code here

}

 

Nun müssen wir die Regel definieren, die beim Analyzer rauskommen kann.

Diese benötig eine eindeutige Id, einen Titel, eine Nachricht, eine Kategorie, einen Schweregrad, ob es standardmäßig aktiviert ist und eine Beschreibung.
Der Schweregrad kann nachträglich mit der .editorconfig angepasst werden.

 

public const string RuleId = "AsyncVoidMethodNotAllowed";

private static readonly DiagnosticDescriptor Rule = new(

    RuleId,

    title: "Async void not allowed",

    messageFormat: "Async void is not allowed for {0}.",

    category: "Async",

    defaultSeverity: DiagnosticSeverity.Warning,

    isEnabledByDefault: true,

    description: "Async void is not allowed because it might cause the application to crash."

);public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics

{

    get

    {

        return ImmutableArray.Create(Rule);

    }

}

 

Wie man sieht, kann man in der Nachricht Formatparameter angeben, die dann gefüllt werden können. Das schauen wir uns später noch an.

Die SupportedDiagnostics Property bestimmt die Regeln, die bei dem Analyzer rauskommen können.

Im Folgenden muss die Initialize Methode überschrieben werden, in welcher definiert wird, wie die Analyze erfolgt.

 

public override void Initialize(AnalysisContext context)

{

    context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

    context.EnableConcurrentExecution();

    context.RegisterSyntaxNodeAction(

 

        AnalyzeMethodForAsyncVoid,

        SyntaxKind.MethodDeclaration 

    );

}

 


Die erste Zeile sagt aus, dass generierter Code von dem Analyzer ausgelassen werden soll.

Die zweite Zeile erlaubt es, dass der Analyzer parallel ausgeführt werden kann. Dadurch wird der Code schneller analysiert.

Der letzte Methodenaufruf richtet den Analyzer ein. Bei jeder Methoden Deklaration im Code soll die AnalyzeMethodForAsyncVoid Methode ausgeführt werden. In dieser Methode geschieht die eigentliche Analyze.

Es ist möglich, die gleiche Methode auf unterschiedliche SyntaxKind aufzuführen, sodass ähnliche SyntaxNodes gleich analysiert werden können. Dazu müssen einfach mehr SyntaxKind als Parameter angefügt werden.

Was müssen wir nun aber machen, um zu überprüfen, ob eine Methode eine async void Methode ist? Wir müssen lediglich überprüfen, ob die Methode den Modifier async besitzt und dass der Rückgabetyp der Methode void ist. Wir müssen nicht überprüfen, ob der Rückgabetyp Task oder IAsyncEnumerable ist, weil async nur mit void oder awaitable Typen benutzbar ist. Deswegen reicht der check auf async + void.

private static void AnalyzeMethodForAsyncVoid(SyntaxNodeAnalysisContext startCodeBlockContext)

{

    // Ignore every other SyntaxNode except MethodDeclerationSyntax
    if (startCodeBlockContext.Node is not MethodDeclarationSyntax methodDeclarationSyntax)
    {
       
return;
    }

 

    // Get the async token from the method declaration and check if it exists
    SyntaxToken asyncKeyword = methodDeclarationSyntax.Modifiers
       
.FirstOrDefault(modifier => modifier.IsKind(SyntaxKind.AsyncKeyword));
   
bool isAsync = asyncKeyword != default;
   
if (!isAsync)
    {
       
return;
    }

    // Check if the return type is void
    TypeSyntax returnType = methodDeclarationSyntax.ReturnType;
   
bool isVoid = returnType is PredefinedTypeSyntax potentialVoid &&
                  potentialVoid.
Keyword.IsKind(SyntaxKind.VoidKeyword);
   
if (!isVoid)
    {
       
return;
    }

    // Create diagnostic result because method is async void
    Diagnostic diagnostic = Diagnostic.Create(Rule, asyncKeyword.GetLocation(), methodDeclarationSyntax.Identifier.Text);
    startCodeBlockContext.
ReportDiagnostic(diagnostic);
}

Am Ende der Methode wird das Diagnoseergebnis erstellt, was dann in der IDE als Warnung angezeigt wird. Es muss zuerst angegeben werden, welche Regel hier nicht eingehalten wurde, dann an welcher Stelle die Meldung angezeigt werden soll und danach folgen die Formatparameter für die Fehlermeldung.

Zum manuellen Testen kann der Analyzer nun in ein Projekt importiert werden.

<ItemGroup>

    <AnalyzerInclude="C:\Analyzers.Async\bin\Debug\netstandard2.0\Analyzers.Async.dll" />

</ItemGroup>

Nun sollte nun für das Beispielprogramm oben eine Warnung bei der DangerousMethod erscheinen.

 

Syntax Visualizer

Zur leichteren Ansicht des Codebaums empfiehlt es sich den Syntax Visualizer zu installieren.

Dafür muss im Visual Studio Installer unter Workloads die Visual Studio-Extensionentwicklung aktiviert werden und unter den Einzelnen Komponenten zusätzlich die .NET Compiler Platform SDK.

Wenn das erfolgreich installiert worden ist, kann unter Ansicht -> Weitere Fenster der Syntax Visualizer gefunden werden.

Der Syntax Visualizer sollte immer synchron zum Cursor sein. So sollte die Node für async einer Methodendeklaration sehr schnell gefunden werden können.

Leider stimmt die Ansicht nicht immer mit der Struktur der Roslyn API überein, hilft jedoch ungemein eine grobe Richtung zu bekommen wie die interessanten Stellen des Codes erreicht werden können.

Debugging

Die einfachste Methode, um den Analyzer Code zu debuggen, ist es Unit Tests zu schreiben und diese dann zu debuggen. Damit schlägt man dann zwei Fliegen mit einer Klappe: Manuelle und automatisierte Tests.

Dazu wird ein Testprojekt mit den beiden Referenzen zu Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit und Microsoft.CodeAnalysis.CSharp.Workspaces. XUnit kann auch durch MSTest oder NUnit ersetzt werden, sollte nicht XUnit das Zieltestframework sein, mit dem gearbeitet wird.

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier<Bof.CodeQuality.Analyzers.Async.AsyncVoidAnalyzer>;

namespace Bof.CodeQuality.Analyzers.Async.Tests
{
    using System.Threading.Tasks;
   
using Microsoft.CodeAnalysis.Testing;
   
using Async;
   
using Xunit;

   
public class AsyncVoidAnalyzerTests
   
{
       
private const string RuleId = AsyncVoidAnalyzer.RuleId;

        [
Fact]
       
public async Task Finds_DiagnosticResult_On_Class_Method()
        {
           
const string test = @"
class C
{
    public async void M(){}
}"
;
           
DiagnosticResult[] expected = {Verify.Diagnostic(RuleId).WithSpan(4, 12, 4, 17)};
           
await Verify.VerifyAnalyzerAsync(test, expected);
        }
    }
}


Wir erstellen zur Einfachheit einen Alias für den AnalyzerVerifier, der benutzt wird um sicherzustellen, dass an der richtigen Stelle im Code der erwünschte Fehler auftritt.
Sollte kein Fehler erwartet werden kann das
expected Array auch leer bleiben.

Hier können jetzt auch Breakpoints im Analyzer Code gesetzt werden, um den Code zu debuggen.

Fazit

Die Analyzer sind teilweise schwer zu schreiben, da diese sehr abstrakt sind und die Sprache C# auch viele Konzepte und Strukturen besitzt, die einem erstmal nicht bekannt waren.

Sind die Analyzer aber erstmal geschrieben, können mögliche Probleme im Code schnell gefunden werden, die einem Entwickler vielleicht entgangen wären.

Die Analyzer sind auch anpassbar an die Codebase, an der gearbeitet wird und damit ein super Tool um neue Teammitgliedern die Coding Conventions schnell, einfach und zuverlässig beizubringen ohne Seitenlange Dokumentationen.

Ressourcen:

Hier einige Ressourcen für weitere Informationen über Roslyn Analyzer.

Syntax Visualizer: https://docs.microsoft.com/de-de/dotnet/csharp/roslyn-sdk/syntax-visualizer?tabs=csharp

.NET Roslyn Analyzer: https://github.com/dotnet/roslyn-analyzers

Benutzung von Analyzer: https://docs.microsoft.com/de-de/visualstudio/code-quality/use-roslyn-analyzers

Mehr zur Erstellung von Roslyn Analyzern: https://docs.microsoft.com/de-de/dotnet/csharp/roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix

© QUIBIQ GmbH · Imprint · Data protection