diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/Assets.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/Assets.cs
new file mode 100644
index 00000000000..8a5c533f676
--- /dev/null
+++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/Assets.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Newtonsoft.Json.Linq;
+
+namespace Semmle.Extraction.CSharp.DependencyFetching
+{
+ ///
+ /// Class for parsing project.assets.json files.
+ ///
+ internal class Assets
+ {
+ private readonly ProgressMonitor progressMonitor;
+
+ private static readonly string[] netFrameworks = new[] {
+ "microsoft.aspnetcore.app.ref",
+ "microsoft.netcore.app.ref",
+ "microsoft.netframework.referenceassemblies",
+ "microsoft.windowsdesktop.app.ref",
+ "netstandard.library.ref"
+ };
+
+
+ internal Assets(ProgressMonitor progressMonitor)
+ {
+ this.progressMonitor = progressMonitor;
+ }
+
+ ///
+ /// In most cases paths in asset files point to dll's or the empty _._ file, which
+ /// is sometimes there to avoid the directory being empty.
+ /// That is, if the path specifically adds a .dll we use that, otherwise we as a fallback
+ /// add the entire directory (which should be fine in case of _._ as well).
+ ///
+ private static string ParseFilePath(string path)
+ {
+ if (path.EndsWith(".dll"))
+ {
+ return path;
+ }
+ return Path.GetDirectoryName(path) ?? path;
+ }
+
+ ///
+ /// Class needed for deserializing parts of an assets file.
+ /// It holds information about a reference.
+ ///
+ private class ReferenceInfo
+ {
+ ///
+ /// This carries the type of the reference.
+ /// We are only interested in package references.
+ ///
+ public string Type { get; set; } = "";
+
+ ///
+ /// If not a .NET framework reference we assume that only the files mentioned
+ /// in the compile section are needed for compilation.
+ ///
+ public Dictionary Compile { get; set; } = new();
+ }
+
+ ///
+ /// Gets the package dependencies from the assets file.
+ ///
+ /// Parse a part of the JSon assets file and returns the paths
+ /// to the dependencies required for compilation.
+ ///
+ /// Example:
+ /// {
+ /// "Castle.Core/4.4.1": {
+ /// "type": "package",
+ /// "compile": {
+ /// "lib/netstandard1.5/Castle.Core.dll": {
+ /// "related": ".xml"
+ /// }
+ /// }
+ /// },
+ /// "Json.Net/1.0.33": {
+ /// "type": "package",
+ /// "compile": {
+ /// "lib/netstandard2.0/Json.Net.dll": {}
+ /// },
+ /// "runtime": {
+ /// "lib/netstandard2.0/Json.Net.dll": {}
+ /// }
+ /// }
+ /// }
+ ///
+ /// Returns {
+ /// "castle.core/4.4.1/lib/netstandard1.5/Castle.Core.dll",
+ /// "json.net/1.0.33/lib/netstandard2.0/Json.Net.dll"
+ /// }
+ ///
+ private IEnumerable GetPackageDependencies(JObject json)
+ {
+ // If there are more than one framework we need to pick just one.
+ // To ensure stability we pick one based on the lexicographic order of
+ // the framework names.
+ var references = json
+ .GetProperty("targets")?
+ .Properties()?
+ .MaxBy(p => p.Name)?
+ .Value
+ .ToObject>();
+
+ if (references is null)
+ {
+ progressMonitor.LogDebug("No references found in the targets section in the assets file.");
+ return Array.Empty();
+ }
+
+ // Find all the compile dependencies for each reference and
+ // create the relative path to the dependency.
+ var dependencies = references
+ .SelectMany(r =>
+ {
+ var info = r.Value;
+ var name = r.Key.ToLowerInvariant();
+ if (info.Type != "package")
+ {
+ return Array.Empty();
+ }
+
+ // If this is a .NET framework reference then include everything.
+ return netFrameworks.Any(framework => name.StartsWith(framework))
+ ? new[] { name }
+ : info
+ .Compile
+ .Select(p => Path.Combine(name, ParseFilePath(p.Key)));
+ })
+ .ToList();
+
+ return dependencies;
+ }
+
+ ///
+ /// Parse `json` as project.assets.json content and populate `dependencies` with the
+ /// relative paths to the dependencies required for compilation.
+ ///
+ /// True if parsing succeeds, otherwise false.
+ public bool TryParse(string json, out IEnumerable dependencies)
+ {
+ dependencies = Array.Empty();
+
+ try
+ {
+ var obj = JObject.Parse(json);
+ var packages = GetPackageDependencies(obj);
+
+ dependencies = packages.ToList();
+
+ return true;
+ }
+ catch (Exception e)
+ {
+ progressMonitor.LogDebug($"Failed to parse assets file (unexpected error): {e.Message}");
+ return false;
+ }
+ }
+ }
+
+ internal static class JsonExtensions
+ {
+ internal static JObject? GetProperty(this JObject json, string property) =>
+ json[property] as JObject;
+ }
+}
diff --git a/csharp/extractor/Semmle.Util/IEnumerableExtensions.cs b/csharp/extractor/Semmle.Util/IEnumerableExtensions.cs
index 4e40288d88c..1ca676f0ce6 100644
--- a/csharp/extractor/Semmle.Util/IEnumerableExtensions.cs
+++ b/csharp/extractor/Semmle.Util/IEnumerableExtensions.cs
@@ -113,5 +113,11 @@ namespace Semmle.Util
h = h * 7 + i.GetHashCode();
return h;
}
+
+ ///
+ /// Returns the sequence with nulls removed.
+ ///
+ public static IEnumerable WhereNotNull(this IEnumerable items) where T : class =>
+ items.Where(i => i is not null)!;
}
}