From b77b3a1198a27dcee9d49192d0a564baeb43c191 Mon Sep 17 00:00:00 2001 From: RogueAI42 Date: Sun, 5 Nov 2017 22:15:08 +1100 Subject: [PATCH] you don't need OrderAttribute! --- README.md | 70 ++++- Tests/Test.cs | 89 ++++--- Tests/Tests.csproj | 1 + Whoa/Properties/AssemblyInfo.cs | 4 +- Whoa/Whoa.cs | 444 +++++++++++++++++++++----------- Whoa/Whoa.csproj | 3 + 6 files changed, 407 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index 484b346..97ff898 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,72 @@ # Whoa ## About -Whoa is a serialisation library for C#. Its output is ultra small and +Whoa is a serialisation library for .NET. Its output is ultra small and you can add members to your type and still be able to deserialise old versions. Since it stores the bare minimum of type data, it is a bit more finicky than other serialisation solutions like BinaryFormatter, but produces much smaller output. +The format may change in non-backwards-compatible ways sometimes as new +features are added. To make sure your data can be deserialised, use the +same version of Whoa for serialisation and deserialisation. That said, +whenever feasible, backwards compatibility will be preserved, and +eventually the format will be finalised. + ## Usage -For Whoa to produce meaningful output, your type will need to have a -public constructor with no arguments, and every field or property that -you would like to save needs to be public (properties must have a public -getter AND a public setter) and have a Whoa.OrderAttribute. Also, if you -want to preserve backwards compatibility, don't rearrange or remove -any of the members with OrderAttributes. You can, however, add new ones -to the end of your class without issue. +Whoa is static and stateless. To serialise an object: -To serialise an object: -`Whoa.Whoa.SerialiseObject(outstream, obj);` +`Whoa.Whoa.SerialiseObject(outstream, object[, options]);` -To deserialise an object: -`Whoa.Whoa.DeserialiseObject(instream);` +... and to deserialise it: +`Whoa.Whoa.DeserialiseObject(instream[, options]);` +There are also reflection-friendly interfaces to these functions: + +`Whoa.Whoa.SerialiseObject(type, outstream, object[, options]);` + +`Whoa.Whoa.DeserialiseObject(type, instream[, options]);` + +If you'd like to take control of the serialisation of a particular +class, you should define a derivative of ISpecialSerialiser, instantiate +it, and pass it to Whoa with: + +`Whoa.Whoa.RegisterSpecialSerialiser(instance);` + +ISpecialSerialiser is a generic interface. Your derivative should fill +in the type argument with the type you want to handle. Whoa contains +inbuilt special serialisers for: + +* System.Guid +* System.BigInteger +* System.String +* System.DateTime +* System.Drawing.Color +* System.Drawing.Font +* System.Drawing.Image +* System.IO.Stream + +It will also handle arrays, Lists, and Dictionaries of types it knows +automatically, and numeric types and enums are passed to BinaryWriter. + +By default, Whoa requires the use of an OrderAttribute on each member +of your class that you would like serialised, and it will only touch +public members. It is also perfectly happy to serialise classes that do +not have a SerializableAttribute. These characteristics can be changed +by passing flags from the SerialisationOptions enum. +Of particular note is the NonSerialized mode. This will remove the +requirement for Order attributes and instead (de)serialise the members +in the order they are received from GetMembers(). The .NET documentation +states that this order is arbitrary, but on both Microsoft's framework +and Mono, it seems to always match declaration order, so it's not +currently a practical issue. In this mode, you can exclude members from +serialisation by giving them a NonSerializedAttribute. +Also, remember that the options will need to be the same on +serialisation and deserialisation. + +## TODO + +* Reference semantics +* Remembering derived type information diff --git a/Tests/Test.cs b/Tests/Test.cs index 49ada24..39a187e 100644 --- a/Tests/Test.cs +++ b/Tests/Test.cs @@ -19,6 +19,7 @@ using System.IO; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Drawing; using Whoa; namespace Whoa.Tests @@ -42,87 +43,95 @@ namespace Whoa.Tests private class Record { - [Order] + public string title; - [Order] + public string artist; - [Order] - public int rpm; - [Order] + public int rpm { get; set; } + + public double price; - [Order] + public bool inLibrary; - [Order] + public bool otherBool; - [Order] + public List songs; - [Order] + public Guid guidForSomeReason; - [Order] + public List moreBools; - [Order] + public string awards; - [Order] + public int? profits; - [Order] + public int? losses; - [Order] + public List somethingElse; - [Order] + public Dictionary staff; - [Order] + public Dictionary plausibleSampleData; - [Order] + public List notActuallyMoreBools; - [Order] + public bool boolThree; - [Order] + public bool boolFour; - [Order] + public bool boolFive; - [Order] + public bool boolSix; - [Order] + public bool boolSeven; - [Order] + public bool boolEight; - [Order] + public BigInteger reallyReallyReallyReallyReallyReallyBigNumber; - [Order] + public DateTime releaseDate; - [Order] + public RecordType kind; - [Order] + public Details details; - [Order] + public string[] thisIsAnArrayNotAList; + public Image img; + + public Color colour; + + public Font font; + + public Stream data; + public override string ToString() { string ret = $@"{title} @@ -150,6 +159,8 @@ Released: {releaseDate} ret += $"{pair.Key} - {pair.Value}" + Environment.NewLine; ret += "Null dictionaries work: " + (plausibleSampleData == null ? "Yes" : "No") + Environment.NewLine; ret += "Null bool lists work: " + (notActuallyMoreBools == null ? "Yes" : "No") + Environment.NewLine; + byte[] buf = new byte[data.Length]; + data.Read(buf, 0, buf.Length); ret += $@"boolThree = {boolThree} boolFour = {boolFour} boolFive = {boolFive} @@ -162,13 +173,17 @@ Is catchy: {details.HasFlag(Details.Catchy)}. Is popular: {details.HasFlag(Details.Popular)}. Is terrible: {details.HasFlag(Details.Terrible)}. The second entry of an array that isn't a list is: {thisIsAnArrayNotAList[1]} +{img.Width} +{colour.R} {colour.G} {colour.B} {colour.A} +{font} +{System.Text.Encoding.UTF8.GetString(buf)} "; return ret; } } public static void Main(string[] args) { - using (var str = new MemoryStream()) + using (var str = args.Length > 0 ? (File.Open(args[0], FileMode.Create) as Stream) : (new MemoryStream() as Stream)) { var rec = new Record() { @@ -216,18 +231,16 @@ The second entry of an array that isn't a list is: {thisIsAnArrayNotAList[1]} "The freedom to study how the program works, and change it so it does your computing as you wish (freedom 1). Access to the source code is a precondition for this.", "The freedom to redistribute copies so you can help your neighbor (freedom 2).", "The freedom to distribute copies of your modified versions to others (freedom 3). By doing this you can give the whole community a chance to benefit from your changes. Access to the source code is a precondition for this." - } + }, + img = new Bitmap(800, 600), + colour = Color.FromArgb(255, 127, 0), + font = new Font("Times New Roman", 12), + data = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("what's up, gamers?")) }; string expected = rec.ToString(); - Whoa.SerialiseObject(str, rec); - if (args.Length > 0) // Save output for debugging. - { - str.Position = 0; - using (var fobj = File.OpenWrite(args[0])) - str.CopyTo(fobj); - } + Whoa.SerialiseObject(str, rec, SerialisationOptions.NonSerialized); str.Position = 0; - var res = Whoa.DeserialiseObject(str); + var res = Whoa.DeserialiseObject(str, SerialisationOptions.NonSerialized); string actual = res.ToString(); Console.Write("Test "); if (expected == actual) diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 66019c0..91a1902 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -41,6 +41,7 @@ + diff --git a/Whoa/Properties/AssemblyInfo.cs b/Whoa/Properties/AssemblyInfo.cs index b1e3cd8..f2f1490 100644 --- a/Whoa/Properties/AssemblyInfo.cs +++ b/Whoa/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.4.0.0")] -[assembly: AssemblyFileVersion("1.4.0.0")] +[assembly: AssemblyVersion("1.5.0.0")] +[assembly: AssemblyFileVersion("1.5.0.0")] diff --git a/Whoa/Whoa.cs b/Whoa/Whoa.cs index b8bf68d..5f44dd9 100644 --- a/Whoa/Whoa.cs +++ b/Whoa/Whoa.cs @@ -24,114 +24,243 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Reflection; using System.Numerics; +using System.Runtime.Serialization; +using System.Drawing; +using System.Drawing.Imaging; namespace Whoa { public static class Whoa - { - private class SpecialSerialiserAttribute: Attribute + { + private enum SpecialSizes { - public Type t { get; private set; } - public SpecialSerialiserAttribute(Type in_t) + Null = -1, + ReferenceEqual = -2 // not used yet... + } + + private static Dictionary SpecialSerialisers = new Dictionary(); + + static Whoa() + { + foreach (Type t in typeof(Whoa).GetNestedTypes(BindingFlags.NonPublic)) { - t = in_t; - } - } - - private class SpecialDeserialiserAttribute: Attribute - { - public Type t { get; private set; } - public SpecialDeserialiserAttribute(Type in_t) - { - t = in_t; - } - } - - [SpecialSerialiser(typeof(Guid))] - private static void SerialiseGuid(Stream fobj, dynamic obj) - { - fobj.Write(obj.ToByteArray(), 0, 16); - } - - [SpecialDeserialiser(typeof(Guid))] - private static object DeserialiseGuid(Stream fobj) - { - var guid = new byte[16]; - fobj.Read(guid, 0, 16); - return new Guid(guid); - } - - [SpecialSerialiser(typeof(BigInteger))] - private static void SerialiseBigInt(Stream fobj, dynamic obj) - { - SerialiseObject(fobj, new List(obj.ToByteArray())); - } - - [SpecialDeserialiser(typeof(BigInteger))] - private static object DeserialiseBigInt(Stream fobj) - { - return new BigInteger((DeserialiseObject(typeof(List), fobj) as List).ToArray()); - } - - [SpecialSerialiser(typeof(string))] - private static void SerialiseString(Stream fobj, dynamic obj) - { - using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) - { - byte[] bytes = null; - int len; - if (obj == null) - len = -1; - else + if (t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISpecialSerialiser<>))) { - bytes = Encoding.UTF8.GetBytes(obj); - len = bytes.Length; - } - typeof(BinaryWriter).GetMethod("Write7BitEncodedInt", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(write, new object[] { len }); - if (bytes != null) - fobj.Write(bytes, 0, len); - } - } - - [SpecialDeserialiser(typeof(string))] - private static object DeserialiseString(Stream fobj) - { - using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) - { - int len = (int)typeof(BinaryReader).GetMethod("Read7BitEncodedInt", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(read, new object[] { }); - if (len < 0) - return null; - else - { - var bytes = new byte[len]; - fobj.Read(bytes, 0, len); - return Encoding.UTF8.GetString(bytes); + RegisterSpecialSerialiser((dynamic)Activator.CreateInstance(t)); } } } - [SpecialSerialiser(typeof(DateTime))] - private static void SerialiseDateTime(Stream fobj, dynamic obj) + public static void RegisterSpecialSerialiser(ISpecialSerialiser serialiser) { - using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) - write.Write(obj.ToBinary()); + SpecialSerialisers.Add(typeof(T), serialiser); } - [SpecialDeserialiser(typeof(DateTime))] - private static object DeserialiseDateTime(Stream fobj) + private class GuidSerialiser: ISpecialSerialiser { - using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) - return DateTime.FromBinary(read.ReadInt64()); + public void SerialiseObject(Stream fobj, Guid obj) + { + fobj.Write(obj.ToByteArray(), 0, 16); + } + + public Guid DeserialiseObject(Stream fobj) + { + var guid = new byte[16]; + fobj.Read(guid, 0, 16); + return new Guid(guid); + } } - private static IOrderedEnumerable Members(Type t) + private class BigIntegerSerialiser: ISpecialSerialiser { - return t.GetProperties().Select(m => m as MemberInfo).Concat(t.GetFields().Select(m => m as MemberInfo)).Where(m => (m.GetCustomAttributes(typeof(OrderAttribute), false).SingleOrDefault() as OrderAttribute) != null).OrderBy(m => (m.GetCustomAttributes(typeof(OrderAttribute), false).SingleOrDefault() as OrderAttribute).Order); + public void SerialiseObject(Stream fobj, BigInteger obj) + { + using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) + { + byte[] bytes = obj.ToByteArray(); + write.Write(bytes.Length); + write.Write(bytes); + } + } + + public BigInteger DeserialiseObject(Stream fobj) + { + using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) + return new BigInteger(read.ReadBytes(read.ReadInt32())); + } + } + + private class StringSerialiser: ISpecialSerialiser + { + private class FriendlyBinaryWriter: BinaryWriter + { + public FriendlyBinaryWriter(Stream fobj) : base(fobj, Encoding.UTF8, true) + { + } + public void Write7BitEncodedIntPublic(int value) + { + Write7BitEncodedInt(value); + } + } + + private class FriendlyBinaryReader: BinaryReader + { + public FriendlyBinaryReader(Stream fobj) : base(fobj, Encoding.UTF8, true) + { + } + public int Read7BitEncodedIntPublic() + { + return Read7BitEncodedInt(); + } + } + public void SerialiseObject(Stream fobj, string obj) + { + using (var write = new FriendlyBinaryWriter(fobj)) + { + byte[] bytes = null; + int len; + if (obj == null) + len = (int)SpecialSizes.Null; + else + { + bytes = Encoding.UTF8.GetBytes(obj); + len = bytes.Length; + } + write.Write7BitEncodedIntPublic(len); + if (bytes != null) + write.Write(bytes); + } + } + + public string DeserialiseObject(Stream fobj) + { + using (var read = new FriendlyBinaryReader(fobj)) + { + int len = read.Read7BitEncodedIntPublic(); + if (len == (int)SpecialSizes.Null) + return null; + return Encoding.UTF8.GetString(read.ReadBytes(len)); + } + } + } + + private class DateTimeSerialiser: ISpecialSerialiser + { + public void SerialiseObject(Stream fobj, DateTime obj) + { + using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) + write.Write(obj.ToBinary()); + } + + public DateTime DeserialiseObject(Stream fobj) + { + using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) + return DateTime.FromBinary(read.ReadInt64()); + } + } + + private class ColorSerialiser: ISpecialSerialiser + { + public void SerialiseObject(Stream fobj, Color obj) + { + using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) + write.Write(obj.ToArgb()); + } + + public Color DeserialiseObject(Stream fobj) + { + using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) + return Color.FromArgb(read.ReadInt32()); + } + } + + private class FontSerialiser: ISpecialSerialiser + { + public void SerialiseObject(Stream fobj, Font obj) + { + using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) + { + write.Write(obj.FontFamily.Name); + write.Write(obj.Size); + write.Write((int)obj.Style); + write.Write((int)obj.Unit); + write.Write(obj.GdiCharSet); + write.Write(obj.GdiVerticalFont); + } + } + + public Font DeserialiseObject(Stream fobj) + { + using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) + return new Font(read.ReadString(), read.ReadSingle(), (FontStyle)read.ReadInt32(), (GraphicsUnit)read.ReadInt32(), read.ReadByte(), read.ReadBoolean()); + } + } + + private class ImageSerialiser: ISpecialSerialiser + { + public void SerialiseObject(Stream fobj, Image obj) + { + // Images can be saved to Streams but the image data will + // be "invalid" if the Stream it is written to starts at a + // non-zero position, so we need to buffer. + using (var mstr = new MemoryStream()) + using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) + { + obj.Save(mstr, ImageFormat.Png); + mstr.Position = 0; + write.Write((int)mstr.Length); + mstr.CopyTo(fobj); + } + } + + public Image DeserialiseObject(Stream fobj) + { + // Images can be loaded from Streams but they must remain + // open for the lifetime of the Image, so, to avoid needing + // a handle on the file being loaded from, we buffer on read + // too. + using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) + return Image.FromStream(new MemoryStream(read.ReadBytes(read.ReadInt32()))); + } + } + + private class StreamSerialiser: ISpecialSerialiser + { + public void SerialiseObject(Stream fobj, Stream obj) + { + using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) + { + obj.Position = 0; + write.Write((int)obj.Length); + obj.CopyTo(fobj); + } + } + + public Stream DeserialiseObject(Stream fobj) + { + using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) + return new MemoryStream(read.ReadBytes(read.ReadInt32())); + } + } + + private static IOrderedEnumerable Members(Type t, SerialisationOptions options) + { + BindingFlags flags = BindingFlags.Instance | BindingFlags.Public; + if (options.HasFlag(SerialisationOptions.NonPublic)) + flags |= BindingFlags.NonPublic; + if (options.HasFlag(SerialisationOptions.FlattenHierarchy)) + flags |= BindingFlags.FlattenHierarchy; + var all = t.GetMembers(flags).Where(m => m.MemberType == MemberTypes.Field || m.MemberType == MemberTypes.Property); + int i = 0; + if (options.HasFlag(SerialisationOptions.NonSerialized)) + return all.Where(m => (m.GetCustomAttributes(typeof(NonSerializedAttribute), false).SingleOrDefault() == null)).OrderBy(m => i++); + else + return all.Where(m => (m.GetCustomAttributes(typeof(OrderAttribute), false).SingleOrDefault() != null)).OrderBy(m => (m.GetCustomAttributes(typeof(OrderAttribute), false).SingleOrDefault() as OrderAttribute).Order); } private static List ReadBitfield(Stream fobj, int count) { - if (count < 0) + if (count == (int)SpecialSizes.Null) return null; sbyte bit = 7; int cur = 0; @@ -173,32 +302,39 @@ namespace Whoa fobj.Write(bitfields, 0, bitfields.Length); } - public static T DeserialiseObject(Stream fobj) + public static T DeserialiseObject(Stream fobj, SerialisationOptions options = SerialisationOptions.None) { - return (T)DeserialiseObject(typeof(T), fobj); + return (T)DeserialiseObject(typeof(T), fobj, options); } - private static object DeserialiseObjectWorker(Type t, Stream fobj) + private static object DeserialiseObjectWorker(Type t, Stream fobj, SerialisationOptions options) { #if DEBUG Console.WriteLine("Deserialising object of type: " + t.ToString()); #endif using (var read = new BinaryReader(fobj, Encoding.UTF8, true)) { + // Look for a special serialiser for this type. + dynamic special = null; + if (SpecialSerialisers.TryGetValue(t, out special)) + { + return special.DeserialiseObject(fobj); + } + if (t.IsEnum) { - return Enum.ToObject(t, DeserialiseObjectWorker(Enum.GetUnderlyingType(t), fobj)); + return Enum.ToObject(t, DeserialiseObjectWorker(Enum.GetUnderlyingType(t), fobj, options)); } if (t.IsArray) { int numelems = read.ReadInt32(); - if (numelems < 0) + if (numelems == (int)SpecialSizes.Null) return null; dynamic reta = Activator.CreateInstance(t, new object[] { numelems }); Type elemtype = t.GetElementType(); for (int i = 0; i < numelems; i++) - reta[i] = (dynamic)DeserialiseObjectWorker(elemtype, fobj); + reta[i] = (dynamic)DeserialiseObjectWorker(elemtype, fobj, options); return reta; } @@ -209,61 +345,49 @@ namespace Whoa if (gent == typeof(Nullable<>)) { bool extant = read.ReadBoolean(); - return extant ? DeserialiseObjectWorker(t.GetGenericArguments()[0], fobj) : null; + return extant ? DeserialiseObjectWorker(t.GetGenericArguments()[0], fobj, options) : null; } if (gent == typeof(List<>)) { int numelems = read.ReadInt32(); - if (numelems < 0) + if (numelems == (int)SpecialSizes.Null) return null; dynamic retl = Activator.CreateInstance(t, new object[] { numelems }); Type elemtype = t.GetGenericArguments()[0]; for (int i = 0; i < numelems; i++) - retl.Add((dynamic)DeserialiseObjectWorker(elemtype, fobj)); + retl.Add((dynamic)DeserialiseObjectWorker(elemtype, fobj, options)); return retl; } if (gent == typeof(Dictionary<,>)) { int numpairs = read.ReadInt32(); - if (numpairs < 0) + if (numpairs == (int)SpecialSizes.Null) return null; dynamic retd = Activator.CreateInstance(t, new object[] { numpairs }); Type[] arguments = t.GetGenericArguments(); for (int i = 0; i < numpairs; i++) { - dynamic key = DeserialiseObjectWorker(arguments[0], fobj); - dynamic val = DeserialiseObjectWorker(arguments[1], fobj); + dynamic key = DeserialiseObjectWorker(arguments[0], fobj, options); + dynamic val = DeserialiseObjectWorker(arguments[1], fobj, options); retd.Add(key, val); } return retd; } } - // A little self reflection. - var specialmethod = typeof(Whoa).GetMethods(BindingFlags.Static | BindingFlags.NonPublic).FirstOrDefault(m => - { - var attr = m.GetCustomAttributes(typeof(SpecialDeserialiserAttribute), false).SingleOrDefault() as SpecialDeserialiserAttribute; - if (attr == null) - return false; - return attr.t == t; - }); - - if (specialmethod != null) - return specialmethod.Invoke(null, new object[] { fobj }); - var readermethod = typeof(BinaryReader).GetMethods().FirstOrDefault(m => m.Name.Length > 4 && m.Name.StartsWith("Read") && m.ReturnType == t); if (readermethod != null) return readermethod.Invoke(read, new object[] { }); int nummembers = read.ReadInt32(); - if (nummembers < 0) + if (nummembers == (int)SpecialSizes.Null) return null; object ret = t.GetConstructor(Type.EmptyTypes).Invoke(new object[] { }); var bools = new List(); - foreach (dynamic member in Members(t).Take(nummembers)) + foreach (dynamic member in Members(t, options).Take(nummembers)) { Type memt; if (member.MemberType == MemberTypes.Field) @@ -278,7 +402,7 @@ namespace Whoa else if (memt == typeof(bool[])) member.SetValue(ret, ReadBitfield(fobj, read.ReadInt32()).ToArray()); else - member.SetValue(ret, DeserialiseObjectWorker(memt, fobj)); + member.SetValue(ret, DeserialiseObjectWorker(memt, fobj, options)); } if (bools.Count > 0) @@ -291,22 +415,32 @@ namespace Whoa } } - public static void SerialiseObject(Stream fobj, dynamic obj) + public static void SerialiseObject(Stream fobj, T obj, SerialisationOptions options = SerialisationOptions.None) { - SerialiseObject(fobj, obj, obj.GetType()); + Type t = typeof(T); + if ((t == typeof(object)) && obj != null) + t = obj.GetType(); + SerialiseObject(t, fobj, obj, options); } - private static void SerialiseObjectWorker(Stream fobj, dynamic obj, Type t) + private static void SerialiseObjectWorker(Stream fobj, dynamic obj, Type t, SerialisationOptions options) { #if DEBUG Console.WriteLine("Serialising object of type: " + t.ToString()); #endif using (var write = new BinaryWriter(fobj, Encoding.UTF8, true)) { + // Look for a special serialiser for this type. + dynamic special = null; + if (SpecialSerialisers.TryGetValue(t, out special)) + { + special.SerialiseObject(fobj, obj); + return; + } if (t.IsEnum) { Type realt = Enum.GetUnderlyingType(t); - SerialiseObjectWorker(fobj, Convert.ChangeType(obj, realt), realt); + SerialiseObjectWorker(fobj, Convert.ChangeType(obj, realt), realt, options); return; } @@ -314,12 +448,12 @@ namespace Whoa { if (obj == null) { - write.Write(-1); + write.Write((int)SpecialSizes.Null); return; } write.Write(obj.Length); foreach (dynamic item in obj) - SerialiseObjectWorker(fobj, item, item.GetType()); + SerialiseObjectWorker(fobj, item, item.GetType(), options); return; } @@ -332,7 +466,7 @@ namespace Whoa bool extant = obj != null; write.Write(extant); if (extant) - SerialiseObjectWorker(fobj, obj, t.GetGenericArguments()[0]); + SerialiseObjectWorker(fobj, obj, t.GetGenericArguments()[0], options); return; } @@ -340,12 +474,12 @@ namespace Whoa { if (obj == null) { - write.Write(-1); + write.Write((int)SpecialSizes.Null); return; } write.Write(obj.Count); foreach (dynamic item in obj) - SerialiseObjectWorker(fobj, item, item.GetType()); + SerialiseObjectWorker(fobj, item, item.GetType(), options); return; } @@ -353,36 +487,22 @@ namespace Whoa { if (obj == null) { - write.Write(-1); + write.Write((int)SpecialSizes.Null); return; } write.Write(obj.Count); foreach (dynamic pair in obj) { - SerialiseObjectWorker(fobj, pair.Key, pair.Key.GetType()); - SerialiseObjectWorker(fobj, pair.Value, pair.Value.GetType()); + SerialiseObjectWorker(fobj, pair.Key, pair.Key.GetType(), options); + SerialiseObjectWorker(fobj, pair.Value, pair.Value.GetType(), options); } return; } } - var specialmethod = typeof(Whoa).GetMethods(BindingFlags.Static | BindingFlags.NonPublic).FirstOrDefault(m => - { - var attr = m.GetCustomAttributes(typeof(SpecialSerialiserAttribute), false).SingleOrDefault() as SpecialSerialiserAttribute; - if (attr == null) - return false; - return attr.t == t; - }); - - if (specialmethod != null) - { - specialmethod.Invoke(null, new object[] { fobj, obj }); - return; - } - try { - write.Write(obj); // Will fail if not an integral type + write.Write(obj); // Will fail if not a primitive type return; } catch @@ -391,12 +511,15 @@ namespace Whoa if (obj == null) { - write.Write(-1); + write.Write((int)SpecialSizes.Null); return; } + if (options.HasFlag(SerialisationOptions.RequireSerializable) && t.GetCustomAttributes(typeof(SerializableAttribute), false).SingleOrDefault() == null) + throw new SerializationException($"{t} is not serialisable."); + var bools = new List(); - var members = Members(t); + var members = Members(t, options); write.Write(members.Count()); foreach (dynamic member in members) @@ -413,7 +536,7 @@ namespace Whoa { var val = member.GetValue(obj) as IEnumerable; if (val == null) - write.Write(-1); + write.Write((int)SpecialSizes.Null); else { write.Write(val.Count()); @@ -423,39 +546,56 @@ namespace Whoa else { dynamic val = member.GetValue(obj); - SerialiseObjectWorker(fobj, val, memt); + SerialiseObjectWorker(fobj, val, memt, options); } } WriteBitfield(fobj, bools); - } } - public static void SerialiseObject(Stream fobj, dynamic obj, Type t) + public static void SerialiseObject(Type t, Stream fobj, dynamic obj, SerialisationOptions options = SerialisationOptions.None) { try { - SerialiseObjectWorker(fobj, obj, t); + SerialiseObjectWorker(fobj, obj, t, options); } catch (Exception ex) { - ex.Data.Add("Whoa: Type", t); - ex.Data.Add("Whoa: Object", obj); + try + { + ex.Data.Add("Whoa: Type", t); + ex.Data.Add("Whoa: Object", obj); + } + catch // Stifle this exception and throw the important one + { + } throw ex; } } - public static object DeserialiseObject(Type t, Stream fobj) + [Obsolete("This argument order is weird.")] + public static void SerialiseObject(Stream fobj, dynamic obj, Type t) + { + SerialiseObject(t, fobj, obj); + } + + public static object DeserialiseObject(Type t, Stream fobj, SerialisationOptions options = SerialisationOptions.None) { try { - return DeserialiseObjectWorker(t, fobj); + return DeserialiseObjectWorker(t, fobj, options); } catch (Exception ex) { - ex.Data.Add("Whoa: Type", t); + try + { + ex.Data.Add("Whoa: Type", t); + } + catch + { + } throw ex; } } diff --git a/Whoa/Whoa.csproj b/Whoa/Whoa.csproj index 4f6dd69..4e04a1e 100644 --- a/Whoa/Whoa.csproj +++ b/Whoa/Whoa.csproj @@ -39,10 +39,13 @@ + + +