diff --git a/src/Hydrogen/Serialization/Factory/SerializerFactory.cs b/src/Hydrogen/Serialization/Factory/SerializerFactory.cs index 006f2abd..e2297db3 100644 --- a/src/Hydrogen/Serialization/Factory/SerializerFactory.cs +++ b/src/Hydrogen/Serialization/Factory/SerializerFactory.cs @@ -19,7 +19,8 @@ namespace Hydrogen; [SuppressMessage("ReSharper", "PossibleNullReferenceException")] public class SerializerFactory { - internal const int RegistrationCodeStart = 10; // 0 - 9 reserved for special types (i.e. 0 is used to indicate cyclic reference) + internal const int PermanentTypeCodeStartDefault = 10; + internal const int EphemeralTypeCodeStartDefault = 65536; // Type codes for ephemerally registered types start here. This allows you to update your core serializer registrations over time without affecting previous serialized objects. private IDictionary _registrations; private BijectiveDictionary _registrationsByType; @@ -53,6 +54,8 @@ public SerializerFactory(SerializerFactory baseFactory) : this() { public static SerializerFactory Default { get; } + private long MinimumGeneratedTypeCode { get; init; } = PermanentTypeCodeStartDefault; + public static void RegisterDefaults(SerializerFactory factory) { // register self-assembling factory for object @@ -159,18 +162,15 @@ public void Register(Func factory) public void Register(Type serializerType) => RegisterInternal(GenerateTypeCode(), typeof(TItem), serializerType, null, null); - public void Register(Type dataType, Type serializerType) => RegisterInternal(GenerateTypeCode(), dataType, serializerType, null, null); - public void RegisterAutoBuild() => RegisterAutoBuild(typeof(T)); public void RegisterAutoBuild(Type dataType) - => GetSerializerInternal(dataType, true); - + => AssembleSerializer(this, dataType, true, MinimumGeneratedTypeCode); private void RegisterInternal(long typeCode, Type dataType, Type serializerType, IItemSerializer serializerInstance, Func factory) { Guard.ArgumentNotNull(dataType, nameof(dataType)); @@ -248,120 +248,16 @@ private void RegisterInternal(long typeCode, Type dataType, Type serializerType, #region GetSerializer public IItemSerializer GetSerializer() - => (IItemSerializer)GetSerializerInternal(typeof(T), false); - - public IItemSerializer GetSerializer(Type type) - => GetSerializerInternal(type, false); - - private IItemSerializer GetSerializerInternal(Type itemType, bool registerTypes) { - - // During the construction, a factory is required to store generated serializers. - var factory = registerTypes ? this : new SerializerFactory(this); + => GetSerializer(EphemeralTypeCodeStartDefault); - var assembledSerializer = AssembleRecursively(factory, itemType); - - return assembledSerializer; - - // TODO: support nested-types by intelligently tracking parent - IItemSerializer AssembleRecursively(SerializerFactory factory, Type itemType) { - - // Ensure serializers for component types are registered - // (i.e. resolving a List serializer requires a serializer for UnregisteredType) - foreach (var genericType in GetUnregisteredComponentTypes(itemType)) - AssembleRecursively(factory, genericType); - - // If serializer already exists for this type in factory, use that - if (factory.HasSerializer(itemType)) - return factory.GetCachedSerializer(itemType); - - // Special Case: if we're serializing an enum (or nullable enum), we register it with the factory now and return - if (itemType.IsEnum || itemType.IsConstructedGenericTypeOf(typeof(Nullable<>)) && itemType.GenericTypeArguments[0].IsEnum) { - factory.RegisterEnum(itemType.IsEnum ? itemType : itemType.GenericTypeArguments[0]); - return factory.GetCachedSerializer(itemType); - } + public IItemSerializer GetSerializer(long typeCodeStart) + => (IItemSerializer)AssembleSerializer(this, typeof(T), false, typeCodeStart); - // No serializer registered so we need to assemble one as a CompositeSerializer. First, we need to - // register the serializer (before it is assembled) as it may recursively refer to itself. So we - // activate a CompositeSerializer with no members (we'll configure it later) - var compositeSerializer = - (IItemSerializer) typeof(CompositeSerializer<>) - .MakeGenericType(itemType) - .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null) - .Invoke(null); - - var serializer = - itemType.IsValueType ? - compositeSerializer : - (IItemSerializer)typeof(ReferenceSerializer<>).MakeGenericType(itemType).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(IItemSerializer<>).MakeGenericType(itemType) }, null).Invoke(new object[] { compositeSerializer }); - - - // register serializer instance now as it may be re-used in component serializers (recursive types) - if (itemType != typeof(object)) - factory.RegisterInternal(factory.GenerateTypeCode(), itemType, compositeSerializer.GetType(), compositeSerializer, null); - - // Create the member serializers - var members = SerializerBuilder.GetSerializableMembers(itemType); - var memberBindings = new List(members.Length); - foreach (var member in members) { - var propertyType = member.PropertyType; - - // Ensure we have a serializer for the member type - if (propertyType != typeof(object) && !factory.HasSerializer(propertyType)) - AssembleRecursively(factory, propertyType); - - // We don't use the member type serializer but instead use a FactorySerializer to ensure cyclic/polymorphic references are handled correctly - var memberSerializer = (IItemSerializer) typeof(FactorySerializer<>).MakeGenericType(propertyType).ActivateWithCompatibleArgs(factory); - memberBindings.Add(new(member, memberSerializer.AsReferenceSerializer())); - } - - // Configure the composite serializer instance (which is already registered) - var itemTypeLocal = itemType; - compositeSerializer - .GetType() - .GetMethod(nameof(CompositeSerializer.Configure), BindingFlags.Instance | BindingFlags.NonPublic) - .Invoke(compositeSerializer, new object[] { () => itemTypeLocal.ActivateWithCompatibleArgs(), memberBindings.ToArray() }); - - return serializer; - } - - IEnumerable GetUnregisteredComponentTypes(Type type, HashSet alreadyVisited = null) { - alreadyVisited ??= new HashSet(); - - // List - // Type[] - // Type1 - - // Avoid recursive loops - if (alreadyVisited.Contains(type)) - yield break; - alreadyVisited.Add(type); - - // Case 1: There is an explicit serializer for this type, no component types need to be assembled - if (factory.HasSerializer(type)) - yield break; - - - // Case 2: Array element type may need assembling - if (type.IsArray) { - var elementType = type.GetElementType(); - if (!factory.HasSerializer(elementType)) { - foreach(var elementTypeUnregisteredComponentTypes in GetUnregisteredComponentTypes(elementType, alreadyVisited)) - yield return elementTypeUnregisteredComponentTypes; - yield return elementType; - } - } + public IItemSerializer GetSerializer(Type type) + => GetSerializer(type, EphemeralTypeCodeStartDefault); - // Case 4: Serializer for generic type definition exists but not for generic type arguments - // e.g. List, Dictionary, etc - if (type.IsConstructedGenericType && factory.HasSerializer(type.GetGenericTypeDefinition())) { - foreach (var genericArgumentType in type.GetGenericArguments().Where(x => !factory.HasSerializer(x))) { - foreach(var subType in GetUnregisteredComponentTypes(genericArgumentType, alreadyVisited)) - yield return subType; - yield return genericArgumentType; - } - } - } - } + public IItemSerializer GetSerializer(Type type, long typeCodeStart) + => AssembleSerializer(this, type, false, typeCodeStart); #endregion @@ -410,6 +306,7 @@ public long CountSubSerializers(long typeCode) { #endregion + #region Aux [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -461,7 +358,7 @@ private Registration GetRegistration(long typeCode) { return registration; } - private long GenerateTypeCode() => _registrations.Count > 0 ? _registrations.Keys.Max() + 1 : RegistrationCodeStart; + private long GenerateTypeCode() => Tools.Values.Max( _registrations.Count > 0 ? _registrations.Keys.Max() + 1 : MinimumGeneratedTypeCode, MinimumGeneratedTypeCode); private static IItemSerializer CreateSerializerInstance(Registration registration, Type requestedDataType, Type registeredDataType, Type registeredSerializerType) { Guard.Argument(!requestedDataType.IsGenericTypeDefinition, nameof(requestedDataType), $"Requested data type {requestedDataType.Name} cannot be a generic type definition"); @@ -526,6 +423,117 @@ private IItemSerializer FromSerializerHierarchyInternal(RecursiveDataType return registration.Factory(registration, type); } + private static IItemSerializer AssembleSerializer(SerializerFactory serializerFactory, Type itemType, bool retainRegisteredTypesInFactory, long typeCodeStart) { + + // During the construction, a factory is required to store generated serializers. + var factoryToUse = retainRegisteredTypesInFactory ? serializerFactory : new SerializerFactory(serializerFactory) { MinimumGeneratedTypeCode = typeCodeStart }; + + var assembledSerializer = AssembleRecursively(factoryToUse, itemType); + + return assembledSerializer; + + // TODO: support nested-types by intelligently tracking parent + IItemSerializer AssembleRecursively(SerializerFactory factory, Type itemType) { + + // Ensure serializers for component types are registered + // (i.e. resolving serializer for List serializer requires a serializer for UnregisteredType) + foreach (var genericType in GetUnregisteredComponentTypes(factory, itemType)) + AssembleRecursively(factory, genericType); + + // If serializer already exists for this type in factory, use that + if (factory.HasSerializer(itemType)) + return factory.GetCachedSerializer(itemType); + + // Special Case: if we're serializing an enum (or nullable enum), we register it with the factory now and return + if (itemType.IsEnum || itemType.IsConstructedGenericTypeOf(typeof(Nullable<>)) && itemType.GenericTypeArguments[0].IsEnum) { + factory.RegisterEnum(itemType.IsEnum ? itemType : itemType.GenericTypeArguments[0]); + return factory.GetCachedSerializer(itemType); + } + + // No serializer registered so we need to assemble one as a CompositeSerializer. First, we need to + // register the serializer (before it is assembled) as it may recursively refer to itself. So we + // activate a CompositeSerializer with no members (we'll configure it later) + var compositeSerializer = + (IItemSerializer) typeof(CompositeSerializer<>) + .MakeGenericType(itemType) + .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null) + .Invoke(null); + + var serializer = + itemType.IsValueType ? + compositeSerializer : + (IItemSerializer)typeof(ReferenceSerializer<>).MakeGenericType(itemType).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(IItemSerializer<>).MakeGenericType(itemType) }, null).Invoke(new object[] { compositeSerializer }); + + + // register serializer instance now as it may be re-used in component serializers (recursive types) + if (itemType != typeof(object)) + factory.RegisterInternal(factory.GenerateTypeCode(), itemType, compositeSerializer.GetType(), compositeSerializer, null); + + // Create the member serializers + var members = SerializerBuilder.GetSerializableMembers(itemType); + var memberBindings = new List(members.Length); + foreach (var member in members) { + var propertyType = member.PropertyType; + + // Ensure we have a serializer for the member type + if (propertyType != typeof(object) && !factory.HasSerializer(propertyType)) + AssembleRecursively(factory, propertyType); + + // We don't use the member type serializer but instead use a FactorySerializer to ensure cyclic/polymorphic references are handled correctly + var memberSerializer = (IItemSerializer) typeof(FactorySerializer<>).MakeGenericType(propertyType).ActivateWithCompatibleArgs(factory); + memberBindings.Add(new(member, memberSerializer.AsReferenceSerializer())); + } + + // Configure the composite serializer instance (which is already registered) + var itemTypeLocal = itemType; + compositeSerializer + .GetType() + .GetMethod(nameof(CompositeSerializer.Configure), BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(compositeSerializer, new object[] { () => itemTypeLocal.ActivateWithCompatibleArgs(), memberBindings.ToArray() }); + + return serializer; + } + + IEnumerable GetUnregisteredComponentTypes(SerializerFactory factory, Type type, HashSet alreadyVisited = null) { + alreadyVisited ??= new HashSet(); + + // List + // Type[] + // Type1 + + // Avoid recursive loops + if (alreadyVisited.Contains(type)) + yield break; + alreadyVisited.Add(type); + + // Case 1: There is an explicit serializer for this type, no component types need to be assembled + if (factory.HasSerializer(type)) + yield break; + + + // Case 2: Array element type may need assembling + if (type.IsArray) { + var elementType = type.GetElementType(); + if (!factory.HasSerializer(elementType)) { + foreach(var elementTypeUnregisteredComponentTypes in GetUnregisteredComponentTypes(factory, elementType, alreadyVisited)) + yield return elementTypeUnregisteredComponentTypes; + yield return elementType; + } + } + + // Case 4: Serializer for generic type definition exists but not for generic type arguments + // e.g. List, Dictionary, etc + if (type.IsConstructedGenericType && factory.HasSerializer(type.GetGenericTypeDefinition())) { + foreach (var genericArgumentType in type.GetGenericArguments().Where(x => !factory.HasSerializer(x))) { + foreach(var subType in GetUnregisteredComponentTypes(factory, genericArgumentType, alreadyVisited)) + yield return subType; + yield return genericArgumentType; + } + } + } + } + + #endregion public class Registration { diff --git a/tests/Hydrogen.Tests/Serialization/SerializerFactoryTests.cs b/tests/Hydrogen.Tests/Serialization/SerializerFactoryTests.cs index 62c2518b..fc875ce7 100644 --- a/tests/Hydrogen.Tests/Serialization/SerializerFactoryTests.cs +++ b/tests/Hydrogen.Tests/Serialization/SerializerFactoryTests.cs @@ -77,7 +77,7 @@ public void GetSerializerHierarchy_Open() { var factory = new SerializerFactory(); factory.Register(typeof(IList<>), typeof(ListInterfaceSerializer<>)); // 0 factory.Register(PrimitiveSerializer.Instance); // 1 - CollectionAssert.AreEqual(factory.GetSerializerHierarchy(typeof(IList)).Flatten(), new[] { SerializerFactory.RegistrationCodeStart + 0, SerializerFactory.RegistrationCodeStart + 1 }); + CollectionAssert.AreEqual(factory.GetSerializerHierarchy(typeof(IList)).Flatten(), new[] { SerializerFactory.PermanentTypeCodeStartDefault + 0, SerializerFactory.PermanentTypeCodeStartDefault + 1 }); } [Test] @@ -95,7 +95,7 @@ public void GetSerializerHierarchy_Open_Complex() { // float, 1 // IList< 2 // int>>>> 0 - CollectionAssert.AreEqual(factory.GetSerializerHierarchy(typeof(IList, KeyValuePair>>>)).Flatten(), new[] { 2, 3, 2, 0, 3, 1, 2, 0 }.Select(x => SerializerFactory.RegistrationCodeStart + x)); + CollectionAssert.AreEqual(factory.GetSerializerHierarchy(typeof(IList, KeyValuePair>>>)).Flatten(), new[] { 2, 3, 2, 0, 3, 1, 2, 0 }.Select(x => SerializerFactory.PermanentTypeCodeStartDefault + x)); } @@ -118,7 +118,7 @@ public void GetSerializerHierarchy_Closed_Complex() { ); factory.Register(instance); // 4 (closed specific instance) - CollectionAssert.AreEqual(factory.GetSerializerHierarchy(typeof(IList, KeyValuePair>>>)).Flatten(), new[] { SerializerFactory.RegistrationCodeStart + 4 }); + CollectionAssert.AreEqual(factory.GetSerializerHierarchy(typeof(IList, KeyValuePair>>>)).Flatten(), new[] { SerializerFactory.PermanentTypeCodeStartDefault + 4 }); } @@ -171,7 +171,7 @@ public void FromSerializerHierarchy_Closed_Complex() { ); factory.Register(instance); // 4 (closed specific instance) - CollectionAssert.AreEqual(factory.GetSerializerHierarchy(typeof(IList, KeyValuePair>>>)).Flatten(), new[] { SerializerFactory.RegistrationCodeStart + 4 }); + CollectionAssert.AreEqual(factory.GetSerializerHierarchy(typeof(IList, KeyValuePair>>>)).Flatten(), new[] { SerializerFactory.PermanentTypeCodeStartDefault + 4 }); } @@ -208,7 +208,7 @@ public void Array() { factory.Register(typeof(System.Array), typeof(ArraySerializer<>)); factory.Register(PrimitiveSerializer.Instance); var hierarchy = factory.GetSerializerHierarchy(typeof(int[])); - Assert.That(hierarchy.Flatten(), Is.EqualTo(new[] { SerializerFactory.RegistrationCodeStart + 0, SerializerFactory.RegistrationCodeStart +1 })); + Assert.That(hierarchy.Flatten(), Is.EqualTo(new[] { SerializerFactory.PermanentTypeCodeStartDefault + 0, SerializerFactory.PermanentTypeCodeStartDefault +1 })); var serializer = factory.FromSerializerHierarchy(hierarchy).AsDereferencedSerializer(); Assert.That(serializer, Is.TypeOf>()); } @@ -228,7 +228,7 @@ public void ResolveNotSpecializedByteArray() { factory.Register(PrimitiveSerializer.Instance); // 1 factory.Register(ByteArraySerializer.Instance); // 2 (special for byte[]) var hierarchy = factory.GetSerializerHierarchy(typeof(int[])); - Assert.That(hierarchy.Flatten(), Is.EqualTo(new[] { SerializerFactory.RegistrationCodeStart + 0, SerializerFactory.RegistrationCodeStart + 1 })); + Assert.That(hierarchy.Flatten(), Is.EqualTo(new[] { SerializerFactory.PermanentTypeCodeStartDefault + 0, SerializerFactory.PermanentTypeCodeStartDefault + 1 })); var serializer = factory.FromSerializerHierarchy(hierarchy).AsDereferencedSerializer(); Assert.That(serializer, Is.TypeOf>()); } @@ -240,7 +240,7 @@ public void ResolveSpecializedByteArray() { factory.Register(PrimitiveSerializer.Instance); // 1 factory.Register(ByteArraySerializer.Instance); // 2 (special for byte[]) var hierarchy = factory.GetSerializerHierarchy(typeof(byte[])); - Assert.That(hierarchy.Flatten(), Is.EqualTo(new[] { SerializerFactory.RegistrationCodeStart + 2 })); + Assert.That(hierarchy.Flatten(), Is.EqualTo(new[] { SerializerFactory.PermanentTypeCodeStartDefault + 2 })); var serializer = factory.FromSerializerHierarchy(hierarchy).AsDereferencedSerializer(); Assert.That(serializer, Is.TypeOf()); } diff --git a/tests/Hydrogen.Tests/Serialization/SerializerSerializerTests.cs b/tests/Hydrogen.Tests/Serialization/SerializerSerializerTests.cs index ebe9595b..22d0d7a6 100644 --- a/tests/Hydrogen.Tests/Serialization/SerializerSerializerTests.cs +++ b/tests/Hydrogen.Tests/Serialization/SerializerSerializerTests.cs @@ -109,7 +109,7 @@ public void OpenComplexAndSimilarClosedSpecificSerializer() { factory.GetRegisteredSerializer, KeyValuePair>>>>() ); Assert.That(secondSerializerBytes.Length, Is.EqualTo(1)); // there was 1 serializer referenced in the first serializer - Assert.That( CVarInt.Read(secondSerializerBytes), Is.EqualTo(SerializerFactory.RegistrationCodeStart + 4)); // the serializer was the 4th serializer registered in the factory + Assert.That( CVarInt.Read(secondSerializerBytes), Is.EqualTo(SerializerFactory.PermanentTypeCodeStartDefault + 4)); // the serializer was the 4th serializer registered in the factory var secondSerializer = serializerSerializer.DeserializeBytesLE( secondSerializerBytes) as IItemSerializer, KeyValuePair>>>>; Assert.That(secondSerializer, Is.Not.Null); }