In this series of articles, I will walk through the process of creating a WCF service and client application which can be used to transfer large files (several gigabytes or more) between the client and the server. The process will be broken down into the following stages:
- Creating the Server Application - 1 (this article)
- Creating the Server Application - 2
- Creating the Client Application
- Creating an Installer for the Server
- Adding Extra Functionality
Disclaimer
Before we begin, it’s probably a good idea to say that I am not an expert on WCF. There are probably many other ways to implement this solution, and I do not claim that what is presented here is the best, or even one of the better, implementations. That being said, it works, and that’s a good start. Please feel free to write a comment if you feel there are areas where the code needs improvement.
Creating the Server Application
I will be using Visual Studio 2008 and .NET 3.5 for this tutorial. As is my preference, I’ll start by creating a new Blank Visual Studio Solution, called WCFFileStreamer. In here, add a new WCF Service Library, named WCFFileStreamerService.
Shared Types
By default, the WCF Service Library includes an Interface called IService1 to define the Service and Data Contracts. The ‘correct’ method of sharing these types between the client and server is to import a service reference to the client, but I have run into problems with this on previous projects so will manually configure this interaction. A couple advantages of this are that it enables more control over the interactions, and that it gives us a better feel for what happens ‘under the covers’ in WCF.
Add a new project to the solution, in this case just a Class Library named WCFFileStreamerTypes. There will be a number of enums, classes and interfaces to create, but they’re all quite small and have no logic in them, so feel free to either create one file per enum / class / interface, or just throw everything in one file. You’ll also need to add references to System.ServiceModel and System.Runtime.Serialization. Here are the enums you’ll need:
[DataContract(Namespace = "http://schemas.my.service.com/2009/07/filestreamer/")]
public enum UploadStatus : int
{
[EnumMember] Complete = 0,
[EnumMember] Uploading = 1,
[EnumMember] CalculatingHash = 2,
[EnumMember] Idle = 3
}
[DataContract(Namespace = "http://schemas.my.service.com/2009/07/filestreamer/")]
public enum InitStatus : int
{
[EnumMember] Ready = 0,
[EnumMember] ExistsAndResumable = 1,
[EnumMember] ExistsMatchingHash = 2,
[EnumMember] ExistsNonMatchingHash = 3
}
[DataContract(Namespace = "http://schemas.my.service.com/2009/07/filestreamer/")]
public enum UploadMode : int
{
[EnumMember] New = 0,
[EnumMember] Overwrite = 1,
[EnumMember] Resume = 2
}
[DataContract(Namespace = "http://schemas.my.service.com/2009/07/filestreamer/")]
public enum UploadResult : int
{
[EnumMember] Success = 0,
[EnumMember] Failure = 1
}
The following classes will be required as Data Contracts:
[DataContract(Namespace = "http://schemas.my.service.com/2009/07/filestreamer/")]
[KnownType(typeof(InitStatus))]
public class InitializeStatus
{
[DataMember(IsRequired = true)] public InitStatus Status { get; set; }
[DataMember(IsRequired = true)] public Guid UploadID { get; set; }
[DataMember(IsRequired = true)] public long Length { get; set; }
[DataMember(IsRequired = true)] public long ServerLength { get; set; }
}
[DataContract(Namespace = "http://schemas.my.service.com/2009/07/filestreamer/")]
[KnownType(typeof(UploadResult))]
public class UploadResultMessage
{
[DataMember(IsRequired = true)] public UploadResult Result { get; set; }
[DataMember(IsRequired = true)] public string Message { get; set; }
}
[DataContract(Namespace = "http://schemas.my.service.com/2009/07/filestreamer/")]
[KnownType(typeof(UploadStatus))]
public class UploadStatusMessage
{
[DataMember(IsRequired = true)] public UploadStatus Status { get; set; }
[DataMember(IsRequired = true)] public int PercentComplete { get; set; }
[DataMember(IsRequired = true)] public string Message { get; set; }
}
[DataContract(Namespace = "http://schemas.my.service.com/2009/07/filestreamer/")]
public class InitDownloadMessage
{
[DataMember(IsRequired = true)] public bool IsValidFile { get; set; }
[DataMember(IsRequired = true)] public string FileName { get; set; }
[DataMember(IsRequired = true)] public long Length { get; set; }
[DataMember(IsRequired = true)] public byte[] FileHash { get; set; }
}
Add the following Message Contracts:
[MessageContract(IsWrapped = false)]
public class InitUploadRequest
{
[MessageHeader(MustUnderstand = true)] public string LicenseKey { get; set; }
[MessageBodyMember] public string FileName { get; set; }
[MessageBodyMember] public long Length { get; set; }
[MessageBodyMember] public byte[] FileHash { get; set; }
}
[MessageContract(IsWrapped = false)]
[KnownType(typeof(InitializeStatus))]
public class InitUploadResponse
{
[MessageBodyMember] public InitializeStatus Status { get; set; }
}
[MessageContract(IsWrapped = false)]
[KnownType(typeof(UploadMode))]
public class UploadStreamRequest
{
[MessageHeader(MustUnderstand = true)] public string LicenseKey { get; set; }
[MessageHeader] public Guid UploadID { get; set; }
[MessageHeader] public UploadMode Mode { get; set; }
[MessageHeader] public string FileName { get; set; }
[MessageHeader] public long Length { get; set; }
[MessageHeader] public long StartOffset { get; set; }
[MessageHeader] public byte[] FileHash { get; set; }
[MessageBodyMember] public Stream DataStream { get; set; }
}
[MessageContract(IsWrapped = false)]
[KnownType(typeof(UploadResultMessage))]
public class UploadStreamResponse
{
[MessageHeader] public UploadResultMessage ResultMessage { get; set; }
}
[MessageContract(IsWrapped = false)]
public class GetUploadStatusRequest
{
[MessageHeader(MustUnderstand = true)] public string LicenseKey { get; set; }
[MessageBodyMember] public Guid UploadID { get; set; }
}
[MessageContract(IsWrapped = false)]
[KnownType(typeof(UploadStatusMessage))]
public class GetUploadStatusResponse
{
[MessageBodyMember] public UploadStatusMessage StatusMessage { get; set; }
}
[MessageContract(IsWrapped = false)]
public class CancelUploadAsyncRequest
{
[MessageHeader(MustUnderstand = true)] public string LicenseKey { get; set; }
[MessageBodyMember] public Guid UploadID { get; set; }
}
[MessageContract(IsWrapped = false)]
public class InitDownloadRequest
{
[MessageHeader(MustUnderstand = true)] public string LicenseKey { get; set; }
[MessageBodyMember] public string FileName { get; set; }
[MessageBodyMember] public bool CalculateHash { get; set; }
}
[MessageContract(IsWrapped = false)]
[KnownType(typeof(InitDownloadMessage))]
public class InitDownloadResponse
{
[MessageBodyMember] public InitDownloadMessage DownloadMessage { get; set; }
}
[MessageContract(IsWrapped = false)]
public class DownloadStreamRequest
{
[MessageHeader(MustUnderstand = true)] public string LicenseKey { get; set; }
[MessageHeader] public string FileName { get; set; }
[MessageHeader] public long Length { get; set; }
[MessageHeader] public long StartOffset { get; set; }
[MessageHeader] public byte[] FileHash { get; set; }
}
[MessageContract(IsWrapped = false)]
public class DownloadStreamResponse
{
[MessageHeader] public string FileName { get; set; }
[MessageHeader] public long Length { get; set; }
[MessageHeader] public long StartOffset { get; set; }
[MessageHeader] public byte[] FileHash { get; set; }
[MessageBodyMember] public Stream DataStream { get; set; }
}
[MessageContract(IsWrapped = false)]
public class GetFreeSpaceRequest
{
[MessageHeader(MustUnderstand = true)] public string LicenseKey { get; set; }
}
[MessageContract(IsWrapped = false)]
public class GetFreeSpaceResponse
{
[MessageBodyMember] public long FreeSpace { get; set; }
}
A couple of things are worth noticing in the above Message Contracts. The first thing is that every request includes a LicenseKey string in the Header. This is a fairly easy way to provide some security to our service. You can either hard code a word or phrase into your service and client (as we will do here), or you could use some other method.
The second thing to notice is the liberal use of MessageHeader parameters in the UploadStreamRequest / UploadStreamResponse contracts. This reason for this will become clear later.
Finally we can create the Service Contracts. Note that there are two interfaces specified for the server. I will talk about the reasons for this more in the next article.
[ServiceContract(Name = "StreamedFileTransferService",
Namespace = "http://www.my.service.com/2009/07/filestreamer/")]
public interface IStreamedFileTransferService
{
[OperationContract]
[FaultContract(typeof(string))]
UploadStreamResponse UploadStream(UploadStreamRequest request);
[OperationContract]
[FaultContract(typeof(string))]
DownloadStreamResponse DownloadStream(DownloadStreamRequest request);
}
[ServiceContract(Name = "FileTransferService",
Namespace = "http://www.my.service.com/2009/07/filestreamer/")]
public interface IFileTransferService
{
[OperationContract]
[FaultContract(typeof(string))]
InitUploadResponse InitializeUpload(InitUploadRequest request);
[OperationContract]
[FaultContract(typeof(string))]
GetUploadStatusResponse GetUploadStatus(GetUploadStatusRequest request);
[OperationContract]
[FaultContract(typeof(string))]
void CancelUploadAsync(CancelUploadAsyncRequest request);
[OperationContract]
[FaultContract(typeof(string))]
InitDownloadResponse InitializeDownload(InitDownloadRequest request);
[OperationContract]
[FaultContract(typeof(string))]
GetFreeSpaceResponse GetFreeSpace(GetFreeSpaceRequest request);
}
We will also provide a couple of interfaces for the client side, including asynchronous pattern calls (I told you we'd be getting dirty with manual configuration). Because the code for this gets pretty long and repetitive, I’ll just show the first few definitions here.
[ServiceContract(Name = "StreamedFileTransferClient",
Namespace = "http://www.my.service.com/2009/07/filestreamer/")]
public interface IStreamedFileTransferClient
{
[OperationContract(
Action = "http://www.my.service.com/2009/07/filestreamer/_
StreamedFileTransferService/UploadStream",
ReplyAction = "http://www.my.service.com/2009/07/filestreamer/_
StreamedFileTransferService/UploadStreamResponse")]
[FaultContract(typeof(string))]
UploadStreamResponse UploadStream(UploadStreamRequest request);
[OperationContract(AsyncPattern = true,
Action = "http://www.my.service.com/2009/07/filestreamer/_
StreamedFileTransferService/UploadStream",
ReplyAction = "http://www.my.service.com/2009/07/filestreamer/_
StreamedFileTransferService/UploadStreamResponse")]
IAsyncResult BeginUploadStream(UploadStreamRequest request, AsyncCallback callback,
object asyncState);
UploadStreamResponse EndUploadStream(IAsyncResult result);
[OperationContract(
Action = "http://www.my.service.com/2009/07/filestreamer/_
StreamedFileTransferService/DownloadStream",
ReplyAction = "http://www.my.service.com/2009/07/filestreamer/_
StreamedFileTransferService/DownloadStreamResponse")]
[FaultContract(typeof(string))]
DownloadStreamResponse DownloadStream(DownloadStreamRequest request);
[OperationContract(AsyncPattern = true,
Action = "http://www.my.service.com/2009/07/filestreamer/_
StreamedFileTransferService/DownloadStream",
ReplyAction = "http://www.my.service.com/2009/07/filestreamer/_
StreamedFileTransferService/DownloadStreamResponse")]
IAsyncResult BeginDownloadStream(DownloadStreamRequest request, AsyncCallback callback,
object asyncState);
DownloadStreamResponse EndDownloadStream(IAsyncResult result);
}
[ServiceContract(Name = "FileTransferClient",
Namespace = "http://www.my.service.com/2009/07/filestreamer/")]
public interface IFileTransferClient
{
[OperationContract(
Action = "http://www.my.service.com/2009/07/filestreamer/_
FileTransferService/InitializeUpload",
ReplyAction = "http://www.my.service.com/2009/07/filestreamer/_
FileTransferService/InitializeUploadResponse")]
[FaultContract(typeof(string))]
InitUploadResponse InitializeUpload(InitUploadRequest request);
[OperationContract(AsyncPattern = true,
Action = "http://www.my.service.com/2009/07/filestreamer/_
FileTransferService/InitializeUpload",
ReplyAction = "http://www.my.service.com/2009/07/filestreamer/_
FileTransferService/InitializeUploadResponse")]
IAsyncResult BeginInitializeUpload(InitUploadRequest request, AsyncCallback callback,
object asyncState);
InitUploadResponse EndInitializeUpload(IAsyncResult result);
// ... Same pattern for all contracts except the following:
[OperationContract(
Action = "http://www.my.service.com/2009/07/filestreamer/_
FileTransferService/CancelUpload",
ReplyAction = "http://www.my.service.com/2009/07/filestreamer/_
FileTransferService/CancelUploadResponse")]
[FaultContract(typeof(string))]
void CancelUploadAsync(CancelUploadAsyncRequest request);
}
Note that the ‘_’ line continuation characters are only to allow the code to fit into my blog – these lines should be on a single line.
That should do for now. Make sure the solution builds (it won't do anything yet), and check out the next article to get the service up and running.