Working on a new project I had a need to integrate Entity Framework 6 migrations as part of the installation package. My goal is simple, a single installer that migrates and rolls back the database as part of it’s installation procedure. The key to making this work is a utility that is part of the EF6 Nuget package – Migrate.exe.
To find this tool, simply navigate to your solution folder, then packages, then EntityFramework.<version>, then tools. Here you will find several files, one of which is the migrate.exe tool being referenced here.
MSDN provides a healthy dose of information on the specifics of how to use migrate.exe, but for my purposes, I am going to collect the connection string from the user. The software in my case supports only SQL Server, so I can default the provider as such. So, this makes the “provide connection string” approach — the last one in the linked article — the perfect option for my needs.
Armed with this tool I went to work creating my UI dialog for WiX. This dialog will allow me to capture the connection string information. I had previously used an article from code project to learn about the WiX dialogs. I used that same code here in order to create my connection string dialog.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<Fragment> <UI> <Dialog Id="ConnectionStringDlg" Width="370" Height="270" Title="Database Settings - [ProductName]" NoMinimize="yes"> <!-- Connection String --> <Control Id="ConnectionStringLabel" Type="Text" X="45" Y="73" Width="100" Height="15" TabSkip="no" Text="SQL Server &Connection String:" /> <Control Id="ConnectionStringEdit" Type="Edit" X="45" Y="95" Width="220" Height="18" Property="CONNECTION_STRING" Text="{200}" /> <!-- Back button --> <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="&Back"> <Publish Event="NewDialog" Value="WelcomeDlg">1</Publish> </Control> <!-- Next Button --> <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="&Next"> <Publish Event="NewDialog" Value="SetupTypeDlg"> <!--if settings are correct, allow next dialog--> <![CDATA[CONNECTION_STRING <> ""]]> </Publish> </Control> <!-- Cancel Button --> <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 database configuration</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}Database Settings</Text> </Control> <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" /> </Dialog> </UI> </Fragment> |
With the dialog fragment in place. It was a fairly simple matter to integrate it into the WiX workflow. Note: I am using WixUI_Mondo as my UI.
1 2 3 4 5 6 7 8 9 10 |
<UI Id='Mondo'> <UIRef Id="WixUI_Mondo" /> <UIRef Id="WixUI_ErrorProgressText" /> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="ConnectionStringDlg" Order="3">1</Publish> <Publish Dialog="ConnectionStringDlg" Control="Next" Event="NewDialog" Value="SetupTypeDlg" Order="3">1</Publish> <Publish Dialog="SetupTypeDlg" Control="Back" Event="NewDialog" Value="ConnectionStringDlg" Order="3">1</Publish> </UI> |
This created the desired result.
Now it was a matter of using Migrate.exe to push the entity information out. In order to do this I needed a custom action. So I added a new CA project to my solution, and referenced it in my installer. For brevity of this post, I’ve removed all of the logging from this object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
[CustomAction] public static ActionResult MigrateEntityData(Session session) { string assemblyPath = session.CustomActionData["ExecutablePath"]; // Basically because connection strings use the ';' as the sentinel and // so does the installer, we have to assume everything that is not an // otherwise known value is part of the connection string. string connectionString = session.CustomActionData["ConnectionString"]; List<string> knownKeys = new List<string>( new string[] { "ConnectionString", "ExecutablePath" }); foreach (var k in session.CustomActionData.Keys) { if (!knownKeys.Contains(k)) { // Put the ; in front as the string should have been initialized // to contain "data source=<value>". if (!connectionString.EndsWith(";")) { connectionString += ";"; } // Now, don't add it to the end because we don't want one at the end // of the connection string. connectionString += k + "=" + session.CustomActionData[k]; } } // Just to make sure we are good, make sure the path is there before trying if (!System.IO.Directory.Exists(assemblyPath)) { throw new InvalidOperationException( "Error setting up database, the provided assembly path is invalid. '" + assemblyPath + "'"); } // Execute the migration string commandText = assemblyPath.EndsWith(@"") ? assemblyPath: assemblyPath + @""; commandText += "Migrate.exe"; System.Diagnostics.Process migrate = new System.Diagnostics.Process(); migrate.StartInfo = new System.Diagnostics.ProcessStartInfo() { WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden, FileName = commandText, Arguments = string.Format( "{0} /connectionString="{1}"" + " /connectionProviderName="System.Data.SqlClient"", "NameOfCustomActionAssembly.dll", connectionString), RedirectStandardError = true, RedirectStandardInput = true, RedirectStandardOutput = true, UseShellExecute = false }; migrate.Start(); migrate.WaitForExit(); // TODO: Do any cleanup (e.g. delete the temp directory) return ActionResult.Success; } |
A few key points about this method:
- It requires some property values to be passed into the custom action as part of the WiX markup, particularly the connection string – which comes from our custom dialog, and the executable path – which comes from the directory structure definition.
12345678910<SetProperty Id="ExecuteEfMigration"Value="ExecutablePath=[INSTALLFOLDER];ConnectionString=[CONNECTION_STRING]"Sequence="execute"Before="ExecuteEfMigration"/><CustomAction Id="ExecuteEfMigration"BinaryKey="PrimaryCaBin"Execute="deferred"DllEntry="MigrateEntityData"HideTarget="yes"/> - Because I’m working with a connection string, I have to do some trickery as the ‘;’ plays havoc on the property values being passed in because the CA uses this same sentinel as a delimiter for the values. This basically means if I have a connection string “data source=localhost;initial catalog=database_name;integrated security=true” it will come through as multiple values into the custom action, splitting on the ‘;’.
- It will execute a shell (i.e. a command prompt window will popup during installation). For me this was ok, and beats the alternative – which is: You can opt to not use shell execute. However, this means you also cannot redirect output, which means you cannot log output in a verbose debugger session (msiexec /l switch).
When I get the chance, I’ll try to post a sample on my github page, and update this post with the link.