I am working with the IO.Compression namespace in PowerShell and am running into an issue where Methods which are available in my C# code are not available in PowerShell
C# snippet
string archfile = #"c:\temp\a1.zip";
string source = #"C:\temp\testing\logs\BatchProcess\BatchProcess_2017_08_22.log";
using (ZipArchive archive = ZipFile.Open(archfile, ZipArchiveMode.Update))
{
archive.CreateEntryFromFile(source, #"myfolder\folder2\file.log");
}
Similar start of code in PowerShell
Add-Type -assembly System.IO.Compression.FileSystem
$archfile = "c:\temp\a1.zip"
# open mode: 1: Create, 2: Update
$archive = [System.IO.Compression.ZipFile]::Open("C:\temp\2.zip", 2)
$archive.CreateEntryFromFile($source, "myfolder\folder2\file.log")
A review of members shows that the function CreateEntryFromFile() (among others) is missing. The variable is a type of ZipArchive and a new file is created.
$archive | Get-Member displays:
TypeName: System.IO.Compression.ZipArchive
Name MemberType Definition
---- ---------- ----------
CreateEntry Method System.IO.Compression.ZipArchiveEntry CreateEntry(string entryName), System.IO.Compression.ZipArchiveE...
Dispose Method void Dispose(), void IDisposable.Dispose()
Equals Method bool Equals(System.Object obj)
GetEntry Method System.IO.Compression.ZipArchiveEntry GetEntry(string entryName)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Entries Property System.Collections.ObjectModel.ReadOnlyCollection[System.IO.Compression.ZipArchiveEntry] Entries {get;}
Mode Property System.IO.Compression.ZipArchiveMode Mode {get;}
Is this simply part of how PowerShell creates .NET objects where not all methods are (or can be) supported?
ZipArchive reference: https://msdn.microsoft.com/en-us/library/system.io.compression.ziparchive(v=vs.110).aspx
CreateEntryFromFile is an extension method, so it doesn't appear on the ZipArchive class in Powershell.
Option 1
In Powershell 3.0 or higher, you can declare the extension method for use in Powershell.
Add-Type -AssemblyName System.IO.Compression.FileSystem
Update-TypeData -TypeName System.IO.Compression.ZipArchive -MemberType
ScriptMethod -MemberName CreateEntryFromFile -Value {
switch ($args.Count)
{
2 { [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($this, $args[0], $args[1]) }
3 { [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($this, $args[0], $args[1], $args[2]) }
default { throw "No overload for CreateEntryFromFile takes the specified number of parameters." }
}
}
$archfile = "c:\temp\a1.zip"
# open mode: 1: Create, 2: Update
$archive = [System.IO.Compression.ZipFile]::Open("C:\temp\2.zip", 2)
$archive.CreateEntryFromFile($source, "myfolder\folder2\file.log")
Option 2
Just use the extension method as a plain old static method.
$archfile = "c:\temp\a1.zip"
# open mode: 1: Create, 2: Update
$archive = [System.IO.Compression.ZipFile]::Open("C:\temp\2.zip", 2)
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($archive, $source, "myfolder\folder2\file.log")
The second option is less code and also works in older versions of Powershell.
Reference: How do I use extension methods in ZipFileExtensionsClass?
Related
This is in continuation to this Question here, I have a PowerShell command which I have created and am able to call the command in a PowerShell window, but when trying to call from C# method, I am getting error as the cmdlet is not recognized, I tried with other existing commands and get same error, so I suspect issue in Importing the Module, though I don't get that error in streams. Error. The only error I get is "Get-RowAndPartitionKey is not a recognized cmndlt, please check the spelling.....".
Would like to know if there is any other way, I should try it or if I can debug more here to see if my Module fetches all command or not. right now I am clueless how to fix this.
public string RunScript( string contentScript, Dictionary<string, EntityProperty> parameters )
{
List<string> parameterList = new List<string>();
foreach( var item in parameters )
{
parameterList.Add( item.Value.ToString() );
}
using( PowerShell ps = PowerShell.Create() )
{
IAsyncResult async =
ps.AddCommand( "Import-Module" ).AddArgument( #"C:\Users\...\.D.PowerShell.dll" )
.AddStatement()
.AddCommand( "Get-RowAndPartitionKey" ).AddParameter( "Properties", "test" )
.BeginInvoke();
StringBuilder stringBuilder = new StringBuilder();
foreach( PSObject result in ps.EndInvoke( async ) )
{
stringBuilder.AppendLine( result.ToString() );
}
return stringBuilder.ToString();
}
}
}
Below method do not return any error in Streams.Error or Verbose but no output also:
public async Task<IEnumerable<object>> RunScript( string scriptContents, List<string> scriptParameters )
{
// create a new hosted PowerShell instance using the default runspace.
// wrap in a using statement to ensure resources are cleaned up.
using( PowerShell ps = PowerShell.Create() )
{
// specify the script code to run.
ps.AddScript( scriptContents );
// specify the parameters to pass into the script.
ps.AddParameter( "Properties" ,"test") ;
// execute the script and await the result.
var pipelineObjects = await ps.InvokeAsync().ConfigureAwait( false );
return pipelineObjects;
}
}
scriptContent
"\"$path = 'C:\\Users...\\.TabularData.PowerShell.dll'\\r\\nImport-Module $path\\r\\nGet-RowAndPartitionKeys\""
The following is self-contained PowerShell sample code that uses on-demand compilation of C# code:
It shows that the approach works in principle, as described in this answer to your original question.
Prerequisites: The PowerShell SDK package and .NET runtime used in the C# project that calls your custom Get-RowAndPartitionKey" cmdlet must be compatible with the PowerShell SDK and .NET runtime that you used to compile the assembly DLL that houses that cmdlet, to be imported via Import-Module.
The sample code below ensures that implicitly, by running directly from PowerShell, using the Add-Type cmdlet to compile C# code on demand - it works in Windows PowerShell as well as in PowerShell (Core) 7+.
In practice I've found that a .NET Framework-compiled DLL (from Windows PowerShell) also works in PowerShell (Core) (.NET (Core) 5.0), but not vice versa.
It shows troubleshooting techniques, namely:
Adding the -Verbose switch to the Import-Module call to produce verbose output that lists the commands being imported from the given module (DLL).
Printing these verbose messages (look for // --- TROUBLESHOOTING CODE)
Printing any non-terminating PowerShell errors that occurred (as opposed to exceptions that you'd have to handle in C# code).
# Create a (temporary) assembly containing cmdlet "Get-RowAndPartitionKey".
# This assembly can directly be imported as a module from PowerShell.
# The cmdlet simply outputs "Hi from Get-RowAndPartitionKey" and
# echoes the elements of the list passed to -Properties, one by one.
$tempModuleDll = Join-Path ([IO.Path]::GetTempPath()) "TempModule_$PID.dll"
Remove-Item -ErrorAction Ignore $tempModuleDll
Add-Type #'
using System.Management.Automation;
using System.Collections.Generic;
[Cmdlet("Get", "RowAndPartitionKey")]
public class GetRowAndPartitionKeyCmdlet : PSCmdlet {
[Parameter] public List<string> Properties { get; set; }
protected override void ProcessRecord() {
WriteObject("Hi from Get-RowAndPartitionKey: ");
WriteObject(Properties, true);
}
}
'# -ErrorAction Stop -OutputAssembly $tempModuleDll
# Compile a C# class ad hoc to simulate your project, and call its static
# method, which imports the module and effectively calls
# Get-RowAndPartitionKey -Properties "foo", "bar"
(Add-Type #"
using System;
using System.Management.Automation;
using System.Collections.Generic;
using System.Text;
public static class Foo {
public static string RunScript(List<string> parameterList)
{
using (System.Management.Automation.PowerShell ps = PowerShell.Create())
{
IAsyncResult async =
// Add -Verbose to the Import-Module call, so that the list of
// commands being imported is written to the verbose output stream.
ps.AddCommand("Import-Module").AddArgument(#"$tempModuleDll").AddParameter("Verbose", true)
.AddStatement()
.AddCommand("Get-RowAndPartitionKey").AddParameter("Properties", parameterList)
.BeginInvoke();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject result in ps.EndInvoke(async))
{
stringBuilder.AppendLine(result.ToString());
}
// --- TROUBLESHOOTING CODE
// Print verbose output from the Import-Module call
foreach (var v in ps.Streams.Verbose) { Console.WriteLine("VERBOSE: " + v.ToString()); }
// Print any errors.
foreach (var e in ps.Streams.Error) { Console.WriteLine("ERROR: " + e.ToString()); }
// ---
return stringBuilder.ToString();
}
}
}
"# -ErrorAction Stop -PassThru)::RunScript(("foo", "bar"))
# Clean-up instructions:
if ($env:OS -eq 'Windows_NT') {
Write-Verbose -vb "NOTE: Re-running this code requires you to start a NEW SESSION."
Write-Verbose -vb "After exiting this session, you can delete the temporary module DLL(s) with:`n`n Remove-Item $($tempModuleDll -replace '_.+', '_*.dll')`n "
} else {
Write-Verbose -vb "NOTE: Re-running this code after modifying the embedded C# code requires you to start a NEW SESSION."
Remove-Item $tempModuleDll
}
On my Windows 10 machine, both from PowerShell (Core) 7.0.5 and Windows PowerShell 5.1, the above yields (clean-up instructions omitted) the following, showing that everything worked as intended:
VERBOSE: Loading module from path 'C:\Users\jdoe\AppData\Local\Temp\TempModule_11876.dll'.
VERBOSE: Importing cmdlet 'Get-RowAndPartitionKey'.
Hi from Get-RowAndPartitionKey:
foo
bar
Specifically, line VERBOSE: Importing cmdlet 'Get-RowAndPartitionKey'. indicates that the custom cmdlet was successfully imported into the session.
I'm using the following code to retrieve a specific address book from outlook:
$outlook = $(New-Object -ComObject Outlook.Application)
$Session = $outlook.Session
$Session.Logon()
$ab = $Session.AddressLists | ? {$_.Name -eq 'test'}
then attempting to use C# to convert the $ab object to .net using the following:
$source = #"
using Microsoft.Office.Interop.Outlook;
public unsafe static class OAB
{
public static void List(object Stream)
{
var i = Stream as Microsoft.Office.Interop.Outlook.AddressList;
return i;
}
}
"#
$cp = new-object System.CodeDom.Compiler.CompilerParameters
$cp.CompilerOptions = "/unsafe"
$cp.ReferencedAssemblies.Add('C:\wkdir\refassem\Microsoft.Office.Interop.Outlook.dll')
Add-Type -TypeDefinition $source -CompilerParameters $cp
The error I receive is:
Since 'OAB.List(object)' returns void, a return keyword must not be followed by an object expression
Disclaimer, I'm a complete newbie to C# so go easy on me!
You have your List function declared as a return type of void but you are returning a value. To fix, just change your function to this:
public static Microsoft.Office.Interop.Outlook.AddressList List(object Stream)
void functions mean that no value is going to be returns. If you do plan on returning a value, then you need to remove void and replace it with the type you want to return.
I am using the following code under ASP.NET 4.0 framework to obtain the version of MSI file from a web app:
string strVersion = "";
try
{
Type InstallerType;
WindowsInstaller.Installer installer;
InstallerType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
installer = (WindowsInstaller.Installer)Activator.CreateInstance(InstallerType);
WindowsInstaller.Database db = installer.OpenDatabase(strMSIFilePath, 0);
WindowsInstaller.View dv = db.OpenView("SELECT `Value` FROM `Property` WHERE `Property`='ProductVersion'");
WindowsInstaller.Record record = null;
dv.Execute(record);
record = dv.Fetch();
strVersion = record.get_StringData(1).ToString();
dv.Close();
//db.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(dv);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(db);
}
catch
{
//Failed
strVersion = "";
}
It works fine except that when the code finishes running it holds an internal MSI file handle so when I try to move or rename the MSI file I get the error that the file is still in use. This continues until I actually navigate away from the ASPX page that calls the method above.
My question is, I obviously didn't close some handle or object in the code above. But what could that be?
PS. I'm testing it in a development IDE from VS2010.
EDIT: Edited the code like it should be after Adriano's suggestion. Thanks!
The COM object has not been released (it should be auto-released when it goes out of scope but in .NET this doesn't work really well). Because it does not implement the IDisposable interface you can't call its Dispose() method and you can't use it inside an using statement. You have to explicitly call Marshal.FinalReleaseComObject. For example:
try
{
// Your stuffs
}
finally
{
dv.Close();
Marshal.FinalReleaseComObject(dv);
Marshal.FinalReleaseComObject(db);
}
Moreover note that you do not really need a call to the Commit() method because you didn't make any change but just a query.
FWIW, you should be using Windows Installer XML (WiX) Deployment Tools Foundation (DTF). It's an FOSS project from Microsoft that can be found on CodePlex. It has MSI interop libraries with classes that are very similar to the COM classes but implement IDisosable and use P/Invoke instead of COM behind the scenes. There is even support for Linq to MSI if you want. And the full source code is available.
DTF is the gold standard for MSI interop in a .NET world. Here are two examples:
using System;
using System.Linq;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Deployment.WindowsInstaller.Linq;
namespace ConsoleApplication3
{
class Program
{
const string DATABASE_PATH = #"C:\FOO..MSI";
const string SQL_SELECT_PRODUCTVERSION = "SELECT `Value` FROM `Property` WHERE `Property`='ProductVersion'";
static void Main(string[] args)
{
using (Database database = new Database(DATABASE_PATH, DatabaseOpenMode.ReadOnly))
{
Console.WriteLine(database.ExecuteScalar(SQL_SELECT_PRODUCTVERSION).ToString());
}
using (QDatabase database = new QDatabase(DATABASE_PATH, DatabaseOpenMode.ReadOnly))
{
var results = from property in database.Properties where property.Property == "ProductVersion" select property.Value;
Console.WriteLine(results.AsEnumerable<string>().First());
}
}
}
}
try to Dispose the Objects.
dv.Dispose();
db.Dispose();
I have written an Extension Method off of DataGridView called HideColumns.
public static class Extensions
{
public static void HideColumns(this DataGridView dataGridView, params string[] columnNames)
{
foreach (string str in columnNames)
{
if (dataGridView.Columns[str] != null)
{
dataGridView.Columns[str].Visible = false;
}
}
}
}
I pass my grid into an IronRuby script as a variable called main_grid
When my script calls
main_grid.HideColumns("FirstName","LastName")
the script blows up with Error in Script
undefined method 'HideColumns' for System.Windows.Forms.DataGridView:System::Windows::Forms::DataGridView
The extension methods seem to work okay from C#. What gives?
FWIW, IronRuby 1.1 (needs .net 4) provides the using_clr_extensions method -- as noted in the release notes this activates all extension methods defined on classes defined in a given namespace, regardless of the assembly they are defined in; assemblies loaded in the future that define extension methods in the activated namespace will automatically appear on the correct types, like this:
load_assembly "System.Core"
using_clr_extensions System::Linq
# ...
products.
where(lambda { |p| p.units_in_stock == 0 }).
each { |x| puts x.product_name }
The release notes also point at a whole set of examples at http://github.com/ironruby/ironruby/blob/master/Languages/Ruby/Samples/Linq/101samples.rb
The extension method is just syntatic sugar, you will need to call it as:
Extensions.HideColumns(main_grid, "FirstName", "LastName")
alternatively create a new class in C# which derives from DataGridView and add the method:
public class DataGridViewExt : DataGridView
{
public void HideColumns(params string[] columnNames)
{
foreach (string str in columnNames)
{
if (this.Columns[str] != null)
{
this.Columns[str].Visible = false;
}
}
}
}
and use this class rather than the System.Windows.Forms class on your form.
Since you mentioned it in the comments to JDunkeryly's answer, here's how you'd extend the grid from the ruby side. Just open the class and add a method (only works from the ruby side).
class System::Windows::Forms::DataGridView
def hide_columns(*columnNames)
column_names.each do |cn|
self.columns[cn].visible = false
end
end
end
As far as the suggestion to use the extension method directly, the params keyword is painful to IronRuby. You need to build a typed array with your arguments and pass it. You can't just wrap your ruby strings in a ruby array. I've pulled this off earlier today in a blog post. But if you have a smoother way to handle that, please let me know.
BACKGROUND
I am using Powershell 2.0 on Windows 7.
I am writing a cmdlet in a Powershell module ("module" is new to Powershell 2.0).
To test the cmdlet I am writing Unit tests in Visual Studio 2008 that programmatically invoke the cmdlet.
REFERENCE
This Article on MSDN called "How to Invoke a Cmdlet from Within a Cmdlet" shows how to call a cmdlet from C#.
THE SOURCE CODE
This is a distilled version of my actual code — I've made it as small as possible so that you can see the problem I am having clearly:
using System;
using System.Management.Automation;
namespace DemoCmdLet1
{
class Program
{
static void Main(string[] args)
{
var cmd = new GetColorsCommand();
foreach ( var i in cmd.Invoke<string>())
{
Console.WriteLine("- " + i );
}
}
}
[Cmdlet("Get", "Colors")]
public class GetColorsCommand : Cmdlet
{
protected override void ProcessRecord()
{
this.WriteObject("Hello");
this.WriteVerbose("World");
}
}
}
COMMENTS
I understand how to enable and capture verbose output from the Powershell command line; that's not the problem.
In this case I am programmatically invoking the cmdlet from C#.
Nothing I've found addresses my specific scenario. Some articles suggest I should implement my own PSHost, but seems expensive and also it seems like a have to call the cmdlet as text, which I would like to avoid because that is not as strongly typed.
UPDATE ON 2009-07-20
Here is is the source code based on the answer below.
Some things are still not clear to me:
* How to call the "Get-Colors" cmdlet (ideally without having to pass it as a string to the ps objet)
* How to get the verbose output as it is generated instead of getting an collection of them at the end.
using System;
using System.Management.Automation;
namespace DemoCmdLet1
{
class Program
{
static void Main(string[] args)
{
var ps = System.Management.Automation.PowerShell.Create();
ps.Commands.AddScript("$verbosepreference='continue'; write-verbose 42");
foreach ( var i in ps.Invoke<string>())
{
Console.WriteLine("normal output: {0}" , i );
}
foreach (var i in ps.Streams.Verbose)
{
Console.WriteLine("verbose output: {0}" , i);
}
}
}
[Cmdlet("Get", "Colors")]
public class GetColorsCommand : Cmdlet
{
protected override void ProcessRecord()
{
this.WriteObject("Red");
this.WriteVerbose("r");
this.WriteObject("Green");
this.WriteVerbose("g");
this.WriteObject("Blue");
this.WriteVerbose("b");
}
}
}
The code above generates this output:
d:\DemoCmdLet1\DemoCmdLet1>bin\Debug\DemoCmdLet1.exe
verbose output: 42
UPDATE ON 2010-01-16
by using the Powershell class (found in System.Management.Automation but only in the version of the assembly that comes with the powershell 2.0 SDK, not what comes out-of-the-box on Windows 7) I can programmatically call the cmdlet and get the verbose output. The remaining part is to actually add a custom cmdlet to that powershell instance - because that was my original goal - to unit test my cmdlets not those that come with powershell.
class Program
{
static void Main(string[] args)
{
var ps = System.Management.Automation.PowerShell.Create();
ps.AddCommand("Get-Process");
ps.AddParameter("Verbose");
ps.Streams.Verbose.DataAdded += Verbose_DataAdded;
foreach (PSObject result in ps.Invoke())
{
Console.WriteLine(
"output: {0,-24}{1}",
result.Members["ProcessName"].Value,
result.Members["Id"].Value);
}
Console.ReadKey();
}
static void Verbose_DataAdded(object sender, DataAddedEventArgs e)
{
Console.WriteLine( "verbose output: {0}", e.Index);
}
}
[Cmdlet("Get", "Colors")]
public class GetColorsCommand : Cmdlet
{
protected override void ProcessRecord()
{
this.WriteObject("Hello");
this.WriteVerbose("World");
}
}
Verbose output is not actually output unless $VerbosePreference is set at least to "Continue."
Use the PowerShell type to run your cmdlet, and read VerboseRecord instances from the Streams.Verbose propery
Example in powershell script:
ps> $ps = [powershell]::create()
ps> $ps.Commands.AddScript("`$verbosepreference='continue'; write-verbose 42")
ps> $ps.invoke()
ps> $ps.streams.verbose
Message InvocationInfo PipelineIterationInfo
------- -------------- ---------------------
42 System.Management.Automation.Invocat... {0, 0}
This should be easy to translate into C#.
1. string scriptFile = "Test.ps1";
2. using (PowerShell ps = PowerShell.Create())
3. {
4. const string getverbose = "$verbosepreference='continue'";
5. ps.AddScript(string.Format(getverbose));
6. ps.Invoke();
7. ps.Commands.Clear();
8. ps.AddScript(#".\" + scriptFile);
9. ps.Invoke();
10. foreach (var v in ps.Streams.Verbose)
11. {
12. Console.WriteLine(v.Message);
13. }
14. }
Important lines are line 5 and 6. This basically set the $verbosepreference for the session and for upcoming new commands and scripts.
First off, if you are unit testing cmdlets, likely Pester is a better (and easier) option.
As per your many updates, all you are likely missing at this point is a strongly typed approach to reference the C# cmdlet
ps.AddCommand(new CmdletInfo("Get-MyCS", typeof(GetMyCS)));
DISCLAIMER: I know this works for PowerShell 5.0, but don't have experience with the older PowerShell 2.0.