容器環境下如何將NuGet包XML文件新增到Swagger

福祿網路技術團隊發表於2021-05-25

容器環境下將NuGet包XML文件新增到Swagger

在.NET Core專案開發過程中,為了實現程式碼複用,我們將可以重複使用的部分拆分成一個個小的NuGet包。這些NuGet包可以在其他系統中複用,這樣我們只需要實現系統特定的程式碼,其餘部分的就可以重用了,包括功能、文件等。使用過程中,功能複用沒有遇到任何問題,但是文件複用卻遇到了問題。我們使用SwashBuckle生成Swagger定義和Swagger UI。Swashbuckle需要XML文件,才能顯示控制器和模型的文件說明。不幸的是,Swagger中不能正常顯示NuGet包的模型文件說明。

我們可以用在專案檔案.csprojPropertyGroup中新增<GenerateDocumentationFile>true</GenerateDocumentationFile>的方式,將XML文件新增到NuGet包中,然後將XML文件手動拷貝到專案裡,作為內容輸出。Swagger中就能正常顯示NuGet包的模型文件說明了。但是這樣做,如果NuGet包版本更新,就需要重新手動拷貝XML文件,感覺不太優雅。是否有更優雅的方式呢?

Google到幾篇文章,方案大同小異,不知道可不可行。試試吧!期待可以成功!

實戰

我們涉及到兩個專案:

  • ICH.NetCore2.Test.WebApi:ASP.NET Core WebApi 主專案
  • ICH.Common:可重用的公共元件NuGet包

我們按照以下幾個步驟分佈實施 。

1、設定.csproj檔案構建NuGet包時包含XML文件

ICH.Common的專案檔案.csprojPropertyGroup中新增<GenerateDocumentationFile>true</GenerateDocumentationFile>,作用是設定.csproj檔案構建NuGet包時包含XML文件。

<PropertyGroup>
      <TargetFramework>netstandard2.0</TargetFramework>
      <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

2、定義從NuGet包資料夾的根目錄到XML文件所在位置的相對路徑

ICH.NetCore2.Test.WebApi的專案檔案.csprojPackageReference中新增<CopyToOutputDirectory>lib\netstandard2.0\*.xml</CopyToOutputDirectory>,作用是定義從NuGet包資料夾的根目錄到XML文件所在位置的相對路徑。

<ItemGroup>
      <PackageReference Include="ICH.Common" Version="$(ICHCoreFXVersion)">
             <CopyToOutputDirectory>lib\netstandard2.0\*.xml</CopyToOutputDirectory>
      </PackageReference>
</ItemGroup>
  • 在.NET Core中,我們的NuGet包位於:
    %USERPROFILE%\.nuget\packages\{PackageName}\{PackageVersion}
  • XML文件檔案位於:
    %USERPROFILE%\.nuget\packages\{PackageName}\{PackageVersion}\lib\netcoreapp2.1\{PackageName}.xml

3、設定構建後從NuGet包中複製XML文件

ICH.NetCore2.Test.WebApi的專案檔案.csproj新增<Target Name="AfterTargetsBuild" AfterTargets="Build">,作用是設定構建後從NuGet包中複製XML文件。

<Target Name="AfterTargetsBuild" AfterTargets="Build">
      <ItemGroup>
             <PackageReferenceFiles 
                    Condition="%(PackageReference.CopyToOutputDirectory) != ''" 
                    Include="$(NugetPackageRoot)\%(PackageReference.Identity)\%(PackageReference.Version)\%(PackageReference.CopyToOutputDirectory)" />
      </ItemGroup>
      <Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(OutDir)" />
</Target>

4、設定釋出後從NuGet包中複製XML文件

與上一步類似,ICH.NetCore2.Test.WebApi的專案檔案.csproj新增<Target Name="AfterTargetsPublish" AfterTargets="Publish">,作用是設定釋出後從NuGet包中複製XML文件。

<Target Name="AfterTargetsPublish" AfterTargets="Publish">
      <ItemGroup>
             <PackageReferenceFiles 
                    Condition="%(PackageReference.CopyToOutputDirectory) != ''" 
                    Include="$(NugetPackageRoot)\%(PackageReference.Identity)\%(PackageReference.Version)\%(PackageReference.CopyToOutputDirectory)" />
      </ItemGroup>
      <Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(PublishDir)" />
</Target>

5、新增XML文件到Swagger定義中

ICH.NetCore2.Test.WebApi的專案Startup設定options.IncludeXmlComments(xmlFile, true),作用是新增XML文件到Swagger定義中。

public static void ConfigureSwagger(this IServiceCollection)
{
    services.AddSwaggerGen(c =>
    {
        string[] xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
        foreach (var xmlFile in xmlFiles) options.IncludeXmlComments(xmlFile, true);
    });
}

6、測試

現在是見證奇蹟的時候了。
首先本地構建一下試試,心情忐忑!
1619667623
可以看到XML文件從NuGet包中複製到輸出目錄了,奈斯!

然後本地釋出一下試試,心情依然忐忑!
1619667701
可以看到XML文件從NuGet包中複製到輸出目錄了, 外瑞奈斯!

最後再看看Swagger效果,Swagger中也能正常顯示NuGet包的模型文件說明。

7、遇坑

就這麼簡單?有一個聲音告訴我,圖樣圖森破!
我們生產環境是採用DockerFile的CICD方式自動釋出到K8S叢集,釋出之後,在生產環境Swagger中並不能正常顯示NuGet包的模型文件說明。
Why?Why?Why?另一個聲音告訴我,不要慌,冷靜思考!

我們的Dockerfile內容如下:

FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY . .
WORKDIR /src/src/ICH.NetCore2.Test.WebApi
RUN dotnet publish ICH.NetCore2.Test.WebApi.csproj -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=build /app .
RUN ls
RUN rm -rf appsettings.Development.json
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezone
ENTRYPOINT ["dotnet", "ICH.NetCore2.Test.WebApi.dll"]

看了一下CICD日誌,發現輸出目錄並沒有ICH.Common.xml檔案。
1619667764
這就存在兩種可能:

  • 要麼快取的NuGet包中不存在XML文件
  • 要麼快取的NuGet包中存在XML文件,主專案釋出時卻沒有從NuGet包中複製XML文件。

我們一個個來測試。先看看快取的NuGet包中到底存不存在XML文件。參考本地NuGet包路徑,在Dockerfile中新增RUN ls /root/.nuget/packages/ich.common/3.0.0-ci.63367-beta/lib/netstandard2.0,列印NuGet包中的檔案。

注意:Windows和Mac/Linux的global‑packages位置不一致
具體可以參考微軟官方文件

FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY . .
WORKDIR /src/src/ICH.NetCore2.Test.WebApi
RUN dotnet publish ICH.NetCore2.Test.WebApi.csproj -c Release -o /app
RUN ls /root/.nuget/packages/ich.common/3.0.0-ci.63367-beta/lib/netstandard2.0 #列印NuGet包中的檔案

FROM base AS final
WORKDIR /app
COPY --from=build /app .
RUN ls
RUN rm -rf appsettings.Development.json
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezone
ENTRYPOINT ["dotnet", "ICH.NetCore2.Test.WebApi.dll"]

再試試!
驚,為什麼快取的NuGet包中不存在XML文件呢?不應該啊,本地測試不是好好的嗎?難道跟平臺有關係?本地是Windows,生產環境是Docker。
1619667812
Google到微軟官方文件,關注NuGet CLI environment variables中有一個環境變數NUGET_XMLDOC_MODE

  • NUGET_XMLDOC_MODE
    Determines how assemblies XML documentation file extraction should be handled.
    Supported modes are skip (do not extract XML documentation files), compress (store XML doc files as a zip archive) or none (default, treat XML doc files as regular files).
    定義如何處理程式集XML文件檔案提取, 不管怎麼樣,試試吧!
    Dockerfile中新增ENV NUGET_XMLDOC_MODE none,設定環境變數NUGET_XMLDOC_MODE值為none,也就是
    NuGet將XML文件檔案視為常規檔案。
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY . .
WORKDIR /src/src/ICH.NetCore2.Test.WebApi
ENV NUGET_XMLDOC_MODE none # 設定環境變數NUGET_XMLDOC_MODE
RUN dotnet publish ICH.NetCore2.Test.WebApi.csproj -c Release -o /app
RUN ls /root/.nuget/packages/ich.common/3.0.0-ci.63367-beta/lib/netstandard2.0 #列印NuGet包中的檔案

FROM base AS final
WORKDIR /app
COPY --from=build /app .
RUN ls
RUN rm -rf appsettings.Development.json
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezone
ENTRYPOINT ["dotnet", "ICH.NetCore2.Test.WebApi.dll"]

再試試!
奈斯,可以看到ICH.Common.xml檔案靜靜的躺在那!
1619667846
再看看輸出目錄,居然...居然...沒有ICH.Common.xml檔案。
1619667893

靜靜思考,會不會是第4步設定釋出後從NuGet包中複製XML文件的路徑問題。

修改ICH.NetCore2.Test.WebApi的專案檔案.csproj

<Target Name="AfterTargetsPublish" AfterTargets="Publish">
      <ItemGroup>
             <PackageReferenceFiles 
                    Condition="%(PackageReference.CopyToOutputDirectory) != ''" 
                    Include="/root/.nuget/packages/ich.common/3.0.0-ci.63367-beta/lib/netstandard2.0/*.xml" />
      </ItemGroup>
      <Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(PublishDir)" />
</Target>

再試試!輸出目錄可以看到ICH.Common.xml檔案了。
1619668003
果然是路徑問題,也就是說$(NugetPackageRoot)\%(PackageReference.Identity)\%(PackageReference.Version)不等於/root/.nuget/packages/ich.common/3.0.0-ci.63367-beta
為什麼,難道是大小寫的問題?

修改ICH.NetCore2.Test.WebApi的專案檔案.csproj,將PackageReference.Identity轉為小寫。

<Target Name="AfterTargetsPublish" AfterTargets="Publish">
      <ItemGroup>
            <PackageReferenceFiles 
                    Condition="%(PackageReference.CopyToOutputDirectory) != ''" 
                    Include="$(NugetPackageRoot)\$([MSBuild]::Escape('%(PackageReference.Identity)').ToLower())\%(PackageReference.Version)\lib\%(PackageReference.CopyToOutputDirectory)\*.xml" />
      </ItemGroup>
      <Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(PublishDir)" />
</Target>

再試試!輸出目錄可以看到ICH.Common.xml檔案了。
1619668003
最後再看看Swagger效果,Swagger中也能正常顯示NuGet包的模型文件說明。完美!

總結

填坑的過程是曲折的,收穫是頗豐的,結局是圓滿的。
最後貼出完整的ICH.NetCore2.Test.WebApi的專案檔案.csprojDockerfile

<Project Sdk="Microsoft.NET.Sdk.Web">
	<PropertyGroup>
		<TargetFramework>netcoreapp2.1</TargetFramework>
		<GenerateDocumentationFile>true</GenerateDocumentationFile>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="ICH.Common" Version="$(ICHCoreFXVersion)">
			<CopyToOutputDirectory>lib\netstandard2.0\*.xml</CopyToOutputDirectory>
		</PackageReference>
	</ItemGroup>

	<Target Name="AfterTargetsBuild" AfterTargets="Build">
		<ItemGroup>
			<PackageReferenceFiles 
                Condition="%(PackageReference.CopyToOutputDirectory) != ''" 
                Include="$(NugetPackageRoot)\$([MSBuild]::Escape('%(PackageReference.Identity)').ToLower())\%(PackageReference.Version)\%(PackageReference.CopyToOutputDirectory) />
		</ItemGroup>
		<Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(OutDir)" />
	</Target>

	<Target Name="AfterTargetsPublish" AfterTargets="Publish">
		<ItemGroup>
			<PackageReferenceFiles 
                Condition="%(PackageReference.CopyToOutputDirectory) != ''"         
                Include="$(NugetPackageRoot)\$([MSBuild]::Escape('%(PackageReference.Identity)').ToLower())\%(PackageReference.Version)\%(PackageReference.CopyToOutputDirectory)" />
		</ItemGroup>
		<Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(PublishDir)" />
	</Target>
</Project>
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY . .
WORKDIR /src/src/ICH.NetCore2.Test.WebApi
ENV NUGET_XMLDOC_MODE none # 設定環境變數NUGET_XMLDOC_MODE
RUN dotnet publish ICH.NetCore2.Test.WebApi.csproj -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=build /app .
RUN ls
RUN rm -rf appsettings.Development.json
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezone
ENTRYPOINT ["dotnet", "ICH.NetCore2.Test.WebApi.dll"]

最後

感謝幾篇文章的作者給與我的啟發,同時也希望這篇文章可以幫助大家解決類似的問題。

福祿ICH·架構組 福小皮

相關文章