January 2016

Volume 31 Number 1

[Essential .NET]

C# Scripting

By Mark Michaelis | January 2016

Mark MichaelisWith the arrival of Visual Studio 2015 Update 1, henceforth Update 1, comes a new C# read-evaluate-print-loop (REPL), available as a new interactive window within Visual Studio 2015 or as a new command-line interface (CLI) called CSI. In addition to bringing the C# language to the command line, Update 1 also introduces a new C# scripting language, traditionally saved into a CSX file.

Before delving into the details of the new C# scripting, it’s important to understand the target scenarios. C# scripting is a tool for testing out your C# and .NET snippets without the effort of creating multiple unit testing or console projects. It provides a lightweight option for quickly coding up a LINQ aggregate method call on the command line, checking the .NET API for unzipping files, or invoking a REST API to figure out what it returns or how it works. It provides an easy means to explore and understand an API without the overhead of a yet another CSPROJ file in your %TEMP% directory.

The C# REPL Command-Line Interface (CSI.EXE)

As with learning C# itself, the best way to get started with learning the C# REPL interface is to run it and begin executing commands. To launch it, run the command csi.exe from the Visual Studio 2015 developer command prompt or use the full path, C:\Program Files (x86)\MSBuild\14.0\bin\csi.exe. From there, begin executing C# statements such as those shown in Figure 1.

Figure 1 CSI REPL Sample

C:\Program Files (x86)\Microsoft Visual Studio 14.0>csi
Microsoft (R) Visual C# Interactive Compiler version 1.1.0.51014
Copyright (C) Microsoft Corporation. All rights reserved.
Type "#help" for more information.
> System.Console.WriteLine("Hello! My name is Inigo Montoya");
Hello! My name is Inigo Montoya
> 
> ConsoleColor originalConsoleColor  = Console.ForegroundColor;
> try{
.  Console.ForegroundColor = ConsoleColor.Red;
.  Console.WriteLine("You killed my father. Prepare to die.");
. }
. finally
. {
.  Console.ForegroundColor = originalConsoleColor;
. }
You killed my father. Prepare to die.
> IEnumerable<Process> processes = Process.GetProcesses();
> using System.Collections.Generic;
> processes.Where(process => process.ProcessName.StartsWith("c") ).
.  Select(process => process.ProcessName ).Distinct()
DistinctIterator { "chrome", "csi", "cmd", "conhost", "csrss" }
> processes.First(process => process.ProcessName == "csi" ).MainModule.FileName
"C:\\Program Files (x86)\\MSBuild\\14.0\\bin\\csi.exe"
> $"The current directory is { Environment.CurrentDirectory }."
"The current directory is C:\\Program Files (x86)\\Microsoft Visual Studio 14.0."
>

The first thing to note is the obvious—it’s like C#—albeit a new dialect of C# (but without the ceremony that’s appreciated in a full production program and unnecessary in a quick-and-dirty prototype). Therefore, as you’d expect, if you want to call a static method you can write out the fully qualified method name and pass arguments within parentheses. As in C#, you declare a variable by prefixing it with the type and, optionally, assigning it a new value at declaration time. Again, as you’d expect, any valid method body syntax—try/catch/finally blocks, variable declaration, lambda expressions and LINQ—works seamlessly.

And even on the command line, other C# features, such as string constructs (case sensitivity, string literals and string interpolation), are maintained. Therefore, when you’re using or outputting paths, backslashes need to be escaped using a C# escape character (“\”) or a string literal, as do double backslashes in the output of a path like that of csi.exe. String interpolation works, too, as the “current directory” sample line in Figure 1 demonstrates.

C# scripting allows far more than statements and expressions, though. You can declare custom types, embed type metadata via attributes, and even simplify verbosity using C# script-specific declaratives. Consider the spell-checking sample in Figure 2.

Figure 2 The C# Scripting Class Spell (Spell.csx)

#r ".\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll"
#load "Mashape.csx"  // Sets a value for the string Mashape.Key
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public class Spell
{
  [JsonProperty("original")]
  public string Original { get; set; }
  [JsonProperty("suggestion")]
  public string Suggestion { get; set; }
  [JsonProperty(PropertyName ="corrections")]
  private JObject InternalCorrections { get; set; }
  public IEnumerable<string> Corrections
  {
    get
    {
      if (!IsCorrect)
      {
        return InternalCorrections?[Original].Select(
          x => x.ToString()) ?? Enumerable.Empty<string>();
      }
      else return Enumerable.Empty<string>();
    }
  }
  public bool IsCorrect
  {
    get { return Original == Suggestion; }
  }
  static public bool Check(string word, out IEnumerable<string> corrections)
  {
    Task <Spell> taskCorrections = CheckAsync(word);
    corrections = taskCorrections.Result.Corrections;
    return taskCorrections.Result.IsCorrect;
  }
  static public async Task<Spell> CheckAsync(string word)
  {
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
      $"https://montanaflynn-spellcheck.p.mashape.com/check/?text={ word }");
    request.Method = "POST";
    request.ContentType = "application/json";
    request.Headers = new WebHeaderCollection();
    // Mashape.Key is the string key available for
    // Mashape for the montaflynn API.
    request.Headers.Add("X-Mashape-Key", Mashape.Key);
    using (HttpWebResponse response =
      await request.GetResponseAsync() as HttpWebResponse)
    {
      if (response.StatusCode != HttpStatusCode.OK)
        throw new Exception(String.Format(
        "Server error (HTTP {0}: {1}).",
        response.StatusCode,
        response.StatusDescription));
      using(Stream stream = response.GetResponseStream())
      using(StreamReader streamReader = new StreamReader(stream))
      {
        string strsb = await streamReader.ReadToEndAsync();
        Spell spell = Newtonsoft.Json.JsonConvert.DeserializeObject<Spell>(strsb);
        // Assume spelling was only requested on first word.
        return spell;
      }
    }
  }
}

For the most part, this is just a standard C# class declaration. However, there are several specific C# scripting features. First, the #r directive serves to reference an external assembly. In this case, the reference is to Newtonsoft.Json.dll, which helps parse the JSON data. Note, however, this is a directive designed for referencing files in the file system. As such, it doesn’t require the unnecessary ceremony of a backslash escape sequence.

Second, you can take the entire listing and save it as a CSX file, then “import” or “inline” the file into the C# REPL window using #load Spell.csx. The #load directive allows you to include additional script files as if all the #load files were included in the same “project” or “compilation.” Placing code into a separate C# script file enables a type of file refactoring and, more important, the ability to persist the C# script over time.

Using declarations are another C# language feature allowed in C# scripting, one that Figure 2 leverages several times. Note that just as in C#, a using declaration is scoped to the file. Therefore, if you called #load Spell.csx from the REPL window, it wouldn’t persist the using Newtonsoft.Json declarative outside the Spell.csx. In other words, using Newtonsoft.Json from within Spell.csx wouldn’t persist into the REPL window without being declared again explicitly in the REPL window (and vice versa). Note that the C# 6.0 using static declarative is also supported. Therefore, a “using static System.Console” declarative eliminates the need to prefix any of the System.Console members with the type, allowing for REPL commands such as “WriteLine("Hello! My name is Inigo Montoya").”

Other constructs of note in C# scripting include the use of attributes, using statements, property and function declaration, and support for async/await. Given the latter support, it’s even possible to leverage await in the REPL window:

 

(await Spell.CheckAsync("entrepreneur")).IsCorrect

Here are some further notes about the C# REPL interface: 

  • You can’t run csi.exe from within Windows PowerShell Integrated Scripting Environment (ISE) as it requires direct console input, which isn’t supported from the “simulated” console windows of Windows PowerShell ISE. (For this reason, consider adding to the unsupported list of console applications—$psUnsupportedConsoleApplications.)
  • There is no “exit” or “quit” command to leave the CSI program. Instead, you use Ctrl+C to end the program.
  • Command history is persisted between csi.exe sessions launched from the same cmd.exe or PowerShell.exe session. For example, if you start csi.exe, invoke Console.WriteLine("HelloWorld"), use Ctrl+C to exit, and then relaunch csi.exe, the up arrow will display the previous Console.WriteLine("HelloWorld") command. Exiting the cmd.exe window and relaunching it will clear out the history.
  • Csi.exe supports the #help REPL command, which displays the output shown in Figure 3.
  • Csi.exe supports a number of command-line options, as shown in Figure 4.

Figure 3 The REPL #help Command Output

> #help
Keyboard shortcuts:
  Enter         If the current submission appears to be complete, evaluate it.
                Otherwise, insert a new line.
  Escape        Clear the current submission.
  UpArrow       Replace the current submission with a previous submission.
  DownArrow     Replace the current submission with a subsequent
                submission (after having previously navigated backward).
REPL commands:
  #help         Display help on available commands and key bindings.

Figure 4 Csi.exe Command-Line Options

Microsoft (R) Visual C# Interactive Compiler version 1.1.0.51014
Copyright (C) Microsoft Corporation. All rights reserved.
Usage: csi [option] ... [script-file.csx] [script-argument] ...
Executes script-file.csx if specified, otherwise launches an interactive REPL (Read Eval Print Loop).
Options:
  /help       Display this usage message (alternative form: /?)
  /i          Drop to REPL after executing the specified script
  /r:<file>   Reference metadata from the specified assembly file
              (alternative form: /reference)
  /r:<file list> Reference metadata from the specified assembly files
                 (alternative form: /reference)
  /lib:<path list> List of directories where to look for libraries specified
                   by #r directive (alternative forms: /libPath /libPaths)
  /u:<namespace>   Define global namespace using
                   (alternative forms: /using, /usings, /import, /imports)
  @<file>     Read response file for more options
  --          Indicates that the remaining arguments should not be
              treated as options

As noted, csi.exe allows you to specify a default “profile” file that customizes your command window:

  • To clear the CSI console, invoke Console.Clear. (Consider a using static System.Console declarative to add support for simply invoking Clear.)
  • If you’re  entering a multi-line command and make an error on an earlier line, you can use Ctrl+Z followed by Enter to cancel and return to an empty command prompt without executing (note that the ^Z will appear in the console).

The Visual Studio C# Interactive Window

As I mentioned, there’s also a new Visual Studio C# Interactive window in Update 1, as shown in Figure 5. The C# Interactive window is launched from the View | Other Windows | C# Interactive menu, which opens up an additional docked window. Like the csi.exe window, it’s a C# REPL window but with a few added features. First, it includes syntax color coding and IntelliSense. Similarly, compilation occurs in real time, as you edit, so syntax errors and the like will automatically be red-squiggle-underlined.

Declaring a C# Script Function Outside of a Class Using the Visual Studio C# Interactive Window
Figure 5 Declaring a C# Script Function Outside of a Class Using the Visual Studio C# Interactive Window

A common association with the C# Interactive Window is, of course, the Visual Studio Immediate and Command windows. While there are overlaps—after all, they’re both REPL windows against which you can execute .NET statements—they have significantly different purposes. The C# Immediate Window is bound directly to the debug context of your application, thus allowing you to inject additional statements into the context, examine data within the debug session, and even manipulate and update the data and debug context. Similarly, the Command Window provides a CLI for manipulating Visual Studio, including executing the various menus, but from the Command Window rather than from the menus themselves. (Executing the command View.C#Interactive, for example, opens the C# Interactive Window.) In contrast, the C# Interactive Window allows you to execute C#, including all the features relating to the C# REPL interface discussed in the previous section. However, the C# Interactive Window doesn’t have access to the debug context. It’s an entirely independent C# session without handles to either the debug context or even to Visual Studio. Like csi.exe, it’s an environment that lets you experiment with quick C# and .NET snippets to verify your understanding without having to start yet another Visual Studio console or unit testing project. Instead of having to launch a separate program, however, the C# Interactive Window is hosted within Visual Studio, where the developer is presumably already residing.

Here are some notes about the C# Interactive Window:

  • The C# Interactive Window supports a number of additional REPL commands not found in csi.exe, including:
    • #cls/#clear to clear the contents of the editor window
    • #reset to restore the execution environment to its initial state while maintaining the command history
  • The keyboard shortcuts are a little unexpected, as the #help output in Figure 6 shows.

Figure 6 Keyboard Shortcuts for the C# Interactive Window

Enter If the current submission appears to be complete, evaluate it. Otherwise, insert a new line.
Ctrl+Enter       Within the current submission, evaluate the current submission.
Shift+Enter      Insert a new line.
Escape Clear the current submission.
Alt+UpArrow      Replace the current submission with a previous submission.   
Alt+DownArrow Replace the current submission with a subsequent submission (after having previously navigated backward).
Ctrl+Alt+UpArrow Replace the current submission with a previous submission beginning with the same text.                  
Ctrl+Alt+DownArrow Replace the current submission with a subsequent submission beginning with the same text (after having previously navigated backward).
UpArrow

At the end of the current submission, replace the current submission with a previous submission.

Elsewhere, move the cursor up one line.

DownArrow

At the end of the current submission, replace the current submission with a subsequent submission (after having previously navigated backward).

Elsewhere, move the cursor down one line.

Ctrl+K, Ctrl+Enter Paste the selection at the end of interactive buffer, leave caret at the end of input.
Ctrl+E, Ctrl+Enter Paste and execute the selection before any pending input in the interactive buffer.
Ctrl+A First press, select the submission containing the cursor. Second press, select all text in the window.

It is important to note that Alt+UpArrow/DownArrow are the keyboard shortcuts for recalling the command history. Microsoft selected these over the simpler UpArrow/DownArrow because it wanted the Interactive window experience to match that of a standard Visual Studio code window.

  • Because the C# Interactive Window is hosted within Visual Studio, there isn’t the same opportunity to pass references, using declaratives or imports via the command line, as there is with csi.exe. Instead, the C# Interactive Window loads its default execution context from C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\PrivateAssemblies\CSharpInteractive.rsp, which identifies the assemblies to reference by default:
# This file contains command-line options that the C# REPL
# will process as part of every compilation, unless
# \"/noconfig\" option is specified in the reset command.
/r:System
/r:System.Core
/r:Microsoft.CSharp
/r:System.Data
/r:System.Data.DataSetExtensions
/r:System.Xml
/r:System.Xml.Linq
SeedUsings.csx

Furthermore, the CSharpInteractive.rsp file references a default C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\PrivateAssemblies\SeedUsings.csx file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

The combination of these two files is why you can use Console.WriteLine and Environment.CurrentDirectory rather than the fully qualified System.Console.WriteLine and System.Environ-ment.Current­Directory, respectively. In addition, referencing assemblies such as Microsoft.CSharp enables the use of language features like dynamic without anything further. (Modifying these files is how you’d change your “profile” or “preferences” so that the changes persist between sessions.)

More on the C# Script Syntax

One thing to keep in mind about the C# script syntax is that much of the ceremony important to a standard C# becomes appropriately optional in a C# script. For example, things like method bodies don’t need to appear within a function and C# script functions can be declared outside of the confines of a class. You could, for example, define a NuGet Install function that appeared directly in the REPL window, as shown in Figure 5. Also, perhaps somewhat surprisingly, C# scripting doesn’t support declaring namespaces. For example, you can’t wrap the Spell class in a Grammar namespace: namespace Grammar { class Spell {} }.

It’s important to note that you can declare the same construct (variable, class, function and so forth) over and over again. The last declaration shadows any earlier declaration.

Another important item to be conscious of is the behavior of the command-ending semicolon. Statements (variable assignments, for example) require a semicolon. Without the semicolon the REPL window will continue to prompt (via a period) for more input until the semicolon is entered. Expressions, on the other hand, will execute without the semicolon. Hence, System.Diagnostics.Pro­cess.Start("notepad") will launch Notepad even without the ending semicolon. Furthermore, because the Start method call returns a process, string output of the expression will appear on the command line: [System.Diagnostics.Process (Notepad)]. Closing an expression with a semicolon, however, hides the output. Invoking Start with an ending semicolon, therefore, won’t produce any output, even though Notepad will still launch. Of course, Console.WriteLine("It would take a miracle."); will still output the text, even with the semicolon, because the method itself is displaying the output (not the return from the method).

The distinction between expressions and statements can result in subtle differences at times. For example, the statement string text = "There’s a shortage of perfect b…."; will result in no output, but text="Stop that rhyming and I mean it" will return the assigned string (because the assignment returns the value assigned and there’s no semicolon to suppress the output).

The C# script directives for referencing additional assemblies (#r) and importing existing C# scripts (#load) are wonderful additions. (One could imagine complex solutions like project.json files to achieve the same thing that would not be as elegant.) Unfortunately, at the time of this writing, NuGet packages aren’t supported. To reference a file from NuGet requires installing the package to a directory and then referencing the specific DLL via the #r directive. (Microsoft assures me this is coming.)

Note that at this time the directives refer to specific files. You can’t, for example, specify a variable in the directive. While you’d expect this with a directive, it prevents the possibility of dynamically loading an assembly. For example, you could dynamically invoke “nuget.exe install” to extract an assembly (again see Figure 5). Doing so, however, doesn’t allow your CSX file to dynamically bind to the extracted NuGet package because there’s no way to dynamically pass the assembly path to the #r directive.

A C# CLI

I confess I have a love-hate relationship with Windows PowerShell. I love the convenience of having the Microsoft .NET Framework on the command line and the possibility of passing .NET objects across the pipe, rather than the traditional text of so many of the CLIs that came before. That said, when it comes to the C# language, I am partisan—I love its elegance and power. (To this day, I’m still impressed with the language extensions that made LINQ possible.) Therefore, the idea that I could have the breadth of Windows PowerShell .NET combined with the elegance of the C# language meant I approached the C# REPL as a replacement for Windows PowerShell. After launching csi.exe, I immediately tried commands like cd, dir, ls, pwd, cls, alias and the like. Suffice it to say, I was disappointed because none of them worked. After pondering the experience and discussing it with the C# team, I realized that replacing Windows PowerShell was not what the team was focused on in version 1. Furthermore, it is the .NET Framework and, therefore, it supports extensibility both by adding your own functions for the preceding commands and even by updating the C# script implementation on Roslyn. I immediately set about defining functions for such commands. The start of such a library is available for download on GitHub at github.com/CSScriptEx.

For those of you looking for a more functional C# CLI that supports the previous command list out of the box now, consider ScriptCS at scriptcs.net (also on GitHub at github.com/scriptcs). It, too, leverages Roslyn, and includes alias, cd, clear, cwd, exit, help, install, references, reset, scriptpacks, usings and vars. Note that, with ScriptCS, the command prefix today is a colon (as in :reset) rather than a number sign (as in #reset).  As an added bonus, ScriptCS also adds support for CSX files, in the form of colorization and IntelliSense, to Visual Studio Code.

Wrapping Up

At least for now, the purpose of the C# REPL interface is not to replace Windows PowerShell or even cmd.exe. To approach it as such at the start will result in disappointment. Rather, I suggest you approach C# scripting and the REPL CLIs as lightweight replacements for Visual Studio | New Project: UnitTestProject105 or the similarly purposed dotnetfiddle.net. These are C# and .NET targeted ways to increase your understanding of the language and .NET APIs. The C# REPL provides a means of coding up short snippets or program units you can noodle on until they’re ready to be cut and pasted into larger programs. It allows you to write more extensive scripts whose syntax is validated (even for little things like casing mismatches) as you write the code, rather than forcing you to execute the script only to discover you mistyped something. Once you understand its place, C# scripting and its interactive windows become a pleasure, the tool you’ve been looking for since version 1.0.

As interesting as C# REPL and C# scripting are on their own, consider that they also provide a stepping stone to being an extension framework for your own application—à la Visual Basic for Applications (VBA). With an interactive window and C# scripting support, you can imagine a world—not too far off—in which you can add .NET “macros” into your own applications again—without inventing a custom language, parser and editor. Now that would be a legacy COM feature worth bringing to the modern world none too soon.


Mark Michaelis is founder of IntelliTect, where he serves as its chief technical architect and trainer. For nearly two decades he has been a Microsoft MVP, and a Microsoft Regional Director since 2007. Michaelis serves on several Microsoft software design review teams, including C#, Microsoft Azure, SharePoint and Visual Studio ALM. He speaks at developer conferences and has written numerous books including his most recent, “Essential C# 6.0 (5th Edition)” (itl.tc/EssentialCSharp). Contact him on Facebook at facebook.com/Mark.Michaelis, on his blog at IntelliTect.com/Mark, on Twitter: @markmichaelis or via e-mail at mark@IntelliTect.com.

Thanks to the following Microsoft technical expert for reviewing this article: Kevin Bost and Kasey Uhlenhuth