/// <exception cref="UnsupportedTypeException"> /// Thrown when no case can map <paramref name="type" /> to <paramref name="schema" />. /// </exception> /// <inheritdoc /> public virtual Expression BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { var exceptions = new List <Exception>(); foreach (var @case in Cases) { var result = @case.BuildExpression(type, schema, context); if (result.Expression != null) { return(result.Expression); } exceptions.AddRange(result.Exceptions); } throw new UnsupportedTypeException(type, $"No deserializer builder case could be applied to {type}.", new AggregateException(exceptions)); }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="StringSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="StringSchema" />; an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <see cref="string" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema is StringSchema stringSchema) { var readString = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadString), Type.EmptyTypes); try { return(BinaryDeserializerBuilderCaseResult.FromExpression( BuildConversion(Expression.Call(context.Reader, readString), type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {stringSchema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryStringDeserializerBuilderCase)} can only be applied to {nameof(StringSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="RecordSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="type" /> /// is not an array or primitive type and <paramref name="schema" /> is a <see cref="RecordSchema" />; /// an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> otherwise. /// </returns> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema is RecordSchema recordSchema) { var underlying = Nullable.GetUnderlyingType(type) ?? type; if (!underlying.IsArray && !underlying.IsPrimitive) { // since record deserialization is potentially recursive, create a top-level // reference: var parameter = Expression.Parameter( Expression.GetDelegateType(context.Reader.Type.MakeByRefType(), underlying)); if (!context.References.TryGetValue((recordSchema, type), out var reference)) { context.References.Add((recordSchema, type), reference = parameter); } // then build/set the delegate if it hasn’t been built yet: if (parameter == reference) { Expression expression; if (GetRecordConstructor(underlying, recordSchema) is ConstructorInfo constructor) { var parameters = constructor.GetParameters(); // map constructor parameters to fields: var mapping = recordSchema.Fields .Select(field => { // there will be a match or we wouldn’t have made it this far: var match = parameters.Single(parameter => IsMatch(field, parameter.Name)); var parameter = Expression.Parameter(match.ParameterType); return( Match: match, Parameter: parameter, Assignment: (Expression)Expression.Assign( parameter, DeserializerBuilder.BuildExpression(match.ParameterType, field.Type, context))); }) .ToDictionary(r => r.Match, r => (r.Parameter, r.Assignment)); expression = Expression.Block( mapping .Select(d => d.Value.Parameter), mapping .Select(d => d.Value.Assignment) .Concat(new[] { Expression.New( constructor, parameters .Select(parameter => mapping.ContainsKey(parameter) ? (Expression)mapping[parameter].Parameter : Expression.Constant(parameter.DefaultValue))), })); } else { var members = underlying.GetMembers(MemberVisibility); // support dynamic deserialization: var value = Expression.Parameter( underlying.IsAssignableFrom(typeof(ExpandoObject)) ? typeof(ExpandoObject) : underlying); expression = Expression.Block( new[] { value }, new[] { (Expression)Expression.Assign(value, Expression.New(value.Type)) } .Concat(recordSchema.Fields.Select(field => { var match = members.SingleOrDefault(member => IsMatch(field, member)); Expression expression; if (match == null) { // always deserialize fields to advance the reader: expression = DeserializerBuilder.BuildExpression(typeof(object), field.Type, context); // fall back to a dynamic setter if the value supports it: if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(value.Type)) { var flags = CSharpBinderFlags.None; var infos = new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }; var binder = Binder.SetMember(flags, field.Name, value.Type, infos); expression = Expression.Dynamic(binder, typeof(void), value, expression); } } else { Expression inner; try { inner = DeserializerBuilder.BuildExpression( match switch { FieldInfo fieldMatch => fieldMatch.FieldType, PropertyInfo propertyMatch => propertyMatch.PropertyType, MemberInfo unknown => throw new InvalidOperationException($"Record fields can only be mapped to fields and properties."), }, field.Type, context); } catch (Exception exception) { throw new UnsupportedTypeException(type, $"The {match.Name} member on {type} could not be mapped to the {field.Name} field on {recordSchema.FullName}.", exception); } expression = Expression.Assign( Expression.PropertyOrField(value, match.Name), inner); } return(expression); }))
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="DurationLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="DurationLogicalType" />; an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> /// 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 <see cref="TimeSpan" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema.LogicalType is DurationLogicalType) { if (!(schema is FixedSchema fixedSchema && fixedSchema.Size == DurationLogicalType.DurationSize)) { throw new UnsupportedSchemaException(schema); } var readFixed = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadFixed), new[] { typeof(int) }); Expression read = Expression.Call(context.Reader, readFixed, Expression.Constant(4)); if (!BitConverter.IsLittleEndian) { var buffer = Expression.Variable(read.Type); var reverse = typeof(Array) .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) }); read = Expression.Block( new[] { buffer }, Expression.Assign(buffer, read), Expression.Call(null, reverse, Expression.Convert(buffer, typeof(Array))), buffer); } var toUInt32 = typeof(BitConverter) .GetMethod(nameof(BitConverter.ToUInt32), new[] { typeof(byte[]), typeof(int) }); read = Expression.ConvertChecked( Expression.Call(null, toUInt32, read, Expression.Constant(0)), typeof(long)); var exceptionConstructor = typeof(OverflowException) .GetConstructor(new[] { typeof(string) }); var timeSpanConstructor = typeof(TimeSpan) .GetConstructor(new[] { typeof(long) }); try { return(BinaryDeserializerBuilderCaseResult.FromExpression( BuildConversion( Expression.Block( Expression.IfThen( Expression.NotEqual(read, Expression.Constant(0L)), Expression.Throw( Expression.New( exceptionConstructor, Expression.Constant($"Durations containing months cannot be accurately deserialized to a {nameof(TimeSpan)}.")))), Expression.New( timeSpanConstructor, Expression.AddChecked( Expression.MultiplyChecked(read, Expression.Constant(TimeSpan.TicksPerDay)), Expression.MultiplyChecked(read, Expression.Constant(TimeSpan.TicksPerMillisecond))))), type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryDurationDeserializerBuilderCase)} can only be applied schemas with a {nameof(DurationLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="TimestampLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="TimestampLogicalType" />; an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> /// 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 <see cref="DateTime" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema.LogicalType is TimestampLogicalType) { if (schema is not LongSchema) { throw new UnsupportedSchemaException(schema, $"{nameof(TimestampLogicalType)} deserializers can only be built for {nameof(LongSchema)}s."); } 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 readInteger = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadInteger), Type.EmptyTypes); Expression expression = Expression.Call(context.Reader, readInteger); var addTicks = typeof(DateTime) .GetMethod(nameof(DateTime.AddTicks), new[] { typeof(long) }); try { // return Epoch.AddTicks(value * factor); return(BinaryDeserializerBuilderCaseResult.FromExpression( BuildConversion( Expression.Call( Expression.Constant(Epoch), addTicks, Expression.Multiply(expression, Expression.Constant(factor))), type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryTimestampDeserializerBuilderCase)} can only be applied to schemas with a {nameof(TimestampLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="UnionSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="UnionSchema" />; an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> /// 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 each <see cref="Schema" /> in /// <paramref name="schema" />. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema is UnionSchema unionSchema) { if (unionSchema.Schemas.Count < 1) { throw new UnsupportedSchemaException(schema, "A deserializer cannot be built for an empty union."); } var readInteger = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadInteger), Type.EmptyTypes); Expression expression = Expression.Call(context.Reader, readInteger); // create a mapping for each schema in the union: var cases = unionSchema.Schemas.Select((child, index) => { var selected = SelectType(type, child); var underlying = Nullable.GetUnderlyingType(selected); if (child is NullSchema && selected.IsValueType && underlying == null) { throw new UnsupportedTypeException(type, $"A deserializer for {unionSchema} cannot be built for {selected} because it contains {nameof(NullSchema)}."); } var @case = DeserializerBuilder.BuildExpression(selected, child, context); return(Expression.SwitchCase( BuildConversion(@case, type), Expression.Constant((long)index))); }); var position = typeof(BinaryReader) .GetProperty(nameof(BinaryReader.Index)); var exceptionConstructor = typeof(InvalidEncodingException) .GetConstructor(new[] { typeof(long), typeof(string), typeof(Exception) }); try { // generate a switch on the index: return(BinaryDeserializerBuilderCaseResult.FromExpression( Expression.Switch( expression, Expression.Throw( Expression.New( exceptionConstructor, Expression.Property(context.Reader, position), Expression.Constant($"Invalid union index; expected a value in [0-{unionSchema.Schemas.Count}). This may indicate invalid encoding earlier in the stream."), Expression.Constant(null, typeof(Exception))), type), cases.ToArray()))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {unionSchema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryUnionDeserializerBuilderCase)} can only be applied to {nameof(UnionSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="MapSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="type" /> /// is a dictionary type and <paramref name="schema" /> is a <see cref="MapSchema" />; an /// unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> is not assignable from any supported dictionary /// <see cref="Type" /> and does not have a constructor that can be used to instantiate it. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext 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(string); var valueType = dictionaryTypes?.Value ?? typeof(object); var instantiateDictionary = BuildIntermediateDictionary(type, keyType, valueType); var readInteger = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadInteger), Type.EmptyTypes); var readKey = DeserializerBuilder .BuildExpression(keyType, new StringSchema(), context); var readValue = DeserializerBuilder .BuildExpression(valueType, mapSchema.Value, context); var dictionary = Expression.Parameter(instantiateDictionary.Type); var index = Expression.Variable(typeof(long)); var size = Expression.Variable(typeof(long)); var outer = Expression.Label(); var inner = Expression.Label(); var add = dictionary.Type .GetMethod("Add", new[] { readKey.Type, readValue.Type }); // var dictionary = new ...; // // outer: while (true) // { // var size = reader.ReadInteger(); // // // if the block is empty, the map is complete: // if (size == 0L) break outer; // // // if the block size is negative, the number of bytes in the block // // follows, so read and discard: // if (size < 0L) // { // size *= -1L; // reader.ReadInteger(); // } // // var index = 0; // // inner: while (true) // { // // primitive for: // if (index++ == size) break inner; // // dictionary.Add(..., ...); // } // } // // return dictionary; Expression expression = Expression.Block( new[] { dictionary, index, size }, Expression.Assign(dictionary, instantiateDictionary), Expression.Loop( Expression.Block( Expression.Assign(size, Expression.Call(context.Reader, readInteger)), Expression.IfThen( Expression.Equal(size, Expression.Constant(0L)), Expression.Break(outer)), Expression.IfThen( Expression.LessThan(size, Expression.Constant(0L)), Expression.Block( Expression.MultiplyAssign(size, Expression.Constant(-1L)), Expression.Call(context.Reader, readInteger))), Expression.Assign(index, Expression.Constant(0L)), Expression.Loop( Expression.Block( Expression.IfThen( Expression.Equal(Expression.PostIncrementAssign(index), size), Expression.Break(inner)), Expression.Call(dictionary, add, readKey, readValue)), inner)), outer), dictionary); if (!type.IsAssignableFrom(expression.Type) && GetDictionaryConstructor(type) is ConstructorInfo constructor) { expression = Expression.New(constructor, expression); } try { return(BinaryDeserializerBuilderCaseResult.FromExpression( BuildConversion(expression, type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {mapSchema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(BinaryMapDeserializerBuilderCase)} can only be applied to dictionary types."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="FixedSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="FixedSchema" />; an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <see cref="T:System.Byte[]" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema is FixedSchema fixedSchema) { var readFixed = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadFixed), new[] { typeof(int) }); try { return(BinaryDeserializerBuilderCaseResult.FromExpression( BuildConversion( Expression.Call( context.Reader, readFixed, Expression.Constant(fixedSchema.Size)), type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {fixedSchema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryFixedDeserializerBuilderCase)} can only be applied to {nameof(FixedSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for an <see cref="EnumSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is an <see cref="EnumSchema" />; an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> is an enum type without a matching member for each /// symbol in <paramref name="schema" /> or when <see cref="string" /> cannot be converted /// to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema is EnumSchema enumSchema) { var readInteger = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadInteger), Type.EmptyTypes); Expression expression = Expression.ConvertChecked( Expression.Call(context.Reader, readInteger), typeof(int)); var underlying = Nullable.GetUnderlyingType(type) ?? type; // enum fields will always be public static, so no need to expose binding flags: var fields = underlying.GetFields(BindingFlags.Public | BindingFlags.Static); var cases = underlying.IsEnum ? enumSchema.Symbols .Select((symbol, index) => { var match = fields.SingleOrDefault(field => IsMatch(symbol, field)); if (enumSchema.Default != null) { match ??= fields.SingleOrDefault(field => IsMatch(enumSchema.Default, field)); } if (match == null) { throw new UnsupportedTypeException(type, $"{type} has no value that matches {symbol} and no default value is defined."); } return(Expression.SwitchCase( BuildConversion(Expression.Constant(Enum.Parse(underlying, match.Name)), type), Expression.Constant(index))); }) : enumSchema.Symbols .Select((symbol, index) => { return(Expression.SwitchCase( BuildConversion(Expression.Constant(symbol), type), Expression.Constant(index))); }); var position = typeof(BinaryReader) .GetProperty(nameof(BinaryReader.Index)) .GetGetMethod(); var exceptionConstructor = typeof(InvalidEncodingException) .GetConstructor(new[] { typeof(long), typeof(string), typeof(Exception) }); try { // generate a switch on the index: return(BinaryDeserializerBuilderCaseResult.FromExpression( Expression.Switch( expression, Expression.Throw( Expression.New( exceptionConstructor, Expression.Property(context.Reader, position), Expression.Constant($"Invalid enum index; expected a value in [0-{enumSchema.Symbols.Count}). This may indicate invalid encoding earlier in the stream."), Expression.Constant(null, typeof(Exception))), type), cases.ToArray()))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {enumSchema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryEnumDeserializerBuilderCase)} can only be applied to {nameof(EnumSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for an <see cref="ArraySchema" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="type" /> /// is an enumerable type and <paramref name="schema" /> is an <see cref="ArraySchema" />; /// an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> is not assignable from any supported array or /// collection <see cref="Type" /> and does not have a constructor that can be used to /// instantiate it. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema is ArraySchema arraySchema) { var enumerableType = GetEnumerableType(type); if (enumerableType is not null || type == typeof(object)) { // support dynamic mapping: var itemType = enumerableType ?? typeof(object); var instantiateCollection = BuildIntermediateCollection(type, itemType); var readInteger = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadInteger), Type.EmptyTypes); var readItem = DeserializerBuilder .BuildExpression(itemType, arraySchema.Item, context); var collection = Expression.Parameter(instantiateCollection.Type); var index = Expression.Variable(typeof(long)); var size = Expression.Variable(typeof(long)); var outer = Expression.Label(); var inner = Expression.Label(); var add = collection.Type .GetMethod("Add", new[] { readItem.Type }); // var collection = new List<T>(); // // outer: while (true) // { // var size = reader.ReadInteger(); // // // if the block is empty, the array is complete: // if (size == 0L) break outer; // // // if the block size is negative, the number of bytes in the block // // follows, so read and discard: // if (size < 0L) // { // size *= -1L; // reader.ReadInteger(); // } // // var index = 0; // // inner: while (true) // { // // primitive for: // if (index++ == size) break inner; // // collection.Add(...); // } // } // // return collection; Expression expression = Expression.Block( new[] { collection, index, size }, Expression.Assign(collection, instantiateCollection), Expression.Loop( Expression.Block( Expression.Assign(size, Expression.Call(context.Reader, readInteger)), Expression.IfThen( Expression.Equal(size, Expression.Constant(0L)), Expression.Break(outer)), Expression.IfThen( Expression.LessThan(size, Expression.Constant(0L)), Expression.Block( Expression.MultiplyAssign(size, Expression.Constant(-1L)), Expression.Call(context.Reader, readInteger))), Expression.Assign(index, Expression.Constant(0L)), Expression.Loop( Expression.Block( Expression.IfThen( Expression.Equal(Expression.PostIncrementAssign(index), size), Expression.Break(inner)), Expression.Call(collection, add, readItem)), inner)), outer), collection); if (!type.IsAssignableFrom(expression.Type) && GetCollectionConstructor(type) is ConstructorInfo constructor) { expression = Expression.New(constructor, expression); } try { return(BinaryDeserializerBuilderCaseResult.FromExpression( BuildConversion(expression, type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {arraySchema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(BinaryArrayDeserializerBuilderCase)} can only be applied to enumerable types."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="NullSchema" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="NullSchema" />; an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema is NullSchema) { return(BinaryDeserializerBuilderCaseResult.FromExpression( Expression.Default(type))); } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryNullDeserializerBuilderCase)} can only be applied to {nameof(NullSchema)}s."))); } }
/// <summary> /// Builds a <see cref="BinaryDeserializer{T}" /> for a <see cref="DecimalLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="BinaryDeserializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="DecimalLogicalType" />; an unsuccessful <see cref="BinaryDeserializerBuilderCaseResult" /> /// 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 <see cref="decimal" /> cannot be converted to <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual BinaryDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context) { if (schema.LogicalType is DecimalLogicalType decimalLogicalType) { var precision = decimalLogicalType.Precision; var scale = decimalLogicalType.Scale; Expression expression; // figure out the size: if (schema is BytesSchema) { var readBytes = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadBytes), Type.EmptyTypes); expression = Expression.Call(context.Reader, readBytes); } else if (schema is FixedSchema fixedSchema) { var readFixed = typeof(BinaryReader) .GetMethod(nameof(BinaryReader.ReadFixed), new[] { typeof(long) }); expression = Expression.Call(context.Reader, readFixed, Expression.Constant((long)fixedSchema.Size)); } else { throw new UnsupportedSchemaException(schema); } // declare variables for in-place transformation: var bytes = Expression.Variable(typeof(byte[])); var remainder = Expression.Variable(typeof(BigInteger)); var divide = typeof(BigInteger) .GetMethod(nameof(BigInteger.DivRem), new[] { typeof(BigInteger), typeof(BigInteger), typeof(BigInteger).MakeByRefType() }); var integerConstructor = typeof(BigInteger) .GetConstructor(new[] { typeof(byte[]) }); var reverse = typeof(Array) .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) }); // var bytes = ...; // // // BigInteger is little-endian, so reverse: // Array.Reverse(bytes); // // var whole = BigInteger.DivRem(new BigInteger(bytes), BigInteger.Pow(10, scale), out var remainder); // var fraction = (decimal)remainder / (decimal)Math.Pow(10, scale); // // return whole + fraction; expression = Expression.Block( new[] { bytes, remainder }, Expression.Assign(bytes, expression), Expression.Call(null, reverse, bytes), Expression.Add( Expression.ConvertChecked( Expression.Call( null, divide, Expression.New(integerConstructor, bytes), Expression.Constant(BigInteger.Pow(10, scale)), remainder), typeof(decimal)), Expression.Divide( Expression.ConvertChecked(remainder, typeof(decimal)), Expression.Constant((decimal)Math.Pow(10, scale))))); try { return(BinaryDeserializerBuilderCaseResult.FromExpression( BuildConversion(expression, type))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception); } } else { return(BinaryDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(BinaryDecimalDeserializerBuilderCase)} can only be applied schemas with a {nameof(DecimalLogicalType)}."))); } }