Procházet zdrojové kódy

Binary serialisation and smarter saves

Kenric Nugteren před 2 roky
rodič
revize
826dfc9b9f

+ 17 - 6
InABox.Client.Remote.Json/JsonClient.cs

@@ -61,7 +61,7 @@ namespace InABox.Clients
         //        System.Net.ServicePointManager.SecurityProtocol &= ~SecurityProtocolType.Ssl3;
         //}
 
-        protected override TResponse SendRequest<TRequest, TResponse>(TRequest request, string Action, SerializationFormat responseFormat, bool includeEntity = true)
+        protected override TResponse SendRequest<TRequest, TResponse>(TRequest request, string Action, SerializationFormat requestFormat, SerializationFormat responseFormat, bool includeEntity = true)
         {
             //DateTime now = DateTime.Now;
             //Log("  * {0}{1}() Starting..", Action, typeof(TEntity).Name);
@@ -82,7 +82,6 @@ namespace InABox.Clients
             }
 
             //sw.Restart();
-            var json = Serialization.Serialize(request);
             //var deserialized = Serialization.Deserialize<TRequest>(json);
             //Log("  * {0}{1}() Serializing data took {2}ms ({3} bytes)", Action, typeof(TEntity).Name, sw.ElapsedMilliseconds, json.Length);
             //sw.Restart();
@@ -90,9 +89,10 @@ namespace InABox.Clients
             var uri = new Uri(string.Format("{0}:{1}", URL, Port));
             var cli = new RestClient(uri);
             var cmd = string.Format(
-                "{0}{1}?format=json&responseFormat={2}", 
+                "{0}{1}?format={2}&responseFormat={3}", 
                 Action, 
                 includeEntity ? typeof(TEntity).Name : "",
+                requestFormat,
                 responseFormat
             );
             var req = new RestRequest(cmd, Method.POST)
@@ -125,9 +125,20 @@ namespace InABox.Clients
                 //Log("  * {0}{1}() Deserializing Stream took {2}ms ({3} bytes)", Action, typeof(TEntity).Name, sw.ElapsedMilliseconds, response.ContentLength);
             };
 
-            //result = new TResponse();
-            req.AddOrUpdateParameter("application/json; charset=utf-8", json, ParameterType.RequestBody);
-            req.RequestFormat = DataFormat.Json;
+            if(requestFormat == SerializationFormat.Binary && request is ISerializeBinary binary)
+            {
+                var data = binary.WriteBinary();
+
+                req.AddOrUpdateParameter("application/octet-stream", data, ParameterType.RequestBody);
+                req.RequestFormat = DataFormat.None;
+            }
+            else
+            {
+                var json = Serialization.Serialize(request);
+
+                req.AddOrUpdateParameter("application/json; charset=utf-8", json, ParameterType.RequestBody);
+                req.RequestFormat = DataFormat.Json;
+            }
             try
             {
                 //sw.Restart();

+ 11 - 11
InABox.Client.Remote.Shared/RemoteClient.cs

@@ -140,7 +140,7 @@ namespace InABox.Clients
                 request.Credentials.Session = session;
             }
 
-            var response = SendRequest<ValidateRequest, ValidateResponse>(request, "validate", SerializationFormat.Json, false);
+            var response = SendRequest<ValidateRequest, ValidateResponse>(request, "validate", SerializationFormat.Json, SerializationFormat.Json, false);
             if (response != null)
                 if (response.Status.Equals(StatusCode.OK))
                 {
@@ -149,7 +149,7 @@ namespace InABox.Clients
                         var notifyRequest = new NotifyRequest();
                         // Session is required so that the server can exclude any requests from bad actors
                         notifyRequest.Credentials.Session = response.Session;
-                        var notifyResponse = SendRequest<NotifyRequest, NotifyResponse>(notifyRequest, "notify", SerializationFormat.Json, false);
+                        var notifyResponse = SendRequest<NotifyRequest, NotifyResponse>(notifyRequest, "notify", SerializationFormat.Json, SerializationFormat.Json, false);
                         if(notifyResponse != null && notifyResponse.Status.Equals(StatusCode.OK))
                         {
                             if (notifyResponse.SocketPort.HasValue)
@@ -185,7 +185,7 @@ namespace InABox.Clients
             );
         }
 
-        protected abstract TResponse SendRequest<TRequest, TResponse>(TRequest request, string Action, SerializationFormat responseFormat, bool includeEntity = true)
+        protected abstract TResponse SendRequest<TRequest, TResponse>(TRequest request, string Action, SerializationFormat requestFormat, SerializationFormat responseFormat, bool includeEntity = true)
             where TRequest : Request, new() where TResponse : Response, new();
 
         #region Query Data
@@ -201,7 +201,7 @@ namespace InABox.Clients
 
             PrepareRequest(request);
 
-            var response = SendRequest<QueryRequest<TEntity>, QueryResponse<TEntity>>(request, "List", SerializationFormat.Binary);
+            var response = SendRequest<QueryRequest<TEntity>, QueryResponse<TEntity>>(request, "List", SerializationFormat.Json, SerializationFormat.Binary);
 
             if (response != null)
             {
@@ -232,7 +232,7 @@ namespace InABox.Clients
 
             PrepareRequest(request);
 
-            var response = SendRequest<QueryRequest<TEntity>, QueryResponse<TEntity>>(request, "List", SerializationFormat.Binary);
+            var response = SendRequest<QueryRequest<TEntity>, QueryResponse<TEntity>>(request, "List", SerializationFormat.Json, SerializationFormat.Binary);
             if (response.Items != null)
                 foreach (var row in response.Items.Rows)
                     result.Add(row.ToObject<TEntity>());
@@ -262,7 +262,7 @@ namespace InABox.Clients
 
             PrepareRequest(request);
 
-            var response = SendRequest<MultiQueryRequest, MultiQueryResponse>(request, "QueryMultiple", SerializationFormat.Binary, false);
+            var response = SendRequest<MultiQueryRequest, MultiQueryResponse>(request, "QueryMultiple", SerializationFormat.Json, SerializationFormat.Binary, false);
             if (response != null)
             {
                 return response.Status switch
@@ -290,7 +290,7 @@ namespace InABox.Clients
 
             PrepareRequest(request);
 
-            var response = SendRequest<SaveRequest<TEntity>, SaveResponse<TEntity>>(request, "Save", SerializationFormat.Json);
+            var response = SendRequest<SaveRequest<TEntity>, SaveResponse<TEntity>>(request, "Save", SerializationFormat.Binary, SerializationFormat.Json);
             switch (response.Status)
             {
                 case StatusCode.OK:
@@ -334,7 +334,7 @@ namespace InABox.Clients
 
             PrepareRequest(request);
 
-            var response = SendRequest<MultiSaveRequest<TEntity>, MultiSaveResponse<TEntity>>(request, "MultiSave", SerializationFormat.Json);
+            var response = SendRequest<MultiSaveRequest<TEntity>, MultiSaveResponse<TEntity>>(request, "MultiSave", SerializationFormat.Binary, SerializationFormat.Json);
             switch (response.Status)
             {
                 case StatusCode.OK:
@@ -388,7 +388,7 @@ namespace InABox.Clients
 
             PrepareRequest(request);
 
-            var response = SendRequest<DeleteRequest<TEntity>, DeleteResponse<TEntity>>(request, "Delete", SerializationFormat.Json);
+            var response = SendRequest<DeleteRequest<TEntity>, DeleteResponse<TEntity>>(request, "Delete", SerializationFormat.Json, SerializationFormat.Json);
             switch (response.Status)
             {
                 case StatusCode.OK:
@@ -409,7 +409,7 @@ namespace InABox.Clients
 
             PrepareRequest(request);
 
-            var response = SendRequest<MultiDeleteRequest<TEntity>, MultiDeleteResponse<TEntity>>(request, "MultiDelete", SerializationFormat.Json);
+            var response = SendRequest<MultiDeleteRequest<TEntity>, MultiDeleteResponse<TEntity>>(request, "MultiDelete", SerializationFormat.Json, SerializationFormat.Json);
             switch (response.Status)
             {
                 case StatusCode.OK:
@@ -431,7 +431,7 @@ namespace InABox.Clients
 
             PrepareRequest(request);
 
-            var response = SendRequest<Check2FARequest, Check2FAResponse>(request, "check_2fa", SerializationFormat.Json, false);
+            var response = SendRequest<Check2FARequest, Check2FAResponse>(request, "check_2fa", SerializationFormat.Json, SerializationFormat.Json, false);
             if (response != null)
             {
                 return response.Status switch

+ 66 - 3
InABox.Core/Client/Request.cs

@@ -6,7 +6,7 @@ using InABox.Core;
 
 namespace InABox.Clients
 {
-    public class Credentials
+    public class Credentials : ISerializeBinary
     {
         [Obsolete]
         public string? UserID { get; set; }
@@ -17,6 +17,23 @@ namespace InABox.Clients
         public string Version { get; set; }
         public Guid Session { get; set; }
 
+        public void SerializeBinary(BinaryWriter writer)
+        {
+            writer.Write(UserID ?? "");
+            writer.Write(Password ?? "");
+            writer.Write(Platform);
+            writer.Write(Version);
+            writer.Write(Session);
+        }
+
+        public void DeserializeBinary(BinaryReader reader)
+        {
+            UserID = reader.ReadString();
+            Password = reader.ReadString();
+            Platform = reader.ReadString();
+            Version = reader.ReadString();
+            Session = reader.ReadGuid();
+        }
     }
 
     public abstract class Request
@@ -31,6 +48,16 @@ namespace InABox.Clients
         public Credentials Credentials { get; set; }
 
         public abstract RequestMethod GetMethod();
+
+        public virtual void SerializeBinary(BinaryWriter writer)
+        {
+            Credentials.SerializeBinary(writer);
+        }
+
+        public virtual void DeserializeBinary(BinaryReader reader)
+        {
+            Credentials.DeserializeBinary(reader);
+        }
     }
 
     public enum StatusCode
@@ -174,7 +201,7 @@ namespace InABox.Clients
         public TEntity[] Items { get; set; }
     }*/
 
-    public class MultiSaveRequest<TEntity> : BaseRequest<TEntity> where TEntity : Entity, new()
+    public class MultiSaveRequest<TEntity> : BaseRequest<TEntity>, ISerializeBinary where TEntity : Entity, new()
     {
         public TEntity[] Items { get; set; }
         public string AuditNote { get; set; }
@@ -184,6 +211,24 @@ namespace InABox.Clients
         public bool ReturnOnlyChanged { get; set; } = false;
 
         public override RequestMethod GetMethod() => RequestMethod.MultiSave;
+
+        public override void SerializeBinary(BinaryWriter writer)
+        {
+            base.SerializeBinary(writer);
+
+            writer.WriteObjects(Items);
+            writer.Write(AuditNote);
+            writer.Write(ReturnOnlyChanged);
+        }
+
+        public override void DeserializeBinary(BinaryReader reader)
+        {
+            base.DeserializeBinary(reader);
+
+            Items = reader.ReadObjects<TEntity>().ToArray();
+            AuditNote = reader.ReadString();
+            ReturnOnlyChanged = reader.ReadBoolean();
+        }
     }
 
     public class MultiSaveResponse<TEntity> : BaseResponse<TEntity> where TEntity : Entity, new()
@@ -194,7 +239,7 @@ namespace InABox.Clients
         public List<Dictionary<string, object?>> ChangedValues { get; set; } = new List<Dictionary<string, object?>>();
     }
 
-    public class SaveRequest<TEntity> : BaseRequest<TEntity> where TEntity : Entity, new()
+    public class SaveRequest<TEntity> : BaseRequest<TEntity>, ISerializeBinary where TEntity : Entity, new()
     {
         public TEntity Item { get; set; }
         public string AuditNote { get; set; }
@@ -202,6 +247,24 @@ namespace InABox.Clients
         public bool ReturnOnlyChanged { get; set; } = false;
 
         public override RequestMethod GetMethod() => RequestMethod.Save;
+
+        public override void SerializeBinary(BinaryWriter writer)
+        {
+            base.SerializeBinary(writer);
+
+            writer.WriteObject(Item);
+            writer.Write(AuditNote);
+            writer.Write(ReturnOnlyChanged);
+        }
+
+        public override void DeserializeBinary(BinaryReader reader)
+        {
+            base.DeserializeBinary(reader);
+
+            Item = reader.ReadObject<TEntity>();
+            AuditNote = reader.ReadString();
+            ReturnOnlyChanged = reader.ReadBoolean();
+        }
     }
 
     public class SaveResponse<TEntity> : BaseResponse<TEntity> where TEntity : Entity, new()

+ 2 - 181
InABox.Core/DataTable.cs

@@ -753,98 +753,6 @@ namespace InABox.Core
 
         #region Serialize Binary
 
-        private static void WriteValue(BinaryWriter writer, Type type, object? value)
-        {
-            value ??= CoreUtils.GetDefault(type);
-            if (type == typeof(byte[]) && value is byte[] bArray)
-            {
-                writer.Write(bArray.Length);
-                writer.Write(bArray);
-            }
-            else if (type == typeof(byte[]) && value is null)
-            {
-                writer.Write(0);
-            }
-            else if (type.IsArray && value is Array array)
-            {
-                var elementType = type.GetElementType();
-                writer.Write(array.Length);
-                foreach (var val1 in array)
-                {
-                    WriteValue(writer, elementType, val1);
-                }
-            }
-            else if (type.IsArray && value is null)
-            {
-                writer.Write(0);
-            }
-            else if (type.IsEnum && value is Enum e)
-            {
-                var underlyingType = type.GetEnumUnderlyingType();
-                WriteValue(writer, underlyingType, Convert.ChangeType(e, underlyingType));
-            }
-            else if (type == typeof(bool) && value is bool b)
-            {
-                writer.Write(b);
-            }
-            else if (type == typeof(string) && value is string str)
-            {
-                writer.Write(str);
-            }
-            else if (type == typeof(string) && value is null)
-            {
-                writer.Write("");
-            }
-            else if (type == typeof(Guid) && value is Guid guid)
-            {
-                writer.Write(guid.ToByteArray());
-            }
-            else if (type == typeof(byte) && value is byte i8)
-            {
-                writer.Write(i8);
-            }
-            else if (type == typeof(Int16) && value is Int16 i16)
-            {
-                writer.Write(i16);
-            }
-            else if (type == typeof(Int32) && value is Int32 i32)
-            {
-                writer.Write(i32);
-            }
-            else if (type == typeof(Int64) && value is Int64 i64)
-            {
-                writer.Write(i64);
-            }
-            else if (type == typeof(float) && value is float f32)
-            {
-                writer.Write(f32);
-            }
-            else if (type == typeof(double) && value is double f64)
-            {
-                writer.Write(f64);
-            }
-            else if (type == typeof(DateTime) && value is DateTime date)
-            {
-                writer.Write(date.Ticks);
-            }
-            else if (type == typeof(TimeSpan) && value is TimeSpan time)
-            {
-                writer.Write(time.Ticks);
-            }
-            else if (type == typeof(LoggablePropertyAttribute))
-            {
-                writer.Write((value as LoggablePropertyAttribute)?.Format ?? "");
-            }
-            else if (typeof(IPackable).IsAssignableFrom(type) && value is IPackable pack)
-            {
-                pack.Pack(writer);
-            }
-            else
-            {
-                throw new Exception($"Invalid type; Target DataType is {type} and value DataType is {value?.GetType().ToString() ?? "null"}");
-            }
-        }
-
         public void WriteBinary(BinaryWriter writer, bool includeColumns)
         {
             writer.Write(TableName);
@@ -866,100 +774,13 @@ namespace InABox.Core
                 foreach (var col in Columns)
                 {
                     var val = row[col.ColumnName];
-                    WriteValue(writer, col.DataType, val);
+                    writer.WriteBinaryValue(col.DataType, val);
                 }
             }
         }
 
         public void SerializeBinary(BinaryWriter writer) => WriteBinary(writer, true);
 
-        private static object? ReadValue(BinaryReader reader, Type type)
-        {
-            if (type == typeof(byte[]))
-            {
-                var length = reader.ReadInt32();
-                return reader.ReadBytes(length);
-            }
-            else if (type.IsArray)
-            {
-                var length = reader.ReadInt32();
-                var elementType = type.GetElementType();
-
-                var array = Array.CreateInstance(elementType, length);
-                for (int i = 0; i < array.Length; ++i)
-                {
-                    array.SetValue(ReadValue(reader, elementType), i);
-                }
-                return array;
-            }
-            else if (type.IsEnum)
-            {
-                var val = ReadValue(reader, type.GetEnumUnderlyingType());
-                return Enum.ToObject(type, val);
-            }
-            else if (type == typeof(bool))
-            {
-                return reader.ReadBoolean();
-            }
-            else if (type == typeof(string))
-            {
-                return reader.ReadString();
-            }
-            else if (type == typeof(Guid))
-            {
-                return new Guid(reader.ReadBytes(16));
-            }
-            else if (type == typeof(byte))
-            {
-                return reader.ReadByte();
-            }
-            else if (type == typeof(Int16))
-            {
-                return reader.ReadInt16();
-            }
-            else if (type == typeof(Int32))
-            {
-                return reader.ReadInt32();
-            }
-            else if (type == typeof(Int64))
-            {
-                return reader.ReadInt64();
-            }
-            else if (type == typeof(float))
-            {
-                return reader.ReadSingle();
-            }
-            else if (type == typeof(double))
-            {
-                return reader.ReadDouble();
-            }
-            else if (type == typeof(DateTime))
-            {
-                return new DateTime(reader.ReadInt64());
-            }
-            else if (type == typeof(TimeSpan))
-            {
-                return new TimeSpan(reader.ReadInt64());
-            }
-            else if (type == typeof(LoggablePropertyAttribute))
-            {
-                String format = reader.ReadString();
-                return String.IsNullOrWhiteSpace(format)
-                    ? null
-                    : new LoggablePropertyAttribute() { Format = format };
-            }
-            else if (typeof(IPackable).IsAssignableFrom(type))
-            {
-                var packable = (Activator.CreateInstance(type) as IPackable)!;
-                packable.Unpack(reader);
-                return packable;
-            }
-            else
-            {
-                throw new Exception($"Invalid type; Target DataType is {type}");
-            }
-        }
-
         public void ReadBinary(BinaryReader reader, IList<CoreColumn>? columns)
         {
             tableName = reader.ReadString();
@@ -989,7 +810,7 @@ namespace InABox.Core
                 var row = NewRow();
                 foreach (var column in Columns)
                 {
-                    var value = ReadValue(reader, column.DataType);
+                    var value = reader.ReadBinaryValue(column.DataType);
                     row.Values.Add(value);
                 }
                 Rows.Add(row);

+ 1 - 0
InABox.Core/DatabaseSchema/DatabaseSchema.cs

@@ -68,6 +68,7 @@ namespace InABox.Core
                 var properties = CoreUtils.PropertyList(
                     type,
                     x => !x.PropertyType.IsInterface &&
+                        x.GetGetMethod()?.IsPublic == true &&
                         (x.DeclaringType.IsSubclassOf(typeof(BaseObject)) 
                         || x.DeclaringType.IsSubclassOf(typeof(BaseEditor)))
                 ); //.OrderBy(x=>x.Name);

+ 2 - 0
InABox.Core/DigitalForms/Layouts/Controls/DFLayoutLabel/DFLayoutTextStyle.cs

@@ -69,10 +69,12 @@ namespace InABox.Core
         
         [JsonIgnore]
         [NullEditor]
+        [DoNotSerialize]
         public Color ForegroundColour { get => GetForegroundColour(); set => SetForegroundColour(value); }
 
         [JsonIgnore]
         [NullEditor]
+        [DoNotSerialize]
         public Color BackgroundColour { get => GetBackgroundColour(); set => SetBackgroundColour(value); }
         
         protected override void Init()

+ 370 - 0
InABox.Core/Serialization.cs

@@ -2,10 +2,15 @@
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices.ComTypes;
 using System.Threading;
+using System.Xml.Linq;
 using InABox.Clients;
 using JetBrains.Annotations;
 using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
 
 namespace InABox.Core
 {
@@ -214,4 +219,369 @@ namespace InABox.Core
 
         #endregion
     }
+
+    public static class SerializationUtils
+    {
+        public static void Write(this BinaryWriter writer, Guid guid)
+        {
+            writer.Write(guid.ToByteArray());
+        }
+        public static Guid ReadGuid(this BinaryReader reader)
+        {
+            return new Guid(reader.ReadBytes(16));
+        }
+
+        /// <summary>
+        /// Binary serialize a bunch of different types of values. <see cref="WriteBinaryValue(BinaryWriter, Type, object?)"/> and
+        /// <see cref="ReadBinaryValue(BinaryReader, Type)"/> are inverses of each other.
+        /// </summary>
+        /// <remarks>
+        /// Handles <see cref="byte"/>[], <see cref="Array"/>s of serialisable values, <see cref="Enum"/>, <see cref="bool"/>, <see cref="string"/>,
+        /// <see cref="Guid"/>, <see cref="byte"/>, <see cref="Int16"/>, <see cref="Int32"/>, <see cref="Int64"/>, <see cref="float"/>, <see cref="double"/>,
+        /// <see cref="DateTime"/>, <see cref="TimeSpan"/>, <see cref="LoggablePropertyAttribute"/>, <see cref="IPackable"/>, <see cref="Nullable{T}"/>
+        /// and <see cref="ISerializeBinary"/>.
+        /// </remarks>
+        /// <param name="writer"></param>
+        /// <param name="type"></param>
+        /// <param name="value"></param>
+        /// <exception cref="Exception">If an object of <paramref name="type"/> is unable to be serialized.</exception>
+        public static void WriteBinaryValue(this BinaryWriter writer, Type type, object? value)
+        {
+            value ??= CoreUtils.GetDefault(type);
+            if (type == typeof(byte[]) && value is byte[] bArray)
+            {
+                writer.Write(bArray.Length);
+                writer.Write(bArray);
+            }
+            else if (type == typeof(byte[]) && value is null)
+            {
+                writer.Write(0);
+            }
+            else if (type.IsArray && value is Array array)
+            {
+                var elementType = type.GetElementType();
+                writer.Write(array.Length);
+                foreach (var val1 in array)
+                {
+                    WriteBinaryValue(writer, elementType, val1);
+                }
+            }
+            else if (type.IsArray && value is null)
+            {
+                writer.Write(0);
+            }
+            else if (type.IsEnum && value is Enum e)
+            {
+                var underlyingType = type.GetEnumUnderlyingType();
+                WriteBinaryValue(writer, underlyingType, Convert.ChangeType(e, underlyingType));
+            }
+            else if (type == typeof(bool) && value is bool b)
+            {
+                writer.Write(b);
+            }
+            else if (type == typeof(string) && value is string str)
+            {
+                writer.Write(str);
+            }
+            else if (type == typeof(string) && value is null)
+            {
+                writer.Write("");
+            }
+            else if (type == typeof(Guid) && value is Guid guid)
+            {
+                writer.Write(guid);
+            }
+            else if (type == typeof(byte) && value is byte i8)
+            {
+                writer.Write(i8);
+            }
+            else if (type == typeof(Int16) && value is Int16 i16)
+            {
+                writer.Write(i16);
+            }
+            else if (type == typeof(Int32) && value is Int32 i32)
+            {
+                writer.Write(i32);
+            }
+            else if (type == typeof(Int64) && value is Int64 i64)
+            {
+                writer.Write(i64);
+            }
+            else if (type == typeof(float) && value is float f32)
+            {
+                writer.Write(f32);
+            }
+            else if (type == typeof(double) && value is double f64)
+            {
+                writer.Write(f64);
+            }
+            else if (type == typeof(DateTime) && value is DateTime date)
+            {
+                writer.Write(date.Ticks);
+            }
+            else if (type == typeof(TimeSpan) && value is TimeSpan time)
+            {
+                writer.Write(time.Ticks);
+            }
+            else if (type == typeof(LoggablePropertyAttribute))
+            {
+                writer.Write((value as LoggablePropertyAttribute)?.Format ?? "");
+            }
+            else if (typeof(IPackable).IsAssignableFrom(type) && value is IPackable pack)
+            {
+                pack.Pack(writer);
+            }
+            else if (typeof(ISerializeBinary).IsAssignableFrom(type) && value is ISerializeBinary binary)
+            {
+                binary.SerializeBinary(writer);
+            }
+            else if (Nullable.GetUnderlyingType(type) is Type t)
+            {
+                if(value == null)
+                {
+                    writer.Write(false);
+                }
+                else
+                {
+                    writer.Write(true);
+                    writer.WriteBinaryValue(t, value);
+                }
+            }
+            else
+            {
+                throw new Exception($"Invalid type; Target DataType is {type} and value DataType is {value?.GetType().ToString() ?? "null"}");
+            }
+        }
+
+        /// <summary>
+        /// Binary deserialize a bunch of different types of values. <see cref="WriteBinaryValue(BinaryWriter, Type, object?)"/> and
+        /// <see cref="ReadBinaryValue(BinaryReader, Type)"/> are inverses of each other.
+        /// </summary>
+        /// <remarks>
+        /// Handles <see cref="byte"/>[], <see cref="Array"/>s of serialisable values, <see cref="Enum"/>, <see cref="bool"/>, <see cref="string"/>,
+        /// <see cref="Guid"/>, <see cref="byte"/>, <see cref="Int16"/>, <see cref="Int32"/>, <see cref="Int64"/>, <see cref="float"/>, <see cref="double"/>,
+        /// <see cref="DateTime"/>, <see cref="TimeSpan"/>, <see cref="LoggablePropertyAttribute"/>, <see cref="IPackable"/>, <see cref="Nullable{T}"/>
+        /// and <see cref="ISerializeBinary"/>.
+        /// </remarks>
+        /// <param name="writer"></param>
+        /// <param name="type"></param>
+        /// <param name="value"></param>
+        /// <exception cref="Exception">If an object of <paramref name="type"/> is unable to be deserialized.</exception>
+        public static object? ReadBinaryValue(this BinaryReader reader, Type type)
+        {
+            if (type == typeof(byte[]))
+            {
+                var length = reader.ReadInt32();
+                return reader.ReadBytes(length);
+            }
+            else if (type.IsArray)
+            {
+                var length = reader.ReadInt32();
+                var elementType = type.GetElementType();
+
+                var array = Array.CreateInstance(elementType, length);
+                for (int i = 0; i < array.Length; ++i)
+                {
+                    array.SetValue(ReadBinaryValue(reader, elementType), i);
+                }
+                return array;
+            }
+            else if (type.IsEnum)
+            {
+                var val = ReadBinaryValue(reader, type.GetEnumUnderlyingType());
+                return Enum.ToObject(type, val);
+            }
+            else if (type == typeof(bool))
+            {
+                return reader.ReadBoolean();
+            }
+            else if (type == typeof(string))
+            {
+                return reader.ReadString();
+            }
+            else if (type == typeof(Guid))
+            {
+                return reader.ReadGuid();
+            }
+            else if (type == typeof(byte))
+            {
+                return reader.ReadByte();
+            }
+            else if (type == typeof(Int16))
+            {
+                return reader.ReadInt16();
+            }
+            else if (type == typeof(Int32))
+            {
+                return reader.ReadInt32();
+            }
+            else if (type == typeof(Int64))
+            {
+                return reader.ReadInt64();
+            }
+            else if (type == typeof(float))
+            {
+                return reader.ReadSingle();
+            }
+            else if (type == typeof(double))
+            {
+                return reader.ReadDouble();
+            }
+            else if (type == typeof(DateTime))
+            {
+                return new DateTime(reader.ReadInt64());
+            }
+            else if (type == typeof(TimeSpan))
+            {
+                return new TimeSpan(reader.ReadInt64());
+            }
+            else if (type == typeof(LoggablePropertyAttribute))
+            {
+                String format = reader.ReadString();
+                return String.IsNullOrWhiteSpace(format)
+                    ? null
+                    : new LoggablePropertyAttribute() { Format = format };
+            }
+            else if (typeof(IPackable).IsAssignableFrom(type))
+            {
+                var packable = (Activator.CreateInstance(type) as IPackable)!;
+                packable.Unpack(reader);
+                return packable;
+            }
+            else if (typeof(ISerializeBinary).IsAssignableFrom(type))
+            {
+                var obj = (Activator.CreateInstance(type) as ISerializeBinary)!;
+                obj.DeserializeBinary(reader);
+                return obj;
+            }
+            else if (Nullable.GetUnderlyingType(type) is Type t)
+            {
+                var isNull = reader.ReadBoolean();
+                if (isNull)
+                {
+                    return null;
+                }
+                else
+                {
+                    return reader.ReadBinaryValue(t);
+                }
+            }
+            else
+            {
+                throw new Exception($"Invalid type; Target DataType is {type}");
+            }
+        }
+
+        public static IEnumerable<IProperty> SerializableProperties(Type type) =>
+            DatabaseSchema.Properties(type)
+                .Where(x => !(x is StandardProperty st) || st.Property.GetCustomAttribute<DoNotSerialize>() == null);
+
+        /// <summary>
+        /// An implementation of binary serialising a <typeparamref name="TObject"/>; this is the inverse of <see cref="ReadObject{TObject}(BinaryReader)"/>.
+        /// </summary>
+        /// <remarks>
+        /// Also serialises the names of properties along with the values.
+        /// </remarks>
+        /// <typeparam name="TObject"></typeparam>
+        /// <param name="writer"></param>
+        /// <param name="entity"></param>
+        public static void WriteObject<TObject>(this BinaryWriter writer, TObject entity)
+            where TObject : BaseObject, new()
+        {
+            var properties = SerializableProperties(typeof(TObject)).ToList();
+
+            writer.Write(properties.Count);
+            foreach (var property in properties)
+            {
+                writer.Write(property.Name);
+                writer.WriteBinaryValue(property.PropertyType, property.Getter()(entity));
+            }
+        }
+
+        /// <summary>
+        /// The inverse of <see cref="WriteObject{TObject}(BinaryWriter, TObject)"/>.
+        /// </summary>
+        /// <typeparam name="TObject"></typeparam>
+        /// <param name="reader"></param>
+        /// <returns></returns>
+        public static TObject ReadObject<TObject>(this BinaryReader reader)
+            where TObject : BaseObject, new()
+        {
+            var obj = new TObject();
+
+            var nProps = reader.ReadInt32();
+            for(int i = 0; i < nProps; ++i)
+            {
+                var propName = reader.ReadString();
+                var property = DatabaseSchema.Property(typeof(TObject), propName);
+                property?.Setter()(obj, reader.ReadBinaryValue(property.PropertyType));
+            }
+            return obj;
+        }
+
+        /// <summary>
+        /// An implementation of binary serialising multiple <typeparamref name="TObject"/>s;
+        /// this is the inverse of <see cref="ReadObjects{TObject}(BinaryReader)"/>.
+        /// </summary>
+        /// <remarks>
+        /// Also serialises the names of properties along with the values.
+        /// </remarks>
+        /// <typeparam name="TObject"></typeparam>
+        /// <param name="writer"></param>
+        /// <param name="entity"></param>
+        public static void WriteObjects<TObject>(this BinaryWriter writer, ICollection<TObject> objects)
+            where TObject : BaseObject, new()
+        {
+            var properties = SerializableProperties(typeof(TObject)).ToList();
+
+            writer.Write(objects.Count);
+            writer.Write(properties.Count);
+            foreach (var property in properties)
+            {
+                writer.Write(property.Name);
+            }
+
+            foreach (var obj in objects)
+            {
+                foreach (var property in properties)
+                {
+                    writer.WriteBinaryValue(property.PropertyType, property.Getter()(obj));
+                }
+            }
+        }
+
+        /// <summary>
+        /// The inverse of <see cref="WriteObjects{TObject}(BinaryWriter, IList{TObject})"/>.
+        /// </summary>
+        /// <typeparam name="TObject"></typeparam>
+        /// <param name="reader"></param>
+        /// <returns></returns>
+        public static List<TObject> ReadObjects<TObject>(this BinaryReader reader)
+            where TObject : BaseObject, new()
+        {
+            var objs = new List<TObject>();
+            var properties = new List<IProperty>();
+
+            var nObjs = reader.ReadInt32();
+            var nProps = reader.ReadInt32();
+            for(int i = 0; i < nProps; ++i)
+            {
+                var property = reader.ReadString();
+                properties.Add(DatabaseSchema.Property(typeof(TObject), property));
+            }
+
+            for(int i = 0; i < nObjs; ++i)
+            {
+                var obj = new TObject();
+
+                foreach(var property in properties)
+                {
+                    property?.Setter()(obj, reader.ReadBinaryValue(property.PropertyType));
+                }
+
+                objs.Add(obj);
+            }
+            return objs;
+        }
+    }
 }

+ 58 - 36
InABox.Server/Rest/RestListener.cs

@@ -1,6 +1,4 @@
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Net;
+using System.Net;
 using System.Reflection;
 using System.Security.Cryptography.X509Certificates;
 using GenHTTP.Api.Content;
@@ -17,11 +15,7 @@ using InABox.Database;
 using InABox.Remote.Shared;
 using InABox.Server.WebSocket;
 using InABox.WebSocket.Shared;
-using NPOI.SS.Formula.Functions;
-using NPOI.XSSF.Streaming.Values;
-using Twilio.Rest.Taskrouter.V1.Workspace.TaskQueue;
 using RequestMethod = GenHTTP.Api.Protocol.RequestMethod;
-using StreamContent = GenHTTP.Modules.IO.Streaming.StreamContent;
 
 namespace InABox.API
 {
@@ -63,6 +57,16 @@ namespace InABox.API
             endpoints.Add("QueryMultiple");
         }
         
+        private RequestData GetRequestData(IRequest request)
+        {
+            var data = new RequestData();
+            if (request.Query.TryGetValue("format", out var formatString) && Enum.TryParse<SerializationFormat>(formatString, out var format))
+            {
+                data.RequestFormat = format;
+            }
+            return data;
+        }
+
         /// <summary>
         /// The main handler for the server; an HTTP request comes in, an HTTP response goes out.
         /// </summary>
@@ -102,11 +106,12 @@ namespace InABox.API
                         var target = request.Target.Current;
                         if (target is not null)
                         {
+                            var data = GetRequestData(request);
                             return target.Value switch
                             {
-                                "validate" => new ValueTask<IResponse?>(Validate(request).Build()),
-                                "check_2fa" => new ValueTask<IResponse?>(Check2FA(request).Build()),
-                                "notify" => new ValueTask<IResponse?>(GetNotify(request).Build()),
+                                "validate" => new ValueTask<IResponse?>(Validate(request, data).Build()),
+                                "check_2fa" => new ValueTask<IResponse?>(Check2FA(request, data).Build()),
+                                "notify" => new ValueTask<IResponse?>(GetNotify(request, data).Build()),
                                 _ => HandleDatabaseRequest(request),
                             };
                         }
@@ -167,9 +172,9 @@ namespace InABox.API
         /// </summary>
         /// <param name="request"></param>
         /// <returns></returns>
-        private IResponseBuilder GetNotify(IRequest request)
+        private IResponseBuilder GetNotify(IRequest request, RequestData data)
         {
-            var requestObj = Deserialize<NotifyRequest>(request.Content, true);
+            var requestObj = Deserialize<NotifyRequest>(request.Content, data.RequestFormat, true);
             if (!CredentialsCache.SessionExists(requestObj.Credentials.Session))
             {
                 return request.Respond().Status(ResponseStatus.NotFound);
@@ -187,9 +192,9 @@ namespace InABox.API
 
         #region Authentication
 
-        private IResponseBuilder Validate(IRequest request)
+        private IResponseBuilder Validate(IRequest request, RequestData data)
         {
-            var requestObj = Deserialize<ValidateRequest>(request.Content, true);
+            var requestObj = Deserialize<ValidateRequest>(request.Content, data.RequestFormat, true);
             var response = RestService.Validate(requestObj);
 
             var serialized = Serialization.Serialize(response);
@@ -198,9 +203,9 @@ namespace InABox.API
                 .Content(new ResourceContent(Resource.FromString(serialized).Build()));
         }
 
-        private IResponseBuilder Check2FA(IRequest request)
+        private IResponseBuilder Check2FA(IRequest request, RequestData data)
         {
-            var requestObj = Deserialize<Check2FARequest>(request.Content, true);
+            var requestObj = Deserialize<Check2FARequest>(request.Content, data.RequestFormat, true);
             var response = RestService.Check2FA(requestObj);
 
             var serialized = Serialization.Serialize(response);
@@ -226,39 +231,57 @@ namespace InABox.API
             new("MultiDelete", GetMethod(nameof(MultiDelete)))
         };
 
-        private static QueryResponse<T> List<T>(IRequest request) where T : Entity, new()
+        private class RequestData
         {
-            var requestObject = Deserialize<QueryRequest<T>>(request.Content, true);
+            public SerializationFormat RequestFormat { get; set; }
+        }
+
+        private static QueryResponse<T> List<T>(IRequest request, RequestData data) where T : Entity, new()
+        {
+            var requestObject = Deserialize<QueryRequest<T>>(request.Content, data.RequestFormat, true);
             return RestService<T>.List(requestObject);
         }
-        private static SaveResponse<T> Save<T>(IRequest request) where T : Entity, new()
+        private static SaveResponse<T> Save<T>(IRequest request, RequestData data) where T : Entity, new()
         {
-            var requestObject = Deserialize<SaveRequest<T>>(request.Content, true);
+            var requestObject = Deserialize<SaveRequest<T>>(request.Content, data.RequestFormat, true);
             return RestService<T>.Save(requestObject);
         }
-        private static DeleteResponse<T> Delete<T>(IRequest request) where T : Entity, new()
+        private static DeleteResponse<T> Delete<T>(IRequest request, RequestData data) where T : Entity, new()
         {
-            var requestObject = Deserialize<DeleteRequest<T>>(request.Content, true);
+            var requestObject = Deserialize<DeleteRequest<T>>(request.Content, data.RequestFormat, true);
             return RestService<T>.Delete(requestObject);
         }
-        private static MultiSaveResponse<T> MultiSave<T>(IRequest request) where T : Entity, new()
+        private static MultiSaveResponse<T> MultiSave<T>(IRequest request, RequestData data) where T : Entity, new()
         {
-            var requestObject = Deserialize<MultiSaveRequest<T>>(request.Content, true);
+            var requestObject = Deserialize<MultiSaveRequest<T>>(request.Content, data.RequestFormat, true);
             return RestService<T>.MultiSave(requestObject);
         }
-        private static MultiDeleteResponse<T> MultiDelete<T>(IRequest request) where T : Entity, new()
+        private static MultiDeleteResponse<T> MultiDelete<T>(IRequest request, RequestData data) where T : Entity, new()
         {
-            var requestObject = Deserialize<MultiDeleteRequest<T>>(request.Content, true);
+            var requestObject = Deserialize<MultiDeleteRequest<T>>(request.Content, data.RequestFormat, true);
             return RestService<T>.MultiDelete(requestObject);
         }
+        private static MultiQueryResponse QueryMultiple(IRequest request, RequestData data)
+        {
+            var requestObject = Deserialize<MultiQueryRequest>(request.Content, data.RequestFormat, true);
 
-        private static T Deserialize<T>(Stream? stream, bool strict = false)
+            return RestService.QueryMultiple(requestObject, false);
+        }
+
+        private static T Deserialize<T>(Stream? stream, SerializationFormat requestFormat, bool strict = false)
         {
             if (stream is null)
                 throw new Exception("Stream is null");
-            var str = new StreamReader(stream).ReadToEnd();
-            return Serialization.Deserialize<T>(str, strict)
-                ?? throw new Exception("Deserialization failed");
+            if (requestFormat == SerializationFormat.Binary && typeof(T).IsAssignableTo(typeof(ISerializeBinary)))
+            {
+                return (T)Serialization.ReadBinary(typeof(T), stream);
+            }
+            else
+            {
+                var str = new StreamReader(stream).ReadToEnd();
+                return Serialization.Deserialize<T>(str, strict)
+                    ?? throw new Exception("Deserialization failed");
+            }
         }
 
         private IResponseBuilder SerializeResponse(IRequest request, SerializationFormat responseFormat, Response? result)
@@ -297,13 +320,12 @@ namespace InABox.API
                 responseFormat = format;
             }
 
-            var endpoint = request.Target.Current.Value;
+            var requestData = GetRequestData(request);
+
+            var endpoint = request.Target.Current?.Value ?? "";
             if (endpoint.StartsWith("QueryMultiple"))
             {
-                var requestObject = Deserialize<MultiQueryRequest>(request.Content, true);
-
-                var result = RestService.QueryMultiple(requestObject, false);
-
+                var result = QueryMultiple(request, requestData);
                 return new ValueTask<IResponse?>(SerializeResponse(request, responseFormat, result).Build());
             }
 
@@ -322,7 +344,7 @@ namespace InABox.API
                         }
 
                         var resolvedMethod = method.MakeGenericMethod(entityType);
-                        var result = resolvedMethod.Invoke(null, new object[] { request }) as Response;
+                        var result = resolvedMethod.Invoke(null, new object[] { request, requestData }) as Response;
 
                         return new ValueTask<IResponse?>(SerializeResponse(request, responseFormat, result).Build());
                     }