Creating source-only NuGet packages : Andrew Lock

Creating source-only NuGet packages
by: Andrew Lock
blow post content copied from  Andrew Lock | .NET Escapades
click here to view original post


In this post I describe how to create a source-only NuGet package. I discuss what source-only NuGet packages are, why you might want to create one, and how to build a project as a source-only package.

What are source-only NuGet packages?

NuGet packages are the standard packaging mechanism for .NET. NuGet packages typically contain one or more dlls, MSBuild-related files (_.props, .targets), as well as various other files. Each NuGet package is just a .zip file, with files stored in specific well-defined folders that the .NET CLI/Visual Studio/MSBuild use to build your app.

If you download a NuGet file you can easily "see" inside the file by renaming it from .nupkg to .zip and opening the file as you would a normal zip file. Alternatively, open the file using the excellent NuGet Package Explorer!

Source-only packages aren't an "official" thing. Rather they're a name sometimes used to describe NuGet packages that don't contain any pre-compiled dll dependencies, but rather include one or more source code files (typically .cs files) instead. Instead of depending on a dll, a project referencing the package includes the contained source code files when you build the project, directly compiling the source-only package into your own code.

Why might you want to create a source-only package?

In most cases, when you're referencing NuGet dependencies, you want a compiled binary. You don't want (or need) to be able to compile your dependencies. This is the standard use case for NuGet dependencies.

Similarly, by using pre-compiled dependencies, you can exchange data with other dependencies. For example, maybe your app and a logging library both reference System.Text.Json; that means your app and the library can easily communicate between each other using types that are defined in the System.Text.Json assembly. Without a shared, pre-compiled dependency, this would not really be possible.

One of the main use-cases for source-only packages is when you don't want that dependency. By design, NuGet/MsBuild only allows a single version of a dependency to be referenced in the complete package graph of your application. If multiple libraries in your app depend on different versions of a given dependency you can run into dependency resolution problems. Using a source-only package side-steps that by compiling a specific version of the dependency into each library.

A classic example of this was the LibLog package. In the days before Microsoft.Extensions.Logging and .NET Core, this was a way for libraries to write logs to whatever logging framework the app used but without taking any hard dependencies on logging frameworks. The design as a source only package meant you could avoid any versioning issues.

Another, more recent, example of where source-only packages shine is for "lighting up" new C# features. Many features in recent versions of C# require specific C# types for the feature to work, which are only present in "recent" versions of C#.

Some concrete examples of this are the nullability attributes like [NotNull] and [NotNullWhen] which are used as part of the nullable reference type feature introduced in C# 8.0. These attributes are only available in .NET Core 3.0+, so you might think you can't use them if you're targeting a .NET Standard 2.0 library, but that's not the case…

All the compiler cares about is that the [NotNull] attribute exists, has the correct shape, and is in the System.Diagnostics.CodeAnalysis namespace. The compiler specifically doesn't care which assembly the attribute can be found in. That means you can define your own version of [NotNull] in your netstandard2.0 assembly, use it just as you would the "real" version, and the compiler will happily use it with the nullable reference type feature!

There are a huge number of C# features that can be "lit-up" like this in down-version target frameworks. As long as the version of the compiler (i.e. the .NET SDK) supports the C# version, you can often just add some extra attributes to your library to enable the features.

Note that this isn't always the case; if the C# feature requires runtime support (for example default interface methods), then there's no way of using it in down-version target frameworks.

All of this means that source-only packages are a great way to easily add these C# compiler attributes to earlier target frameworks. These packages conditionally define the attributes for earlier target frameworks:

  • Polyfill—My personal favourite, provides conditional attributes for a vast number of C# features! See the GitHub project for the latest list.
  • IsExternalInit—Adds init and record support to < .NET 5.
  • Nullable—Adds support for nullable reference types to < .NET Core 3.0.
  • IndexRange—Adds support for the C# Index and Range features in < .NET Core 3.0.

When you add one of these packages to your app, a bunch of additional C# files are added to your application and included in the compilation, lighting up all the additional C# features.

We've covered some of the reasons why you might want a source-only package, so for the rest of the post we'll look at how you can create your own.

Creating a source-only NuGet package

For this example I'm going to create an incredibly simple library which exposes a single method, SayHello(). I'll then package it as a source-only NuGet package and show it in action.

Creating the basic library

We're going to create a very simple library called SourceOnlyExample (original, I know). First we create a basic library project:

mkdir SourceOnlyExample
cd SourceOnlyExample
dotnet new classlib

Next we replace the initial Class1 class with the following:

namespace Helpers
{
    internal class Class1
    {
        public static void SayHello() => global::System.Console.WriteLine("Hello!");
    }
}

There's a couple of things to note about this:

  • I'm generally not using recent C# features. For maximum compatibility with early .NET SDK versions, I'm not using file-scoped namespaces for example. How far you want (or need) to go with this largely depends on which versions of the compiler you want to support, rather than which target frameworks you want to support.
  • I'm using the full type names for built-in types, including the global:: namespace alias, to avoid any potential ambiguity. You can read more about namespace aliases in my previous post.

Finally, I've disabled "implicit global usings" in the .csproj file:

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Disabling implicit usings is a good idea in source-only packages. You don't know whether the target application will have this feature enabled, and if it doesn't and your .cs files rely on them, the target project will get type resolution errors.

At this point, if we pack this project into a NuGet file (by running dotnet pack) we'll get a "standard" NuGet package containing a SourceOnlyExample.dll file. In the next step we'll add a .nuspec file and create our source-only package.

A minimal nuspec to create a source-only package

The .nuspec file format is the historical way of defining how a project should be packaged as a NuGet package using the NuGet.exe tool. With the advent of the .NET CLI and the new SDK-style csproj files, they have largely become obsolete, but there's some situations in which you may need to fall back to them: source-only packages is one such case.

The nuspec file is an XML file in which you declare all the metadata for your NuGet package, as well as define the contents of the package. The following annotated nuspec file includes various basic metadata values,

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <!-- 👇 This is the ID of the package in NuGet -->
    <id>SourceOnlyExample</id>
    <version>1.0.0</version>
    <!-- 👇 Indicates the package is only used at build time, not deploy time -->
    <developmentDependency>true</developmentDependency>
    <authors>Andrew Lock</authors>
    <license type="expression">MIT</license>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <projectUrl>https://andrewlock.net/creating-source-only-nuget-packages/</projectUrl>
    <description>An example source-code only package</description>
    <tags>source compiletime</tags>
  </metadata>
  <files>
      <!-- 👇 Package all .cs files in the project, and place them in the contentFiles folder -->
      <file src="*.cs" target="contentFiles/cs/netstandard2.0/SourceOnlyExample/"/>
  </files>
</package>

The <metadata> node is relatively self explanatory and specifies a whole load of values used by nuget.org and Visual Studio etc to show a description of your package. The really important node is <files> which defines what should be packed into the NuGet package.

The <file> node shown above ensures that all the .cs files in the root folder are packaged under the contentFiles/cs/netstandard2.0/SourceOnlyExample path. Let's break down that path:

  • contentFiles—any files which should be included in the target project
  • cs—indicates the files should only be included in C# projects
  • netstandard2.0—any project which targets netstandard2.0+ will get the files
  • SourceOnlyExample—the files will be nested in the target folder under a folder called SourceOnlyExample. This isn't necessary but is common, and avoids conflicts between multiple NuGet packages.

Note that the target for the files includes the netstandard2.0 path, but if you multi-target your project (using <TargetFrameworks>net461;netstandard2.0</TargetFrameworks> for example) you could include multiple <file> elements and include different files for each each if you wish, something like this:

<files>
    <file src="*.net461.cs" target="contentFiles/cs/net461/SourceOnlyExample/"/>
    <file src="*.netstandard.cs" target="contentFiles/cs/netstandard2.0/SourceOnlyExample/"/>
</files>

With the nuspec created, we'll update the csproj file to reference it:

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>disable</Nullable>
    <!-- 👇 Add this line -->
    <NuSpecFile>SourceOnlyExample.nuspec</NuSpecFile>
  </PropertyGroup>

</Project>

After this change we can call dotnet pack on the project:

> dotnet pack
MSBuild version 17.9.8+b34f75857 for .NET
Determining projects to restore...
All projects are up-to-date for restore.
SourceOnlyExample -> C:\repos\temp\temp47\SourceOnlyExample\bin\Release\netstandard2.0\SourceOnlyExample.dll
Successfully created package 'C:\repos\temp\temp47\SourceOnlyExample\bin\Release\SourceOnlyExample.1.0.0.nupkg'.

And there we have it, our first source-only package! If we open the package in NuGet Package Explorer then we can see, sure enough, package contains only our .cs file:

The source only-package

What's more, if we add this NuGet package to a test project, we can see the Class1.cs file listed in the solution explorer:

Using the source only-package in Visual Studio

And we can use the SayHello() method in our app!

Helpers.Class1.SayHello();

This may be as much as you need to do, but I'll show a couple of tweaks you can make to improve your source-only package.

Hiding the files in Visual Studio

In the previous screenshot I showed that the .cs files in the SourceOnlyExample package show up in Visual Studio's Solution Explorer. You typically don't want that behaviour as it adds noise, so it's a common practice to hide these files by default.

To hide the files you need to customise the MSBuild properties associated with the added files. You can do this by creating a props file with the same name as your NuGet package, SourceOnlyExample.props in my case, with the following content:

<Project>
  <!--
    Hide content files from Visual Studio solution explorer. Adapted from:
   -->
  <ItemGroup>
    <Compile Update="@(Compile)">
      <!-- 👇 Replace 'SourceOnlyExample' with your own package name below -->
      <Visible Condition="'%(NuGetItemType)' == 'Compile' and '%(NuGetPackageId)' == 'SourceOnlyExample'">false</Visible>
    </Compile>
  </ItemGroup>
</Project>

You then need to package this file inside the build folder of the NuGet package. Update your nuspec files as follows:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>SourceOnlyExample</id>
    <version>1.0.0</version>
    <developmentDependency>true</developmentDependency>
    <authors>Andrew Lock</authors>
    <license type="expression">MIT</license>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <projectUrl>https://andrewlock.net/creating-source-only-nuget-packages/</projectUrl>
    <description>An example source-code only package</description>
    <tags>source compiletime</tags>
  </metadata>
  <files>
      <file src="*.cs" target="contentFiles/cs/netstandard2.0/SourceOnlyExample/"/>

      <!-- 👇 Hide content files from Visual Studio solution explorer  -->
      <file src="SourceOnlyExample.props" target="build/SourceOnlyExample.props" />
  </files>
</package>

Now after you create the NuGet package and add it an application the .cs files are hidden from Solution Explorer, but you can see that they're still included in the application (because the app still compiles).

The source only-package with files hidden from solution explorer

If you're only building modern .NET apps, then that should be pretty much all you need to be able to create source-only packages. Unfortunately, not everyone is that lucky.

Supporting packages.config as well as PackageReference

The contentfiles folder is used by projects using <PackageReference> to manage their NuGet dependencies. However the legacy approach, using a packages.config file requires that the files are included in a different folder: content. If you want your source-only package to support both approaches, you'll need to include both in your nusepc file:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>SourceOnlyExample</id>
    <version>1.0.0</version>
    <developmentDependency>true</developmentDependency>
    <authors>Andrew Lock</authors>
    <license type="expression">MIT</license>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <projectUrl>https://andrewlock.net/creating-source-only-nuget-packages/</projectUrl>
    <description>An example source-code only package</description>
    <tags>source compiletime</tags>
  </metadata>
  <files>
      <!--
      The files are included twice:
        As contentFiles (for PackageReferences)
        As content (for packages.config).
      -->
      <file src="*.cs" target="contentFiles/cs/netstandard2.0/SourceOnlyExample/"/>
      <file src="*.cs" target="content/cs/netstandard2.0/SourceOnlyExample/"/>

      <!-- Hide content files from Visual Studio solution explorer  -->
      <file src="SourceOnlyExample.props" target="build/SourceOnlyExample.props" />
  </files>
</package>

With this change your source-only package will have much wider support!

Before you upload your package to nuget.org you may want to make a few improvements to aid users of your package like adding a README or embedding an icon file, but functionality wise, your package is ready!

Summary

In this post I discussed source-only packages, what they are, and why you might want to use them. For the remainder of the post I showed how to create your own source-only Nuget package using a nuspec file and the <NuSpecFile> property. I then showed how to use a .props file to hide the source files in Solution Explorer in Visual Studio, and finally showed how to support packages.config files in addition to <PackageReference>.


July 30, 2024 at 02:30PM
Click here for more details...

=============================
The original post is available in Andrew Lock | .NET Escapades by Andrew Lock
this post has been published as it is through automation. Automation script brings all the top bloggers post under a single umbrella.
The purpose of this blog, Follow the top Salesforce bloggers and collect all blogs in a single place through automation.
============================

Salesforce