/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for an <see cref="EnumSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> if <paramref name="schema" /> /// is an <see cref="EnumSchema" />; an unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> is an enum type and <paramref name="schema" /> /// does not have a matching symbol for each member or when <paramref name="type" /> cannot /// be converted to <see cref="string" />. /// </exception> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema is EnumSchema enumSchema) { var writeInteger = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteInteger), new[] { typeof(long) }); var underlying = Nullable.GetUnderlyingType(type) ?? type; // enum fields will always be public static, so no need to expose binding flags: var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static); var symbols = enumSchema.Symbols.ToList(); var cases = type.IsEnum ? fields .Select(field => { var index = symbols.FindIndex(symbol => IsMatch(symbol, field)); if (index < 0) { throw new UnsupportedTypeException(type, $"{type} has a field {field.Name} that cannot be serialized."); } if (symbols.FindLastIndex(symbol => IsMatch(symbol, field)) != index) { throw new UnsupportedTypeException(type, $"{type} has an ambiguous field {field.Name}."); } return(Expression.SwitchCase( Expression.Call(context.Writer, writeInteger, Expression.Constant((long)index)), Expression.Constant(Enum.Parse(type, field.Name)))); }) : symbols .Select((symbol, index) => { return(Expression.SwitchCase( Expression.Call(context.Writer, writeInteger, Expression.Constant((long)index)), Expression.Constant(symbol))); }); var exceptionConstructor = typeof(ArgumentOutOfRangeException) .GetConstructor(new[] { typeof(string) }); var exception = Expression.New(exceptionConstructor, Expression.Constant("Enum value out of range.")); var intermediate = Expression.Variable(type.IsEnum ? type : typeof(string)); return(BinarySerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { intermediate }, Expression.Assign(intermediate, BuildConversion(value, intermediate.Type)), Expression.Switch(intermediate, Expression.Throw(exception), cases.ToArray())))); } else { return(BinarySerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryEnumSerializerBuilderCase)} can only be applied to {nameof(EnumSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for a <see cref="DurationLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="DurationLogicalType" />; an unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedSchemaException"> /// Thrown when <paramref name="schema" /> is not a <see cref="FixedSchema" /> with size /// <c>12</c>. /// </exception> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be converted to <see cref="TimeSpan" />. /// </exception> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema.LogicalType is DurationLogicalType) { if (!(schema is FixedSchema fixedSchema && fixedSchema.Size == 12)) { throw new UnsupportedSchemaException(schema); } Expression Write(Expression value) { var getBytes = typeof(BitConverter) .GetMethod(nameof(BitConverter.GetBytes), new[] { value.Type }); Expression bytes = Expression.Call(null, getBytes, value); if (!BitConverter.IsLittleEndian) { var buffer = Expression.Variable(bytes.Type); var reverse = typeof(Array) .GetMethod(nameof(Array.Reverse), new[] { bytes.Type }); bytes = Expression.Block( new[] { buffer }, Expression.Assign(buffer, bytes), Expression.Call(null, reverse, buffer), buffer); } var writeFixed = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteFixed), new[] { bytes.Type }); return(Expression.Call(context.Writer, writeFixed, bytes)); } var totalDays = typeof(TimeSpan).GetProperty(nameof(TimeSpan.TotalDays)); var totalMs = typeof(TimeSpan).GetProperty(nameof(TimeSpan.TotalMilliseconds)); return(BinarySerializerBuilderCaseResult.FromExpression( Expression.Block( Write(Expression.Constant(0U)), Write( Expression.ConvertChecked(Expression.Property(value, totalDays), typeof(uint))), Write( Expression.ConvertChecked( Expression.Subtract( Expression.Convert(Expression.Property(value, totalMs), typeof(ulong)), Expression.Multiply( Expression.Convert(Expression.Property(value, totalDays), typeof(ulong)), Expression.Constant(86400000UL))), typeof(uint)))))); } else { return(BinarySerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryDurationSerializerBuilderCase)} can only be applied schemas with a {nameof(DurationLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for a <see cref="NullSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="NullSchema" />; an unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema is NullSchema) { return(BinarySerializerBuilderCaseResult.FromExpression(Expression.Empty())); } else { return(BinarySerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryNullSerializerBuilderCase)} can only be applied to {nameof(NullSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for a <see cref="TimestampLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="TimestampLogicalType" />; an unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedSchemaException"> /// Thrown when <paramref name="schema" /> is not a <see cref="LongSchema" /> or when /// <paramref name="schema" /> does not have a <see cref="MicrosecondTimestampLogicalType" /> /// or a <see cref="MillisecondTimestampLogicalType" />. /// </exception> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be converted to <see cref="DateTimeOffset" />. /// </exception> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema.LogicalType is TimestampLogicalType) { if (schema is not LongSchema) { throw new UnsupportedSchemaException(schema); } Expression expression; try { expression = BuildConversion(value, typeof(DateTimeOffset)); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception); } var factor = schema.LogicalType switch { MicrosecondTimestampLogicalType => TimeSpan.TicksPerMillisecond / 1000, MillisecondTimestampLogicalType => TimeSpan.TicksPerMillisecond, _ => throw new UnsupportedSchemaException(schema, $"{schema.LogicalType} is not a supported {nameof(TimestampLogicalType)}."), }; var utcTicks = typeof(DateTimeOffset) .GetProperty(nameof(DateTimeOffset.UtcTicks)); var writeInteger = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteInteger), new[] { typeof(long) }); // return writer.WriteInteger((value.UtcTicks - epoch) / factor); return(BinarySerializerBuilderCaseResult.FromExpression( Expression.Call( context.Writer, writeInteger, Expression.Divide( Expression.Subtract( Expression.Property(expression, utcTicks), Expression.Constant(Epoch.Ticks)), Expression.Constant(factor))))); } else { return(BinarySerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryTimestampSerializerBuilderCase)} can only be applied schemas with a {nameof(TimestampLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for a <see cref="StringSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="StringSchema" />; an unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be converted to <see cref="string" />. /// </exception> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema is StringSchema stringSchema) { var writeString = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteString), new[] { typeof(string) }); try { return(BinarySerializerBuilderCaseResult.FromExpression( Expression.Call(context.Writer, writeString, BuildConversion(value, typeof(string))))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {stringSchema} to {type}.", exception); } } else { return(BinarySerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryStringSerializerBuilderCase)} can only be applied to {nameof(StringSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for an <see cref="FixedSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> <paramref name="schema" /> /// is a <see cref="FixedSchema" />; an unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be converted to <see cref="T:System.Byte[]" />. /// </exception> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema is FixedSchema fixedSchema) { Expression expression; try { expression = BuildConversion(value, typeof(byte[])); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {fixedSchema} to {type}.", exception); } var bytes = Expression.Parameter(typeof(byte[])); var exceptionConstructor = typeof(OverflowException) .GetConstructor(new[] { typeof(string) }); var writeFixed = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteFixed), new[] { typeof(byte[]) }); return(BinarySerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { bytes }, Expression.Assign(bytes, expression), Expression.IfThen( Expression.NotEqual(Expression.ArrayLength(bytes), Expression.Constant(fixedSchema.Size)), Expression.Throw(Expression.New(exceptionConstructor, Expression.Constant($"Only arrays of size {fixedSchema.Size} can be serialized to {fixedSchema}.")))), Expression.Call(context.Writer, writeFixed, bytes)))); } else { return(BinarySerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryFixedSerializerBuilderCase)} can only be applied to {nameof(FixedSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for an <see cref="MapSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> if <paramref name="type" /> /// is a dictionary type and <paramref name="schema" /> is a <see cref="MapSchema" />; an /// unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> does not implement <see cref="T:System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair`2}" />. /// </exception> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema is MapSchema mapSchema) { var dictionaryTypes = GetDictionaryTypes(type); if (dictionaryTypes is not null || type == typeof(object)) { // support dynamic mapping: var keyType = dictionaryTypes?.Key ?? typeof(object); var valueType = dictionaryTypes?.Value ?? typeof(object); var pairType = typeof(KeyValuePair <,>).MakeGenericType(keyType, valueType); var collection = Expression.Variable(typeof(ICollection <>).MakeGenericType(pairType)); var enumerable = Expression.Variable(typeof(IEnumerable <>).MakeGenericType(pairType)); var enumerator = Expression.Variable(typeof(IEnumerator <>).MakeGenericType(pairType)); var loop = Expression.Label(); var getCount = collection.Type .GetProperty("Count"); var getEnumerator = enumerable.Type .GetMethod("GetEnumerator", Type.EmptyTypes); var getCurrent = enumerator.Type .GetProperty(nameof(IEnumerator.Current)); var getKey = pairType .GetProperty("Key") .GetGetMethod(); var getValue = pairType .GetProperty("Value") .GetGetMethod(); var moveNext = typeof(IEnumerator) .GetMethod(nameof(IEnumerator.MoveNext), Type.EmptyTypes); var writeInteger = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteInteger), new[] { typeof(long) }); var writeKey = SerializerBuilder .BuildExpression(Expression.Property(Expression.Property(enumerator, getCurrent), getKey), new StringSchema(), context); var writeValue = SerializerBuilder .BuildExpression(Expression.Property(Expression.Property(enumerator, getCurrent), getValue), mapSchema.Value, context); var dispose = typeof(IDisposable) .GetMethod(nameof(IDisposable.Dispose), Type.EmptyTypes); Expression expression; try { expression = BuildConversion(value, collection.Type); } catch (Exception exception) { throw new UnsupportedTypeException(type, $"Failed to map {mapSchema} to {type}.", exception); } // if (collection.Count > 0) // { // writer.WriteInteger((long)collection.Count); // // var enumerator = collection.GetEnumerator(); // // try // { // // primitive foreach: // loop: while (true) // { // if (enumerator.MoveNext()) // { // ... // } // else // { // break loop; // } // } // } // finally // { // enumerator.Dispose(); // } // } // // // write closing block: // writer.WriteInteger(0L); return(BinarySerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { collection, enumerator }, Expression.Assign(collection, expression), Expression.IfThen( Expression.GreaterThan( Expression.Property(collection, getCount), Expression.Constant(0)), Expression.Block( Expression.Call( context.Writer, writeInteger, Expression.Convert( Expression.Property(collection, getCount), typeof(long))), Expression.Assign( enumerator, Expression.Call(collection, getEnumerator)), Expression.TryFinally( Expression.Loop( Expression.IfThenElse( Expression.Call(enumerator, moveNext), Expression.Block(writeKey, writeValue), Expression.Break(loop)), loop), Expression.Call(enumerator, dispose)))), Expression.Call( context.Writer, writeInteger, Expression.Constant(0L))))); } else { return(BinarySerializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(BinaryMapSerializerBuilderCase)} can only be applied to dictionary types."))); } }
/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for a <see cref="UnionSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="UnionSchema" />; an unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedSchemaException"> /// Thrown when <paramref name="schema" /> has no <see cref="UnionSchema.Schemas" />. /// </exception> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be mapped to at least one <see cref="Schema" /> /// in <paramref name="schema" />. /// </exception> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema is UnionSchema unionSchema) { if (unionSchema.Schemas.Count < 1) { throw new UnsupportedSchemaException(schema, "A serializer cannot be built for an empty union."); } var schemas = unionSchema.Schemas.ToList(); var candidates = schemas.Where(s => s is not NullSchema).ToList(); var @null = schemas.Find(s => s is NullSchema); var writeInteger = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteInteger), new[] { typeof(long) }); Expression expression; // if there are non-null schemas, select the first matching one for each possible type: if (candidates.Count > 0) { var cases = new Dictionary <Type, Expression>(); var exceptions = new List <Exception>(); foreach (var candidate in candidates) { var selected = SelectType(type, candidate); if (cases.ContainsKey(selected)) { continue; } var underlying = Nullable.GetUnderlyingType(selected) ?? selected; Expression body; try { body = Expression.Block( Expression.Call( context.Writer, writeInteger, Expression.Constant((long)schemas.IndexOf(candidate))), SerializerBuilder.BuildExpression(Expression.Convert(value, underlying), candidate, context)); } catch (Exception exception) { exceptions.Add(exception); continue; } if (@null != null && !(selected.IsValueType && Nullable.GetUnderlyingType(selected) == null)) { body = Expression.IfThenElse( Expression.Equal(value, Expression.Constant(null, selected)), Expression.Call( context.Writer, writeInteger, Expression.Constant((long)schemas.IndexOf(@null))), body); } cases.Add(selected, body); } if (cases.Count == 0) { throw new UnsupportedTypeException( type, $"{type.Name} does not match any non-null members of {unionSchema}.", new AggregateException(exceptions)); } if (cases.Count == 1 && cases.First() is var first && first.Key == type) { expression = first.Value; } else { var exceptionConstructor = typeof(InvalidOperationException) .GetConstructor(new[] { typeof(string) }); expression = Expression.Throw(Expression.New( exceptionConstructor, Expression.Constant($"Unexpected type encountered serializing to {type}."))); foreach (var @case in cases) { expression = Expression.IfThenElse( Expression.TypeIs(value, @case.Key), @case.Value, expression); } } } // otherwise, we know that the schema is just ["null"]: else { expression = Expression.Call( context.Writer, writeInteger, Expression.Constant((long)schemas.IndexOf(@null))); } return(BinarySerializerBuilderCaseResult.FromExpression(expression)); }
/// <summary> /// Builds a <see cref="BinarySerializer{T}" /> for a <see cref="DecimalLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="BinarySerializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="DecimalLogicalType" />; an unsuccessful <see cref="BinarySerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedSchemaException"> /// Thrown when <paramref name="schema" /> is not a <see cref="BytesSchema" /> or a /// <see cref="FixedSchema "/>. /// </exception> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be converted to <see cref="decimal" />. /// </exception> /// <inheritdoc /> public virtual BinarySerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, BinarySerializerBuilderContext context) { if (schema.LogicalType is DecimalLogicalType decimalLogicalType) { var precision = decimalLogicalType.Precision; var scale = decimalLogicalType.Scale; Expression expression; try { expression = BuildConversion(value, typeof(decimal)); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception); } // declare variables for in-place transformation: var bytes = Expression.Variable(typeof(byte[])); var integerConstructor = typeof(BigInteger) .GetConstructor(new[] { typeof(decimal) }); var reverse = typeof(Array) .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) }); var toByteArray = typeof(BigInteger) .GetMethod(nameof(BigInteger.ToByteArray), Type.EmptyTypes); // var fraction = new BigInteger(...) * BigInteger.Pow(10, scale); // var whole = new BigInteger((... % 1m) * (decimal)Math.Pow(10, scale)); // var bytes = (fraction + whole).ToByteArray(); // // // BigInteger is little-endian, so reverse: // Array.Reverse(bytes); // // return bytes; expression = Expression.Block( Expression.Assign( bytes, Expression.Call( Expression.Add( Expression.Multiply( Expression.New( integerConstructor, expression), Expression.Constant(BigInteger.Pow(10, scale))), Expression.New( integerConstructor, Expression.Multiply( Expression.Modulo(expression, Expression.Constant(1m)), Expression.Constant((decimal)Math.Pow(10, scale))))), toByteArray)), Expression.Call(null, reverse, bytes), bytes); // figure out how to write: if (schema is BytesSchema) { var writeBytes = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteBytes), new[] { typeof(byte[]) }); expression = Expression.Block( new[] { bytes }, expression, Expression.Call(context.Writer, writeBytes, bytes)); } else if (schema is FixedSchema fixedSchema) { var exceptionConstructor = typeof(OverflowException) .GetConstructor(new[] { typeof(string) }); var writeFixed = typeof(BinaryWriter) .GetMethod(nameof(BinaryWriter.WriteFixed), new[] { typeof(byte[]) }); expression = Expression.Block( new[] { bytes }, expression, Expression.IfThen( Expression.NotEqual(Expression.ArrayLength(bytes), Expression.Constant(fixedSchema.Size)), Expression.Throw(Expression.New(exceptionConstructor, Expression.Constant($"Size mismatch between {fixedSchema} (size {fixedSchema.Size}) and decimal with precision {precision} and scale {scale}.")))), Expression.Call(context.Writer, writeFixed, bytes)); } else { throw new UnsupportedSchemaException(schema); } return(BinarySerializerBuilderCaseResult.FromExpression(expression)); } else { return(BinarySerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryDecimalSerializerBuilderCase)} can only be applied schemas with a {nameof(DecimalLogicalType)}."))); } }