Photo taken by Vance Osterhout

Scenario

  1. You provide a nuget package to x amount of different customers.
  2. You want to share common code between the different nuget packages.
  3. You don't want your shared code to be a nuget package on it's own.
  4. You've the following project structure:

the-project-structure

On your build server you have separate pipelines for the different clients where you run the following code:

dotnet pack JOS.MyNugetPackage.Client1 -c Release

Everything builds just fine and you ship the package to your client and call it a day, great.

There's a catch though...the client can't install the nuget package.
When they try to install the package they get the following error:

Restoring packages for C:\utv\projects\JOS.DotnetPackIncludeBaseLibrary\src\Client1App\Client1App.csproj...
  GET https://api.nuget.org/v3-flatcontainer/jos.mynugetpackage.core/index.json
  NotFound https://api.nuget.org/v3-flatcontainer/jos.mynugetpackage.core/index.json 658ms
Installing JOS.MyNugetPackage.Client1 1.0.0.
NU1101: Unable to find package JOS.MyNugetPackage.Core. No packages exist with this id in source(s): Dotnet Roslyn, Local feed, Microsoft Visual Studio Offline Packages, nuget.org
Package restore failed. Rolling back package changes for 'Client1App'.
Time Elapsed: 00:00:01.5727356

That's weird? It can't find the JOS.MyNugetPackage.Core nuget package?
And that's the problem right there. When running dotnet pack and having references to other projects, the following nuspec file will be produced:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  <metadata>
    <id>JOS.MyNugetPackage.Client1</id>
    <version>1.0.0</version>
    <authors>JOS.MyNugetPackage.Client1</authors>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Package Description</description>
    <dependencies>
      <group targetFramework=".NETStandard2.0">
        <dependency id="JOS.MyNugetPackage.Core" version="1.0.0" exclude="Build,Analyzers" />
      </group>
    </dependencies>
  </metadata>
</package>

We have a dependency on JOS.MyNugetPackage.Core, as expected right? Well, the problem here is that a dependency in a nuspec file == a nuget package.

The dependencies element within metadata contains any number of dependency elements that identify other packages upon which the top-level package depends.

And since a JOS.MyNugetPackage.Core nuget package doesn't exists the restore fails.

This issue is currently discussed in a couple of different GitHub issues and it seems like many people want this feature.

I encourage you to read the comments in both issues, they contain many great tips and insights.

The first ticket has been open for almost 4 years (!!!) so I don't expect a solution to this problem anytime soon.

Workarounds

It's important to realize that this is workarounds, both solutions have their own pros and cons.

There's a couple of workarounds linked in the GitHub issues, let's try them out.

"The csproj approach"

This workaround was posted in the comments by "teroenko". So let's update the JOS.MyNugetPackage.Client1 csproj.

Note: I'll be honest here, without the comments provided I would've been somewhat lost. Custom build targets and stuff like that in MSBuild has always been (more or less) a black box for me.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\JOS.MyNugetPackage.Core\JOS.MyNugetPackage.Core.csproj" PrivateAssets="All" />
  </ItemGroup>

  <PropertyGroup>
    <TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
  </PropertyGroup>

  <Target Name="CopyProjectReferencesToPackage" DependsOnTargets="BuildOnlySettings;ResolveReferences">
    <ItemGroup>
      <!-- Filter out unnecessary files -->
      <_ReferenceCopyLocalPaths Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'All'))"/>
    </ItemGroup>

    <!-- Print batches for debug purposes -->
    <Message Text="Batch for .nupkg: ReferenceCopyLocalPaths = @(_ReferenceCopyLocalPaths), ReferenceCopyLocalPaths.DestinationSubDirectory = %(_ReferenceCopyLocalPaths.DestinationSubDirectory) Filename = %(_ReferenceCopyLocalPaths.Filename) Extension = %(_ReferenceCopyLocalPaths.Extension)" Importance="High" Condition="'@(_ReferenceCopyLocalPaths)' != ''" />

    <ItemGroup>
      <!-- Add file to package with consideration of sub folder. If empty, the root folder is chosen. -->
      <BuildOutputInPackage Include="@(_ReferenceCopyLocalPaths)" TargetPath="%(_ReferenceCopyLocalPaths.DestinationSubDirectory)"/>
    </ItemGroup>
  </Target>
</Project>

It's important that you add the PrivateAssets="All" to your project reference since that's what we filter on in the CopyProjectReferencesToPackage step above.

<ProjectReference Include="..\JOS.MyNugetPackage.Core\JOS.MyNugetPackage.Core.csproj" PrivateAssets="All" /> 

If we now try to publish our nuget package again the produced nuspec will look like this:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <id>JOS.MyNugetPackage.Client1</id>
    <version>1.3.0</version>
    <authors>JOS.MyNugetPackage.Client1</authors>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Package Description</description>
    <dependencies>
      <group targetFramework=".NETStandard2.0" />
    </dependencies>
  </metadata>
</package>  

Now we don't have any dependency to JOS.MyNugetPackage.Core and if we check the lib folder in the npkg file, we can see that the JOS.MyNugetPackage.Core.dll file is included.
csproj-lib-output

Client1 can now install the package and use it.
Example

namespace Client1App
{    
    class Program
    {
        static void Main(string[] args)
        {
            var companyQuery = new GetCompanyNameQuery();
            var result = companyQuery.Execute();
            Console.WriteLine(result);
        }
    }
}

Outputs...

This is Client 1, brought to you by My Awesome Company

Pros

It works...

Cons

You need to edit your csproj and add PrivateAssets=All to your references. This can mess up references in your test projects for example.

"The nuspec approach"

Let's get Client 2 up and running. They don't want to update their csproj file. By creating the following nuspec file we are basically saying "copy all the dll files starting with JOS.MyNugetPackage. in the bin\Release\netstandard2.0\ folder to the lib\netstandard2.0 folder in the nuget package.

<?xml version="1.0"?>
<package >
  <metadata>
    <id>JOS.MyNugetPackage.Client2</id>
    <version>1.4.0</version>
    <title>$title$</title>
    <authors>Josef Ottosson</authors>
    <owners>Josef Ottosson</owners>
    <description>Really awesome package</description>
    <dependencies>
      <group targetFramework="netstandard2.0" />
    </dependencies>
  </metadata>
   <files>
     <file src="bin\Release\netstandard2.0\JOS.MyNugetPackage.*.dll" target="lib\netstandard2.0" />
  </files>
</package>

If we then use dotnet pack like this...

dotnet pack -c Release -p:NuspecFile=JOS.MyNugetPackage.Client2.nuspec

...the following nuspec will be produced:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <id>JOS.MyNugetPackage.Client2</id>
    <version>1.4.0</version>
    <title></title>
    <authors>Josef Ottosson</authors>
    <owners>Josef Ottosson</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Really awesome package</description>
    <dependencies>
      <group targetFramework=".NETStandard2.0" />
    </dependencies>
  </metadata>
</package> 

Again, no references to the JOS.MyNugetPackage.Core package anywhere.
And if we look in the lib folder, it contains both the JOS.MyNugetPackage.Core.dll and JOS.MyNugetPackage.Client2.dll.
csproj-lib-output-client2

Pros

You don't need to add PrivateAssets=All to your csproj.

Cons

If you start targeting more frameworks/rename stuff, you need to update the paths etc.

Final thoughts

To me, the nuspec solution feels cleaner. It's important to note though that you need to keep it up to date regarding target framework, paths etc.
IMO this is something that should work out of the box but it seems like Microsoft wants everything to be a (public) nuget package.

What if you actually created the JOS.MyNugetPackage.Core package and published it to your internal company nuget feed?
Your client would still not be able to restore the package since they can't access your internal feed...
Should you in that case ship both packages? And setup a new pipeline only for your Core project? Nah, doesn't make much sense to me :)

I've created a working example that contains the two different approaches that you can find here