On Windows operating systems, administrators have traditionally used PowerShell as an automation tool. It allows you to execute command line commands like cd , dir and provides convenient access to various APIs: COM, WMI, Active Directory, etc. PowerShell is good for automating tasks on local and remote systems using cmdlets – lightweight commands. Initially, it was built on the .NET Framework, later, when it became cross-platform in 2016, it switched to .NET Core, and its code became open source. Although PowerShell is .NET-based, its scripting syntax differs from the C # language that .NET developers are used to, many of whom are also involved in configuring assemblies. For them, an alternative to PowerShell scripts might be C # scripts , whose syntax is almost identical to that of C #. TeamCity 2021.2 has a dedicated runner to support them. It works in 2 ways:

  • To run a script from a file, usually with a .csx extension
  • To run a script edited through the TeamCity build step UI

For the first option, the path to the C # script file is indicated, for the second – its contents, otherwise they are similar. The Hello World example gives an overview.

Hello world

This example is of no practical value, it helps to understand the basic features, and its result is the output of the Hello World from project C # Script greeting to the build log:

The TeamCity DSL for this build step is concise:

object HelloWorldBuildType: BuildType({
  name = "Say hello"
  steps {
    csharpScript {
      content = "WriteLine(\"Hello World from project \" + Args[0])"
      arguments = "\"%system.teamcity.projectName%\""
      dockerImage = Settings.dockerImageRuntime
      dockerImagePlatform = CSharpScriptCustomBuildStep.ImagePlatform.Linux

Several parameters are defined here:

  • content , corresponds to the C # Script field and defines the content of the script
  • arguments , the Script parameters field – defines optional script parameters, here this is the name of the project % system.teamcity.projectName% , in fact, this is C # Script
  • dockerImage , the Run step within Docker container field – contains the name of the docker image for the container in which the script will be run
  • dockerImagePlatform , Docker image platform field – specifies to use Linux docker container in case the docker host supports multiplatform

This example does not use the NuGet package sources field . It can define one or more NuGet package sources, separated by spaces. This field allows you to use arbitrary NuGet package sources, in addition to https://api.nuget.org/v3/index.json , including private and built-in TeamCity. Another TeamCity C # script tool field is undefined, but it has a default value of Default and instructs to use the default version of the TeamCity C# script tool . Therefore, before using the runner for the first time, you need to download this tool through the TeamCity agent tools and define the default version. In fact, you download NuGet package that contains a .NET tool for running C # scripts.

C # script tool

Microsoft provides a cross-platform scripting tool for F# , but for C#, there is only a Windows version at the moment . This is partly the reason TeamCity has made an alternative tool. The second reason is deeper integration with TeamCity. Compared to the Microsoft tool, the TeamCity tool has a number of additional features:

  • Cross-platform
  • Integration with TeamCity, as well as the ability to use private and built-in TeamCity NuGet package sources
  • REPL directive for dependencies on NuGet packages
  • Additional API that is planned to be expanded to support the most popular use cases

Under the hood, it uses Microsoft.CodeAnalysis.Scripting to run C# scripts, NuGet.Build.Tasks to work with NuGet packages from those scripts, the TeamCity.ServiceMessages package to send service messages to TeamCity, and Pure.DI to tie it all together. The tool requires .NET 6 Runtime. Therefore, in the examples, all C# scripts in TeamCity are executed in a docker container, otherwise the .NET Runtime must be pre-installed. Several use cases for this .NET tool are suggested:

  • command line mode
  • interactive mode
  • in a special TeamCity runner (example above)

For the first two, the installation of the tool is performed by the command:

dotnet tool install dotnet-csi -g --version <version>

where version is better to use the most recent version from the NuGet repository.

To run the tool interactively, run:

dotnet csi

To execute some script MyScript.csx in command line mode:

dotnet csi MyScript.csx

You can learn more about this here.

Build and deploy

In this example, the TeamCity build configuration consists of 6 steps, two of which we will dwell on in more detail.

The first step runs a C # script that restores the latest version of a package named MySampleLib from the NuGet.org repository. Its current version is determined and a new version is calculated. The second and third steps build and test the MySampleLib library using the value of the new version. In the fourth step, a Telegram bot is created, which publishes intermediate assembly results with a test report, and votes among those interested. This vote decides whether the library is ready to be published to the NuGet repository. If, within the specified voting time, all concerned unanimously decide that the library is ready, TeamCity packages the previously built and tested assembly into a NuGet package namedMySampleLib, and the version calculated earlier. In the final step, TeamCity publishes this package to the NuGet repository. If at least one person voted against or the voting time expired, the build ends with a description of the reason. Let’s take a closer look at the first step, where the next NuGet version of the MySampleLib package is defined.

The C # script above accepts the NuGet package name as the only parameter in the Script parameters field . Its value is available as the 0th element of the global Args array . Line # 3 calls the global GetService function to access the NuGet API . # 4 restores the most recent package named MySampleLib (from Args [0]) and returns information about it and all the packages it depends on. Line # 7 calculates the value of the new version of the MySampleLib package . On line # 2, this value is stored in the global Props dictionary with the version index , and will be available in all the following steps in the TeamCity build: both in C # scripts and in commands such asdotnet build , dotnet test , dotnet pack as if passed to them via the -p: version = 1.0.9 command line argument . TeamCity will expose this value as a system.version parameter and can be referenced using the % system.version%expression . Further, at other stages of the build in this example, the value of the new version will be used when building, testing, packaging the package and publishing it without any additional effort. Line # 11 prints the current version of the MySampleLib package to the build log in the Success color – green in the TeamCity palette.

At the third stage of the assembly, a vote is taken on the Telegram channel. Before its launch, there is already a ready-made library and the results of its testing.

The C # voting script is located in the Samples / Scripts / TelegramBot.csx file. First, the script gets the bot’s Telegram token and the voting duration from the TeamCity system parameters defined in the parent project:

if(!Props.TryGetValue("telegram.bot.token", out var token))
    throw ...

if(!Props.TryGetValue("telegram.bot.poll.timeout", out var timeoutStr) ||
   !TimeSpan.TryParse(timeoutStr, out var timeout))
    throw ...

The script then creates a Telegram voting bot using the Telegram.Bot and Telegram.Bot.Extensions.Polling packages:

#r "nuget: Telegram.Bot, 15.6.0" #r "nuget: Telegram.Bot.Extensions.Polling, 0.2.0"

To participate in the vote, everyone interested in the assembly must execute the / start command in the bot’s Telegram channel. The script sends everyone a link to the current build and votes:

After the vote, the C # script analyzes the results. In the case when a unanimous decision on publication is made at the set time, the Telegram names of the people who voted for this decision are displayed in the build log.

The package is created and published to the NuGet repository. Otherwise, when at least one has voted against, the build stops at the current step by calling the Error method from the script. The error line contains a list of people who voted against publishing the package. A similar result will be in the case when the voting time has expired, and the error text will change to the corresponding one.

We welcome your wishes or PR in this repository to extend the built-in API , for example, for deeper integration with TeamCity or to support frequently used scripts.


TeamCity C# Script Runner can be useful for cases when you need to efficiently automate any aspect of the assembly, by .NET developers or administrators who are familiar with C # syntax.