From c3a2521f995a63085a855ee086909dbae5a71ed9 Mon Sep 17 00:00:00 2001 From: rikimaru0345 Date: Wed, 27 Feb 2019 18:57:04 +0100 Subject: [PATCH] new: allow providing custom assemblies to SimpleTypeBinder; move some tests over to the test project; also resolve SchemaFormatter in DynamicObjectFormatterResolver --- samples/LiveTesting/Program.cs | 491 +----------------- src/Ceras/CerasSerializer.cs | 30 +- src/Ceras/Config/SerializerConfig.cs | 5 +- src/Ceras/Formatters/TypeFormatter.cs | 2 +- src/Ceras/Helpers/NaiveTypeBinder.cs | 90 ---- src/Ceras/Helpers/SimpleTypeBinder.cs | 99 ++++ .../DynamicObjectFormatterResolver.cs | 26 +- tests/Ceras.Test/Tests/Arrays.cs | 75 --- .../Tests/{Basics.cs => BuiltInTypes.cs} | 100 +++- .../Tests/ConstructionAndPooling.cs | 308 +++++++++++ tests/Ceras.Test/Tests/Internals.cs | 35 +- tests/Ceras.Test/Tests/Misc.cs | 53 ++ tests/Ceras.Test/Tests/TypeConfig.cs | 34 ++ 13 files changed, 647 insertions(+), 701 deletions(-) delete mode 100644 src/Ceras/Helpers/NaiveTypeBinder.cs create mode 100644 src/Ceras/Helpers/SimpleTypeBinder.cs delete mode 100644 tests/Ceras.Test/Tests/Arrays.cs rename tests/Ceras.Test/Tests/{Basics.cs => BuiltInTypes.cs} (76%) create mode 100644 tests/Ceras.Test/Tests/ConstructionAndPooling.cs create mode 100644 tests/Ceras.Test/Tests/Misc.cs diff --git a/samples/LiveTesting/Program.cs b/samples/LiveTesting/Program.cs index 6886c8d..6e25f43 100644 --- a/samples/LiveTesting/Program.cs +++ b/samples/LiveTesting/Program.cs @@ -25,7 +25,7 @@ class Program static void Main(string[] args) { - new Basics().BasicUsage(); + new BuiltInTypes().BasicUsage(); Benchmarks(); @@ -33,20 +33,12 @@ static void Main(string[] args) ExpressionTreesTest(); - DictInObjArrayTest(); - - TestDirectPoolingMethods(); - - DelegatesTest(); - TuplesTest(); EnsureSealedTypesThrowsException(); InjectSpecificFormatterTest(); - - BigIntegerTest(); - + VersionToleranceTest(); ReadonlyTest(); @@ -72,9 +64,7 @@ static void Main(string[] args) NullableTest(); ErrorOnDirectEnumerable(); - - CtorTest(); - + PropertyTest(); NetworkTest(); @@ -331,209 +321,6 @@ static void ExpressionTreesTest() } - class Person - { - public const string CtorSuffix = " (modified by constructor)"; - - public string Name; - public int Health; - public Person BestFriend; - - public Person() - { - } - - public Person(string name) - { - Name = name + CtorSuffix; - } - - public int GetHealth() => Health; - - public string SayHello() => $"Hello I'm {Name}"; - } - - static void TestDirectPoolingMethods() - { - var pool = new InstancePoolTest(); - - // Test: Ctor with argument - { - SerializerConfig config = new SerializerConfig(); - - config.ConfigType() - // Select ctor, not delegate - .ConstructBy(() => new Person("name")); - - var clone = DoRoundTripTest(config); - Debug.Assert(clone != null); - Debug.Assert(clone.Name.StartsWith("riki")); - Debug.Assert(clone.Name.EndsWith(Person.CtorSuffix)); - } - - // Test: Manual config - { - SerializerConfig config = new SerializerConfig(); - - config.ConfigType() - .ConstructBy(TypeConstruction.ByStaticMethod(() => StaticPoolTest.CreatePerson())); - - var clone = DoRoundTripTest(config); - Debug.Assert(clone != null); - } - - // Test: Normal ctor, but explicitly - { - SerializerConfig config = new SerializerConfig(); - - config.ConfigType() - // Select ctor, not delegate - .ConstructBy(() => new Person()); - - var clone = DoRoundTripTest(config); - Debug.Assert(clone != null); - } - - // Test: Construct from instance-pool - { - SerializerConfig config = new SerializerConfig(); - - config.ConfigType() - // Instance + method select - .ConstructBy(pool, () => pool.CreatePerson()); - - var clone = DoRoundTripTest(config); - Debug.Assert(clone != null); - Debug.Assert(pool.IsFromPool(clone)); - } - - // Test: Construct from static-pool - { - SerializerConfig config = new SerializerConfig(); - - config.ConfigType() - // method select - .ConstructBy(() => StaticPoolTest.CreatePerson()); - - var clone = DoRoundTripTest(config); - Debug.Assert(clone != null); - Debug.Assert(StaticPoolTest.IsFromPool(clone)); - } - - // Test: Construct from any delegate (in this example: a lambda expression) - { - SerializerConfig config = new SerializerConfig(); - - Person referenceCapturedByLambda = null; - - config.ConfigType() - // Use delegate - .ConstructByDelegate(() => - { - var obj = new Person(); - referenceCapturedByLambda = obj; - return obj; - }); - - var clone = DoRoundTripTest(config); - Debug.Assert(clone != null); - Debug.Assert(ReferenceEquals(clone, referenceCapturedByLambda)); - } - - // Test: Construct from instance-pool, with parameter - { - SerializerConfig config = new SerializerConfig(); - - config.ConfigType() - // Use instance + method selection - .ConstructBy(pool, () => pool.CreatePersonWithName("abc")); - - var clone = DoRoundTripTest(config); - Debug.Assert(clone != null); - Debug.Assert(clone.Name.StartsWith("riki")); - Debug.Assert(pool.IsFromPool(clone)); - } - - // Test: Construct from static-pool, with parameter - { - SerializerConfig config = new SerializerConfig(); - - config.ConfigType() - // Use instance + method selection - .ConstructBy(() => StaticPoolTest.CreatePersonWithName("abc")); - - var clone = DoRoundTripTest(config); - Debug.Assert(clone != null); - Debug.Assert(clone.Name.StartsWith("riki")); - Debug.Assert(StaticPoolTest.IsFromPool(clone)); - } - } - - static Person DoRoundTripTest(SerializerConfig config, string name = "riki") - { - var ceras = new CerasSerializer(config); - - var p = new Person(); - p.Name = name; - - var data = ceras.Serialize(p); - - var clone = ceras.Deserialize(data); - return clone; - } - - static class StaticPoolTest - { - static HashSet _objectsCreatedByPool = new HashSet(); - - public static Person CreatePerson() - { - var p = new Person(); - _objectsCreatedByPool.Add(p); - return p; - } - - public static Person CreatePersonWithName(string name) - { - var p = new Person(); - p.Name = name; - _objectsCreatedByPool.Add(p); - return p; - } - - public static bool IsFromPool(Person p) => _objectsCreatedByPool.Contains(p); - - public static void DiscardPooledObjectTest(Person p) - { - } - } - - class InstancePoolTest - { - HashSet _objectsCreatedByPool = new HashSet(); - - public Person CreatePerson() - { - var p = new Person(); - _objectsCreatedByPool.Add(p); - return p; - } - - public Person CreatePersonWithName(string name) - { - var p = new Person(); - p.Name = name; - _objectsCreatedByPool.Add(p); - return p; - } - - public bool IsFromPool(Person p) => _objectsCreatedByPool.Contains(p); - - public void DiscardPooledObjectTest(Person p) - { - } - } - static void Benchmarks() { @@ -665,20 +452,6 @@ class DependencyInjectionTestFormatter : IFormatter public void Deserialize(byte[] buffer, ref int offset, ref Person value) => throw new NotImplementedException(); } - static void BigIntegerTest() - { - BigInteger big = new BigInteger(28364526879); - big = BigInteger.Pow(big, 6); - - CerasSerializer ceras = new CerasSerializer(); - - var data = ceras.Serialize(big); - - var clone = ceras.Deserialize(data); - - Debug.Assert(clone.ToString() == big.ToString()); - } - static void ReadonlyTest() { // Test #1: @@ -905,168 +678,8 @@ static void MemberInfoAndTypeInfoTest() } - static int Add1(int x) => x + 1; - static int Add2(int x) => x + 2; - - static void DelegatesTest() - { - var config = new SerializerConfig(); - config.Advanced.DelegateSerialization = DelegateSerializationFlags.AllowStatic; - var ceras = new CerasSerializer(config); - - // 1. Simple test: can ceras persist a static-delegate - { - Func staticFunc = Add1; - var data = ceras.Serialize(staticFunc); - var staticFuncClone = ceras.Deserialize>(data); - - Debug.Assert(staticFuncClone != null); - Debug.Assert(object.Equals(staticFunc, staticFuncClone) == true); // must be considered the same - Debug.Assert(object.ReferenceEquals(staticFunc, staticFuncClone) == false); // must be a new instance - - - Debug.Assert(staticFuncClone(5) == staticFunc(5)); - } - - // 2. What about a collection of them - { - var rng = new Random(); - List> funcs = new List>(); - - for (int i = 0; i < rng.Next(15, 20); i++) - { - Func f; - - if (rng.Next(100) < 50) - f = Add1; - else - f = Add2; - - funcs.Add(f); - } - - var data = ceras.Serialize(funcs); - var cloneList = ceras.Deserialize>>(data); - - // Check by checking if the result is the same - Debug.Assert(funcs.Count == cloneList.Count); - for (int i = 0; i < funcs.Count; i++) - { - var n = rng.Next(); - Debug.Assert(funcs[i](n) == cloneList[i](n)); - } - } - - // 3. If we switch to "allow instance", it should persist instance-delegates, but no lambdas - { - config.Advanced.DelegateSerialization = DelegateSerializationFlags.AllowInstance; - ceras = new CerasSerializer(config); - - - // - // A) Direct Instance - // - var method = GetMethod(() => new Person().GetHealth()); - var p = new Person("direct instance") { Health = 3456 }; - var del = (Func)Delegate.CreateDelegate(typeof(Func), p, method); - - // Does our delegate even work? - var testResult = del(); - Debug.Assert(testResult == p.Health); - - // Can we serialize the normal instance delegate? - var data = ceras.Serialize(del); - var clone = ceras.Deserialize>(data); - - // Does it still work? - Debug.Assert(testResult == clone()); - - - - - } - - - return; - /* - Func myFunc = Add1; - - int localCapturedInt = 6; - - myFunc = x => - { - Console.WriteLine("Original delegate got called!"); - return localCapturedInt + 7; - }; - - myFunc = (Func)Delegate.Combine(myFunc, myFunc); - myFunc = (Func)Delegate.Combine(myFunc, myFunc); - myFunc = (Func)Delegate.Combine(myFunc, myFunc); - - var targets = myFunc.GetInvocationList(); - - - var result = myFunc(1); // writes the console message above 8 times, *facepalm* - - Debug.Assert(myFunc(5) == 6); - - */ - - - // Expected: no type-name appears multiple times, and deserialization works correctly. - - - //var multipleTypesHolderData = ceras.Serialize(multipleTypesHolder); - //multipleTypesHolderData.VisualizePrint("TypeTestClass"); - //var multipleTypesHolderClone = ceras.Deserialize(multipleTypesHolderData); - - - /* - - var memberInfo = new MemberInfoHolder(); - memberInfo.Field = typeof(MemberInfoHolder).GetFields()[0]; - memberInfo.Property = typeof(MemberInfoHolder).GetProperty("property", BindingFlags.NonPublic | BindingFlags.Instance); - memberInfo.Method = typeof(MemberInfoHolder).GetMethod("method", BindingFlags.NonPublic | BindingFlags.Instance); - - var memberInfoClone = ceras.Deserialize(ceras.Serialize(memberInfo)); - - var valueHolder = new DelegateValueHolder(); - - valueHolder.A = 1; - valueHolder.B = 0; - - Action action = () => - { - valueHolder.B += valueHolder.A; - }; - - HiddenFieldsTestClass test = new HiddenFieldsTestClass(); - test.SimpleActionEvent += () => { }; - - var testType = typeof(HiddenFieldsTestClass); - - - var clonedAction = ceras.Deserialize(ceras.Serialize(action)); - - clonedAction(); - - Func get2 = () => 2; - var t = get2.GetType(); - - var get2Clone = ceras.Deserialize>(ceras.Serialize(get2)); - - - Debug.Assert(get2() == 2); - Debug.Assert(get2Clone() == get2()); - */ - } - - class DelegateTestClass - { - public event Action OnSomeNumberHappened; - } class TypeTestClass { @@ -1081,28 +694,7 @@ class TypeTestClass public MethodInfo Method; } - class MemberInfoHolder - { - public FieldInfo Field; - public PropertyInfo Property; - public MethodInfo Method; - - string property { get; set; } - void method() { } - } - - class DelegateValueHolder - { - public int A; - public int B; - } - - class HiddenFieldsTestClass - { - public event Action SimpleActionEvent; - public event Action SimpleEventWithArg; - } - + static void SimpleDictionaryTest() { var dict = new Dictionary @@ -1121,44 +713,6 @@ static void SimpleDictionaryTest() Debug.Assert(n1 == n2); } - static void DictInObjArrayTest() - { - var dict = new Dictionary - { - ["test"] = new Dictionary - { - ["test"] = new object[] - { - new Dictionary - { - ["test"] = 3 - } - } - } - }; - - - var s = new CerasSerializer(); - - var data = s.Serialize(dict); - - var cloneDict = s.Deserialize>(data); - - var inner1 = cloneDict["test"] as Dictionary; - Debug.Assert(inner1 != null); - - var objArray = inner1["test"] as object[]; - Debug.Assert(objArray != null); - - var dictElement = objArray[0] as Dictionary; - Debug.Assert(dictElement != null); - - var three = dictElement["test"]; - - Debug.Assert(three.GetType() == typeof(int)); - Debug.Assert(3.Equals(three)); - } - static void MaintainTypeTest() { CerasSerializer ceras = new CerasSerializer(); @@ -1536,23 +1090,7 @@ static void ErrorOnDirectEnumerable() } } - static void CtorTest() - { - var obj = new ConstructorTest(5); - var ceras = new CerasSerializer(); - - try - { - // Expected to throw: no default ctor - var data = ceras.Serialize(obj); - var clone = ceras.Deserialize(data); - - Debug.Assert(false, "objects with no ctor and no TypeConfig should not serialize"); - } - catch (Exception e) - { - } - } + static void PropertyTest() { @@ -1749,12 +1287,14 @@ class DebugVersionTypeBinder : ITypeBinder { typeof(VersionTest2), "*" } }; + SimpleTypeBinder _simpleTypeBinder = new SimpleTypeBinder(); + public string GetBaseName(Type type) { if (_commonNames.TryGetValue(type, out string v)) return v; - return SimpleTypeBinderHelper.GetBaseName(type); + return _simpleTypeBinder.GetBaseName(type); } public Type GetTypeFromBase(string baseTypeName) @@ -1764,10 +1304,10 @@ public Type GetTypeFromBase(string baseTypeName) if (_commonNames.ContainsValue(baseTypeName)) return typeof(VersionTest2); - return SimpleTypeBinderHelper.GetTypeFromBase(baseTypeName); + return _simpleTypeBinder.GetTypeFromBase(baseTypeName); } - public Type GetTypeFromBaseAndAgruments(string baseTypeName, params Type[] genericTypeArguments) + public Type GetTypeFromBaseAndArguments(string baseTypeName, params Type[] genericTypeArguments) { throw new NotSupportedException("this binder is only for debugging"); // return SimpleTypeBinderHelper.GetTypeFromBaseAndAgruments(baseTypeName, genericTypeArguments); @@ -1795,16 +1335,7 @@ class VersionTest2 public int D = 53; } - class ConstructorTest - { - public int x; - - public ConstructorTest(int x) - { - this.x = x; - } - } - + public enum LongEnum : long { a = 1, diff --git a/src/Ceras/CerasSerializer.cs b/src/Ceras/CerasSerializer.cs index 84e1642..4a2239f 100644 --- a/src/Ceras/CerasSerializer.cs +++ b/src/Ceras/CerasSerializer.cs @@ -77,11 +77,11 @@ internal static bool IsPrimitiveType(Type type) return false; } - static HashSet _frameworkAssemblies = new HashSet + internal static HashSet _frameworkAssemblies = new HashSet { - typeof(object).Assembly, // mscorelib - typeof(Uri).Assembly, // System.dll - typeof(Enumerable).Assembly, // System.Core.dll + typeof(object).Assembly, // mscorelib + typeof(Uri).Assembly, // System.dll + typeof(Enumerable).Assembly, // System.Core.dll }; @@ -181,7 +181,7 @@ public CerasSerializer(SerializerConfig config = null) if (Config.Advanced.AotMode != AotMode.None && Config.VersionTolerance.Mode != VersionToleranceMode.Disabled) throw new NotSupportedException("You can not use 'AotMode.Enabled' and version tolerance at the same time for now. If you would like this feature implemented, please open an issue on GitHub explaining your use-case, or join the Discord server."); - TypeBinder = Config.Advanced.TypeBinder ?? new NaiveTypeBinder(); + TypeBinder = Config.Advanced.TypeBinder; DiscardObjectMethod = Config.Advanced.DiscardObjectMethod; _userResolvers = Config.OnResolveFormatter.ToArray(); @@ -618,23 +618,7 @@ IFormatter GetSpecificFormatter(Type type, TypeMetaData meta) } } - - // 4.) Depending on the VersionTolerance we use different formatters - if (Config.VersionTolerance.Mode != VersionToleranceMode.Disabled) - { - if (!meta.IsFrameworkType && !meta.IsPrimitive && !meta.Type.IsArray) - { - // Create SchemaFormatter, it will automatically adjust itself to the schema when it's read - var formatterType = typeof(SchemaDynamicFormatter<>).MakeGenericType(type); - var schemaFormatter = (IFormatter)Activator.CreateInstance(formatterType, args: new object[] { this, meta.PrimarySchema }); - - meta.SpecificFormatter = schemaFormatter; - return schemaFormatter; - } - } - - - // 5.) Built-in + // 4.) Built-in for (int i = 0; i < _resolvers.Count; i++) { var formatter = _resolvers[i].GetFormatter(type); @@ -646,7 +630,7 @@ IFormatter GetSpecificFormatter(Type type, TypeMetaData meta) } } - // 6.) Dynamic + // 5.) Dynamic (optionally using Schema) { var formatter = _dynamicResolver.GetFormatter(type); if (formatter != null) diff --git a/src/Ceras/Config/SerializerConfig.cs b/src/Ceras/Config/SerializerConfig.cs index fadee53..6572528 100644 --- a/src/Ceras/Config/SerializerConfig.cs +++ b/src/Ceras/Config/SerializerConfig.cs @@ -208,7 +208,7 @@ public Action OnConfigNewType bool IAdvancedConfigOptions.PersistTypeCache { get; set; } = false; bool IAdvancedConfigOptions.SealTypesWhenUsingKnownTypes { get; set; } = true; bool IAdvancedConfigOptions.SkipCompilerGeneratedFields { get; set; } = true; - ITypeBinder IAdvancedConfigOptions.TypeBinder { get; set; } = null; + ITypeBinder IAdvancedConfigOptions.TypeBinder { get; set; } = new SimpleTypeBinder(); DelegateSerializationFlags IAdvancedConfigOptions.DelegateSerialization { get; set; } = DelegateSerializationFlags.Off; bool IAdvancedConfigOptions.UseReinterpretFormatter { get; set; } = true; bool IAdvancedConfigOptions.RespectNonSerializedAttribute { get; set; } = true; @@ -291,8 +291,9 @@ public interface IAdvancedConfigOptions /// Examples: /// - Mapping server objects to client objects /// - Shortening / abbreviating type-names to save space and performance - /// The default type binder (NaiveTypeBinder) simply uses '.FullName' /// See the readme on github for more information. + /// + /// Default: new SimpleTypeBinder() /// ITypeBinder TypeBinder { get; set; } diff --git a/src/Ceras/Formatters/TypeFormatter.cs b/src/Ceras/Formatters/TypeFormatter.cs index eb431e6..1841dc4 100644 --- a/src/Ceras/Formatters/TypeFormatter.cs +++ b/src/Ceras/Formatters/TypeFormatter.cs @@ -150,7 +150,7 @@ public void Deserialize(byte[] buffer, ref int offset, ref Type type) // Read construct full composite (example: Dictionary) var compositeProxy = typeCache.CreateDeserializationProxy(); - type = _typeBinder.GetTypeFromBaseAndAgruments(baseType.FullName, genericArgs); + type = _typeBinder.GetTypeFromBaseAndArguments(baseType.FullName, genericArgs); compositeProxy.Type = type; // make it available for future deserializations if (_isSealed) diff --git a/src/Ceras/Helpers/NaiveTypeBinder.cs b/src/Ceras/Helpers/NaiveTypeBinder.cs deleted file mode 100644 index b81033d..0000000 --- a/src/Ceras/Helpers/NaiveTypeBinder.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Ceras -{ - using System; - using System.Collections.Generic; - using System.Reflection; - - public interface ITypeBinder - { - string GetBaseName(Type type); - Type GetTypeFromBase(string baseTypeName); - Type GetTypeFromBaseAndAgruments(string baseTypeName, params Type[] genericTypeArguments); - } - - public class NaiveTypeBinder : ITypeBinder - { - public string GetBaseName(Type type) - { - return SimpleTypeBinderHelper.GetBaseName(type); - } - - public Type GetTypeFromBase(string baseTypeName) - { - return SimpleTypeBinderHelper.GetTypeFromBase(baseTypeName); - } - - public Type GetTypeFromBaseAndAgruments(string baseTypeName, params Type[] genericTypeArguments) - { - return SimpleTypeBinderHelper.GetTypeFromBaseAndArguments(baseTypeName, genericTypeArguments); - } - } - - - public static class SimpleTypeBinderHelper - { - static readonly HashSet _typeAssemblies = new HashSet(); - - static SimpleTypeBinderHelper() - { - _typeAssemblies.Add(typeof(int).Assembly); - _typeAssemblies.Add(typeof(List<>).Assembly); - _typeAssemblies.Add(Assembly.GetCallingAssembly()); - _typeAssemblies.Add(Assembly.GetEntryAssembly()); - - _typeAssemblies.RemoveWhere(a => a == null); - } - - // given List it would return "System.Collections.List" - public static string GetBaseName(Type type) - { - if (type.IsGenericType) - return type.GetGenericTypeDefinition().FullName; - - return type.FullName; - } - - public static Type GetTypeFromBase(string baseTypeName) - { - // todo: let the user provide a way! - // todo: alternatively, search in ALL loaded assemblies... but that is slow as fuck - - foreach (var a in _typeAssemblies) - { - var t = a.GetType(baseTypeName); - if (t != null) - return t; - } - - // Oh no... did the user forget to add the right assembly?? - // Lets search in everything that's loaded... - foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) - { - var t = a.GetType(baseTypeName); - if (t != null) - { - _typeAssemblies.Add(a); - return t; - } - } - - throw new Exception("Cannot find type " + baseTypeName + " after searching in all user provided assemblies and all loaded assemblies. Is the type in some plugin-module that was not yet loaded? Or did the assembly that contains the type change (ie the type got removed)?"); - } - - public static Type GetTypeFromBaseAndArguments(string baseTypeName, params Type[] genericTypeArguments) - { - var baseType = GetTypeFromBase(baseTypeName); - return baseType.MakeGenericType(genericTypeArguments); - } - } - -} \ No newline at end of file diff --git a/src/Ceras/Helpers/SimpleTypeBinder.cs b/src/Ceras/Helpers/SimpleTypeBinder.cs new file mode 100644 index 0000000..21b80a6 --- /dev/null +++ b/src/Ceras/Helpers/SimpleTypeBinder.cs @@ -0,0 +1,99 @@ +namespace Ceras +{ + using System; + using System.Collections.Generic; + using System.Reflection; + + /// + /// A type binder is simple. It is responsible to converts a type to a string and back. + /// For generic types it must do so by deconstructing the type though. So giving would return "System.Collections.List". + /// + public interface ITypeBinder + { + string GetBaseName(Type type); + Type GetTypeFromBase(string baseTypeName); + Type GetTypeFromBaseAndArguments(string baseTypeName, params Type[] genericTypeArguments); + } + + /// + /// This simple type binder does two things: + /// - does the basic ITypeBinder thing (converting types to names, and back) + /// - allows the user to add assemblies that will be searched for types + /// + public class SimpleTypeBinder : ITypeBinder + { + readonly HashSet _searchAssemblies = new HashSet(); + + /// + /// Put your own assemblies in here for Ceras to discover them. If you don't and a type is not found, Ceras will have to look in all loaded assemblies (which is slow) + /// + public HashSet CustomSearchAssemblies { get; } = new HashSet(); + + public SimpleTypeBinder() + { + // Search in framework + foreach (var frameworkAsm in CerasSerializer._frameworkAssemblies) + _searchAssemblies.Add(frameworkAsm); + + // Search in user code + _searchAssemblies.Add(Assembly.GetEntryAssembly()); + + _searchAssemblies.RemoveWhere(a => a == null); + } + + + public string GetBaseName(Type type) + { + if (type.IsGenericType) + return type.GetGenericTypeDefinition().FullName; + + return type.FullName; + } + + public Type GetTypeFromBase(string baseTypeName) + { + foreach (var a in _searchAssemblies) + { + var t = a.GetType(baseTypeName); + if (t != null) + return t; + } + + foreach (var a in CustomSearchAssemblies) + { + if (_searchAssemblies.Contains(a)) + continue; // We've already searched there + + var t = a.GetType(baseTypeName); + if (t != null) + { + _searchAssemblies.Add(a); + return t; + } + } + + // Oh no... did the user forget to add the right assembly?? + // Lets search in everything that's loaded... + foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) + { + if (_searchAssemblies.Contains(a) || CustomSearchAssemblies.Contains(a)) + continue; // We've already searched there + + var t = a.GetType(baseTypeName); + if (t != null) + { + _searchAssemblies.Add(a); + return t; + } + } + + throw new Exception("Cannot find type " + baseTypeName + " after searching in all user provided assemblies and all loaded assemblies. Is the type in some plugin-module that was not yet loaded? Or did the assembly that contains the type change (ie the type got removed)?"); + } + + public Type GetTypeFromBaseAndArguments(string baseTypeName, params Type[] genericTypeArguments) + { + var baseType = GetTypeFromBase(baseTypeName); + return baseType.MakeGenericType(genericTypeArguments); + } + } +} \ No newline at end of file diff --git a/src/Ceras/Resolvers/DynamicObjectFormatterResolver.cs b/src/Ceras/Resolvers/DynamicObjectFormatterResolver.cs index 7456662..835145c 100644 --- a/src/Ceras/Resolvers/DynamicObjectFormatterResolver.cs +++ b/src/Ceras/Resolvers/DynamicObjectFormatterResolver.cs @@ -10,11 +10,12 @@ public class DynamicObjectFormatterResolver : IFormatterResolver { CerasSerializer _ceras; - TypeDictionary _dynamicFormatters = new TypeDictionary(); + VersionToleranceMode _versionToleranceMode; public DynamicObjectFormatterResolver(CerasSerializer ceras) { _ceras = ceras; + _versionToleranceMode = ceras.Config.VersionTolerance.Mode; } public IFormatter GetFormatter(Type type) @@ -23,16 +24,25 @@ public IFormatter GetFormatter(Type type) { throw new InvalidOperationException($"No formatter for the Type '{type.FullName}' was found. Ceras is trying to fall back to the DynamicFormatter, but that formatter will never work in on AoT compiled platforms. Use the code generator tool to automatically generate a formatter for this type."); } + + var meta = _ceras.GetTypeMetaData(type); + if (meta.IsPrimitive) + throw new InvalidOperationException("DynamicFormatter is not allowed to serialize serialization-primitives."); - ref var formatter = ref _dynamicFormatters.GetOrAddValueRef(type); - if (formatter != null) - return formatter; - var dynamicFormatterType = typeof(DynamicFormatter<>).MakeGenericType(type); - formatter = (IFormatter)Activator.CreateInstance(dynamicFormatterType, _ceras); - - return formatter; + if ((_versionToleranceMode == VersionToleranceMode.Standard && !meta.IsFrameworkType) || + (_versionToleranceMode == VersionToleranceMode.Extended && meta.IsFrameworkType)) + { + // SchemaFormatter will automatically adjust itself to the schema when it's read + var formatterType = typeof(SchemaDynamicFormatter<>).MakeGenericType(type); + return (IFormatter)Activator.CreateInstance(formatterType, args: new object[] { _ceras, meta.PrimarySchema }); + } + else + { + var formatterType = typeof(DynamicFormatter<>).MakeGenericType(type); + return (IFormatter)Activator.CreateInstance(formatterType, _ceras); + } } } } \ No newline at end of file diff --git a/tests/Ceras.Test/Tests/Arrays.cs b/tests/Ceras.Test/Tests/Arrays.cs deleted file mode 100644 index 300fe3f..0000000 --- a/tests/Ceras.Test/Tests/Arrays.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using Xunit; - -namespace Ceras.Test -{ - public class Arrays : TestBase - { - [Fact] - public void Primitives() - { - var nullBytes = (byte[])null; - TestDeepEquality(nullBytes, TestMode.AllowNull); - - TestDeepEquality(new byte[0]); - - var byteAr = new byte[rng.Next(100, 200)]; - rng.NextBytes(byteAr); - TestDeepEquality(byteAr); - } - - [Fact] - public void Structs() - { - TestDeepEquality((sbyte[])null, TestMode.AllowNull); - TestDeepEquality(new sbyte[0]); - TestDeepEquality(new sbyte[] { -5, -128, 0, 34 }); - - TestDeepEquality((decimal[])null, TestMode.AllowNull); - TestDeepEquality(new decimal[0]); - TestDeepEquality(new decimal[] { 1M, 2M, 3M, decimal.MinValue, decimal.MaxValue }); - - TestDeepEquality(new[] - { - new Vector3(1, rngFloat, 3), - new Vector3(rngFloat, rngFloat, float.NaN), - new Vector3(float.Epsilon, rngFloat, float.NegativeInfinity), - new Vector3(-5, float.MaxValue, rngFloat), - }); - - TestDeepEquality(new[] - { - ValueTuple.Create((byte)150, 5f, 3.0, "a"), - ValueTuple.Create((byte)150, 5f, 3.0, "b"), - ValueTuple.Create((byte)150, -5f, 1.0, "c"), - }); - - var r = new Random(DateTime.Now.GetHashCode()); - var decimalData = new decimal[r.Next(100, 200)]; - for (var i = 0; i < decimalData.Length; ++i) - decimalData[i] = (decimal)r.NextDouble(); - - TestDeepEquality(decimalData); - } - - [Fact] - public void Objects() - { - TestDeepEquality(new[] { new object(), new object(), new object() }); - - TestDeepEquality(new[] { "asdfg", "asdfg", "asdfg", "", "", "1", "2", "3", ",.-üä#ß351293ß6!§`?=&=$&" }); - - TestDeepEquality(new[] { (object)DateTime.Now, (object)DateTime.Now, (object)DateTime.Now, (object)DateTime.Now, }); - - TestDeepEquality(new[] - { - new List> { Tuple.Create(5, "a"), Tuple.Create(-2222, "q"), Tuple.Create(int.MinValue, "x") }, - new List> { Tuple.Create(6, "a"), Tuple.Create(33333, "v"), Tuple.Create(int.MinValue / 2, "y") }, - new List> { Tuple.Create(7, "a"), Tuple.Create(23457, "w"), Tuple.Create(int.MaxValue, "z") }, - }); - } - - - } -} diff --git a/tests/Ceras.Test/Tests/Basics.cs b/tests/Ceras.Test/Tests/BuiltInTypes.cs similarity index 76% rename from tests/Ceras.Test/Tests/Basics.cs rename to tests/Ceras.Test/Tests/BuiltInTypes.cs index 959b7e8..7450905 100644 --- a/tests/Ceras.Test/Tests/Basics.cs +++ b/tests/Ceras.Test/Tests/BuiltInTypes.cs @@ -9,9 +9,9 @@ namespace Ceras.Test using System.Numerics; using Xunit; - public class Basics : TestBase + public class BuiltInTypes : TestBase { - public Basics() + public BuiltInTypes() { SetSerializerConfigurations(Config_NoReinterpret, Config_WithReinterpret, Config_WithVersioning); } @@ -80,21 +80,75 @@ public void DateTimeZone() } - static void AssertDateTimeEqual(DateTime t1, DateTime t2) + + [Fact] + public void PrimitiveArrays() { - Assert.True(t1.Kind == t2.Kind); + var nullBytes = (byte[])null; + TestDeepEquality(nullBytes, TestMode.AllowNull); - Assert.True(t1.Ticks == t2.Ticks); + TestDeepEquality(new byte[0]); - Assert.True(t1.Year == t2.Year && - t1.Month == t2.Month && - t1.Day == t2.Day && - t1.Hour == t2.Hour && - t1.Minute == t2.Minute && - t1.Second == t2.Second && - t1.Millisecond == t2.Millisecond); + var byteAr = new byte[rng.Next(100, 200)]; + rng.NextBytes(byteAr); + TestDeepEquality(byteAr); + } + + [Fact] + public void StructArrays() + { + TestDeepEquality((sbyte[])null, TestMode.AllowNull); + TestDeepEquality(new sbyte[0]); + TestDeepEquality(new sbyte[] { -5, -128, 0, 34 }); + + TestDeepEquality((decimal[])null, TestMode.AllowNull); + TestDeepEquality(new decimal[0]); + TestDeepEquality(new decimal[] { 1M, 2M, 3M, decimal.MinValue, decimal.MaxValue }); + + TestDeepEquality(new[] + { + new Vector3(1, rngFloat, 3), + new Vector3(rngFloat, rngFloat, float.NaN), + new Vector3(float.Epsilon, rngFloat, float.NegativeInfinity), + new Vector3(-5, float.MaxValue, rngFloat), + }); + + TestDeepEquality(new[] + { + ValueTuple.Create((byte)150, 5f, 3.0, "a"), + ValueTuple.Create((byte)150, 5f, 3.0, "b"), + ValueTuple.Create((byte)150, -5f, 1.0, "c"), + }); + + var r = new Random(DateTime.Now.GetHashCode()); + var decimalData = new decimal[r.Next(100, 200)]; + for (var i = 0; i < decimalData.Length; ++i) + decimalData[i] = (decimal)r.NextDouble(); + + TestDeepEquality(decimalData); + } + + [Fact] + public void ObjectArrays() + { + TestDeepEquality(new[] { new object(), new object(), new object() }); + + TestDeepEquality(new[] { "asdfg", "asdfg", "asdfg", "", "", "1", "2", "3", ",.-üä#ß351293ß6!§`?=&=$&" }); + + TestDeepEquality(new[] { (object)DateTime.Now, (object)DateTime.Now, (object)DateTime.Now, (object)DateTime.Now, }); + + TestDeepEquality(new[] + { + new List> { Tuple.Create(5, "a"), Tuple.Create(-2222, "q"), Tuple.Create(int.MinValue, "x") }, + new List> { Tuple.Create(6, "a"), Tuple.Create(33333, "v"), Tuple.Create(int.MinValue / 2, "y") }, + new List> { Tuple.Create(7, "a"), Tuple.Create(23457, "w"), Tuple.Create(int.MaxValue, "z") }, + }); } + + + + #if NETFRAMEWORK [Fact] @@ -253,7 +307,7 @@ public void BasicUsage() var data = ceras.Serialize(c); var clone = ceras.Deserialize(data); - Assert.True(clone.Elements.Length == 2); + Assert.True(clone.Elements.Length == 3); Assert.True(clone.Elements[0].Name == "a"); Assert.True(clone.Elements[1].Id == 123); @@ -262,6 +316,26 @@ public void BasicUsage() #endif } + + + + + + static void AssertDateTimeEqual(DateTime t1, DateTime t2) + { + Assert.True(t1.Kind == t2.Kind); + + Assert.True(t1.Ticks == t2.Ticks); + + Assert.True(t1.Year == t2.Year && + t1.Month == t2.Month && + t1.Day == t2.Day && + t1.Hour == t2.Hour && + t1.Minute == t2.Minute && + t1.Second == t2.Second && + t1.Millisecond == t2.Millisecond); + } + } class Container diff --git a/tests/Ceras.Test/Tests/ConstructionAndPooling.cs b/tests/Ceras.Test/Tests/ConstructionAndPooling.cs new file mode 100644 index 0000000..07f00b0 --- /dev/null +++ b/tests/Ceras.Test/Tests/ConstructionAndPooling.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ceras.Test +{ + using Xunit; + + public class ConstructionAndPooling : TestBase + { + static int Add1(int x) => x + 1; + static int Add2(int x) => x + 2; + + + [Fact] + public void TestDirectPoolingMethods() + { + var pool = new InstancePoolTest(); + + // Test: Ctor with argument + { + SerializerConfig config = new SerializerConfig(); + + config.ConfigType() + // Select ctor, not delegate + .ConstructBy(() => new Person("name")); + + var clone = DoRoundTripTest(config); + Assert.True(clone != null); + Assert.True(clone.Name.StartsWith("riki")); + Assert.True(clone.Name.EndsWith(Person.CtorSuffix)); + } + + // Test: Manual config + { + SerializerConfig config = new SerializerConfig(); + + config.ConfigType() + .ConstructBy(TypeConstruction.ByStaticMethod(() => StaticPoolTest.CreatePerson())); + + var clone = DoRoundTripTest(config); + Assert.True(clone != null); + } + + // Test: Normal ctor, but explicitly + { + SerializerConfig config = new SerializerConfig(); + + config.ConfigType() + // Select ctor, not delegate + .ConstructBy(() => new Person()); + + var clone = DoRoundTripTest(config); + Assert.True(clone != null); + } + + // Test: Construct from instance-pool + { + SerializerConfig config = new SerializerConfig(); + + config.ConfigType() + // Instance + method select + .ConstructBy(pool, () => pool.CreatePerson()); + + var clone = DoRoundTripTest(config); + Assert.True(clone != null); + Assert.True(pool.IsFromPool(clone)); + } + + // Test: Construct from static-pool + { + SerializerConfig config = new SerializerConfig(); + + config.ConfigType() + // method select + .ConstructBy(() => StaticPoolTest.CreatePerson()); + + var clone = DoRoundTripTest(config); + Assert.True(clone != null); + Assert.True(StaticPoolTest.IsFromPool(clone)); + } + + // Test: Construct from any delegate (in this example: a lambda expression) + { + SerializerConfig config = new SerializerConfig(); + + Person referenceCapturedByLambda = null; + + config.ConfigType() + // Use delegate + .ConstructByDelegate(() => + { + var obj = new Person(); + referenceCapturedByLambda = obj; + return obj; + }); + + var clone = DoRoundTripTest(config); + Assert.True(clone != null); + Assert.True(ReferenceEquals(clone, referenceCapturedByLambda)); + } + + // Test: Construct from instance-pool, with parameter + { + SerializerConfig config = new SerializerConfig(); + + config.ConfigType() + // Use instance + method selection + .ConstructBy(pool, () => pool.CreatePersonWithName("abc")); + + var clone = DoRoundTripTest(config); + Assert.True(clone != null); + Assert.True(clone.Name.StartsWith("riki")); + Assert.True(pool.IsFromPool(clone)); + } + + // Test: Construct from static-pool, with parameter + { + SerializerConfig config = new SerializerConfig(); + + config.ConfigType() + // Use instance + method selection + .ConstructBy(() => StaticPoolTest.CreatePersonWithName("abc")); + + var clone = DoRoundTripTest(config); + Assert.True(clone != null); + Assert.True(clone.Name.StartsWith("riki")); + Assert.True(StaticPoolTest.IsFromPool(clone)); + } + } + + [Fact] + public void DelegatesTest() + { + var config = new SerializerConfig(); + config.Advanced.DelegateSerialization = DelegateSerializationFlags.AllowStatic; + var ceras = new CerasSerializer(config); + + // 1. Simple test: can ceras persist a static-delegate + { + Func staticFunc = Add1; + + var data = ceras.Serialize(staticFunc); + + var staticFuncClone = ceras.Deserialize>(data); + + Assert.True(staticFuncClone != null); + Assert.True(object.Equals(staticFunc, staticFuncClone) == true); // must be considered the same + Assert.True(object.ReferenceEquals(staticFunc, staticFuncClone) == false); // must be a new instance + + + Assert.True(staticFuncClone(5) == staticFunc(5)); + } + + // 2. What about a collection of them + { + var rng = new Random(); + List> funcs = new List>(); + + for (int i = 0; i < rng.Next(15, 20); i++) + { + Func f; + + if (rng.Next(100) < 50) + f = Add1; + else + f = Add2; + + funcs.Add(f); + } + + var data = ceras.Serialize(funcs); + var cloneList = ceras.Deserialize>>(data); + + // Check by checking if the result is the same + Assert.True(funcs.Count == cloneList.Count); + for (int i = 0; i < funcs.Count; i++) + { + var n = rng.Next(); + Assert.True(funcs[i](n) == cloneList[i](n)); + } + } + + // 3. If we switch to "allow instance", it should persist instance-delegates, but no lambdas + { + config.Advanced.DelegateSerialization = DelegateSerializationFlags.AllowInstance; + ceras = new CerasSerializer(config); + + + // + // A) Direct Instance + // + var method = GetMethod(() => new Person().GetHealth()); + var p = new Person("direct instance") { Health = 3456 }; + var del = (Func)Delegate.CreateDelegate(typeof(Func), p, method); + + // Does our delegate even work? + var testResult = del(); + Assert.True(testResult == p.Health); + + // Can we serialize the normal instance delegate? + var data = ceras.Serialize(del); + var clone = ceras.Deserialize>(data); + + // Does it still work? + Assert.True(testResult == clone()); + + + + + } + } + + + + + static Person DoRoundTripTest(SerializerConfig config, string name = "riki") + { + var ceras = new CerasSerializer(config); + + var p = new Person(); + p.Name = name; + + var data = ceras.Serialize(p); + + var clone = ceras.Deserialize(data); + return clone; + } + + + class StaticPoolTest + { + static HashSet _objectsCreatedByPool = new HashSet(); + + public static Person CreatePerson() + { + var p = new Person(); + _objectsCreatedByPool.Add(p); + return p; + } + + public static Person CreatePersonWithName(string name) + { + var p = new Person(); + p.Name = name; + _objectsCreatedByPool.Add(p); + return p; + } + + public static bool IsFromPool(Person p) => _objectsCreatedByPool.Contains(p); + + public static void DiscardPooledObjectTest(Person p) + { + } + } + + class InstancePoolTest + { + HashSet _objectsCreatedByPool = new HashSet(); + + public Person CreatePerson() + { + var p = new Person(); + _objectsCreatedByPool.Add(p); + return p; + } + + public Person CreatePersonWithName(string name) + { + var p = new Person(); + p.Name = name; + _objectsCreatedByPool.Add(p); + return p; + } + + public bool IsFromPool(Person p) => _objectsCreatedByPool.Contains(p); + + public void DiscardPooledObjectTest(Person p) + { + } + } + + class Person + { + public const string CtorSuffix = " (modified by constructor)"; + + public string Name; + public int Health; + public Person BestFriend; + + public Person() + { + } + + public Person(string name) + { + Name = name + CtorSuffix; + } + + public int GetHealth() => Health; + + public string SayHello() => $"Hello I'm {Name}"; + } + + } +} diff --git a/tests/Ceras.Test/Tests/Internals.cs b/tests/Ceras.Test/Tests/Internals.cs index b1eace9..c405bee 100644 --- a/tests/Ceras.Test/Tests/Internals.cs +++ b/tests/Ceras.Test/Tests/Internals.cs @@ -5,6 +5,7 @@ namespace Ceras.Test { using Helpers; using System; + using System.Collections.Generic; public class Internals : TestBase { @@ -71,22 +72,19 @@ class Dog : IDog { } [Fact] - public void TypeChecks() + public void IsBlittableChecks() { Assert.True(ReflectionHelper.IsBlittableType(typeof(bool))); Assert.True(ReflectionHelper.IsBlittableType(typeof(double))); Assert.True(ReflectionHelper.IsBlittableType(typeof(double))); - - + Assert.True(ReflectionHelper.IsBlittableType(typeof(DayOfWeek))); // actual enum + Assert.False(ReflectionHelper.IsBlittableType(typeof(string))); - Assert.False(ReflectionHelper.IsBlittableType(typeof(DayOfWeek))); - Assert.False(ReflectionHelper.IsBlittableType(typeof(Enum))); + Assert.False(ReflectionHelper.IsBlittableType(typeof(Enum))); // enum class itself Assert.False(ReflectionHelper.IsBlittableType(typeof(byte*))); Assert.False(ReflectionHelper.IsBlittableType(typeof(int*))); Assert.False(ReflectionHelper.IsBlittableType(typeof(IntPtr))); Assert.False(ReflectionHelper.IsBlittableType(typeof(UIntPtr))); - Assert.False(ReflectionHelper.IsBlittableType(typeof(void))); - } [Fact] @@ -96,11 +94,30 @@ public void TypeMetaData() var ceras = new CerasSerializer(); - var m1 = ceras.GetTypeMetaData(typeof(int)); - Assert.True(m1.IsFrameworkType && m1.IsPrimitive); + // true and false keywords are both highlighted in the same color, so this makes it easier to see :P + const bool False = false; + // isPrimitive means "is a serialization primitive", not primitive as in "primitive type" like int or something. + var tests = new List<(bool isFramework, bool isPrimitive, bool isBlittable, Type testType)>(); + tests.Add((isFramework: true, isPrimitive: true, isBlittable: true, typeof(int))); + tests.Add((isFramework: true, isPrimitive: true, isBlittable: true, typeof(bool))); + tests.Add((isFramework: true, isPrimitive: true, isBlittable: true, typeof(char))); + + tests.Add((isFramework: true, isPrimitive: true, isBlittable: False, typeof(Type))); + tests.Add((isFramework: true, isPrimitive: true, isBlittable: False, typeof(Type).GetType())); + + tests.Add((isFramework: true, isPrimitive: False, isBlittable: False, typeof(List))); + + foreach (var test in tests) + { + var meta = ceras.GetTypeMetaData(test.testType); + + Assert.True(meta.IsFrameworkType == test.isFramework); + Assert.True(meta.IsPrimitive == test.isPrimitive); + Assert.True(ReflectionHelper.IsBlittableType(test.testType) == test.isBlittable); + } } } diff --git a/tests/Ceras.Test/Tests/Misc.cs b/tests/Ceras.Test/Tests/Misc.cs new file mode 100644 index 0000000..30bd309 --- /dev/null +++ b/tests/Ceras.Test/Tests/Misc.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ceras.Test +{ + using Xunit; + + public class Misc : TestBase + { + [Fact] + public void DictInObjArrayTest() + { + var dict = new Dictionary + { + ["test"] = new Dictionary + { + ["test"] = new object[] + { + new Dictionary + { + ["test"] = 3 + } + } + } + }; + + + var s = new CerasSerializer(); + + var data = s.Serialize(dict); + + var cloneDict = s.Deserialize>(data); + + var inner1 = cloneDict["test"] as Dictionary; + Assert.True(inner1 != null); + + var objArray = inner1["test"] as object[]; + Assert.True(objArray != null); + + var dictElement = objArray[0] as Dictionary; + Assert.True(dictElement != null); + + var three = dictElement["test"]; + + Assert.True(three.GetType() == typeof(int)); + Assert.True(3.Equals(three)); + } + + } +} diff --git a/tests/Ceras.Test/Tests/TypeConfig.cs b/tests/Ceras.Test/Tests/TypeConfig.cs index a4ea600..e39302a 100644 --- a/tests/Ceras.Test/Tests/TypeConfig.cs +++ b/tests/Ceras.Test/Tests/TypeConfig.cs @@ -76,6 +76,29 @@ public void CustomFormatterForEnum() } + [Fact] + public void CtorTest() + { + var obj = new ConstructorTest(5); + var ceras = new CerasSerializer(); + + bool success = false; + try + { + // Expected to throw: no default ctor + var data = ceras.Serialize(obj); + var clone = ceras.Deserialize(data); + } + catch (Exception e) + { + success = true; + } + + Assert.True(success, "objects with no ctor and no TypeConfig should not serialize"); + } + + + static void ExpectException(Action f) { try @@ -109,4 +132,15 @@ class ErrorFormatter : IFormatter void Throw() => throw new InvalidOperationException("This shouldn't happen"); } + + class ConstructorTest + { + public int x; + + public ConstructorTest(int x) + { + this.x = x; + } + } + }