dotnet templating 訂製自己的項目模板

由於工作需要,研究了一下VS 項目模板生成的相關內容,本文做一下記錄藉助 .NET Core Template Engine創建一個加單的項目模板。

創建項目程式碼和配置文件

首先創建一個Minimal 版本的Web Api項目,基於這個項目定義自己的項目模板。Template Engine 脫離$safeprojectname$這種項目模板參數的方式,好處是模板項目本身始終是一個可以正常編譯運行的項目,可以及時發現製作模板過程中的問題。

.template.config介紹

在項目根目錄下創建.template.config文件夾,存在項目相關配置文件。文件夾內創建三個json文件

  • template.json  必須的模板配置文件,定義一些模板配置資訊以及模板生成的邏輯等
  • ide.host.json  非必須的VS介面配置文件,定義模板圖標以及配置展示參數等
  • dotnetcli.host.json 非必須的Cli配置文件,定義命令中參數的短名稱

官方文檔中給了template.json文件必須參數的說明:

除了這三個文件還可以增加多語言的支援,具體用法可以參考aspnetcore的項目模板

修改.template.config下的文件

template.json

對symbols欄位做一些說明,其他必須參數上面的圖片中有說明。這裡我定義了四個參數:Framework,UseAuthorize,SeparateRouteHandler,IsLast。Framework和IsLast只作演示,UseAuthorize,SeparateRouteHandler兩個參數是訂製模板時需要用到的參數。

Framework參數的datatype為choice類型,vs中會展示一個下拉列表。replaces的指定當前我們選擇的值替換項目中的哪個值,類似sourceName。其他欄位均為字面意思。

UseAuthorize,SeparateRouteHandler兩個類型為bool類型,UseAuthorize是否引入認證鑒權,演示程式碼中的判斷。SeparateRouteHandler演示模板的文件排除邏輯,生成項目時根據該值判斷排除多餘文件。

IsLast參數則是簡單的演示計算參數。

sources參數用來自定義輸出源,定義modifiers通過condition指定一個條件包含或者排除哪些文件,本文使用的是exclude。

primaryOutputs參數自定義輸出,這裡只定義了項目名。

postActions則是定義模板生成後的動作,支援的Action參考文檔。

更多介紹(如生成Guid,隨機數等)參考Wiki

示例文件內容:

{
  "$schema": "//json.schemastore.org/template",
  "author": "Stacking",
  "classifications": ["web", "api"],
  "identity": "stacking.web.tmpl",
  "name": "Stacking Web Api",
  "shortName": "stack.tmpl",
  "tags": {
    "language": "C#",
    "type": "project"
  },
  "sourceName": "Stacking.Web",
  "preferNameDirectory": true,
  "sources": [
    {
      "modifiers": [
        {
          "condition": "(!SeparateRouteHandler)",
          "exclude": ["DemoHandler.cs"]
        }
      ]
    }
  ],
  "symbols": {
    "Framework": {
      "type": "parameter",
      "description": "The target .net framework for the project.",
      "datatype": "choice",
      "choices": [
        {
          "choice": "net6.0",
          "description": "Target net6.0"
        },
        {
          "choice": "net5.0",
          "description": "Target net5.0"
        }
      ],
      "replaces": "net6.0",
      "defaultValue": "net6.0"
    },
    "UseAuthorize": {
      "type": "parameter",
      "datatype": "bool",
      "defaultValue": "false",
      "description": "this application add authorize."
    },
    "SeparateRouteHandler": {
      "type": "parameter",
      "datatype": "bool",
      "defaultValue": "false",
      "description": "route handler use a separate file."
    },
    "IsLast": {
      "type": "computed",
      "value": "(Framework == \"net6.0\")"
    }
  },
  "primaryOutputs": [
    {
      "path": "Stacking.Web.csproj"
    }
  ],
  "postActions": [
    {
      "description": "Restore NuGet packages required by this project.",
      "manualInstructions": [
        {
          "text": "Run 'dotnet restore'"
        }
      ],
      "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
      "continueOnError": true
    }
  ]
}

exclude指定路徑數組支援Dic/** 以及Dic/*.cs的方式

ide.host.json

該文件只有兩個簡單的定義,通過icon指定模板圖標(相對路徑),symbolInfo定義參數資訊,如vs上顯示哪些參數以及參數說明和默認值等。

示例文件內容:

{
  "$schema": "//json.schemastore.org/vs-2017.3.host",
  "icon": "WebAPI.png",
  "symbolInfo": [
    {
      "id": "Framework",
      "name": {
        "text": ".net framework"
      },
      "description": {
        "text": ".net framework for the project"
      },
      "isVisible": true,
      "defaultValue": "net6.0"
    },
    {
      "id": "UseAuthorize",
      "name": {
        "text": "the project use authorize"
      },
      "isVisible": true
    },
    {
      "id": "SeparateRouteHandler",
      "name": {
        "text": "separate route handler file"
      },
      "invertBoolean": false,
      "isVisible": true
    }
  ]
}

invertBoolean參數的值為true時會將bool類型參數的值反轉,如SeparateRouteHandler的值在指定為true時,模板的判斷邏輯程式碼中這個值會變為false。

dotnetcli.host.json

cli的json文件中只定義了一個symbolInfo參數,定義命令行方式創建模板時參數的短名稱。

示例文件內容:

{
  "$schema": "//json.schemastore.org/dotnetcli.host",
  "symbolInfo": {
    "Framework": {
      "longName": "framework"
    },
    "UseAuthorize": {
      "longName": "use-authorize",
      "shortName": "au"
    },
    "SeparateRouteHandler": {
      "longName": "separate-route-handler",
      "shortName": "separate"
    }
  }
}

自定義模板

自定義CS文件

cs文件自定義使用類似預編譯指令的方式 ,使用#if,#endif等命令自定義輸出邏輯

#if (SeparateRouteHandler)
using Stacking.Web;
#endif
#if(UseAuthorize)
using Microsoft.AspNetCore.Authentication.JwtBearer;
#endif

如果判斷流程複雜且模板文件變動較大可以參考aspnetcore官網模板實例,定義兩個獨立文件再通過rename和exclude控制模板輸出內容。

{
     "condition": "(UseMinimalAPIs && (IndividualAuth || OrganizationalAuth))",
     "rename": {
       "Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs": "Program.cs"
     },
     "exclude": [
       "Program.MinimalAPIs.WindowsOrNoAuth.cs"
     ]
}

自定義csproj文件

csproj文件使用注釋的方式,判斷參數控制邏輯

<!--#if(UseAuthorize)-->
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<!--#endif-->

自定義json文件

json文件同樣使用注釋的方式

//#if(EnableOpenAPI)
"launchUrl": "swagger",
//#else
"launchUrl": "weatherforecast",
//#endif

安裝、卸載模板

在項目根目錄下執行dotnet new -i ./ 命令安裝自定義的模板。

卸載模板命令:dotnet new -u ./  如果不是模板目錄則需要指定模板全路徑

模板發布到nuget不再說明,寫不動了。參考官網模板定義一個templates.nuspec文件,執行nuget.exe pack命令即可。

最終效果

 

示例程式碼