Click here to Skip to main content
Click here to Skip to main content

Deploy SQL Server databases easily with an Installer class

By , 6 Apr 2005
 

Sample Image - sqlscriptinstall.jpg

Introduction

If you made an application that is using an SQL Server database that needs to be located on the client server, VS.NET Setup Project doesn't help too much, you could go for InstallShild or other product that will make things easy but the costs will get higher. So searching for a free solution I've found an article on MSDN about using the Installer class and custom actions to make this happen. The code is in VB.NET, so this article is porting it to c# with some new features that I've found useful. All you need to do is to make a class derived from System.Configuration.Install and add to the solution two Embedded Resources named install.txt & uninstall.txt. The install.txt will contain the SQL script for your database and uninstall.txt the drop script. For the database script, I am using the ASPstate script made by Microsoft team for the ASP.NET InSQL session state:

< sessionState
mode ="SQLServer"
stateConnectionString ="tcpip=127.0.0.1:42424"
sqlConnectionString ="data source=aleph;User ID=ASPsession;Password=ASPsession;"
cookieless ="false"
timeout ="60"
/>

For my web application to work on a client server, I need to make a MSI with my app and the SQL script. To run the script at install time I've made a .dll named ScriptInstall, the code for it will follow.

Installer Class Code

First we declare a string that will have a default value and can be overwrite by the Install method:

string conStr="packet size=4096;integrated security=SSPI;"+
        "data source=\"(local)\";persist security info=False;"+
        "initial catalog=master";

I use two functions that will return the connection string and the script content:

private static string GetScript(string name)
{
    Assembly asm = Assembly.GetExecutingAssembly();
    Stream str = asm.GetManifestResourceStream(
                    asm.GetName().Name+ "." + name);
    StreamReader reader = new StreamReader(str);
    return reader.ReadToEnd();
}
private static string GetLogin(string databaseServer,
                 string userName,string userPass,string database)
{
    return "server=" + databaseServer + 
     ";database="+database+";User ID=" + userName +
     ";Password=" + userPass;
}

Then I use two functions that will run the install.txt and uninstall.txt onto the SQL server. The ExecuteSQL has a regex that splits the script after GO so that I can execute one by one with SQLCommand, I am doing this because ADO.NET will throw an exception if the SQL script contains "GO".

private static void ExecuteSql(SqlConnection sqlCon)
{
    string[] SqlLine;
    Regex regex = new Regex("^GO",RegexOptions.IgnoreCase | RegexOptions.Multiline);
    
    string txtSQL = GetScript("install.txt");
    SqlLine = regex.Split(txtSQL);

    SqlCommand cmd = sqlCon.CreateCommand();
    cmd.Connection = sqlCon;

    foreach(string line in SqlLine)
    {
        if(line.Length>0)
        {
            cmd.CommandText = line;
            cmd.CommandType = CommandType.Text;
            try
            {
                cmd.ExecuteNonQuery();
            }
            catch(SqlException)
            {
                //rollback
                ExecuteDrop(sqlCon);
                break;
            }
        }
    }
}
private static void ExecuteDrop(SqlConnection sqlCon)
{    
    if(sqlCon.State!=ConnectionState.Closed)sqlCon.Close();
    sqlCon.Open();
    SqlCommand cmd = sqlCon.CreateCommand();
    cmd.Connection = sqlCon;
    cmd.CommandText = GetScript("uninstall.txt");
    cmd.CommandType = CommandType.Text;
    cmd.ExecuteNonQuery();
    sqlCon.Close();
}

Having the functions now we can override the Install(IDictionary stateSaver) and Uninstall(IDictionary savedState). In the Install method besides running the SQL script on to the server I save the connection data submitted by the user. It's dangerous to save connection strings in clear, so I use RijndaelManaged to encrypt it. You can find the class in the source as well. I am saving the connection string because I need it at uninstall to drop the database ASPstate.

public override void Install(IDictionary stateSaver)
{
    base.Install (stateSaver);

    if(Context.Parameters["databaseServer"].Length>0 &&
        Context.Parameters["userName"].Length>0 &&
        Context.Parameters["userPass"].Length>0)
    {
        conStr = GetLogin(
            Context.Parameters["databaseServer"],
            Context.Parameters["userName"],
            Context.Parameters["userPass"],
            "master");

        RijndaelCryptography rijndael = new RijndaelCryptography();
        rijndael.GenKey();
        rijndael.Encrypt(conStr);
        //save information in the state-saver IDictionary
        //to be used in the Uninstall method
        stateSaver.Add("key",rijndael.Key);
        stateSaver.Add("IV",rijndael.IV);
        stateSaver.Add("conStr",rijndael.Encrypted);
    }

    SqlConnection sqlCon = new SqlConnection(conStr);

    sqlCon.Open();
    ExecuteSql(sqlCon);
    if(sqlCon.State!=ConnectionState.Closed)sqlCon.Close();
}

public override void Uninstall(IDictionary savedState)
{
    base.Uninstall (savedState);

    if(savedState.Contains("conStr"))
    {
        RijndaelCryptography rijndael = new RijndaelCryptography();
        rijndael.Key = (byte[])savedState["key"];
        rijndael.IV = (byte[])savedState["IV"];
        conStr = rijndael.Decrypt((byte[])savedState["conStr"]);            
    }

    SqlConnection sqlCon = new SqlConnection(conStr);

    ExecuteDrop(sqlCon);
}

Setup Project

Now that the installer class is done I can make a Setup project and add the primary output. In the User Interface Editor, select the Start node under Install. On the Action menu, choose Add Dialog. In the Add Dialog dialog box, select the Textboxes (A) dialog, then click OK to close the dialog box. On the Action menu, choose Move Up. Repeat until the Textboxes(A) dialog is above the Installation Folder node. Edit the properties of the Textboxes(A) form like this:

Textboxes(A) propeties

Go to the Custom Actions Editor and add the Primary output to the install and uninstall nodes. Click on the Primary output at the Install node and edit the properties. In the CustomActionData, type this:

/databaseServer=[EDITA1] /userName=[EDITA2] /userPass=[EDITA3]

I am getting the values of the Text Boxes in the Installer class using the Context.Parameters.

conStr = GetLogin(
        Context.Parameters["databaseServer"],
        Context.Parameters["userName"],
        Context.Parameters["userPass"],
        "master");

All is done now. Just build the two projects and you are ready to install the database. I hope you'll find this code useful, there are a lot of things that can be added to the code like storing safer the Key and IV of the Rijndael or using in the ExecuteSql transactions. Looking foreword to your comments.

History

  • Version 1.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Stefan Prodan
Technical Lead
Romania Romania
Member
I am a software architect who likes to develop under the .net framework. I am working with C# since 2004. Please visit my tech blog www.stefanprodan.eu.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 1memberleelaprasad0092 Jan '13 - 19:48 
its not working
GeneralRe: My vote of 1membersupernorb6 Apr '13 - 10:23 
Look below
GeneralMy vote of 5memberHernán Hegykozi29 Oct '12 - 13:02 
good article!
GeneralRe: My vote of 5membersupernorb6 Apr '13 - 10:23 
Look above
Questiondatbase connection live - then try to un intallmemberdacku_198724 Aug '11 - 3:04 
hey, its nice article but what if we have database connection active on that. ?
 
i have made .sql file but its still not working any other way out ?
darshan thacker

GeneralMy vote of 5memberKanasz Robert13 Nov '10 - 6:03 
Great article. Thanks a lot.
QuestionHow to use SqlInstall.dllmemberpavan_contractor6 Jul '09 - 1:14 
I have run setup project demo and SqlInstall.dll is generated at destination.Now how can i use this dll to run my database script?
Generalexport database on to another machinememberwaquasalig16 Oct '08 - 0:23 
Hi
I am not able to export database on to another machine using the setup.
I am using vc++(MFC) and sql express. How i will do it using script and where to write script.
 
thanx
Generalvalue cannot be null parameter name: streammemberjothy_cse14 Jul '08 - 22:03 
Hi i am encountering the following issue when i run setup.
 
"value cannot be null parameter name: stream". Hope not able to read the script file i guess.
 
Kindly provide the solution
AnswerRe: value cannot be null parameter name: streammemberSivarajselvakumar17 May '11 - 0:01 
Hi,
It seems u didn't change the Build Action property of the install.txt and uninstall.txt to embedded resource. Also verify that the assembly name and namespace are same.
GeneralThanksmembergirish_tinnu16 Apr '08 - 22:02 
Hi,
Your article was a great help in getting our setup project released.
Thanks a lot
GeneralAgain about GOmemberWin32nipuh13 Aug '07 - 19:54 
Good work.
 
One question:
how will work your regular expression parsing in this case?
 
create table go(a int, b varchar(50))
go
insert into go (1,b) select (1, '1')
/*go ?*/
go
if exists /*go*/ (select 1 from go where a=1)
begin -- go
-- go
select 1
end
 

Thank you,
Regards.
GeneralRe: Again about GOmemberStefan Prodan13 Aug '07 - 21:39 
The script has some problems, it will not work in SQL, maybe this is what you want:
 
create table go(a int, b varchar(50))
go
insert into go (a,b) select 1,'1'
/*go ?*/
go
if exists /*go*/ (select 1 from go where a=1)
begin -- go
-- go
select 1
end

 
For this script to work you need to change the regex like this:
Regex regex = new Regex("^GO?",RegexOptions.IgnoreCase | RegexOptions.Multiline);
 
I will update the article.
 
http://stefanprodan.wordpress.com

GeneralRe: Again about GOmemberWin32nipuh14 Aug '07 - 19:36 
Yes, you are right. It was only example of script, i didn't check it, sorry.
And thank you for the reply.
 
Smile | :)
GeneralRegEx ChangememberThe Punisher28 Feb '07 - 11:56 
The Regular expression needs one small change. It should read:
Regex regex = new Regex("^GO?",RegexOptions.IgnoreCase | RegexOptions.Multiline);
 
Instead of:
 
Regex regex = new Regex("^GO",RegexOptions.IgnoreCase | RegexOptions.Multiline);
 
See the difference??? It's the question mark after "GO". The question mark tells the Regular expression that is the absolute end... nothing after that. I tried and tried to get this to work with my install script and it just wouldn't work. It wouldn't work because I have tables with "GO" in the middle of the name of the table. So it would split on the name of my table. Took me all day to figure it out.
 
Hope this helps someone!
QuestionHow to stop installation if database is not validate?memberYulaw13 Jul '06 - 2:24 
How to stop installation if database is not validate?
AnswerRe: How to stop installation if database is not validate?memberkovalov9 May '07 - 14:59 
Yes. There is no validation of user input. Is there any way to stop installation?
GeneralRe: How to stop installation if database is not validate?membermikker_12331 May '07 - 7:07 
As I've saw from this and this article someone must in c++ make custom action that will validate connection to database and using ORCA create setup dialog that will fire that custom action.
 
Easier way is this shown in article with Installer class. If you can't validate credentials you can show modal window in which you'll accept credentials again... and if user can't provide right combination just throw exception and installer will rollback everything.
 
Good project that provides dialog with password field can be found here. If someone finds way to fire custom actions from MSI dialogs and handle them with C#, please provide link!
QuestionGO problem ;)memberbeatles169218 Feb '06 - 21:21 
Hi
Your article is very interesting but I'm here because I don't know how to solve this little GO problem in ADO.NET (It seems that this problem happens because of escape squences and I think it's a bug)
Your solution is a good one .I was familiar with osql.exe tool and its usage but I rather to use a managed code (as you mentioned)
I wonder if there is any other solutions to execute a large batch file using ADO.Net?
Thank you

 
Nima
QuestionReferences?memberEduard Ralph8 Dec '05 - 21:59 
Hi,
 
you mention that you found an article on the custom installer subject on MSDN. It might be good idea to add that as reference to the article. I like it otherwise, good work.
 
Greets,
Eduard
GeneralGreat article got my 5 + Customize the database namememberCohen Shwartz Oren4 Dec '05 - 22:08 
Hi Stefan,
 
Great article got my 5.
 
Can you help with the following missing feature Customizing of the database name.
 
Thanks in advnace
 
Oren.
GeneralMSI Repair Problemmemberdjspirit7 Nov '05 - 0:45 
Has anyone be having a problem using the repair option once the product has installed?
 
In short it appears to write over the '*.InstallState' file thus corrupting the remove option as well (InstallState file is rewritten excluding the user\pass details once you choose and run the repair option).
QuestionWeb ServicesmemberAsi BS11 Oct '05 - 0:39 
Thanks for the article, needed it badly.
It’s a new territory for me so …
 
Maybe you know how can I use the installer to install several web services in one shot?
 
Thanks Smile | :)
GeneralRegex &quot;^Go&quot; doesn't workmembershakoosh1 Aug '05 - 5:28 
I love the way you do the database installation it's really neat.
But when i tried it, the ReadToEnd() method returns one big string of the sql statements and all the carriage returns are converted to \r\n, and thus the regex doesn't catch "GO", coz it's not at the begining of a line.
 
so for example:
create table mytable(x int, y int, z int)
GO
insert into mytable values(x,y,z)
GO
 
is being read as
create table mytable(x int, y int, z int)\r\nGO\r\ninsert into mytable values(x,y,z)\r\nGO\r\n
 
any ideas???
thanks in advance

 
shakoosh
GeneralRe: Regex &quot;^Go&quot; doesn't workmemberMMaslyk22 Jun '06 - 2:03 
Regex(@"\r?\n[ \t]*GO[ \t]*\r?\n", RegexOptions.IgnoreCase) ?

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 6 Apr 2005
Article Copyright 2005 by Stefan Prodan
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid