How to compile C# / VB code with string interpolation using PowerShell? - c#

I'm on Windows 10.0.18363.959 with PowerShell 5.1.18362.752
When trying to compile a C# or VB.NET code within PowerShell like this:
$Source = #'
Imports Microsoft.VisualBasic
Imports System
Public NotInheritable Class MainClass
Public Shared Sub Main()
Console.WriteLine($"{True}")
End Sub
End Class
'#
$vbType = Add-Type -TypeDefinition $Source `
-CodeDomProvider (New-Object Microsoft.VisualBasic.VBCodeProvider) `
-PassThru `
-ReferencedAssemblies "Microsoft.VisualBasic.dll", `
"System.dll" `
| where { $_.IsPublic }
[MainClass]::Main()
$Console = [System.Console]
$Console::WriteLine("Press any key to exit...")
$Console::ReadKey($true)
Exit(0)
I get a compiler error because the '$' character used for string interpolation:
C:\Users\Administrador\AppData\Local\Temp\oshdpbp1\oshdpbp1.0.vb(12) : >>> Console.WriteLine($"{1}")
I'm aware that I could use String.Format() function instead, but I would like to know whether this issue can be solved without modifying the original VB.NET code (which of course it compiles right on Visual Studio).
Note that string interpolation was added in VB14. Maybe I'm missing how to specify the right VB compiler version, I don't have idea how to do so with PowerShell.

The VBCodeProvider has a constructor with an IDictionary parameter which allows you to specify the compiler version.
This could work, but I can't test it right now:
$provOptions = [System.Collections.Generic.Dictionary[string,string]]::new()
$provOptions['CompilerVersion'] = 'v14.0'
$vbType = Add-Type -TypeDefinition $Source `
-CodeDomProvider (New-Object Microsoft.VisualBasic.VBCodeProvider $provOptions) `
-PassThru `
-ReferencedAssemblies "Microsoft.VisualBasic.dll", `
"System.dll" `
| where { $_.IsPublic }

Related

Powershell and .NET5.0 fails loading the assembly

Background
Using visual studio 2019 v16.10.3 I have created this (standard MS example source) class library and successfully compiled into MyMathLib.DLL
using System;
namespace MyMathLib
{
public class Methods
{
public Methods()
{
}
public static int Sum(int a, int b)
{
return a + b;
}
public int Product(int a, int b)
{
return a * b;
}
}
}
My MyMathLib.csproj file looks like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
And then I want to try executing this code using powershell using the following script:
[string]$assemblyPath='S:\Sources\ResearchHans\DotNetFromPowerShell\MyMathLib\MyMathLib\bin\Debug\net5.0\MyMathLib.dll'
Add-Type -Path $assemblyPath
When I run this I get an error, and ask what's going on by querying the error for LoaderExceptions:
PS C:\WINDOWS\system32> S:\Sources\ResearchHans\DotNetFromPowerShell\TestDirectCSharpScript2.ps1
Add-Type : Kan een of meer van de gevraagde typen niet laden. Haal de LoaderExceptions-eigenschap op voor meer informatie.
At S:\Sources\ResearchHans\DotNetFromPowerShell\TestDirectCSharpScript2.ps1:2 char:1
+ Add-Type -Path $assemblyPath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Add-Type], ReflectionTypeLoadException
+ FullyQualifiedErrorId : System.Reflection.ReflectionTypeLoadException,Microsoft.PowerShell.Commands.AddTypeCommand
PS C:\WINDOWS\system32> $Error[0].Exception.LoaderExceptions
Kan bestand of assembly System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a of een van de afhankelijkhed
en hiervan niet laden. Het systeem kan het opgegeven bestand niet vinden.
PS C:\WINDOWS\system32>
(Sorry for dutch windows messages: something like "Cannot load one or more of the types" and the loader exception Cannot load file or assembly "System.Runtime" etc...
I have seen many related issues on SO, but they are usually quite old and do not refer to .NET5.0
This works
When I directly compile the example source like below, it works just fine.
$code = #"
using System;
namespace MyNameSpace
{
public class Responder
{
public static void StaticRespond()
{
Console.WriteLine("Static Response");
}
public void Respond()
{
Console.WriteLine("Instance Respond");
}
}
}
"#
# Check the type has not been previously added within the session, otherwise an exception is raised
if (-not ([System.Management.Automation.PSTypeName]'MyNameSpace.Responder').Type)
{
Add-Type -TypeDefinition $code -Language CSharp;
}
[MyNameSpace.Responder]::StaticRespond();
$instance = New-Object MyNameSpace.Responder;
$instance.Respond();
Why?
The whole Idea to have a proof of concept that I can utilize my .NET5 libraries using e.g. powershell scripts. I actually have to load a much more complicated assembly, but this oversimplified example seems to be my primary issue at the moment.
The question
What am I missing? - How do I get this to work?
TIA for bearing with me.
You didn't state what version of Powershell that you're using. However, I can reproduce the error by the following.
In VS 2019, create a Class Library (C# Windows Library).
Rename the class from "Class1.cs" to "Methods.cs"
Methods.cs
using System;
namespace MyMathLib
{
public class Methods
{
public int Sum(int a, int b)
{
return a + b;
}
public int Product(int a, int b)
{
return a * b;
}
}
}
Compile.
Open Windows Powershell
Type: (Get-Host).version
Then type the following:
PS C:\Users\Test> [string]$assemblyPath="C:\Temp\MyMathLib.dll"
PS C:\Users\Test> Add-Type -Path $assemblyPath
which results in the following error:
Add-Type : Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.
At line:1 char:1
+ Add-Type -Path $assemblyPath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Add-Type], ReflectionTypeLoadException
+ FullyQualifiedErrorId : System.Reflection.ReflectionTypeLoadException,Microsoft.PowerShell.Commands.AddTypeCommand
This occurs because .NETCore 5 isn't supported by Windows Powershell 5.1 - only on versions 6 and above. For Powershell 5.1 or below, it's necessary to use .NETFramework instead.
See Resolving PowerShell module assembly dependency conflicts which states the following:
PowerShell and .NET
... In general, PowerShell 5.1 and below run on .NET Framework, while PowerShell 6 and above run on .NET Core. These two
implementations of .NET load and handle assemblies differently.
See Installing PowerShell on Windows for how to get the latest version of Powershell.
In Powershell 7.1.3, one can call a method from MyMathLib.dll by doing the following:
PS C:\Users\Test> [string]$assemblyPath="C:\Temp\MyMathLib.dll"
PS C:\Users\Test> Add-Type -Path $assemblyPath
PS C:\Users\Test> $m1 = New-Object -TypeName MyMathLib.Methods
PS C:\Users\Test> $m1.Sum(2,3)
Note: Since the class/methods aren't static, it's necessary to create an instance by using $m1 = New-Object -TypeName MyMathLib.Methods
However, if it's static (as shown below):
MyMathLib.cs
using System;
namespace MyMathLib
{
public static class Methods
{
public static int Sum(int a, int b)
{
return a + b;
}
public static int Product(int a, int b)
{
return a * b;
}
}
}
Then one can do the following in Powershell:
PS C:\Users\Test> [string]$assemblyPath="C:\Temp\MyMathLib.dll"
PS C:\Users\Test> Add-Type -Path $assemblyPath
PS C:\Users\Test> [MyMathLib.Methods]::Sum(3,2)

How can i get a Windows.System.User in PowerShell that I can use with the GetForUser() method of Windows.UI.Notifications.ToastNotificationManager?

I have been creating a powershell script to display toast notifications, this code works but there is one method on the toastnotification object I dont understand how to use:
$Load = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$Load = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($App).Show($ToastXml)
Looking at the [Windows.UI.Notifications.ToastNotificationManager] object there is one method named "GetForUser()"
https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotificationmanager.getforuser?view=winrt-19041
This method needs a Windows.System.User object as input.
https://learn.microsoft.com/en-us/uwp/api/windows.system.user?view=winrt-19041
I have tried the following code
$Load = [Windows.System.User, Windows.System, ContentType = WindowsRuntime]
$users = [Windows.System.User]::FindAllAsync()
$users is then a "System.__ComObject" without any methods.
So the question is, how can i get a Windows.System.User in PowerShell that I can use with the GetForUser() method of Windows.UI.Notifications.ToastNotificationManager?
I have also tried managed code
$code = #"
using Windows.System;
namespace CUser
{
public static class GetUsers{
public static void Main(){
IReadOnlyList<User> users = await User.FindAllAsync(UserType.LocalUser, UserAuthenticationStatus.LocallyAuthenticated);
User user = users.FirstOrDefault();
}
}
}
"#
Add-Type -TypeDefinition $code -Language CSharp
But that gives the error:
"The type or namespace name 'System' does not exist in the namespace 'Windows' (are
you missing an assembly reference?)"
I am not sure what assembly or dll contains the "Windows.System" reference.
I was searching for a similar problem with DeviceInformation and ran across your question. The solution turned out to be in this blog post https://fleexlab.blogspot.com/2018/02/using-winrts-iasyncoperation-in.html
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
Function Await($WinRtTask, $ResultType) {
$asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
$netTask = $asTask.Invoke($null, #($WinRtTask))
$netTask.Wait(-1) | Out-Null
$netTask.Result
}
You can then run FindAllAsync() like this
$windowsSystemUserClass = [Windows.System.User, Windows.System, ContentType = WindowsRuntime]
$users = Await ([Windows.System.User]::FindAllAsync()) ([System.Collections.Generic.IReadOnlyList`1[Windows.System.User]])

How to combine simple .cs files into one .cs file with all usings at the top so it compiles

So I am aware that in general, this is not possible because Jon Skeet said so.
But my .cs files are simple classes with one or two usings at the top. And I need one file with all the classes in it so I can paste it into a web browser IDE as a single file to compile and run.
I tried using PowerShell, and just copy all files into one like this:
get-content *.cs | out-file bigBadFile.cs
But this file will not compile because it has usings in the middle, which isn't allowed:
CS1529: A using clause must precede all other elements defined in the namespace except extern alias declarations
If you are curious why I need that - it's for the CodinGame platform, and I'm sick of keeping all my code in a single file.
Sample files to merge:
GameData.cs:
using System.Collections.Generic;
public class GameData
{
public GameData(int width, int height)
{
...
}
public int Width { get; set; }
public int Height { get; set; }
public List<int> List { get; set; }
...
}
Player.cs:
using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
public class Player
{
private static string[] inputs;
private static bool isFirstTurn = true;
public static StringBuilder MoveBuilder;
public static GameData Game;
static void Main()
{
...
}
}
Just to offer a more concise and faster PSv4+ alternative to your own helpful answer:
$usings, $rest = (Get-Content *.cs).Where({ $_ -match '^\s*using\s' }, 'Split')
# Encoding note: Creates a BOM-less UTF-8 file in PowerShell [Core] 6+,
# and an ANSI file in Windows PowerShell. Use -Encoding as needed.
Set-Content bigBadFile.txt -Value (#($usings | Select-Object -Unique) + $rest)
Finally I managed to do this. Those PowerShell commands worked for me:
get-content *.cs | where { $_ -match "^using" } | Select-Object -Unique | out-file bigBadFile.txt
get-content *.cs | where { $_ -notmatch "^using" } | out-file -append bigBadFile.txt
So what I do here is I take all the usings from all files and put them into bigBadFile.txt. Then I take all code without usings from all files, and append it to the bigBadFile.txt
The result is working for me, even though it has duplicated using statements. I've added | Select-Object -Unique as Theo suggested in his comment to avoid usings duplication.
After -match the code inside braces "^using" is just a regular expression, so if your usings have spaces before them in .cs files (which is unusual, you can just change this to "^[ ]*using".
This is a project doing specifically what you need:
https://github.com/nima-ghomri/SingleCS
Just add your cs files as arguments and it combines all of them:
SingleCS.exe "MyProject\**\*.cs" "D:\Helpers\Utils.cs"
Download:
https://github.com/nima-ghomri/SingleCS/releases/latest

Loading C# DLL with powershell, [System.Type]::GetType returns null

I have a simple DotNet DLL like this
namespace ClassLibrary1
{
public class Class1
{
public static void Test()
{
Process.Start("CMD.exe", "/C calc");
}
}
}
When I try to load this DLL with powershell
$Path = "c:\\test\\ClassLibrary1.dll";
$Namespace = "ClassLibrary1";
$ClassName = "Class1";
$Method = "Test";
$Arguments = $null
$Full_Path = [System.IO.Path]::GetFullPath($Path);
$AssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName($Full_Path)
$Full_Class_Name = "$Namespace.$ClassName"
$Type_Name = "$Full_Class_Name, $($AssemblyName.FullName)"
$Type = [System.Type]::GetType($Type_Name)
$MethodInfo = $Type.GetMethod($Method)
$MethodInfo.Invoke($null, $Arguments)
It does not work, because [System.Type]::GetType($Type_Name) returned $null
Any ideas?
Use Add-Type -Path to load your assembly.
After loading, to get a reference to that assembly's [ClassLibrary1.Class1] type as a [Type] instance (to use for reflection) via a string variable, simply cast to [Type].
The following corrected and annotated version of your code demonstrates the approach:
# Compile the C# source code to assembly .\ClassLibrary1.dll
Add-Type -TypeDefinition #'
namespace ClassLibrary1
{
public class Class1
{
public static void Test()
{
System.Diagnostics.Process.Start("CMD.exe", "/C calc");
}
}
}
'# -OutputAssembly .\ClassLibrary1.dll
# Define the path to the assembly. Do NOT use "\\" as the path separator.
# PowerShell doesn't use "\" as the escape character.
$Path = ".\ClassLibrary1.dll"
$Namespace = "ClassLibrary1"
$ClassName = "Class1"
$Method = "Test"
$Arguments = $null
# Load the assembly by its filesystem path, using the Add-Type cmdlet.
# Use of relative paths works.
Add-Type -Path $Path
$Full_Class_Name = "$Namespace.$ClassName"
# To get type [ClassLibrary1.Class1] by its full name as a string,
# simply cast to [type]
$Type = [type] $Full_Class_Name
$MethodInfo = $Type.GetMethod($Method)
$MethodInfo.Invoke($null, $Arguments)
As for what you tried:
As PetSerAl points out, [System.Type]::GetType() can only find a given type if:
its assembly is already loaded
its assembly can be located via standard assembly resolution, as detailed here, which involves many complicated rules, the only two of which likely apply to your scenario is if you're trying to reference a built-in type from the mscorlib assembly, or an assembly located in the GAC (Global Assembly Cache).
By definition, only strongly-named assemblies - those signed with a private key matching the public key mentioned in the assembly's full name - can be placed in the GAC.
While you could have called [System.Reflection.Assembly]::LoadFile($Full_Path) first in order to load the assembly via its filesystem path, after which [System.Type]::GetType($Type_Name) would have succeeded, it is ultimately simpler - and more PowerShell-idiomatic - to use Add-Type -Path to load the assembly (which has the added advantage of not requiring a full (absolute) file path), and, once loaded, to use [Type] with just the type's full name (no reference to the assembly needed anymore).

Using `Add-Type` within a Function. Memory Leak?

If the Add-Type function is called to add a snippet of C# code within a function, will the "type" be garbage-collected once the function has finished executing? Or will the type still exist in memory because it was part of a (virtual) assembly that was loaded into the AppDomain? (Thus being a "memory leak")
In the below Lock-WorkStation definition, Add-Type is used to add a code written in C#:
function Lock-WorkStation {
$signature = #'
[DllImport("user32.dll", SetLastError = true)]
public static extern bool LockWorkStation();
'#
$name = "Win32LockWorkStation"
$LockWorkStation = Add-Type -MemberDefinition $signature -Name $name -Namespace Win32Functions -PassThru
$LockWorkStation::LockWorkStation() | Out-Null
}
My question is, if this function is called multiple types, then it is constantly parsing and adding the C# code as a (virtual) assembly. (Or is it garbage-collected?)
Is it better to write it as:
Function Lock-WorkStation {
if (!$Global:LockWorkStation) {
$signature = #'
[DllImport("user32.dll", SetLastError = true)]
public static extern bool LockWorkStation();
'#
$name = "Win32LockWorkStation"
$global:LockWorkStation = Add-Type -MemberDefinition $signature -Name $name -Namespace Win32Functions -PassThru
}
$global:LockWorkStation::LockWorkStation() | Out-Null
}
So that the C# code is only instantiated as an assembly once?
Test if the resulting type already exists:
function Lock-WorkStation {
$name = 'Win32LockWorkStation'
$namespace = 'Win32Functions'
$signature = #'
[DllImport("user32.dll", SetLastError = true)]
public static extern bool LockWorkStation();
'#
if(-not ($LockWorkStation = "$namespace.$name" -as [type])){
$LockWorkStation = Add-Type -MemberDefinition $signature -Name $name -Namespace $namespace -PassThru
}
$LockWorkStation::LockWorkStation() | Out-Null
}
Thank you Mathias for your answer. My question, however, was about whether calling Add-Type instantiates the type each time it is called. I looked at the documentation, which says:
The type name and namespace must be unique within a session. You cannot unload a type or change it. If you need to change the code for a type, you must change the name or start a new Windows PowerShell session. Otherwise, the command fails. (Add-Type Documentation)
Add-Type will instantiate the type the first time it is called. On subsequent calls with the same arguments, because the type already exists, it will do nothing. Therefore, the only reason to include an if condition over the Add-Type statement, is for efficiency reasons, because a call to Add-Type has the overhead of checking if the type already exists.
Your if condition:
-not ($LockWorkStation = "$namespace.$name" -as [type])
IMHO, is not efficient because it uses string interpolation to compute the type name, then it checks if the type exists, then it assigns the type to a variable, each time it runs.
Instead, you can set the type variable once (the scope being Script:). The if statement can check if it is null, and if so, instantiate it:
function Lock-WorkStation {
if ($Script:Win32LockWorkStation -eq $null) {
$namespace = 'Win32Functions'
$name = 'Win32LockWorkStation'
$signature = #'
[DllImport("user32.dll", SetLastError = true)]
public static extern bool LockWorkStation();
'#
$Script:Win32LockWorkStation = Add-Type -Namespace $namespace -Name $name -MemberDefinition $signature -PassThru
}
$Script:Win32LockWorkStation::LockWorkStation() | Out-Null
}
If you wish, you can check my performance comparison of different ways of using Add-Type within a function.

Categories