Getting Started with NDepend API
- Introduction
- Which license is required to run a program using NDepend.API?
- Getting started with NDepend API
- How to build and deploy your programs that use NDepend.API?
- APIs not supported within a .NET 7.0, .NET 6.0, .NET 5.0, .NET Core 3.x context
- Sample: Export a code rule or query result to json
- Annexe: AssemblyResolver.cs
- NDepend API Documentation
Introduction
The NDepend.API.dll assembly can be found in the $NDependInstallPath$\Lib directory.
Since NDepend v2021.2, NDepend.API.dll is compiled against netstandard2.0. It can then be referenced and executed from a net472 (or upper), netcore3.X (or upper) or a net50 (or upper) application.
This assembly contains a set of interfaces to develop custom sophisticated .NET static analysis tools. NDepend.API is related to CQLinq and NDepend.PowerTools:
CQLinq (Code Query through Linq) is a facility to develop easily lightweight static analyzer.
CQLinq is the ability of NDepend to edit and run live linq queries (C# syntax) based on NDepend.CodeModel types, contained in NDepend.API.dll. NDepend proposes out-of-the-box around 200 code queries and rules based on CQLinq that can be used and customized easily.
NDepend.PowerTools are a set of short open-source static analyzers, packed into the Visual Studio solution: $NDependInstallPath$\NDepend.PowerTools.SourceCode\NDepend.PowerTools.sln
The Power Tools demonstrate NDepend.API usage and capabilities. The solution contains one shared project used from two projects:
- NDepend.PowerTools.csproj that compiles to the net472 assembly $NDependInstallPath$\NDepend.PowerTools.exe
- NDepend.PowerTools.MultiOS.csproj that compiles to the net8.0 assembly $NDependInstallPath$\net8.0\NDepend.PowerTools.MultiOS.dll
With NDepend.API it is easy to programmatically:
- Analyze one or several .NET assemblies and create NDepend reports (only with Build Machine licensing)
- Explore dependencies between this assemblies namespaces, types, methods and fields
- Gather usual code metrics, computed by NDepend on code elements, and create your own code metrics
- Explore diff between two versions analyzed of a .NET code base, and even follow evolution across many versions
- Open source file declaration(s) of a code element
- Generate on the fly and execute CQLinq rules or queries
UI related API are not supported within a .NET 7.0, .NET 6.0, .NET 5.0, .NET Core 3.x context. See the list of not supported APIs here, at the bottom of the documentation.
Which license is required to run a program using NDepend.API?
100% of NDepend API can be used from:
- the 14-day trial edition
- and with a Build-Machine license
Almost all the NDepend API can be used with a Developer license, the only exception being these two API methods:
Running an analysis and building a report can be done with Developer licensing only by clicking a button in the UI.
Only the Build-Machine licensing can run automatically an analysis and automatically build a report.
NDepend API cannot be used with an Azure DevOps / TFS Extension or GitHub Action license.
Getting started with NDepend API
Certainly the best way to get started with the NDepend.API infrastructure is to use the Power Tools and browse their source code packed in the Visual Studio solution: $NDependInstallPath$\NDepend.PowerTools.SourceCode\NDepend.PowerTools.sln. You can tweak existing power tools or create your own power tools.
Also you can learn about the NDepend API types in the namespace NDepend.CodeModel by browsing the source code of the default CQLinq code queries and code rules written in top of it.
The NDepend.API.dll assembly is an access point to most of NDepend features. Internally this assembly relies on most of others NDepend assemblies. As a consequence, using this assembly cannot be as easy as copy/pasting it. At the end of this documentation, see further explanations on How to build and deploy your program using NDepend.API.
The file $NDependInstallPath$\NDepend.PowerTools.SourceCode\NDepend_API_GettingStarted_Program.cs shows the basic code to get started with NDepend.API, here is the most relevant part. Types and methods linked are the backbone of the API:
using System; using System.Collections.Generic; using System.Linq; using NDepend.Analysis; using NDepend.CodeModel; using NDepend.Path; using NDepend.Project; namespace YourNamespace { class Program { public static void Main() { // 0) Creates a NDependServicesProvider object var ndependServicesProvider = new NDependServicesProvider(); // 1) obtain some VS solution or project file path var visualStudioManager = ndependServicesProvider.VisualStudioManager; ICollection<IAbsoluteFilePath> vsSlnOrProjFilePaths; // get a non IntPtr.Zero owner Window Handle. // If you are in a System.Console try this. IntPtr ownerWindowHandle = ...; visualStudioManager.ShowDialogSelectVisualStudioSolutionsOrProjects( ownerWindowHandle, out vsSlnOrProjFilePaths); // Could also use: // visualStudioManager.GetMostRecentlyUsedVisualStudioSolutionOrProject() // 2) obtains assemblies file path to analyze var assembliesFilePath = (from vsSlnOrProjFilePath in vsSlnOrProjFilePaths from assembliesFilePathTmp in visualStudioManager.GetAssembliesFromVisualStudioSolutionOrProject(vsSlnOrProjFilePath) select assembliesFilePathTmp).Distinct().ToArray(); // 3) gets or create a IProject object var projectManager = ndependServicesProvider.ProjectManager; IProject project = projectManager.CreateTemporaryProject( assembliesFilePath, TemporaryProjectMode.Temporary); // Or, to get a IProject object, could also use // var project = projectManager.CreateBlankProject() to create a new project // and then project.CodeToAnalyze.SetApplicationAssemblies(...) // or project.CodeToAnalyze.SetIDEFiles( // new [] { new IDEFile("C:\File.sln".ToAbsoluteFilePath()","-test") } ) // and then projectManager.SaveProject(project); to save the project file // // Or, to get an existing IProject object, could also use // projectManager.ShowDialogChooseAnExistingProject(out project) // Or programmatically list most recently used NDepend projects on this machine // with this call: // projectManager.GetMostRecentlyUsedProjects() // 4) gets an IAnalysisResult object from the IProject object // **> Both RunAnalysis() and RunAnalysisAndBuildReport() methods // work only with a Build Machine license IAnalysisResult analysisResult = project.RunAnalysis(); // Or project.RunAnalysisAndBuildReport() // Or, to get a IAnalysisResult object, first gets a IAnalysisResultRef object, // that represents a reference to a persisted IAnalysisResult object // then call // project.TryGetMostRecentAnalysisResultRef() or // project.GetAvailableAnalysisResultsRefs() or // project.GetAvailableAnalysisResultsRefsGroupedPerMonth() // and then call // analysisResultRef.Load() // 5) gets a ICodeBase object from the IAnalysisResult object ICodeBase codeBase = analysisResult.CodeBase; // Or eventually use a ICompareContext object if you wish to analyze // the code delta between two ICodeBase. // codeBase.CreateCompareContextWithOlder(olderCodeBase) // 6) Use the code model API to query code and do develop any algorithm you need! // For example here we are looking for complex methods var complexMethods = (from m in codeBase.Application.Methods where m.ILCyclomaticComplexity > 10 orderby m.ILCyclomaticComplexity descending select m).ToArray(); if (complexMethods.Length == 0) { return; } Console.WriteLine( $"Press a key to show the {complexMethods.Length} most complex methods"); Console.ReadKey(); foreach (var m in complexMethods) { Console.WriteLine( $"{m.FullName} has a IL cyclomatic complexity of {m.ILCyclomaticComplexity}"); } // 7) eventually lets the user opens source file declaration if (complexMethods.First().SourceFileDeclAvailable) { var mostComplexMethod = complexMethods.First(); Console.WriteLine( "Press a key to open the source code decl of the most complex method?"); Console.ReadKey(); mostComplexMethod.TryOpenSource(); // Eventually use // ExtensionMethodsTooling.TryCompareSourceWith( // NDepend.CodeModel.ISourceFileLine, // NDepend.CodeModel.ISourceFileLine) // to compare 2 different versions of a code element } } } }
How to build and deploy your programs that use NDepend.API?
To create your own program that references the assembly NDepend.API.dll there are a few requirements:
- NDepend.API.dll Reference Properties (in Visual Studio) must have Copy Local set to False.
- You must include in your project the source file $NDependInstallPath$\NDepend.PowerTools.SourceCode\AssemblyResolver.cs to load NDepend.API.dll at runtime
(see also the source code AssemblyResolver.cs below).
In your Main() method you must use AssemblyResolver this way (see the code sample below).
Notice that if your executable is not generated in $NDependInstallDir$ the relative path .\Lib (highlighted in the code below) must be updated to provide a relative path to this directory.using System; using System.Linq; using System.Runtime.CompilerServices; using NDepend; using NDepend.Analysis; using NDepend.Path; using NDepend.PowerTools; using NDepend.Project; namespace YourNamespace { class Program { // ************************** IMPORTANT *********************************** // All programs using NDepend.API.dll should have this type AssemblyResolver // parametrized with the relative path to the dir "$NDependInstallDir$\Lib". // Since NDepend.PowerTool.exe is in the dir "$NDependInstallDir$" // the relative path is @".\Lib" private static readonly AssemblyResolver s_AssemblyResolver = new AssemblyResolver(@".\Lib"); [STAThread] static void Main() { AppDomain.CurrentDomain.AssemblyResolve += s_AssemblyResolver.AssemblyResolveHandler; MainSub(); } // Need this MethodImplAttribute to make sure that AssemblyResolve // has been registered successfully before JITing any method // that uses anything from the NDepend.API [MethodImpl(MethodImplOptions.NoInlining)] static void MainSub() { var ndependServicesProvider = new NDependServicesProvider(); // Example of using the NDepend.API var projectManager = ndependServicesProvider.ProjectManager; IProject project = projectManager.LoadProject( @"C:\YourPathToYourNDependProjectFile\File.ndproj".ToAbsoluteFilePath()); // Try to load the most recent analysis result var refs = project.GetAvailableAnalysisResultsRefs(); if (!refs.Any()) { return; } IAnalysisResult result = refs[0].Load(); // Obtain source files paths from the analysis result code base model // For example this can be done this way: var sourceFiles = result.CodeBase.Application.CodeElements .Where(c => c.SourceFileDeclAvailable) .SelectMany(c => c.SourceDecls) .Select(c => c.SourceFile.FilePath) .Distinct() .ToList(); // Print all source files paths foreach (IAbsoluteFilePath sourceFilePath in sourceFiles) { Console.WriteLine(sourceFilePath.ToString()); } } } }
- NDepend assemblies other than NDepend.API.dll contain the code behind the interfaces defined in NDepend.API.dll, hence all NDepend assemblies must be present in $NDependInstallPath$\, $NDependInstallPath$\Lib and $NDependInstallPath$\net8.0.
- NDepend licensing must have been registered on a machine before redeploying a program that references the assembly NDepend.API.dll on it.
- As an alternative of using the class AssemblyResolver you can create an App.Config file for your executable assembly, and add a <probing> element that refers to $NDependInstallPath$\Lib (like for example C:\NDepend\Lib). When the CLR won't resolve an assembly, then it'll look at dir in such element
APIs not supported within a .NET 7.0, .NET 6.0, .NET 5.0, .NET Core 3.x context
netstandard 2.0 assemblies can be consumed from any .NET Framework 4.7.2 or 4.8 project. Since NDepend v2021.2 NDepend.API.dll is a netstandard 2.0 assembly and all its APIs can be used within a .NET Framework 4.7.2 or 4.8 context.
netstandard 2.0 assemblies can be consumed from any .NET 7.0, .NET 6.0, .NET 5.0, .NET Core 3.x project. However there are a few UI or Windows related APIs in NDepend.API.dll that are not supported within such context. As a consequence, in such context those APIs throw a NotSupportedException.
Note that in the NDepend.PowerTools.MultiOS project sources (found within $NDependInstallPath$\NDepend.PowerTools.SourceCode\NDepend.PowerTools.sln) these APIs are not invoked thanks to the usage of a NETCORE compilation symbol.
Here is the list:
- IProjectManager.ShowDialogChooseAnExistingProject(...)
- IProjectManager.ShowDialogSelectAssemblies(...)
- IAnalysisManager.ShowDialogBuildComparison(...)
- IVisualStudioManager.ShowDialogSelectVisualStudioSolutionsOrProjects(...)
- IVisualStudioManager.GetMostRecentlyUsedVisualStudioSolutionOrProject(...)
- IVisualStudioManager.IsVisualStudioVersionInstalled(...)
- ExtensionMethodsTooling.TryOpenSource(...)
- ExtensionMethodsTooling.TryCompareSourceWith(...)
- ExtensionMethodsTooling.TryDiffSource(...)
Example: Export a code rule or query result to json
This code can be used in any .NET Framework or .NET Core (3,5,6...) console project.
using System; using System.Runtime.CompilerServices; using System.Linq; using System.IO; using NDepend; using NDepend.Path; using NDepend.Project; using NDepend.Analysis; using NDepend.CodeQuery; public class ExportQueryResult { private static readonly string SEP = Path.DirectorySeparatorChar.ToString(); private static readonly AssemblyResolver s_AssemblyResolver = new AssemblyResolver( // TODO Update the path to $NDependInstallDir$\Lib\NDepend.API.dll // relative to the path of the present assembly $@"..{SEP}..{SEP}..{SEP}..{SEP}..{SEP}bin{SEP}Debug{SEP}Lib" ); public static void Main(string[] args) { AppDomain.CurrentDomain.AssemblyResolve += s_AssemblyResolver.AssemblyResolveHandler; MainSub(args); } // MainSub() is here to avoids that the Main() method uses something // from NDepend.API without having registered AssemblyResolveHandler! [MethodImpl(MethodImplOptions.NoInlining)] public static void MainSub(string[] args) { // Init NDepend.API var ndependServicesProvider = new NDependServicesProvider(); // Load the NDepend project IProject project = ndependServicesProvider.ProjectManager.LoadProject( @"C:\Projects\GifRecorder.ndproj".ToAbsoluteFilePath()); // Load the most recent analysis result if(!project.TryGetMostRecentAnalysisResultRef(out IAnalysisResultRef @ref)) { return; } IAnalysisResult aResult = @ref.Load(); // Get the query IQuery query = project.CodeQueries.CodeQueriesSet.AllQueriesRecursive.FirstOrDefault( q => q.QueryString.Contains("Avoid types with too many methods")); if(query == null) { return; } // Compile the query against the codeBase IQueryCompiled qCompiled = query.QueryString.Compile(aResult.CodeBase); if(qCompiled.HasErrors) { return; } // Execute the query compiled IQueryExecutionResult qResult = qCompiled.QueryCompiledSuccess.Execute(aResult.CodeBase); if(qResult.Status != QueryExecutionStatus.Success) { return; } // Export the query result (DocumentKind can be HTML, XML, CSV, Excel...) string jsonContent = qResult.ExportQueryResult(QueryResultExportDocumentKind.JSON, QueryResultExportFlags.UnfoldMultiItemsCell); File.WriteAllText(@"C:\temp\result.json", jsonContent); } }
Annexe: AssemblyResolver.cs
When you want to run a project against NDepend.API, you need to have this class in your project as explained in the section How to build and deploy your programs that use NDepend.API?.
using System; using System.Diagnostics; using System.IO; using System.Reflection; internal class AssemblyResolver { internal AssemblyResolver(string relativePathToLib) { // Assert we have a relative path to the NDepend lib folder! Debug.Assert(!string.IsNullOrEmpty(relativePathToLib)); Debug.Assert(relativePathToLib.Length >= 5); Debug.Assert(relativePathToLib[0] == '.'); Debug.Assert(relativePathToLib.ToLower().EndsWith(Path.DirectorySeparatorChar + @"lib")); relativePathToLib += Path.DirectorySeparatorChar; m_RelativePathToLib = relativePathToLib; } private readonly string m_RelativePathToLib; internal Assembly AssemblyResolveHandler(object sender, ResolveEventArgs args) { Debug.Assert(args != null); var assemblyName = new AssemblyName(args.Name); Debug.Assert(assemblyName != null); var assemblyNameString = assemblyName.Name; Debug.Assert(!string.IsNullOrEmpty(assemblyNameString)); // Special processing for NDepend.API and NDepend.Core because they are defined in $NDependInstallDir$\Lib if (assemblyNameString.ToLower() != "ndepend.api" && assemblyNameString.ToLower() != "ndepend.core") { return null; } var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); Debug.Assert(!string.IsNullOrEmpty(location)); // Notice that the relative dirs "..\" and ".\" get resolved only when browsing the path, // in the methods File.Exists() and Assembly.LoadFrom() // http://stackoverflow.com/a/6875932/27194 var asmFilePath = Path.Combine(location, m_RelativePathToLib + assemblyName.Name + ".dll"); if (!File.Exists(asmFilePath)) { return null; } var assembly = Assembly.LoadFrom(asmFilePath); return assembly; } }