dotnet / sdk

Core functionality needed to create .NET Core projects, that is shared between Visual Studio and CLI
https://dot.net/core
MIT License
2.67k stars 1.06k forks source link

3rd party XML documentation files in NuGet packages are not copied to my project's build folder #9498

Open fizxmike opened 6 years ago

fizxmike commented 6 years ago

I've dug around for a solution to this problem, but I only find issues related to dotnet publish not including a project's xml documentation. My issue is that XML documentation files in NuGet packages are not copied to my project's build folder. I hope the distinction is clear to a human (google couldn't get it).

Steps to reproduce

Add some_other_library nuget package which includes xml documentation to you project. Do dotnet build

Expected behavior

The some_other_library.xml is included in the build (output) folder like so: image (screenshot from output of VS 2017)

Actual behavior

some_other_library.xml file is not present in the build (output) folder: image (screenshot of output of dotnet cli)

Environment data

dotnet --info output:

.NET Command Line Tools (2.1.101)

Product Information:
 Version:            2.1.101
 Commit SHA-1 hash:  6c22303bf0

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.14393
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\2.1.101\

Microsoft .NET Core Shared Framework Host

  Version  : 2.0.6
  Build    : 74b1c703813c8910df5b96f304b0f2b78cdf194d
livarcocc commented 6 years ago

@wli3 you were looking at something right this. Can you comment?

wli3 commented 6 years ago

give me some time i need to find the related issue

wli3 commented 6 years ago

Sorry for the delay. It has similar root cause with https://github.com/dotnet/sdk/issues/1458

wli3 commented 6 years ago

@fizxmike there are some work around in dotnet/sdk#1458 a solution is still work in progress. cc @nguerrera

MonDeveloper commented 5 years ago

This limitation is really painfull when the xml documentation are the one describing the model packed with NuGET and used on the WebAPI contracts where something like swagger is used to self document the WebApi themselves, in such a case there's no possibility to auto-describe the contracts (since it is autodiscovered using the xml documentations)

wuchang commented 5 years ago

怎么解决?

sergiodank commented 4 years ago

The following solution helped me. Add it in you .csproj file for project which is building:

<ItemGroup>
    <PackageReference Include="{PackageName}" Version="{PackageVersion}" GeneratePathProperty="true"/>
</ItemGroup>

<Target Name="CopyXMLFromPackages" AfterTargets="Build">
    <Copy SourceFiles="$(PkgPackage_Name)\lib\netcoreapp3.1\{PackageName}.xml" DestinationFolder="$(OutDir)" />
</Target>
Barsonax commented 4 years ago

Any timeframe when this is getting fixed? Its already been 2 years ago this issue was opened and is a breaking change compared to the old system.

I really want my csproj to just copy all documentation xml files and pdb files to the output directory just like it used to do.

The workarounds that were mentioned do not work. Tried to do this with msbuild but failed since it only includes 1 of the xml files...:

<PackageReferenceFiles Include="$(NugetPackageRoot)\%(PackageReference.FileName)\%(PackageReference.Version)\**\*.xml" />
blacksnake-rus commented 4 years ago

Any timeframe when this is getting fixed? Its already been 2 years ago this issue was opened and is a breaking change compared to the old system.

Try this:

<ItemGroup>
    <PackageReference Include="MyNuget" Version="0.0.2526">
        <CopyToOutputDirectory>lib/netcoreapp3.1/*.xml</CopyToOutputDirectory>
    </PackageReference>
</ItemGroup>

<Target Name="CopyXMLFromPackagesForBuild" 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="CopyXMLFromPackagesForPublish" BeforeTargets="PrepareForPublish">
    <ItemGroup>
        <PackageReferenceFiles Condition="%(PackageReference.CopyToOutputDirectory) != ''" Include="$(NugetPackageRoot)$([MSBuild]::Escape('%(PackageReference.Identity)').ToLower())/%(PackageReference.Version)/%(PackageReference.CopyToOutputDirectory)" />
    </ItemGroup>
    <Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(PublishDir)" />
</Target>
Barsonax commented 4 years ago

That means I have to do that for every nuget package and need to know if said nuget package has a netcoreapp3.1 folder... This means my customers (other developers) cannot use the package manager and have to manually edit the csproj for packages they add. Not a very good solution in my case.

To give a bit more context this is for a game engine, currently we have a integrated nuget (2.14) package manager that lets you install plugins (which are nuget packages) but in order to support the newer netstandard and netcore we want to upgrade nuget. Since the newer nuget contains many breaking changes that make this hard to do we are now looking how far we could get if we just used the default tooling (thus having all dependencies in a csproj).

JoFrMueller commented 4 years ago

Also this approach doesn't work on Linux or docker containers.

Any news on this? Does anyone know how to transfer the csproj hack to support Linux or docker environments as well?

What we're currently doing is basically running a script during deployment to extract the NuGet XML content and put it next to the publish output. This solution is !!!terrible!!! but we don't have another one for now...

Barsonax commented 4 years ago

@JoFrMueller you can do this to copy the xml and/or pdb files to the build output if it exists:

  <Target Name="CopyReferenceFiles" BeforeTargets="Build">
    <ItemGroup>
      <ReferenceFiles Include="%(Reference.RelativeDir)%(Reference.Filename).xml;%(Reference.RelativeDir)%(Reference.Filename).pdb" />
    </ItemGroup>

    <Message Text="Copying reference files to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(ReferenceFiles)" DestinationFolder="$(OutputPath)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
  </Target>

Also see my blog about this https://the-photographing-programmer.com/packagereferences-and-xml-documentation/

Granted it would be nice if there was build in support for this common use case but this seems to work pretty well and takes into account transitive dependencies as well and doesn't lock your project into a specific targetframework.

We use this in our new project template for our game engine https://github.com/Barsonax/DualityProjectTemplate/blob/master/Source/Duality/Directory.Build.props#L12

AntoCanza commented 4 years ago

@Barsonax @JoFrMueller the solution can do the trick!

just for sharing infos, into build stage i have set: ENV NUGET_XMLDOC_MODE=none into linux image the dotnet restore wasn't able to pick up all xml files.

Logikoz commented 3 years ago

Any timeframe when this is getting fixed? Its already been 2 years ago this issue was opened and is a breaking change compared to the old system.

Try this:

<ItemGroup>
  <PackageReference Include="MyNuget" Version="0.0.2526">
      <CopyToOutputDirectory>lib/netcoreapp3.1/*.xml</CopyToOutputDirectory>
  </PackageReference>
</ItemGroup>

<Target Name="CopyXMLFromPackagesForBuild" 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="CopyXMLFromPackagesForPublish" BeforeTargets="PrepareForPublish">
  <ItemGroup>
      <PackageReferenceFiles Condition="%(PackageReference.CopyToOutputDirectory) != ''" Include="$(NugetPackageRoot)$([MSBuild]::Escape('%(PackageReference.Identity)').ToLower())/%(PackageReference.Version)/%(PackageReference.CopyToOutputDirectory)" />
  </ItemGroup>
  <Copy SourceFiles="@(PackageReferenceFiles)" DestinationFolder="$(PublishDir)" />
</Target>

it worked for me! ty ❤️

LGinC commented 2 years ago

@JoFrMueller you can do this to copy the xml and/or pdb files to the build output if it exists:

  <Target Name="CopyReferenceFiles" BeforeTargets="Build">
    <ItemGroup>
      <ReferenceFiles Include="%(Reference.RelativeDir)%(Reference.Filename).xml;%(Reference.RelativeDir)%(Reference.Filename).pdb" />
    </ItemGroup>

    <Message Text="Copying reference files to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(ReferenceFiles)" DestinationFolder="$(OutputPath)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
  </Target>

Also see my blog about this https://the-photographing-programmer.com/packagereferences-and-xml-documentation/

Granted it would be nice if there was build in support for this common use case but this seems to work pretty well and takes into account transitive dependencies as well and doesn't lock your project into a specific targetframework.

We use this in our new project template for our game engine https://github.com/Barsonax/DualityProjectTemplate/blob/master/Source/Duality/Directory.Build.props#L12

how to filter xml? I just want to copy file start with xxx (xxx.*.xml), like xxx.Application.xml

nkoudelia commented 2 years ago

@LGinC like this, just replace the placeholders:

  <Target Name="CopyXmlDocs" BeforeTargets="Build">
    <ItemGroup>
      <XmlDocs Include="%(Reference.RelativeDir)your_filename_1.xml" />
      <XmlDocs Include="%(Reference.RelativeDir)*.your_partial_filename_2.xml" />
    </ItemGroup>
    <Message Text="Copying XML docs to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(XmlDocs)" DestinationFolder="$(OutputPath)" Condition="Exists(%(FullPath))" />
  </Target>
LGinC commented 2 years ago

@LGinC like this, just replace the placeholders:

  <Target Name="CopyXmlDocs" BeforeTargets="Build">
    <ItemGroup>
      <XmlDocs Include="%(Reference.RelativeDir)your_filename_1.xml" />
      <XmlDocs Include="%(Reference.RelativeDir)*.your_partial_filename_2.xml" />
    </ItemGroup>
    <Message Text="Copying XML docs to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(XmlDocs)" DestinationFolder="$(OutputPath)" Condition="Exists(%(FullPath))" />
  </Target>

dotnet publish -c Release not copy xml to /bin/Release/net5.0/publish folder, just copy to /bin/Release/net5.0 folder. change $(OutputPath) to $(PublishDir), and it work @nkoudelia thx

<Target Name="CopyXmlDocs" BeforeTargets="Build">
<ItemGroup>
<XmlDocs Include="%(Reference.RelativeDir)My.*.xml" />
</ItemGroup>
<Message Text="Copying XML docs to $(OutputPath)" Importance="High" />
<Copy SourceFiles="@(XmlDocs)" DestinationFolder="$(PublishDir)" Condition="Exists(%(FullPath))" />
</Target>

copy My.*.xml to publish folder

lichti81 commented 2 years ago

In addition to already promoted workarounds: A generic copy for all xml files for all assemblies in build directory

  <Target Name="CopyReferenceFiles" BeforeTargets="Build">
    <ItemGroup>
      <XmlReferenceFiles Condition="Exists('$(OutputPath)%(Filename).dll')" Include="%(Reference.RelativeDir)%(Reference.Filename).xml" />
    </ItemGroup>

    <Message Text="Copying reference files to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(OutputPath)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
  </Target>
MofaggolHoshen commented 2 years ago

More or less all workaround I've tried nothing worked in the Azure Pipeline. Is there any other workaround that works in the Azure Pipeline?

MofaggolHoshen commented 2 years ago

More or less all workaround I've tried nothing worked in the Azure Pipeline. Is there any other workaround that works in the Azure Pipeline?

I've used CopyFile (CopyFiles@2) task

batuhankara commented 2 years ago

In addition to already promoted workarounds: A generic copy for all xml files for all assemblies in build directory

  <Target Name="CopyReferenceFiles" BeforeTargets="Build">
    <ItemGroup>
      <XmlReferenceFiles Condition="Exists('$(OutputPath)%(Filename).dll')" Include="%(Reference.RelativeDir)%(Reference.Filename).xml" />
    </ItemGroup>

    <Message Text="Copying reference files to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(OutputPath)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
  </Target>

I am bad at ms build, so I just want to ask you, It is worked like charm but it copies from ALL DLLS , what I need just get xml where xml name contains "magic string" is it possible?

baronfel commented 2 years ago

It is possible, in this case you would add more logic to the Condition on the XmlReferenceFiles element on the third line. Something like Condition="Exists('$(OutputPath)%(Filename).dll') and %(Filename.Contains('magic string'))" would get you I believe.

KalleOlaviNiemitalo commented 2 years ago

No, you cannot use String instance methods directly on metadata like %(Filename.Contains('magic string')); https://github.com/dotnet/msbuild/issues/1155 is still open.

Instead, use $([System.String]::Copy('%(Filename)').Contains('magic string')) as in https://github.com/dotnet/msbuild/issues/4615.

batuhankara commented 2 years ago

No, you cannot use String instance methods directly on metadata like %(Filename.Contains('magic string')); dotnet/msbuild#1155 is still open.

Instead, use $([System.String]::Copy('%(Filename)').Contains('magic string')) as in dotnet/msbuild#4615.

it worked like charm thanks.

FalconWu2017 commented 1 year ago

Make records:

<Target Name="CopyReferenceFiles" BeforeTargets="Build">
    <ItemGroup>
        <XmlReferenceFiles Condition="Exists('$(OutputPath)%(Filename).dll')" Include="%(Reference.RelativeDir)%(Reference.Filename).xml" />
    </ItemGroup>
    <Message Text="Copying reference files to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(OutputPath)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
</Target>
<Target Name="CopyReferenceFilesToPublish" BeforeTargets="PrepareForPublish">
    <ItemGroup>
        <XmlReferenceFiles Condition="Exists('$(OutputPath)%(Filename).dll')" Include="%(Reference.RelativeDir)%(Reference.Filename).xml" />
    </ItemGroup>
    <Message Text="Copying reference files to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(PublishDir)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
</Target>

So far, everything seems to be working normally。

PontusMagnusson commented 1 year ago

@lichti81

In addition to already promoted workarounds: A generic copy for all xml files for all assemblies in build directory

  <Target Name="CopyReferenceFiles" BeforeTargets="Build">
    <ItemGroup>
      <XmlReferenceFiles Condition="Exists('$(OutputPath)%(Filename).dll')" Include="%(Reference.RelativeDir)%(Reference.Filename).xml" />
    </ItemGroup>

    <Message Text="Copying reference files to $(OutputPath)" Importance="High" />
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(OutputPath)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
  </Target>

Thank you so much.

The fact that this is not default behaviour is beyond me.

helluvamatt commented 1 year ago

Here's an easier way for folks still on .NET 6 SDK:

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Asp.Versioning.Mvc" Version="6.4.0" />
        <PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="6.4.0" />
        <PackageReference Include="Hellang.Middleware.ProblemDetails" Version="6.5.1">
            <!-- Only references with this metadata will be included -->
            <CopyToOutputDirectory>xmldoc</CopyToOutputDirectory>
        </PackageReference>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.8" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0">
            <CopyToOutputDirectory>xmldoc</CopyToOutputDirectory>
        </PackageReference>
        <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.4.0" />
    </ItemGroup>

    <!-- This target adds our XML files to the @(ReferenceCopyLocalPaths) list.    -->
    <!-- It uses MSBuild's batching by executing the target once per entry in      -->
    <!-- @(PackageReferenceWithCopyToOutputDirectory) thanks to the "Inputs" and   -->
    <!-- "Outputs" attrs. We also assume the XML doc has the same name as the DLL. -->                                            
    <Target Name="_ResolveReferenceCopyLocalPaths" AfterTargets="ResolveReferences" DependsOnTargets="_ResolveCopyToOutputDirectory" Inputs="@(PackageReferenceWithCopyToOutputDirectory)" Outputs="%(PackageReferenceWithCopyToOutputDirectory.Identity)">
        <PropertyGroup>
            <PackageIdentity>%(PackageReferenceWithCopyToOutputDirectory.Identity)</PackageIdentity>
        </PropertyGroup>
        <ItemGroup>
            <ReferenceCopyLocalPaths Include="@(ReferenceCopyLocalPaths->'%(RootDir)%(Directory)%(Filename).xml')" Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' == '$(PackageIdentity)' and Exists('%(RootDir)%(Directory)%(Filename).xml')" />
        </ItemGroup>
    </Target>

    <!-- This target filters all <PackageReference> entries to those with the given condition -->
    <Target Name="_ResolveCopyToOutputDirectory">
        <ItemGroup>
            <PackageReferenceWithCopyToOutputDirectory Include="@(PackageReference)" Condition="'%(PackageReference.CopyToOutputDirectory)' == 'xmldoc'" />
        </ItemGroup>
    </Target>

</Project>

Seems to work well with Build and Publish, one or more packages to copy, etc...

lmandras-iura commented 5 months ago

This has worked for me (slightly tweaked from examples above) for a Docker environment in Azure pipelines:

<Target Name="CopyReferenceFiles" BeforeTargets="Build">
    <ItemGroup>

      <XmlReferenceFiles Condition="Exists('$(OutputPath)%(Filename).dll')" Include="%(Reference.RelativeDir)%(Reference.Filename).xml" />

    </ItemGroup>

    <Message Text="Found XML doc for copy: %(XmlReferenceFiles.FullPath)" Importance="normal"/>

    <!--    In local build need to copy to output folder, ex: bin/debug/net8.0/-->
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(OutputPath)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)') AND $([System.String]::Copy('%(Filename)').StartsWith('<my.assembly.name>'))"/>

    <!--    In Azure CI/CD pipeline need to copy to final publish dir-->
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(PublishDir)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)') AND $([System.String]::Copy('%(Filename)').StartsWith('<my.assembly.name>'))"/>

</Target>

It extended condition will make sure to copy only the file i need.

Also a super important piece to have in the docker file before dotnet restore ENV NUGET_XMLDOC_MODE=none

By default that is set to 'skip' in a docker environment to skip xml files from the nuget package to save time/space when unpacking

juarola commented 5 months ago

This has worked for me (slightly tweaked from examples above) for a Docker environment in Azure pipelines: ... By default that is set to 'skip' in a docker environment to skip xml files from the nuget package to save time/space when unpacking

Works like a charm, thank you! And yep, that env var is critical.

spaasis commented 1 month ago

And here's an implementation for defining multiple assemblies to copy docs for:

<PropertyGroup>
      <!-- Define the external assemblies (; separated) that you want to include in your openapi specs -->
      <AssemblyNames>MyAssembly;OtherAssembly</AssemblyNames>
    </PropertyGroup>

    <ItemGroup>
      <XmlReferenceFiles
        Condition="Exists('$(OutputPath)%(Filename).dll') AND $(AssemblyNames.Contains(%(Filename)))"
        Include="%(Reference.RelativeDir)%(Reference.Filename).xml"/>
    </ItemGroup>

        <Message Text="Copy XML files for external dependencies to $(OutputPath)" Importance="High"/>
    <!--    In local build need to copy to output folder, ex: bin/debug/net8.0/-->
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(OutputPath)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')"/>

    <!--    In Azure CI/CD pipeline need to copy to final publish dir-->
    <Copy SourceFiles="@(XmlReferenceFiles)" DestinationFolder="$(PublishDir)" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')"/>