diff --git a/src/AvroConvert/AvroConvert.DeserializeContainer.cs b/src/AvroConvert/AvroConvert.DeserializeContainer.cs new file mode 100644 index 00000000..0d334d88 --- /dev/null +++ b/src/AvroConvert/AvroConvert.DeserializeContainer.cs @@ -0,0 +1,146 @@ +#region license +/**Copyright (c) 2021 Adrian Strugala +* +* Licensed under the CC BY-NC-SA 3.0 License(the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://creativecommons.org/licenses/by-nc-sa/3.0/ +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* You are free to use or modify the code for personal usage. +* For commercial usage purchase the product at +* +* https://xabe.net/product/avroconvert/ +*/ +#endregion + +using System; +using System.Collections; +using System.IO; +using SolTechnology.Avro.AvroObjectServices.BuildSchema; +using SolTechnology.Avro.Features.Deserialize; +using SolTechnology.Avro.Infrastructure.Exceptions; +using SolTechnology.Avro.Infrastructure.Extensions; + +namespace SolTechnology.Avro +{ + public static partial class AvroConvert + { + /// + /// Deserializes Avro data from a byte array into a specified .NET type. + /// + /// The type of object to deserialize into. + /// The byte array containing the Avro data to be deserialized. + /// The deserialized object of the specified type. + /// + /// This method takes a byte array containing Avro data and deserializes it into an object of the specified type (generic parameter T). + /// + public static T DeserializeContainer(byte[] avroBytes) + where T: IEnumerable + { + var type = GetEnumerableType(); + + using (var stream = new MemoryStream(avroBytes)) + { + var decoder = new Decoder(); + var deserialized = decoder.Decode( + stream, + Schema.Create(type) + ); + return deserialized; + } + } + + /// + /// Deserializes Avro data from a byte array into a specified .NET type using the provided Avro conversion options. + /// + /// The type of object to deserialize into. + /// The byte array containing the Avro data to be deserialized. + /// The Avro conversion options that control the deserialization process. + /// The deserialized object of the specified type. + /// + /// This method takes a byte array containing Avro data and deserializes it into an object of the specified type (generic parameter T). + /// The deserialization process is controlled by the provided Avro conversion options. + /// + public static T DeserializeContainer(byte[] avroBytes, AvroConvertOptions options) + where T: IEnumerable + { + var type = GetEnumerableType(); + + using (var stream = new MemoryStream(avroBytes)) + { + var decoder = new Decoder(options); + var deserialized = decoder.Decode( + stream, + Schema.Create(type) + ); + return deserialized; + } + } + + /// + /// Deserializes Avro data from a byte array into an object of the specified target type using reflection. + /// + /// The byte array containing the Avro data to be deserialized. + /// The target type into which the Avro data should be deserialized. + /// The deserialized object of the specified target type. + /// + /// This method uses reflection to dynamically invoke the generic method + /// to deserialize Avro data from a byte array into an object of the specified target type. + /// + public static dynamic DeserializeContainer(byte[] avroBytes, Type targetType) + { + object result = typeof(AvroConvert) + .GetMethod(nameof(DeserializeContainer), new[] { typeof(byte[]) }) + ?.MakeGenericMethod(targetType) + .Invoke(null, new object[] { avroBytes }); + + return result; + } + + + /// + /// Deserializes Avro data from a of bytes into an object of the specified type. + /// + /// The type of object to deserialize into. + /// The of bytes containing the Avro data to be deserialized. + /// The deserialized object of the specified type. + /// + /// This method performs Avro data deserialization from a of bytes into an object of the specified type. + /// It is suitable for scenarios where minimizing memory allocation is crucial. + /// + public static unsafe T DeserializeContainer(ReadOnlySpan avroBytes) + where T: IEnumerable + { + var type = GetEnumerableType(); + + fixed (byte* ptr = avroBytes) + { + using UnmanagedMemoryStream stream = new(ptr, avroBytes.Length); + var decoder = new Decoder(); + var obj = decoder.Decode( + stream, + Schema.Create(type) + ); + + return obj; + } + } + + private static Type GetEnumerableType() + { + var type = typeof(T); + + if (!type.IsEnumerable()) + throw new AvroException("[IEnumerable] required to deserialize container but found " + type); + + return type.GetEnumeratedType(); + } + } +} diff --git a/src/AvroConvert/AvroConvert.SerializeContainer.cs b/src/AvroConvert/AvroConvert.SerializeContainer.cs new file mode 100644 index 00000000..2d779974 --- /dev/null +++ b/src/AvroConvert/AvroConvert.SerializeContainer.cs @@ -0,0 +1,87 @@ +#region license +/**Copyright (c) 2020 Adrian Strugała +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#endregion + +using System.Collections.Generic; +using System.IO; +using SolTechnology.Avro.AvroObjectServices.BuildSchema; +using SolTechnology.Avro.Features.Serialize; + +namespace SolTechnology.Avro +{ + public static partial class AvroConvert + { + /// + /// Serializes the given into Avro container format (including header with metadata) + /// + public static byte[] SerializeContainer(IEnumerable items) + { + return SerializeContainer(items, CodecType.Null); + } + + /// + /// Serializes the given into Avro container format (including header with metadata) + /// Choosing reduces output object size + /// + public static byte[] SerializeContainer(IEnumerable items, CodecType codecType) + where T : notnull + { + using (MemoryStream resultStream = new MemoryStream()) + { + var schema = Schema.Create(typeof(T)); + + using (var writer = new Encoder(schema, resultStream, codecType)) + { + foreach (var item in items) + { + writer.Append(item); + } + } + + byte[] result = resultStream.ToArray(); + return result; + } + } + + /// + /// Serializes the given into Avro container format (including header with metadata) and returns the serialized data as a byte array. + /// + /// The items to be serialized into Avro container format. + /// The Avro conversion options that control the serialization process. + /// A byte array containing the serialized Avro data. + /// + /// This method takes an and serializes it into Avro container format based on the provided Avro conversion options. + /// The resulting serialized data is returned as a byte array. + /// + public static byte[] SerializeContainer(IEnumerable items, AvroConvertOptions options) + where T : notnull + { + using (MemoryStream resultStream = new MemoryStream()) + { + var schema = Schema.Create(typeof(T), options); + using (var writer = new Encoder(schema, resultStream, options.Codec, options)) + { + foreach (var item in items) + { + writer.Append(item); + } + } + byte[] result = resultStream.ToArray(); + return result; + } + } + } +} \ No newline at end of file diff --git a/src/AvroConvert/AvroObjectServices/Read/Resolvers/Array.cs b/src/AvroConvert/AvroObjectServices/Read/Resolvers/Array.cs index a3ca5ab5..35c8eaa7 100644 --- a/src/AvroConvert/AvroObjectServices/Read/Resolvers/Array.cs +++ b/src/AvroConvert/AvroObjectServices/Read/Resolvers/Array.cs @@ -41,7 +41,11 @@ internal object ResolveArray(TypeSchema writerSchema, TypeSchema readerSchema, I { writerSchema = ((ArraySchema)writerSchema).ItemSchema; } - readerSchema = ((ArraySchema)readerSchema).ItemSchema; + + if (readerSchema is ArraySchema arraySchema) + { + readerSchema = arraySchema.ItemSchema; + } if (type.IsDictionary()) { diff --git a/src/AvroConvert/Infrastructure/Extensions/TypeExtensions.cs b/src/AvroConvert/Infrastructure/Extensions/TypeExtensions.cs index f0c731c2..5ac42b75 100644 --- a/src/AvroConvert/Infrastructure/Extensions/TypeExtensions.cs +++ b/src/AvroConvert/Infrastructure/Extensions/TypeExtensions.cs @@ -417,6 +417,11 @@ internal static bool IsList(this Type type) return typeof(IList).IsAssignableFrom(type); } + internal static bool IsEnumerable(this Type type) + { + return typeof(IEnumerable).IsAssignableFrom(type); + } + internal static bool IsGuid(this Type type) { return type == typeof(Guid); diff --git a/tests/AvroConvertTests/FullSerializationAndDeserialization/ContainerTests.cs b/tests/AvroConvertTests/FullSerializationAndDeserialization/ContainerTests.cs new file mode 100644 index 00000000..10b61fb5 --- /dev/null +++ b/tests/AvroConvertTests/FullSerializationAndDeserialization/ContainerTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoFixture; +using Xunit; + +namespace AvroConvertComponentTests.FullSerializationAndDeserialization +{ + public class ContainerTests + { + private readonly Fixture _fixture = new(); + + [Theory] + [MemberData(nameof(TestEngine.ContainerOnly), MemberType = typeof(TestEngine))] + public void AvroMap(Func engine) + { + //Arrange + List expected = new List() { 1, 2, 3, 5, 8, 13 }; + + + //Act + var actual = engine.Invoke(expected, typeof(List)); + + + //Assert + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(TestEngine.ContainerOnly), MemberType = typeof(TestEngine))] + public void List_of_class_map(Func engine) + { + //Arrange + List expected = _fixture.CreateMany(5).ToList(); + + + //Act + var actual = engine.Invoke(expected, typeof(List)); + + + //Assert + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(TestEngine.ContainerOnly), MemberType = typeof(TestEngine))] + public void Array_of_class_map(Func engine) + { + //Arrange + ExtendedBaseTestClass[] expected = _fixture.CreateMany(5).ToArray(); + + + //Act + var actual = engine.Invoke(expected, typeof(ExtendedBaseTestClass[])); + + + //Assert + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(TestEngine.ContainerOnly), MemberType = typeof(TestEngine))] + public void Enumerable_of_class_map_to_list(Func engine) + { + //Arrange + IEnumerable expected = _fixture.CreateMany(5); + + + //Act + var actual = engine.Invoke(expected, typeof(List)); + + + //Assert + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(TestEngine.ContainerOnly), MemberType = typeof(TestEngine))] + public void Enumerable_of_class_map_to_array(Func engine) + { + //Arrange + IEnumerable expected = _fixture.CreateMany(5); + + + //Act + var actual = engine.Invoke(expected, typeof(ExtendedBaseTestClass[])); + + + //Assert + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + } +} \ No newline at end of file diff --git a/tests/AvroConvertTests/TestEngine.cs b/tests/AvroConvertTests/TestEngine.cs index 4c7b4aed..44af2969 100644 --- a/tests/AvroConvertTests/TestEngine.cs +++ b/tests/AvroConvertTests/TestEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using SolTechnology.Avro; namespace AvroConvertComponentTests; @@ -36,6 +37,11 @@ public static IEnumerable Core() yield return Snappy; } + public static IEnumerable ContainerOnly() + { + yield return Container; + } + public static IEnumerable CoreUsingSchema() { yield return DefaultWithSchema; @@ -83,6 +89,26 @@ private static object[] Headless return new object[] { headless }; } } + + private static object[] Container + { + get + { + var @default = new Func((input, type) => + { + var actualType = input.GetType().IsArray ? input.GetType().GetElementType() : input.GetType().GetGenericArguments().First(); + + var serializeContainerMethod = typeof(AvroConvert).GetMethods() + .Where(x => x.Name == nameof(AvroConvert.SerializeContainer)) + .First(x => x.IsGenericMethod)!.MakeGenericMethod(actualType!); + + var serialized = (byte[])serializeContainerMethod.Invoke(null, new [] { input }); + return AvroConvert.DeserializeContainer(serialized, type); + }); + + return new object[] { @default }; + } + } private static object[] GenericJson {