65.9K
CodeProject 正在变化。 阅读更多。
Home

NuTemplates

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (3投票s)

2014年12月20日

CPOL

6分钟阅读

viewsIcon

9868

downloadIcon

82

如何使用 NuGet 包制作解决方案模板

Visual Studio 模板的功能相当有限。是的,您可以节省一些工作,但基本上只能处理项目和文件,且限制很多。如果我们把一些可执行代码加入到等式中会怎样?

问题

在我们组织 (NuGetting Soft) 内部,开发者需要经常创建一个典型的 VS 解决方案。它有 3 个项目:

  • 接口,包含所有公共内容、接口、服务参数和返回类型。
  • 领域,包含实际逻辑和所有私有实现内容。
  • 测试,包含所有单元测试。

除了节省人们的时间,我们还希望所有解决方案都以特定方式排列,并根据所实现的特定领域相应地命名程序集和命名空间。如果某个团队想要为人力资源构建一些东西,我们希望接口是NuGettingSoft.HumanResources.Interface,领域是NuGeetingSoft.HumanResources,单元测试是HumanResources.Tests

NuGetting

NuGet 包是部署库的一种简单而干净的方式。我最近发现您也可以在其中放入一些代码。PowerShell 脚本功能强大,而且入门不难。

您需要安装 NuGet 并确保可以从命令行访问它。然后继续创建一个带有项目的 VS 解决方案,并打开包管理器控制台 (PM)。我们看到类似这样的内容:

Playing with NuGet

创建一个 Tools\Init.ps1 文件

Param($installPath, $toolsPath, $package, $project)

Write-Host $installPath
Write-Host $toolsPath

用处不大,但我保证它会成长。这个 PowerShell 文件将在第一次安装包时执行,并在之后每次加载解决方案时执行。PowerShell 是我们可以在 NuGet 包中编程自定义操作的语言。

现在我们创建一个 nuspec 文件

<package>
  <metadata>
    <id>NuGetting</id>
    <version>1.0.0.0</version>
    <authors>NuGetteer</authors>
    <owners>NuGetteer</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Fiddle with Nuget scripts</description>
  </metadata>
  <files>
    <file src="Tools\**\*.*" target="Tools"/>
  </files>
</package>

打包后,我们将得到一个只执行我们 Init.ps1 文件的 NuGet 包。让我们打包它并将其安装到我们自己的解决方案中。转到 PM 并执行:

NuGet pack NuGetting\NuGetting.nuspec

我们得到类似这样的内容:

Attempting to build package from 'NuGetting.nuspec'.
Successfully created package 'C:\Path\NuGetting.1.0.0.0.nupkg'.

现在我们安装新创建的包:

Install-Package NuGetting -Source 'C:\Path\'

瞧。我们得到以下输出:

Installing 'NuGetting 1.0.0.0'.
Successfully installed 'NuGetting 1.0.0.0'.
C:\Path\NuGetting.1.0.0.0
C:\Path\NuGetting.1.0.0.0\tools

如您所见,我们得到了用 Write-Host 命令写入的两行。我们需要卸载包,以便我们可以继续玩弄它,直到我们厌倦为止。

Uninstall-Package NuGetting

制作模板

让我们创建一个与我们需要的解决方案完全一样的解决方案。

Work in progress

我们将所有内容都放在那里:所有项目以及项目之间的引用。然后我们关闭解决方案并开始有趣的部分

首先清理:删除所有已编译的二进制文件 (objbin),以及您的 IDE 可能留下的任何其他东西。所有这些都是常见的垃圾。

然后我们必须编辑所有包含我们希望在应用模板时修改的信息的文件。

从解决方案文件开始

在文件的顶部,您会找到重要的行

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = 
    "Interface", "Interface\Interface.csproj", "%Interface.ProjectId%"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = 
    "Domain", "Domain\Domain.csproj", "%Domain.ProjectId%"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = 
    "Tests", "Tests\Tests.csproj", "%Tests.ProjectId%"
EndProject

以及 GlobalSection

%Interface.ProjectId%.Debug|Any CPU.ActiveCfg = Debug|Any CPU

我们已经用令牌替换了项目 ID(例如:%Interface.ProjectId%)。项目名称和路径也可以替换,但为了简单起见我们不会。我建议不要触碰左侧的 ID,不知道它们是什么。

项目

在这里我们需要更改项目 ID(使用 GUID 的大括号格式,格式说明符:“b”)、根命名空间和程序集名称

<PropertyGroup>
  <ProjectGuid>%Interface.ProjectId%</ProjectGuid>
  <RootNamespace>NuGettingSoft.%Domain.Name%</RootNamespace>
  <AssemblyName>NuGettingSoft.%Domain.Name%.Interface</AssemblyName>
</PropertyGroup>

以及引用

    <ProjectReference Include="..\Interface\Interface.csproj">
      <Project>%Interface.ProjectId%</Project>
      <Name>Interface</Name>
    </ProjectReference>

我们对所有其他项目也这样做。

代码文件

我们在这里也做同样的事情。在我们的例子中,我们只有 AssemblyInfo.cs 文件。这里我们需要设置程序集 ID(对于这些文件使用 GUID 的数字格式,格式说明符:"d"

[assembly: AssemblyTitle("NuGettingSoft.%Domain.Name%.Interface")]
[assembly: AssemblyProduct("NuGettingSoft.%Domain.Name%.Interface")]
[assembly: Guid("%Interface.AssemblyId")]

模板准备好了!!现在我们需要研究如何应用它。

核心

创建一个带有 Init.ps1 文件以应用模板的 NuGet 包。让我们详细查看脚本文件内容。

定义令牌值

$tokens = @{
    "Domain.Name" = $domainName
    "Domain.ProjectId" = Create-ProjectId
    "Interface.ProjectId" = Create-ProjectIdId
    "Tests.ProjectId" = Create-ProjectIdId
    "Domain.AssemblyId" = Create-AssemblyId
    "Interface.AssemblyId" = Create-AssemblyId
    "Tests.AssemblyId" = Create-AssemblyId
}

这些 Create-ProjectIdIdCreate-AssemblyId 只是生成一个 GUID 并按我之前说的格式化,无论是 "b" 还是 "d"

应用所有模板

对于模板文件夹中的所有文件,为此我们需要知道脚本从哪里执行。还记得 $toolsPath 参数吗?让我们抓住它并使用它

Apply-All-Templates "$toolsPath\.." .

在这里,我们只是要求应用所有模板并将结果放入解决方案文件夹中。

Function Apply-All-Templates($sourcePath, $destinationPath) {
    New-Item $sourcePath\Output -type Directory
    Get-ChildItem $sourcePath\Template -Recurse `
    | Where { -not $_.PSIsContainer } `
    | ForEach-Object {
        $source = $_.FullName
        $destination = $source -replace "\\Template\\", "\Output\" 
        Apply-Template $source $destination $tokens
    }
    Robocopy $sourcePath\Output $destinationPath * /S
    Remove-Item $sourcePath\Output -Recurse    
}

我真心希望我做得很好,并且之前的代码不需要太多解释。这个想法是遍历所有文件,并在每个文件上调用 Apply-Template,将结果放入一个临时文件夹中。稍后将该文件夹内容移动到 $destinationPath 并清除所有我们的痕迹。

逐个文件

Function Apply-Template($source, $destination, $tokens) {
    New-Item $destination -Force -Type File
    (Get-Content $source) `
    | Replace-Tokens $tokens `
    | Out-File -Encoding ASCII $destination
}

很简单,逐行读取并替换所有能找到的令牌。输出是 ASCII 编码的,因为我过去曾遇到过默认编码(我猜测是 UTF-8)的问题。

替换令牌

Function Replace-Tokens($tokens) {
    Process {
        $result = $_
        $match = [regex]::Match($_, "%((?:\w|\.)*)%")
        While ($match.Success) {
            Foreach($capture in $match.Captures) {
                $token = $capture.Groups[1].Value

                If ($tokens.ContainsKey($token)) {
                    $replacement = $tokens.Get_Item($token)
                    $result = $result -replace "%$token%", $replacement
                }                   
            }
            $match = $match.NextMatch()
        }
        $result
    }
}

这个通过管道工作。它遍历 $tokens 中的所有键,对其输入进行正则表达式匹配,如果找到,则将其替换为对应的值。

将项目添加到解决方案

在所有模板都应用后,我们只需要将每个项目添加到解决方案中。由于存在依赖关系,我们需要指定顺序。现在我们只有 3 个项目,可以手动完成,但如果项目数量增加,也许可以使用自动化流程。

@("Interface", "Domain", "Tests") `
| ForEach-Object { Add-Project-To-Solution $_ }

当然

Function Add-Project-To-Solution($project) {
    $projectPath = Resolve-Path "$project\$project.csproj"
    $dte.Solution.AddFromFile($projectPath, $false)
}

这里我们使用了一个 $dte,这是一个从 PM 可用的对象。所有与 VS 的交互都可以通过它完成,甚至可以激活菜单项。在这里阅读有关 dte 的信息:http://msdn.microsoft.com/en-us/library/envdte.dte.aspx

最后但并非最不重要的一点

Uninstall-Package $package.Id

奇怪,对吧?问题是 Init.ps1 每次解决方案打开时都会执行。我们不希望模板每天都应用,这可能会擦除所有已完成的工作。此外,一旦安装,这个包就没有任何作用了。可以使用其他技术来检测模板是否已应用……甚至允许通过包更新进行更多模板化。但目前,让我们保持简单。

这里我们还使用了另一个对象 $package,它是一个 NuGet.OptimizedZipPackage,您可以在此处阅读更多信息。它有数不尽的属性和方法。您需要导航层次结构或反映已编译的 NuGet 才能获取完整列表。我发现这些非常有用:

class OptimizedZipPackage {
  string Id { get; }
  string Title { get; }
  IEnumerable<string> Authors { get; }
  IEnumerable<string> Owners { get; }
  string Summary { get; }
  string Description { get; }
}

如果包安装到了项目中,您可以获取 $project。您可以在此处找到有关它的信息。

总结

这就是大部分重型武器,还有一些其他辅助函数,但它们不值得在这里展示。我已将所有这些函数提取到一个库文件:Template-Tools.ps1 中,因为我很可能再次需要它们。这实际上会在 NuGet 打包时触发警告,说它是一个 Unrecognized PowerScript file,只需忽略它。

在我们 NuGet 打包之前,我们只需要一个 nuspec 文件

<package>
  <metadata>
    <id>NuGettingSoft.SolutionTemplate</id>
    <version>1.0.0.0</version>
    <authors>NuGetteer</authors>
    <owners>NuGetteer</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>NuGettingSoft Solution Template</description>
  </metadata>
  <files>
    <file src="Template\**\*.*" target="Tempalte"/>
    <file src="Tools\**\*.*" target="Tools"/>
  </files>
</package>

现在我们运行

NuGet pack NuGettingSoft.SolutionTemplate.nuspec

将生成的 NuGettingSoft.SolutionTemplate.nupkg 文件移动到我们组织的私有和秘密 NuGet 仓库中。我们的工作到此结束。

解决方案

我们的新 Omega 团队已被指派为我们的人力资源部门构建域。Sam,他们的一位开发人员,去创建了一个名为 HumanResources 的空解决方案。

Empty solution for Human Resources

然后他在 PM 中运行

Install-Package NuGettingSoft.SolutionTemplate

当进程结束时,他得到

Good to go

热爱自动化……你不也是吗?

结论

NuGet 包为组织提供了强大的脚手架工具,可以大大提高统一性和生产力。在此基础上稍加想象,您就可以无所不能。明天您也许就能轻松生成非常大的 Shell。

通常,权力越大,责任越大。您将部署可执行代码。请注意,安装您包的开发人员可能以管理员身份运行 VS。

参考文献

 
© . All rights reserved.