Χρήση των API Roslyn για την ανάλυση μιας λύσης .NET – Steve Gordon

Χρήση των API Roslyn για την ανάλυση μιας λύσης .NET – Steve Gordon

Januar 24, 2023 0 Von admin

Σε μια προηγούμενη ανάρτηση «Ξεκινώντας με τα Roslyn API: Γράφοντας κώδικα με κώδικα», επέδειξα έναν σχετικά απλό τρόπο δημιουργίας κώδικα χρησιμοποιώντας τα Roslyn API. Σε αυτήν την ανάρτηση, θέλω να επανεξετάσω το θέμα από μια νέα οπτική γωνία και να δείξω τα θεμέλια για την επίτευξη ενός ελαφρώς διαφορετικού στόχου.

Τι είναι η Roslyn;

Ως υπενθύμιση, Roslyn είναι το όνομα της πλατφόρμας μεταγλωττιστών .NET, η οποία περιλαμβάνει μεταγλωττιστές τόσο για C# όσο και για VB.NET και διάφορα API και εργαλεία. Τα API είναι εξαιρετικά ισχυρά και μπορούν να χρησιμοποιηθούν για την κατανόηση του υπάρχοντος κώδικα και τη δημιουργία πρόσθετου κώδικα. Μπορεί να έχετε ακούσει πρόσφατα για Γεννήτριες Πηγών σε C#, που επιτρέπουν την ανάλυση κώδικα σε χρόνο μεταγλώττισης και τη δημιουργία κώδικα. Αυτές βελτιστοποιούν τις περιοχές που παραδοσιακά βασίζονταν στην αντανάκλαση χρόνου εκτέλεσης και σε πολύ κώδικα boilerplate, όπως η σειριοποίηση JSON και το Regex. Αυτά προσφέρουν βελτιώσεις τόσο στην απόδοση όσο και στη συντήρηση κώδικα.

Αναλύοντας μια υπάρχουσα λύση

Για αυτήν την ανάρτηση, ήθελα να εστιάσω στο πώς μπορούμε να αρχίσουμε να αξιοποιούμε τα API Roslyn για να αναλύσουμε τον υπάρχοντα κώδικα. Το εξερευνώ αυτήν τη στιγμή καθώς σχεδιάζω μελλοντικές βελτιώσεις στη δημιουργία κώδικα για το Πελάτης Elasticsearch .NET v8. Σήμερα, δημιουργώ ένα μεγάλο μέρος των τύπων που απαιτούνται για τη μοντελοποίηση των αιτημάτων και των απαντήσεων για τα τελικά σημεία στον πελάτη. Η τρέχουσα διαδικασία λειτουργεί, αλλά είναι αρκετά βάναυση στην προσέγγισή της. Πριν αναδημιουργηθούν τα πάντα από το σχήμα, κάθε εκτέλεση διαγράφει τον υπάρχοντα φάκελο προορισμού για τα αρχεία C# που δημιουργούνται. Μετά τη δημιουργία κώδικα, μπορώ να χρησιμοποιήσω το git diff για να ελέγξω τις αλλαγές πριν τις συγχωνεύσω.

Στο μέλλον, η γεννήτρια κώδικα μπορεί να είναι πολύ πιο έξυπνη. Τι θα γινόταν αν, αντί να ξεκινά από καινούργια κάθε φορά, μπορούσε να αναλύσει τον υπάρχοντα κώδικα, να προσδιορίσει ποιες (εάν υπάρχουν) αλλαγές να κάνει σε κάθε τύπο και να ενημερώσει ανάλογα. Ένα σημαντικό πλεονέκτημα αυτού είναι ότι η γεννήτρια θα μπορούσε να γνωρίζει τυχόν αλλαγές που μπορεί να εισάγει και να τις αναφέρει για γρήγορη αναθεώρηση.

Για να υποστηρίξω αυτήν την ιδέα, άρχισα να πειραματίζομαι στον καιρό μου με το πώς να αξιοποιήσω τα API για να αρχίσω να αναλύω κώδικα. Σε αυτήν την ανάρτηση, θα ξεκινήσουμε προσπαθώντας να αναλύσουμε τα έργα που περιέχονται σε μια λύση-στόχο. Θα δείξω δύο γκοτσάδες που μπορεί να συναντήσετε και πώς τις ξεπέρασα στο αρχικό μου πρωτότυπο. Το σχέδιο είναι να συνεχιστεί αυτή η σειρά με πιο πολύτιμες εργασίες στο μέλλον.

Ξεκινώντας με ένα MsBuildWorkspace

Για να κρατήσω τα πράγματα απλά, ξεκίνησα δημιουργώντας μια μικρή λύση .NET για να λειτουργήσει ως στόχος για την ανάλυση. Αυτή η λύση ξεκινά με ένα έργο βιβλιοθήκης μίας κλάσης που περιέχει μία κλάση. Η δομή έχει ως εξής:

Στη συνέχεια, δημιούργησα μια νέα εφαρμογή κονσόλας .NET 6 χρησιμοποιώντας το πρότυπο δηλώσεων ανώτατου επιπέδου.

Πριν προσθέσουμε οποιονδήποτε κώδικα, πρέπει να αναφερθούμε στη βιβλιοθήκη Roslyn API που υποστηρίζει την ανάλυση μιας υπάρχουσας λύσης. Το Roslyn περιλαμβάνει την έννοια των χώρων εργασίας που παρέχουν ένα λογικό δοχείο για μια συλλογή πληροφοριών και εγγράφων που σχετίζονται με λύσεις, έργα και κώδικα. IDE όπως το Visual Studio φορτώνουν έναν χώρο εργασίας για την τρέχουσα ανοιχτή λύση και επίπεδο σε άλλα Roslyn API για ανάλυση κώδικα, συμπλήρωση κώδικα, αυτοματοποιημένα fixers κ.λπ. Μπορούμε να χρησιμοποιήσουμε αυτήν την ίδια επιφάνεια API μέσω προγραμματισμού εκτός ενός IDE, προσφέροντας τις ίδιες ισχυρές κατασκευές και ικανότητες .

Υπάρχουν διάφοροι τύποι χώρων εργασίας που εξυπηρετούν διαφορετικές ανάγκες. Χρησιμοποιώ ένα AdhocWorkspace ως σημείο εκκίνησης για την τρέχουσα δημιουργία κώδικα για τη δημιουργία αρχείων πηγαίου κώδικα για διάφορους τύπους. Σε αυτό το νέο σενάριο, θέλουμε να αναλύσουμε μια υπάρχουσα λύση .NET. Οι λύσεις χρησιμοποιούνται για τη λογική ομαδοποίηση και εργασία σε ένα σύνολο (μηδέν ή περισσότερων) έργων .NET. Για να υποστηρίξουμε την ανάλυση και την εργασία με υπάρχουσες λύσεις, μπορούμε να χρησιμοποιήσουμε ένα συγκεκριμένο API MsBuildWorkspace.

Για να αναφερθούμε σε αυτό, μπορούμε να προσθέσουμε μια αναφορά πακέτου στην εφαρμογή της κονσόλας που χρησιμεύει ως „οικοδεσπότης“ για την ανάλυση της λύσης στόχου μας.

<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.1.0" />

Θα χρειαστούμε κάποια άλλα πακέτα σύντομα, αλλά προς το παρόν, θα ξεκινήσουμε με αυτό, ώστε να μπορέσω να δείξω μερικά γούστα που είναι αρκετά εύκολο να συναντήσετε.

Πίσω στο αρχείο μας program.cs, μπορούμε να γράψουμε κάποιο αρχικό κώδικα που θα άνοιγε τη λύση και θα απαριθμήσει τα έργα που περιέχει.

using Microsoft.CodeAnalysis.MSBuild;

const string targetPath = @"e:\Projects\roslyn-playground\target\Sample.sln";

var workspace = MSBuildWorkspace.Create();

var sln = await workspace.OpenSolutionAsync(targetPath);

foreach (var project in sln.Projects)
{
    Console.WriteLine(project.AssemblyName);
}

Αυτός ο κώδικας περιλαμβάνει μια οδηγία χρήσης για τον χώρο ονομάτων Microsoft.CodeAnalysis.MSBuild. Ορίζει μια σταθερά συμβολοσειράς για την πλήρη διαδρομή προς το αρχείο λύσης προορισμού.

Στη συνέχεια δημιουργεί ένα στιγμιότυπο ενός MsBuildWorkspace και το καλεί OpenSolutionAsync μέθοδο, περνώντας τη διαδρομή λύσης ως το μόνο όρισμα. Αυτό επιστρέφει α Solution παράδειγμα που περιέχει ορισμένες ιδιότητες ανώτατου επιπέδου για τη λύση. Μέσα στον βρόχο foreach, απαριθμεί μια συλλογή έργων μέσα στη λύση, γράφοντας το όνομα της διάταξης έργου στην κονσόλα.

Μπορούμε να προσπαθήσουμε να εκτελέσουμε αυτόν τον κώδικα, αλλά πιθανότατα θα αποτύχει με μια εξαίρεση κατά την προσπάθεια δημιουργίας του χώρου εργασίας.

Unhandled exception. System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types.
Could not load file or assembly 'Microsoft.Build.Framework, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. The system cannot find the file specified.
...

Αυτό το ReflectionTypeLoadException είναι αρκετά τερματικό και η αιτία και η επιδιόρθωση ενδέχεται να μην είναι άμεσα εμφανείς. Το πρόβλημα είναι ότι η βιβλιοθήκη πρέπει να εντοπίσει μια παρουσία του MsBuild στο σύστημά σας. Δεν έχουμε δώσει συμβουλές σχετικά με το πού βρίσκεται, καθώς μπορεί να εγκατασταθεί με διάφορους τρόπους.

Ευτυχώς, αυτό το πρόβλημα μπορεί να ξεπεραστεί με αναφορά σε ένα πρόσθετο πακέτο βοήθειας:

<PackageReference Include="Microsoft.Build.Locator" Version="1.4.1" />

Όπως υποδηλώνει το όνομα, αυτό το πακέτο περιλαμβάνει μια βιβλιοθήκη που ξέρει πώς να εντοπίσει μια κατάλληλη έκδοση MsBuild, υποθέτοντας ότι είναι εγκατεστημένη κάπου στον προγραμματιστή σας. Όχι μόνο αυτό, αλλά διαμορφώνει τα πράγματα έτσι ώστε να μπορούμε να χρησιμοποιούμε σωστά τα API του MsBuildWorkspace.

Πρέπει να προσθέσουμε μία γραμμή στο πρόγραμμά μας πριν δημιουργήσουμε τον χώρο εργασίας, απαιτώντας μία επιπλέον οδηγία χρήσης.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;

const string targetPath = @"e:\Projects\roslyn-playground\target\Sample.sln";

MSBuildLocator.RegisterDefaults();

var workspace = MSBuildWorkspace.Create();

var solution = await workspace.OpenSolutionAsync(targetPath);

foreach (var project in solution.Projects)
{
    Console.WriteLine(project.AssemblyName);
}

MSBuildLocator.RegisterDefaults() κάνει όλη τη βαριά ανύψωση εδώ και ρυθμίζει τα πράγματα όπως τα χρειαζόμαστε.

Σε αυτό το σημείο, μπορούμε να προσπαθήσουμε να εκτελέσουμε ξανά την εφαρμογή. Δεν θα πρέπει πλέον να βλέπουμε το ReflectionTypeLoadException και η εφαρμογή θα πρέπει να εκτελείται μέχρι να ολοκληρωθεί. Ωστόσο, μπορεί να παρατηρήσετε ότι η έξοδος της κονσόλας είναι κενή και δεν βλέπουμε το όνομα συγκρότησης του έργου Sample.Library στην έξοδο. Γιατί είναι αυτό?

Λοιπόν, ο χώρος εργασίας MsBuild έχει σχεδιαστεί για να λειτουργεί είτε με έργα C# είτε με έργα Visual Basic. Ωστόσο, χρειαζόμαστε μια επιπλέον αναφορά πακέτου για τους τύπους έργων με τους οποίους αναμένουμε να εργαστούμε. Στο παράδειγμά μου, είναι ένα έργο C# στη λύση, επομένως πρέπει να εισαγάγουμε την αντίστοιχη βιβλιοθήκη που ξέρει πώς να δουλεύει με έργα C#.

<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.1.0" />

Μπορούμε να προσπαθήσουμε να εκτελέσουμε ξανά την εφαρμογή χωρίς πρόσθετες αλλαγές κώδικα τώρα που γίνεται αναφορά σε αυτό το πακέτο. Αυτή τη φορά, βλέπουμε το όνομα της συναρμολόγησης για το έργο μας γραμμένο στην κονσόλα.

Αυτό δεν είναι ιδιαίτερα συναρπαστικό σε αυτό το στάδιο, αλλά έχουμε βάλει τις βάσεις για να ξεκινήσουμε μια περαιτέρω ανάλυση των έργων, των εγγράφων και των τύπων που ορίζονται στο έργο. Αυτό είναι θέμα για άλλη ανάρτηση (ή δύο, ή πολλές)!