From d1c9ce9bdffa2d5e765651bbfae35684c0ad11a9 Mon Sep 17 00:00:00 2001 From: Abdelwahab Afifi Date: Wed, 1 Feb 2023 20:41:10 +0200 Subject: [PATCH] Add source code of Compression Message Encoder Sample --- .../Compression/Client/Client.csproj | 25 +++ .../ConnectedService.json | 17 ++ .../Reference.cs | 72 +++++++++ .../Compression/Client/Program.cs | 38 +++++ ...tensibility.MessageEncoder.Compression.sln | 43 +++++ .../GZipEncoder/GZipEncoder.csproj | 18 +++ .../GZipEncoder/GZipMessageEncoderFactory.cs | 147 ++++++++++++++++++ .../GZipMessageEncodingBindingElement.cs | 54 +++++++ .../GZipEncoderCoreWCF.csproj | 21 +++ .../GZipMessageEncoderFactory.cs | 147 ++++++++++++++++++ .../GZipMessageEncodingBindingElement.cs | 116 ++++++++++++++ .../Compression/Service/EchoService.cs | 23 +++ .../Compression/Service/IEchoService.cs | 16 ++ .../Compression/Service/Program.cs | 34 ++++ .../Service/Properties/launchSettings.json | 28 ++++ .../Compression/Service/Service.csproj | 27 ++++ .../Service/appsettings.Development.json | 8 + .../Compression/Service/appsettings.json | 9 ++ 18 files changed, 843 insertions(+) create mode 100644 Extensibility/MessageEncoder/Compression/Client/Client.csproj create mode 100644 Extensibility/MessageEncoder/Compression/Client/Connected Services/CoreWcf.Samples.CompressionMessageEncoder/ConnectedService.json create mode 100644 Extensibility/MessageEncoder/Compression/Client/Connected Services/CoreWcf.Samples.CompressionMessageEncoder/Reference.cs create mode 100644 Extensibility/MessageEncoder/Compression/Client/Program.cs create mode 100644 Extensibility/MessageEncoder/Compression/Extensibility.MessageEncoder.Compression.sln create mode 100644 Extensibility/MessageEncoder/Compression/GZipEncoder/GZipEncoder.csproj create mode 100644 Extensibility/MessageEncoder/Compression/GZipEncoder/GZipMessageEncoderFactory.cs create mode 100644 Extensibility/MessageEncoder/Compression/GZipEncoder/GZipMessageEncodingBindingElement.cs create mode 100644 Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipEncoderCoreWCF.csproj create mode 100644 Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipMessageEncoderFactory.cs create mode 100644 Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipMessageEncodingBindingElement.cs create mode 100644 Extensibility/MessageEncoder/Compression/Service/EchoService.cs create mode 100644 Extensibility/MessageEncoder/Compression/Service/IEchoService.cs create mode 100644 Extensibility/MessageEncoder/Compression/Service/Program.cs create mode 100644 Extensibility/MessageEncoder/Compression/Service/Properties/launchSettings.json create mode 100644 Extensibility/MessageEncoder/Compression/Service/Service.csproj create mode 100644 Extensibility/MessageEncoder/Compression/Service/appsettings.Development.json create mode 100644 Extensibility/MessageEncoder/Compression/Service/appsettings.json diff --git a/Extensibility/MessageEncoder/Compression/Client/Client.csproj b/Extensibility/MessageEncoder/Compression/Client/Client.csproj new file mode 100644 index 0000000..fb65c84 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Client/Client.csproj @@ -0,0 +1,25 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/Extensibility/MessageEncoder/Compression/Client/Connected Services/CoreWcf.Samples.CompressionMessageEncoder/ConnectedService.json b/Extensibility/MessageEncoder/Compression/Client/Connected Services/CoreWcf.Samples.CompressionMessageEncoder/ConnectedService.json new file mode 100644 index 0000000..e595574 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Client/Connected Services/CoreWcf.Samples.CompressionMessageEncoder/ConnectedService.json @@ -0,0 +1,17 @@ +{ + "ExtendedData": { + "inputs": [ + "http://localhost:5000/gzipMessageEncoding?wsdl" + ], + "collectionTypes": [ + "System.Array", + "System.Collections.Generic.Dictionary`2" + ], + "namespaceMappings": [ + "*, CoreWcf.Samples.CompressionMessageEncoder" + ], + "sync": true, + "targetFramework": "net6.0", + "typeReuseMode": "All" + } +} \ No newline at end of file diff --git a/Extensibility/MessageEncoder/Compression/Client/Connected Services/CoreWcf.Samples.CompressionMessageEncoder/Reference.cs b/Extensibility/MessageEncoder/Compression/Client/Connected Services/CoreWcf.Samples.CompressionMessageEncoder/Reference.cs new file mode 100644 index 0000000..c4006c9 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Client/Connected Services/CoreWcf.Samples.CompressionMessageEncoder/Reference.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace CoreWcf.Samples.CompressionMessageEncoder +{ + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")] + [System.ServiceModel.ServiceContractAttribute(ConfigurationName="CoreWcf.Samples.CompressionMessageEncoder.IEchoService")] + public interface IEchoService + { + + [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IEchoService/Echo", ReplyAction="http://tempuri.org/IEchoService/EchoResponse")] + string Echo(string input); + + [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IEchoService/Echo", ReplyAction="http://tempuri.org/IEchoService/EchoResponse")] + System.Threading.Tasks.Task EchoAsync(string input); + + [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IEchoService/BigEcho", ReplyAction="http://tempuri.org/IEchoService/BigEchoResponse")] + string[] BigEcho(string[] input); + + [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IEchoService/BigEcho", ReplyAction="http://tempuri.org/IEchoService/BigEchoResponse")] + System.Threading.Tasks.Task BigEchoAsync(string[] input); + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")] + public interface IEchoServiceChannel : CoreWcf.Samples.CompressionMessageEncoder.IEchoService, System.ServiceModel.IClientChannel + { + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")] + public partial class EchoServiceClient : System.ServiceModel.ClientBase, CoreWcf.Samples.CompressionMessageEncoder.IEchoService + { + + public EchoServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : + base(binding, remoteAddress) + { + } + + public string Echo(string input) + { + return base.Channel.Echo(input); + } + + public System.Threading.Tasks.Task EchoAsync(string input) + { + return base.Channel.EchoAsync(input); + } + + public string[] BigEcho(string[] input) + { + return base.Channel.BigEcho(input); + } + + public System.Threading.Tasks.Task BigEchoAsync(string[] input) + { + return base.Channel.BigEchoAsync(input); + } + + public virtual System.Threading.Tasks.Task OpenAsync() + { + return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginOpen(null, null), new System.Action(((System.ServiceModel.ICommunicationObject)(this)).EndOpen)); + } + } +} diff --git a/Extensibility/MessageEncoder/Compression/Client/Program.cs b/Extensibility/MessageEncoder/Compression/Client/Program.cs new file mode 100644 index 0000000..59233a4 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Client/Program.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//The service contract is defined using Connected Service "WCF Web Service", generated from the service by the dotnet svcutil tool. + +BasicHttpBinding basicHttpBinding = new BasicHttpBinding(); +HttpTransportBindingElement httpTransportBindingElement = basicHttpBinding.CreateBindingElements().Find(); +MessageEncodingBindingElement encodingBindingElement = new GZipMessageEncodingBindingElement(); +CustomBinding binding = new CustomBinding(new BindingElement[] +{ + encodingBindingElement, + httpTransportBindingElement +}); + +var endpointAddress = new EndpointAddress("http://localhost:5000/gzipMessageEncoding"); + +// Create a client with given client endpoint configuration +EchoServiceClient client = new EchoServiceClient(binding, endpointAddress); + +Console.WriteLine("Calling Echo(string):"); +Console.WriteLine("Server responds: {0}", client.Echo("Simple hello")); + +Console.WriteLine(); +Console.WriteLine("Calling BigEcho(string[]):"); +string[] messages = new string[64]; +for (int i = 0; i < 64; i++) +{ + messages[i] = "Hello " + i; +} + +Console.WriteLine("Server responds: {0}", client.BigEcho(messages)); + +//Closing the client gracefully closes the connection and cleans up resources +client.Close(); + +Console.WriteLine(); +Console.WriteLine("Press to terminate client."); +Console.ReadLine(); diff --git a/Extensibility/MessageEncoder/Compression/Extensibility.MessageEncoder.Compression.sln b/Extensibility/MessageEncoder/Compression/Extensibility.MessageEncoder.Compression.sln new file mode 100644 index 0000000..e061c41 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Extensibility.MessageEncoder.Compression.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32422.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service\Service.csproj", "{BF126326-3393-407C-B24A-8FCCC388BE27}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{B533CADA-93BB-40E1-8FBA-FE37100062C3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GZipEncoder", "GZipEncoder\GZipEncoder.csproj", "{D1EAF072-0F66-4BD0-BC53-D4ABC45163C5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GZipEncoderCoreWCF", "GZipEncoderCoreWCF\GZipEncoderCoreWCF.csproj", "{5DA1D92E-E835-49B7-8009-BD75E1ECC050}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BF126326-3393-407C-B24A-8FCCC388BE27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF126326-3393-407C-B24A-8FCCC388BE27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF126326-3393-407C-B24A-8FCCC388BE27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF126326-3393-407C-B24A-8FCCC388BE27}.Release|Any CPU.Build.0 = Release|Any CPU + {B533CADA-93BB-40E1-8FBA-FE37100062C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B533CADA-93BB-40E1-8FBA-FE37100062C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B533CADA-93BB-40E1-8FBA-FE37100062C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B533CADA-93BB-40E1-8FBA-FE37100062C3}.Release|Any CPU.Build.0 = Release|Any CPU + {D1EAF072-0F66-4BD0-BC53-D4ABC45163C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1EAF072-0F66-4BD0-BC53-D4ABC45163C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1EAF072-0F66-4BD0-BC53-D4ABC45163C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1EAF072-0F66-4BD0-BC53-D4ABC45163C5}.Release|Any CPU.Build.0 = Release|Any CPU + {5DA1D92E-E835-49B7-8009-BD75E1ECC050}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DA1D92E-E835-49B7-8009-BD75E1ECC050}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DA1D92E-E835-49B7-8009-BD75E1ECC050}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DA1D92E-E835-49B7-8009-BD75E1ECC050}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AD996EFD-70DC-4431-B411-5A2771DD02D3} + EndGlobalSection +EndGlobal diff --git a/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipEncoder.csproj b/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipEncoder.csproj new file mode 100644 index 0000000..d0bfff1 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipEncoder.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipMessageEncoderFactory.cs b/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipMessageEncoderFactory.cs new file mode 100644 index 0000000..cfbcb3a --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipMessageEncoderFactory.cs @@ -0,0 +1,147 @@ + +using System.IO.Compression; + +namespace CoreWcf.Samples.GZipEncoder +{ + //This class is used to create the custom encoder (GZipMessageEncoder) + internal class GZipMessageEncoderFactory : MessageEncoderFactory + { + private readonly 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(nameof(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 => _encoder; + + public override MessageVersion MessageVersion => _encoder.MessageVersion; + + //This is the actual GZip encoder + public class GZipMessageEncoder : MessageEncoder + { + private static readonly string s_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. + private readonly MessageEncoder _innerEncoder; + + //We require an inner encoder to be supplied (see comment above) + internal GZipMessageEncoder(MessageEncoder messageEncoder) + : base() + { + _innerEncoder = messageEncoder ?? throw new ArgumentNullException(nameof(messageEncoder), "A valid message encoder must be passed to the GZipEncoder"); + } + + public override string ContentType => s_gZipContentType; + + public override string MediaType => s_gZipContentType; + + //SOAP version to use - we delegate to the inner encoder for this + public override MessageVersion MessageVersion => _innerEncoder.MessageVersion; + + //Helper method to compress an array of bytes + static ArraySegment CompressBuffer(ArraySegment 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 byteArray = new ArraySegment(bufferedBytes, messageOffset, bufferedBytes.Length - messageOffset); + + return byteArray; + } + + //Helper method to decompress an array of bytes + static ArraySegment DecompressBuffer(ArraySegment 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 byteArray = new ArraySegment(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 buffer, BufferManager bufferManager, string contentType) + { + //Decompress the buffer + ArraySegment 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; + } + + //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 WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) + { + //Use the inner encoder to encode a Message into a buffered byte array + ArraySegment buffer = _innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, 0); + //Compress the resulting byte array + return CompressBuffer(buffer, 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); + } + + 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(); + } + } + } +} diff --git a/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipMessageEncodingBindingElement.cs b/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipMessageEncodingBindingElement.cs new file mode 100644 index 0000000..8944af9 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/GZipEncoder/GZipMessageEncodingBindingElement.cs @@ -0,0 +1,54 @@ +using System.Xml; + +namespace CoreWcf.Samples.GZipEncoder +{ + //This is the binding element that, when plugged into a custom binding, will enable the GZip encoder + public sealed class GZipMessageEncodingBindingElement : MessageEncodingBindingElement + { + //By default, use the default text encoder as the inner encoder + public GZipMessageEncodingBindingElement() + : this(new TextMessageEncodingBindingElement()) { } + + public GZipMessageEncodingBindingElement(MessageEncodingBindingElement messageEncoderBindingElement) + { + InnerMessageEncodingBindingElement = messageEncoderBindingElement; + } + + public MessageEncodingBindingElement InnerMessageEncodingBindingElement { get; } + + //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(InnerMessageEncodingBindingElement.CreateMessageEncoderFactory()); + } + + public override MessageVersion MessageVersion + { + get { return InnerMessageEncodingBindingElement.MessageVersion; } + set { InnerMessageEncodingBindingElement.MessageVersion = value; } + } + + public override BindingElement Clone() => new GZipMessageEncodingBindingElement(InnerMessageEncodingBindingElement); + + public override T GetProperty(BindingContext context) + { + if (typeof(T) == typeof(XmlDictionaryReaderQuotas)) + { + return InnerMessageEncodingBindingElement.GetProperty(context); + } + else + { + return base.GetProperty(context); + } + } + + public override IChannelFactory BuildChannelFactory(BindingContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + context.BindingParameters.Add(this); + return context.BuildInnerChannelFactory(); + } + } +} diff --git a/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipEncoderCoreWCF.csproj b/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipEncoderCoreWCF.csproj new file mode 100644 index 0000000..0739e21 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipEncoderCoreWCF.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipMessageEncoderFactory.cs b/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipMessageEncoderFactory.cs new file mode 100644 index 0000000..290e135 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipMessageEncoderFactory.cs @@ -0,0 +1,147 @@ + +using System.IO.Compression; + +namespace CoreWcf.Samples.GZipEncoder +{ + //This class is used to create the custom encoder (GZipMessageEncoder) + internal class GZipMessageEncoderFactory : MessageEncoderFactory + { + private readonly 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(nameof(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 => _encoder; + + public override MessageVersion MessageVersion => _encoder.MessageVersion; + + //This is the actual GZip encoder + public class GZipMessageEncoder : MessageEncoder + { + private static readonly string s_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. + private readonly MessageEncoder _innerEncoder; + + //We require an inner encoder to be supplied (see comment above) + internal GZipMessageEncoder(MessageEncoder messageEncoder) + : base() + { + _innerEncoder = messageEncoder ?? throw new ArgumentNullException(nameof(messageEncoder), "A valid message encoder must be passed to the GZipEncoder"); + } + + public override string ContentType => s_gZipContentType; + + public override string MediaType => s_gZipContentType; + + //SOAP version to use - we delegate to the inner encoder for this + public override MessageVersion MessageVersion => _innerEncoder.MessageVersion; + + //Helper method to compress an array of bytes + static ArraySegment CompressBuffer(ArraySegment 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 byteArray = new ArraySegment(bufferedBytes, messageOffset, bufferedBytes.Length - messageOffset); + + return byteArray; + } + + //Helper method to decompress an array of bytes + static ArraySegment DecompressBuffer(ArraySegment 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 byteArray = new ArraySegment(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 buffer, BufferManager bufferManager, string contentType) + { + //Decompress the buffer + ArraySegment 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; + } + + //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 WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) + { + //Use the inner encoder to encode a Message into a buffered byte array + ArraySegment buffer = _innerEncoder.WriteMessage(message, maxMessageSize, bufferManager, 0); + //Compress the resulting byte array + return CompressBuffer(buffer, bufferManager, messageOffset); + } + + public override async Task ReadMessageAsync(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 await _innerEncoder.ReadMessageAsync(gzStream, maxSizeOfHeaders); + } + + public override async Task WriteMessageAsync(Message message, System.IO.Stream stream) + { + using (GZipStream gzStream = new GZipStream(stream, CompressionMode.Compress, true)) + { + await _innerEncoder.WriteMessageAsync(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(); + } + } + } +} diff --git a/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipMessageEncodingBindingElement.cs b/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipMessageEncodingBindingElement.cs new file mode 100644 index 0000000..6bf59b6 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/GZipEncoderCoreWCF/GZipMessageEncodingBindingElement.cs @@ -0,0 +1,116 @@ + +using System.Configuration; +using System.Xml; + +namespace CoreWcf.Samples.GZipEncoder +{ + // 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, IPolicyExportExtension + { + //By default, use the default text encoder as the inner encoder + public GZipMessageEncodingBindingElement() + : this(new TextMessageEncodingBindingElement()) { } + + public GZipMessageEncodingBindingElement(MessageEncodingBindingElement messageEncoderBindingElement) + { + InnerMessageEncodingBindingElement = messageEncoderBindingElement; + } + + public MessageEncodingBindingElement InnerMessageEncodingBindingElement { get; set; } + + //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(InnerMessageEncodingBindingElement.CreateMessageEncoderFactory()); + } + + public override MessageVersion MessageVersion + { + get { return InnerMessageEncodingBindingElement.MessageVersion; } + set { InnerMessageEncodingBindingElement.MessageVersion = value; } + } + + public override BindingElement Clone() => new GZipMessageEncodingBindingElement(InnerMessageEncodingBindingElement); + + public override T GetProperty(BindingContext context) + { + if (typeof(T) == typeof(XmlDictionaryReaderQuotas)) + { + return InnerMessageEncodingBindingElement.GetProperty(context); + } + else + { + return base.GetProperty(context); + } + } + + void IPolicyExportExtension.ExportPolicy(MetadataExporter exporter, PolicyConversionContext policyContext) + { + if (policyContext == null) + { + throw new ArgumentNullException(nameof(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 => 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 = ElementInformation.Properties; + if (propertyInfo["innerMessageEncoding"].ValueOrigin != PropertyValueOrigin.Default) + { + switch (InnerMessageEncoding) + { + case "textMessageEncoding": + binding.InnerMessageEncodingBindingElement = new TextMessageEncodingBindingElement(); + 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(); + ApplyConfiguration(bindingElement); + return bindingElement; + } + } +} diff --git a/Extensibility/MessageEncoder/Compression/Service/EchoService.cs b/Extensibility/MessageEncoder/Compression/Service/EchoService.cs new file mode 100644 index 0000000..9b5a573 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Service/EchoService.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CoreWcf.Samples.CompressionMessageEncoder +{ + // Service class which implements the service contract interface. + public class EchoService : IEchoService + { + public string Echo(string input) + { + Console.WriteLine("\n\tServer Echo(string input) called:", input); + Console.WriteLine("\tClient message:\t{0}\n", input); + return input + " " + input; + } + + public string[] BigEcho(string[] input) + { + Console.WriteLine("\n\tServer BigEcho(string[] input) called:", input); + Console.WriteLine("\t{0} client messages", input.Length); + return input; + } + } +} diff --git a/Extensibility/MessageEncoder/Compression/Service/IEchoService.cs b/Extensibility/MessageEncoder/Compression/Service/IEchoService.cs new file mode 100644 index 0000000..b35f6da --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Service/IEchoService.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CoreWcf.Samples.CompressionMessageEncoder +{ + // Define a service contract. + [ServiceContract] + public interface IEchoService + { + [OperationContract] + string Echo(string input); + + [OperationContract] + string[] BigEcho(string[] input); + } +} diff --git a/Extensibility/MessageEncoder/Compression/Service/Program.cs b/Extensibility/MessageEncoder/Compression/Service/Program.cs new file mode 100644 index 0000000..d93693e --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Service/Program.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +var builder = WebApplication.CreateBuilder(); + +//Enable CoreWCF Services, with metadata (WSDL) support +builder.Services.AddServiceModelServices() + .AddServiceModelMetadata() + .AddSingleton(); + +var app = builder.Build(); + +app.UseServiceModel(builder => +{ + // Add the Calculator Service + builder.AddService(serviceOptions => { }); + + BasicHttpBinding basicHttpBinding = new BasicHttpBinding(); + HttpTransportBindingElement httpTransportBindingElement = basicHttpBinding.CreateBindingElements().Find(); + MessageEncodingBindingElement encodingBindingElement = new GZipMessageEncodingBindingElement(); + CustomBinding binding = new CustomBinding(new BindingElement[] + { + encodingBindingElement, + httpTransportBindingElement + }); + + builder.AddServiceEndpoint(binding, "gzipMessageEncoding"); + + // Configure WSDL to be available + var serviceMetadataBehavior = app.Services.GetRequiredService(); + serviceMetadataBehavior.HttpGetEnabled = true; +}); + +app.Run(); diff --git a/Extensibility/MessageEncoder/Compression/Service/Properties/launchSettings.json b/Extensibility/MessageEncoder/Compression/Service/Properties/launchSettings.json new file mode 100644 index 0000000..47a16a7 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Service/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:57035/gzipMessageEncoding", + "sslPort": 44315 + } + }, + "profiles": { + "Service": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "http://localhost:5000/gzipMessageEncoding", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Extensibility/MessageEncoder/Compression/Service/Service.csproj b/Extensibility/MessageEncoder/Compression/Service/Service.csproj new file mode 100644 index 0000000..01d8e08 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Service/Service.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + true + InProcess + + + + + + + + + + + + + + + + + + + + diff --git a/Extensibility/MessageEncoder/Compression/Service/appsettings.Development.json b/Extensibility/MessageEncoder/Compression/Service/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Service/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Extensibility/MessageEncoder/Compression/Service/appsettings.json b/Extensibility/MessageEncoder/Compression/Service/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Extensibility/MessageEncoder/Compression/Service/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}