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
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}\”}]}”
<?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>
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"]
<packageSourceCredentials> <DevOpsArtifactsFeed> <add key="Username" value="foo" /> <add key="ClearTextPassword" value="%NUGET_PAT%" /> </DevOpsArtifactsFeed> </packageSourceCredentials>
ARG NUGET_PAT ENV NUGET_PAT=$NUGET_PAT
docker build -t my-image --build-arg NUGET_PAT="your PAT" .
Wednesday, April 15, 2020
A Framework to DotNet Core Conversion Report
Background
Motivation
Process
Analysis
-tr
option:asmspy.exe <path to application executable> -tr
Converting Projects to dotnet Standard and Core
.csproj
files, did not go well, so we soon settled on the practice of creating entirely new solutions and projects and simply copying the .cs files across. For this Git Worktree is your very good friend. Worktree allows you to create a new branch with a new working tree in a separate directory, so you can maintain both your main branch (master for example), and your conversion branch side by side. The project conversion process looks something like this:- Create a new branch in a new worktree with the worktree command:
git worktree add -b core-conversion <path to new working directory>
- In the new branch open the solution in Visual Studio and remove all the projects.
- Delete all the project files using explorer or the command line.
- Create new projects, copying the names of the old projects, but using the dotnet Standard project type for libraries, ‘Class Library (.NET Standard)’, and the dotnet Core project type for services and applications. In our case all the services were created as ‘Console App (.NET Core)’. For unit tests we used ‘xUnit Test Project (.NET Core)’, or ‘MSTest Test Project (.NET Core)’, depending on the source project test framework.
- From our analysis (above), add the project references and NuGet packages required by each project.
- Copy the .cs files only from the old projects to the new projects. An interesting little issue we found was that old .cs files were still in the repository despite being removed from their projects. .NET Framework projects enumerate each file by name (the source of many a problematic merge conflict) but Core and Standard projects simply use a wildcard to include every .cs file in the project directory, so a compile would include these previously deleted files and cause build problems. Easily fixed by deleting the rogue files.
- Once all this is done the solution should build and the tests should all pass.
- NuGet package information is now maintained in the project file itself, so for your libraries you will need to copy that from your old
.nuspec
files. - One you are happy that the application is working as expected, merge your changes back into your main branch.
Taking advantage of new dotnet core frameworks
Microsoft.Extensions.*
namespaces.Framework NuGet Package | Microsoft.Extensions.* equivalent |
---|---|
TopShelf | Microsoft.Extensions.Hosting.WindowsServices |
Ninject | Microsoft.Extensions.DependencyInjection |
System.Configuration | Microsoft.Extensions.Configuration |
log4net | Microsoft.Extensions.Logging |
Microsoft.Extensions.*
frameworks, so some refactoring is required to replace these. In the case of TopShelf and Ninject, the scope of this refactoring is limited; largely to the Program.cs file and the main service class for TopShelf, and to the NinjectModules where service registration occurs for Ninject. This makes it relatively painless to do the substitution. With Ninject, the main issue is the limited feature set of Microsoft.Extensions.DependencyInjection
. If you make widespread use of advanced container features, you’ll find yourself writing a lot of new code to make the same patterns work. Most of our registrations were pretty straightforward to convert.log4net
with Microsoft.Extensions.Logging
is a bit more of a challenge since references to log4net
, especially the ILog
class and its methods, were spread liberally throughout our codebase. Here we found that the best refactoring method was to let the type system do the heavy lifting, using the following steps:- Uninstall the
log4net
NuGet package. The build will fail with many missing class and method exceptions. - Create a new interface named
ILog
with namespacelog4net
, now the build will fail with just missing method exceptions. - Add methods to your
Ilog
interface to match the missinglog4net
methods (for examplevoid Info(object message);
) until you get a clean build. - Now use Visual Studio’s rename symbol refactoring to change your
ILog
interface to match theMicrosoft.Extensions.Logging
ILogger
interface and its methods to matchILogger
's methods. For example renamevoid Info(object message);
tovoid LogInformation(string message);
. - Rename the namespace from
log4net
toMicrosoft.Extensions.Logging
. This is a two step process because you can’t use rename symbol to turn one symbol into three, so renamelog4net
to some unique string, then use find and replace to change it toMicrosoft.Extensions.Logging
. - Finally delete your interface .cs file, and assuming you’ve already added the
Microsoft.Extensions.Hosting
NuGet package and its dependencies (which include logging), everything should build and work as expected.
App.config
and System.Configuration.ConfigurationManager
to be replaced with a new configuration framework, Microsoft.Extensions.Configuration
. This is far more flexible and can load configuration from various sources, including JSON files, environment variables, and command line arguments. We replaced our App.config
files with appsettings.json
, and refactored our attributed configuration classes into POCOs and used the IConfigurationSection.Bind<T>(..)
method to load the config. An easier and more streamlined process than the clunky early 2000’s era System.Configuration
. At a later date we will probably move to loading environment specific configuration from environment variables to better align with the Docker/k8s way of doing things.Changes to our build and deployment pipeline
- Using the
dotnet
tool: The build and test process changed from using NuGet, MSBuild and xUnit, to having every step, except the Octopus trigger, run with thedotnet
tool. This simplifies the process. One very convenient change is how easy it is to version the build with the command line switch/p:Version=%build.number%
. We also took advantage of the self-contained feature to free us from having to ensure that each deployment target had the correct version of Core installed. This is a great advantage. - JSON configuration variables: We previously used the Octopus variable substitution feature to inject environment specific values into our
App.config
files. This involved annotating the config file with Octopus substitution variables, a rather fiddly and error prone process. But now with the newappsettings.json
file we can use the convenient JSON configuration variable feature to do the replacement, with no need for any Octopus specific annotation in our config file. - Windows service installation and startup: Previously, with TopShelf, installing our windows services on target machines was a simple case of calling
ourservice.exe install
andourservice.exe start
to start it. Although theMicrosoft.Extensions.Hosting
framework provides hooks into the Windows service start and stop events, it doesn’t provide any facilities to install or start the service itself, so we had to write somewhat complex powershell scripts to invokeSC.exe
to do the installation and the powershellStart-Service
command to start. This is definitely a step backward.
Observations
Microsoft.Extensions.*
frameworks. This was a significant refactoring effort. Doing a thorough analysis of your project and its dependencies before embarking on the conversion is essential. With large scale distributed applications such as ours, it’s often surprising how deep the organisations internal dependency graph goes, especially if, like me, you are converting large codebases which you didn’t have any input into writing. With the actual project conversion I would highly recommend starting with new projects rather than trying to convert them in place. This turned out to be a far more reliable method.dotnet
command), making automated build processes so much easier, is probably the most prominent example. I for one am very pleased we were able to take the effort to make the change.