Friday, November 30, 2012

Sending stream to ServiceStack

Recently I needed to make a ServiceStack service which can receive big files, so I wanted to use streaming to accomplish this. Unfortunately there isn't much information about using streams with ServiceStack, so I decided to share my experience.

We'll create a sample solution containing both Server and Client. We will create a class library containing the service itself and an Utility project. So here is the structure of our solution:
So let's continue with the implementation of the service.
First of all we'll need to install the ServiceStack nuget package in the ServiceStackStreaming.Service project:
PM> Install-Package ServiceStack
This will add the dlls needed for ServiceStack. Now let's create the DTO:
using ServiceStack.ServiceHost;

namespace ServiceStackStreaming.Service
{
    [Route("/upload/{FileName}", "POST")]
    public class UploadPackage : IRequiresRequestStream
    {
        public System.IO.Stream RequestStream { get; set; }

        public string FileName { get; set; }
    }
}
To enable Streaming support we need to implement IRequiresRequestStream which needs a RequestStream property of type System.IO.Stream. We'll add a FileName property and include it in the Route so that we would be able to pass the uploaded file name.
The next thing to do is to create the service itself:
using ServiceStack.Common.Web;
using ServiceStackStreaming.Utility;
using System;
using System.IO;

namespace ServiceStackStreaming.Service
{
    public class UploadService : ServiceStack.ServiceInterface.Service
    {
        public object Post(UploadPackage request)
        {
            // hack - get the properties from the request
            if (string.IsNullOrEmpty(request.FileName))
            {
                var segments = base.Request.PathInfo.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                request.FileName = segments[1];
            }

            string resultFile = Path.Combine(@"C:\Temp", request.FileName);
            if (File.Exists(resultFile))
            {
                File.Delete(resultFile);
            }
            using (FileStream file = File.Create(resultFile))
            {
                request.RequestStream.Copy(file);
            }
            
            return new HttpResult(System.Net.HttpStatusCode.OK);
        }
    }
}

Our dummy service will save the incoming file in the "C:\Temp" directory. With the code from line 12 to line 17 we are getting the FileName property if it's not set. It seems that when using streaming the additional properties are not processed and they are always null, so we'll do this little hack to get the properties parsing the request url.
The other trick we use here is the extension method of the System.IO.Stream class wich we have implemented in the ServiceStackStreaming.Utility project:
using System.IO;

namespace ServiceStackStreaming.Utility
{
    public static class StreamExtender
    {
        public static void Copy(this Stream instance, Stream target)
        {
            int bytesRead = 0;
            int bufSize = copyBuf.Length;

            while ((bytesRead = instance.Read(copyBuf, 0, bufSize)) > 0)
            {
                target.Write(copyBuf, 0, bytesRead);
            }
        }
        private static readonly byte[] copyBuf = new byte[0x1000];
    }
}
this simply copes the instance stream to the target stream. Another option is to use ServiceStack StreamExtensions WriteTo method instead of creating this utility method.
The last thing we need to do to create a functional service is to add the AppHost class, we will inherit  AppHostHttpListenerBase as we want to host the service in a window console application.
using ServiceStack.WebHost.Endpoints;

namespace ServiceStackStreaming.Service
{
    public class AppHost : AppHostHttpListenerBase
    {
        public AppHost() : base("Agent", typeof(UploadService).Assembly) { }

        public override void Configure(Funq.Container container)
        {
            // we can add the routing here instead of adding it as attribute to the DTO
            //Routes
            //    .Add("/upload/{FileName}", "POST");
        }
    }
}
We can configure the route here, but I prefer doing this with attribute.
Now let's host the service. To do this we'll need to add the same ServiceStack nuget to the SertviceStackStreaming.Server project and add the following code to the Program.cs file:
using ServiceStackStreaming.Service;
using System;

namespace ServiceStackStreaming.Server
{
    class Program
    {
        static void Main(string[] args)
        {
            var appHost = new AppHost();
            appHost.Init();
            appHost.Start("http://*:1999/");

            Console.WriteLine("Service listening on port 1999!");
            Console.ReadKey();
        }
    }
}

This will be enough to host the service listening to port 1999.
Now let's call the service from the ServiceStackStreaming.Client (again we'll have to instal the sam e nuget package here).
using ServiceStackStreaming.Utility;
using System.IO;
using System.Net;

namespace ServiceStackStreaming.Client
{
    class Program
    {
        static void Main(string[] args)
        {
            string filePath = @"c:\temp\upload.zip";
            
            HttpWebRequest client = (HttpWebRequest)WebRequest.Create("http://localhost:1999/upload/upload-copy.zip");
            client.Method = WebRequestMethods.Http.Post;
            
            // the following 4 rows enable streaming 
            client.AllowWriteStreamBuffering = false;
            client.SendChunked = true;
            client.ContentType = "multipart/form-data;";
            client.Timeout = int.MaxValue;

            using (FileStream fileStream = File.OpenRead(filePath))
            {
                fileStream.Copy(client.GetRequestStream());
            }

            var response = new StreamReader(client.GetResponse().GetResponseStream()).ReadToEnd();
        }
    }
}
And that's it. We create WebRequest, set the needed properties to enable streaming on the client and copy the file stream to the request stream. This will call the service and will upload the "C:\Temp\upload.zip" file as upload-copy.zip file.

You can find the sample code here: ServiceStackStreaming.zip

1 comment: