An updated custom WCF transport channel example

Nicholas Allen has an excellent series of blog posts on how to create a WCF transport channel. The last post in the series has links to all the previous posts: https://blogs.msdn.com/b/drnick/archive/2006/05/08/592110.aspx

Click here to see all the posts in this series. All the code is now available on github here: https://github.com/dmetzgar/custom-transport-channel

I attempted to follow his posts to create my own transport channel. There were a few areas that needed to be updated to work with .Net 4.0. In this blog post, I will create a WCF transport channel from scratch. This is simply an update to Nicholas's file transport channel. At the end of the post is the full code for download in case you don't have the patience to read the article.

For the source code I must plug a friend's very useful utility. Ron Jacobs has an excellent utility called CleanProject that cleans out all the files you don't want to send with your Visual Studio solution. For more information, go here: https://wf.codeplex.com/wikipage?title=Clean%20Project%20Overview

Transport Channel Library 

In Visual Studio, create a new empty solution called CustomTransportChannel. Add a new library project called CustomTransportChannelLibrary. Add references to System.ServiceModel and System.ServiceModel.Channels to the library.

The first and easiest class is actually the binding. This is the one that WCF consumes directly. Add a class called FileTransportBinding to the library project. Have it subclass System.ServiceModel.Channels.Binding. The code is pretty small:

 using System.ServiceModel.Channels;

namespace CustomTransportChannelLibrary
{
    public class FileTransportBinding : Binding
    {
        readonly MessageEncodingBindingElement messageElement;
        readonly FileTransportBindingElement transportElement;

        public FileTransportBinding()
        {
            this.messageElement = new TextMessageEncodingBindingElement();
            this.transportElement = new FileTransportBindingElement();
        }

        public override BindingElementCollection CreateBindingElements()
        {
            return new BindingElementCollection(new BindingElement[] {
                this.messageElement,
                this.transportElement
            });
        }

        public override string Scheme
        {
            get { return this.transportElement.Scheme; }
        }
    }
}

One thing to note here is the choice to include a message encoding binding element. You can choose to pass the message encoding element in through the constructor if you want to allow other encodings. Also, if you add parameters to the FileTransportBindingElement, that can be passed in through the constructor as well.

The next class is the binding element. The binding element has the configuration that we want to apply to the channel. Create a new class in the library called FileTransportBindingElement. This class should subclass System.ServiceModel.Channels.TransportBindingElement. There are no options for the file transport so you do not need to add any properties to the binding element. Only a few members need to be overridden:

 using System;
using System.ServiceModel.Channels;

namespace CustomTransportChannelLibrary
{
    public class FileTransportBindingElement : TransportBindingElement
    {
        public FileTransportBindingElement() { }

        public FileTransportBindingElement(FileTransportBindingElement other) { }

        public override string Scheme
        {
            get { return "my.file"; }
        }

        public override BindingElement Clone()
        {
            return new FileTransportBindingElement(this);
        }

        public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
        {
            return typeof(TChannel) == typeof(IRequestChannel);
        }

        public override bool CanBuildChannelListener<TChannel>(BindingContext context)
        {
            return typeof(TChannel) == typeof(IReplyChannel);
        }

        public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (!CanBuildChannelFactory<TChannel>(context))
            {
                throw new ArgumentException(String.Format("Unsupported channel type: {0}.", typeof(TChannel).Name));
            }
            return (IChannelFactory<TChannel>)(object)new FileRequestChannelFactory(this, context);
        }

        public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (!CanBuildChannelListener<TChannel>(context))
            {
                throw new ArgumentException(String.Format("Unsupported channel type: {0}.", typeof(TChannel).Name));
            }
            return (IChannelListener<TChannel>)(object)new FileReplyChannelListener(this, context);
        }
    }
}

The Scheme property can be whatever you want. This is something like "http" or "tcp" that would make your transport easily identifiable. The next step is to create the FileRequestChannelFactory, which is used to submit requests.

 using System;
using System.ServiceModel.Channels;

namespace CustomTransportChannelLibrary
{
    class FileRequestChannelFactory : ChannelFactoryBase<IRequestChannel>
    {
        public readonly long MaxReceivedMessageSize;
        readonly BufferManager bufferManager;
        readonly MessageEncoderFactory encoderFactory;

        public FileRequestChannelFactory(FileTransportBindingElement transportElement, BindingContext context)
            : base(context.Binding)
        {
            MessageEncodingBindingElement messageElement = context.BindingParameters.Remove<MessageEncodingBindingElement>();
            this.MaxReceivedMessageSize = transportElement.MaxReceivedMessageSize;
            this.bufferManager = BufferManager.CreateBufferManager(transportElement.MaxBufferPoolSize, (int)this.MaxReceivedMessageSize);
            this.encoderFactory = messageElement.CreateMessageEncoderFactory();
        }

        protected override IRequestChannel OnCreateChannel(System.ServiceModel.EndpointAddress address, Uri via)
        {
            return new FileRequestChannel(this.bufferManager, this.encoderFactory, address, this, via);
        }

        protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        protected override void OnEndOpen(IAsyncResult result)
        {
            throw new NotImplementedException();
        }

        protected override void OnOpen(TimeSpan timeout)
        {
        }
    }
}

The asynchronous methods won't be called in the example code so we don't need to implement it. The OnOpen call does not need to do anything since we're only interacting with files.

Before implementing the FileRequestChannel, let's move to the FileReplyChannelListener since it is very similar to the FileRequestChannelFactory.

 using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace CustomTransportChannelLibrary
{
    class FileReplyChannelListener : ChannelListenerBase<IReplyChannel>
    {
        public readonly long MaxReceivedMessageSize;
        readonly BufferManager bufferManager;
        readonly MessageEncoderFactory encoderFactory;
        readonly Uri uri;

        public override Uri Uri
        {
            get { return this.uri; }
        }

        public FileReplyChannelListener(FileTransportBindingElement transportElement, BindingContext context)
            : base(context.Binding)
        {
            this.MaxReceivedMessageSize = transportElement.MaxReceivedMessageSize;
            MessageEncodingBindingElement messageElement = context.BindingParameters.Remove<MessageEncodingBindingElement>();
            this.bufferManager = BufferManager.CreateBufferManager(transportElement.MaxBufferPoolSize, (int)this.MaxReceivedMessageSize);
            this.encoderFactory = messageElement.CreateMessageEncoderFactory();
            this.uri = new Uri(context.ListenUriBaseAddress, context.ListenUriRelativeAddress);
        }

        protected override IReplyChannel OnAcceptChannel(TimeSpan timeout)
        {
            EndpointAddress address = new EndpointAddress(this.Uri);
            return new FileReplyChannel(this.bufferManager, this.encoderFactory, address, this);
        }

        protected override void OnOpen(TimeSpan timeout)
        {
            Directory.CreateDirectory(this.Uri.AbsolutePath);
        }

        protected override void OnClose(TimeSpan timeout)
        {
        }

        protected override IAsyncResult OnBeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        protected override IReplyChannel OnEndAcceptChannel(IAsyncResult result)
        {
            throw new NotImplementedException();
        }

        protected override IAsyncResult OnBeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        protected override bool OnEndWaitForChannel(IAsyncResult result)
        {
            throw new NotImplementedException();
        }

        protected override bool OnWaitForChannel(TimeSpan timeout)
        {
            throw new NotImplementedException();
        }

        protected override void OnAbort()
        {
            throw new NotImplementedException();
        }

        protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        protected override void OnEndClose(IAsyncResult result)
        {
            throw new NotImplementedException();
        }

        protected override void OnEndOpen(IAsyncResult result)
        {
            throw new NotImplementedException();
        }
    }
}

As you can see, there is very little that needs to be implemented for this sample to work. Not that OnOpen creates the directory the files will be written to.

With the listener and factory done, let's go back to the request channel. First, we'll create an abstract base class that is used for both the request and reply channels. This has a lot of the actual file handling code. Add a file called FileChannelBase to the project and inherit from System.ServiceModel.Channels.ChannelBase.

 using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace CustomTransportChannelLibrary
{
    abstract class FileChannelBase : ChannelBase
    {
        const int MaxBufferSize = 64 * 1024;
        const int MaxSizeOfHeaders = 4 * 1024;

        readonly EndpointAddress address;
        readonly BufferManager bufferManager;
        readonly MessageEncoder encoder;
        readonly long maxReceivedMessageSize;

        public EndpointAddress RemoteAddress
        {
            get { return this.address; }
        }

        public FileChannelBase(BufferManager bufferManager, MessageEncoderFactory encoderFactory, EndpointAddress address, ChannelManagerBase parent,
         long maxReceivedMessageSize)
            : base(parent)
        {
            this.address = address;
            this.bufferManager = bufferManager;
            this.encoder = encoderFactory.CreateSessionEncoder();
            this.maxReceivedMessageSize = maxReceivedMessageSize;
        }

        protected static Exception ConvertException(Exception exception)
        {
            Type exceptionType = exception.GetType();
            if (exceptionType == typeof(System.IO.DirectoryNotFoundException) ||
                exceptionType == typeof(System.IO.FileNotFoundException) ||
                exceptionType == typeof(System.IO.PathTooLongException))
            {
                return new EndpointNotFoundException(exception.Message, exception);
            }
            return new CommunicationException(exception.Message, exception);
        }

        protected static string PathToFile(Uri path, String name)
        {
            UriBuilder address = new UriBuilder(path);
            address.Scheme = "file";
            address.Path = Path.Combine(path.AbsolutePath, name);
            return address.Uri.AbsolutePath;
        }

        protected override void OnAbort()
        {
        }

        protected override void OnOpen(TimeSpan timeout)
        {
        }

        protected override void OnClose(TimeSpan timeout)
        {
        }

        protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        protected override void OnEndClose(IAsyncResult result)
        {
            throw new NotImplementedException();
        }

        protected override void OnEndOpen(IAsyncResult result)
        {
            throw new NotImplementedException();
        }

        protected Message ReadMessage(string path)
        {
            return this.BufferedReadMessage(path);
        }

        protected void WriteMessage(string path, Message message)
        {
            this.BufferedWriteMessage(path, message);
        }

        Message BufferedReadMessage(string path)
        {
            byte[] data;
            long bytesTotal;
            try
            {
                using (FileStream stream = new FileStream(path, FileMode.Open))
                {
                    bytesTotal = stream.Length;
                    if (bytesTotal > int.MaxValue)
                    {
                        throw new CommunicationException(
                           String.Format("Message of size {0} bytes is too large to buffer. Use a streamed transfer instead.", bytesTotal)
                        );
                    }
                    if (bytesTotal > this.maxReceivedMessageSize)
                    {
                        throw new CommunicationException(String.Format("Message exceeds maximum size: {0} > {1}.", bytesTotal, maxReceivedMessageSize));
                    }
                    data = this.bufferManager.TakeBuffer((int)bytesTotal);
                    int bytesRead = 0;
                    while (bytesRead < bytesTotal)
                    {
                        int count = stream.Read(data, bytesRead, (int)bytesTotal - bytesRead);
                        if (count == 0)
                        {
                            throw new CommunicationException(String.Format("Unexpected end of message after {0} of {1} bytes.", bytesRead, bytesTotal));
                        }
                        bytesRead += count;
                    }
                }
            }
            catch (IOException exception)
            {
                throw ConvertException(exception);
            }
            ArraySegment<byte> buffer = new ArraySegment<byte>(data, 0, (int)bytesTotal);
            Message message = this.encoder.ReadMessage(buffer, this.bufferManager);
            this.bufferManager.ReturnBuffer(data);
            return message;
        }

        void BufferedWriteMessage(string path, Message message)
        {
            ArraySegment<byte> buffer;
            using (message)
            {
                this.address.ApplyTo(message);
                buffer = this.encoder.WriteMessage(message, MaxBufferSize, this.bufferManager);
            }
            try
            {
                using (FileStream stream = new FileStream(path, FileMode.Create))
                {
                    stream.Write(buffer.Array, buffer.Offset, buffer.Count);
                }
                this.bufferManager.ReturnBuffer(buffer.Array);
            }
            catch (IOException exception)
            {
                throw ConvertException(exception);
            }
        }
    }
}

If you are comparing to Nicholas's original post, I've excluded streaming and I'm sticking with just buffering. I've also updated this code to return the buffers to the buffer manager since that was not in the original code. This wouldn't have much effect on running some sample code but it's a good practice to keep in mind.

Now we can create the reply channel. Add a new file called FileReplyChannel that inherits from FileChannelBase and implements IReplyChannel. Make this a partial class.

 using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace CustomTransportChannelLibrary
{
    partial class FileReplyChannel : FileChannelBase, IReplyChannel
    {
        readonly EndpointAddress localAddress;
        readonly object readLock;

        public System.ServiceModel.EndpointAddress LocalAddress
        {
            get { return this.localAddress; }
        }

        public FileReplyChannel(BufferManager bufferManager, MessageEncoderFactory encoderFactory, EndpointAddress address,
            FileReplyChannelListener parent)
            : base(bufferManager, encoderFactory, address, parent, parent.MaxReceivedMessageSize)
        {
            this.localAddress = address;
            this.readLock = new object();
        }

        public RequestContext ReceiveRequest(TimeSpan timeout)
        {
            ThrowIfDisposedOrNotOpen();
            lock (readLock)
            {
                Message message = ReadMessage(PathToFile(LocalAddress.Uri, "request"));
                return new FileRequestContext(message, this);
            }
        }

        public RequestContext ReceiveRequest()
        {
            return ReceiveRequest(DefaultReceiveTimeout);
        }

        public bool TryReceiveRequest(TimeSpan timeout, out RequestContext context)
        {
            context = null;
            bool complete = WaitForRequest(timeout);
            if (!complete)
            {
                return false;
            }
            context = ReceiveRequest(DefaultReceiveTimeout);
            return true;
        }

        public bool WaitForRequest(TimeSpan timeout)
        {
            ThrowIfDisposedOrNotOpen();
            try
            {
                File.Delete(PathToFile(LocalAddress.Uri, "request"));
                using (FileSystemWatcher watcher = new FileSystemWatcher(LocalAddress.Uri.AbsolutePath, "request"))
                {
                    watcher.EnableRaisingEvents = true;
                    WaitForChangedResult result = watcher.WaitForChanged(WatcherChangeTypes.Changed, (int)timeout.TotalMilliseconds);
                    return !result.TimedOut;
                }
            }
            catch (IOException exception)
            {
                throw ConvertException(exception);
            }
        }

        public IAsyncResult BeginReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        public IAsyncResult BeginReceiveRequest(AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        public IAsyncResult BeginTryReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        public IAsyncResult BeginWaitForRequest(TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        public RequestContext EndReceiveRequest(IAsyncResult result)
        {
            throw new NotImplementedException();
        }

        public bool EndTryReceiveRequest(IAsyncResult result, out RequestContext context)
        {
            throw new NotImplementedException();
        }

        public bool EndWaitForRequest(IAsyncResult result)
        {
            throw new NotImplementedException();
        }
    }
}

Again, not much has to be implemented for the sample. The key method is the WaitForRequest which essentially blocks the thread while waiting for the "request" file to appear.

One class is still needed to make the reply channel work and that's the request context. This correlates the reply with the incoming request. Add a new file called FileRequestContext and inherit from System.ServiceModel.Channels.RequestContext. The class will actually be an inner class for FileReplyChannel.

 using System;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace CustomTransportChannelLibrary
{
    partial class FileReplyChannel
    {
        class FileRequestContext : RequestContext
        {
            bool aborted;
            readonly Message message;
            readonly FileReplyChannel parent;
            CommunicationState state;
            readonly object thisLock;
            readonly object writeLock;

            public override Message RequestMessage
            {
                get { return this.message; }
            }

            public FileRequestContext(Message message, FileReplyChannel parent)
            {
                this.aborted = false;
                this.message = message;
                this.parent = parent;
                this.state = CommunicationState.Opened;
                this.thisLock = new object();
                this.writeLock = new object();
            }

            public override void Abort()
            {
                lock (thisLock)
                {
                    if (this.aborted)
                    {
                        return;
                    }
                    this.aborted = true;
                    this.state = CommunicationState.Faulted;
                }
            }

            public override void Close(TimeSpan timeout)
            {
                lock (thisLock)
                {
                    this.state = CommunicationState.Closed;
                }
            }

            public override void Close()
            {
                this.Close(this.parent.DefaultCloseTimeout);
            }

            public override void Reply(Message message, TimeSpan timeout)
            {
                lock (thisLock)
                {
                    if (this.aborted)
                    {
                        throw new CommunicationObjectAbortedException();
                    }
                    if (this.state == CommunicationState.Faulted)
                    {
                        throw new CommunicationObjectFaultedException();
                    }
                    if (this.state == CommunicationState.Closed)
                    {
                        throw new ObjectDisposedException("this");
                    }
                }
                this.parent.ThrowIfDisposedOrNotOpen();
                lock (writeLock)
                {
                    this.parent.WriteMessage(FileChannelBase.PathToFile(this.parent.LocalAddress.Uri, "reply"), message);
                }
            }

            public override void Reply(Message message)
            {
                this.Reply(message, this.parent.DefaultSendTimeout);
            }

            public override IAsyncResult BeginReply(Message message, TimeSpan timeout, AsyncCallback callback, object state)
            {
                throw new NotImplementedException();
            }

            public override IAsyncResult BeginReply(Message message, AsyncCallback callback, object state)
            {
                throw new NotImplementedException();
            }

            public override void EndReply(IAsyncResult result)
            {
                throw new NotImplementedException();
            }
        }
    }
}

This is mostly boiler plate goo for WCF. The one interesting method is Reply, which initiates the write of the reply message.

For the client side, the request channel is next. Add a file called FileRequestChannel that inherits from FileChannelBase and implements IRequestChannel.

 using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;

namespace CustomTransportChannelLibrary
{
    class FileRequestChannel : FileChannelBase, IRequestChannel
    {
        readonly Uri via;
        readonly object writeLock;

        public Uri Via
        {
            get { return this.via; }
        }

        public FileRequestChannel(BufferManager bufferManager, MessageEncoderFactory encoderFactory, EndpointAddress address,
            FileRequestChannelFactory parent, Uri via)
            : base(bufferManager, encoderFactory, address, parent, parent.MaxReceivedMessageSize)
        {
            this.via = via;
            this.writeLock = new object();
        }

        public Message Request(Message message, TimeSpan timeout)
        {
            ThrowIfDisposedOrNotOpen();
            lock (this.writeLock)
            {
                try
                {
                    File.Delete(PathToFile(Via, "reply"));
                    using (FileSystemWatcher watcher = new FileSystemWatcher(Via.AbsolutePath, "reply"))
                    {
                        ManualResetEvent replyCreated = new ManualResetEvent(false);
                        watcher.Changed += new FileSystemEventHandler(
                           delegate(object sender, FileSystemEventArgs e) { replyCreated.Set(); }
                        );
                        watcher.EnableRaisingEvents = true;
                        WriteMessage(PathToFile(via, "request"), message);
                        if (!replyCreated.WaitOne(timeout, false))
                        {
                            throw new TimeoutException(timeout.ToString());
                        }
                    }
                }
                catch (IOException exception)
                {
                    throw ConvertException(exception);
                }
                return ReadMessage(PathToFile(Via, "reply"));
            }
        }

        public Message Request(Message message)
        {
            return this.Request(message, DefaultReceiveTimeout);
        }

        public IAsyncResult BeginRequest(Message message, TimeSpan timeout, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        public IAsyncResult BeginRequest(Message message, AsyncCallback callback, object state)
        {
            throw new NotImplementedException();
        }

        public Message EndRequest(IAsyncResult result)
        {
            throw new NotImplementedException();
        }
    }
}

The request method above writes the "request" file and blocks the thread waiting for the "reply" message to appear.

Service 

So that's it for the actual transport channel code. Now it's time to see if it all works. Let's first create the service. Create a new console project in the solution called CustomTransportChannelService. Add references to CustomTransportChannelLibrary, System.ServiceModel, System.ServiceModel.Channels, and System.Runtime.Serialization. Edit the Program.cs as follows:

 using System;
using System.ServiceModel.Channels;
using CustomTransportChannelLibrary;

namespace CustomTransportChannelService
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Creating listener...");
            Binding binding = new FileTransportBinding();
            Uri uri = new Uri("my.file://localhost/x");
            IChannelListener<IReplyChannel> listener = binding.BuildChannelListener<IReplyChannel>(uri, new BindingParameterCollection());
            listener.Open(TimeSpan.FromSeconds(5));
            Console.WriteLine(" done.");
            Console.Write("Creating channel...");
            IReplyChannel channel = listener.AcceptChannel(TimeSpan.FromSeconds(5));
            channel.Open(TimeSpan.FromSeconds(5));
            Console.WriteLine(" done.");
            Console.Write("Waiting for request...");
            while (channel.WaitForRequest(TimeSpan.FromMinutes(1)))
            {
                using (RequestContext context = channel.ReceiveRequest(TimeSpan.FromSeconds(5)))
                {
                    Console.WriteLine(" done.");
                    using (Message message = context.RequestMessage)
                    {
                        Console.WriteLine("Processing request: {0}", message.Headers.Action);
                        if (message.Headers.Action == "https://reflect")
                        {
                            string response = ProcessReflectRequest(message.GetBody<string>());
                            Console.Write("Sending reply...");
                            Message replyMessage = Message.CreateMessage(MessageVersion.Default, "https://reflection", response);
                            context.Reply(replyMessage, TimeSpan.FromSeconds(5));
                            Console.WriteLine(" done.");
                        }
                    }
                }
                Console.Write("Waiting for request...");
            }
            Console.WriteLine(" terminated.");
            channel.Close(TimeSpan.FromSeconds(5));
        }

        static string ProcessReflectRequest(string request)
        {
            char[] output = new char[request.Length];
            for (int index = 0; index < request.Length; index++)
            {

                output[index] = request[request.Length - index - 1];
            }
            return new string(output);
        }
    }
}

This code waits for an incoming message, reads the body of the message as a string, reverses the string, and sends the reversed string back as a reply.

Client

One thing is left, and that is the client. Create a new console project in the solution called CustomTransportChannelClient. Add references to CustomTransportChannelLibrary, System.ServiceModel, System.ServiceModel.Channels, and System.Runtime.Serialization. Edit the Program.cs as follows:

 using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using CustomTransportChannelLibrary;

namespace CustomTransportChannelClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Creating factory...");
            Binding binding = new FileTransportBinding();
            IChannelFactory<IRequestChannel> factory = binding.BuildChannelFactory<IRequestChannel>();
            factory.Open(TimeSpan.FromSeconds(5));
            Console.WriteLine(" done.");
            Console.Write("Creating channel...");
            Uri uri = new Uri("my.file://localhost/x");
            IRequestChannel channel = factory.CreateChannel(new EndpointAddress(uri));
            Console.WriteLine(" done.");
            Console.Write("Opening channel...");
            channel.Open(TimeSpan.FromSeconds(5));
            Console.WriteLine(" done.");
            while (true)
            {
                Console.Write("Enter some text (Ctrl-Z to quit): ");
                String text = Console.ReadLine();
                if (text == null)
                {
                    break;
                }
                Console.Write("Sending request...");
                Message requestMessage = Message.CreateMessage(MessageVersion.Default, "https://reflect", text);
                Message replyMessage = channel.Request(requestMessage, TimeSpan.FromSeconds(5));
                Console.WriteLine(" done.");
                using (replyMessage)
                {
                    Console.WriteLine("Processing reply: {0}", replyMessage.Headers.Action);
                    Console.WriteLine("Reply: {0}", replyMessage.GetBody<string>());
                }
            }
            channel.Close(TimeSpan.FromSeconds(5));
            factory.Close();
        }
    }
}

Now it's ready for testing. The client will wait for you to type in input before sending data to the service. That means you should be able to set multiple startup projects in your solution and start both the service and client at the same time. Be aware of the file path where it's going to put the files. "my.file://localhost/x" will translate into %SystemDrive%\x.

If you're interested in extending this example a little farther to work with service contracts, continue to the next post here.

CustomTransportChannel.zip