Пример #1
0
        /// <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, JsonDeserializerBuilderContext 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));
        }
Пример #2
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="LongSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="LongSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="long" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is LongSchema longSchema)
            {
                var getInt64 = typeof(Utf8JsonReader)
                               .GetMethod(nameof(Utf8JsonReader.GetInt64), Type.EmptyTypes);

                try
                {
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(Expression.Call(context.Reader, getInt64), type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {longSchema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonLongDeserializerBuilderCase)} can only be applied to {nameof(LongSchema)}s.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="FixedSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="FixedSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="T:System.Byte[]" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is FixedSchema fixedSchema)
            {
                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                var getBytes = typeof(Encoding)
                               .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) });

                var getUnexpectedSizeException = typeof(JsonExceptionHelper)
                                                 .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedSizeException));

                var bytes = Expression.Parameter(typeof(byte[]));

                try
                {
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(
                                   Expression.Block(
                                       new[] { bytes },
                                       Expression.IfThen(
                                           Expression.NotEqual(
                                               Expression.Property(context.Reader, tokenType),
                                               Expression.Constant(JsonTokenType.String)),
                                           Expression.Throw(
                                               Expression.Call(
                                                   null,
                                                   getUnexpectedTokenException,
                                                   context.Reader,
                                                   Expression.Constant(new[] { JsonTokenType.String })))),
                                       Expression.Assign(
                                           bytes,
                                           Expression.Call(
                                               Expression.Constant(JsonEncoding.Bytes),
                                               getBytes,
                                               Expression.Call(context.Reader, getString))),
                                       Expression.IfThen(
                                           Expression.NotEqual(
                                               Expression.ArrayLength(bytes),
                                               Expression.Constant(fixedSchema.Size)),
                                           Expression.Throw(
                                               Expression.Call(
                                                   null,
                                                   getUnexpectedSizeException,
                                                   context.Reader,
                                                   Expression.Constant(fixedSchema.Size),
                                                   Expression.ArrayLength(bytes)))),
                                       bytes),
                                   type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {fixedSchema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonFixedDeserializerBuilderCase)} can only be applied to {nameof(FixedSchema)}s.")));
            }
        }
Пример #4
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="DecimalLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// has a <see cref="DecimalLogicalType" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// 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 JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema.LogicalType is DecimalLogicalType decimalLogicalType)
            {
                var precision = decimalLogicalType.Precision;
                var scale     = decimalLogicalType.Scale;

                var bytes = Expression.Variable(typeof(byte[]));

                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                var getBytes = typeof(Encoding)
                               .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) });

                var getUnexpectedSizeException = typeof(JsonExceptionHelper)
                                                 .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedSizeException));

                Expression expression;

                if (schema is BytesSchema)
                {
                    expression = Expression.Block(
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.Property(context.Reader, tokenType),
                                Expression.Constant(JsonTokenType.String)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedTokenException,
                                    context.Reader,
                                    Expression.Constant(new[] { JsonTokenType.String })))),
                        Expression.Assign(
                            bytes,
                            Expression.Call(
                                Expression.Constant(JsonEncoding.Bytes),
                                getBytes,
                                Expression.Call(context.Reader, getString))),
                        bytes);
                }
                else if (schema is FixedSchema fixedSchema)
                {
                    expression = Expression.Block(
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.Property(context.Reader, tokenType),
                                Expression.Constant(JsonTokenType.String)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedTokenException,
                                    context.Reader,
                                    Expression.Constant(new[] { JsonTokenType.String })))),
                        Expression.Assign(
                            bytes,
                            Expression.Call(
                                Expression.Constant(JsonEncoding.Bytes),
                                getBytes,
                                Expression.Call(context.Reader, getString))),
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.ArrayLength(bytes),
                                Expression.Constant(fixedSchema.Size)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedSizeException,
                                    context.Reader,
                                    Expression.Constant(fixedSchema.Size),
                                    Expression.ArrayLength(bytes)))),
                        bytes);
                }
                else
                {
                    throw new UnsupportedSchemaException(schema);
                }

                // declare variables for in-place transformation:
                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(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(expression, type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDecimalDeserializerBuilderCase)} can only be applied schemas with a {nameof(DecimalLogicalType)}.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for an <see cref="ArraySchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="type" />
        /// is an enumerable type and <paramref name="schema" /> is an <see cref="ArraySchema" />;
        /// an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> 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 JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext 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 readItem = DeserializerBuilder
                                   .BuildExpression(itemType, arraySchema.Item, context);

                    var collection = Expression.Parameter(instantiateCollection.Type);
                    var loop       = Expression.Label();

                    var tokenType = typeof(Utf8JsonReader)
                                    .GetProperty(nameof(Utf8JsonReader.TokenType));

                    var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                      .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                    var read = typeof(Utf8JsonReader)
                               .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes);

                    var add = collection.Type.GetMethod("Add", new[] { readItem.Type });

                    Expression expression = Expression.Block(
                        new[] { collection },
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.Property(context.Reader, tokenType),
                                Expression.Constant(JsonTokenType.StartArray)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedTokenException,
                                    context.Reader,
                                    Expression.Constant(new[] { JsonTokenType.StartArray })))),
                        Expression.Assign(collection, instantiateCollection),
                        Expression.Loop(
                            Expression.Block(
                                Expression.Call(context.Reader, read),
                                Expression.IfThen(
                                    Expression.Equal(
                                        Expression.Property(context.Reader, tokenType),
                                        Expression.Constant(JsonTokenType.EndArray)),
                                    Expression.Break(loop)),
                                Expression.Call(collection, add, readItem)),
                            loop),
                        collection);

                    if (!type.IsAssignableFrom(expression.Type) && GetCollectionConstructor(type) is ConstructorInfo constructor)
                    {
                        expression = Expression.New(constructor, expression);
                    }

                    try
                    {
                        return(JsonDeserializerBuilderCaseResult.FromExpression(
                                   BuildConversion(expression, type)));
                    }
                    catch (InvalidOperationException exception)
                    {
                        throw new UnsupportedTypeException(type, $"Failed to map {arraySchema} to {type}.", exception);
                    }
                }
                else
                {
                    return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonArrayDeserializerBuilderCase)} can only be applied to enumerable types.")));
                }
            }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="DurationLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful result if <paramref name="type" /> the schema’s logical type is a
        /// <see cref="DurationLogicalType" />; an unsuccessful result otherwise.
        /// </returns>
        /// <exception cref="UnsupportedSchemaException">
        /// Thrown when the schema is not a <see cref="FixedSchema" /> with size 12 and logical
        /// type <see cref="DurationLogicalType" />.
        /// </exception>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="TimeSpan" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema.LogicalType is DurationLogicalType)
            {
                if (!(schema is FixedSchema fixedSchema && fixedSchema.Size == 12))
                {
                    throw new UnsupportedSchemaException(schema);
                }

                var bytes = Expression.Parameter(typeof(byte[]));

                var copy = typeof(Array)
                           .GetMethod(nameof(Array.Copy), new[] { typeof(Array), typeof(int), typeof(Array), typeof(int), typeof(int) });

                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                var getBytes = typeof(Encoding)
                               .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) });

                var getUnexpectedSizeException = typeof(JsonExceptionHelper)
                                                 .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedSizeException));

                var reverse = typeof(Array)
                              .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) });

                var toUInt32 = typeof(BitConverter)
                               .GetMethod(nameof(BitConverter.ToUInt32), new[] { typeof(byte[]), typeof(int) });

                Expression Read(Expression offset)
                {
                    var component = Expression.Variable(typeof(byte[]));

                    var expressions = new List <Expression>
                    {
                        Expression.Assign(
                            component,
                            Expression.NewArrayBounds(typeof(byte), Expression.Constant(4))),
                        Expression.Call(null, copy, bytes, offset, component, Expression.Constant(0), Expression.ArrayLength(component)),
                    };

                    if (!BitConverter.IsLittleEndian)
                    {
                        expressions.Add(Expression.Call(null, reverse, Expression.Convert(component, typeof(Array))));
                    }

                    expressions.Add(component);

                    return(Expression.ConvertChecked(
                               Expression.Call(
                                   null,
                                   toUInt32,
                                   Expression.Block(
                                       new[] { component },
                                       expressions),
                                   Expression.Constant(0)),
                               typeof(long)));
                }

                var exceptionConstructor = typeof(OverflowException)
                                           .GetConstructor(new[] { typeof(string) });

                var timeSpanConstructor = typeof(TimeSpan)
                                          .GetConstructor(new[] { typeof(long) });

                try
                {
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(
                                   Expression.Block(
                                       new[] { bytes },
                                       Expression.IfThen(
                                           Expression.NotEqual(
                                               Expression.Property(context.Reader, tokenType),
                                               Expression.Constant(JsonTokenType.String)),
                                           Expression.Throw(
                                               Expression.Call(
                                                   null,
                                                   getUnexpectedTokenException,
                                                   context.Reader,
                                                   Expression.Constant(new[] { JsonTokenType.String })))),
                                       Expression.Assign(
                                           bytes,
                                           Expression.Call(
                                               Expression.Constant(JsonEncoding.Bytes),
                                               getBytes,
                                               Expression.Call(context.Reader, getString))),
                                       Expression.IfThen(
                                           Expression.NotEqual(
                                               Expression.ArrayLength(bytes),
                                               Expression.Constant(fixedSchema.Size)),
                                           Expression.Throw(
                                               Expression.Call(
                                                   null,
                                                   getUnexpectedSizeException,
                                                   context.Reader,
                                                   Expression.Constant(fixedSchema.Size),
                                                   Expression.ArrayLength(bytes)))),
                                       Expression.IfThen(
                                           Expression.NotEqual(Read(Expression.Constant(0)), Expression.Constant(0L)),
                                           Expression.Throw(
                                               Expression.New(
                                                   exceptionConstructor,
                                                   Expression.Constant("Durations containing months cannot be accurately deserialized to a TimeSpan.")))),
                                       Expression.New(
                                           timeSpanConstructor,
                                           Expression.AddChecked(
                                               Expression.MultiplyChecked(Read(Expression.Constant(4)), Expression.Constant(TimeSpan.TicksPerDay)),
                                               Expression.MultiplyChecked(Read(Expression.Constant(8)), Expression.Constant(TimeSpan.TicksPerMillisecond))))),
                                   type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDurationDeserializerBuilderCase)} can only be applied schemas with a {nameof(DurationLogicalType)}.")));
            }
        }
Пример #7
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="StringSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="StringSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="string" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is StringSchema stringSchema)
            {
                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                try
                {
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(
                                   Expression.Block(
                                       Expression.IfThen(
                                           Expression.And(
                                               Expression.NotEqual(
                                                   Expression.Property(context.Reader, tokenType),
                                                   Expression.Constant(JsonTokenType.PropertyName)),
                                               Expression.NotEqual(
                                                   Expression.Property(context.Reader, tokenType),
                                                   Expression.Constant(JsonTokenType.String))),
                                           Expression.Throw(
                                               Expression.Call(
                                                   null,
                                                   getUnexpectedTokenException,
                                                   context.Reader,
                                                   Expression.Constant(new[] { JsonTokenType.PropertyName, JsonTokenType.String })))),
                                       Expression.Call(context.Reader, getString)),
                                   type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {stringSchema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonStringDeserializerBuilderCase)} can only be applied to {nameof(StringSchema)}s.")));
            }
        }
Пример #8
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="MapSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="type" />
        /// is a dictionary type and <paramref name="schema" /> is a <see cref="MapSchema" />; an
        /// unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> 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 JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext 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 readKey = DeserializerBuilder
                                  .BuildExpression(keyType, new StringSchema(), context);

                    var readValue = DeserializerBuilder
                                    .BuildExpression(valueType, mapSchema.Value, context);

                    var dictionary = Expression.Parameter(instantiateDictionary.Type);
                    var key        = Expression.Parameter(readKey.Type);
                    var loop       = Expression.Label();

                    var tokenType = typeof(Utf8JsonReader)
                                    .GetProperty(nameof(Utf8JsonReader.TokenType));

                    var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                      .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                    var read = typeof(Utf8JsonReader)
                               .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes);

                    var add = dictionary.Type.GetMethod("Add", new[] { readKey.Type, readValue.Type });

                    Expression expression = Expression.Block(
                        new[] { dictionary },
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.Property(context.Reader, tokenType),
                                Expression.Constant(JsonTokenType.StartObject)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedTokenException,
                                    context.Reader,
                                    Expression.Constant(new[] { JsonTokenType.StartObject })))),
                        Expression.Assign(dictionary, instantiateDictionary),
                        Expression.Loop(
                            Expression.Block(
                                new[] { key },
                                Expression.Call(context.Reader, read),
                                Expression.IfThen(
                                    Expression.Equal(
                                        Expression.Property(context.Reader, tokenType),
                                        Expression.Constant(JsonTokenType.EndObject)),
                                    Expression.Break(loop)),
                                Expression.Assign(key, readKey),
                                Expression.Call(context.Reader, read),
                                Expression.Call(dictionary, add, key, readValue)),
                            loop),
                        dictionary);

                    if (!type.IsAssignableFrom(expression.Type) && GetDictionaryConstructor(type) is ConstructorInfo constructor)
                    {
                        expression = Expression.New(constructor, expression);
                    }

                    try
                    {
                        return(JsonDeserializerBuilderCaseResult.FromExpression(
                                   BuildConversion(expression, type)));
                    }
                    catch (InvalidOperationException exception)
                    {
                        throw new UnsupportedTypeException(type, $"Failed to map {mapSchema} to {type}.", exception);
                    }
                }
                else
                {
                    return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonMapDeserializerBuilderCase)} can only be applied to dictionary types.")));
                }
            }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for an <see cref="EnumSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is an <see cref="EnumSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// 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 JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is EnumSchema enumSchema)
            {
                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                Expression expression = Expression.Call(context.Reader, getString);

                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);

                // find a match for each enum in the schema:
                var cases = underlying.IsEnum
                    ? enumSchema.Symbols
                            .Select(symbol =>
                {
                    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(symbol)));
                })
                    : enumSchema.Symbols
                            .Select(symbol =>
                {
                    return(Expression.SwitchCase(
                               BuildConversion(Expression.Constant(symbol), type),
                               Expression.Constant(symbol)));
                });

                var position = typeof(Utf8JsonReader)
                               .GetProperty(nameof(Utf8JsonReader.TokenStartIndex))
                               .GetGetMethod();

                var exceptionConstructor = typeof(InvalidEncodingException)
                                           .GetConstructor(new[] { typeof(long), typeof(string), typeof(Exception) });

                // generate a switch on the symbol:
                return(JsonDeserializerBuilderCaseResult.FromExpression(
                           Expression.Switch(
                               expression,
                               Expression.Throw(
                                   Expression.New(
                                       exceptionConstructor,
                                       Expression.Property(context.Reader, position),
                                       Expression.Constant($"Invalid enum symbol."),
                                       Expression.Constant(null, typeof(Exception))),
                                   type),
                               cases.ToArray())));
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonEnumDeserializerBuilderCase)} can only be applied to {nameof(EnumSchema)}s.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="UnionSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="UnionSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// 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 JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext 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 tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var read = typeof(Utf8JsonReader)
                           .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes);

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                var getUnknownUnionMemberException = typeof(JsonExceptionHelper)
                                                     .GetMethod(nameof(JsonExceptionHelper.GetUnknownUnionMemberException));

                var schemas    = unionSchema.Schemas.ToList();
                var candidates = schemas.Where(s => s is not NullSchema).ToList();
                var @null      = schemas.Find(s => s is NullSchema);

                var cases = candidates.Select(child =>
                {
                    var selected = SelectType(type, child);

                    return(Expression.SwitchCase(
                               BuildConversion(
                                   Expression.Block(
                                       Expression.Call(context.Reader, read),
                                       DeserializerBuilder.BuildExpression(selected, child, context)),
                                   type),
                               Expression.Constant(GetSchemaName(child))));
                }).ToArray();

                var value = Expression.Parameter(type);

                Expression expression = Expression.Block(
                    new[] { value },
                    Expression.IfThen(
                        Expression.NotEqual(
                            Expression.Property(context.Reader, tokenType),
                            Expression.Constant(JsonTokenType.StartObject)),
                        Expression.Throw(
                            Expression.Call(
                                null,
                                getUnexpectedTokenException,
                                context.Reader,
                                Expression.Constant(new[] { JsonTokenType.StartObject })))),
                    Expression.Call(context.Reader, read),
                    Expression.Assign(
                        value,
                        Expression.Switch(
                            Expression.Call(context.Reader, getString),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnknownUnionMemberException,
                                    context.Reader),
                                type),
                            cases)),
                    Expression.Call(context.Reader, read),
                    Expression.IfThen(
                        Expression.NotEqual(
                            Expression.Property(context.Reader, tokenType),
                            Expression.Constant(JsonTokenType.EndObject)),
                        Expression.Throw(
                            Expression.Call(
                                null,
                                getUnexpectedTokenException,
                                context.Reader,
                                Expression.Constant(new[] { JsonTokenType.EndObject })))),
                    value);

                if (@null != null)
                {
                    var selected   = SelectType(type, @null);
                    var underlying = Nullable.GetUnderlyingType(selected);

                    if (selected.IsValueType && underlying == null)
                    {
                        throw new UnsupportedTypeException(type, $"A deserializer for a union containing {typeof(NullSchema)} cannot be built for {selected}.");
                    }

                    expression = Expression.Condition(
                        Expression.Equal(
                            Expression.Property(context.Reader, tokenType),
                            Expression.Constant(JsonTokenType.Null)),
                        BuildConversion(
                            DeserializerBuilder.BuildExpression(selected, @null, context),
                            type),
                        expression);
                }

                return(JsonDeserializerBuilderCaseResult.FromExpression(expression));
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonUnionDeserializerBuilderCase)} can only be applied to {nameof(UnionSchema)}s.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="TimestampLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// has a <see cref="TimestampLogicalType" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// 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 JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema.LogicalType is TimestampLogicalType)
            {
                if (schema is not LongSchema)
                {
                    throw new UnsupportedSchemaException(schema);
                }

                Expression epoch = Expression.Constant(Epoch);
                Expression factor;

                if (schema.LogicalType is MicrosecondTimestampLogicalType)
                {
                    factor = Expression.Constant(TimeSpan.TicksPerMillisecond / 1000);
                }
                else if (schema.LogicalType is MillisecondTimestampLogicalType)
                {
                    factor = Expression.Constant(TimeSpan.TicksPerMillisecond);
                }
                else
                {
                    throw new UnsupportedSchemaException(schema);
                }

                var getInt64 = typeof(Utf8JsonReader)
                               .GetMethod(nameof(Utf8JsonReader.GetInt64), Type.EmptyTypes);

                Expression expression = Expression.Call(context.Reader, getInt64);

                var addTicks = typeof(DateTime)
                               .GetMethod(nameof(DateTime.AddTicks));

                try
                {
                    // return Epoch.AddTicks(value * factor);
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(
                                   Expression.Call(epoch, addTicks, Expression.Multiply(expression, factor)),
                                   type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonTimestampDeserializerBuilderCase)} can only be applied schemas with a {nameof(TimestampLogicalType)}.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="RecordSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="type" />
        /// is not an array or primitive type and <paramref name="schema" /> is a <see cref="RecordSchema" />;
        /// an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> otherwise.
        /// </returns>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext 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(), type));

                    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;

                        var loop = Expression.Label();

                        var tokenType = typeof(Utf8JsonReader)
                                        .GetProperty(nameof(Utf8JsonReader.TokenType));

                        var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                          .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                        var read = typeof(Utf8JsonReader)
                                   .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes);

                        var getString = typeof(Utf8JsonReader)
                                        .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                        var getUnknownRecordFieldException = typeof(JsonExceptionHelper)
                                                             .GetMethod(nameof(JsonExceptionHelper.GetUnknownRecordFieldException));

                        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(
                                    Field: field,
                                    Match: match,
                                    Parameter: parameter,
                                    Assignment: (Expression)Expression.Block(
                                        Expression.Call(context.Reader, read),
                                        Expression.Assign(
                                            parameter,
                                            DeserializerBuilder.BuildExpression(match.ParameterType, field.Type, context))));
                            })
                                          .ToDictionary(r => r.Match);

                            expression = Expression.Block(
                                mapping
                                .Select(d => d.Value.Parameter),
                                Expression.IfThen(
                                    Expression.NotEqual(
                                        Expression.Property(context.Reader, tokenType),
                                        Expression.Constant(JsonTokenType.StartObject)),
                                    Expression.Throw(
                                        Expression.Call(
                                            null,
                                            getUnexpectedTokenException,
                                            context.Reader,
                                            Expression.Constant(new[] { JsonTokenType.StartObject })))),
                                Expression.Loop(
                                    Expression.Block(
                                        Expression.Call(context.Reader, read),
                                        Expression.IfThen(
                                            Expression.Equal(
                                                Expression.Property(context.Reader, tokenType),
                                                Expression.Constant(JsonTokenType.EndObject)),
                                            Expression.Break(loop)),
                                        Expression.Switch(
                                            Expression.Call(context.Reader, getString),
                                            Expression.Throw(
                                                Expression.Call(
                                                    null,
                                                    getUnknownRecordFieldException,
                                                    context.Reader)),
                                            mapping
                                            .Select(pair =>
                                                    Expression.SwitchCase(
                                                        Expression.Block(pair.Value.Assignment, Expression.Empty()),
                                                        Expression.Constant(pair.Value.Field.Name)))
                                            .ToArray())),
                                    loop),
                                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 },
                                Expression.Assign(value, Expression.New(value.Type)),
                                Expression.IfThen(
                                    Expression.NotEqual(
                                        Expression.Property(context.Reader, tokenType),
                                        Expression.Constant(JsonTokenType.StartObject)),
                                    Expression.Throw(
                                        Expression.Call(
                                            null,
                                            getUnexpectedTokenException,
                                            context.Reader,
                                            Expression.Constant(new[] { JsonTokenType.StartObject })))),
                                Expression.Loop(
                                    Expression.Block(
                                        Expression.Call(context.Reader, read),
                                        Expression.IfThen(
                                            Expression.Equal(
                                                Expression.Property(context.Reader, tokenType),
                                                Expression.Constant(JsonTokenType.EndObject)),
                                            Expression.Break(loop)),
                                        Expression.Switch(
                                            Expression.Call(context.Reader, getString),
                                            Expression.Throw(
                                                Expression.Call(
                                                    null,
                                                    getUnknownRecordFieldException,
                                                    context.Reader)),
                                            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.SwitchCase(
                                           Expression.Block(
                                               Expression.Call(context.Reader, read),
                                               expression,
                                               Expression.Empty()),
                                           Expression.Constant(field.Name)));
                            })
Пример #13
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="NullSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="NullSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is NullSchema)
            {
                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                return(JsonDeserializerBuilderCaseResult.FromExpression(
                           Expression.Block(
                               Expression.IfThen(
                                   Expression.NotEqual(
                                       Expression.Property(context.Reader, tokenType),
                                       Expression.Constant(JsonTokenType.Null)),
                                   Expression.Throw(
                                       Expression.Call(
                                           null,
                                           getUnexpectedTokenException,
                                           context.Reader,
                                           Expression.Constant(new[] { JsonTokenType.Null })))),
                               Expression.Default(type))));
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonNullDeserializerBuilderCase)} can only be applied to {nameof(NullSchema)}s.")));
            }
        }