C#如何建立一個可快速重複使用的專案模板

乔达摩(嘿~)發表於2024-06-21

寫在前面

其實很多公司或者資深的開發都有自己快速建立專案的腳手架的,有的是魔改程式碼生成器實現,有的直接基於T4,RazorEngine等模板引擎打造;但無論如何,其最終目的其實就是搭建一個自定義專案模板(腳手架)。

今天我們聊聊:如何基於官方的cli donet new 命令建立自己的專案模板。

什麼是專案模板

我想用一個命令來說明:

dotnet new list

image-20240515170358858

到這裡大家就非常熟悉了,原來大家平時建立專案都是基於已有的模板建立的(紅圈部分大家應該不陌生);我們今天目的就是建立一個這樣的模板,並在vs新建專案時可供選擇建立專案,或者使用cli命令直接建立;

當然,還有公開模板:

https://dotnetnew.azurewebsites.net/

建立自己的模板

1、先準備好一個專案

這裡準備的專案就是平時普通的專案,後面會以這個專案為藍本建立模板;因為我最近使用Azure Function型別專案比較多,我就以Function專案為例,其他型別專案同理的;

專案結構圖:

image-20240515171904545

專案檔案結構:

D:.
│  appsettings.CI.json
│  appsettings.Development.json
│  appsettings.json
│  appsettings.Production.json
│  Dockerfile
│  Function1.cs
│  host.json
│  local.settings.json
│  MyCompany.Cutapi.FunctionTemp.csproj #這個名字後面要被替換的
│  settings.CI.yaml
│  settings.Production.yaml
│  Startup.cs
│
├─build
│      CD.yaml
│      CI.yaml
│      _deploy.yaml
│
└─deploy
    │  kustomization.yaml
    │
    ├─base
    │      deploy.yaml
    │      kustomization.yaml
    │
    ├─ci
    │      deploy.yaml
    │      kustomization.yaml
    │
    └─prod
            deploy.yaml
            kustomization.yaml

可以看到其實有很多跟構建,部署等有關的配置檔案;

Function1.cs

#模板專案的名稱空間
namespace MyCompany.Cutapi.FunctionTemp 
{
    public class Function1
    {
        private readonly Stopwatch _sw;
        private readonly IExtractSegmentService _extractSegmentService;
        private readonly ILogger<Function1> _logger;

        public Function1(IExtractSegmentService extractSegmentService, ILogger<Function1> logger)
        {
            _sw = new Stopwatch();
            _extractSegmentService = extractSegmentService;
            _logger = logger;
        }

		#模板專案的FunctionName 和一些跟佇列有關的配置,這些後面都要
        [FunctionName("function1")]
        [return: ServiceBus("cutapi-queue1-notify", Connection = "ServiceBusConnection")]
        public async Task<VideoTranscodeNotify> Run([ServiceBusTrigger("cutapi-queue1", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message
            , string messageId
            , ServiceBusMessageActions messageActions
            , Int32 deliveryCount
            , DateTime enqueuedTimeUtc
            , ILogger log
            )
        {
            _sw.Start();
            var messageBody = Encoding.UTF8.GetString(message.Body);
            log.LogInformation($"{Environment.MachineName} -> function1 begin ->{messageId}: {messageBody}");
            await messageActions.CompleteMessageAsync(message);

            var result = new VideoTranscodeNotify();
            try
            {
                //todo...
            }
            catch (Exception ex)
            {
                log.LogError(ex, $"{Environment.MachineName} -> {messageId}:function1 Exception:{ex.Message}");
            }

            _sw.Stop();
            log.LogInformation($"{Environment.MachineName} function1 Over ->{messageId} Elapsed: {_sw.Elapsed}");

            return result;
        }
    }
}

以這個檔案為例,模板專案裡很多檔案內容都可以按自定義引數被替換;當然檔名也可以替換;

2、建立配置檔案

在專案根目錄下建立配置檔案:/.template.config/template.json

結構如下:

├─.template.config
│ template.json

內容:

{
  "author": "Heiner Wang", //作者
  "classifications": [ "Azure Functions" ], //專案歸類 classifications 還會出現在“Tags”列中
  "name": "Heiner Function", //專案全名,使用者應看到的模板名稱。
  "identity": "HeinerFunction", //專案唯一id
  "shortName": "hfunc", //專案簡寫
  "tags": {
    "language": "C#",
    "type": "project"
  },
  "sourceName": "MyCompany.Cutapi.FunctionTemp", //執行模板時使用 -n 或 --name 選項提供要替換的值,不寫了話專案名稱不變
  "preferNameDirectory": true, //建立專案的目錄層級;
  "symbols": { //自定義語法
    //自定義引數,新專案名稱空間
    "Namespace": {
      "type": "parameter",
      "dataType": "text", //文字型別
      "defaultValue": "Heiner.Function",
      "replaces": "MyCompany.Cutapi.FunctionTemp" //專案裡這個值將會被替換掉
      //"fileRename": "MyCompany.Cutapi.FunctionTemp" //也可以指定替換檔名
    },
    "FunctionName": {
      "type": "parameter",
      "dataType": "text",
      "defaultValue": "function1",
      "replaces": "function1"
    },
    "QueueName": {
      "type": "parameter",
      "dataType": "text",
      "defaultValue": "cutapi-queue1",
      "replaces": "cutapi-queue1"
    },
    "EnableRedis": {
      "type": "parameter",
      "dataType": "bool", #布林型別的
      "defaultValue": "true"
    }
  }
}

更多引數請參考:https://github.com/dotnet/templating/wiki/Reference-for-template.json

程式碼段過濾

cs檔案

//EnableRedis是自定義引數
#if (EnableRedis) 
            ConnectionMultiplexer redisConnection = ConnectionMultiplexer.Connect(AppSettings.GetConnectionString("Redis"));
            builder.Services.AddSingleton<IConnectionMultiplexer>(redisConnection);
            builder.Services.AddSingleton<IDatabase>(c => redisConnection.GetDatabase());
#endif

專案檔案

<ItemGroup>
  <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
  <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.9.0" />
  <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.ServiceBus" Version="5.9.0" />
  <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.1" />
  <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
</ItemGroup>  
    
<ItemGroup Condition="'$(EnableRedis)' == 'True' ">
  <PackageReference Include="StackExchange.Redis" Version="2.6.48" />
</ItemGroup>  

檔案過濾

模板檔案加入如下配置

"symbols":{...},
"sources": [
      {
          "modifiers": [
              {
                  "condition": "(!EnableRedis)", //EnableRedis!=true
                  "exclude": [ //排除下面的檔案(這裡僅做示例),後面的模板專案當設定引數:EnableRedis==false時,下面的檔案就被過濾掉了
                    "src/MyCompany.Cutapi.FunctionTemp/Redis.cs",  
                  ]
              }
          ]
      }
    ]    

3、執行模板安裝

這一步是將根據配置檔案,將普通專案安裝成一個專案模板,理論上建立自定義模板到這步就完成了;

專案根目錄執行:

dotnet new install .
這裡命令後面的`.` 是安裝當前目錄的專案的意思;

dotnet new install D:\MyCompany.Cutapi.FunctionTemp
也可以這樣,用絕對路徑

更新模板

強制覆蓋安裝

dotnet new install . --force

先刪除再安裝

#先刪除
dotnet new uninstall .

#重新安裝
dotnet new install .

後面的.都代表在專案根目錄執行,後面不再贅述;

4、檢查安裝結果

dotnet new list

image-20240515180319820

image-20240515180417880

無論用cli還是vs 都可以看到我們專案模板了,建立模板成功;

參考

5、推送到nuget服務端(可選)

這步是可選的! 注意!很多內部模板要脫密處理後再執行推送,請勿將機密資訊推送到公網;

1、模板專案根目錄建立檔案MyCompany.Cutapi.FunctionTemp.nuspec

<?xml version="1.0"?>
<package >
<metadata>
	<id>HeinerFunction</id>
	<version>1.0.0</version>
	<authors>Heiner Wang</authors>
	<owners>Heiner Wang</owners>
	<requireLicenseAcceptance>false</requireLicenseAcceptance>
	<description>xxx 公司 Azure Function 快速模板.</description>
	<tags>dotnet-new;template</tags>
</metadata>
<files>
	<file src="**\*" target="content"/>
</files>
</package>

2、生成nuget包

在專案根目錄執行

nuget pack MyCompany.Cutapi.FunctionTemp.nuspec

生成nuget包:

HeinerFunction.1.0.0.nupkg

3、推送到服務端

nuget push HeinerFunction.1.0.0.nupkg  -Source https://api.nuget.org/v3/index.json -ApiKey YOUR_API_KEY

這步的--Source引數,如果你有搭建好自己的nuget服務端的話改成你自己的;

如何使用一個模板

模板有了,怎麼用這個就簡單了;

vs使用

在建立專案時直接選擇自定義模板

image-20240516093813918

不過這樣的話,自定義引數都是用預設值,所以我還是更推薦用命令列方式;

命令列使用(推薦)

大家做demo的時候都應該執行過這樣的命令,其實這就是使用了官方shotname為console的模板

 dotnet new console -n MyConsoleApp1

一樣,自定義模板命令為:

#預設引數
dotnet new hfunc -n MyCompany.Heiner.Test 

#指定引數
dotnet new hfunc -n MyCompany.Heiner.Test  --Namespace MyCompany.Heiner.Test --FunctionName function-live-record --QueueName cutapi-live-record --EnableRedis false

建立成功

image-20240516155502856

[參考]

https://learn.microsoft.com/zh-cn/dotnet/core/tools/custom-templates

https://cloud.tencent.com/developer/article/2319366

https://github.com/dotnet/templating/wiki/Reference-for-template.json

相關文章