One of the cool things that shipped with the Visual Studio Team System 2008 Database Edition GDR (aka the GDR) is the command line utility VSDBCMD.EXE. By using this utility, you can take the artifacts created by building a Database Project and deploy them to a database instance. If the database doesn’t exist on the instance of SQL Server, it will be created. If the database does exist, it will be altered to look like the source code in your project.
We regularly get questions from developers that are implementing multi-tier applications asking to incorporate database deployment into their WiX based MSI. This blog post describes how you can do that.
An important thing to point out before we get started is that the VSDBCMD.EXE utility has several prerequisites that must be met before the utility will execute successfully. The prerequisites are
Ensure that these prerequisites are installed on the computers on which you plan to run your MSI.
I have written this blog post assuming the reader knows very little about WiX authoring. For the experts out there you can skim the information you are already familiar with. Here’s what we’re going to do in the remainder of this blog post.
Note: On operating systems such as Windows Vista or Windows 7, the installer will need to be run using Administrator privileges (aka run with elevated privileges).
For the purposes of this blog post we will create a new Database project. If you already have a database project you can simply add it to the solution instead of creating a new one.
Our installer is pretty simple. It will do the following
Implement the WiX code by doing the following
<!-- Setup the folder structure for the install --> <Directory Id="TARGETDIR" Name="SourceDir"> <Directory Id="ProgramFilesFolder"> <Directory Id="INSTALLLOCATION" Name="MyDatabase"> <Directory Id="Extensions" Name="Extensions"> <Directory Id="SqlServer" Name="SqlServer"> <Directory Id="__2008" Name="2008"> <Directory Id="DbSchemas" Name="DbSchemas"> </Directory> </Directory> </Directory> </Directory> </Directory> </Directory> </Directory>
A couple interesting things to note about the values in the Id attributes for the <Directory> elements; 1) As you will see, WiX turns these values into properties that we can use in our authoring and 2) properties that are UPPERCASED can be overridden from the command line when the MSI is run. I’ll demonstrate this in the section that covers running the MSI we’re building.
<?include $(var.SolutionDir)InstallMyDatabase\DatabaseArtifacts.wxi ?> <?include $(var.SolutionDir)InstallMyDatabase\VsdbcmdArtifacts.wxi?>
<DirectoryRef Id="INSTALLLOCATION"> <Component Id="DatabaseSqlcmd" Guid="PUT-GUID-HERE"> <File Id="Database.sqlcmdvars" Source="$(var.SolutionDir)MyDatabase\sql\debug\Database.sqlcmdvars" KeyPath="yes"/> </Component> <Component Id="DatabaseSqldeployment" Guid="PUT-GUID-HERE"> <File Id="Database.sqldeployment" Source="$(var.SolutionDir)MyDatabase\sql\debug\Database.sqldeployment" KeyPath="yes"/> </Component> <Component Id="DatabaseSqlsettings" Guid="PUT-GUID-HERE"> <File Id="Database.sqlsettings" Source="$(var.SolutionDir)MyDatabase\sql\debug\Database.sqlsettings" KeyPath="yes"/> </Component> <Component Id="DATABASEDBSCHEMA" Guid="PUT-GUID-HERE"> <File Id="Database1.dbschema" Source="$(var.SolutionDir)MyDatabase\sql\debug\MyDatabase.dbschema" KeyPath="yes"/> </Component> <Component Id="DatabaseDeploymanifest" Guid="PUT-GUID-HERE"> <File Id="Database1.deploymanifest" Source="$(var.SolutionDir)MyDatabase\sql\debug\MyDatabase.deploymanifest" KeyPath="yes"/> </Component> <Component Id="MicrosoftSqlTypesDbschema" Guid="PUT-GUID-HERE"> <File Id="Microsoft.SqlTypes.dbschema" Source="$(var.SolutionDir)MyDatabase\sql\debug\Microsoft.SqlTypes.dbschema" KeyPath="yes"/> </Component> <Component Id="PostDeploymentScript" Guid="PUT-GUID-HERE"> <File Id="Script.PostDeployment.sql" Source="$(var.SolutionDir)MyDatabase\sql\debug\Script.PostDeployment.sql" KeyPath="yes"/> </Component> <Component Id="PreDeploymentScript" Guid="PUT-GUID-HERE"> <File Id="Script.PreDeployment.sql" Source="$(var.SolutionDir)MyDatabase\sql\debug\Script.PreDeployment.sql" KeyPath="yes"/> </Component> </DirectoryRef>
Complete the code by replacing each instance of PUT-GUID-HERE with your own unique GUIDs. You can launch a GUID generation tool from the Visual Studio Tools menu: Tools\Create GUID. Choose the Registry Format and delete the open and close curly braces “{ }” after you paste the GUIDs into the code.
Those of you familiar with WiX syntax will understand all the details of this code. For you rookies that don’t feel like reading the WiX documentation, here’s what’s going on:
<!--Make sure we have the correct path to the Data Dude redistributables regardless of the OS architecture.--> <?if $(var.Platform)=x64 ?> <?define SrcArchPath = "C:\Program Files (x86)\Microsoft Visual Studio 9.0\VSTSDB\Deploy\"?> <?else?> <?define SrcArchPath = "C:\Program Files\Microsoft Visual Studio 9.0\VSTSDB\Deploy\"?> <?endif?> <DirectoryRef Id="INSTALLLOCATION"> <Component Id="MicrosoftDataSchema" Guid="PUT-GUID-HERE"> <File Id="Microsoft.Data.Schema.dll" Source="$(var.SrcArchPath)Microsoft.Data.Schema.dll" KeyPath="yes" Checksum="yes"/> </Component> <Component Id="MicrosoftDataSchemaScriptDom" Guid="PUT-GUID-HERE"> <File Id="Microsoft.Data.Schema.ScriptDom.dll" Source="$(var.SrcArchPath)Microsoft.Data.Schema.ScriptDom.dll" KeyPath="yes" Checksum="yes"/> </Component> <Component Id="MicrosoftDataSchemaScriptDomSql" Guid="PUT-GUID-HERE"> <File Id="Microsoft.Data.Schema.ScriptDom.Sql.dll" Source="$(var.SrcArchPath)Microsoft.Data.Schema.ScriptDom.Sql.dll" KeyPath="yes" Checksum="yes"/> </Component> <Component Id="MicrosoftDataSchemaSql" Guid="PUT-GUID-HERE"> <File Id="Microsoft.Data.Schema.Sql.dll" Source="$(var.SrcArchPath)Microsoft.Data.Schema.Sql.dll" KeyPath="yes" Checksum="yes"/> </Component> <Component Id="Vsdbcmd" Guid="PUT-GUID-HERE"> <File Id="vsdbcmd.exe" Source="$(var.SrcArchPath)vsdbcmd.exe" KeyPath="yes" Checksum="yes"/> </Component> </DirectoryRef> <DirectoryRef Id="SqlServer"> <Component Id="DatabaseSchemaProvidersExtensionsXml" Guid="PUT-GUID-HERE"> <File Id="DatabaseSchemaProviders.Extensions.Xml" Source="$(var.SrcArchPath)Extensions\SqlServer\DatabaseSchemaProviders.Extensions.xml" KeyPath="yes"/> </Component> </DirectoryRef> <DirectoryRef Id="DbSchemas"> <Component Id="MicrosoftSqlTypes" Guid="PUT-GUID-HERE"> <File Id="MicrosoftSqlTypesDbschema" Source="$(var.SrcArchPath)Extensions\SqlServer\2008\DBSchemas\Microsoft.SqlTypes.dbschema" KeyPath="yes"/> </Component> </DirectoryRef>
<!--Install the Visual Studio 9.0 registry key if it doesn't exist. --> <Feature Id="RegistryPreReqs" Title="Registry Prerequisites" Level="1"> <ComponentRef Id="RegistryEntries"/> <Condition Level="0">NOT VISUALSTUDIOREGISTRY</Condition> </Feature> <!--VSDBCMD requires this REGKEY to be installed. Note that this will NOT be true for the Visual Studio 2010 version--> <!--HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0--> <!-- Define the search for the Visual Studio 9.0 registry key --> <Property Id="VISUALSTUDIOREGISTRY"> <RegistrySearch Id="VisualStudio90Registry" Root="HKCU" Key="Software\Microsoft\VisualStudio\9.0" Name="(Default)" Type="raw" /> </Property> <!-- Define the Visual Studio 9.0 Registry entry--> <DirectoryRef Id="TARGETDIR"> <Component Id="RegistryEntries" Guid="PUT-GUID-HERE"> <RegistryKey Root="HKCU" Key="Software\Microsoft\VisualStudio\9.0" Action="create" > <RegistryValue Type="string" Name="(Default)" Value="Default Value" KeyPath="no"/> </RegistryKey> </Component> </DirectoryRef>
Just like you did previously, replace each instance of PUT-GUID-HERE with your own GUIDs. There are a few new things to point out in this code
<!-- Define which files to install --> <Feature Id="ProductFeature" Title="InstallMyDatabase" Level="1"> <ComponentRef Id="DatabaseSqlcmd"/> <ComponentRef Id="DatabaseSqldeployment"/> <ComponentRef Id="DatabaseSqlsettings"/> <ComponentRef Id="DATABASEDBSCHEMA"/> <ComponentRef Id="DatabaseDeploymanifest"/> <ComponentRef Id="MicrosoftSqlTypesDbschema"/> <ComponentRef Id="PostDeploymentScript"/> <ComponentRef Id="PreDeploymentScript"/> <ComponentRef Id="MicrosoftDataSchema"/> <ComponentRef Id="MicrosoftDataSchemaScriptDom"/> <ComponentRef Id="MicrosoftDataSchemaScriptDomSql"/> <ComponentRef Id="MicrosoftDataSchemaSql"/> <ComponentRef Id="Vsdbcmd"/> <ComponentRef Id="DatabaseSchemaProvidersExtensionsXml"/> <ComponentRef Id="MicrosoftSqlTypes"/> <ComponentRef Id="RegistryEntries"/> </Feature>
Similar to the registry stuff that I described previously, this code defines a <Feature> which contains <ComponentRef> elements, one for each component that we want to install on the user’s system.
<!--Define the dialog to get the Server and Database name information from the user--> <UI> <Dialog Id="DatabaseInformationDialog" Width="370" Height="270" Title="[ProductName] [Setup]" NoMinimize="yes"> <Control Id="ServerNameLabel" Type="Text" X="45" Y="73" Width="100" Height="15" TabSkip="no" Text="SQL Instance Name:" /> <Control Id="ServerNameEdit" Type="Edit" X="45" Y="85" Width="220" Height="18" Property="SERVERNAME" Text="{80}" /> <Control Id="DatabaseNameLabel" Type="Text" X="45" Y="110" Width="100" Height="15" TabSkip="no" Text="Database Name:" /> <Control Id="DatabaseNameEdit" Type="Edit" X="45" Y="122" Width="220" Height="18" Property="DATABASENAME" Text="{80}" /> <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="&Back"/> <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="&Next"/> <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel"> <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish> </Control> <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="WixUI_Bmp_Banner" /> <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes"> <Text>”Please enter a SQL instance and database name.”</Text> </Control> <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" /> <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes"> <Text>{\WixUI_Font_Title}”SQL instance and database information.”</Text> </Control> <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" /> </Dialog> </UI>
This code defines a dialog that contains two labels and associated text input controls. When the user enters text into the ServerNameEdit and DatabaseNameEdit text controls it is persisted in the properties SERVERNAME and DATABASENAME, respectively.
<!--Define the overall user interface - this is based on the WixUI_Mondo dialog set--> <UI Id="MyWixUI"> <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" /> <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" /> <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" /> <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" /> <Property Id="WixUI_Mode" Value="Mondo" /> <DialogRef Id="ErrorDlg" /> <DialogRef Id="FatalError" /> <DialogRef Id="FilesInUse" /> <DialogRef Id="MsiRMFilesInUse" /> <DialogRef Id="PrepareDlg" /> <DialogRef Id="ProgressDlg" /> <DialogRef Id="ResumeDlg" /> <DialogRef Id="UserExit" /> <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish> <Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish> <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="DatabaseInformationDialog" Order="2">LicenseAccepted = "1"</Publish> <Publish Dialog="DatabaseInformationDialog" Control="Next" Event="NewDialog" Value="SetupTypeDlg">1</Publish> <Publish Dialog="DatabaseInformationDialog" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish> <Publish Dialog="SetupTypeDlg" Control="Back" Event="NewDialog" Value="DatabaseInformationDialog">1</Publish> <Publish Dialog="SetupTypeDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish> <Publish Dialog="SetupTypeDlg" Control="TypicalButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> <Publish Dialog="SetupTypeDlg" Control="CustomButton" Event="NewDialog" Value="CustomizeDlg">1</Publish> <Publish Dialog="SetupTypeDlg" Control="CompleteButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> <Publish Dialog="CustomizeDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="1">WixUI_InstallMode = "Change"</Publish> <Publish Dialog="CustomizeDlg" Control="Back" Event="NewDialog" Value="SetupTypeDlg" Order="2">WixUI_InstallMode = "InstallCustom"</Publish> <Publish Dialog="CustomizeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="CustomizeDlg" Order="1">WixUI_InstallMode = "InstallCustom"</Publish> <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="SetupTypeDlg" Order="2">WixUI_InstallMode = "InstallTypical" OR WixUI_InstallMode = "InstallComplete"</Publish> <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="CustomizeDlg" Order="3">WixUI_InstallMode = "Change"</Publish> <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="4">WixUI_InstallMode = "Repair" OR WixUI_InstallMode = "Remove"</Publish> <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish> <Publish Dialog="MaintenanceTypeDlg" Control="ChangeButton" Event="NewDialog" Value="CustomizeDlg">1</Publish> <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish> <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish> </UI>
<UIRef Id="WixUI_Common" />
I injected my new dialog in the overall sequence changing the LicenseAgreementDlg dialog’s Next button event to open my DatabaseInformationDialog and the Next button in my dialog subsequently launches the SetupTypeDlg. Similarly for the Back button events.
<!--Define the custom action to build the vsdbcmd.exe command line string--> <CustomAction Id="LaunchVsdbcmdCommandLine" Property="LaunchVsdbcmd" Value=""[#vsdbcmd.exe]" /a:Deploy /cs:"Server=[SERVERNAME];Integrated Security=true;" /dsp:Sql /dd+ /model:"[INSTALLLOCATION]Database1.dbschema" /p:TargetDatabase="[DATABASENAME]" /DeploymentScriptFile:"[INSTALLLOCATION][DATABASENAME].sql"" Execute="immediate"/> <!--Define the custom action to execute vsdbcmd.exe--> <CustomAction Id="LaunchVsdbcmd" BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="deferred" Return="check" Impersonate="yes"/> <!--Define when the two custom actions will be executed--> <InstallExecuteSequence> <Custom Action="LaunchVsdbcmdCommandLine" Before="InstallFiles"/> <Custom Action="LaunchVsdbcmd" After="InstallFiles"/> </InstallExecuteSequence>
I found this code to be the most difficult part of the whole project to get right. The first custom action’s (LaunchVsdbcmdCommandLine) purpose is to create the command line string that the second custom action (LaunchVsdbcmd) will actually execute. LaunchVsdbcmdCommandLine declares the property LaunchVsdbcmd and sets its Value. The identifiers inside [] are properties that will get bound to their actual values when the custom action is executed. The “#” inside the property references gets expanded to the folder where the files are being installed to (aka INSTALLLOCATION). The second custom action LaunchVsdbcmd uses the property with the name that matches the custom action’s Id as the command line when it invokes the entry point CAQuietExec. The custom action LaunchVsdbcmd knows to do this because the Execute attribute is set to deferred. The <InstallExecuteSequence> element is pretty straightforward. It defines when the Windows Installer will actually execute each custom action. Windows Installer executes a series of “actions.” InstallFiles is one of those actions. I chose this action because I know the user has entered the values for SERVERNAME and DATABASENAME. After this action is complete I know all the files I need are installed on the user’s system.
There are numerous additional things that you will probably want to do in your MSI. For example, display your own EULA, make the project localizable or check that all of the VSDBCMD.EXE prerequisites exist on the system before attempting to run the utility. I leave the implementation of these additional features to you. If you would like to share your enhancements, implementation(s) or give me feedback on this blog post please attach a comment or send me a note: duke dot kamstra at microsoft dot com.