Click here to Skip to main content
15,904,416 members
Articles / Database Development / SQL Server

Processing SQL Server FILESTREAM Data, Part 4 - Readin' and Writin'

Rate me:
Please Sign up or sign in to vote.
4.60/5 (5 votes)
14 Oct 2014CPOL3 min read 13.3K   6   11
In this final installment, we'll finally see some C# code that I used to read and write the FILESTREAM data.

In the prior installments in this series, I covered some background, FILESTREAM setup, and the file and table creation for this project. In this final installment, we'll finally see some C# code that I used to read and write the FILESTREAM data.

The Three "R"s

I was always amused by the irony that only one of the legendary Three "R"'s actually starts with an "R". Yet another indictment of American education? But I digress.

Before we work on the code to read FILESTREAM data, let's write to it first. First, we'll need a couple of structures to store information returned from various database operations.

C#
public class InsertResult
{
    public decimal Id { get; set; }
    public byte[] timestamp { get; set; }
}

public class FilestreamInsertResult
{
    public decimal Id { get; set; }
    public byte[] timestamp { get; set; }
    public string FilestreamPath { get; set; }
    public byte[] FilestreamContext { get; set; }
}

public class FilestreamSelectResult
{
    public string FilestreamPath { get; set; }
    public byte[] FilestreamContext { get; set; }
}

Then, we can create a routine that mimics an SMTP send, but instead stores the email information to the database tables we created in "Processing SQL Server FILESTREAM Data, Part 3 - Creating Tables". Pardon the formatting in order to make the overlong lines fit within the blog template.

C#
public bool Send( string fromAddress, string fromAlias, string recipients, 
                  string ccRecipients, string bccRecipients, 
                  string subject, string body, string[] attachments )
{
    try {
        using( IDbConnection connection = new SqlConnection( connectionString ) ) {
            connection.Open();

            using( IDbTransaction trans = connection.BeginTransaction() ) {
                try {
                    InsertResult insertId = connection.Query<insertresult>( 
                        @"INSERT INTO Notification.EmailMessages( 
                          TransmitStatusId, SubmitDate, TransmitDate, 
                          AttemptCount, FromAddress, FromAlias, ToAddresses, 
                          CcAddresses, BccAddresses, Subject, Body ) 
                          OUTPUT Inserted.EmailMessageId AS Id, Inserted.timestamp
                          VALUES( @TransmitStatusId, @SubmitDate, @TransmitDate, 
                          @AttemptCount, @FromAddress, @FromAlias, 
                          @ToAddresses, @CcAddresses, @BccAddresses, 
                          @Subject, @Body )", 
                          new
                          {
                              TransmitStatusId = Model.TransmitStatus.Queued,
                              SubmitDate = DateTime.Now,
                              TransmitDate = (System.Nullable<datetime>)null,
                              AttemptCount = 0,
                              FromAddress = fromAddress,
                              FromAlias = fromAlias,
                              ToAddresses = recipients,
                              CcAddresses = ccRecipients,
                              BccAddresses = bccRecipients,
                              Subject = subject,
                              Body = body
                          }, trans,
                          commandType: CommandType.Text ).FirstOrDefault();


                    if( attachments != null && attachments.Length > 0 ) {
                        for( int attachmentIdx = 0; attachmentIdx < attachments.Length; attachmentIdx++ ) {
                            FilestreamInsertResult filestreamId =
                                connection.Query<filestreaminsertresult>( 
                                    @"INSERT INTO Notification.EmailAttachments( EmailMessageId, 
                                      AttachmentFileId, SequenceNum, Filename, FileData ) 
                                      OUTPUT Inserted.EmailAttachmentId AS Id, 
                                      Inserted.timestamp, 
                                      Inserted.FileData.PathName() AS FilestreamPath,
                                      GET_FILESTREAM_TRANSACTION_CONTEXT() AS FilestreamContext
                                      VALUES( @EmailMessageId, NEWID(), @SequenceNum, @Filename, 0x00 )",
                                      new
                                      {
                                          EmailMessageId = insertId.Id,
                                          SequenceNum = attachmentIdx + 1,
                                          Filename = Path.GetFileName( attachments[ attachmentIdx ] )
                                      }, trans,
                                      commandType: CommandType.Text ).FirstOrDefault();

                            const int BUFSIZ = 32768;
                            using( Stream sqlFilestream = new SqlFileStream( 
                                      filestreamId.FilestreamPath, filestreamId.FilestreamContext, 
                                      FileAccess.Write ) ) {
                                using( FileStream infileStream = 
                                            File.Open( attachments[ attachmentIdx ], 
                                                       FileMode.Open, FileAccess.Read, 
                                                       FileShare.None ) ) {
                                    infileStream.CopyTo( sqlFilestream, BUFSIZ );
                                    infileStream.Close();
                                }
                                sqlFilestream.Close();
                            }
                        }
                    }

                    trans.Commit();
                }
                catch {
                    trans.Rollback();
                    throw;
                }
            }

            connection.Close();

            return true;
        }
    }
    catch( Exception ex ) {
        logger.Error( "Error in Send() method", ex );
        throw;
    }
}

A couple of notes about the code shown above:

  • The code uses Marc Gravell and Sam Saffron's superb Micro-ORM Dapper which I highly recommend. While religious wars rage over the use of Micro-ORMs vs heavy ORMs, I far prefer Dapper to other approaches.
  • The INSERT statements use the SQL Server OUTPUT clause to return ID information about the inserted rows, which is a more efficient method than sending a subsequent SELECT query for the information.
  • Once the streams have been opened, the .NET 4.0 CopyTo method will do a nice job of copying the bytes. If you're on an earlier version of the framework, this method can easily be created. See Jon Skeet's sample implementation here.

Once the email message has been inserted into the master table and we have its ID, we can then attempt to insert the attachments into their corresponding detail table. This is done in two steps:

  1. Insert the metadata about the attachment to the EmailAttachments table. Once this is complete, you can retrieve a file name and context ID for streaming attachment data to the FILESTREAM.
  2. Open the FILESTREAM using provided framework methods for doing so. Write the attachment data to the FILESTREAM.

Seems simple, but there is a subtlety. The INSERT statement to add the metadata must add at least one byte of data to the file using Transact-SQL. That is indicated by the null byte ("0x00") that is the last value of the statement. If you don't supply this, instead supplying NULL or, as I initially attempted, default, SQL Server will not create a file since you haven't given it any data. Consequently, the SQL Server PathName() function will return NULL and the call to open the SqlFileStream will fail unceremoniously.

There are two ways that I could have submitted the attachment data to SQL Server, as the last value of the INSERT statement to the EmailAttachments table, or using streaming as I did in the example. I chose the latter so that, in the case of very large attachment, I could stream the file in chunks rather than reading the entire file into memory to submit via INSERT statement. This is less resource intensive under the heavy load I expect for this utility.

I then created a separate Windows service to read the messages, attempt to send via SMTP, log successes and failures, and queue for retrying a certain number of times. The heart of the portion that reads the attachments looks quite similar to the write operation.

C#
public void GetAttachment( int attachmentId, string outfileName )
{
    try {
        using( IDbConnection connection = new SqlConnection( connectionString ) ) {
            connection.Open();

            using( IDbTransaction trans = connection.BeginTransaction() ) {
                try {
                    FilestreamSelectResult fileInfo =
                        connection.Query<filestreamselectresult>( 
                                @"SELECT FileData.PathName() AS FilestreamPath, 
                                  GET_FILESTREAM_TRANSACTION_CONTEXT() AS FilestreamContext
                                  FROM Notification.EmailAttachments 
                                  WHERE EmailAttachmentId = @EmailAttachmentId",
                                  new
                                  {
                                      EmailAttachmentId = attachmentId
                                  },
                                  transaction: trans,
                                  commandType: CommandType.Text).FirstOrDefault();

                    const int BUFSIZ = 32767;
                    using(FileStream outfileStream = File.Open( 
                            outfileName, FileMode.Create, 
                            FileAccess.Write, FileShare.None)) {

                        using( Stream sqlFilestream = 
                                    new SqlFileStream( fileInfo.FilestreamPath, 
                                            fileInfo.FilestreamContext, FileAccess.Read ) ) {
                            sqlFilestream.CopyTo( outfileStream, BUFSIZ )
                            sqlFilestream.Close();
                        }
                        outfileStream.Close();
                    }

                    connection.Close();

                }
                catch {
                    // Log error here
                    throw;
                }
            }

            connection.Close();
        }
    }
    catch( Exception ex ) {
        logger.Error( "Error in GetAttachment method.", ex );
        throw;
    }
}
</filestreamselectresult>

Some notes about the code shown above:

  • I created a result class, shown earlier in this post, for retaining the file path and transaction context returned from the query.
  • Note that you must create a transaction for the SELECT in order for the GET_FILESTREAM_TRANSACTION_CONTEXT method to return a context that can be used in the SqlFileStream constructor.
  • Once again, I have used the CopyTo method to move the bytes between the streams.

Summary

That finishes the heart of the SQL Server FILESTREAM operations for the utility I was constructing. The real trick of it was the initial configuration and understanding the process. Hopefully, this series of articles will help someone past the problems I encountered. Good luck and good coding!

License

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


Written By
CEO RAMSoft Solutions, Inc
United States United States
Bob McGowan is the founder and CEO of RAMSoft Solutions and has led the company since its inception in 1993. He has over 30 years of experience managing projects and developing applications software for microcomputers. He has managed multi-million dollar software development projects for many Fortune 100 companies, including Chase Manhattan Bank, Prudential Insurance, and True North Media Services and has acted as a senior technical advisor for dozens of other projects.

Bob has been developing software for microcomputers since 1980, and has developed applications for many companies using a wide variety of development tools, languages, architectures, and operating systems. In 2004 he co-founded an information technology services company specializing in the Yellow Pages Advertising industry. As Chief Technology Officer he architected and implemented a service allowing agencies to view electronic "tear pages" via web services or interactively via a web site. The process included an optical character recognition (OCR) workflow automation process in for scanning, OCR, and review of Yellow Pages telephone directories. After 12 months of operation the database contained over 1.6 million scanned pages and accompanying extracted text in a searchable format.

He continues to be passionate about software development and technology in general. You can follow his thoughts on software development at http://blog.ramsoftsolutions.com

Comments and Discussions

 
GeneralMy vote of 2 Pin
kiberg21-Oct-14 0:55
professionalkiberg21-Oct-14 0:55 
GeneralRe: My vote of 2 Pin
Bob McGowan21-Oct-14 2:55
Bob McGowan21-Oct-14 2:55 
GeneralRe: My vote of 2 Pin
kiberg21-Oct-14 3:51
professionalkiberg21-Oct-14 3:51 
Thanks for your comment. I changed my vote. I really dislike inline SQL. It's the only reason for my vote.
I am waiting a new articles from you Smile | :)
GeneralRe: My vote of 2 Pin
Bob McGowan21-Oct-14 4:25
Bob McGowan21-Oct-14 4:25 
GeneralRe: My vote of 2 Pin
kiquenet.com20-Mar-18 13:41
professionalkiquenet.com20-Mar-18 13:41 
QuestionThis was a blog... Pin
OriginalGriff14-Oct-14 4:47
mveOriginalGriff14-Oct-14 4:47 
AnswerRe: This was a blog... Pin
Bob McGowan14-Oct-14 8:37
Bob McGowan14-Oct-14 8:37 
GeneralRe: This was a blog... Pin
OriginalGriff14-Oct-14 8:52
mveOriginalGriff14-Oct-14 8:52 
GeneralRe: This was a blog... Pin
Bob McGowan15-Oct-14 8:04
Bob McGowan15-Oct-14 8:04 
GeneralRe: This was a blog... Pin
OriginalGriff15-Oct-14 8:16
mveOriginalGriff15-Oct-14 8:16 
GeneralRe: This was a blog... Pin
Bob McGowan15-Oct-14 9:10
Bob McGowan15-Oct-14 9:10 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.