Monday, December 5, 2011

Enable FILESTREAM during SQL Express 2008 R2 Unattended Setup

A previous post about unattended setup explained about how to install SQL Server Express 2008 R2 in unattended mode. This post explains how to enable FILESTREAM by adding some additional parameters to it. FILESTREAM can be enabled either during SQL installation or by using SQL Configuration Manager after installtion. (And it can also be enabled by using SQL WMI provider. So all kinds of method using SQL WMI such as Powershell, C#, VBscript can be also used for that purpose.)
In order to enable FILESTREAM during SQL setup, both FILESTREAMLEVEL and FILESTREAMSHARENAME should be specified.

C> SETUP.exe /ACTION=Install /INSTANCENAME=SQLExpress
/FEATURES=SQLENGINE /QS /IACCEPTSQLSERVERLICENSETERMS=true
/SQLSVCSTARTUPTYPE=Automatic /SQLSVCACCOUNT="NT AUTHORITY\SYSTEM"
/BROWSERSVCSTARTUPTYPE=Disabled /ADDCURRENTUSERASSQLADMIN=true
/TCPENABLED=1 /HIDECONSOLE
/FILESTREAMLEVEL=3 /FILESTREAMSHARENAME=SQLEXPRESS


The command above installs SQL Server Engine only in silent mode and named instance (.\SQLExpress) will be created. It enables FILESTREAM with full access level.

Friday, December 2, 2011

Using SqlFileStream and LINQ togather

LINQ to SQL provides consistent way of accessing SQL database. It is possible to insert BLOB data into SQL FILESTREAM column by using LINQ insertion. However, in order to take benefit of fast FILESTREAM insertion, we have to use SqlFileStream instead of regular LINQ insertion. This post shows an example of using SqlFileStream and LINQ togather in one transaction.
First, this is a sample table that includes FILESTREAM column. FileStream column requires a ROWGUIDCOL, so we have RowId in the table.
CREATE TABLE MyTable
(
[Name] NVARCHAR(50) PRIMARY KEY,
-- ROWGUID column is required
[RowId] [uniqueidentifier] ROWGUIDCOL NOT NULL UNIQUE,
-- This is FILESTREAM column
[Data] VARBINARY(MAX) FILESTREAM NULL
)

The following code (Save() method) shows how to use LINQ and SqlFileStream togather in a single transaction. Multiple table insertion by LINQ can be performed, even if the sample only shows one table insertion.
class Program
{
   static void Main(string[] args)
   {
      Program p = new Program();
      p.Name = "Tom";
      p.FileName = @"C:\temp\Book1.xls";
      p.Save();
   }

   public string Name { get; set; }
   public string FileName { get; set; }

   void Save()
   {
      DbTransaction trans = null;
      MyDataClassesDataContext db = new MyDataClassesDataContext();
      try
      {
         db.Connection.Open();
         trans = db.Connection.BeginTransaction();
         db.Transaction = trans;

         // Insert data into Table
         // Add empty data to FILESTREAM column
         Guid rowId = Guid.NewGuid();
         var tab1 = new MyTable
         {
            Name = this.Name,
            RowId = rowId,
            Data = new System.Data.Linq.Binary(new byte[] { })
         };
         db.MyTables.InsertOnSubmit(tab1);
         
         // Can have more table insertion here.
         // ...

         db.SubmitChanges();

         // Save data file to FILESTREAM column by using SqlFileStream
         SqlConnection conn = db.Connection as SqlConnection;
         SqlCommand cmd = new SqlCommand("SELECT Data.PathName() FROM MyTable WHERE [Name]=@name", conn);
         cmd.Transaction = db.Transaction as SqlTransaction;

         SqlParameter paramName = new SqlParameter("@name", System.Data.SqlDbType.NVarChar, 50);
         paramName.Value = this.Name;
         cmd.Parameters.Add(paramName);
         object path = cmd.ExecuteScalar();
         if (path == DBNull.Value)
         {
            throw new InvalidOperationException();
         }

         cmd.CommandText = "SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()";
         byte[] transCtx = cmd.ExecuteScalar() as byte[];

         // Actual Insert into FILESTREAM
         SqlFileStream sqlFS = new SqlFileStream(path.ToString(), transCtx, FileAccess.ReadWrite);
         byte[] bytes = File.ReadAllBytes(this.FileName);
         sqlFS.Write(bytes, 0, bytes.Length);
         sqlFS.Close();

         trans.Commit();
      }
      catch
      {
         if (trans != null)
         {
            trans.Rollback();
         }
         throw;
      }
      finally
      {
         if (db.Connection != null && db.Connection.State == System.Data.ConnectionState.Open)
         {
            db.Connection.Close();
         }
      }
   }
}
SqlFileStream needs to acquire PathName (which is logical UNC of FILESTREAM column location) and transaction context before using SqlFileStream. So some SELECT statement was sent to SQL Server first. And then, SqlFileStream actually wrote BLOB data by using SqlFileStream.Write() method.
Once all data are inserted, Commit() method is called and finished the transaction.