Skip to content

Instantly share code, notes, and snippets.

@Lakerfield
Last active March 25, 2021 21:23
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lakerfield/32276ccb27f29316ddae to your computer and use it in GitHub Desktop.
Save Lakerfield/32276ccb27f29316ddae to your computer and use it in GitHub Desktop.
A WCF SelfHost GZip Content-Encoding implementation for .NET 4.5
// A WCF SelfHost GZip Content-Encoding implementation for .NET 4.5 based on:
// Work of Francesco De Vittori @ http://www.frenk.com/2009/12/gzip-compression-wcfsilverlight/
// and Microsoft’s Compression Encoder sample http://msdn.microsoft.com/en-us/library/ms751458.aspx
// Files of blog post: http://lakerfield.eu/post/2014/06/17/A-WCF-SelfHost-GZip-Content-Encoding-implementation-for-NET-45.aspx
_serviceHost = new ServiceHost(typeof(MyService), new Uri(Options.Url));
var encoding = new GZipMessageEncodingBindingElement(new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8));
var transport = new HttpTransportBindingElement();
var binding = new CustomBinding(encoding, transport);
var endpoint = _serviceHost.AddServiceEndpoint(typeof(IMyServiceInterface), binding, Options.Url);
endpoint.Behaviors.Add(new GZipBehavior());
_serviceHost.Open();
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Threading.Tasks;
namespace Lakerfield.Wcf.Compression
{
public class DoCompressExtension : IExtension<OperationContext>
{
public void Attach(OperationContext owner) { }
public void Detach(OperationContext owner) { }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Threading.Tasks;
namespace Lakerfield.Wcf.Compression
{
public class GZipBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{ }
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
throw new Exception("Behavior not supported on the client side");
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new GZipInspector());
}
public void Validate(ServiceEndpoint endpoint)
{ }
}
public class GzipBehaviorExtensionElement : BehaviorExtensionElement
{
public GzipBehaviorExtensionElement()
{ }
public override Type BehaviorType
{
get { return typeof(GZipBehavior); }
}
protected override object CreateBehavior()
{
return new GZipBehavior();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Threading.Tasks;
namespace Lakerfield.Wcf.Compression
{
public class GZipInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(ref Message request, IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
{
try
{
var prop = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
var accept = prop.Headers[HttpRequestHeader.AcceptEncoding];
if (!string.IsNullOrEmpty(accept) && accept.Contains("gzip"))
OperationContext.Current.Extensions.Add(new DoCompressExtension());
}
catch { }
return null;
}
public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
if (OperationContext.Current.Extensions.OfType<DoCompressExtension>().Any())
{
HttpResponseMessageProperty httpResponseProperty = new HttpResponseMessageProperty();
httpResponseProperty.Headers.Add(HttpResponseHeader.ContentEncoding, "gzip");
reply.Properties[HttpResponseMessageProperty.Name] = httpResponseProperty;
}
}
}
}
//----------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//----------------------------------------------------------------
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace Lakerfield.Wcf.Compression
{
//This class is used to create the custom encoder (GZipMessageEncoder)
internal class GZipMessageEncoderFactory : MessageEncoderFactory
{
MessageEncoder encoder;
//The GZip encoder wraps an inner encoder
//We require a factory to be passed in that will create this inner encoder
public GZipMessageEncoderFactory(MessageEncoderFactory messageEncoderFactory)
{
if (messageEncoderFactory == null)
throw new ArgumentNullException("messageEncoderFactory", "A valid message encoder factory must be passed to the GZipEncoder");
encoder = new GZipMessageEncoder(messageEncoderFactory.Encoder);
}
//The service framework uses this property to obtain an encoder from this encoder factory
public override MessageEncoder Encoder
{
get { return encoder; }
}
public override MessageVersion MessageVersion
{
get { return encoder.MessageVersion; }
}
//This is the actual GZip encoder
class GZipMessageEncoder : MessageEncoder
{
//static string GZipContentType = "application/x-gzip";
//This implementation wraps an inner encoder that actually converts a WCF Message
//into textual XML, binary XML or some other format. This implementation then compresses the results.
//The opposite happens when reading messages.
//This member stores this inner encoder.
MessageEncoder innerEncoder;
//We require an inner encoder to be supplied (see comment above)
internal GZipMessageEncoder(MessageEncoder messageEncoder)
: base()
{
if (messageEncoder == null)
throw new ArgumentNullException("messageEncoder", "A valid message encoder must be passed to the GZipEncoder");
innerEncoder = messageEncoder;
}
public override string ContentType
{
//get { return GZipContentType; }
get { return innerEncoder.ContentType; }
}
public override string MediaType
{
//get { return GZipContentType; }
get { return innerEncoder.ContentType; }
}
//SOAP version to use - we delegate to the inner encoder for this
public override MessageVersion MessageVersion
{
get { return innerEncoder.MessageVersion; }
}
//Helper method to compress an array of bytes
static ArraySegment<byte> CompressBuffer(ArraySegment<byte> buffer, BufferManager bufferManager, int messageOffset)
{
MemoryStream memoryStream = new MemoryStream();
using (GZipStream gzStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
{
gzStream.Write(buffer.Array, buffer.Offset, buffer.Count);
}
byte[] compressedBytes = memoryStream.ToArray();
int totalLength = messageOffset + compressedBytes.Length;
byte[] bufferedBytes = bufferManager.TakeBuffer(totalLength);
Array.Copy(compressedBytes, 0, bufferedBytes, messageOffset, compressedBytes.Length);
bufferManager.ReturnBuffer(buffer.Array);
ArraySegment<byte> byteArray = new ArraySegment<byte>(bufferedBytes, messageOffset, bufferedBytes.Length - messageOffset);
return byteArray;
}
//Helper method to decompress an array of bytes
static ArraySegment<byte> DecompressBuffer(ArraySegment<byte> buffer, BufferManager bufferManager)
{
MemoryStream memoryStream = new MemoryStream(buffer.Array, buffer.Offset, buffer.Count);
MemoryStream decompressedStream = new MemoryStream();
int totalRead = 0;
int blockSize = 1024;
byte[] tempBuffer = bufferManager.TakeBuffer(blockSize);
using (GZipStream gzStream = new GZipStream(memoryStream, CompressionMode.Decompress))
{
while (true)
{
int bytesRead = gzStream.Read(tempBuffer, 0, blockSize);
if (bytesRead == 0)
break;
decompressedStream.Write(tempBuffer, 0, bytesRead);
totalRead += bytesRead;
}
}
bufferManager.ReturnBuffer(tempBuffer);
byte[] decompressedBytes = decompressedStream.ToArray();
byte[] bufferManagerBuffer = bufferManager.TakeBuffer(decompressedBytes.Length + buffer.Offset);
Array.Copy(buffer.Array, 0, bufferManagerBuffer, 0, buffer.Offset);
Array.Copy(decompressedBytes, 0, bufferManagerBuffer, buffer.Offset, decompressedBytes.Length);
ArraySegment<byte> byteArray = new ArraySegment<byte>(bufferManagerBuffer, buffer.Offset, decompressedBytes.Length);
bufferManager.ReturnBuffer(buffer.Array);
return byteArray;
}
//One of the two main entry points into the encoder. Called by WCF to decode a buffered byte array into a Message.
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
////Decompress the buffer
//ArraySegment<byte> decompressedBuffer = DecompressBuffer(buffer, bufferManager);
////Use the inner encoder to decode the decompressed buffer
//Message returnMessage = innerEncoder.ReadMessage(decompressedBuffer, bufferManager);
//returnMessage.Properties.Encoder = this;
//return returnMessage;
return innerEncoder.ReadMessage(buffer, bufferManager, contentType);
}
//One of the two main entry points into the encoder. Called by WCF to encode a Message into a buffered byte array.
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
////Use the inner encoder to encode a Message into a buffered byte array
//ArraySegment<byte> buffer = innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, 0);
////Compress the resulting byte array
//return CompressBuffer(buffer, bufferManager, messageOffset);
if (OperationContext.Current.Extensions.OfType<DoCompressExtension>().Any())
{
var buffer = innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
return CompressBuffer(buffer, bufferManager, messageOffset);
}
else
return innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
}
public override Message ReadMessage(System.IO.Stream stream, int maxSizeOfHeaders, string contentType)
{
////Pass false for the "leaveOpen" parameter to the GZipStream constructor.
////This will ensure that the inner stream gets closed when the message gets closed, which
////will ensure that resources are available for reuse/release.
//GZipStream gzStream = new GZipStream(stream, CompressionMode.Decompress, false);
//return innerEncoder.ReadMessage(gzStream, maxSizeOfHeaders);
return innerEncoder.ReadMessage(stream, maxSizeOfHeaders, contentType);
}
public override void WriteMessage(Message message, System.IO.Stream stream)
{
//using (GZipStream gzStream = new GZipStream(stream, CompressionMode.Compress, true))
//{
// innerEncoder.WriteMessage(message, gzStream);
//}
//// innerEncoder.WriteMessage(message, gzStream) depends on that it can flush data by flushing
//// the stream passed in, but the implementation of GZipStream.Flush will not flush underlying
//// stream, so we need to flush here.
//stream.Flush();
if (OperationContext.Current.Extensions.OfType<DoCompressExtension>().Any())
{
using (var gzStream = new GZipStream(stream, CompressionMode.Compress, true))
{
innerEncoder.WriteMessage(message, gzStream);
}
stream.Flush();
}
else
innerEncoder.WriteMessage(message, stream);
}
}
}
}
//----------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//----------------------------------------------------------------
using System;
using System.Configuration;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.Text;
using System.Xml;
namespace Lakerfield.Wcf.Compression
{
// This is constants for GZip message encoding policy.
static class GZipMessageEncodingPolicyConstants
{
public const string GZipEncodingName = "GZipEncoding";
public const string GZipEncodingNamespace = "http://schemas.microsoft.com/ws/06/2004/mspolicy/netgzip1";
public const string GZipEncodingPrefix = "gzip";
}
//This is the binding element that, when plugged into a custom binding, will enable the GZip encoder
public sealed class GZipMessageEncodingBindingElement
: MessageEncodingBindingElement //BindingElement
, IPolicyExportExtension
{
//We will use an inner binding element to store information required for the inner encoder
MessageEncodingBindingElement innerBindingElement;
//By default, use the default text encoder as the inner encoder
public GZipMessageEncodingBindingElement()
: this(new TextMessageEncodingBindingElement()) { }
public GZipMessageEncodingBindingElement(MessageEncodingBindingElement messageEncoderBindingElement)
{
this.innerBindingElement = messageEncoderBindingElement;
}
public MessageEncodingBindingElement InnerMessageEncodingBindingElement
{
get { return innerBindingElement; }
set { innerBindingElement = value; }
}
//Main entry point into the encoder binding element. Called by WCF to get the factory that will create the
//message encoder
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new GZipMessageEncoderFactory(innerBindingElement.CreateMessageEncoderFactory());
}
public override MessageVersion MessageVersion
{
get { return innerBindingElement.MessageVersion; }
set { innerBindingElement.MessageVersion = value; }
}
public override BindingElement Clone()
{
return new GZipMessageEncodingBindingElement(this.innerBindingElement);
}
public override T GetProperty<T>(BindingContext context)
{
if (typeof(T) == typeof(XmlDictionaryReaderQuotas))
{
return innerBindingElement.GetProperty<T>(context);
}
else
{
return base.GetProperty<T>(context);
}
}
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
if (context == null)
throw new ArgumentNullException("context");
context.BindingParameters.Add(this);
return context.BuildInnerChannelFactory<TChannel>();
}
public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
{
if (context == null)
throw new ArgumentNullException("context");
context.BindingParameters.Add(this);
return context.BuildInnerChannelListener<TChannel>();
}
public override bool CanBuildChannelListener<TChannel>(BindingContext context)
{
if (context == null)
throw new ArgumentNullException("context");
context.BindingParameters.Add(this);
return context.CanBuildInnerChannelListener<TChannel>();
}
void IPolicyExportExtension.ExportPolicy(MetadataExporter exporter, PolicyConversionContext policyContext)
{
if (policyContext == null)
{
throw new ArgumentNullException("policyContext");
}
XmlDocument document = new XmlDocument();
policyContext.GetBindingAssertions().Add(document.CreateElement(
GZipMessageEncodingPolicyConstants.GZipEncodingPrefix,
GZipMessageEncodingPolicyConstants.GZipEncodingName,
GZipMessageEncodingPolicyConstants.GZipEncodingNamespace));
}
}
//This class is necessary to be able to plug in the GZip encoder binding element through
//a configuration file
public class GZipMessageEncodingElement : BindingElementExtensionElement
{
public GZipMessageEncodingElement()
{
}
//Called by the WCF to discover the type of binding element this config section enables
public override Type BindingElementType
{
get { return typeof(GZipMessageEncodingBindingElement); }
}
//The only property we need to configure for our binding element is the type of
//inner encoder to use. Here, we support text and binary.
[ConfigurationProperty("innerMessageEncoding", DefaultValue = "textMessageEncoding")]
public string InnerMessageEncoding
{
get { return (string)base["innerMessageEncoding"]; }
set { base["innerMessageEncoding"] = value; }
}
//Called by the WCF to apply the configuration settings (the property above) to the binding element
public override void ApplyConfiguration(BindingElement bindingElement)
{
//GZipMessageEncodingBindingElement binding = (GZipMessageEncodingBindingElement)bindingElement;
//PropertyInformationCollection propertyInfo = this.ElementInformation.Properties;
//if (propertyInfo["innerMessageEncoding"].ValueOrigin != PropertyValueOrigin.Default)
//{
// switch (this.InnerMessageEncoding)
// {
// case "textMessageEncoding":
// binding.InnerMessageEncodingBindingElement = new TextMessageEncodingBindingElement();
// break;
// case "binaryMessageEncoding":
// binding.InnerMessageEncodingBindingElement = new BinaryMessageEncodingBindingElement();
// break;
// }
//}
GZipMessageEncodingBindingElement binding = (GZipMessageEncodingBindingElement)bindingElement;
PropertyInformationCollection propertyInfo = this.ElementInformation.Properties;
if (propertyInfo["innerMessageEncoding"].ValueOrigin != PropertyValueOrigin.Default)
{
switch (this.InnerMessageEncoding)
{
case "textMessageEncoding":
binding.InnerMessageEncodingBindingElement = new TextMessageEncodingBindingElement()
{
MessageVersion = MessageVersion.Soap11,
WriteEncoding = Encoding.UTF8
};
break;
case "binaryMessageEncoding":
binding.InnerMessageEncodingBindingElement = new BinaryMessageEncodingBindingElement();
break;
}
}
}
//Called by the WCF to create the binding element
protected override BindingElement CreateBindingElement()
{
GZipMessageEncodingBindingElement bindingElement = new GZipMessageEncodingBindingElement();
this.ApplyConfiguration(bindingElement);
return bindingElement;
}
}
}
//----------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//----------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ServiceModel.Description;
using System.Xml;
namespace Lakerfield.Wcf.Compression
{
public class GZipMessageEncodingBindingElementImporter : IPolicyImportExtension
{
public GZipMessageEncodingBindingElementImporter()
{
}
void IPolicyImportExtension.ImportPolicy(MetadataImporter importer, PolicyConversionContext context)
{
if (importer == null)
{
throw new ArgumentNullException("importer");
}
if (context == null)
{
throw new ArgumentNullException("context");
}
ICollection<XmlElement> assertions = context.GetBindingAssertions();
foreach (XmlElement assertion in assertions)
{
if ((assertion.NamespaceURI == GZipMessageEncodingPolicyConstants.GZipEncodingNamespace) &&
(assertion.LocalName == GZipMessageEncodingPolicyConstants.GZipEncodingName)
)
{
assertions.Remove(assertion);
context.BindingElements.Add(new GZipMessageEncodingBindingElement());
break;
}
}
}
}
}
@kenoma
Copy link

kenoma commented Jul 17, 2018

Is MemoryStream at CompressBuffer shouldn't be disposed?

@ormiz
Copy link

ormiz commented Jan 6, 2020

In GZipMessageEncoderFactory.cs line 94: ArraySegment<byte> byteArray = new ArraySegment<byte>(bufferedBytes, messageOffset, bufferedBytes.Length - messageOffset);
bufferedBytes.Length - messageOffset should be replaced with compressedBytes.Length as bufferedBytes can be a lot bigger. This causes to problems in decoding gzip in Postman and Chrome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment