容器環境下如何將NuGet包XML文檔添加到Swagger
容器環境下將NuGet包XML文檔添加到Swagger
在.NET Core項目開發過程中,為了實現程式碼復用,我們將可以重複使用的部分拆分成一個個小的NuGet包。這些NuGet包可以在其他系統中復用,這樣我們只需要實現系統特定的程式碼,其餘部分的就可以重用了,包括功能、文檔等。使用過程中,功能復用沒有遇到任何問題,但是文檔復用卻遇到了問題。我們使用SwashBuckle生成Swagger定義和Swagger UI。Swashbuckle需要XML文檔,才能顯示控制器和模型的文檔說明。不幸的是,Swagger中不能正常顯示NuGet包的模型文檔說明。
我們可以用在項目文件.csproj的PropertyGroup中添加<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的項目文件.csproj的PropertyGroup中添加<GenerateDocumentationFile>true</GenerateDocumentationFile>,作用是設置.csproj文件構建NuGet包時包含XML文檔。
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
2、定義從NuGet包文件夾的根目錄到XML文檔所在位置的相對路徑
ICH.NetCore2.Test.WebApi的項目文件.csproj的PackageReference中添加<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、測試
現在是見證奇蹟的時候了。
首先本地構建一下試試,心情忐忑!

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

可以看到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文件。

這就存在兩種可能:
- 要麼快取的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。

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文件靜靜的躺在那!

再看看輸出目錄,居然...居然...沒有ICH.Common.xml文件。

靜靜思考,會不會是第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文件了。

果然是路徑問題,也就是說$(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文件了。

最後再看看Swagger效果,Swagger中也能正常顯示NuGet包的模型文檔說明。完美!
總結
填坑的過程是曲折的,收穫是頗豐的,結局是圓滿的。
最後貼出完整的ICH.NetCore2.Test.WebApi的項目文件.csproj和Dockerfile
<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"]
最後
感謝幾篇文章的作者給與我的啟發,同時也希望這篇文章可以幫助大家解決類似的問題。
