14

I am trying to use PowerShell to automate the process of creating an n-tier solution based on a seed (think EDMX file or DbContext) configuration. I want to be able to open a skeleton solution, get the active instance, and populate project files with auto-generated code.

I'm trying to transcode the example provided here to powershell, however, I am getting errors.

Here is the PowerShell code I am testing:

First, I execute a little function to reference the DTE assemblies.

$libs = "envdte.dll", "envdte80.dll", "envdte90.dll", "envdte100.dll"
function LoadDTELibs {
    param(
        $path = "\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies"
    )

    Process {
        $libs |
            ForEach {
                $dll = Join-Path "$env:ProgramFiles\$path" $_

                if(-not (Test-Path $dll)) {
                    $dll = Join-Path "${env:ProgramFiles(x86)}\$path" $_
                }

                Add-Type -Path $dll -PassThru | Where {$_.IsPublic -and $_.BaseType} | Sort Name
            }
    }
}


LoadDTELibs

Then, I try to create a object to reference the result of calling [System.Runtime.InteropServices.Marshal]::GetActiveObject("VisualStudio.DTE.11.0")

PS> $dte = New-Object -ComObject EnvDTE80.DTE2

New-Object : Retrieving the COM class factory for component with CLSID {00000000-0000-0000-0000-000000000000} failed due to the following error: 80040154 
Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG)).
At line:1 char:8
+ $dte = New-Object -ComObject EnvDTE80.DTE2
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ResourceUnavailable: (:) [New-Object], COMException
    + FullyQualifiedErrorId : NoCOMClassIdentified,Microsoft.PowerShell.Commands.NewObjectCommand

or:

PS> $dte = New-Object EnvDTE80.DTE2

New-Object : Constructor not found. Cannot find an appropriate constructor for type EnvDTE80.DTE2.
At line:1 char:8
+ $dte = New-Object EnvDTE80.DTE2
+        ~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (:) [New-Object], PSArgumentException
    + FullyQualifiedErrorId : CannotFindAppropriateCtor,Microsoft.PowerShell.Commands.NewObjectCommand

Finally, this does not work either:

PS> [EnvDTE80.DTE2]$dte = [System.Runtime.InteropServices.Marshal]::GetActiveObject("VisualStudio.DTE.11.0")

Cannot convert the "System.__ComObject" value of type "System.__ComObject#{04a72314-32e9-48e2-9b87-a63603454f3e}" to type "EnvDTE80.DTE2".
At line:1 char:1
+ [EnvDTE80.DTE2]$dte = [System.Runtime.InteropServices.Marshal]::GetActiveObject( ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [], ArgumentTransformationMetadataException
    + FullyQualifiedErrorId : RuntimeException

So, my question is, how do you use DTE from PowerShell? More specifically, how do you cast the result of calling GetActiveObject to type EnvDTE.DTE2?

a_arias
  • 3,036
  • 2
  • 21
  • 20
  • I believe NuGet's [TypeWrapper](http://nuget.codeplex.com/SourceControl/changeset/view/46278ab10d9a#src/VsConsole/PowerShellHost/Utils/TypeWrapper.cs) class works around this same issue. (**Caution:** Apache-licensed code owned by the Outercurve Foundation) – bricelam Mar 04 '13 at 21:11
  • This is a great suggestion, and, lots of insight was provided by reviewing this code. However, it seems I have found a simple alternative. As you will see in my follow up answer, PowerShell handles the process a bit differently, so, casting as described on MSDN is not required. – a_arias Mar 04 '13 at 23:52
  • If you are using the package manager console in VS, the current instance's EnvDTE is already provided by the `$dte` variable. – StingyJack Apr 29 '17 at 13:43
  • @bricelam https://github.com/NuGet/NuGet2/blob/2.14/src/VsConsole/PowerShellHost/Utils/TypeWrapper.cs – as9876 Dec 21 '20 at 20:58

3 Answers3

17

I found a simple answer by playing with the idea in ISE for a little while.

Basically, the call to GetActiveObject returns a COM object, which can be used directly in PowerShell. After executing LoadDTELibs, you can get an instance of DTE by calling GetActiveObject and then refer to the result directly.

So...

PS> $dte = [System.Runtime.InteropServices.Marshal]::GetActiveObject("VisualStudio.DTE.11.0")

Then:

PS> $dte.solution.Create("D:\Testing", "Acme.sln")
PS> $dte.solution.SaveAs("D:\Testing\Acme.sln")

I'm not 100% sure, because I don't know PowerShell or COM all that well, but I think you don't really have to worry about releasing the COM instance.

a_arias
  • 3,036
  • 2
  • 21
  • 20
  • It was [this](http://www.computerperformance.co.uk/powershell/powershell_com.htm) article which provided the clues which were helpful in finding the answer. – a_arias Mar 05 '13 at 00:08
  • "After executing LoadDTELibs" - How did you execute this? – Rhyous Feb 29 '16 at 16:20
  • With this code I can't reach the part of the api to do remote debugging though, like [Debugger2.Transports](https://learn.microsoft.com/en-us/dotnet/api/envdte80.debugger2.transports?view=visualstudiosdk-2017) – Swimburger Apr 11 '18 at 16:23
2

For VS 2017 it is as follows:

$dte = [System.Runtime.InteropServices.Marshal]::GetActiveObject("VisualStudio.DTE.15.0")
Roland Roos
  • 1,003
  • 10
  • 4
1

Running Visual Studio 2019, I've been able to start the debugger with the 'VisualStudio.DTE' COM interface (without the version):

#Get the ProcessID from an AppPool's worker process: 
[int] $ProcessId = ([xml] (& "$env:SystemRoot\system32\inetsrv\appcmd.exe" list wp /xml /apppool.name:"DefaultAppPool")).appcmd.WP."WP.NAME"

# Start the debugger, attached to that ProcessID
[Runtime.InteropServices.Marshal]::GetActiveObject('VisualStudio.DTE').Debugger.LocalProcesses | 
       ? {$_.ProcessID -eq $ProcessId} | %{$_.Attach()}

Previously it was necessary to specify the version.

Rich Moss
  • 2,195
  • 1
  • 13
  • 18