Example #1
0
    private HubMessage?ParseMessage(ReadOnlySequence <byte> input, IInvocationBinder binder)
    {
        try
        {
            // We parse using the Utf8JsonReader directly but this has a problem. Some of our properties are dependent on other properties
            // and since reading the json might be unordered, we need to store the parsed content as JsonDocument to re-parse when true types are known.
            // if we're lucky and the state we need to directly parse is available, then we'll use it.

            int?       type         = null;
            string?    invocationId = null;
            string?    target       = null;
            string?    error        = null;
            var        hasItem      = false;
            object?    item         = null;
            var        hasResult    = false;
            object?    result       = null;
            var        hasArguments = false;
            object?[]? arguments    = null;
            string[]? streamIds = null;
            bool                        hasArgumentsToken        = false;
            Utf8JsonReader              argumentsToken           = default;
            bool                        hasItemsToken            = false;
            Utf8JsonReader              itemsToken               = default;
            bool                        hasResultToken           = false;
            Utf8JsonReader              resultToken              = default;
            ExceptionDispatchInfo?      argumentBindingException = null;
            Dictionary <string, string>?headers = null;
            var completed      = false;
            var allowReconnect = false;

            var reader = new Utf8JsonReader(input, isFinalBlock: true, state: default);

            reader.CheckRead();

            // We're always parsing a JSON object
            reader.EnsureObjectStart();

            do
            {
                switch (reader.TokenType)
                {
                case JsonTokenType.PropertyName:
                    if (reader.ValueTextEquals(TypePropertyNameBytes.EncodedUtf8Bytes))
                    {
                        type = reader.ReadAsInt32(TypePropertyName);

                        if (type == null)
                        {
                            throw new InvalidDataException($"Expected '{TypePropertyName}' to be of type {JsonTokenType.Number}.");
                        }
                    }
                    else if (reader.ValueTextEquals(InvocationIdPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        invocationId = reader.ReadAsString(InvocationIdPropertyName);
                    }
                    else if (reader.ValueTextEquals(StreamIdsPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        reader.CheckRead();

                        if (reader.TokenType != JsonTokenType.StartArray)
                        {
                            throw new InvalidDataException(
                                      $"Expected '{StreamIdsPropertyName}' to be of type {SystemTextJsonExtensions.GetTokenString(JsonTokenType.StartArray)}.");
                        }

                        var newStreamIds = new List <string>();
                        reader.Read();
                        while (reader.TokenType != JsonTokenType.EndArray)
                        {
                            newStreamIds.Add(reader.GetString() ?? throw new InvalidDataException($"Null value for '{StreamIdsPropertyName}' is not valid."));
                            reader.Read();
                        }

                        streamIds = newStreamIds.ToArray();
                    }
                    else if (reader.ValueTextEquals(TargetPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        target = reader.ReadAsString(TargetPropertyName);
                    }
                    else if (reader.ValueTextEquals(ErrorPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        error = reader.ReadAsString(ErrorPropertyName);
                    }
                    else if (reader.ValueTextEquals(AllowReconnectPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        allowReconnect = reader.ReadAsBoolean(AllowReconnectPropertyName);
                    }
                    else if (reader.ValueTextEquals(ResultPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        hasResult = true;

                        reader.CheckRead();

                        if (string.IsNullOrEmpty(invocationId))
                        {
                            // If we don't have an invocation id then we need to value copy the reader so we can parse it later
                            hasResultToken = true;
                            resultToken    = reader;
                            reader.Skip();
                        }
                        else
                        {
                            // If we have an invocation id already we can parse the end result
                            var returnType = binder.GetReturnType(invocationId);
                            result = BindType(ref reader, returnType);
                        }
                    }
                    else if (reader.ValueTextEquals(ItemPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        reader.CheckRead();

                        hasItem = true;

                        string?id = null;
                        if (!string.IsNullOrEmpty(invocationId))
                        {
                            id = invocationId;
                        }
                        else
                        {
                            // If we don't have an id yet then we need to value copy the reader so we can parse it later
                            hasItemsToken = true;
                            itemsToken    = reader;
                            reader.Skip();
                            continue;
                        }

                        try
                        {
                            var itemType = binder.GetStreamItemType(id);
                            item = BindType(ref reader, itemType);
                        }
                        catch (Exception ex)
                        {
                            return(new StreamBindingFailureMessage(id, ExceptionDispatchInfo.Capture(ex)));
                        }
                    }
                    else if (reader.ValueTextEquals(ArgumentsPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        reader.CheckRead();

                        int initialDepth = reader.CurrentDepth;
                        if (reader.TokenType != JsonTokenType.StartArray)
                        {
                            throw new InvalidDataException($"Expected '{ArgumentsPropertyName}' to be of type {SystemTextJsonExtensions.GetTokenString(JsonTokenType.StartArray)}.");
                        }

                        hasArguments = true;

                        if (string.IsNullOrEmpty(target))
                        {
                            // We don't know the method name yet so just value copy the reader so we can parse it later
                            hasArgumentsToken = true;
                            argumentsToken    = reader;
                            reader.Skip();
                        }
                        else
                        {
                            try
                            {
                                var paramTypes = binder.GetParameterTypes(target);
                                arguments = BindTypes(ref reader, paramTypes);
                            }
                            catch (Exception ex)
                            {
                                argumentBindingException = ExceptionDispatchInfo.Capture(ex);

                                // Could be at any point in argument array JSON when an error is thrown
                                // Read until the end of the argument JSON array
                                while (reader.CurrentDepth == initialDepth && reader.TokenType == JsonTokenType.StartArray ||
                                       reader.CurrentDepth > initialDepth)
                                {
                                    reader.CheckRead();
                                }
                            }
                        }
                    }
                    else if (reader.ValueTextEquals(HeadersPropertyNameBytes.EncodedUtf8Bytes))
                    {
                        reader.CheckRead();
                        headers = ReadHeaders(ref reader);
                    }
                    else
                    {
                        reader.CheckRead();
                        reader.Skip();
                    }
                    break;

                case JsonTokenType.EndObject:
                    completed = true;
                    break;
                }
            }while (!completed && reader.CheckRead());

            HubMessage message;

            switch (type)
            {
            case HubProtocolConstants.InvocationMessageType:
            {
                if (target is null)
                {
                    throw new InvalidDataException($"Missing required property '{TargetPropertyName}'.");
                }

                if (hasArgumentsToken)
                {
                    // We weren't able to bind the arguments because they came before the 'target', so try to bind now that we've read everything.
                    try
                    {
                        var paramTypes = binder.GetParameterTypes(target);
                        arguments = BindTypes(ref argumentsToken, paramTypes);
                    }
                    catch (Exception ex)
                    {
                        argumentBindingException = ExceptionDispatchInfo.Capture(ex);
                    }
                }

                message = argumentBindingException != null
                            ? new InvocationBindingFailureMessage(invocationId, target, argumentBindingException)
                            : BindInvocationMessage(invocationId, target, arguments, hasArguments, streamIds);
            }
            break;

            case HubProtocolConstants.StreamInvocationMessageType:
            {
                if (target is null)
                {
                    throw new InvalidDataException($"Missing required property '{TargetPropertyName}'.");
                }

                if (hasArgumentsToken)
                {
                    // We weren't able to bind the arguments because they came before the 'target', so try to bind now that we've read everything.
                    try
                    {
                        var paramTypes = binder.GetParameterTypes(target);
                        arguments = BindTypes(ref argumentsToken, paramTypes);
                    }
                    catch (Exception ex)
                    {
                        argumentBindingException = ExceptionDispatchInfo.Capture(ex);
                    }
                }

                message = argumentBindingException != null
                            ? new InvocationBindingFailureMessage(invocationId, target, argumentBindingException)
                            : BindStreamInvocationMessage(invocationId, target, arguments, hasArguments, streamIds);
            }
            break;

            case HubProtocolConstants.StreamItemMessageType:
                if (invocationId is null)
                {
                    throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
                }

                if (hasItemsToken)
                {
                    try
                    {
                        var returnType = binder.GetStreamItemType(invocationId);
                        item = BindType(ref itemsToken, returnType);
                    }
                    catch (JsonException ex)
                    {
                        message = new StreamBindingFailureMessage(invocationId, ExceptionDispatchInfo.Capture(ex));
                        break;
                    }
                }

                message = BindStreamItemMessage(invocationId, item, hasItem, binder);
                break;

            case HubProtocolConstants.CompletionMessageType:
                if (invocationId is null)
                {
                    throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
                }

                if (hasResultToken)
                {
                    var returnType = binder.GetReturnType(invocationId);
                    result = BindType(ref resultToken, returnType);
                }

                message = BindCompletionMessage(invocationId, error, result, hasResult, binder);
                break;

            case HubProtocolConstants.CancelInvocationMessageType:
                message = BindCancelInvocationMessage(invocationId);
                break;

            case HubProtocolConstants.PingMessageType:
                return(PingMessage.Instance);

            case HubProtocolConstants.CloseMessageType:
                return(BindCloseMessage(error, allowReconnect));

            case null:
                throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");

            default:
                // Future protocol changes can add message types, old clients can ignore them
                return(null);
            }

            return(ApplyHeaders(message, headers));
        }
        catch (JsonException jrex)
        {
            throw new InvalidDataException("Error reading JSON.", jrex);
        }
    }
Example #2
0
    private HubMessage?ParseMessage(Utf8BufferTextReader textReader, IInvocationBinder binder)
    {
        try
        {
            // We parse using the JsonTextReader directly but this has a problem. Some of our properties are dependent on other properties
            // and since reading the json might be unordered, we need to store the parsed content as JToken to re-parse when true types are known.
            // if we're lucky and the state we need to directly parse is available, then we'll use it.

            int?       type         = null;
            string?    invocationId = null;
            string?    target       = null;
            string?    error        = null;
            var        hasItem      = false;
            object?    item         = null;
            JToken?    itemToken    = null;
            var        hasResult    = false;
            object?    result       = null;
            JToken?    resultToken  = null;
            var        hasArguments = false;
            object?[]? arguments    = null;
            string[]? streamIds = null;
            JArray?argumentsToken = null;
            ExceptionDispatchInfo?      argumentBindingException = null;
            Dictionary <string, string>?headers = null;
            var completed      = false;
            var allowReconnect = false;

            using (var reader = JsonUtils.CreateJsonTextReader(textReader))
            {
                reader.DateParseHandling = DateParseHandling.None;

                JsonUtils.CheckRead(reader);

                // We're always parsing a JSON object
                JsonUtils.EnsureObjectStart(reader);

                do
                {
                    switch (reader.TokenType)
                    {
                    case JsonToken.PropertyName:
                        var memberName = reader.Value?.ToString();

                        switch (memberName)
                        {
                        case TypePropertyName:
                            var messageType = JsonUtils.ReadAsInt32(reader, TypePropertyName);

                            if (messageType == null)
                            {
                                throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");
                            }

                            type = messageType.Value;
                            break;

                        case InvocationIdPropertyName:
                            invocationId = JsonUtils.ReadAsString(reader, InvocationIdPropertyName);
                            break;

                        case StreamIdsPropertyName:
                            JsonUtils.CheckRead(reader);

                            if (reader.TokenType != JsonToken.StartArray)
                            {
                                throw new InvalidDataException($"Expected '{StreamIdsPropertyName}' to be of type {JTokenType.Array}.");
                            }

                            var newStreamIds = new List <string>();
                            reader.Read();
                            while (reader.TokenType != JsonToken.EndArray)
                            {
                                newStreamIds.Add(reader.Value?.ToString() ?? throw new InvalidDataException($"Null value for '{StreamIdsPropertyName}' is not valid."));
                                reader.Read();
                            }

                            streamIds = newStreamIds.ToArray();
                            break;

                        case TargetPropertyName:
                            target = JsonUtils.ReadAsString(reader, TargetPropertyName);
                            break;

                        case ErrorPropertyName:
                            error = JsonUtils.ReadAsString(reader, ErrorPropertyName);
                            break;

                        case AllowReconnectPropertyName:
                            allowReconnect = JsonUtils.ReadAsBoolean(reader, AllowReconnectPropertyName);
                            break;

                        case ResultPropertyName:
                            hasResult = true;

                            if (string.IsNullOrEmpty(invocationId))
                            {
                                JsonUtils.CheckRead(reader);

                                // If we don't have an invocation id then we need to store it as a JToken so we can parse it later
                                resultToken = JToken.Load(reader);
                            }
                            else
                            {
                                // If we have an invocation id already we can parse the end result
                                var returnType = binder.GetReturnType(invocationId);

                                if (!JsonUtils.ReadForType(reader, returnType))
                                {
                                    throw new JsonReaderException("Unexpected end when reading JSON");
                                }

                                result = PayloadSerializer.Deserialize(reader, returnType);
                            }
                            break;

                        case ItemPropertyName:
                            JsonUtils.CheckRead(reader);

                            hasItem = true;

                            string?id = null;
                            if (!string.IsNullOrEmpty(invocationId))
                            {
                                id = invocationId;
                            }
                            else
                            {
                                // If we don't have an id yet then we need to store it as a JToken to parse later
                                itemToken = JToken.Load(reader);
                                break;
                            }

                            try
                            {
                                var itemType = binder.GetStreamItemType(id);
                                item = PayloadSerializer.Deserialize(reader, itemType);
                            }
                            catch (Exception ex)
                            {
                                return(new StreamBindingFailureMessage(id, ExceptionDispatchInfo.Capture(ex)));
                            }
                            break;

                        case ArgumentsPropertyName:
                            JsonUtils.CheckRead(reader);

                            int initialDepth = reader.Depth;
                            if (reader.TokenType != JsonToken.StartArray)
                            {
                                throw new InvalidDataException($"Expected '{ArgumentsPropertyName}' to be of type {JTokenType.Array}.");
                            }

                            hasArguments = true;

                            if (string.IsNullOrEmpty(target))
                            {
                                // We don't know the method name yet so just parse an array of generic JArray
                                argumentsToken = JArray.Load(reader);
                            }
                            else
                            {
                                try
                                {
                                    var paramTypes = binder.GetParameterTypes(target);
                                    arguments = BindArguments(reader, paramTypes);
                                }
                                catch (Exception ex)
                                {
                                    argumentBindingException = ExceptionDispatchInfo.Capture(ex);

                                    // Could be at any point in argument array JSON when an error is thrown
                                    // Read until the end of the argument JSON array
                                    while (reader.Depth == initialDepth && reader.TokenType == JsonToken.StartArray ||
                                           reader.Depth > initialDepth)
                                    {
                                        JsonUtils.CheckRead(reader);
                                    }
                                }
                            }
                            break;

                        case HeadersPropertyName:
                            JsonUtils.CheckRead(reader);
                            headers = ReadHeaders(reader);
                            break;

                        default:
                            // Skip read the property name
                            JsonUtils.CheckRead(reader);
                            // Skip the value for this property
                            reader.Skip();
                            break;
                        }
                        break;

                    case JsonToken.EndObject:
                        completed = true;
                        break;
                    }
                }while (!completed && JsonUtils.CheckRead(reader));
            }

            HubMessage message;

            switch (type)
            {
            case HubProtocolConstants.InvocationMessageType:
            {
                if (target is null)
                {
                    throw new InvalidDataException($"Missing required property '{TargetPropertyName}'.");
                }

                if (argumentsToken != null)
                {
                    // We weren't able to bind the arguments because they came before the 'target', so try to bind now that we've read everything.
                    try
                    {
                        var paramTypes = binder.GetParameterTypes(target);
                        arguments = BindArguments(argumentsToken, paramTypes);
                    }
                    catch (Exception ex)
                    {
                        argumentBindingException = ExceptionDispatchInfo.Capture(ex);
                    }
                }

                message = argumentBindingException != null
                            ? new InvocationBindingFailureMessage(invocationId, target, argumentBindingException)
                            : BindInvocationMessage(invocationId, target, arguments, hasArguments, streamIds, binder);
            }
            break;

            case HubProtocolConstants.StreamInvocationMessageType:
            {
                if (target is null)
                {
                    throw new InvalidDataException($"Missing required property '{TargetPropertyName}'.");
                }

                if (argumentsToken != null)
                {
                    // We weren't able to bind the arguments because they came before the 'target', so try to bind now that we've read everything.
                    try
                    {
                        var paramTypes = binder.GetParameterTypes(target);
                        arguments = BindArguments(argumentsToken, paramTypes);
                    }
                    catch (Exception ex)
                    {
                        argumentBindingException = ExceptionDispatchInfo.Capture(ex);
                    }
                }

                message = argumentBindingException != null
                            ? new InvocationBindingFailureMessage(invocationId, target, argumentBindingException)
                            : BindStreamInvocationMessage(invocationId, target, arguments, hasArguments, streamIds, binder);
            }
            break;

            case HubProtocolConstants.StreamItemMessageType:
                if (invocationId is null)
                {
                    throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
                }

                if (itemToken != null)
                {
                    try
                    {
                        var itemType = binder.GetStreamItemType(invocationId);
                        item = itemToken.ToObject(itemType, PayloadSerializer);
                    }
                    catch (Exception ex)
                    {
                        message = new StreamBindingFailureMessage(invocationId, ExceptionDispatchInfo.Capture(ex));
                        break;
                    };
                }

                message = BindStreamItemMessage(invocationId, item, hasItem, binder);
                break;

            case HubProtocolConstants.CompletionMessageType:
                if (invocationId is null)
                {
                    throw new InvalidDataException($"Missing required property '{InvocationIdPropertyName}'.");
                }

                if (resultToken != null)
                {
                    var returnType = binder.GetReturnType(invocationId);
                    result = resultToken.ToObject(returnType, PayloadSerializer);
                }

                message = BindCompletionMessage(invocationId, error, result, hasResult, binder);
                break;

            case HubProtocolConstants.CancelInvocationMessageType:
                message = BindCancelInvocationMessage(invocationId);
                break;

            case HubProtocolConstants.PingMessageType:
                return(PingMessage.Instance);

            case HubProtocolConstants.CloseMessageType:
                return(BindCloseMessage(error, allowReconnect));

            case null:
                throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");

            default:
                // Future protocol changes can add message types, old clients can ignore them
                return(null);
            }

            return(ApplyHeaders(message, headers));
        }
        catch (JsonReaderException jrex)
        {
            throw new InvalidDataException("Error reading JSON.", jrex);
        }
    }