从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

=========================================================================================

 

微软官方创建NuGet包说明文档

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

 

 

具体的可以参考:https://docs.microsoft.com/zh-cn/nuget/create-packages/overview-and-workflow

 

=========================================================================================

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

 

默认情况下,我们打包 NuGet 包时,目标项目安装我们的 NuGet 包会引用我们生成的库文件(dll)。除此之外,我们也可以专门做 NuGet 工具包,还可以做 NuGet 源代码包。然而做源代码包可能是其中最困难的一种了,目标项目安装完后,这些源码将直接随目标项目一起编译。

本文将从零开始,教你制作一个支持 .NET 各种类型项目的源代码包。


前置知识

在开始制作一个源代码包之间,建议你提前了解项目文件的一些基本概念:

当然就算不了解也没有关系。跟着本教程你也可以制作出来一个源代码包,只不过可能遇到了问题的时候不容易调试和解决。

制作一个源代码包

接下来,我们将从零开始制作一个源代码包。

我们接下来的将创建一个完整的解决方案,这个解决方案包括:

  1. 一个将打包成源代码包的项目
  2. 一个调试专用的项目(可选)
  3. 一个测试源代码包的项目(可选)

第一步:创建一个 .NET 项目

像其他 NuGet 包的引用项目一样,我们需要创建一个空的项目。不过差别是我们需要创建的是控制台程序。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

当创建好之后,Main 函数中的所有内容都是不需要的,于是我们删除 Main 函数中的所有内容但保留 Main 函数。

这时 Program.cs 中的内容如下:

namespace Walterlv.PackageDemo.SourceCode
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

双击创建好的项目的项目,或者右键项目 “编辑项目文件”,我们可以编辑此项目的 csproj 文件。

在这里,我将目标框架改成了 net48。实际上如果我们不制作动态源代码生成,那么这里无论填写什么目标框架都不重要。在这篇博客中,我们主要篇幅都会是做静态源代码生成,所以你大可不必关心这里填什么。

提示:如果 net48 让你无法编译这个项目,说明你电脑上没有装 .NET Framework 4.8 框架,请改成 net473, net472, net471, net47, net462, net 461, net46, net45, netcoreapp3.0, netcoreapp2.1, netcoreapp2.0 中的任何一个可以让你编译通过的目标框架即可。

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net48</TargetFramework>
  </PropertyGroup>

</Project>

第二步:组织项目的目录结构

接下来,我们会让这个项目像一个 NuGet 包的样子。当然,是 NuGet 源代码包。

请在你的项目当中创建这些文件和文件夹:

- Assets
    - build
        + Package.props
        + Package.targets
    - buildMultiTargeting
        + Package.props
        + Package.targets
    - src
        + Foo.cs
    - tools
+ Program.cs

在这里,- 号表示文件夹,+ 号表示文件。

Program.cs 是我们一开始就已经有的,可以不用管。src 文件夹里的 Foo.cs 是我随意创建的一个类,你就想往常创建正常的类文件一样创建一些类就好了。

比如我的 Foo.cs 里面的内容很简单:

using System;

namespace Walterlv.PackageDemo.SourceCode
{
    internal class Foo
    {
        public static void Print() => Console.WriteLine("Walterlv is a 逗比.");
    }
}

props 和 targets 文件你可能在 Visual Studio 的新建文件的模板中找不到这样的模板文件。这不重要,你随便创建一个文本文件,然后将名称修改成上面列举的那样即可。接下来我们会依次修改这些文件中的所有内容,所以无需担心模板自动为我们生成了哪些内容。

为了更直观,我将我的解决方案截图贴出来,里面包含所有这些文件和文件夹的解释。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我特别说明了哪些文件和文件夹是必须存在的,哪些文件和文件夹的名称一定必须与本文说明的一样。如果你是以教程的方式阅读本文,建议所有的文件和文件夹都跟我保持一样的结构和名称;如果你已经对 NuGet 包的结构有一定了解,那么可自作主张修改一些名称。

第三步:编写项目文件 csproj

现在,我们要双击项目名称或者右键“编辑项目文件”来编辑项目的 csproj 文件

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我们编辑项目文件的目的,是让我们前一步创建的项目文件夹结构真正成为 NuGet 包中的文件夹结构。

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net48</TargetFramework>

    <!-- 要求此项目编译时要生成一个 NuGet 包。-->
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>

    <!-- 这里为了方便,我将 NuGet 包的输出路径设置在了解决方案根目录的 bin 文件夹下,而不是项目的 bin 文件夹下。-->
    <PackageOutputPath>..\bin\$(Configuration)</PackageOutputPath>

    <!-- 创建 NuGet 包时,项目的输出文件对应到 NuGet 包的 tools 文件夹,这可以避免目标项目引用我们的 NuGet 包的输出文件。
         同时,如果将来我们准备动态生成源代码,而不只是引入静态源代码,还可以有机会运行我们 Program 中的 Main 函数。-->
    <BuildOutputTargetFolder>tools</BuildOutputTargetFolder>

    <!-- 此包将不会传递依赖。意味着如果目标项目安装了此 NuGet 包,那么安装目标项目包的项目不会间接安装此 NuGet 包。-->
    <DevelopmentDependency>true</DevelopmentDependency>
    
    <!-- 包的版本号,我们设成了一个预览版;当然你也可以设置为正式版,即没有后面的 -alpha 后缀。-->
    <Version>0.1.0-alpha</Version>
    
    <!-- 设置包的作者。在上传到 nuget.org 之后,如果作者名与 nuget.org 上的账号名相同,其他人浏览包是可以直接点击链接看作者页面。-->
    <Authors>walterlv</Authors>

    <!-- 设置包的组织名称。我当然写成我所在的组织 dotnet 职业技术学院啦。-->
    <Company>dotnet-campus</Company>
  </PropertyGroup>

  <!-- 在生成 NuGet 包之前,我们需要将我们项目中的文件夹结构一一映射到 NuGet 包中。-->
  <Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
    <ItemGroup>

      <!-- 将 Package.props / Package.targets 文件的名称在 NuGet 包中改为需要的真正名称。
           因为 NuGet 包要自动导入 props 和 targets 文件,要求文件的名称必须是 包名.props 和 包名.targets;
           然而为了避免我们改包名的时候还要同步改四个文件的名称,所以就在项目文件中动态生成。-->
      <None Include="Assets\build\Package.props" Pack="True" PackagePath="build\$(PackageId).props" />
      <None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
      <None Include="Assets\buildMultiTargeting\Package.props" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).props" />
      <None Include="Assets\buildMultiTargeting\Package.targets" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).targets" />

      <!-- 我们将 src 目录中的所有源代码映射到 NuGet 包中的 src 目录中。-->
      <None Include="Assets\src\**" Pack="True" PackagePath="src" />

    </ItemGroup>
  </Target>

</Project>

第四步:编写编译文件 targets

接下来,我们将编写编译文件 props 和 targets。注意,我们需要写的是四个文件的内容,不要弄错了。

如果我们做好的 NuGet 源码包被其他项目使用,那么这四个文件中的其中一对会在目标项目被自动导入(Import)。在你理解 理解 C# 项目 csproj 文件格式的本质和编译流程 一文内容之前,你可能不明白“导入”是什么意思。但作为从零开始的入门博客,你也不需要真的理解导入是什么意思,只要知道这四个文件中的代码将在目标项目编译期间运行就好。

buildMultiTargeting 文件夹中的 Package.props 文件

你只需要将下面的代码拷贝到 buildMultiTargeting 文件夹中的 Package.props 文件即可。注意将包名换成你自己的包名,也就是项目名。

<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>
  
  <!-- 为了简单起见,如果导入了这个文件,那么我们将直接再导入 ..\build\Walterlv.PackageDemo.SourceCode.props 文件。
       注意到了吗?我们并没有写 Package.props,因为我们在第三步编写项目文件时已经将这个文件转换为真实的包名了。-->
  <Import Project="..\build\Walterlv.PackageDemo.SourceCode.props" />

</Project>

buildMultiTargeting 文件夹中的 Package.targets 文件

你只需要将下面的代码拷贝到 buildMultiTargeting 文件夹中的 Package.targets 文件即可。注意将包名换成你自己的包名,也就是项目名。

<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>
  
  <!-- 为了简单起见,如果导入了这个文件,那么我们将直接再导入 ..\build\Walterlv.PackageDemo.SourceCode.targets 文件。
       注意到了吗?我们并没有写 Package.targets,因为我们在第三步编写项目文件时已经将这个文件转换为真实的包名了。-->
  <Import Project="..\build\Walterlv.PackageDemo.SourceCode.targets" />

</Project>

build 文件夹中的 Package.props 文件

下面是 build 文件夹中 Package.props 文件的全部内容。可以注意到我们几乎没有任何实质性的代码在里面。即便我们在此文件中还没有写任何代码,依然需要创建这个文件,因为后面第五步我们将添加更复杂的代码时将再次用到这个文件完成里面的内容。

现在,保持你的文件中的内容与下面一模一样就好。

<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>

</Project>

build 文件夹中的 Package.targets 文件

下面是 build 文件夹中的 Package.targets 文件的全部内容。

我们写了两个编译目标,即 Target。_WalterlvDemoEvaluateProperties 没有指定任何执行时机,但帮我们计算了两个属性:

  • _WalterlvDemoRoot 即 NuGet 包的根目录
  • _WalterlvDemoSourceFolder 即 NuGet 包中的源代码目录

另外,我们添加了一个 Message 任务,用于在编译期间显示一条信息,这对于调试来说非常方便。

_WalterlvDemoIncludeSourceFiles 这个编译目标指定在 CoreCompile 之前执行,并且执行需要依赖于 _WalterlvDemoEvaluateProperties 编译目标。这意味着当编译执行到 CoreCompile 步骤时,将在它执行之前插入 _WalterlvDemoIncludeSourceFiles 编译目标来执行,而 _WalterlvDemoIncludeSourceFiles 依赖于 _WalterlvDemoEvaluateProperties,于是 _WalterlvDemoEvaluateProperties 会插入到更之前执行。那么在微观上来看,这三个编译任务的执行顺序将是:_WalterlvDemoEvaluateProperties -> _WalterlvDemoIncludeSourceFiles -> CoreCompile

_WalterlvDemoIncludeSourceFiles 中,我们定义了一个集合 _WalterlvDemoCompile,集合中包含 NuGet 包源代码文件夹中的所有 .cs 文件。另外,我们又定义了 Compile 集合,将 _WalterlvDemoCompile 集合中的所有内容添加到 Compile 集合中。Compile 是 .NET 项目中的一个已知集合,当 CoreCompile 执行时,所有 Compile 集合中的文件将参与编译。注意到我没有直接将 NuGet 包中的源代码文件引入到 Compile 集合中,而是经过了中转。后面第五步中,你将体会到这样做的作用。

我们也添加一个 Message 任务,用于在编译期间显示信息,便于调试。

<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>

  <Target Name="_WalterlvDemoEvaluateProperties">
    <PropertyGroup>
      <_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
      <_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
    </PropertyGroup>
    <Message Text="1. 初始化源代码包的编译属性" />
  </Target>

  <!-- 引入 C# 源码。 -->
  <Target Name="_WalterlvDemoIncludeSourceFiles"
          BeforeTargets="CoreCompile"
          DependsOnTargets="_WalterlvDemoEvaluateProperties">
    <ItemGroup>
      <_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
      <Compile Include="@(_WalterlvDemoCompile)" />
    </ItemGroup>
    <Message Text="2 引入源代码包中的所有源代码:@(_WalterlvDemoCompile)" />
  </Target>

</Project>

这四个文件分别的作用

我们刚刚花了很大的篇幅教大家完成 props 和 targets 文件,那么这四个文件是做什么的呢?

如果安装我们源代码包的项目使用 TargetFramework 属性写目标框架,那么 NuGet 会自动帮我们导入 build 文件夹中的两个编译文件。如果安装我们源代码包的项目使用 TargetFrameworks(注意复数形式)属性写目标框架,那么 NuGet 会自动帮我们导入 buildMultiTargeting 文件夹中的两个编译文件。

如果你对这个属性不熟悉,请回到第一步看我们一开始创建的代码,你会看到这个属性的设置的。如果还不清楚,请阅读博客:

体验和查看 NuGet 源代码包

也许你已经从本文拷贝了很多代码过去了,但直到目前我们还没有看到这些代码的任何效果,那么现在我们就可以来看看了。这可算是一个阶段性成果呢!

先编译生成一下我们一直在完善的项目,我们就可以在解决方案目录的 bin\Debug 目录下找到一个 NuGet 包。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

现在,我们要打开这个 NuGet 包看看里面的内容。你需要先去应用商店下载 NuGet Package Explorer,装完之后你就可以开始直接双击 NuGet 包文件,也就是 nupkg 文件。现在我们双击打开看看。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我们的体验到此为止。如果你希望在真实的项目当中测试,可以阅读其他博客了解如何在本地测试 NuGet 包。

第五步:加入 WPF 项目支持

截至目前,我们只是在源代码包中引入了 C# 代码。如果我们需要加入到源代码包中的代码包含 WPF 的 XAML 文件,或者安装我们源代码包的目标项目包含 WPF 的 XAML 文件,那么这个 NuGet 源代码包直接会导致无法编译通过。至于原因,你需要阅读我的另一篇博客来了解:

即便你不懂 WPF 程序的编译过程,你也可以继续完成本文的所有内容,但可能就不会明白为什么接下来我们要那样去修改我们之前创建的文件。

接下来我们将修改这些文件:

  • build 文件夹中的 Package.props 文件
  • build 文件夹中的 Package.targets 文件

build 文件夹中的 Package.props 文件

在这个文件中,我们将新增一个属性 ShouldFixNuGetImportingBugForWpfProjects。这是我取的名字,意为“是否应该修复 WPF 项目中 NuGet 包自动导入的问题”。

我做一个开关的原因是怀疑我们需要针对 WPF 项目进行特殊处理是 WPF 项目自身的 Bug,如果将来 WPF 修复了这个 Bug,那么我们将可以直接通过此开关来关闭我们在这一节做的特殊处理。另外,后面我们将采用一些特别的手段来调试我们的 NuGet 源代码包,在调试项目中我们也会将这个属性设置为 False 以关闭 WPF 项目的特殊处理。

    <Project>
    
      <PropertyGroup>
        <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
    
++      <!-- 当生成 WPF 临时项目时,不会自动 Import NuGet 中的 props 和 targets 文件,这使得在临时项目中你现在看到的整个文件都不会参与编译。
++           然而,我们可以通过欺骗的方式在主项目中通过 _GeneratedCodeFiles 集合将需要编译的文件传递到临时项目中以间接参与编译。
++           WPF 临时项目不会 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
++           所以我们通过一个属性开关 `ShouldFixNuGetImportingBugForWpfProjects` 来决定是否修复这个错误。-->
++      <ShouldFixNuGetImportingBugForWpfProjects Condition=" ‘$(ShouldFixNuGetImportingBugForWpfProjects)‘ == ‘‘ ">True</ShouldFixNuGetImportingBugForWpfProjects>
++    </PropertyGroup>
    
    </Project>

build 文件夹中的 Package.targets 文件

请按照下面的差异说明来修改你的 Package.targets 文件。实际上我们几乎删除任何代码,所以其实你可以将下面的所有内容作为你的新的 Package.targets 中的内容。

    <Project>
    
      <PropertyGroup>
        <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
      </PropertyGroup>

++    <PropertyGroup>
++      <!-- 我们增加了一个属性,用于处理 WPF 特殊项目的源代码之前,确保我们已经收集到所有需要引入的源代码。 -->
++      <_WalterlvDemoImportInWpfTempProjectDependsOn>_WalterlvDemoIncludeSourceFiles</_WalterlvDemoImportInWpfTempProjectDependsOn>
++    </PropertyGroup>
    
      <Target Name="_WalterlvDemoEvaluateProperties">
        <PropertyGroup>
          <_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
          <_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
        </PropertyGroup>
        <Message Text="1. 初始化源代码包的编译属性" />
      </Target>
    
      <!-- 引入 C# 源码。 -->
      <Target Name="_WalterlvDemoIncludeSourceFiles"
              BeforeTargets="CoreCompile"
              DependsOnTargets="_WalterlvDemoEvaluateProperties">
        <ItemGroup>
          <_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
++        <_WalterlvDemoAllCompile Include="@(_WalterlvDemoCompile)" />
          <Compile Include="@(_WalterlvDemoCompile)" />
        </ItemGroup>
--      <Message Text="2 引入源代码包中的所有源代码:@(_WalterlvDemoCompile)" />
++      <Message Text="2.1 引入源代码包中的所有源代码:@(_WalterlvDemoCompile)" />
      </Target>
    
++    <!-- 引入 WPF 源码。 -->
++    <Target Name="_WalterlvDemoIncludeWpfFiles"
++            BeforeTargets="MarkupCompilePass1"
++            DependsOnTargets="_WalterlvDemoEvaluateProperties">
++      <ItemGroup>
++        <_WalterlvDemoPage Include="$(_WalterlvDemoSourceFolder)**\*.xaml" />
++        <Page Include="@(_WalterlvDemoPage)" Link="%(_WalterlvDemoPage.FileName).xaml" />
++      </ItemGroup>
++      <Message Text="2.2 引用 WPF 相关源码:@(_WalterlvDemoPage)" />
++    </Target>

++    <!-- 当生成 WPF 临时项目时,不会自动 Import NuGet 中的 props 和 targets 文件,这使得在临时项目中你现在看到的整个文件都不会参与编译。
++         然而,我们可以通过欺骗的方式在主项目中通过 _GeneratedCodeFiles 集合将需要编译的文件传递到临时项目中以间接参与编译。
++         WPF 临时项目不会 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
++         所以我们通过一个属性开关 `ShouldFixNuGetImportingBugForWpfProjects` 来决定是否修复这个错误。-->
++    <Target Name="_WalterlvDemoImportInWpfTempProject"
++            AfterTargets="MarkupCompilePass1"
++            BeforeTargets="GenerateTemporaryTargetAssembly"
++            DependsOnTargets="$(_WalterlvDemoImportInWpfTempProjectDependsOn)"
++            Condition=" ‘$(ShouldFixNuGetImportingBugForWpfProjects)‘ == ‘True‘ ">
++      <ItemGroup>
++        <_GeneratedCodeFiles Include="@(_WalterlvDemoAllCompile)" />
++      </ItemGroup>
++      <Message Text="3. 正在欺骗临时项目,误以为此 NuGet 包中的文件是 XAML 编译后的中间代码:@(_WalterlvDemoAllCompile)" />
++    </Target>

    </Project>

我们增加了 _WalterlvDemoImportInWpfTempProjectDependsOn 属性,这个属性里面将填写一个到多个编译目标(Target)的名称(多个用分号分隔),用于告知 _WalterlvDemoImportInWpfTempProject 这个编译目标在执行之前必须确保执行的依赖编译目标。而我们目前的依赖目标只有一个,就是 _WalterlvDemoIncludeSourceFiles 这个引入 C# 源代码的编译目标。如果你有其他考虑有引入更多 C# 源代码的编译目标,则需要把他们都加上(当然本文是不需要的)。为此,我还新增了一个 _WalterlvDemoAllCompile 集合,如果存在多个依赖的编译目标会引入 C# 源代码,则需要像 _WalterlvDemoIncludeSourceFiles 一样,将他们都加入到 Compile 的同时也加入到 _WalterlvDemoAllCompile 集合中。

为什么可能有多个引入 C# 源代码的编译目标?因为本文我们只考虑了引入我们提前准备好的源代码放入源代码包中,而我们提到过可能涉及到动态生成 C# 源代码的需求。如果你有一两个编译目标会动态生成一些 C# 源代码并将其加入到 Compile 集合中,那么请将这个编译目标的名称加入到 _WalterlvDemoImportInWpfTempProjectDependsOn 属性(注意多个用分号分隔),同时将集合也引入一份到 _WalterlvDemoAllCompile 中。

_WalterlvDemoIncludeWpfFiles 这个编译目标的作用是引入 WPF 的 XAML 文件,这很容易理解,毕竟我们的源代码中包含 WPF 相关的文件。

请特别注意

  1. 我们加了一个 Link 属性,并且将其指定为 %(_WalterlvDemoPage.FileName).xaml。这意味着我们会把所有的 XAML 文件都当作在项目根目录中生成,如果你在其他的项目中用到了相对或绝对的 XAML 文件的路径,这显然会改变路径。但是,我们没有其他的方法来根据 XAML 文件所在的目录层级来自定指定 Link 属性让其在正确的层级上,所以这里才写死在根目录中。
    • 如果要解决这个问题,我们就需要在生成 NuGet 包之前生成此项目中所有 XAML 文件的正确的 Link 属性(例如改为 Views\%(_WalterlvDemoPage.FileName).xaml),这意味着需要在此项目编译期间执行一段代码,把 Package.targets 文件中为所有的 XAML 文件生成正确的 Link 属性。本文暂时不考虑这个问题,但你可以参考 dotnet-campus/SourceYard 项目来了解如何动态生成 Link
  2. 我们使用了 _WalterlvDemoPage 集合中转地存了 XAML 文件,这是必要的。因为这样才能正确通过 % 符号获取到 FileName 属性。

_WalterlvDemoImportInWpfTempProject 这个编译目标就不那么好理解了,而这个也是完美支持 WPF 项目源代码包的关键编译目标!这个编译目标指定在 MarkupCompilePass1 之后,GenerateTemporaryTargetAssembly 之前执行。GenerateTemporaryTargetAssembly 编译目标的作用是生成一个临时的项目,用于让 WPF 的 XAML 文件能够依赖同项目的 .NET 类型而编译。然而此临时项目编译期间是不会导入任何 NuGet 的 props 或 targets 文件的,这意味着我们特别添加的所有 C# 源代码在这个临时项目当中都是不存在的——如果项目使用到了我们源代码包中的源代码,那么必然因为类型不存在而无法编译通过——临时项目没有编译通过,那么整个项目也就无法编译通过。但是,我们通过在 MarkupCompilePass1GenerateTemporaryTargetAssembly 之间将我们源代码包中的所有源代码加入到 _GeneratedCodeFiles 集合中,即可将这些文件加入到临时项目中一起编译。而原本 _GeneratedCodeFiles 集合中是什么呢?就是大家熟悉的 XAML 转换而成的 xxx.g.cs 文件。

测试和发布源代码包

现在我们再次编译这个项目,你将得到一个支持 WPF 项目的 NuGet 源代码包。

完整项目结构和源代码

至此,我们已经完成了编写一个 NuGet 源代码包所需的全部源码。接下来你可以在项目中添加更多的源代码,这样打出来的源代码包也将包含更多源代码。由于我们将将 XAML 文件都通过 Link 属性指定到根目录了,所以如果你需要添加 XAML 文件,你将只能添加到我们项目中的 Assets\src 目录下,除非做 dotnet-campus/SourceYard 中类似的动态 Link 生成的处理,或者在 Package.targets 文件中手工为每一个 XAML 编写一个特别的 Link 属性。

另外,在不改变我们整体项目结构的情况下,你也可以任意添加 WPF 所需的图片资源等。但也需要在 Package.targets 中添加额外的 Resource 引用。如果没有 dotnet-campus/SourceYard 的自动生成代码,你可能也需要手工编写 Resource

接下来我会贴出更复杂的代码,用于处理更复杂的源代码包的场景。

目录结构

更复杂源代码包的项目组织形式会是下面这样图这样:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我们在 Assets 文件夹中新增了一个 assets 文件夹。由于资源在此项目中的路径必须和安装后的目标项目中一样才可以正确用 Uri 的方式使用资源,所以我们在项目文件 csproj 和编译文件 Package.targets 中都对这两个文件设置了 Link 到同一个文件夹中,这样才可以确保两边都能正常使用。

我们在 src 文件夹的不同子文件夹中创建了 XAML 文件。按照我们前面的说法,我们也需要像资源文件一样正确在 Package.targets 中设置 Link 才可以确保 Uri 是一致的。注意,我们接下来的源代码中没有在项目文件中设置 Link,原则上也是需要设置的,就像资源一样,这样才可以确保此项目和安装此 NuGet 包中的目标项目具有相同的 XAML Uri。此例子只是因为没有代码使用到了 XAML 文件的路径,所以才能得以幸免。

我们还利用了 tools 文件夹。我们在项目文件的末尾将输出文件拷贝到了 tools 目录下,这样,我们项目的 Assets 文件夹几乎与最终的 NuGet 包的文件夹结构一模一样,非常利于调试。但为了防止将生成的文件上传到版本管理,我在 tools 中添加了 .gitignore 文件:

/net*/*

项目文件

--  <Project Sdk="Microsoft.NET.Sdk">
++  <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net48</TargetFramework>
++      <UseWpf>True</UseWpf>
    
        <!-- 要求此项目编译时要生成一个 NuGet 包。-->
        <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    
        <!-- 这里为了方便,我将 NuGet 包的输出路径设置在了解决方案根目录的 bin 文件夹下,而不是项目的 bin 文件夹下。-->
        <PackageOutputPath>..\bin\$(Configuration)</PackageOutputPath>
    
        <!-- 创建 NuGet 包时,项目的输出文件对应到 NuGet 包的 tools 文件夹,这可以避免目标项目引用我们的 NuGet 包的输出文件。
             同时,如果将来我们准备动态生成源代码,而不只是引入静态源代码,还可以有机会运行我们 Program 中的 Main 函数。-->
        <BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
    
        <!-- 此包将不会传递依赖。意味着如果目标项目安装了此 NuGet 包,那么安装目标项目包的项目不会间接安装此 NuGet 包。-->
        <DevelopmentDependency>true</DevelopmentDependency>
    
        <!-- 包的版本号,我们设成了一个预览版;当然你也可以设置为正式版,即没有后面的 -alpha 后缀。-->
        <Version>0.1.0-alpha</Version>
    
        <!-- 设置包的作者。在上传到 nuget.org 之后,如果作者名与 nuget.org 上的账号名相同,其他人浏览包是可以直接点击链接看作者页面。-->
        <Authors>walterlv</Authors>
    
        <!-- 设置包的组织名称。我当然写成我所在的组织 dotnet 职业技术学院啦。-->
        <Company>dotnet-campus</Company>
      </PropertyGroup>
    
++    <!-- 我们添加的其他资源需要在这里 Link 到一个统一的目录下,以便在此项目和安装 NuGet 包的目标项目中可以用同样的 Uri 使用。 -->
++    <ItemGroup>
++      <Resource Include="Assets\assets\Icon.ico" Link="Assets\Icon.ico" Visible="False" />
++      <Resource Include="Assets\assets\Background.png" Link="Assets\Background.png" Visible="False" />
++    </ItemGroup>
      
      <!-- 在生成 NuGet 包之前,我们需要将我们项目中的文件夹结构一一映射到 NuGet 包中。-->
      <Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
        <ItemGroup>
    
          <!-- 将 Package.props / Package.targets 文件的名称在 NuGet 包中改为需要的真正名称。
               因为 NuGet 包要自动导入 props 和 targets 文件,要求文件的名称必须是 包名.props 和 包名.targets;
               然而为了避免我们改包名的时候还要同步改四个文件的名称,所以就在项目文件中动态生成。-->
          <None Include="Assets\build\Package.props" Pack="True" PackagePath="build\$(PackageId).props" />
          <None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
          <None Include="Assets\buildMultiTargeting\Package.props" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).props" />
          <None Include="Assets\buildMultiTargeting\Package.targets" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).targets" />
    
          <!-- 我们将 src 目录中的所有源代码映射到 NuGet 包中的 src 目录中。-->
          <None Include="Assets\src\**" Pack="True" PackagePath="src" />

++        <!-- 我们将 assets 目录中的所有源代码映射到 NuGet 包中的 assets 目录中。-->
++        <None Include="Assets\assets\**" Pack="True" PackagePath="assets" />
    
        </ItemGroup>
      </Target>
    
++    <!-- 在编译结束后将生成的可执行程序放到 Tools 文件夹中,使得 Assets 文件夹的目录结构与 NuGet 包非常相似,便于 Sample 项目进行及时的 NuGet 包调试。 -->
++    <Target Name="_WalterlvDemoCopyOutputToDebuggableFolder" AfterTargets="AfterBuild">
++        <ItemGroup>
++        <_WalterlvDemoToCopiedFiles Include="$(OutputPath)**" />
++        </ItemGroup>
++        <Copy SourceFiles="@(_WalterlvDemoToCopiedFiles)" DestinationFolder="Assets\tools\$(TargetFramework)" />
++    </Target>

    </Project>

编译文件

    <Project>
    
      <PropertyGroup>
        <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
      </PropertyGroup>
    
      <PropertyGroup>
        <!-- 我们增加了一个属性,用于处理 WPF 特殊项目的源代码之前,确保我们已经收集到所有需要引入的源代码。 -->
        <_WalterlvDemoImportInWpfTempProjectDependsOn>_WalterlvDemoIncludeSourceFiles</_WalterlvDemoImportInWpfTempProjectDependsOn>
      </PropertyGroup>
      
      <Target Name="_WalterlvDemoEvaluateProperties">
        <PropertyGroup>
          <_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
          <_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
        </PropertyGroup>
        <Message Text="1. 初始化源代码包的编译属性" />
      </Target>
    
      <!-- 引入主要的 C# 源码。 -->
      <Target Name="_WalterlvDemoIncludeSourceFiles"
              BeforeTargets="CoreCompile"
              DependsOnTargets="_WalterlvDemoEvaluateProperties">
        <ItemGroup>
          <_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
          <_WalterlvDemoAllCompile Include="@(_WalterlvDemoCompile)" />
          <Compile Include="@(_WalterlvDemoCompile)" />
        </ItemGroup>
        <Message Text="2.1 引入源代码包中的所有源代码:@(_WalterlvDemoCompile)" />
      </Target>
    
      <!-- 引入 WPF 源码。 -->
      <Target Name="_WalterlvDemoIncludeWpfFiles"
              BeforeTargets="MarkupCompilePass1"
              DependsOnTargets="_WalterlvDemoEvaluateProperties">
        <ItemGroup>
--        <_WalterlvDemoPage Include="$(_WalterlvDemoSourceFolder)**\*.xaml" />
--        <Page Include="@(_WalterlvDemoPage)" Link="Views\%(_WalterlvDemoPage.FileName).xaml" />
++        <_WalterlvDemoRootPage Include="$(_WalterlvDemoSourceFolder)FooView.xaml" />
++        <Page Include="@(_WalterlvDemoRootPage)" Link="Views\%(_WalterlvDemoRootPage.FileName).xaml" />
++        <_WalterlvDemoThemesPage Include="$(_WalterlvDemoSourceFolder)Themes\Walterlv.Windows.xaml" />
++        <Page Include="@(_WalterlvDemoThemesPage)" Link="Views\%(_WalterlvDemoThemesPage.FileName).xaml" />
++        <_WalterlvDemoIcoResource Include="$(_WalterlvDemoRoot)assets\*.ico" />
++        <_WalterlvDemoPngResource Include="$(_WalterlvDemoRoot)assets\*.png" />
++        <Resource Include="@(_WalterlvDemoIcoResource)" Link="assets\%(_WalterlvDemoIcoResource.FileName).ico" />
++        <Resource Include="@(_WalterlvDemoPngResource)" Link="assets\%(_WalterlvDemoPngResource.FileName).png" />
        </ItemGroup>
--      <Message Text="2.2 引用 WPF 相关源码:@(_WalterlvDemoPage);@(_WalterlvDemoIcoResource);@(_WalterlvDemoPngResource)" />
++      <Message Text="2.2 引用 WPF 相关源码:@(_WalterlvDemoRootPage);@(_WalterlvDemoThemesPage);@(_WalterlvDemoIcoResource);@(_WalterlvDemoPngResource)" />
      </Target>
    
      <!-- 当生成 WPF 临时项目时,不会自动 Import NuGet 中的 props 和 targets 文件,这使得在临时项目中你现在看到的整个文件都不会参与编译。
           然而,我们可以通过欺骗的方式在主项目中通过 _GeneratedCodeFiles 集合将需要编译的文件传递到临时项目中以间接参与编译。
           WPF 临时项目不会 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
           所以我们通过一个属性开关 `ShouldFixNuGetImportingBugForWpfProjects` 来决定是否修复这个错误。-->
      <Target Name="_WalterlvDemoImportInWpfTempProject"
              AfterTargets="MarkupCompilePass1"
              BeforeTargets="GenerateTemporaryTargetAssembly"
              DependsOnTargets="$(_WalterlvDemoImportInWpfTempProjectDependsOn)"
              Condition=" ‘$(ShouldFixNuGetImportingBugForWpfProjects)‘ == ‘True‘ ">
        <ItemGroup>
          <_GeneratedCodeFiles Include="@(_WalterlvDemoAllCompile)" />
        </ItemGroup>
        <Message Text="3. 正在欺骗临时项目,误以为此 NuGet 包中的文件是 XAML 编译后的中间代码:@(_WalterlvDemoAllCompile)" />
      </Target>
    
    </Project>

开源项目

本文涉及到的所有代码均已开源到:

更多内容

SourceYard 开源项目

本文服务于开源项目 SourceYard,为其提供支持 WPF 项目的解决方案。dotnet-campus/SourceYard: Add a NuGet package only for dll reference? By using dotnetCampus.SourceYard, you can pack a NuGet package with source code. By installing the new source code package, all source codes behaviors just like it is in your project.

相关博客

更多制作源代码包的博客可以阅读。从简单到复杂的顺序:

 

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/build-source-code-package-for-wpf-projects.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,或者前往 CSDN 关注我的主页

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

 

 

出处:https://blog.walterlv.com/post/build-source-code-package-for-wpf-projects.html

 =========================================================================================

 

默认情况下,我们打包 NuGet 包时,目标项目安装我们的 NuGet 包会引用我们生成的库文件(dll)。除此之外,我们也可以专门做 NuGet 工具包,还可以做 NuGet 源代码包。然而做源代码包可能是其中最困难的一种了,目标项目安装完后,这些源码将直接随目标项目一起编译。

本文将从零开始,教你制作一个支持 .NET 各种类型项目的源代码包。


前置知识

在开始制作一个源代码包之间,建议你提前了解项目文件的一些基本概念:

当然就算不了解也没有关系。跟着本教程你也可以制作出来一个源代码包,只不过可能遇到了问题的时候不容易调试和解决。

制作一个源代码包

接下来,我们将从零开始制作一个源代码包。

我们接下来的将创建一个完整的解决方案,这个解决方案包括:

  1. 一个将打包成源代码包的项目
  2. 一个调试专用的项目(可选)
  3. 一个测试源代码包的项目(可选)

第一步:创建一个 .NET 项目

像其他 NuGet 包的引用项目一样,我们需要创建一个空的项目。不过差别是我们需要创建的是控制台程序。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

当创建好之后,Main 函数中的所有内容都是不需要的,于是我们删除 Main 函数中的所有内容但保留 Main 函数。

这时 Program.cs 中的内容如下:

namespace Walterlv.PackageDemo.SourceCode
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

双击创建好的项目的项目,或者右键项目 “编辑项目文件”,我们可以编辑此项目的 csproj 文件。

在这里,我将目标框架改成了 net48。实际上如果我们不制作动态源代码生成,那么这里无论填写什么目标框架都不重要。在这篇博客中,我们主要篇幅都会是做静态源代码生成,所以你大可不必关心这里填什么。

提示:如果 net48 让你无法编译这个项目,说明你电脑上没有装 .NET Framework 4.8 框架,请改成 net473, net472, net471, net47, net462, net 461, net46, net45, netcoreapp3.0, netcoreapp2.1, netcoreapp2.0 中的任何一个可以让你编译通过的目标框架即可。

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net48</TargetFramework>
  </PropertyGroup>

</Project>

第二步:组织项目的目录结构

接下来,我们会让这个项目像一个 NuGet 包的样子。当然,是 NuGet 源代码包。

请在你的项目当中创建这些文件和文件夹:

- Assets
    - build
        + Package.props
        + Package.targets
    - buildMultiTargeting
        + Package.props
        + Package.targets
    - src
        + Foo.cs
    - tools
+ Program.cs

在这里,- 号表示文件夹,+ 号表示文件。

Program.cs 是我们一开始就已经有的,可以不用管。src 文件夹里的 Foo.cs 是我随意创建的一个类,你就想往常创建正常的类文件一样创建一些类就好了。

比如我的 Foo.cs 里面的内容很简单:

using System;

namespace Walterlv.PackageDemo.SourceCode
{
    internal class Foo
    {
        public static void Print() => Console.WriteLine("Walterlv is a 逗比.");
    }
}

props 和 targets 文件你可能在 Visual Studio 的新建文件的模板中找不到这样的模板文件。这不重要,你随便创建一个文本文件,然后将名称修改成上面列举的那样即可。接下来我们会依次修改这些文件中的所有内容,所以无需担心模板自动为我们生成了哪些内容。

为了更直观,我将我的解决方案截图贴出来,里面包含所有这些文件和文件夹的解释。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我特别说明了哪些文件和文件夹是必须存在的,哪些文件和文件夹的名称一定必须与本文说明的一样。如果你是以教程的方式阅读本文,建议所有的文件和文件夹都跟我保持一样的结构和名称;如果你已经对 NuGet 包的结构有一定了解,那么可自作主张修改一些名称。

第三步:编写项目文件 csproj

现在,我们要双击项目名称或者右键“编辑项目文件”来编辑项目的 csproj 文件

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我们编辑项目文件的目的,是让我们前一步创建的项目文件夹结构真正成为 NuGet 包中的文件夹结构。

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net48</TargetFramework>

    <!-- 要求此项目编译时要生成一个 NuGet 包。-->
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>

    <!-- 这里为了方便,我将 NuGet 包的输出路径设置在了解决方案根目录的 bin 文件夹下,而不是项目的 bin 文件夹下。-->
    <PackageOutputPath>..\bin\$(Configuration)</PackageOutputPath>

    <!-- 创建 NuGet 包时,项目的输出文件对应到 NuGet 包的 tools 文件夹,这可以避免目标项目引用我们的 NuGet 包的输出文件。
         同时,如果将来我们准备动态生成源代码,而不只是引入静态源代码,还可以有机会运行我们 Program 中的 Main 函数。-->
    <BuildOutputTargetFolder>tools</BuildOutputTargetFolder>

    <!-- 此包将不会传递依赖。意味着如果目标项目安装了此 NuGet 包,那么安装目标项目包的项目不会间接安装此 NuGet 包。-->
    <DevelopmentDependency>true</DevelopmentDependency>
    
    <!-- 包的版本号,我们设成了一个预览版;当然你也可以设置为正式版,即没有后面的 -alpha 后缀。-->
    <Version>0.1.0-alpha</Version>
    
    <!-- 设置包的作者。在上传到 nuget.org 之后,如果作者名与 nuget.org 上的账号名相同,其他人浏览包是可以直接点击链接看作者页面。-->
    <Authors>walterlv</Authors>

    <!-- 设置包的组织名称。我当然写成我所在的组织 dotnet 职业技术学院啦。-->
    <Company>dotnet-campus</Company>
  </PropertyGroup>

  <!-- 在生成 NuGet 包之前,我们需要将我们项目中的文件夹结构一一映射到 NuGet 包中。-->
  <Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
    <ItemGroup>

      <!-- 将 Package.props / Package.targets 文件的名称在 NuGet 包中改为需要的真正名称。
           因为 NuGet 包要自动导入 props 和 targets 文件,要求文件的名称必须是 包名.props 和 包名.targets;
           然而为了避免我们改包名的时候还要同步改四个文件的名称,所以就在项目文件中动态生成。-->
      <None Include="Assets\build\Package.props" Pack="True" PackagePath="build\$(PackageId).props" />
      <None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
      <None Include="Assets\buildMultiTargeting\Package.props" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).props" />
      <None Include="Assets\buildMultiTargeting\Package.targets" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).targets" />

      <!-- 我们将 src 目录中的所有源代码映射到 NuGet 包中的 src 目录中。-->
      <None Include="Assets\src\**" Pack="True" PackagePath="src" />

    </ItemGroup>
  </Target>

</Project>

第四步:编写编译文件 targets

接下来,我们将编写编译文件 props 和 targets。注意,我们需要写的是四个文件的内容,不要弄错了。

如果我们做好的 NuGet 源码包被其他项目使用,那么这四个文件中的其中一对会在目标项目被自动导入(Import)。在你理解 理解 C# 项目 csproj 文件格式的本质和编译流程 一文内容之前,你可能不明白“导入”是什么意思。但作为从零开始的入门博客,你也不需要真的理解导入是什么意思,只要知道这四个文件中的代码将在目标项目编译期间运行就好。

buildMultiTargeting 文件夹中的 Package.props 文件

你只需要将下面的代码拷贝到 buildMultiTargeting 文件夹中的 Package.props 文件即可。注意将包名换成你自己的包名,也就是项目名。

<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>
  
  <!-- 为了简单起见,如果导入了这个文件,那么我们将直接再导入 ..\build\Walterlv.PackageDemo.SourceCode.props 文件。
       注意到了吗?我们并没有写 Package.props,因为我们在第三步编写项目文件时已经将这个文件转换为真实的包名了。-->
  <Import Project="..\build\Walterlv.PackageDemo.SourceCode.props" />

</Project>

buildMultiTargeting 文件夹中的 Package.targets 文件

你只需要将下面的代码拷贝到 buildMultiTargeting 文件夹中的 Package.targets 文件即可。注意将包名换成你自己的包名,也就是项目名。

<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>
  
  <!-- 为了简单起见,如果导入了这个文件,那么我们将直接再导入 ..\build\Walterlv.PackageDemo.SourceCode.targets 文件。
       注意到了吗?我们并没有写 Package.targets,因为我们在第三步编写项目文件时已经将这个文件转换为真实的包名了。-->
  <Import Project="..\build\Walterlv.PackageDemo.SourceCode.targets" />

</Project>

build 文件夹中的 Package.props 文件

下面是 build 文件夹中 Package.props 文件的全部内容。可以注意到我们几乎没有任何实质性的代码在里面。即便我们在此文件中还没有写任何代码,依然需要创建这个文件,因为后面第五步我们将添加更复杂的代码时将再次用到这个文件完成里面的内容。

现在,保持你的文件中的内容与下面一模一样就好。

<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>

</Project>

build 文件夹中的 Package.targets 文件

下面是 build 文件夹中的 Package.targets 文件的全部内容。

我们写了两个编译目标,即 Target。_WalterlvDemoEvaluateProperties 没有指定任何执行时机,但帮我们计算了两个属性:

  • _WalterlvDemoRoot 即 NuGet 包的根目录
  • _WalterlvDemoSourceFolder 即 NuGet 包中的源代码目录

另外,我们添加了一个 Message 任务,用于在编译期间显示一条信息,这对于调试来说非常方便。

_WalterlvDemoIncludeSourceFiles 这个编译目标指定在 CoreCompile 之前执行,并且执行需要依赖于 _WalterlvDemoEvaluateProperties 编译目标。这意味着当编译执行到 CoreCompile 步骤时,将在它执行之前插入 _WalterlvDemoIncludeSourceFiles 编译目标来执行,而 _WalterlvDemoIncludeSourceFiles 依赖于 _WalterlvDemoEvaluateProperties,于是 _WalterlvDemoEvaluateProperties 会插入到更之前执行。那么在微观上来看,这三个编译任务的执行顺序将是:_WalterlvDemoEvaluateProperties -> _WalterlvDemoIncludeSourceFiles -> CoreCompile

_WalterlvDemoIncludeSourceFiles 中,我们定义了一个集合 _WalterlvDemoCompile,集合中包含 NuGet 包源代码文件夹中的所有 .cs 文件。另外,我们又定义了 Compile 集合,将 _WalterlvDemoCompile 集合中的所有内容添加到 Compile 集合中。Compile 是 .NET 项目中的一个已知集合,当 CoreCompile 执行时,所有 Compile 集合中的文件将参与编译。注意到我没有直接将 NuGet 包中的源代码文件引入到 Compile 集合中,而是经过了中转。后面第五步中,你将体会到这样做的作用。

我们也添加一个 Message 任务,用于在编译期间显示信息,便于调试。

<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>

  <Target Name="_WalterlvDemoEvaluateProperties">
    <PropertyGroup>
      <_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
      <_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
    </PropertyGroup>
    <Message Text="1. 初始化源代码包的编译属性" />
  </Target>

  <!-- 引入 C# 源码。 -->
  <Target Name="_WalterlvDemoIncludeSourceFiles"
          BeforeTargets="CoreCompile"
          DependsOnTargets="_WalterlvDemoEvaluateProperties">
    <ItemGroup>
      <_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
      <Compile Include="@(_WalterlvDemoCompile)" />
    </ItemGroup>
    <Message Text="2 引入源代码包中的所有源代码:@(_WalterlvDemoCompile)" />
  </Target>

</Project>

这四个文件分别的作用

我们刚刚花了很大的篇幅教大家完成 props 和 targets 文件,那么这四个文件是做什么的呢?

如果安装我们源代码包的项目使用 TargetFramework 属性写目标框架,那么 NuGet 会自动帮我们导入 build 文件夹中的两个编译文件。如果安装我们源代码包的项目使用 TargetFrameworks(注意复数形式)属性写目标框架,那么 NuGet 会自动帮我们导入 buildMultiTargeting 文件夹中的两个编译文件。

如果你对这个属性不熟悉,请回到第一步看我们一开始创建的代码,你会看到这个属性的设置的。如果还不清楚,请阅读博客:

体验和查看 NuGet 源代码包

也许你已经从本文拷贝了很多代码过去了,但直到目前我们还没有看到这些代码的任何效果,那么现在我们就可以来看看了。这可算是一个阶段性成果呢!

先编译生成一下我们一直在完善的项目,我们就可以在解决方案目录的 bin\Debug 目录下找到一个 NuGet 包。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

现在,我们要打开这个 NuGet 包看看里面的内容。你需要先去应用商店下载 NuGet Package Explorer,装完之后你就可以开始直接双击 NuGet 包文件,也就是 nupkg 文件。现在我们双击打开看看。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我们的体验到此为止。如果你希望在真实的项目当中测试,可以阅读其他博客了解如何在本地测试 NuGet 包。

第五步:加入 WPF 项目支持

截至目前,我们只是在源代码包中引入了 C# 代码。如果我们需要加入到源代码包中的代码包含 WPF 的 XAML 文件,或者安装我们源代码包的目标项目包含 WPF 的 XAML 文件,那么这个 NuGet 源代码包直接会导致无法编译通过。至于原因,你需要阅读我的另一篇博客来了解:

即便你不懂 WPF 程序的编译过程,你也可以继续完成本文的所有内容,但可能就不会明白为什么接下来我们要那样去修改我们之前创建的文件。

接下来我们将修改这些文件:

  • build 文件夹中的 Package.props 文件
  • build 文件夹中的 Package.targets 文件

build 文件夹中的 Package.props 文件

在这个文件中,我们将新增一个属性 ShouldFixNuGetImportingBugForWpfProjects。这是我取的名字,意为“是否应该修复 WPF 项目中 NuGet 包自动导入的问题”。

我做一个开关的原因是怀疑我们需要针对 WPF 项目进行特殊处理是 WPF 项目自身的 Bug,如果将来 WPF 修复了这个 Bug,那么我们将可以直接通过此开关来关闭我们在这一节做的特殊处理。另外,后面我们将采用一些特别的手段来调试我们的 NuGet 源代码包,在调试项目中我们也会将这个属性设置为 False 以关闭 WPF 项目的特殊处理。

    <Project>
    
      <PropertyGroup>
        <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
    
++      <!-- 当生成 WPF 临时项目时,不会自动 Import NuGet 中的 props 和 targets 文件,这使得在临时项目中你现在看到的整个文件都不会参与编译。
++           然而,我们可以通过欺骗的方式在主项目中通过 _GeneratedCodeFiles 集合将需要编译的文件传递到临时项目中以间接参与编译。
++           WPF 临时项目不会 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
++           所以我们通过一个属性开关 `ShouldFixNuGetImportingBugForWpfProjects` 来决定是否修复这个错误。-->
++      <ShouldFixNuGetImportingBugForWpfProjects Condition=" ‘$(ShouldFixNuGetImportingBugForWpfProjects)‘ == ‘‘ ">True</ShouldFixNuGetImportingBugForWpfProjects>
++    </PropertyGroup>
    
    </Project>

build 文件夹中的 Package.targets 文件

请按照下面的差异说明来修改你的 Package.targets 文件。实际上我们几乎删除任何代码,所以其实你可以将下面的所有内容作为你的新的 Package.targets 中的内容。

    <Project>
    
      <PropertyGroup>
        <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
      </PropertyGroup>

++    <PropertyGroup>
++      <!-- 我们增加了一个属性,用于处理 WPF 特殊项目的源代码之前,确保我们已经收集到所有需要引入的源代码。 -->
++      <_WalterlvDemoImportInWpfTempProjectDependsOn>_WalterlvDemoIncludeSourceFiles</_WalterlvDemoImportInWpfTempProjectDependsOn>
++    </PropertyGroup>
    
      <Target Name="_WalterlvDemoEvaluateProperties">
        <PropertyGroup>
          <_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
          <_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
        </PropertyGroup>
        <Message Text="1. 初始化源代码包的编译属性" />
      </Target>
    
      <!-- 引入 C# 源码。 -->
      <Target Name="_WalterlvDemoIncludeSourceFiles"
              BeforeTargets="CoreCompile"
              DependsOnTargets="_WalterlvDemoEvaluateProperties">
        <ItemGroup>
          <_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
++        <_WalterlvDemoAllCompile Include="@(_WalterlvDemoCompile)" />
          <Compile Include="@(_WalterlvDemoCompile)" />
        </ItemGroup>
--      <Message Text="2 引入源代码包中的所有源代码:@(_WalterlvDemoCompile)" />
++      <Message Text="2.1 引入源代码包中的所有源代码:@(_WalterlvDemoCompile)" />
      </Target>
    
++    <!-- 引入 WPF 源码。 -->
++    <Target Name="_WalterlvDemoIncludeWpfFiles"
++            BeforeTargets="MarkupCompilePass1"
++            DependsOnTargets="_WalterlvDemoEvaluateProperties">
++      <ItemGroup>
++        <_WalterlvDemoPage Include="$(_WalterlvDemoSourceFolder)**\*.xaml" />
++        <Page Include="@(_WalterlvDemoPage)" Link="%(_WalterlvDemoPage.FileName).xaml" />
++      </ItemGroup>
++      <Message Text="2.2 引用 WPF 相关源码:@(_WalterlvDemoPage)" />
++    </Target>

++    <!-- 当生成 WPF 临时项目时,不会自动 Import NuGet 中的 props 和 targets 文件,这使得在临时项目中你现在看到的整个文件都不会参与编译。
++         然而,我们可以通过欺骗的方式在主项目中通过 _GeneratedCodeFiles 集合将需要编译的文件传递到临时项目中以间接参与编译。
++         WPF 临时项目不会 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
++         所以我们通过一个属性开关 `ShouldFixNuGetImportingBugForWpfProjects` 来决定是否修复这个错误。-->
++    <Target Name="_WalterlvDemoImportInWpfTempProject"
++            AfterTargets="MarkupCompilePass1"
++            BeforeTargets="GenerateTemporaryTargetAssembly"
++            DependsOnTargets="$(_WalterlvDemoImportInWpfTempProjectDependsOn)"
++            Condition=" ‘$(ShouldFixNuGetImportingBugForWpfProjects)‘ == ‘True‘ ">
++      <ItemGroup>
++        <_GeneratedCodeFiles Include="@(_WalterlvDemoAllCompile)" />
++      </ItemGroup>
++      <Message Text="3. 正在欺骗临时项目,误以为此 NuGet 包中的文件是 XAML 编译后的中间代码:@(_WalterlvDemoAllCompile)" />
++    </Target>

    </Project>

我们增加了 _WalterlvDemoImportInWpfTempProjectDependsOn 属性,这个属性里面将填写一个到多个编译目标(Target)的名称(多个用分号分隔),用于告知 _WalterlvDemoImportInWpfTempProject 这个编译目标在执行之前必须确保执行的依赖编译目标。而我们目前的依赖目标只有一个,就是 _WalterlvDemoIncludeSourceFiles 这个引入 C# 源代码的编译目标。如果你有其他考虑有引入更多 C# 源代码的编译目标,则需要把他们都加上(当然本文是不需要的)。为此,我还新增了一个 _WalterlvDemoAllCompile 集合,如果存在多个依赖的编译目标会引入 C# 源代码,则需要像 _WalterlvDemoIncludeSourceFiles 一样,将他们都加入到 Compile 的同时也加入到 _WalterlvDemoAllCompile 集合中。

为什么可能有多个引入 C# 源代码的编译目标?因为本文我们只考虑了引入我们提前准备好的源代码放入源代码包中,而我们提到过可能涉及到动态生成 C# 源代码的需求。如果你有一两个编译目标会动态生成一些 C# 源代码并将其加入到 Compile 集合中,那么请将这个编译目标的名称加入到 _WalterlvDemoImportInWpfTempProjectDependsOn 属性(注意多个用分号分隔),同时将集合也引入一份到 _WalterlvDemoAllCompile 中。

_WalterlvDemoIncludeWpfFiles 这个编译目标的作用是引入 WPF 的 XAML 文件,这很容易理解,毕竟我们的源代码中包含 WPF 相关的文件。

请特别注意

  1. 我们加了一个 Link 属性,并且将其指定为 %(_WalterlvDemoPage.FileName).xaml。这意味着我们会把所有的 XAML 文件都当作在项目根目录中生成,如果你在其他的项目中用到了相对或绝对的 XAML 文件的路径,这显然会改变路径。但是,我们没有其他的方法来根据 XAML 文件所在的目录层级来自定指定 Link 属性让其在正确的层级上,所以这里才写死在根目录中。
    • 如果要解决这个问题,我们就需要在生成 NuGet 包之前生成此项目中所有 XAML 文件的正确的 Link 属性(例如改为 Views\%(_WalterlvDemoPage.FileName).xaml),这意味着需要在此项目编译期间执行一段代码,把 Package.targets 文件中为所有的 XAML 文件生成正确的 Link 属性。本文暂时不考虑这个问题,但你可以参考 dotnet-campus/SourceYard 项目来了解如何动态生成 Link
  2. 我们使用了 _WalterlvDemoPage 集合中转地存了 XAML 文件,这是必要的。因为这样才能正确通过 % 符号获取到 FileName 属性。

_WalterlvDemoImportInWpfTempProject 这个编译目标就不那么好理解了,而这个也是完美支持 WPF 项目源代码包的关键编译目标!这个编译目标指定在 MarkupCompilePass1 之后,GenerateTemporaryTargetAssembly 之前执行。GenerateTemporaryTargetAssembly 编译目标的作用是生成一个临时的项目,用于让 WPF 的 XAML 文件能够依赖同项目的 .NET 类型而编译。然而此临时项目编译期间是不会导入任何 NuGet 的 props 或 targets 文件的,这意味着我们特别添加的所有 C# 源代码在这个临时项目当中都是不存在的——如果项目使用到了我们源代码包中的源代码,那么必然因为类型不存在而无法编译通过——临时项目没有编译通过,那么整个项目也就无法编译通过。但是,我们通过在 MarkupCompilePass1GenerateTemporaryTargetAssembly 之间将我们源代码包中的所有源代码加入到 _GeneratedCodeFiles 集合中,即可将这些文件加入到临时项目中一起编译。而原本 _GeneratedCodeFiles 集合中是什么呢?就是大家熟悉的 XAML 转换而成的 xxx.g.cs 文件。

测试和发布源代码包

现在我们再次编译这个项目,你将得到一个支持 WPF 项目的 NuGet 源代码包。

完整项目结构和源代码

至此,我们已经完成了编写一个 NuGet 源代码包所需的全部源码。接下来你可以在项目中添加更多的源代码,这样打出来的源代码包也将包含更多源代码。由于我们将将 XAML 文件都通过 Link 属性指定到根目录了,所以如果你需要添加 XAML 文件,你将只能添加到我们项目中的 Assets\src 目录下,除非做 dotnet-campus/SourceYard 中类似的动态 Link 生成的处理,或者在 Package.targets 文件中手工为每一个 XAML 编写一个特别的 Link 属性。

另外,在不改变我们整体项目结构的情况下,你也可以任意添加 WPF 所需的图片资源等。但也需要在 Package.targets 中添加额外的 Resource 引用。如果没有 dotnet-campus/SourceYard 的自动生成代码,你可能也需要手工编写 Resource

接下来我会贴出更复杂的代码,用于处理更复杂的源代码包的场景。

目录结构

更复杂源代码包的项目组织形式会是下面这样图这样:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我们在 Assets 文件夹中新增了一个 assets 文件夹。由于资源在此项目中的路径必须和安装后的目标项目中一样才可以正确用 Uri 的方式使用资源,所以我们在项目文件 csproj 和编译文件 Package.targets 中都对这两个文件设置了 Link 到同一个文件夹中,这样才可以确保两边都能正常使用。

我们在 src 文件夹的不同子文件夹中创建了 XAML 文件。按照我们前面的说法,我们也需要像资源文件一样正确在 Package.targets 中设置 Link 才可以确保 Uri 是一致的。注意,我们接下来的源代码中没有在项目文件中设置 Link,原则上也是需要设置的,就像资源一样,这样才可以确保此项目和安装此 NuGet 包中的目标项目具有相同的 XAML Uri。此例子只是因为没有代码使用到了 XAML 文件的路径,所以才能得以幸免。

我们还利用了 tools 文件夹。我们在项目文件的末尾将输出文件拷贝到了 tools 目录下,这样,我们项目的 Assets 文件夹几乎与最终的 NuGet 包的文件夹结构一模一样,非常利于调试。但为了防止将生成的文件上传到版本管理,我在 tools 中添加了 .gitignore 文件:

/net*/*

项目文件

--  <Project Sdk="Microsoft.NET.Sdk">
++  <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net48</TargetFramework>
++      <UseWpf>True</UseWpf>
    
        <!-- 要求此项目编译时要生成一个 NuGet 包。-->
        <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    
        <!-- 这里为了方便,我将 NuGet 包的输出路径设置在了解决方案根目录的 bin 文件夹下,而不是项目的 bin 文件夹下。-->
        <PackageOutputPath>..\bin\$(Configuration)</PackageOutputPath>
    
        <!-- 创建 NuGet 包时,项目的输出文件对应到 NuGet 包的 tools 文件夹,这可以避免目标项目引用我们的 NuGet 包的输出文件。
             同时,如果将来我们准备动态生成源代码,而不只是引入静态源代码,还可以有机会运行我们 Program 中的 Main 函数。-->
        <BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
    
        <!-- 此包将不会传递依赖。意味着如果目标项目安装了此 NuGet 包,那么安装目标项目包的项目不会间接安装此 NuGet 包。-->
        <DevelopmentDependency>true</DevelopmentDependency>
    
        <!-- 包的版本号,我们设成了一个预览版;当然你也可以设置为正式版,即没有后面的 -alpha 后缀。-->
        <Version>0.1.0-alpha</Version>
    
        <!-- 设置包的作者。在上传到 nuget.org 之后,如果作者名与 nuget.org 上的账号名相同,其他人浏览包是可以直接点击链接看作者页面。-->
        <Authors>walterlv</Authors>
    
        <!-- 设置包的组织名称。我当然写成我所在的组织 dotnet 职业技术学院啦。-->
        <Company>dotnet-campus</Company>
      </PropertyGroup>
    
++    <!-- 我们添加的其他资源需要在这里 Link 到一个统一的目录下,以便在此项目和安装 NuGet 包的目标项目中可以用同样的 Uri 使用。 -->
++    <ItemGroup>
++      <Resource Include="Assets\assets\Icon.ico" Link="Assets\Icon.ico" Visible="False" />
++      <Resource Include="Assets\assets\Background.png" Link="Assets\Background.png" Visible="False" />
++    </ItemGroup>
      
      <!-- 在生成 NuGet 包之前,我们需要将我们项目中的文件夹结构一一映射到 NuGet 包中。-->
      <Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
        <ItemGroup>
    
          <!-- 将 Package.props / Package.targets 文件的名称在 NuGet 包中改为需要的真正名称。
               因为 NuGet 包要自动导入 props 和 targets 文件,要求文件的名称必须是 包名.props 和 包名.targets;
               然而为了避免我们改包名的时候还要同步改四个文件的名称,所以就在项目文件中动态生成。-->
          <None Include="Assets\build\Package.props" Pack="True" PackagePath="build\$(PackageId).props" />
          <None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
          <None Include="Assets\buildMultiTargeting\Package.props" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).props" />
          <None Include="Assets\buildMultiTargeting\Package.targets" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).targets" />
    
          <!-- 我们将 src 目录中的所有源代码映射到 NuGet 包中的 src 目录中。-->
          <None Include="Assets\src\**" Pack="True" PackagePath="src" />

++        <!-- 我们将 assets 目录中的所有源代码映射到 NuGet 包中的 assets 目录中。-->
++        <None Include="Assets\assets\**" Pack="True" PackagePath="assets" />
    
        </ItemGroup>
      </Target>
    
++    <!-- 在编译结束后将生成的可执行程序放到 Tools 文件夹中,使得 Assets 文件夹的目录结构与 NuGet 包非常相似,便于 Sample 项目进行及时的 NuGet 包调试。 -->
++    <Target Name="_WalterlvDemoCopyOutputToDebuggableFolder" AfterTargets="AfterBuild">
++        <ItemGroup>
++        <_WalterlvDemoToCopiedFiles Include="$(OutputPath)**" />
++        </ItemGroup>
++        <Copy SourceFiles="@(_WalterlvDemoToCopiedFiles)" DestinationFolder="Assets\tools\$(TargetFramework)" />
++    </Target>

    </Project>

编译文件

    <Project>
    
      <PropertyGroup>
        <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
      </PropertyGroup>
    
      <PropertyGroup>
        <!-- 我们增加了一个属性,用于处理 WPF 特殊项目的源代码之前,确保我们已经收集到所有需要引入的源代码。 -->
        <_WalterlvDemoImportInWpfTempProjectDependsOn>_WalterlvDemoIncludeSourceFiles</_WalterlvDemoImportInWpfTempProjectDependsOn>
      </PropertyGroup>
      
      <Target Name="_WalterlvDemoEvaluateProperties">
        <PropertyGroup>
          <_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
          <_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
        </PropertyGroup>
        <Message Text="1. 初始化源代码包的编译属性" />
      </Target>
    
      <!-- 引入主要的 C# 源码。 -->
      <Target Name="_WalterlvDemoIncludeSourceFiles"
              BeforeTargets="CoreCompile"
              DependsOnTargets="_WalterlvDemoEvaluateProperties">
        <ItemGroup>
          <_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
          <_WalterlvDemoAllCompile Include="@(_WalterlvDemoCompile)" />
          <Compile Include="@(_WalterlvDemoCompile)" />
        </ItemGroup>
        <Message Text="2.1 引入源代码包中的所有源代码:@(_WalterlvDemoCompile)" />
      </Target>
    
      <!-- 引入 WPF 源码。 -->
      <Target Name="_WalterlvDemoIncludeWpfFiles"
              BeforeTargets="MarkupCompilePass1"
              DependsOnTargets="_WalterlvDemoEvaluateProperties">
        <ItemGroup>
--        <_WalterlvDemoPage Include="$(_WalterlvDemoSourceFolder)**\*.xaml" />
--        <Page Include="@(_WalterlvDemoPage)" Link="Views\%(_WalterlvDemoPage.FileName).xaml" />
++        <_WalterlvDemoRootPage Include="$(_WalterlvDemoSourceFolder)FooView.xaml" />
++        <Page Include="@(_WalterlvDemoRootPage)" Link="Views\%(_WalterlvDemoRootPage.FileName).xaml" />
++        <_WalterlvDemoThemesPage Include="$(_WalterlvDemoSourceFolder)Themes\Walterlv.Windows.xaml" />
++        <Page Include="@(_WalterlvDemoThemesPage)" Link="Views\%(_WalterlvDemoThemesPage.FileName).xaml" />
++        <_WalterlvDemoIcoResource Include="$(_WalterlvDemoRoot)assets\*.ico" />
++        <_WalterlvDemoPngResource Include="$(_WalterlvDemoRoot)assets\*.png" />
++        <Resource Include="@(_WalterlvDemoIcoResource)" Link="assets\%(_WalterlvDemoIcoResource.FileName).ico" />
++        <Resource Include="@(_WalterlvDemoPngResource)" Link="assets\%(_WalterlvDemoPngResource.FileName).png" />
        </ItemGroup>
--      <Message Text="2.2 引用 WPF 相关源码:@(_WalterlvDemoPage);@(_WalterlvDemoIcoResource);@(_WalterlvDemoPngResource)" />
++      <Message Text="2.2 引用 WPF 相关源码:@(_WalterlvDemoRootPage);@(_WalterlvDemoThemesPage);@(_WalterlvDemoIcoResource);@(_WalterlvDemoPngResource)" />
      </Target>
    
      <!-- 当生成 WPF 临时项目时,不会自动 Import NuGet 中的 props 和 targets 文件,这使得在临时项目中你现在看到的整个文件都不会参与编译。
           然而,我们可以通过欺骗的方式在主项目中通过 _GeneratedCodeFiles 集合将需要编译的文件传递到临时项目中以间接参与编译。
           WPF 临时项目不会 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
           所以我们通过一个属性开关 `ShouldFixNuGetImportingBugForWpfProjects` 来决定是否修复这个错误。-->
      <Target Name="_WalterlvDemoImportInWpfTempProject"
              AfterTargets="MarkupCompilePass1"
              BeforeTargets="GenerateTemporaryTargetAssembly"
              DependsOnTargets="$(_WalterlvDemoImportInWpfTempProjectDependsOn)"
              Condition=" ‘$(ShouldFixNuGetImportingBugForWpfProjects)‘ == ‘True‘ ">
        <ItemGroup>
          <_GeneratedCodeFiles Include="@(_WalterlvDemoAllCompile)" />
        </ItemGroup>
        <Message Text="3. 正在欺骗临时项目,误以为此 NuGet 包中的文件是 XAML 编译后的中间代码:@(_WalterlvDemoAllCompile)" />
      </Target>
    
    </Project>

开源项目

本文涉及到的所有代码均已开源到:

更多内容

SourceYard 开源项目

本文服务于开源项目 SourceYard,为其提供支持 WPF 项目的解决方案。dotnet-campus/SourceYard: Add a NuGet package only for dll reference? By using dotnetCampus.SourceYard, you can pack a NuGet package with source code. By installing the new source code package, all source codes behaviors just like it is in your project.

相关博客

更多制作源代码包的博客可以阅读。从简单到复杂的顺序:

 

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/build-source-code-package-for-wpf-projects.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,或者前往 CSDN 关注我的主页

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

 

 

出处:https://blog.walterlv.com/post/build-source-code-package-for-wpf-projects.html

 =========================================================================================

使用命令行打包 nuget 包

 

对于那些不打算涉及这么复杂而又想制作自己的 nuget 包的园友们,我是推荐使用 Nuget Package Explorer 来制作的。关于这个图形化的 nuget 包管理软件的使用,博客园内有相关的文章,大家可以搜索看看。

 

好,回归正题。但是我们都知道,图形化最大的问题就是自动化不高。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

这是我其中的一个 nuget 包,可以看见里面的文件还是比较多的,那么我每一次重新编译,需要发布新版本的时候,就得把里面大部分的文件都替换成新的文件。每次都一个一个的替换,烦啊。而且有时候还得担心有没有替换少了。那么自动化打包肯定是值得研究研究一番了。

 

在 nuget 官网上面我们可以找到一篇关于如何创建 nuget 包的教程:https://docs.nuget.org/Create/Creating-and-Publishing-a-Package

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

那么我们先下载这个命令行工具下来。

 

然后准备我们需要打包的文件(就是 dll 之类的东西)。

看了下文档,说是支持读取直接 csproj 文件打包,那我们先试一下吧,毕竟程序员的准则就是能简单就简单,能懒就懒。

这里我的项目结构是这样的:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

然后我们运行 cmd 并输入命令

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

报了个错,重新生成一下项目吧,再次运行。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

好像成了,看看目录。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

但是这 dll 所属的分类,错了吧,我建的可是 Win10 应用程序的 dll。。。

 

既然 csproj 方案不行的话,我们继续看文档。看见有一个通过描述 nuget 包的方案:https://docs.nuget.org/Create/Creating-and-Publishing-a-Package#create-the-manifest

要建立这么一个描述文件也很简单,跑个命令

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

然后我们用文本编辑器来打开这个新的 Package.nuspec 文件。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

可以看见是一个 xml 文件,然后修改下里面的属性就可以了。

再次执行。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

有警告,简单看了下,是说不知道应该打包哪些文件。

 

那么继续找找文档,最后我们可以发现这个。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

修改下我们的 nuspec 文件。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

新加上红色部分,也就是将这个 dll 打包进 nuget 包的 lib\uap10.0\ 这个目录里面。

再次执行打包命令。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

这次没警告了。

而且包的结构也没问题。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

那么只要编写好 nuspec 里面的 files 节点的话,以后执行 nuget pack XX.nuspec 这个命令的话,就能够简单地生成 nuget 包了。

 

为了再懒一点,我们将上面这个命令弄成批处理。

%~dp0nuget.exe pack %~dp0Package.nuspec -OutputDirectory %~dp0

%~dp0 这个是获取当前正在执行的这个 bat 文件的所在目录。

那么最后就变成这样:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

确保这三个文件放在一起,然后执行 package.bat 就能在当前目录生成 nuget 包了。

 

接下来我们就可以发布 nuget 包了,用命令也行,但保障一点,我还是用 GUI 工具来发布(毕竟发布前再检查一次还是有必要的)。

用 Nuget Package Explorer 打开上面生成的那个 nuget 包。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

按这里就可以了。

注意:一旦发布,nuget 上的包是不能够删除的!!只能隐藏,因此,请确保无误再发布。

 

到最后一步我们已经使用批处理来做了,那么可以再玩的花样就多去了,例如用 PowerShell 来自动化包的版本。在项目的 AssemblyInfo.cs 有一个 AssemblyVersion 的 Attribute,然后我们就可以用 PowerShell 来先读取这个版本号,然后修改 nuspec 文件里的 version 节点,再生成包。其实我现在就已经是这么做了从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目),鉴于我 PowerShell 也没学过,那段代码就不放出来丢脸了从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)。大家可以发散思维,期望在 nuget 上能看见园友发布的包包从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

 

 

https://www.cnblogs.com/h82258652/p/4898983.html

 

=========================================================================================

NuGet的使用、部署、搭建私有服务

 

 


前言

什么是NuGet?

Nuget是一个.NET平台下的开源的项目,它是Visual Studio的扩展。在使用Visual Studio开发基于.NET Framework的应用时,Nuget能把在项目中添加、移除和更新引用的工作变得更加快捷方便。

为什么要使用NuGet

如果我们项目开发不需要引用任何第三方库包或者我们自己的公共库包,那么使用NuGet毫无作用,但是实际情况恰恰相反,任何项目都需要记录日志,最好的情况是我们有一个公共的日志模块,任何老项目或新项目我们可以引用它,就无需再做开发。就那我们自己的项目来说,FC,FGOnline,FGMain,FGClient,FGServer,目前我们没有一个公共的日志模块,底层使用Kernal及其他库包可能也不是一个版本,即使是同一个版本我们开发上都是将dll手工拷来拷去。在新项目上来说这增大了工作量和开发量,因此我们需要一个库包管理机制来管理我们私有库包和我们需要使用的第三方库包。

NuGet的优点

AsyncModule.NetMQ.dll举例,AsyncModule.NetMQ.dll依赖NetMQ.dll,而NetMQ.dll又依赖AsyncIO.dll
目前我们需要数据库连接的地方我们需要引用AsyncModule.NetMQ.dll,我们可能会把它手工烤到我们需要的项目中,但是由于AsyncModule.NetMQ.dll需要依赖NetMQ.dll,因此我们还需要手工把NetMQ.dll拷到我们的项目中,同时由于NetMQ.dll需要依赖AsyncIO.dll,因此我们还需要手工把AsyncIO.dll拷到我们的项目中。依赖这当中就会有些问题,比如我们忘记拷了,或者我们拷的版本不是我们当前需要的,就会导致很多问题。
NuGet就可以让我们避免这个问题。若我们需要的库包已经导入到我们库包服务器中,那么我们只需要一条语句就可以引用该dll,同时NuGet会自动将其依赖包一起引用到我们的项目中,这完全是自动的。

使用

在VS中找到 Package Manager Console对话框
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

若界面上没有找到,则从工具-NuGet Package Manager下找
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

Get-Help NuGet

使用Get-Help NuGet命令查看帮助

PM> Get-Help nuget
TOPIC
    about_NuGet
    
SHORT DESCRIPTION
    Provides information about NuGet Package Manager commands.
           
LONG DESCRIPTION
    This topic describes the NuGet Package Manager commands. NuGet is an integrated package 
    management tool for adding libraries and tools to .NET projects.

                 
    The following NuGet cmdlets are included.

        Cmdlet					Description
        ------------------		----------------------------------------------
        Get-Package				Gets the set of installed packages.  With -ListAvailable, 
                                gets the set of packages available from the package source.

        Install-Package			Installs a package and its dependencies into the project.

        Uninstall-Package		Uninstalls a package. If other packages depend on this package, 
                                the command will fail unless the –Force option is specified.

        Update-Package			Updates a package and its dependencies to a newer version.

        Add-BindingRedirect		Examines all assemblies within the output path for a project
                                and adds binding redirects to the application (or web) 
                                configuration file where necessary.
                            
        Get-Project				Returns a reference to the DTE (Development Tools Environment) 
                                for the specified project. If none is specifed, returns the 
                                default project selected in the Package Manager Console.

        Open-PackagePage        Open the browser pointing to ProjectUrl, LicenseUrl or 
                                ReportAbuseUrl of the specified package.

        Register-TabExpansion	Registers a tab expansion for the parameters of a command.

SEE ALSO
    Online documentation: http://go.microsoft.com/fwlink/?LinkID=206619
    Get-Package
    Install-Package
    Uninstall-Package
    Update-Package
    Add-BindingRedirect
    Get-Project
    Open-PackagePage
    Register-TabExpansion

Install-Package

使用Install-Package安装库包,安装时会自动安装当前Framework知道的库包及依赖包,若不支持则会提示错误。

PM> Install-Package AsyncModule.NetMQ
Attempting to resolve dependency ‘NetMQ (≥ 4.0.0.1)‘.
Attempting to resolve dependency ‘AsyncIO (≥ 0.1.26)‘.
Installing ‘AsyncIO 0.1.26.0‘.
Successfully installed ‘AsyncIO 0.1.26.0‘.
Installing ‘NetMQ 4.0.0.1‘.
Successfully installed ‘NetMQ 4.0.0.1‘.
Installing ‘AsyncModule.NetMQ 1.1.0‘.
Successfully installed ‘AsyncModule.NetMQ 1.1.0‘.
Adding ‘AsyncIO 0.1.26.0to NuGet.Client.
Successfully added ‘AsyncIO 0.1.26.0to NuGet.Client.
Adding ‘NetMQ 4.0.0.1to NuGet.Client.
Successfully added ‘NetMQ 4.0.0.1to NuGet.Client.
Adding ‘AsyncModule.NetMQ 1.1.0to NuGet.Client.
Successfully added ‘AsyncModule.NetMQ 1.1.0to NuGet.Client.

安装的时候注意对应的库包源

Get-Package

使用Get-Package安装库包

PM> Get-Package

Id                             Version              Description/Release Notes                                                                                                                                     
--                             -------              -------------------------                                                                                                                                     
AsyncIO                        0.1.26.0             AsyncIO                                                                                                                                                       
AsyncModule.NetMQ              1.1.0                基于NetMQ的异步Socket框架                                                                                                                                            
NetMQ                          4.0.0.1              A 100% native C# port of the lightweight high performance messaging library ZeroMQ                                                                            

Uninstall-Package

使用Uninstall-Package卸载已安装的库包,依赖包不会自动卸载,有需要则需要手工卸载依赖包

PM> Uninstall-Package AsyncModule.NetMQ
Removing ‘AsyncModule.NetMQ 1.1.0‘ from NuGet.Client.
Successfully removed ‘AsyncModule.NetMQ 1.1.0‘ from NuGet.Client.
Uninstalling ‘AsyncModule.NetMQ 1.1.0‘.
Successfully uninstalled ‘AsyncModule.NetMQ 1.1.0‘.

若库包有多个版本则在库包后面加上-Version 版本号参数安装指定版本的库包。若依赖包指定版本已经安装则不会重复重新安装。

PM> Install-Package AsyncModule.NetMQ -Version 1.1.0
Attempting to resolve dependency ‘NetMQ (≥ 4.0.0.1)‘.
Attempting to resolve dependency ‘AsyncIO (≥ 0.1.26)‘.
Installing ‘AsyncModule.NetMQ 1.1.0‘.
Successfully installed ‘AsyncModule.NetMQ 1.1.0‘.
Adding ‘AsyncModule.NetMQ 1.1.0to NuGet.Client.
Successfully added ‘AsyncModule.NetMQ 1.1.0to NuGet.Client.

当然也可以使用图形界面找到上图中的Manager NuGet Package For Solution...打开图形界面,在需要安装的库包右侧点击安装,和输入命令是一样的。
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

界面左侧列表包含已安装库包,在线,更新等筛选,在线里面根据数据源分类。中间则是当前数据源库包列表,右侧则是搜索栏和选中库包的详细信息。

当安装了依赖包我们可以在项目根目录找到packages.config文件,会记录我们安装的库包及版本信息
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

同时在我们的项目文件夹下会有个packages的文件夹用于保存我们下载下来的库包
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

制作NuGet库包

若我们需要上传我们的dll到NuGet服务器中,首先需要让我们VS编译时能导出NuGet所支持的.nupkg文件
在解决方案上面右击找到Enable NuGet Package Restore点击开启功能
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
开启后我们需要手动在项目的.csproj文件中在PropertyGroup下加入以下节点

    <BuildPackage>true</BuildPackage>
    <RestorePackages>true</RestorePackages>

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
同时在Project节点内增加以下内容

<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists(‘$(SolutionDir)\.nuget\NuGet.targets‘)" />
  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
    <PropertyGroup>
      <ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
    </PropertyGroup>
    <Error Condition="!Exists(‘$(SolutionDir)\.nuget\NuGet.targets‘)" Text="$([System.String]::Format(‘$(ErrorText)‘, ‘$(SolutionDir)\.nuget\NuGet.targets‘))" />
  </Target>

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

再次编译项目就会自动编译出.nupkg文件。

如果是.Net Standard 项目直接在程序右键打包即可打包。

搭建NuGet服务器

新建一个项目
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
这里使用3.0版本的NuGet.Server,需要.Net Framework 4.6支持。
然后引用NuGet.Server库包

PM> Install-Package NuGet.Server

安装完成后,编译启动即可,就是这么简单,然后托管到IIS上。
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
上传库包的时候可能需要apikey,需要在web.config中设置。

上传NetGet库包

编译出NuGet我们需要将包上传到NuGet服务器中,这样我们才能在VS中从NuGet服务器中下载下来。这里我使用NuGet Package Explorer工具进行上传,官方支持Win10商店和使用Chocolatey下载。
若需要上传到NuGet官方服务器中可以在NuGet官网上传,但是我们一般需要上传到指定NuGet服务器上,如我们自己的NuGet服务器。
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
选择第一项找到本地的.nupkg文件
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
左侧可以编译一下信息,
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
当上传了多个版本的dll,NuGet.Server会根据包Id和Version进行分组
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

在输入命令的时候可以用TAB键智能提示出当前所有版本号
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

我们也可用通过命令上传

nuget.exe push {package file} {apikey} -Source http://www.jnuget.com:10080/nuget

当我们同一个包上传过同一个版本的时候再次上传会报错,我们需要删除NuGet.Server已存在的包,后才能再次上传。或者我们可以允许通过包同一个版本允许覆盖上传,将web.ConfigallowOverrideExistingPackageOnPush配置改为true即可

新增NuGet源

在Tools-Options-NuGet Package Manager-Package Sources可以增加数据源
从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
点击右上角的加号新增,输入完地址后点一下更新即可。

总结

通过此片文章讲解了如何使用、部署NuGet,如何编译生成,上传库包到NuGet。

 

 

出处:https://www.cnblogs.com/Jack-Blog/p/7890369.html

=========================================================================================

使用NuGet发布自己的类库包(Library Package)

NuGet是一个为大家所熟知的Visual Studio扩展,通过这个扩展,开发人员可以非常方便地在Visual Studio中安装或更新项目中所需要的第三方组件,同时也可以通过NuGet来安装一些Visual Studio的插件等。作为一名开发人员,您可能也会开发一些公共组件以供他人使用,本文将一步步介绍如何以最简单的方式将自己所开发的类库包发布到nuget上,以供更多的人使用。

背景

如果你还是不知道什么是NuGet,那么就看这样一个案例:我现在需要在我的项目中引用Castle.Core程序集,按照以往的做法,就是从Castle Projects官方网站,下载一个最新版本的dll,然后把它复制到项目的lib目录下(或者随便什么地方都行),这样做不仅繁琐,而且你需要时刻关心官网上这个程序集的最新版本信息(当然或许你也不会去关注),更烦的是,如果你是一个开源项目的Contributor,你还需要花一定的时间去管理所有的这些libs,不仅如此,如果你是使用的源代码管理系统来管理项目源码,比如使用git等,那你还不得不把这些libs上传到源代码管理系统中,否则团队中的其他组员即使获得了源代码,也无法正确编译。但这样做又大大增加了源代码的存储空间,使得代码克隆和下载都变得非常耗时。

现在,就可以直接使用NuGet来解决所有问题,我们先创建一个Class Library,命名为DaxnetNugetTest,然后在这个项目上点右键,选择Manage NuGet Packages:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

在弹出的对话框中,搜索Castle关键字,然后在搜索结果列表中选择Castle.Core,单击Install按钮:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

安装完成后,Castle.Core的程序集就被引用到项目中了,同时在项目中多出了一个packages.config文件,以向NuGet表明,当前项目使用了哪些Package,版本是什么,以及是基于哪个版本的.NET Framework。

今后,如果Castle.Core程序集有版本更新,则同样可以使用Manage NuGet Packages菜单打开上面的对话框,然后在左边的Updates列表中,就会列出发生了版本更新的Package,如果有,则单击Update按钮即可更新。

更有趣的是,如果你在解决方案上点右键,选择Enable NuGet Package Restore菜单,那么在你编译项目的时候,NuGet会自动分析出你项目所依赖的第三方组件,然后在编译开始之前会自动上网下载所需的版本,因此,你也就不要去维护这些libs了,更没必要把这些libs也上传到源代码管理系统中。

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

不过这些也都不是本文的重点,本文的重点是,介绍如何将自己的Class Library发布到NuGet上。

发布自己的类库包(Library Package)

STEP 1:在NuGet上注册并获取API Key

首先,你需要到NuGet上注册一个新的账号,然后在My Account页面,获取一个API Key,这个过程很简单,我就不作说明了。

STEP 2:下载NuGet.exe

NuGet有个命令行工具:NuGet.exe,非常好用,不过使用之前需要下载,下载地址:http://nuget.codeplex.com/downloads/get/669083。为了方便使用,请设置机器的PATH环境变量,将NuGet.exe的路径添加到PATH中。

STEP 3:设置API Key

使用以下命令设置NuGet API Key:

1
nuget setApiKey <my_api_key>

记得将上面的my_api_key替换为STEP 1中获得的API Key。

STEP 4:开发自己的类库(Class Library)

上面我们新建了一个类库:DaxnetNugetTest,并通过NuGet添加了对Castle.Core的引用,现在我们添加一些代码,来使用Castle.Core所提供的一些功能。我们将Class1.cs改名为CastleHelper.cs,此时也会将Class1类改名为CastleHelper。在CastleHelper.cs中写入以下代码:

1
2
3
4
5
6
7
public class CastleHelper
{
    public static Castle.Core.Pair<int, int> GetIntPair()
    {
        return new Castle.Core.Pair<int, int>(20, 30);
    }
}

然后,打开AssemblyInfo.cs文件,将assembly的属性设置好,记得再设置一下AssemblyVersion特性,以指定我们类库的版本。目前我们使用1.0.0.0版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[assembly: AssemblyTitle("DaxnetNugetTest")]
[assembly: AssemblyDescription("Daxnet‘s test of the NuGet.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("daxnet")]
[assembly: AssemblyProduct("DaxnetNugetTest")]
[assembly: AssemblyCopyright("Copyright © daxnet 2013")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
 
[assembly: ComVisible(false)]
 
[assembly: Guid("20662b9f-91de-4515-9c8c-ced3d61589e1")]
 
[assembly: AssemblyVersion("1.0.0.0")]

全部设置好以后,编译整个项目待用。

STEP 5:产生并修改nuspec

nuspec是NuGet将项目打包成nupkg的输入文件,可以通过nuget spec命令产生。在命令提示符下,进入DaxnetNugetTest.csproj文件所在目录,然后执行:

1
nuget spec

此时会提示创建成功:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

用notepad打开DaxnetNugetTest.nuspec文件,把需要替换的信息替换掉,不需要的tag全部删掉,注意里面的$xxx$宏,这些就是引用了AssemblyInfo.cs中的设置值,在编译产生package的时候,会使用AssemblyInfo.cs中的相应值进行替换。完成编辑后,我们的nuspec文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0"?>
<package >
  <metadata>
    <id>$id$</id>
    <version>$version$</version>
    <title>$title$</title>
    <authors>$author$</authors>
    <owners>$author$</owners>
    <licenseUrl>http://www.apache.org/licenses/LICENSE-2.0.html</licenseUrl>
    <projectUrl>http://apworks.org</projectUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>$description$</description>
    <releaseNotes>First release</releaseNotes>
    <copyright>Copyright 2013</copyright>
  </metadata>
</package>

注意两点:1、$description$使用AssemblyDescriptionAttribute的值进行替换,在产生package之前,一定要记得先编译项目,否则会提示$description$找不到的错误;2、releaseNotes如果没有,就直接删掉这个节点,如果有,则填入自己的内容,不要使用默认内容,否则会在下一步产生警告信息。

STEP 6:产生类库包(Library Package)

同样在DaxnetNugetTest.csproj路径下,使用下面的命令产生NuGet类库包:

1
nuget pack DaxnetNugetTest.csproj

成功后,提示:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

注意:由于我们的项目通过NuGet引用了Castle.Core,因此,它将会作为一个依赖组件(dependency)打包到产生的nupkg文件中。

另外,NuGet会使用默认的项目配置所产生的程序集进行打包。如果项目默认是Debug,而你需要用Release打包,则使用下面的命令:

1
nuget pack DaxnetNugetTest.csproj -Prop Configuration=Release

STEP 7:发布类库包

现在,通过以下命令发布类库包:

1
nuget push DaxnetNugetTest.1.0.0.0.nupkg

完成以后,出现以下提示:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

STEP 8:测试已发布的类库包

新建一个控制台应用程序,在项目上点右键,选择Manage NuGet Packages,在搜索框中输入DaxnetNugetTest,此时我们发布的Package已经可以显示了:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

单击Install按钮,NuGet会自动分析组件依赖关系,然后把所需要的所有程序集都下载下来并添加到项目引用中:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

写一点代码来测试:

1
2
3
4
5
6
7
8
9
class Program
{
    static void Main(string[] args)
    {
        var pair = DaxnetNugetTest.CastleHelper.GetIntPair();
        Console.WriteLine(pair.First);
        Console.WriteLine(pair.Second);
    }
}

输出如下:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

STEP 9:更新类库包

随着类库开发进度不断向前,必然会有版本更新。更新类库包很简单,只需要在AssemblyInfo.cs中更新一下版本号,然后重新执行上面的STEP 6、7即可。注意在执行STEP 7的时候,nupkg的文件名应该使用新版本的文件名。

现在,我们重新打开DaxnetNugetTest项目,将CastleHelper类中的20,30改为40,50,然后打开AssemblyInfo.cs,版本号升级为2.0.0.0,重新编译项目,并重新产生、发布nupkg:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

再打开用来测试的控制台程序,同样打开Manage NuGet Packages对话框,我们可以在Updates中看到,DaxnetNugetTest有了更新:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

点击Update按钮,将类库更新到最新版本。重新运行这个控制台程序,我们发现,输出已经是最新版本的值了:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

STEP 10:删除已发布的包

原则上,NuGet不允许用户删除已发布的包,而只能将其设置为不显示在Manage NuGet Packages的列表中。打开www.nuget.org,用已注册的账户登录后,可以在My Account页面选择Manage My Packages链接进入管理页面:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

进入后,可以看到我们已发布的Packages:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

点击DaxnetNugetTest左边的小垃圾桶图标,即可进入Listing页面,页面中我们也能看到“Permanently deleting packages is not supported”的提示。要将Package从Package List中移除,只需要去掉List DaxnetNugetTest 2.0.0.0 in search results选项前面的钩钩,然后单击Save按钮保存即可:

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

总结

本文简要介绍了NuGet的使用,并介绍了一种将自己开发的类库以NuGet Package的方式发布到NuGet服务器的简单方法。NuGet功能非常强大,有兴趣的朋友可以上www.nuget.org进行学习研究。

 

出处:https://www.cnblogs.com/daxnet/archive/2013/05/07/3064577.html

=========================================================================================

从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

上一篇:Windows10 下安装虚拟机


下一篇:C#设计模式学习笔记:(6)适配器模式