diff --git a/.gitignore b/.gitignore index 2a7adc0..9549575 100644 --- a/.gitignore +++ b/.gitignore @@ -249,6 +249,10 @@ $RECYCLE.BIN/ .DS_Store .vs/config/applicationhost.config .vs/ + +# VS Code +.vscode +*.code-workspace .idea/ # Windows Installer files diff --git a/Obvs.Tests/GZippedMessageSerializationTest.cs b/Obvs.Tests/GZippedMessageSerializationTest.cs new file mode 100644 index 0000000..fbde0c1 --- /dev/null +++ b/Obvs.Tests/GZippedMessageSerializationTest.cs @@ -0,0 +1,157 @@ +using System; +using System.Text; +using System.IO; + +using FakeItEasy; + +using Obvs.Types; +using Obvs.Serialization; + +using Xunit; + +namespace Obvs.Tests { + + /// + /// Test Gzipped message serialization + /// + public class GZippedMessageSerializationTest { + + public class TestMessage: IMessage { + public string Content {get; set;} = null; + + public override bool Equals(object obj) + { + // + // See the full list of guidelines at + // http://go.microsoft.com/fwlink/?LinkID=85237 + // and also the guidance for operator== at + // http://go.microsoft.com/fwlink/?LinkId=85238 + // + + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + + return this.Content.Equals(((TestMessage) obj).Content); + } + + // override object.GetHashCode + public override int GetHashCode() + { + return Content.GetHashCode(); + } + + } + + #region "Serialization tests" + [Fact] + public void Test_GZippedMessageSerializerNullActionConstruction_Fails() { + Assert.Throws(typeof(ArgumentNullException), + () => new GZippedMessageSerializer(null as Action)); + } + + [Fact] + public void Test_GZippedMessageSerializerNullGzippedSerializerConstruction_Fails() { + Assert.Throws(typeof(ArgumentNullException), + () => new GZippedMessageSerializer(null as GZippedMessageSerializer)); + } + + [Fact] + public void Test_GZippedMessageSerializerGzippedSerializerConstruction_Fails() { + Assert.Throws(typeof(ArgumentException), + () => new GZippedMessageSerializer(new GZippedMessageSerializer( (Stream stream, object obj) => {}))); + } + + #endregion + + #region "Deserialization tests" + + [Fact] + public void Test_GZippedMessageDeserializerNullActionConstruction_Fails() { + Assert.Throws( + typeof(ArgumentNullException), + () => new GZippedMessageDeserializer(null as Func)); + } + + + [Fact] + public void Test_GZippedMessageDeserializerNullGzippedSerializerConstruction_Fails() { + Assert.Throws(typeof(ArgumentNullException), + () => new GZippedMessageDeserializer(null as GZippedMessageDeserializer)); + } + #endregion + + [Fact] + public void Test_GZippedMessageDeserializerGzippedSerializerConstruction_Fails() { + Assert.Throws(typeof(ArgumentException), + () => new GZippedMessageDeserializer(new GZippedMessageDeserializer( (Stream stream) => new TestMessage{}))); + } + + [Fact] + public void Test_GzippedMessageFullSerialization_Succeeds() { + var content = "test"; + var wasSerializeActionCalled = false; + Action serializeAction = (stream, obj) => { + wasSerializeActionCalled = true; + var message = obj as TestMessage; + var contentBytes = Encoding.UTF8.GetBytes(message.Content); + stream.Write(contentBytes, 0, contentBytes.Length); + }; + var messageSerializer = new GZippedMessageSerializer(serializeAction); + + var wasDeserializeFuncCalled = false; + Func deserializeFunction = stream => { + wasDeserializeFuncCalled = true; + var buffer = new byte[Encoding.UTF8.GetByteCount(content)]; + var numBytesRead = stream.Read(buffer, 0, buffer.Length); + var deserializedMessage = new TestMessage { + Content = Encoding.UTF8.GetString(buffer) + }; + return deserializedMessage; + }; + var messageDeserializer = new GZippedMessageDeserializer(deserializeFunction); + + var testMessage = new TestMessage { + Content = content + }; + var messageStream = new MemoryStream(); + messageSerializer.Serialize(messageStream, testMessage); + Assert.True(wasSerializeActionCalled); + messageStream.Position = 0; + var deserializedTestMessage = messageDeserializer.Deserialize(messageStream); + Assert.True(wasDeserializeFuncCalled); + + Assert.Equal(testMessage, deserializedTestMessage); + } + + #region "IMessage serializer extensions" + [Fact] + public void Test_SerializeGZipped_Fails() { + var messageSerializer = new GZippedMessageSerializer((stream, obj) => {}); + Assert.Throws(typeof(ArgumentException), () => messageSerializer.SerializeGZipped()); + } + + [Fact] + public void Test_DeserializeGZipped_Fails() { + var messageDeserializer = new GZippedMessageDeserializer((stream) => new TestMessage{}); + Assert.Throws(typeof(ArgumentException), () => messageDeserializer.DeserializeGZipped()); + } + + [Fact] + public void Test_SerializeGZipped_Success() { + var messageSerializer = A.Fake(); + var gzippedMessageSerializer = messageSerializer.SerializeGZipped(); + Assert.NotNull(gzippedMessageSerializer); + } + + [Fact] + public void Test_DeserializeGZipped_Success() { + var messageDeserializer = A.Fake>(); + var gzippedMessageDeserializer = messageDeserializer.DeserializeGZipped(); + Assert.NotNull(gzippedMessageDeserializer); + } + #endregion + + } +} \ No newline at end of file diff --git a/Obvs.Tests/Obvs.Tests.csproj b/Obvs.Tests/Obvs.Tests.csproj index a656c5f..6ccc5ef 100644 --- a/Obvs.Tests/Obvs.Tests.csproj +++ b/Obvs.Tests/Obvs.Tests.csproj @@ -3,8 +3,9 @@ net5.0 Christopher Read Copyright © Christopher Read 2014 - + false + true @@ -17,6 +18,6 @@ - + \ No newline at end of file diff --git a/Obvs/Obvs.csproj b/Obvs/Obvs.csproj index 5b86eff..88580d7 100644 --- a/Obvs/Obvs.csproj +++ b/Obvs/Obvs.csproj @@ -9,6 +9,7 @@ obvs messaging microservice bus messagebus rx reactive servicebus New csproj format An observable microservice bus .NET library, based on Reactive Extensions. Search 'Obvs' for other transport, serialization, and logging extensions. + true diff --git a/Obvs/Serialization/GZippedMessageDeserializer.cs b/Obvs/Serialization/GZippedMessageDeserializer.cs new file mode 100644 index 0000000..098bb60 --- /dev/null +++ b/Obvs/Serialization/GZippedMessageDeserializer.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.IO.Compression; + +namespace Obvs.Serialization +{ + + /// + /// Gzipped message deserializer + /// + /// Type of message + public class GZippedMessageDeserializer : MessageDeserializerBase + where TMessage : class + { + /// + /// Message from stream deserializer functor + /// + private readonly Func _messageStreamDeserializerFn; + + /// + /// Constructor + /// + /// Message deserializer fn + public GZippedMessageDeserializer(Func messageStreamDeserializerFn) + { + if (messageStreamDeserializerFn == null) { + throw new ArgumentNullException(nameof(messageStreamDeserializerFn)); + } + _messageStreamDeserializerFn = messageStreamDeserializerFn; + } + + /// + /// Constructor + /// + /// Message deserializer implementation< + public GZippedMessageDeserializer(IMessageDeserializer messageDeserializer) { + if (messageDeserializer == null) { + throw new ArgumentNullException(nameof(messageDeserializer)); + } + var gzippedMessageDeserializer = messageDeserializer as GZippedMessageDeserializer; + if (gzippedMessageDeserializer != null) { + throw new ArgumentException("Invalid message deserializer GZippedMessageDeserializer"); + } + _messageStreamDeserializerFn = messageDeserializer.Deserialize; + } + + /// + public override TMessage Deserialize(Stream stream) + { + using (var gzipStream = new GZipStream(stream, CompressionMode.Decompress, true)) + { + return _messageStreamDeserializerFn(gzipStream); + } + } + } + + public static class GZippedMessageDeserializerExtensions { + + /// + /// Apply GZip decompression to an existing message serializer + /// + /// Type of message + public static GZippedMessageDeserializer DeserializeGZipped( + this IMessageDeserializer messageDeserializer + ) where TMessage : class { + if (messageDeserializer is GZippedMessageDeserializer) { + throw new ArgumentException("Message serializer implementation cannot be GzippedMessageDeserializer"); + } + return new GZippedMessageDeserializer(messageDeserializer.Deserialize); + } + } +} \ No newline at end of file diff --git a/Obvs/Serialization/GZippedMessageSerializer.cs b/Obvs/Serialization/GZippedMessageSerializer.cs new file mode 100644 index 0000000..94b13ea --- /dev/null +++ b/Obvs/Serialization/GZippedMessageSerializer.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using System.IO.Compression; + +namespace Obvs.Serialization +{ + /// + /// Gzipped message serializer + /// + public class GZippedMessageSerializer : IMessageSerializer + { + + /// + /// Message to strem serializer functor + /// + private readonly Action _messageStreamSerializerFn; + + /// + /// Compression level + /// + private readonly CompressionLevel _compressionLevel; + + /// + /// Constructor + /// + /// Message stream serializer + /// Compression level + public GZippedMessageSerializer(Action messageStreamSerializerFn, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (messageStreamSerializerFn == null) { + throw new ArgumentNullException(nameof(messageStreamSerializerFn)); + } + _messageStreamSerializerFn = messageStreamSerializerFn; + _compressionLevel = compressionLevel; + } + + /// + /// Constructor + /// + /// Message stream serializer + /// Compression level + public GZippedMessageSerializer(IMessageSerializer messageSerializer, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + if (messageSerializer == null) { + throw new ArgumentNullException(nameof(messageSerializer)); + } + var gzippedMessageSerializer = messageSerializer as GZippedMessageSerializer; + if (gzippedMessageSerializer != null) { + throw new ArgumentException("Invalid message deserializer GZippedMessageSerializer"); + } + _messageStreamSerializerFn = messageSerializer.Serialize; + _compressionLevel = compressionLevel; + } + + /// + public void Serialize(Stream stream, object message) + { + using (var gzipStream = new GZipStream(stream, _compressionLevel, true)) + { + _messageStreamSerializerFn(gzipStream, message); + } + } + } + + public static class GZippedMessageSerializerExtensions { + + /// + /// Apply GZip compression to an existing message serializer + /// + /// Type of message + public static GZippedMessageSerializer SerializeGZipped(this IMessageSerializer messageSerializer) { + if (messageSerializer is GZippedMessageSerializer) { + throw new ArgumentException("Message serializer implementation cannot be GzippedMessageSerializer"); + } + return new GZippedMessageSerializer(messageSerializer.Serialize); + } + } +} \ No newline at end of file diff --git a/Obvs/Serialization/IMessageSerializer.cs b/Obvs/Serialization/IMessageSerializer.cs index 7d92f6a..24fef17 100644 --- a/Obvs/Serialization/IMessageSerializer.cs +++ b/Obvs/Serialization/IMessageSerializer.cs @@ -2,7 +2,7 @@ namespace Obvs.Serialization { - public static class MessageSerializerExtentions + public static class MessageSerializerExtensions { public static byte[] Serialize(this IMessageSerializer serializer, object obj) {