原文地址:www.jerriepelser.com/blog/analyz…
发布时间:2018年6月26日
PS:如果你在任何ASP.NET Core项目上需要帮助,我是可以被雇佣做自由职业者的。
介紹
在上一篇博文中,我看了一下我们如何使用.NET Core CLI来生成依赖关系图,它允许你确定.NET Core或.NET Standard项目的包引用。这也是我在开发dotnet-outdated时使用的技术。
当dotnet-outdated开始获得一定的吸引力后,GitHub版本库中的一个问题是要求支持检测过时的过渡性依赖。
过渡性依赖关系概述
那么什么是转义依赖呢?
好吧,我们再以dotnet-outdated的源代码为例。从上一篇博文中的示例应用程序中我们可以看到,我在该项目中引用的一个包是McMaster.Extensions.CommandLineUtils包。
如果我们查看McMaster.Extensions.CommandLineUtils NuGet页面,我们会发现,它又依赖于一些包,其中之一是System.ComponentModel.Annotations。
在这个例子中,System.ComponentModel.Annotations是所谓的过渡性依赖。这意味着它将被我们的应用程序转义引用,因为我们的应用程序引用的另一个包依赖于它。
特别是,它依赖于该包的v4.4.1或更高版本。NuGet的工作方式是,默认情况下,它将引用满足该标准的最低要求版本--换句话说,v4.4.1。
如果我们查看System.ComponentModel.Annotations的NuGet页面,我们会看到该包的一个较新的版本。
所以这就是GitHub问题的要求。他们希望dotnet-outdated报告说,尽管我们的应用程序引用了System.ComponentModel.Annotations的v4.4.1,但该包的一个较新的版本存在。如果开发者想使用新的版本,而不是临时引用的旧版本,那么就可以直接引用那个版本。
在这篇博文的其余部分,我将演示如何确定应用程序的过渡性依赖。确定该依赖关系是否过时不在本博文的范围内。
确定转义依赖关系
好了,现在我们知道了什么是转义依赖,那么我们该如何找到它们呢?
我的第一个想法是,我需要使用NuGet API来确定这些。事实上,在GitHub的那个问题被打开后不久,就有人提交了一个pull请求,这样做。
我接受了那个PR,但开始寻找更好的方法。我下定决心要弄清楚是否不能再搭载.NET Core CLI正在做的工作--就像我做依赖图一样。
结果发现确实有一种方法可以用.NET Core CLI来做这件事。
当.NET Core CLI恢复包时,它会创建一个project.assets.json文件,其中列出了应用程序的依赖关系。该文件生成并放置在项目的构建资产输出路径中。这通常是项目的/obj目录,但也可能有所不同。值得庆幸的是,我们上次生成的依赖关系图在项目的RestoreMetadata.OutputPath属性中包含了这些信息。
下面是project.assets.json文件的简化版。
{
"version": 3,
"targets": {
".NETCoreApp,Version=v2.1": {
"McMaster.Extensions.CommandLineUtils/2.2.4": {
"type": "package",
"dependencies": {
"System.ComponentModel.Annotations": "4.4.1"
},
"compile": {
"lib/netstandard2.0/McMaster.Extensions.CommandLineUtils.dll": {}
},
"runtime": {
"lib/netstandard2.0/McMaster.Extensions.CommandLineUtils.dll": {}
}
},
...
"System.ComponentModel.Annotations/4.4.1": {
"type": "package",
"compile": {
"ref/netcoreapp2.0/_._": {}
},
"runtime": {
"lib/netcoreapp2.0/_._": {}
}
}
...
},
"libraries": {
...
},
"projectFileDependencyGroups": {
...
},
"packageFolders": {
...
},
"project": {
...
}
}
对于我们的目的来说,project.assets.json 的重要部分是 targets 节点。它包含了应用程序的每个目标框架的条目,而这些目标框架又包含了应用程序引用的每个包的条目--不管它是直接依赖还是转义依赖。这些依赖关系中的每一个都可以有一个dependencies关系节点,列出它的依赖关系。
因此,为了得到应用程序的正确的依赖树,我们需要将project.assets.json的内容与上次的依赖关系图的内容结合起来。
首先,我们将使用依赖关系图来确定应用程序的显式依赖关系。对于每一个依赖关系,我们都会进入project.assets.json,并在相关的目标框架下找到该依赖关系。然后,我们将查看依赖关系的依赖关系节点,以找到它的过渡性依赖关系。当然,每个转义依赖关系又可以有自己的依赖关系,所以我们需要递归处理。
应用逻辑
首先,我们需要一个方法来运行dotnet restore命令,然后加载project.assets.json文件。为此,我创建了一个LockFileService类。和上次的依赖图一样,NuGet.ProjectModel包中包含了一个LockFile类,它是project.assets.json文件的一个很好的.NET封装器。我们还将使用 LockFileUtilities 来加载 project.assets.json 文件的内容。
public class LockFileService
{
public LockFile GetLockFile(string projectPath, string outputPath)
{
// Run the restore command
var dotNetRunner = new DotNetRunner();
string[] arguments = new[] {"restore", $"\"{projectPath}\""};
var runStatus = dotNetRunner.Run(Path.GetDirectoryName(projectPath), arguments);
// Load the lock file
string lockFilePath = Path.Combine(outputPath, "project.assets.json");
return LockFileUtilities.GetLockFile(lockFilePath, NuGet.Common.NullLogger.Instance);
}
}
现在我们可以更新上次的逻辑,从 LockFile 实例中定位每个依赖关系,然后调用 ReportDependency 方法将依赖关系的名称打印到控制台。
foreach(var project in dependencyGraph.Projects.Where(p => p.RestoreMetadata.ProjectStyle == ProjectStyle.PackageReference))
{
// Generate lock file
var lockFileService = new LockFileService();
var lockFile = lockFileService.GetLockFile(project.FilePath, project.RestoreMetadata.OutputPath);
Console.WriteLine(project.Name);
foreach(var targetFramework in project.TargetFrameworks)
{
Console.WriteLine($" [{targetFramework.FrameworkName}]");
var lockFileTargetFramework = lockFile.Targets.FirstOrDefault(t => t.TargetFramework.Equals(targetFramework.FrameworkName));
if (lockFileTargetFramework != null)
{
foreach(var dependency in targetFramework.Dependencies)
{
var projectLibrary = lockFileTargetFramework.Libraries.FirstOrDefault(library => library.Name == dependency.Name);
ReportDependency(projectLibrary, lockFileTargetFramework, 1);
}
}
}
}
ReportDependency方法对每一个子依赖都是递归调用的。
private static void ReportDependency(LockFileTargetLibrary projectLibrary, LockFileTarget lockFileTargetFramework, int indentLevel)
{
Console.Write(new String(' ', indentLevel * 2));
Console.WriteLine($"{projectLibrary.Name}, v{projectLibrary.Version}");
foreach (var childDependency in projectLibrary.Dependencies)
{
var childLibrary = lockFileTargetFramework.Libraries.FirstOrDefault(library => library.Name == childDependency.Id);
ReportDependency(childLibrary, lockFileTargetFramework, indentLevel + 1);
}
}
这就是结果。
实际的输出可能会变得相当冗长,但在上面的截图中,你可以看到输出的层次性,因为我们报告了每个依赖关系,对于每个依赖关系,我们报告了它的子依赖关系,等等。
结束语 这篇博文演示了如何使用.NET Core CLI生成的标准资产来理解应用程序的结构--特别是应用程序的依赖关系以及转义依赖关系。
演示这些技术的示例应用程序可以在 github.com/jerriepelse… 找到。