Monday, August 31, 2020

C# preprocessor directive symbols from the dotnet build command line via DefineConstants

Invoking the C# compiler directly allows one to pass in symbols for the preprocessor via a command option (-define or -d). But it's not at all obvious how to do this with the dotnet build command. There is no 'define' flag, so how do you do it?

Let me first show you how this works using the C# compiler directly:

Create a new file 'Program.cs' with this code:

using System;

namespace CscTest
{
    class Program
    {
        static void Main(string[] args)
        {
#if FOO
            Console.WriteLine("Hello FOO!");
#else
            Console.WriteLine("NOT FOO!");
#endif
        }
    }
}

Now compile it with CSC:

>csc -d:FOO Program.cs

And run it:

>Program
Hello FOO!

Happy days.

It is possible to do the same thing with dotnet build, it relies on populating the MSBuild DefineConstants property, but unfortunately one is not allowed to access this directly from the command line:

If you invoke this command:

dotnet build -v:diag -p:DefineConstants=FOO myproj.csproj

It has no effect, and somewhere deep in the diagnostic output you will find this line:

The "DefineConstants" property is a global property, and cannot be modified.

Instead one has to employ a little indirection. In your csproj file it is possible to populate DefineConstants. Create a project file, say 'CscTest.csproj', with a DefineConstants PropertyGroup element with the value FOO:

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <DefineConstants>FOO</DefineConstants>
  </PropertyGroup>

</Project>

Build and run it with dotnet run:

>dotnet run .
Hello FOO!

The csproj file is somewhat like a template, one can pass in arbitrary properties using the -p flag, so we can replace our hard coded FOO in DefineConstants with a property placeholder:

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <DefineConstants>$(MyOption)</DefineConstants>
  </PropertyGroup>

</Project>

And pass in FOO (or not) on the command line. Unfortunately it now means building and running as two individual steps:

>dotnet build -p:MyOption=FOO .
...
>dotnet run --no-build
Hello FOO!

And all is well with the world. It would be nice if the MSBuild team allowed preprocessor symbols to be added directly from the command line though.

Tuesday, August 04, 2020

Restoring from an Azure Artifacts NuGet feed from inside a Docker Build

If you are using Azure DevOps pipelines to automate building your .NET Core application Docker images, it's natural to also want to use the DevOps Artifacts NuGet feed for your internally hosted NuGet packages. Unfortunately there is much confusion and misinformation about how to authenticate against the Artifacts NuGet feed. While researching this topic I found various sources saying that you needed to install the NuGet credential provider as part of the docker build, and then set various environment variables. I followed this route (excerpt from an example below), even to the extent of creating a custom Docker image for all our dotnet builds with the credential provider already installed.  
ARG PAT
RUN wget -qO- https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh | bash
ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED true
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS “{\”endpointCredentials\”: [{\”endpoint\”:\”https://pkgs.dev.azure.com/jakob/_packaging/DockerBuilds/nuget/v3/index.json\”, \”password\”:\”${PAT}\”}]}”
The technique is to install the credential provider, then configure it with the DevOps Artifacts endpoint and a Personal Access Token (PAT), which you can generate by going to your user settings from the DevOps UI:

DevOps User Settings


After wasting over a day on this, I was then very surprised indeed to find that a colleague was restoring from the same DevOps Artifacts feed on a locally hosted TeamCity server, simply by providing the PAT as the NuGet API-Key! They hadn't installed the NuGet credential provider, so according to the Microsoft documentation it shouldn't work?

I tried it myself. The PAT does indeed work as a NuGet API-Key. A slight further complication is that the 'dotnet restore' command doesn't have an API-Key switch, so the next easiest thing is to simply use a nuget.config file as follows:

<?xml version="1.0" encoding="utf-8"?>
<configuration>

    <packageSources>
        <add key="DevOpsArtifactsFeed" value="your-devops-artifacts-nuget-source-URL" />
    </packageSources>

    <packageSourceCredentials>
        <DevOpsArtifactsFeed>
            <add key="Username" value="foo" />
            <add key="ClearTextPassword" value="your-PAT" />
        </DevOpsArtifactsFeed>
    </packageSourceCredentials>

</configuration>
Replace the place-holders with your Artifacts NuGet feed URL and your PAT. The Username is not considered by Artifacts feed and can be any string. Copy the above configuration into a file named 'nuget.config' and run create a Dockerfile like this:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build

WORKDIR /app

# copy source code, nuget.config file should be placed in the 'src' directory for this to work.
COPY src/ .

# restore nuget packages
RUN dotnet restore --configfile nuget.config

# build
RUN dotnet build

# publish
RUN dotnet publish -o output

# build runtime image
FROM mcr.microsoft.com/dotnet/core/runtime:3.1 AS runtime
WORKDIR /app
COPY --from=build /app/output/ ./
# 
ENTRYPOINT ["your/entry/point"]

This is the simplest thing that will possibly work. But you really shouldn't hard code secrets such as your PAT into your source control system. Very conveniently, the dotnet restore command will do environment variable replacement in the nuget.config file, so you can replace your hard-coded PAT with a reference to an ENV var and then pass that to docker build: In your nuget.config file:
    <packageSourceCredentials>
        <DevOpsArtifactsFeed>
            <add key="Username" value="foo" />
            <add key="ClearTextPassword" value="%NUGET_PAT%" />
        </DevOpsArtifactsFeed>
    </packageSourceCredentials>
In your Dockerfile:
ARG NUGET_PAT
ENV NUGET_PAT=$NUGET_PAT
Your docker build command:
docker build -t my-image --build-arg NUGET_PAT="your PAT" .
I hope this short post saves somebody from the many hours that I wasted on this. I also hope that Microsoft updates their documentation!