This article could also be entitled, “Using an Inproc COM server in C# and PowerShell”.
Part I of this series shows how to invoke the GetDiskSpaceInformation method of the IOfflineFilesCache COM interface via C++. This was accomplished after failing miserably at trying to get it to work with C# and PowerShell. However, after I solved the problem in C++ I understood exactly how it worked and was able to ask better questions in Google to find out how to do the same in C#.
The challenge
First, to explain why it’s hard. When I first looked at the docs, I thought this would be easy. In PowerShell, a COM object can be invoked by instantiating a com object with new-object. For example:
$app = new-object -comobject excel.application
Even if that didn’t work, I knew that often times C++ code could be Marshaled to and from the world of C# and managed code with a bit of tinkering. This is usually where you see pinvoke.net and code that leverages add-type. However, in this case, the libraries do not exist in pinvoke. Basically, because this is really COM, you cannot do this. Also, because there are no tlb files associated, you cannot easily just use the interfaces like they are COM.
Just to be clear as to why this is the case: This is a new interface. It was plugged in by the Windows developers into the latest versions of Windows. It’s implemented in COM so that other languages can get access to it. However, it’s not fully at the point where it needs to be flushed into general use. I expect that in the years to come, we’ll see these interfaces exposed with a tlb and eventually there may even be a PowerShell module that is used to manage the offline files cache directly. However, if you want access before that day comes, you need to get crafty.
Finding GUIDs OLE/COM Object Viewer
The key to invoking this code from C# is to know the GUIDs that were used for both the CLSID and the interface we are trying to get access to. This can be accomplished by running the OLE/COM Object Viewer. For me, this was installed with my Visual Studio 2013 and can be found here: C:\Program Files (x86)\Windows Kits\8.1\bin\x86\oleview.exe.
Once in the GUI, you can browse to Object Classes->All Objects->Offline Files Cache Control
We’re looking for the GUID: 48C6BE7C-3871-43CC-B46F-1449A1BB2FF3
Next, if you double-click on that you’ll see the interfaces. In our case, we want the IOfflineFilesCache interface.
The GUID is 855D6203-7914-48B9-8D40-4C56F5ACFFC5
It should be noted that these GUIDs are static. You do not need to run the COM viewer on your desktop if you are invoking the exact same interface that I am demonstrating. The GUIDs are the same on ever computer. However, this is here to show the generic steps that are needed to invoke the methods on one of these no-TLB COM interfaces.
CreateInstance
The first step is to create an instance of the CLSID using the GUID we found above.
Guid ID = new Guid("48C6BE7C-3871-43cc-B46F-1449A1BB2FF3"); Type idtype = Type.GetTypeFromCLSID(ID); IOfflineFilesCache obj = (IOfflineFilesCache) Activator.CreateInstance(idtype, true);
It should be noted that the code we are using requires:
using System.Runtime.InteropServices;
Unfortunately, in the above bit of code, we are referencing the IOfflineFilesCache type, but it does not yet exist anywhere. Therefore, we have to help C# know what this is by creating an interface with the ComImport attribute
Interface Attributes
I should note that everything I’m demonstrating is documented here. However, it’s a bit cludgy to get through. Also, there are a few key elements it leaves out. Specifically, it neglects to inform you that when you create the interface, you must implement all of the methods that exist in the interface in exact order leading up to the method you care about using. The best way to get the methods and the order they are implemented is to read the C++ header file. This was one of the #include files I showed in part I of this article. Specifically, you need to view cscobj.h. A quick search of your hard drive should find it in an SDK folder. Once you have read through this, you can create the interface in the proper order:
[ComImport] [Guid("855D6203-7914-48B9-8D40-4C56F5ACFFC5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IOfflineFilesCache { [PreserveSig()] int Synchronize(); [PreserveSig()] int DeleteItems(); [PreserveSig()] int DeleteItemsForUser(); [PreserveSig()] int Pin(); [PreserveSig()] int UnPin(); [PreserveSig()] int GetEncryptionStatus(); [PreserveSig()] int Encrypt(); [PreserveSig()] int FindItem(); [PreserveSig()] int FindItemEx(); [PreserveSig()] int RenameItem(); [PreserveSig()] int GetLocation(); [PreserveSig()] int GetDiskSpaceInformation(ref ulong pcbVolumeTotal, ref ulong pcbLimit, ref ulong pcbUsed, ref ulong pcbUnpinnedLimit, ref ulong pcbUnpinnedUsed); // only need to go as far as the function you need, but rest here for completeness [PreserveSig()] int SetDiskSpaceLimits(); [PreserveSig()] int ProcessAdminPinPolicy(); [PreserveSig()] int GetSettingObject(); [PreserveSig()] int EnumSettiingObjects(); [PreserveSig()] int IsPathCacheable(); }
You’ll notice that in the above, the GUID is the GUID we found when looking at the interface in the OLE/COM viewer.
Finished code, but let’s do it in PowerShell
So, now that we have the interface and an instance of it, the rest is easy. The following final bit of code injects the C# into PowerShell via add-type. I simply return the results as a collection and then convert it into an object in PowerShell, but you could just as easily modify the code to have the object returned directly from C#.
$code = @' using System; using System.Runtime.InteropServices; public class offlinecache { public static ulong[] GetOfflineCache() { ulong pcbVolumeTotal=0, pcbLimit=0, pcbUsed=0, pcbUnpinnedLimit=0, pcbUnpinnedUsed=0; Guid ID = new Guid("48C6BE7C-3871-43cc-B46F-1449A1BB2FF3"); Type idtype = Type.GetTypeFromCLSID(ID); IOfflineFilesCache obj = (IOfflineFilesCache) Activator.CreateInstance(idtype, true); int i = obj.GetDiskSpaceInformation(ref pcbVolumeTotal, ref pcbLimit, ref pcbUsed, ref pcbUnpinnedLimit, ref pcbUnpinnedUsed); ulong[] output = new ulong[5]; output[0] = pcbVolumeTotal; output[1] = pcbLimit; output[2] = pcbUsed; output[3] = pcbUnpinnedLimit; output[4] = pcbUnpinnedUsed; return output; } [ComImport] [Guid("855D6203-7914-48B9-8D40-4C56F5ACFFC5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IOfflineFilesCache { [PreserveSig()] int Synchronize(); [PreserveSig()] int DeleteItems(); [PreserveSig()] int DeleteItemsForUser(); [PreserveSig()] int Pin(); [PreserveSig()] int UnPin(); [PreserveSig()] int GetEncryptionStatus(); [PreserveSig()] int Encrypt(); [PreserveSig()] int FindItem(); [PreserveSig()] int FindItemEx(); [PreserveSig()] int RenameItem(); [PreserveSig()] int GetLocation(); [PreserveSig()] int GetDiskSpaceInformation(ref ulong pcbVolumeTotal, ref ulong pcbLimit, ref ulong pcbUsed, ref ulong pcbUnpinnedLimit, ref ulong pcbUnpinnedUsed); // only need to go as far as the function you need, but rest here for completeness [PreserveSig()] int SetDiskSpaceLimits(); [PreserveSig()] int ProcessAdminPinPolicy(); [PreserveSig()] int GetSettingObject(); [PreserveSig()] int EnumSettiingObjects(); [PreserveSig()] int IsPathCacheable(); } } '@ add-type -TypeDefinition $code $output = ([offlinecache]::GetOfflineCache()) new-object psobject -Property ([ordered] @{ VolumeTotal = $output[0] Limit = $output[1] Used = $output[2] UnpinnedLimit = $output[3] UnpinnedUsed = $output[4] })
Here’s a trail of it running:
06:50:06 PS C:\Dropbox\scripts> .\GetOfflineCacheDiskInfo.ps1 VolumeTotal : 469636214784 Limit : 109076041728 Used : 0 UnpinnedLimit : 109076041728 UnpinnedUsed : 0
