Volume 32 Number 8
Creating Extensions for Multiple Visual Studio Versions
By Carlos Quintero | August 2017
The release of a new version of Visual Studio is always a challenge for developers of extensions (packages, add-ins, templates and so forth). For example, Visual Studio 2010 introduced the new Visual Studio Installer for eXtensions (VSIX files); Visual Studio 2012 introduced the light/dark themes; and Visual Studio 2015 removed add-ins (with the Add-In Manager); not to mention that each Visual Studio version provides a new SDK, new extensibility assemblies and new APIs. With Visual Studio 2017, this challenge is even bigger, due to its new modular setup based on workloads and individual components, and to a new version of the manifest for the VSIX deployment mechanism. While some developers (most notably from Microsoft) release a different new extension for each Visual Studio version, most would prefer to release a single updated extension that can target the widest range of Visual Studio versions.
In this article, I’ll show you how to accomplish this. For this purpose, I’ll focus on the most common scenario: a package with a command, created in a managed language (C#, in this case) and deployed as a VSIX file.
The goals to be accomplished are the following:
- To use a single Visual Studio project to create the package.
- To use Visual Studio 2017 for development and debugging.
- To generate a single package DLL as the result of the build.
- To put that single DLL inside a single VSIX file.
- To be able to install that VSIX file on Visual Studio 2017 and on many past versions (2015, 2013 and so on).
Because two artifacts are needed—a DLL file (which is the package) and a VSIX file (which is the deployment vehicle for the package)—I’ll explain each of these separately: First, how they work at installation or run time; second, how to develop them.
The VSIX File
As mentioned earlier, Visual Studio 2010 introduced the VSIX deployment mechanism to install Visual Studio extensions, and it’s been the preferred way ever since. A VSIX file has the extension .vsix and can be installed in different ways. If the VSIX file is published on the Visual Studio Marketplace (formerly Visual Studio Gallery) and it’s compatible with the Visual Studio version and edition you’re using, you can install it using the Extensions and Updates dialog. Under the Tools menu, click on Extensions and Updates and then go to Online | Visual Studio Marketplace (see Figure 1 The Extensions and Updates Dialog Window).
Figure 1 The Extensions and Updates Dialog Window
You can also double-click a VSIX file. When this happens, a Visual Studio Launcher (C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\VSLauncher.exe) associated to the .vsix file extension is executed; this locates the VSIXInstaller.exe utility of the highest installed Visual Studio version (the highest version is required to be able to install to all lower versions). Then, the VSIX Installer shows the dialog in Figure 2 The VSIX Installer so you can select the compatible Visual Studio versions and editions in which to install the extension.
Figure 2 The VSIX Installer
VSIX files can be installed programmatically, too, using the VSIXInstaller.exe utility with its command-line options, such as the target Visual Studio version (2017, 2015 and so on) and edition (Community, Professional and the like). You can find that utility in the Common7\IDE subfolder of your Visual Studio installation.
In any case, either Visual Studio or the VSIXInstaller.exe utility needs to know which Visual Studio versions and editions the VSIX file supports. That information can be discovered via a manifest file inside the file. The VSIX file is actually a .zip file, so you can rename its .vsix file extension to .zip and then open it to examine its contents (see Figure 3 Contents of a VSIX File3).
Figure 3 Contents of a VSIX File
As you can see, there are several files inside: The .dll file is the package DLL. The .pkgdef file is used at installation time to add some keys to the Windows Registry that allows Visual Studio to recognize the DLL as a package. The [Content_Types].xml file describes the content type for each file extension (.dll, .json and so forth). The catalog.json and manifest.json files are required by Visual Studio 2017. And the extension.vsixmanifest file describes the name of the extension, version, and more, and which Visual Studio versions and editions it supports.
You can unzip the extension.vsixmanifest file and open it with a text editor to examine its contents, which will look similar to what’s shown in Figure 4 The Contents of a Manifest File4.
Figure 4 The Contents of a Manifest File
As you can see, the manifest states in the InstallationTarget XML element the supported Visual Studio editions. Here, the Microsoft.VisualStudio.Pro value targets the Professional edition and higher, such as the Premium, Ultimate, Enterprise and any other such editions. Note that it also targets the Community edition, which is basically a Professional edition with some licensing restrictions and without some features. It also states the range of supported Visual Studio versions: 10.0 (2010), 11.0 (2012), 12.0 (2013), 14.0 (2015), 15.0 (2017).
When the VSIX file of a per-user extension is installed (either by Visual Studio or by the VSIX Installer), the files inside are unzipped and copied to a random folder at this location: C:\Users\<user>\AppData\Local\Microsoft\VisualStudio\<version number>\Extensions\<random folder>. The <version number> can have an “Exp” suffix appended for the “Experimental Instance” (explained later), and for Visual Studio 2017 it will also include the “instance id” of the installed Visual Studio. This instance id is randomly generated at Visual Studio install time; it was added to support side-by-side installations of different editions of the same version (2017) of Visual Studio, something that wasn’t possible before. For machine-wide extensions, the subfolder Common7\IDE\Extensions is used. Notice that in any case each Visual Studio version uses its own folder for its extensions.
While it would be nice if all Visual Studio versions supported the same manifest format, unfortunately that’s not the case. Visual Studio 2010 introduced VSIX and the first version of the manifest. Visual Studio 2012 introduced version 2, which is completely different and incompatible with version 1. However, Visual Studio 2012, 2013 and 2015—all of which support version 2—can still accept a version 1 manifest, so you can build a VSIX file with a version 1 and target from Visual Studio 2010 to Visual Studio 2015. But Visual Studio 2017 supports neither version 1 nor version 2. Instead, it requires a third version of the manifest. Fortunately, version 3 keeps using the value “184.108.40.206” in the Version attribute of the PackageManifest XML element and it adds only an XML element named <Prerequisites> (and the two new files, catalog.json and manifest.json, into the VSIX file). So, it’s completely backward-compatible with the second version, supported by Visual Studio 2012, 2013 and 2015 (but not by Visual Studio 2010, which only supports version 1). This means that you can’t target Visual Studio 2010-2017 with a single VSIX file. From this point, I’ll give up on Visual Studio 2010 and will continue with a VSIX file that supports Visual Studio 2012, 2013, 2015 and 2017.
The Package DLL
A managed Visual Studio package is a DLL that contains a class that inherits from Microsoft.VisualStudio.Shell.Package. It’s decorated with certain attributes that help at build time to generate a .pkgdef file (which, as mentioned earlier, you can find inside the VSIX file and in the installation folder of the extension). The .pkgdef file is used at startup (older versions of Visual Studio) or at installation time (version 15.3 of Visual Studio 2017) to register the DLL as a package for Visual Studio. Once it’s registered, Visual Studio will try to load the package at some point, either on startup or when one of its commands is executed if the package uses delay loading (which is the best practice). During the attempt to load the managed DLL and initialize the package, three things happen: the DLL will be loaded by the Common Language Runtime (CLR) of a Microsoft .NET Framework version; it will use some DLLs provided by a .NET Framework; and it will use some DLLs provided by Visual Studio. I will examine each of these in turn.
A .NET Framework is the sum of two things: The CLR + libraries (both base class and additional libraries). The CLR is the runtime (the JIT compiler, garbage collector and so forth) and it loads managed DLLs. In the distant past, each .NET Framework version 1.0, 1.1 and 2.0 (used by Visual Studio.NET 2002, Visual Studio.NET 2003 and Visual Studio 2005) provided its own CLR version (1.0, 1.1 and 2.0). However, the .NET Frameworks 3.0 and 3.5, used by Visual Studio 2008, continued to use the exact same CLR 2.0 of .NET Framework 2.0, instead of introducing a new one. Visual Studio 2010 introduced .NET Framework 4 and CLR 4.0, but since then all new .NET Frameworks 4.x have used CLR 4.0 (although swapping it “in-place” with a backward-compatible version rather than reusing the exact CLR 4.0 of .NET Framework 4). Since Visual Studio 2012 and higher all use CLR 4.0, the CLR version is not a problem when the DLL of an extension targets Visual Studio 2012, 2013, 2015 and 2017.
Libraries constitute the second part of a .NET Framework; these are DLLs referenced by a Visual Studio project and used at run time. To develop a single extension that targets multiple versions of Visual Studio, you must use the highest .NET Framework installed by default by the lowest Visual Studio version that you want to target. This means that if you want to target Visual Studio 2012 and higher, you need to use .NET Framework 4.5. You can’t use, say, .NET Framework 4.5.1 introduced by Visual Studio 2013, because any DLL introduced in that version would not be present on a computer with only Visual Studio 2012 installed. And unless you really need that DLL, you won’t want to force such users to install .NET Framework 4.5.1 to use your extension (it could hurt sales or downloads and support).
The extension also needs DLLs that are provided by Visual Studio (typically named Microsoft.VisualStudio.*). At run time, Visual Studio finds its DLLs at some well-known locations, such as the folder Common7\IDE with its subfolders Common7\IDE\PublicAssemblies and Common7\IDE\PrivateAssemblies, and from the Global Assembly Cache (GAC). The GAC for .NET Framework 4.x is located at C:\Windows\Microsoft.NET\assembly (there’s another GAC at C:\Windows\assembly, but that one is for older .NET Frameworks). Visual Studio 2017 uses a more isolated installation that avoids the GAC, relying instead on the folders described previously.
There are a couple of key principles to follow when developing and generating a VSIX file: You must use the versions provided by the lowest Visual Studio version your extension targets. That means that if you want to target Visual Studio 2012 and higher, you must use only assemblies and extensibility APIs provided by that version (or lower). If your extension uses a DLL introduced by Visual Studio 2013 or higher, the extension won’t work on a machine with only Visual Studio 2012. The second principle is that the extension never must deploy Visual Studio DLLs, neither to the locations I mentioned (folders of Visual Studio or GAC), nor to the installation folder of the extension. These DLLs are provided by the target Visual Studio, which means that the VSIX file shouldn’t include them.
Many Visual Studio DLLs have a version number (8.0 … 15.0) in the name, such as Microsoft.VisualStudio.Shell.11.0.dll or Microsoft.VisualStudio.Shell.Immutable.10.0.dll. These help to identify the Visual Studio version that introduced them, but don’t get fooled: it’s a name, not a version. For example, there are four versions (220.127.116.11, 18.104.22.168, 22.214.171.124 and 126.96.36.199) of Microsoft.Visual.Studio.Shell.11.0.dll, each one provided, respectively, by a Visual Studio version (2012, 2013, 2015 and 2017). The first three 188.8.131.52 to 184.108.40.206 are installed by the respective Visual Studio version in the GAC and the fourth version, 220.127.116.11, used by Visual Studio 2017, is installed in the Common\IDE\PrivateAssemblies folder.
Because an extension that targets Visual Studio 2012 and higher must use Visual Studio assemblies with version 18.104.22.168 (the first principle mentioned earlier), this means that the reference Microsoft.Visual.Studio.Shell.11.0.dll must be version 22.214.171.124. But because that version isn’t installed by Visual Studio 2013 and higher (they start at version 126.96.36.199), and the extension shouldn’t deploy Visual Studio DLLs (the second principle), wouldn’t the extension fail when trying to use that Visual Studio DLL? The answer is no, and it’s thanks to an assembly-binding redirection mechanism provided by the .NET Framework, which allows you to specify rules like “when something requests this version of an assembly, use this newer version of it.” Of course, the new version must be fully backward-compatible with the old version. There are several ways to redirect assemblies from one version to another. One way is this: An executable (.exe file extension) can provide an accompanying configuration file (.exe.config file extension) that specifies the redirections. So, if you go to the Common7\IDE folder of your Visual Studio installation, you’ll find the devenv.exe executable of Visual Studio, and a devenv.exe.config file. If you open the .config file with a text editor, you’ll see that it contains lots of assembly redirections:
<dependentAssembly> <assemblyIdentity name="Microsoft.VisualStudio.Shell.11.0" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/> <bindingRedirect oldVersion="188.8.131.52-184.108.40.206 newVersion="220.127.116.11"/> </dependentAssembly>
So, Visual Studio 2017 (15.0) has an assembly version redirection for Microsoft.VisualStudio.Shell.11.0 that states that whenever something requests old versions 18.104.22.168 to 22.214.171.124, use the new version 126.96.36.199 instead. That’s how Visual Studio 2013 or later can use an extension referencing Microsoft.VisualStudio.Shell.11.0 version 188.8.131.52, even if they don’t provide that exact version.
Developing the Extension
Now that you know how things work at run time, you can develop the package. To recap, you’ll create a VSIX project using Visual Studio 2017 with a manifest that targets Visual Studio versions from 12.0 to 15.0; it will contain a package and a command; and it will use only references with version 184.108.40.206 (or lower) installed by Visual Studio 2012.
You might wonder at this moment which Visual Studio versions should be installed on your development machine. The best practice is to have two development machines as follows: On the first, if you have enough space on your disk, install all the Visual Studio versions—2012, 2013, 2015 and 2017. They can all coexist side by side and you’ll be able to test them during development. For Visual Studio 2017, even different editions such as Community, Professional and Enterprise can coexist at the same time, something that wasn’t possible with older versions of Visual Studio. If available space is a concern, install the minimal components for the old versions, or skip some version in the middle of the range (2013 or 2015).
On your second development machine, install only Visual Studio 2017 or, even better, a build server with no Visual Studio version installed (just the Build Tools 2017), to build your extension for release. This approach will help ensure that you’re not inadvertently using DLLs or other dependencies from folders installed by older Visual Studio versions. You might also wonder if it wouldn’t be safer to develop or build on a machine with only Visual Studio 2012 installed and the answer is that it’s not possible: To generate a VSIX file for Visual Studio 2017 (which creates a version 3 manifest and adds the catalog.json and manifest.json files), you need the Visual Studio SDK 15.0 of Visual Studio 2017 or, with some work, the Visual Studio SDK 14.0 of Visual Studio 2015. Neither the Visual Studio SDK 12.0 of Visual Studio 2013 nor the Visual Studio SDK 11.0 of Visual Studio 2012 can generate VSIX files for Visual Studio 2017.
And the best practice for (serious) testing is: Use a separate machine (virtual or cloud-based) for each Visual Studio version (so you’ll need four machines to test your extension on Visual Studio 2012 to Visual Studio 2017 in isolation). This best practice helped me to find some errors in the code sample for this article!
To get the Visual Studio 2017 project templates to create a package (or any other kind of extension) you need the “Visual Studio extension development” workload. If you didn’t install it when you first installed Visual Studio 2017, go to the folder C:\Program Files (x86)\Microsoft Visual Studio\Installer, launch vs_Installer.exe, click the Modify button and select that workload at the bottom of the list.
Create a new VSIX project using the File | New | Project menu; go to the Visual C# | Extensibility templates; ensure you’ve selected .NET Framework 4.5 on the dropdown list at the top; and select the VSIX Project template. Name the project VSIXProjectVS2012_2017. Double-click the source.extension.vsixmanifest file to open its custom editor. In the Metadata tab, set the product name, author, version and so on. In the Install Targets tab, click the Edit button, select the Microsoft.VisualStudio.Pro identifier (that value also targets the Community edition, which is basically a Professional edition) and set the target installation range, [11.0,15.0], as shown in Figure 5. A square bracket means the value is included. A parenthesis would mean that the value is excluded, so you can also set [11.0,16.0). You can also target a minor version (like 15.3) using the build number (such as 15.0.26208.1).
Figure 5 Installation Targets
In the Dependencies tab, delete all items. In the Prerequisites tab, click the Edit button and set the minimal Visual Studio 2017 component your extension requires. In this example, only the Visual Studio core editor is required. This section is new for Visual Studio 2017 and the version 3 manifest, so it only applies to version 15.0 (see Figure 6 Prerequisites6):
Figure 6 Prerequisites
Add a package to the VSIX project by right-clicking the VSIX project node in Solution Explorer, then select the Add | New Item menu to bring up the Add New Item dialog. Now, go to the Visual Studio C# Items | Extensibility | VSPackage node, select the Visual Studio Package template and name it MyPackage.cs. Add a command to the package repeating the actions of the previous step, but selecting this time the Custom Command template. Name this MyCommand1.cs.
To follow the principle of using the fewest dependencies required, in the source code of MyPackage.cs and MyCommand1.cs, remove the unused (grayed) namespaces. Then right-click the VSIX project node in Solution Explorer and click the Manage NuGet Packages for Solution entry. In the Installed section, uninstall all the packages in the order shown here:
Microsoft.VisualStudio.Shell.15.0 Microsoft.VisualStudio.Shell.Framework Microsoft.VisualStudio.CoreUtility Microsoft.VisualStudio.Imaging Microsoft.VisualStudio.Shell.Interop.12.0 Microsoft.VisualStudio.Shell.Interop.11.0 Microsoft.VisualStudio.Shell.Interop.10.0 Microsoft.VisualStudio.Threading Microsoft.VisualStudio.Shell.Interop.9.0 Microsoft.VisualStudio.Shell.Interop.8.0 Microsoft.VisualStudio.TextManager.Interop.8.0 Microsoft.VisualStudio.Shell.Interop Microsoft.VisualStudio.TextManager.Interop Microsoft.VisualStudio.Validation Microsoft.VisualStudio.Utilities Microsoft.VisualStudio.OLE.Interop
(Don’t uninstall the Microsoft.VSSDK.BuildTools package, which is the Visual Studio SDK.)
In the project’s References node in Solution Explorer, uninstall all the remaining references (that weren’t acquired as NuGet packages) except System and System.Design. Now you can rebuild the solution. You’ll get compilation errors that will be solved adding just the references shown in Figure 7.
Figure 7 Visual Studio 2012 References
|Assembly Name||Assembly Version||Visual Studio 2012 SDK Subfolder|
Unfortunately, Microsoft doesn’t provide an official NuGet package for Microsoft.VisualStudio.Shell.11.0 (you can find an unofficial NuGet VSSDK.Shell.11 package, though). If you have Visual Studio 2012 installed (you should if that’s the minimal-supported version for your extension), you can get it from the GAC as explained earlier. Alternatively, you can get all the required assemblies by installing the Visual Studio 2012 SDK (bit.ly/2rnGsfq) that provides them in the subfolders v2.0 and v4.0 of the folder C:\Program Files (x86)\Microsoft Visual Studio 11.0\VSSDK\VisualStudioIntegration\Common\Assemblies. The last column of the table shows the subfolder of the Visual Studio 2012 SDK where you can find each assembly.
To avoid dependencies on unofficial NuGet packages or on specific local folders (either from a Visual Studio SDK or from a Visual Studio installation), the best approach is to get the assemblies from wherever and create a folder called VS2012Assemblies under the root folder of the project. Then, copy the DLLs to that folder, reference them from there (using the Browse button of the project’s Reference Manager dialog) and add the VS2012Assemblies folder to source code control, ensuring that the DLLs are added to it (normally source code control tools don’t add DLLs by default). So, from this point, the required Visual Studio assemblies are part of the source code.
To follow the principle of not including assembly references in the VSIX file and not even in the output folder, select each reference and in the Properties window ensure that the Copy Local property is set to False. At this point the solution can be rebuilt without errors. Using Windows Explorer, go to the output folder. Only these files should be generated: extension.vsixmanifest, VSIXProjectVS2012_2017.dll, VSIXProjectVS2012_2017.pkgdef and VSIXProjectVS2012_2017.vsix.
When you build the project, one of the MSBuild targets deploys the extension to the Experimental instance of Visual Studio. This is an instance of Visual Studio that uses different folders and Registry entries than the normal instance, so that you don’t make the normal instance unusable if something goes wrong with your extension during development. (You can always reset the Experimental instance clicking the Windows Start button, typing “Reset the” and executing the “Reset the Visual Studio 2017 Experimental Instance” command.) If you go to the Debug tab on the Properties page of the project, you can set the Start external program field to the Visual Studio 2017 devenv.exe file. (It’s important to change this if upgrading, since it would point to an old version of Visual Studio.) You can also see that the Command line arguments specify “Exp” as the root suffix (see Figure 8 Debug Experimental Instance8), so that the Experimental Instance is also used for debugging.
Figure 8 Debug Experimental Instance
Click the Debug | Start Debugging menu entry and a new Visual Studio instance will be launched (notice its caption indicates “Experimental Instance”). If you click the Tools | Invoke MyCommand1 menu entry, the package will be loaded, the command will be executed and a message box will be shown.
If you want to use Visual Studio 2017 to debug the extension on a previous Visual Studio version, you need to make two changes: First, because once the extension is built it’s deployed to the Visual Studio Experimental Instance of the version whose SDK was used to build the project, you need to remove the NuGet package Microsoft.VSSDK.BuildTools version 15.0 and use version 14.0 for Visual Studio 2015 or version 12.0 for Visual Studio 2013. For Visual Studio 2012 there isn’t a NuGet package for the VSDK, so you need to edit the .csproj file and point the VSToolsPath variable to the location of the VSSDK 11.0 (C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v11.0), which you must install separately. Second, you need to go to the Debug tab on the Properties page of the project and set the Start external program field to the matching Common7\IDE\devenv.exe executable.
As you probably know, many Visual Studio projects support round-tripping. That is, they can be opened and debugged by several Visual Studio versions without suffering modifications. This is not the case with extensibility projects “out of the box.” However, with some mastering of MSBuild and Visual Studio SDKs, you may achieve it, but it’s always a tricky approach.
Once you’re done with the development and debugging, you can build your extension in Release configuration and test it on Visual Studio versions installed in isolated instances on test machines. If everything goes well, you can then publish your extension on the Visual Studio Marketplace!
Carlos Quintero* has received the Microsoft Most Valuable Professional award 14 times, currently in the category of Visual Studio and Development Technologies. He has been helping other developers to create extensions for Visual Studio since 2002, blogging about it since 2006 at visualstudioextensibility.com and more recently tweeting about it: @VSExtensibility.*
Thanks to the following technical experts for reviewing this article: Justin Clareburt, Alex Eyler and Mads Kristensen