Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / PowerShell

Code Generation using C# Scripts (CSX Scripts) in Visual Studio

4.88/5 (19 votes)
7 Aug 2020CPOL6 min read 35.9K   288  
How to run C# Scripts (CSX scripts), load third-party libraries, and generate code directly from inside Visual Studio
In this article, you will see step by step instructions and scripts to run CSX scripts (C# Scripts). You will also learn how to invoke third party libraries by using Powershell scripts which can be invoked directly from Visual Studio.

Introduction

CSX scripts are very powerful because they can use C# and the full .NET Framework. In this article, I'll show step by step instructions and scripts to run CSX scripts (C# Scripts), with references to external assemblies (3rd party libraries, including NuGet packages), using Powershell scripts which can be invoked directly from Visual Studio. I'll also show some tricks to make the scripts work without modifications across your development team.
In the end, I'll use a simple code generation library to generate POCOs based on AdventureWorks database schema.

C# Scripts (CSX Files)

C# Script Files (CSX) were introduced with Roslyn and can be executed in Roslyn or other compatible cross-platform scripting engines like dotnet-script or even with C# REPL (called csi.exe).

Those scripting engines have some limitations (like lack of namespaces), but they allow us to virtually invoke any C# code, which gives us some amazing features like strong typing, compile-time checking, full IDE support (with debugging), cross-platform (dotnet core), full access to all .NET Framework (including not only SqlServer libraries but also some amazing third-party libraries like Dapper, Newtonsoft JSON, and others). So if we're talking about automation, we get a full-fledged language with a familiar syntax, instead of relying for example on PowerShell. And if we're talking about code generation we also get a full-fledged language with a familiar syntax, instead of relying on a templating-engine which would only offer a subset of features of the underlying language.

Sample CSX Script

CSX scripts inside Visual Studio have some support for Intellisense (auto-completion) and compile-time checks, but those features work much better in CS files. So it's a good idea to put as much as possible into cs files and as little as possible in CSX scripts. I like to use CSX only for basic things like loading libraries, setting connection strings, settings paths, and invoking the real code in CS files.

MyProgram.cs

C#
public class MyProgram
{
   public void MyMethod()
   {
      Console.WriteLine("Hello from MyMethod");
   }  
}

MyScript.csx

PowerShell
#load "MyProgram.cs" 

new MyProgram().MyMethod(); 
Console.WriteLine("Hello Code-Generation!");

Running CSX Script using C# REPL (CSI.EXE)

Visual Studio ships with a command-line REPL called CSI that can be used to run .csx scripts.

You can run CSI.EXE directly from Visual Studio Developer Command Prompt (csi MyScript.csx):

Image 1

Image 2

Assembly References

In the same sense that it's a good idea to use simple statements in CSX to invoke more complex CS code, it's also a good idea to load external assemblies when you can rely on existing libraries.

CSX allows loading assembly references by using the #r directive in the top of your scripts:

PowerShell
// CSI.EXE requires absolute paths for loading external assemblies: 
#r "C:\Users\drizin\.nuget\packages\dapper\2.0.35\lib\netstandard2.0\Dapper.dll" 

#load "File1.cs" 
#load "File2.cs" 
#load "MyProgram.cs" 

new MyProgram().MyMethod(); 
Console.WriteLine("Hello Code-Generation!");

NuGet Packages

If you need to reference a NuGet package, you can just rely on NuGet tools (and Visual Studio build process) to automatically restore the packages required by your script. For achieving that, you can just add the CSX as part of a Visual Studio project, so when each developer tries to build the project Visual Studio will download the missing packages, and the developer just needs to fix the assemblies location.

Invoking C# REPL (to run CSX scripts) from PowerShell

Although you can run CSI.exe directly from Visual Studio Developer Command Prompt, invoking it through PowerShell is very helpful for a few reasons:

  • You can run outside of Visual Studio. You don't even need Visual Studio to run CSX.
  • PowerShell allows us to reference external assemblies using relative paths (more about this below).

To invoke CSI using Powershell, we must know the location of csi.exe.

CSI is shipped with Visual Studio, but there are different possible folders for that according to your Visual Studio version. Even if you don't have Visual Studio, you can still install CSI by using the NuGet package Microsoft.Net.Compilers.Toolset.

So the first step is to search for csi.exe in multiple locations as I show in the sample Powershell script RunMyScript.ps1 below:

PowerShell
# Locate CSI.EXE by searching common paths 
$csi = (
  "$Env:userprofile\.nuget\packages\microsoft.net.compilers.toolset\3.6.0\tasks\net472\csi.exe", 
  "$Env:programfiles 
   (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Roslyn\csi.exe", 
  "$Env:programfiles 
   (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\Roslyn\csi.exe",
  "$Env:programfiles 
   (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\Roslyn\csi.exe",
  "$Env:programfiles 
   (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\Roslyn\csi.exe",
  "$Env:programfiles 
   (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\Roslyn\csi.exe",
  "$Env:programfiles 
   (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\Roslyn\csi.exe" 
) | Where-Object { Test-Path $_ } | Select-Object -first 1 

$dir = Split-Path $MyInvocation.MyCommand.Path 
$script = Join-Path $dir "MyScript.csx" 

& $csi $script

To launch the PowerShell script from the command line, it's just about running:

Powershell Full-Path-To-Your-Script-ps1 

Running from Visual Studio IDE

To run from Visual Studio, you can just add the PS1 to your project or solution, right-click the file, and click the option "Open with PowerShell ISE", which is the IDE for editing/running PowerShell scripts.

Image 3

Another alternative is that you can add new actions to your right-button actions -
you can click "Open With..." and configure PowerShell to be executed directly from Visual Studio:

Image 4

The list of possible actions will include this new option of invoking PS1 scripts directly from the IDE, and you can also set this as the default action for opening PS1 files.

Image 5

Allowing Unsigned Scripts

If you have never executed unsigned PowerShell scripts, you may have to enable PowerShell unsigned scripts by running PowerShell as Administrator (both the x64 version and in the x86 version which is what's executed from inside Visual Studio) and running this:

PowerShell
Set-ExecutionPolicy -ExecutionPolicy Unrestricted

Relative Assembly References

One of the major problems with CSI is that the #r directive (for loading assembly references) doesn’t accept NuGet-like references or environment variables so all assembly references should be specified with full paths. This is not a showstopper but it’s a little annoying since it makes it harder to share code among multiple developers since each developer would have to fix their references.

As we've seen before, CSX expects absolute references like this:

PowerShell
#r "C:\Users\drizin\.nuget\packages\dapper\2.0.35\lib\netstandard2.0\Dapper.dll"

One of the advantages of using PowerShell (as described above) is that we can use environment-variables and use #r directive with relative paths. In the PowerShell script, we just have to locate the base path where your assemblies are located and pass that to CSI so it can search for assemblies in this folder, like this:

PowerShell
$assemblies = "${env:userprofile}\.nuget\packages\";
& $csi /lib:"$assemblies" $script

And then in the CSX, you can use relative paths like this:

C#
#r "dapper\2.0.35\lib\netstandard2.0\Dapper.dll"

PackageReference (NuGet 4) vs packages.config (NuGet 3)

The new MSBuild format ("SDK-Style", which uses PackageReference inside the csproj) installs the NuGet packages in this per-user folder.

The old MSBuild format ("non-SDK-Style", before Visual Studio 2017, which uses packages.config) installs the NuGet packages in the "packages" folder under the Solution folder.

We can adjust our PowerShell scripts according to where our project will restore NuGet packages:

PowerShell
$csi = ... # (locate your csi.exe)
$dir = Split-Path $MyInvocation.MyCommand.Path 
$script = Join-Path $dir "MyScript.csx"

# Call csi.exe and specify that libraries referenced by #r directives 
# should search in a few nuget locations

# New NuGet 4.0+ (PackageReference) saves User-specific packages
# in "%userprofile%\.nuget\packages\"
$nuget1 = "${env:userprofile}\.nuget\packages\";

# New NuGet 4.0+ (PackageReference) saves Machine-wide packages 
# in "%ProgramFiles(x86)%\Microsoft SDKs\NuGetPackages\"
$nuget2 = "${env:ProgramFiles(x86)}\Microsoft SDKs\NuGetPackages\";

# Old NuGet (packages.config) saves packages in "\packages" folder at solution level.
# Locate by searching a few levels above
$nuget3 = ( 
    (Join-Path $dir ".\packages\"),
    (Join-Path $dir "..\packages\"),
    (Join-Path $dir "..\..\packages\"),
    (Join-Path $dir "..\..\..\packages\"),
    (Join-Path $dir "..\..\..\..\packages\")
) | Where-Object { Test-Path $_ } | Select-Object -first 1

# if you're using new NuGet format (PackageReference defined inside csproj) 
& $csi /lib:"$nuget1" $script  

# if you're using old NuGet format (packages.config)
# & $csi /lib:"$nuget3" $script  

And our CSX would use relative references:

C#
// CSX can load libraries by defining their relative paths

// New NuGets (PackageReference) are installed under "${env:userprofile}\.nuget\packages\" 
// or "${env:ProgramFiles(x86)}\Microsoft SDKs\NuGetPackages\")
// and have this format:
#r "dapper\2.0.35\lib\netstandard2.0\Dapper.dll"

// Old NuGets (packages.config) are installed under "(SolutionFolder)\packages"
// and have this format
// #r "Dapper.2.0.35\lib\netstandard2.0\Dapper.dll"

//...
new MyProgram().MyMethod();
Console.WriteLine("Hello Code-Generation!");

So cool and so easy, isn’t it?

Creating a Simple POCO Generator

This article up to now was cross-posted from this article in my blog. I'll skip some steps for brevity, but in this other post, I've created a tool to extract schemas from a SQL database and save it as a JSON file.

Based on this JSON schema and using CodegenCS code generator library, we can easily generate POCOs:

C#
public class SimplePOCOGenerator
{
  CodegenContext generatorContext = new CodegenContext();

  public void Generate()
  {
     DatabaseSchema schema = JsonConvert.DeserializeObject<DatabaseSchema>(
        File.ReadAllText(_inputJsonSchema));

     foreach (var table in schema.Tables)
        GeneratePOCO(table);

     // This saves one .cs for each table
     generatorContext.SaveFiles(outputFolder: targetFolder);

     // This will add each .cs to our csproj file (if using old format)
     //generatorContext.AddToProject(csProj, targetFolder);
  }

  void GeneratePOCO(DatabaseTable table)
  {
      var writer = generatorContext[table.TableName + ".cs"];
      writer
          .WriteLine(@"using System;")
          .WriteLine(@"using System.Collections.Generic;")
          .WriteLine(@"using System.ComponentModel.DataAnnotations;")
          .WriteLine(@"using System.ComponentModel.DataAnnotations.Schema;")
          .WriteLine(@"using System.Linq;")
          .WriteLine();

      writer.WithCBlock($"namespace {myNamespace}", () =>
      {
         writer.WithCBlock($"public partial class {table.TableName}", () =>
         {
             foreach (var column in table.Columns)
                GenerateProperty(writer, table, column);
         });
      });
  }

    void GenerateProperty(CodegenOutputFile writer, DatabaseTable table, 
                          DatabaseTableColumn column)
    {
        string propertyName = GetPropertyNameForDatabaseColumn(table, column.ColumnName);
        string typeDefinition = GetTypeDefinitionForDatabaseColumn(table, column);
        if (column.IsPrimaryKeyMember)
            writer.WriteLine("[Key]");
        if (propertyName.ToLower() != column.ColumnName.ToLower())
            writer.WriteLine($"[Column(\"{column.ColumnName}\")]");
        writer.WriteLine($"public {typeDefinition} {propertyName} {{ get; set; }}");
    }
}

The end result is one POCO for each table:

Image 6

And the POCO can be used by your favorite micro-ORM:

Image 7

So cool and so easy, isn’t it? I hope you enjoyed this article as much as I did!

The full source code for this article is available for download (at the top) and it's also published here.

Code contains CSX and PowerShell scripts both for SDK-style and non-SDK-style projects:

  • Visual Studio 2017+ (SDK-style project, which is also used by dotnetcore)
    In SDK-style format, the NuGet packages are stored per-user profile
  • Visual Studio <=2015 (non-SDK-style project)
    In non-SDK-style format, the NuGet packages are stored under the solution folder, and source files must be explicitly described in csproj.

The POCO generator code above was just a simplified version (for brevity), but in the attached sources, you'll find the complete code, which allows both generating the POCOs in multiple files or in a single file.

Disclaimer: I'm the author of the CodegenCS code generator library.

History

  • 16th July, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)