Click here to Skip to main content
15,886,032 members
Articles / Desktop Programming / WPF

Large Data Upload via WCF

Rate me:
Please Sign up or sign in to vote.
4.80/5 (16 votes)
19 May 2017CPOL4 min read 41.8K   2K   39   14
Efficiently uploading large data files in chunks via WCF Service, and storing data in SQL Server using Entity Framework

 

Background

Transferring large volumes of data over the wire has always been a challenge. There are several approaches exist that try solving this problem from different angles. Each of those solutions suggests different usage scenarios and has pros and cons. Here are 3 major options:

  • FTP. Although FTP is fast and efficient, it is not easy to use. It does not have well designed API or object model around it, it has issues with firewall, and requires long sessions to be opened during data transfer, which are not supposed to be interrupted. In addition, FTP infrastructure requires configuration and maintenance efforts.
  • Streaming. Streaming is efficient solution for transferring large amounts of data over HTTP or TCP. However, it does not support reliable messaging and not very well scalable for high traffic scenarios.
  • Chunking. This solution, among all others, has some overhead associated with chunking data and reconstituting it back into single stream. However, most parts of the overhead may be eliminated by having efficient architectural design. Chunking solution is ideal for large data transfer in high traffic scenarios, can support reliable messaging, security on both message and transport levels, offers efficient memory utilization, and suggests a straightforward way of implementing partial uploads in case of connection failures.

In this article, we will walk through one of the possible implementations of Chunking solution.

Introduction

Current solution consists of Client (WPF UI application) and Server (WCF Console application). On the server side, we have WCF Service exposing several methods to manipulate data files. It is using Entity Framework Code First approach to talk to SQL Server to persist data.

On the database side, we got two tables - BlobFiles and BlobFileChunks. Every record in BlobFiles table corresponds to the description of uploaded file and includes its Id (Guid), Name, Description, Size, User Id created, and creation timestamp. BlobFileChunks, on the other hand, is a detail table referencing BlobFiles by foreign key. Every record in this table represents a chunk of data in binary format as well as its sequential number (position) in the original file stored in the ChunkId column.

On the client side, we have a grid displaying Data Files pulled from the database, and few buttons allowing to refresh data, upload a new file, remove existing file, as well as calculate file hash code and save file to disk. The latter two actions are purely demonstrational and serve the purpose of emulating actual consumption of file content on the server side.

Using the Code

Before using the code, make sure that connection settings configured correctly in server's App.config file. By default, it is using local instance of SQL Server with Integrated Security:

<entityFramework>
  <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
    <parameters>
      <parameter value="Data Source=.;Integrated Security=True;MultipleActiveResultSets=True;" />
    </parameters>
  </defaultConnectionFactory>
  <providers>
    <provider invariantName="System.Data.SqlClient" 
              type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
  </providers>
</entityFramework>

Since project contains EF configured with automatic DB Migrations, the database is going to be created automatically, on the first run.

Without getting too deep into details, let's focus on major points of the solution - the mechanism of uploading chunks and later combining them together.

Splitting into Chunks

On the client side, the following code used to upload chunks to the server:

using (var service = new FileUploadServiceClient())
{
  var fileId = await service.CreateBlobFileAsync(Path.GetFileName(fileName),
    fileName, new FileInfo(fileName).Length, Environment.UserName);

  using (var stream = File.OpenRead(fileName))
  {
    var edb = new ExecutionDataflowBlockOptions {BoundedCapacity = 5, MaxDegreeOfParallelism = 5};

    var ab = new ActionBlock<Tuple<byte[], int>>(x => service.AddBlobFileChunkAsync(fileId, x.Item2, x.Item1), edb);

    foreach (var item in stream.GetByteChunks(chunkSize).Select((x, i) => Tuple.Create(x, i)))
      await ab.SendAsync(item);

    ab.Complete();

    await ab.Completion;
  }
}

As you can see, ActionBlock class (part of great library TPL Dataflow) has been used in the code, which abstracts away asynchronous parallel processing, allowing to send maximum 5 chunks at a time (MaxDegreeOfParallelism) and maintaining a buffer of 5 chunks in the memory (BoundedCapacity).

By calling CreateBlobFileAsync, we create master record with file details and obtain BlobFileId. We then open a File Stream, call an extension method GetByteChunks that returns an IEnumerable<byte[]>. This enumerable is evaluated on the fly and every element contains a byte array of specified size (obviously, the last element may contain less data). Chunk size by default is set to 2MB and can be adjusted according to the usage scenario. Those chunks together with their sequential IDs are sent to the server in parallel via AddBlobFileChunkAsync method. Note that the chunks do not necessarily have to be sent one after another as long as their sequential IDs represent their actual positions in the file.

Implementation of GetByteChunks extension method is presented below:

 public static IEnumerable<byte[]> GetByteChunks(this Stream stream, int length)
{
  if (stream == null)
    throw new ArgumentNullException("stream");

  var buffer = new byte[length];
  int count;
  while ((count = stream.Read(buffer, 0, buffer.Length)) != 0)
  {
    var result = new byte[count];
    Array.Copy(buffer, 0, result, 0, count);
    yield return result;
  }
}

Combining the Chunks

Let's explore ProcessFileAsync method on the server side:

 public async Task<string> ProcessFileAsync(Guid blobFileId)
{
  List<int> chunks;
  using (var context = new FileUploadDemoContext())
  {
    chunks = await context.Set<BlobFileChunks>()
      .Where(x => x.BlobFileId == blobFileId)
      .OrderBy(x => x.ChunkId)
      .Select(x => x.ChunkId)
      .ToListAsync();
  }

  var result = 0;

  using (var stream = new MultiStream(GetBlobStreams(blobFileId, chunks)))
  {
    foreach (var chunk in stream.GetByteChunks(1024))
      result = (result*31) ^ ComputeHash(chunk);
  }

  return string.Format("File Hash Code is: {0}", result);
}

This method reads entire file content in a lazy manner and calculates its hash code. First, we retrieve all chunks associated with given BlobFileId sorted by ChunkId from database. Those chunk IDs then passed to GetByteChunks helper method, which retrieves actual binary content of those chunks from database on demand, one after another.

The key role in combining chunks together plays MultiStream class described below.

MultiStream Class

MultiStream is inhereted from standard Stream class and represents a read-only stream with ability to seek forward. The Seek method ensures that position never gets backwards:

public override long Seek(long offset, SeekOrigin origin)
{
  switch (origin)
  {
    case SeekOrigin.Begin:
      m_position = offset;
      break;
    case SeekOrigin.Current:
      m_position += offset;
      break;
    case SeekOrigin.End:
      m_position = m_length - offset;
      break;
  }

  if (m_position > m_length)
    m_position = m_length;

  if (m_position < m_minPosition)
  {
    m_position = m_minPosition;
    throw new NotSupportedException("Cannot seek backwards");
  }

  return m_position;
}

In the Read method, we pull chunks on "as needed" basis until we reach the last one. This way only one chunk at a time is held in memory, which very efficient and scalable:

public override int Read(byte[] buffer, int offset, int count)
{
  var result = 0;

  while (true)
  {
    if (m_stream == null)
    {
      if (!m_streamEnum.MoveNext())
      {
        m_length = m_position;
        break;
      }
      m_stream = m_streamEnum.Current;
    }

    if (m_position >= m_minPosition + m_stream.Length)
    {
      m_minPosition += m_stream.Length;
      m_stream.Dispose();
      m_stream = null;
    }
    else
    {
      m_stream.Position = m_position - m_minPosition;
      var bytesRead = m_stream.Read(buffer, offset, count);
      result += bytesRead;
      offset += bytesRead;
      m_position += bytesRead;
      if (bytesRead < count)
      {
        count -= bytesRead;
        m_minPosition += m_stream.Length;
        m_stream.Dispose();
        m_stream = null;
      }
      else
        break;
    }
  }

  return result;
}

Conclusion

  • Current approach requires both Client and Server to be aware of protocol
  • Solution is well suited for intranet and internet scenarios
  • DB persistence layer can be easily replaced by File System if needed
  • Reliable messaging can be easily implemented on top of current architecture
  • There is a lot of room for improvement, so please feel free to post your suggestions or concerns.

License

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


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionUnable to locate NuGet.exe Pin
ugo.marchesini13-Sep-18 4:49
professionalugo.marchesini13-Sep-18 4:49 
QuestionExcelent Pin
Juan Francisco Morales Larios23-May-17 22:25
Juan Francisco Morales Larios23-May-17 22:25 
GeneralMy vote of 5 Pin
EvelineGarde21-May-17 3:19
EvelineGarde21-May-17 3:19 
GeneralMy vote of 5 Pin
Humayun Kabir Mamun29-Sep-15 18:31
Humayun Kabir Mamun29-Sep-15 18:31 
AnswerOOM Errors Pin
Jeff Bowman4-Apr-15 13:34
professionalJeff Bowman4-Apr-15 13:34 
GeneralRe: OOM Errors Pin
Yuriy Magurdumov7-Apr-15 4:39
Yuriy Magurdumov7-Apr-15 4:39 
GeneralRe: OOM Errors Pin
Jeff Bowman7-Apr-15 13:06
professionalJeff Bowman7-Apr-15 13:06 
QuestionFixed Progress Possible Pin
Jeff Bowman31-Mar-15 17:08
professionalJeff Bowman31-Mar-15 17:08 
AnswerRe: Fixed Progress Possible Pin
Jeff Bowman31-Mar-15 21:45
professionalJeff Bowman31-Mar-15 21:45 
GeneralRe: Fixed Progress Possible Pin
Jeff Bowman1-Apr-15 0:30
professionalJeff Bowman1-Apr-15 0:30 
AnswerRe: Fixed Progress Possible Pin
Yuriy Magurdumov7-Apr-15 4:52
Yuriy Magurdumov7-Apr-15 4:52 
GeneralRe: Fixed Progress Possible Pin
Jeff Bowman7-Apr-15 13:09
professionalJeff Bowman7-Apr-15 13:09 
QuestionMessage level security? Pin
Marakai16-Nov-14 21:03
Marakai16-Nov-14 21:03 
AnswerRe: Message level security? Pin
Yuriy Magurdumov17-Nov-14 3:51
Yuriy Magurdumov17-Nov-14 3:51 

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.