如何使用本地构建的 nuget 包进行调试?

Posted

技术标签:

【中文标题】如何使用本地构建的 nuget 包进行调试?【英文标题】:How can you debug with locally built nuget packages? 【发布时间】:2021-10-31 18:23:22 【问题描述】:

我们有几个使用共享公共库(内部,非公共)的项目。 (C#.net 项目,使用 Visual Studio,设置为解决方案)

Solution A ---| 
              |----> Common Library (Nuget Package)
Solution B ---|

这通常可以正常工作。公共库发布到内部 nuget 提要,然后两个项目都可以使用该提要,无论是在本地运行时还是在构建服务器上运行时。

问题是当我们想要对公共库进行更改时。似乎测试更改的唯一方法是重建公共库,将更新的包发布到提要,升级项目 A 中的包(并且由于解决方案中有很多项目需要一段时间),然后进行测试。这个循环需要很长时间。

理想情况下,我正在寻找一种能够将公共库项目包含在与项目 A(或 B)相同的解决方案中,并对库进行更改,然后运行和调试代码的方法一种解决方案。但这样做需要很长时间。它需要遍历项目 A 中的所有 csproj 文件,并将所有引用更改为项目引用而不是包引用。我尝试在 csproj 文件中使用条件,以便在使用本地项目和使用 nuget 包进行构建之间轻松切换,但它似乎并不有效。 Visual Studio 似乎对引用感到困惑,构建失败。

还有其他选择吗?我觉得这应该是一个相当普遍的情况,所以是否有一种公认的典型设置方式,这样既可以为公共库使用 nuget 包,又可以将它们连接在一起并轻松调试。


作为对@vernou 的回应,我们尝试在 csproj 文件中使用条件属性进行设置。我们设置了一个配置类型来控制使用哪个引用。

  <ItemGroup Condition=" '$(Configuration)' == 'DebugWithLocalCommon' ">
    <ProjectReference Include="$(CommonPath)Common.Project.Name\Common.Project.Name.csproj">
      <Project>11111111-1111-1111-1111-111111111111</Project>
      <Name>Common.Project.Name</Name>
    </ProjectReference>
    ...
    Multiple project references continue here
    ...
  </ItemGroup>

  <ItemGroup Condition=" '$(Configuration)' != 'DebugWithLocalCommon' ">
    <Reference Include="Common.Project.Name, Version=4.0.26.0, Culture=neutral, PublicKeyToken=1111111111111111, processorArchitecture=MSIL">
      <HintPath>..\packages\Common.Project.Name.4.0.26\lib\netstandard2.0\Common.Project.Name.dll</HintPath>
    </Reference>
    ...
    Multiple package references continue here
    ...
  </ItemGroup>

我们实际上有一些项目使用经典的 csproj 引用格式(如上),还有一些使用较新的“包引用”格式来引用 nuget 包。在这些情况下,第二个块会更像这样:

  <ItemGroup Condition=" '$(Configuration)' != 'DebugWithLocalCommon' ">
    <PackageReference Include="Common.Project.Name">
      <Version>4.0.26</Version>
    </PackageReference>
    ...
    Multiple package references continue here
    ...
  </ItemGroup>

我们遇到的问题是它似乎混淆了视觉工作室。有时 UI 中应该出现多个引用(这可能只是烦人而不是完全破坏交易),但有时它似乎也无法完全理解引用并拒绝构建或识别正在引用的包并且只是给出“您可能缺少参考”类型错误。

我们也尝试过在 csproj 中使用 &lt;Choose&gt;&lt;When&gt;&lt;Otherwise&gt;,但结果行为完全相同。

【问题讨论】:

问题是“在发布前用其他解决方案测试包修改”。对吗? @vernou,是的,差不多。现在,测试公共库修改的唯一方法似乎是发布新包并更新每个项目的包引用。 你如何测试?你是从 VS 调试开始的吗? @vernou,是的,理想情况下,我们可以通过从 Visual Studio 开始调试来进行测试。 csproj 中的条件属性似乎非常合适。你能展示一下你的尝试吗? 【参考方案1】:

这是一个挑战,解决方案相当复杂。它还取决于您的源代码控制和开发环境的设置方式。但我可以解释一个潜在的解决方案。

简而言之,您需要一个自定义工具,它可以只是一个命令行 exe 项目,它将更新 csproj 文件中的引用,而无需进行所有手动更改。

开发环境设置:

您会发现,作为个人的开发人员都可能将源代码克隆到他们机器上的不同位置。所以每个人的代码在不同的地方。只要每个人在主文件夹中具有相同的结构,这样就可以使用相对位置添加引用。

EG,如果每个人都使用: C:\Source\Common C:\Source\App1\

或者: C:\代码\通用 C:\代码\App1\

这很好,但是如果在某人的机器上将 Common 文件夹称为“cmn”,这对他们来说将是一个问题。

所以现在,在 VS 中,您可以在 App1 中使用相对位置 EG 对您的 Common 项目进行项目引用: ..\Common\Common.csproj

项目类型问题:

您需要考虑您拥有的项目类型。例如,如果您在 Visual Studio 中创建一个新项目并使用 .Net Framework 4.xx,您将拥有现在所谓的“旧”项目类型。如果您使用 .Net Standard 或 .Net Core 创建,您将获得新的项目类型(这对于做所有这些 nuget 的东西来说要好得多)。

旧项目类型在每个项目的 packages.config 中引用了所有 nuget 包。它还引用了实际项目,包括 csproj 中的确切路径和版本。这就是使过程更加繁琐的原因。

新项目类型只引用了csproj文件中的nuget包(超级骗子)。

但是如果您仍然拥有旧的项目类型也没关系,自定义工具仍然可以处理这个问题。

工具:

这只是基础知识,因此不是一个完整的解决方案,但希望您能从中获得足够的实施。

确保您在副本上进行测试,或首先启动一个新分支以确保没有任何东西永久损坏。这只会迎合我假设您正在使用的旧式项目。

创建一个新的控制台应用程序项目并添加一些用于处理文件的基本类:

PackageConfig.cs

[Serializable]
[DesignerCategory("code")]
[XmlType(AnonymousType = true, IncludeInSchema = true)]
[XmlRoot(ElementName = "packages", IsNullable = false)]
public class PackageConfig

    [XmlElement("package")]
    public Package[] Packages  get; set; 


[Serializable]
[DesignerCategory("code")]
public class Package

    [XmlAttribute("id")]
    public string Id  get; set; 

    [XmlAttribute("version")]
    public string Version  get; set; 

    [XmlAttribute("targetFramework")]
    public string TargetFramework  get; set; 

XmlSerializer.cx

class XmlSerializer

    private System.Xml.Serialization.XmlSerializer GetSerializer<T>()
    
        return new System.Xml.Serialization.XmlSerializer(typeof(T));
    

    public string Serialize<T>(T instance, bool omitXmlDeclaration = false)
    
        System.Xml.Serialization.XmlSerializer xmlSerializer = GetSerializer<T>();
        StringBuilder stringBuilder = new StringBuilder();

        XmlWriterSettings settings = new XmlWriterSettings
        
            OmitXmlDeclaration = omitXmlDeclaration,
            Encoding = Encoding.UTF8
        ;

        using (XmlWriter writer = XmlWriter.Create(new StringWriter(stringBuilder), settings))
        
            xmlSerializer.Serialize(writer, instance);
        

        return stringBuilder.ToString();
    

    public T Deserialize<T>(string xml)
    
        System.Xml.Serialization.XmlSerializer xmlSerializer = GetSerializer<T>();
        using (XmlTextReader reader = new XmlTextReader(new StringReader(xml)))
            return (T)xmlSerializer.Deserialize(reader);
    

将您的 program.cs 更新为:

class Program

    static void Main(string[] args)
    
        try
        
            string packageVersion = null;
            if (args != null && args.length == 1)
                packageVersion = args[0];
            UpdateProjects(packageVersion);
        
        catch (Exception ex)
        
            Console.Error.WriteLine(ex.ToString());
        
    

    const string _packageId = "Common";
    delegate string UpdateContent(string content);

    static void UpdateProjects(string packageVersion)
    
        if (packageVersion == null)
            UseLocalReferences();
        else
            UsePackageReferences(packageVersion);
    

    static void UseLocalReferences()
    
        RemoveNugetPackage();
        ProcessProjects(content =>
        
            content = RemoveReference(content);
            content = AddReference(content, null);
            return content;
        );
    

    static void UsePackageReferences(string packageVersion)
    
        AddNugetPackage(packageVersion);
        ProcessProjects(content =>
        
            content = RemoveReference(content);
            content = AddReference(content, packageVersion);
            return content;
        );
    

    static void ProcessProjects(UpdateContent update)
    
        string folder = Environment.CurrentDirectory;
        string[] files = Directory.GetFiles(folder, "*.csproj", SearchOption.AllDirectories);

        foreach (string fileName in files)
        
            try
            
                string content = File.ReadAllText(fileName);

                content = update(content);

                using (StreamWriter writer = new StreamWriter(fileName))
                
                    writer.Write(content);
                

                Console.WriteLine("Updated: " + fileName);
            
            catch (Exception ex)
            
                Console.Error.WriteLine($"Error updating fileName:");
                Console.Error.WriteLine(ex.ToString());
            
        
    

    static void RemoveNugetPackage()
    
        string folder = Environment.CurrentDirectory;
        Console.WriteLine($"Updating all packages.config files under folder");

        XmlSerializer xmlSerializer = new XmlSerializer();

        string[] packageConfigFiles = Directory.GetFiles(folder, "packages.config", SearchOption.AllDirectories);
        foreach (string packageConfigFile in packageConfigFiles)
        
            PackageConfig packageConfig = xmlSerializer.Deserialize<PackageConfig>(File.ReadAllText(packageConfigFile));
            List<Package> packages = new List<Package>(packageConfig.Packages);

            if (packages.Any(x => x.Id == _packageId))
                packages.First(x => x.Id == _packageId).Version = packageVersion;
            else
                packages.Add(new Package  Id = _packageId, Version = packageVersion, TargetFramework = "net462" ); // may need to change the framework here

            packages.Sort((x, y) => string.Compare(x.Id, y.Id));
            packageConfig.Packages = packages.ToArray();
            File.WriteAllText(packageConfigFile, xmlSerializer.Serialize(packageConfig));
            Console.WriteLine($"packageConfigFile updated");
        

        Console.WriteLine("Update of packages.config files complete");
    

    static void AddNugetPackage(string packageVersion)
    
        string folder = Environment.CurrentDirectory;
        Console.WriteLine($"Updating all packages.config files under folder");

        XmlSerializer xmlSerializer = new XmlSerializer();

        string[] packageConfigFiles = Directory.GetFiles(folder, "packages.config", SearchOption.AllDirectories);
        foreach (string packageConfigFile in packageConfigFiles)
        
            PackageConfig packageConfig = xmlSerializer.Deserialize<PackageConfig>(File.ReadAllText(packageConfigFile));
            List<Package> packages = new List<Package>(packageConfig.Packages);

            Package package = packages.FirstOrDefault(x => x.Id == _packageId);
            if (package != null)
                packages.Remove(package);

            packageConfig.Packages = packages.ToArray();
            File.WriteAllText(packageConfigFile, xmlSerializer.Serialize(packageConfig));
            Console.WriteLine($"packageConfigFile updated");
        

        Console.WriteLine("Update of packages.config files complete");
    

    static string RemoveReference(string content)
    
        string[] lines = content.Split(new string[]  "\r\n" , StringSplitOptions.None);

        StringBuilder sb = new StringBuilder();
        bool removing = false;

        foreach (string line in lines)
        
            if (!removing && line.Trim().StartsWith($"<Reference Include=\"_packageId\""))
                removing = true;
            else if (removing)
            
                if (line.Trim() == "</Reference>")
                    removing = false;
            
            else
                sb.AppendLine(line);
        

        return sb.ToString();
    

    static string AddReference(string content, string packageVersion)
    
        string[] lines = content.Split(new string[]  "\r\n" , StringSplitOptions.None);

        StringBuilder sb = new StringBuilder();

        foreach (string line in lines)
        
            if (line.Trim() == "<Reference Include=\"System\" />") // find where the references need to be inserted
            
                if (packageVersion == null)
                
                    sb.AppendLine($"    <Reference Include=\"_packageId\">");
                    sb.AppendLine($"      <SpecificVersion>False</SpecificVersion>");
                    sb.AppendLine($"      <HintPath>..\\Common\\bin\\$(Configuration)\\_packageId.dll</HintPath>");
                    sb.AppendLine($"    </Reference>");
                
                else
                
                    sb.AppendLine($"    <Reference Include=\"_packageId, Version = packageVersion, Culture = neutral, processorArchitecture = MSIL\">");
                    sb.AppendLine($"      <HintPath>..\\packages\\_packageId.packageVersion\\lib\\net462\\_packageId.dll</HintPath>"); // again, framework may need updating
                    sb.AppendLine($"    </Reference>");
                
            

            sb.AppendLine(line);
        

        return sb.ToString();
    

使用工具:

当您编译该工具时,将 exe 复制到您的主项目文件夹 EG C:\Source\App1 - 然后您可以使用以下命令运行它:

updatetool
(to set the local references)

updatetool 1.0.1
(to set to the nuget package and references)

这一切都未经测试,但基于我使用的解决方案!

【讨论】:

【参考方案2】:

如果你使用 git,可能是git-submodule。

我也有类似的情况。我为 Entity Framework Core 开发了一个自定义提供程序,为了帮助开发,我使用 EF Core 代码源进行调试。

csproj 看起来像:

<Project Sdk="Microsoft.NET.Sdk">

  ...

  <ItemGroup Condition=" '$(Configuration)'!='Debug' ">
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[3.1,4)" />
  </ItemGroup>

  <ItemGroup Condition=" '$(Configuration)'=='Debug' ">
    <ProjectReference Include="..\efcore\src\EFCore.Relational\EFCore.Relational.csproj" PrivateAssets="contentfiles;build" />
  </ItemGroup>

</Project>

当我在Debug中启动我的项目时,我也可以调试EF Core代码源,但是当我在Release中启动时,使用的是NuGet包,我无法访问EF Core代码源。

git 的好处:

我们使用 git 对我们的代码源进行版本控制。如果我的同事获取代码源并尝试调试,则失败,因为 EF Core 不存在。

然后我在我们的 git 存储库中添加了 EF Core 作为子模块:

git submodule add https://github.com/dotnet/efcore.git
# Load a specific version
cd efcore
git checkout v3.1.18
cd ../
git commit -m "Add the submodule efcore"

现在,当我的同事克隆我们的提供程序存储库时,会自动加载 EF Core 代码源(只需在从 Visual Studio 克隆时检查 case 'load submodule')。

【讨论】:

以上是关于如何使用本地构建的 nuget 包进行调试?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确引用nuget包,这样就不会对路径进行硬编码

nuget服务器搭建,以及如何发布一个Nuget包

如何使用本地 nuget 包源进行 Dockerfile dotnet restore [重复]

如何调试从 TeamCity 部署的我的 nuget 包?

如何使用符号和源代码打包和部署 NuGet 包,以便调试器可以使用该源代码?

本地工作中的 nuget 包