/// <exception cref="UnsupportedTypeException"> /// Thrown when no case can map the <see cref="Expression.Type" /> of <paramref name="value" /> /// to <paramref name="schema" />. /// </exception> /// <inheritdoc /> public virtual Expression BuildExpression(Expression value, Schema schema, JsonSerializerBuilderContext context) { var exceptions = new List <Exception>(); foreach (var @case in Cases) { var result = @case.BuildExpression(value, value.Type, schema, context); if (result.Expression != null) { return(result.Expression); } exceptions.AddRange(result.Exceptions); } throw new UnsupportedTypeException(value.Type, $"No serializer builder case could be applied to {value.Type}.", new AggregateException(exceptions)); }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="LongSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="LongSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be converted to <see cref="long" />. /// </exception> /// <inheritdoc /> public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context) { if (schema is LongSchema longSchema) { var writeNumber = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteNumberValue), new[] { typeof(long) }); try { return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Call(context.Writer, writeNumber, BuildConversion(value, typeof(long))))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {longSchema} to {type}.", exception); } } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonLongSerializerBuilderCase)} can only be applied to {nameof(LongSchema)}s."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for an <see cref="FixedSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> <paramref name="schema" /> /// is a <see cref="FixedSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be converted to <see cref="T:System.Byte[]" />. /// </exception> /// <inheritdoc /> public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext 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 chars = Expression.Parameter(typeof(char[])); var exceptionConstructor = typeof(OverflowException) .GetConstructor(new[] { typeof(string) }); var copyTo = typeof(Array) .GetMethod(nameof(Array.CopyTo), new[] { typeof(Array), typeof(int) }); var encode = typeof(JsonEncodedText) .GetMethod(nameof(JsonEncodedText.Encode), new[] { typeof(ReadOnlySpan <char>), typeof(JavaScriptEncoder) }); var writeString = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(JsonEncodedText) }); return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { bytes, chars }, 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.Name}.")))), Expression.Assign(chars, Expression.NewArrayBounds(typeof(char), Expression.ArrayLength(bytes))), Expression.Call(bytes, copyTo, chars, Expression.Constant(0)), Expression.Call( context.Writer, writeString, Expression.Call( null, encode, Expression.Convert(chars, typeof(ReadOnlySpan <char>)), Expression.Constant(JsonEncoder.Bytes)))))); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonFixedSerializerBuilderCase)} can only be applied to {nameof(FixedSchema)}s."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for an <see cref="ArraySchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="type" /> /// is an enumerable type and <paramref name="schema" /> is an <see cref="ArraySchema" />; /// an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> does not implement <see cref="IEnumerable{T}" />. /// </exception> /// <inheritdoc /> public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext 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 enumerable = Expression.Variable(typeof(IEnumerable <>).MakeGenericType(itemType)); var enumerator = Expression.Variable(typeof(IEnumerator <>).MakeGenericType(itemType)); var loop = Expression.Label(); var writeStartArray = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStartArray), Type.EmptyTypes); var getEnumerator = enumerable.Type .GetMethod("GetEnumerator", Type.EmptyTypes); var getCurrent = enumerator.Type .GetProperty(nameof(IEnumerator.Current)) .GetGetMethod(); var moveNext = typeof(IEnumerator) .GetMethod(nameof(IEnumerator.MoveNext), Type.EmptyTypes); var writeItem = SerializerBuilder .BuildExpression(Expression.Property(enumerator, getCurrent), arraySchema.Item, context); var writeEndArray = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteEndArray), Type.EmptyTypes); var dispose = typeof(IDisposable) .GetMethod(nameof(IDisposable.Dispose), Type.EmptyTypes); Expression expression; try { expression = BuildConversion(value, enumerable.Type); } catch (Exception exception) { throw new UnsupportedTypeException(type, $"Failed to map {arraySchema} to {type}.", exception); } return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { enumerator }, Expression.Call(context.Writer, writeStartArray), Expression.Assign( enumerator, Expression.Call(expression, getEnumerator)), Expression.TryFinally( Expression.Loop( Expression.IfThenElse( Expression.Call(enumerator, moveNext), writeItem, Expression.Break(loop)), loop), Expression.Call(enumerator, dispose)), Expression.Call(context.Writer, writeEndArray)))); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonArraySerializerBuilderCase)} can only be applied to enumerable types."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="DecimalLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="DecimalLogicalType" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> /// 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 JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext 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 chars = Expression.Parameter(typeof(char[])); 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 copyTo = typeof(Array) .GetMethod(nameof(Array.CopyTo), new[] { typeof(Array), typeof(int) }); // 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); // // var chars = new char[bytes.Length]; // bytes.CopyTo(chars, 0); 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), Expression.Assign(chars, Expression.NewArrayBounds(typeof(char), Expression.ArrayLength(bytes))), Expression.Call(bytes, copyTo, chars, Expression.Constant(0))); var encode = typeof(JsonEncodedText) .GetMethod(nameof(JsonEncodedText.Encode), new[] { typeof(ReadOnlySpan <char>), typeof(JavaScriptEncoder) }); var writeString = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(JsonEncodedText) }); // figure out how to write: if (schema is BytesSchema) { expression = Expression.Block( new[] { bytes, chars }, expression, Expression.Call( context.Writer, writeString, Expression.Call( null, encode, Expression.Convert(chars, typeof(ReadOnlySpan <char>)), Expression.Constant(JsonEncoder.Bytes)))); } else if (schema is FixedSchema fixedSchema) { var exceptionConstructor = typeof(OverflowException) .GetConstructor(new[] { typeof(string) }); expression = Expression.Block( new[] { bytes, chars }, expression, Expression.IfThen( Expression.NotEqual(Expression.ArrayLength(bytes), Expression.Constant(fixedSchema.Size)), Expression.Throw(Expression.New(exceptionConstructor, Expression.Constant($"Size mismatch between {fixedSchema.Name} (size {fixedSchema.Size}) and decimal with precision {precision} and scale {scale}.")))), Expression.Call( context.Writer, writeString, Expression.Call( null, encode, Expression.Convert(chars, typeof(ReadOnlySpan <char>)), Expression.Constant(JsonEncoder.Bytes)))); } else { throw new UnsupportedSchemaException(schema); } return(JsonSerializerBuilderCaseResult.FromExpression(expression)); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDecimalSerializerBuilderCase)} can only be applied schemas with a {nameof(DecimalLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="NullSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="NullSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <inheritdoc /> public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context) { if (schema is NullSchema) { var writeNull = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteNullValue), Type.EmptyTypes); return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Call(context.Writer, writeNull))); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonNullSerializerBuilderCase)} can only be applied to {nameof(NullSchema)}s."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="BytesSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="BytesSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> /// otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> cannot be converted to <see cref="T:System.Byte[]" />. /// </exception> /// <inheritdoc /> public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context) { if (schema is BytesSchema bytesSchema) { var bytes = Expression.Parameter(typeof(byte[])); var chars = Expression.Parameter(typeof(char[])); var copyTo = typeof(Array) .GetMethod(nameof(Array.CopyTo), new[] { typeof(Array), typeof(int) }); var encode = typeof(JsonEncodedText) .GetMethod(nameof(JsonEncodedText.Encode), new[] { typeof(ReadOnlySpan <char>), typeof(JavaScriptEncoder) }); var writeString = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(JsonEncodedText) }); try { return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { bytes, chars }, Expression.Assign(bytes, BuildConversion(value, typeof(byte[]))), Expression.Assign(chars, Expression.NewArrayBounds(typeof(char), Expression.ArrayLength(bytes))), Expression.Call(bytes, copyTo, chars, Expression.Constant(0)), Expression.Call( context.Writer, writeString, Expression.Call( null, encode, Expression.Convert(chars, typeof(ReadOnlySpan <char>)), Expression.Constant(JsonEncoder.Bytes)))))); } catch (InvalidOperationException exception) { throw new UnsupportedTypeException(type, $"Failed to map {bytesSchema} to {type}.", exception); } } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonBytesSerializerBuilderCase)} can only be applied to {nameof(BytesSchema)}s."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="RecordSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="type" /> /// is not an array or primitive type and <paramref name="schema" /> is a <see cref="RecordSchema" />; /// an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="type" /> does not have a matching member for each /// <see cref="RecordField" /> on <paramref name="schema" />. /// </exception> /// <inheritdoc /> public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context) { if (schema is RecordSchema recordSchema) { if (!type.IsArray && !type.IsPrimitive) { // since record serialization is potentially recursive, create a top-level // reference: var parameter = Expression.Parameter( Expression.GetDelegateType(type, context.Writer.Type, typeof(void))); 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) { var members = type.GetMembers(MemberVisibility); var writeStartObject = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStartObject), Type.EmptyTypes); var writePropertyName = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WritePropertyName), new[] { typeof(string) }); var writeEndObject = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteEndObject), Type.EmptyTypes); var argument = Expression.Variable(type); var writes = new List <Expression> { Expression.Call(context.Writer, writeStartObject), }; foreach (var field in recordSchema.Fields) { var match = members.SingleOrDefault(member => IsMatch(field, member)); Expression inner; if (match == null) { // if the type could be dynamic, attempt to use a dynamic getter: if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type) || type == typeof(object)) { var flags = CSharpBinderFlags.None; var infos = new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }; var binder = Binder.GetMember(flags, field.Name, type, infos); inner = Expression.Dynamic(binder, typeof(object), value); } else { if (field.Default is not null) { inner = Expression.Constant(field.Default.ToObject <dynamic>()); } else { throw new UnsupportedTypeException(type, $"{type} does not have a field or property that matches the {field.Name} field on {recordSchema.FullName}."); } } } else { inner = Expression.PropertyOrField(argument, match.Name); } try { writes.Add(Expression.Call(context.Writer, writePropertyName, Expression.Constant(field.Name))); writes.Add(SerializerBuilder.BuildExpression(inner, field.Type, context)); } catch (Exception exception) { throw new UnsupportedTypeException(type, $"{(match is null ? "A" : $"The {match.Name}")} member on {type} could not be mapped to the {field.Name} field on {recordSchema.FullName}.", exception); } } writes.Add(Expression.Call(context.Writer, writeEndObject)); var expression = Expression.Lambda( parameter.Type, Expression.Block(writes), $"{recordSchema.Name} serializer", new[] { argument, context.Writer }); context.Assignments.Add(reference, expression); } return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Invoke(reference, value, context.Writer))); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonRecordSerializerBuilderCase)} cannot be applied to array or primitive types."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="TimestampLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="TimestampLogicalType" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> /// 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 JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext 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 writeNumber = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteNumberValue), new[] { typeof(long) }); // return writer.WriteNumber((value.UtcTicks - epoch) / factor); return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Call( context.Writer, writeNumber, Expression.Divide( Expression.Subtract( Expression.Property(expression, utcTicks), Expression.Constant(Epoch.Ticks)), Expression.Constant(factor))))); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonTimestampSerializerBuilderCase)} can only be applied schemas with a {nameof(TimestampLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="UnionSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" /> /// is a <see cref="UnionSchema" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> /// 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 JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context) { if (schema is UnionSchema unionSchema) { if (unionSchema.Schemas.Count < 1) { throw new UnsupportedSchemaException(schema); } var schemas = unionSchema.Schemas.ToList(); var candidates = schemas.Where(s => s is not NullSchema).ToList(); var @null = schemas.Find(s => s is NullSchema); var writeNull = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteNullValue), Type.EmptyTypes); Expression expression = null !; // 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>(); var writeStartObject = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStartObject), Type.EmptyTypes); var writePropertyName = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WritePropertyName), new[] { typeof(string) }); var writeEndObject = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteEndObject), Type.EmptyTypes); 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, writeStartObject), Expression.Call(context.Writer, writePropertyName, Expression.Constant(GetSchemaName(candidate))), SerializerBuilder.BuildExpression(Expression.Convert(value, underlying), candidate, context), Expression.Call(context.Writer, writeEndObject)); } 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, writeNull), 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, writeNull); } return(JsonSerializerBuilderCaseResult.FromExpression(expression)); }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for a <see cref="DurationLogicalType" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="schema" /> /// has a <see cref="DurationLogicalType" />; an unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> /// 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 JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context) { if (schema.LogicalType is DurationLogicalType) { if (!(schema is FixedSchema fixedSchema && fixedSchema.Size == 12)) { throw new UnsupportedSchemaException(schema); } var chars = Expression.Parameter(typeof(char[])); var getBytes = typeof(BitConverter) .GetMethod(nameof(BitConverter.GetBytes), new[] { typeof(uint) }); var reverse = typeof(Array) .GetMethod(nameof(Array.Reverse), new[] { getBytes.ReturnType }); var copyTo = typeof(Array) .GetMethod(nameof(Array.CopyTo), new[] { typeof(Array), typeof(int) }); Expression Write(Expression value, Expression offset) { Expression component = Expression.Call(null, getBytes, value); if (!BitConverter.IsLittleEndian) { var buffer = Expression.Variable(component.Type); component = Expression.Block( new[] { buffer }, Expression.Assign(buffer, component), Expression.Call(null, reverse, buffer), buffer); } return(Expression.Call(component, copyTo, chars, offset)); } var totalDays = typeof(TimeSpan).GetProperty(nameof(TimeSpan.TotalDays)); var totalMs = typeof(TimeSpan).GetProperty(nameof(TimeSpan.TotalMilliseconds)); var encode = typeof(JsonEncodedText) .GetMethod(nameof(JsonEncodedText.Encode), new[] { typeof(ReadOnlySpan <char>), typeof(JavaScriptEncoder) }); var writeString = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(JsonEncodedText) }); return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { chars }, Expression.Assign( chars, Expression.NewArrayBounds(typeof(char), Expression.Constant(12))), Write( Expression.ConvertChecked(Expression.Property(value, totalDays), typeof(uint)), Expression.Constant(4)), 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)), Expression.Constant(8)), Expression.Call( context.Writer, writeString, Expression.Call( null, encode, Expression.Convert(chars, typeof(ReadOnlySpan <char>)), Expression.Constant(JsonEncoder.Bytes)))))); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDurationSerializerBuilderCase)} can only be applied schemas with a {nameof(DurationLogicalType)}."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for an <see cref="EnumSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="type" /> /// is an enum and <paramref name="schema" /> is an <see cref="EnumSchema" />; an /// unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> otherwise. /// </returns> /// <exception cref="UnsupportedTypeException"> /// Thrown when <paramref name="schema" /> does not have a matching symbol for each member /// of <paramref name="type" />. /// </exception> /// <inheritdoc /> public virtual JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext context) { if (schema is EnumSchema enumSchema) { var writeString = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStringValue), new[] { typeof(string) }); // 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(); // find a match for each enum in the type: var cases = type.IsEnum ? fields .Select(field => { var match = symbols.Find(symbol => IsMatch(symbol, field)); if (match == null) { throw new UnsupportedTypeException(type, $"{type} has a field {field.Name} that cannot be serialized."); } if (symbols.FindLast(symbol => IsMatch(symbol, field)) != match) { throw new UnsupportedTypeException(type, $"{type.Name} has an ambiguous field {field.Name}."); } return(Expression.SwitchCase( Expression.Call(context.Writer, writeString, Expression.Constant(match)), Expression.Constant(Enum.Parse(type, field.Name)))); }) : symbols .Select(symbol => { return(Expression.SwitchCase( Expression.Call(context.Writer, writeString, Expression.Constant(symbol)), 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(JsonSerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { intermediate }, Expression.Assign(intermediate, BuildConversion(value, intermediate.Type)), Expression.Switch(intermediate, Expression.Throw(exception), cases.ToArray())))); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonEnumSerializerBuilderCase)} can only be applied to {nameof(EnumSchema)}s."))); } }
/// <summary> /// Builds a <see cref="JsonSerializer{T}" /> for an <see cref="MapSchema" />. /// </summary> /// <returns> /// A successful <see cref="JsonSerializerBuilderCaseResult" /> if <paramref name="type" /> /// is a dictionary type and <paramref name="schema" /> is a <see cref="MapSchema" />; an /// unsuccessful <see cref="JsonSerializerBuilderCaseResult" /> 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 JsonSerializerBuilderCaseResult BuildExpression(Expression value, Type type, Schema schema, JsonSerializerBuilderContext 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 enumerable = Expression.Variable(typeof(IEnumerable <>).MakeGenericType(pairType)); var enumerator = Expression.Variable(typeof(IEnumerator <>).MakeGenericType(pairType)); var loop = Expression.Label(); var writeStartObject = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteStartObject), Type.EmptyTypes); var getEnumerator = enumerable.Type .GetMethod("GetEnumerator", Type.EmptyTypes); var getCurrent = enumerator.Type .GetProperty(nameof(IEnumerator.Current)) .GetGetMethod(); var moveNext = typeof(IEnumerator) .GetMethod(nameof(IEnumerator.MoveNext), Type.EmptyTypes); var getKey = pairType .GetProperty("Key") .GetGetMethod(); var getValue = pairType .GetProperty("Value") .GetGetMethod(); var writeKey = new KeySerializerVisitor().Visit( 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 writeEndObject = typeof(Utf8JsonWriter) .GetMethod(nameof(Utf8JsonWriter.WriteEndObject), Type.EmptyTypes); var dispose = typeof(IDisposable) .GetMethod(nameof(IDisposable.Dispose), Type.EmptyTypes); Expression expression; try { expression = BuildConversion(value, enumerable.Type); } catch (Exception exception) { throw new UnsupportedTypeException(type, $"Failed to map {mapSchema} to {type}.", exception); } return(JsonSerializerBuilderCaseResult.FromExpression( Expression.Block( new[] { enumerator }, Expression.Call(context.Writer, writeStartObject), Expression.Assign( enumerator, Expression.Call(expression, 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, writeEndObject)))); } else { return(JsonSerializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonMapSerializerBuilderCase)} can only be applied to dictionary types."))); } }