public ExternalMethodDefinition(IExternalServiceDefinition service, MethodId methodId)
 {
     Id      = methodId.CopyTo(new MethodId());
     Service = service;
 }
Esempio n. 2
0
        public async Task HandleAsync(PathString basePath, HttpContext context, CancellationToken ct)
        {
            var isQueryRequest   = context.Request.Method == "GET";
            var isCommandRequest = context.Request.Method == "POST";

            if (!isQueryRequest && !isCommandRequest)
            {
                await ReplyWithTextError(context.Response, 405, "Only GET and POST verbs are allowed");

                return;
            }

            var headers = context.Response.Headers;

            headers.Add(DasyncHttpHeaders.PoweredBy, "D-ASYNC");

            var basePathSegmentCount = basePath.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Length;
            var pathSegments         = context.Request.Path.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Skip(basePathSegmentCount).ToArray();

            if (pathSegments.Length == 0)
            {
                await ReplyWithTextError(context.Response, 404, "Empty request URL");

                return;
            }

            var serviceId = new ServiceId {
                Name = pathSegments[0]
            };

            if (!_serviceResolver.TryResolve(serviceId, out var serviceReference))
            {
                await ReplyWithTextError(context.Response, 404, $"Service '{serviceId.Name}' is not registered");

                return;
            }

            if (serviceReference.Definition.Type == ServiceType.External)
            {
                await ReplyWithTextError(context.Response, 404, $"Cannot invoke external service '{serviceReference.Definition.Name}' on your behalf");

                return;
            }

            if (pathSegments.Length == 1)
            {
                await ReplyWithTextError(context.Response, 404, "The request URL does not contain a service method");

                return;
            }

            var methodId = new MethodId {
                Name = pathSegments[1]
            };
            string methodIntentId = null;

            if (pathSegments.Length == 3)
            {
                methodIntentId = pathSegments[2];

                if (isQueryRequest)
                {
                    await HandleResultPoll(context, serviceReference.Id, methodId, methodIntentId);

                    return;
                }
            }

            if (pathSegments.Length > 3)
            {
                await ReplyWithTextError(context.Response, 404, "The request URL contains extra segments");

                return;
            }

            var         contentType = context.Request.GetContentType();
            ISerializer serializer;

            try
            {
                serializer = GetSerializer(contentType, isQueryRequest);
            }
            catch (ArgumentException)
            {
                await ReplyWithTextError(context.Response, 406, $"The Content-Type '{contentType.MediaType}' is not supported");

                return;
            }

            if (!_methodResolver.TryResolve(serviceReference.Definition, methodId, out var methodReference))
            {
                await ReplyWithTextError(context.Response, 404, $"The service '{serviceReference.Definition.Name}' does not have method '{methodId.Name}'");

                return;
            }

            if (isQueryRequest && !methodReference.Definition.IsQuery)
            {
                await ReplyWithTextError(context.Response, 404, $"The method '{methodId.Name}' of service '{serviceReference.Definition.Name}' is a command, but not a query, thus must be invoked with the POST verb");

                return;
            }

            MethodInvocationData   invokeData   = null;
            MethodContinuationData continueData = null;
            IValueContainer        parametersContainer;
            bool compressResponse    = false;
            bool respondWithEnvelope = false;

            if (isQueryRequest)
            {
                var inputObject = new JObject();

                foreach (var kvPair in context.Request.Query)
                {
                    if (kvPair.Value.Count == 0)
                    {
                        continue;
                    }

                    var parameterName = kvPair.Key;

                    if (kvPair.Value.Count == 1)
                    {
                        var parameterValue = kvPair.Value[0];
                        inputObject.Add(parameterName, parameterValue);
                    }
                    else
                    {
                        var values = new JArray();
                        foreach (var value in kvPair.Value)
                        {
                            values.Add(value);
                        }
                        inputObject.Add(parameterName, values);
                    }
                }

                var parametersJson = inputObject.ToString();

                parametersContainer = new SerializedValueContainer(parametersJson, _jsonSerializer);

                invokeData = new MethodInvocationData
                {
                    Service    = serviceId,
                    Method     = methodId,
                    Parameters = parametersContainer,
                    Caller     = GetCaller(context.Request.Headers)
                };
            }
            else
            {
                var payloadStream = context.Request.Body;

                var encoding = context.Request.GetContentEncoding();
                if (!string.IsNullOrEmpty(encoding))
                {
                    if ("gzip".Equals(encoding, StringComparison.OrdinalIgnoreCase))
                    {
                        compressResponse = true;
                        payloadStream    = new GZipStream(payloadStream, CompressionMode.Decompress, leaveOpen: true);
                    }
                    else if ("deflate".Equals(encoding, StringComparison.OrdinalIgnoreCase))
                    {
                        payloadStream = new DeflateStream(payloadStream, CompressionMode.Decompress, leaveOpen: true);
                    }
                    else
                    {
                        await ReplyWithTextError(context.Response, 406, $"The Content-Encoding '{encoding}' is not supported");

                        return;
                    }
                }

                var envelopeType = context.Request.Headers.GetValue(DasyncHttpHeaders.Envelope);

                if (string.IsNullOrEmpty(envelopeType))
                {
                    var payload = await payloadStream.ToBytesAsync();

                    parametersContainer = new SerializedValueContainer(payload, serializer);

                    if (string.IsNullOrEmpty(methodIntentId))
                    {
                        invokeData = new MethodInvocationData
                        {
                            Service    = serviceId,
                            Method     = methodId,
                            Parameters = parametersContainer,
                            Caller     = GetCaller(context.Request.Headers)
                        };
                    }
                    else
                    {
                        continueData = new MethodContinuationData
                        {
                            Service = serviceId,
                            Method  = methodId.CopyTo(new PersistedMethodId()),
                            Result  = parametersContainer,
                            Caller  = GetCaller(context.Request.Headers)
                        };
                        continueData.Method.IntentId = methodIntentId;
                        // TODO: get ETag from the query string
                    }
                }
                else if (envelopeType.Equals("invoke", StringComparison.OrdinalIgnoreCase))
                {
                    respondWithEnvelope = true;
                    invokeData          = await DeserializeAsync <MethodInvocationData>(serializer, payloadStream);
                }
                else if (envelopeType.Equals("continue", StringComparison.OrdinalIgnoreCase))
                {
                    respondWithEnvelope = true;
                    continueData        = await DeserializeAsync <MethodContinuationData>(serializer, payloadStream);
                }
                else
                {
                    await ReplyWithTextError(context.Response, 406, $"Unknown envelope type '{envelopeType}'");

                    return;
                }
            }

            var intentId              = context.Request.Headers.GetValue(DasyncHttpHeaders.IntentId) ?? _idGenerator.NewId();
            var externalRequestId     = context.Request.Headers.GetValue(DasyncHttpHeaders.RequestId);
            var externalCorrelationId = context.Request.Headers.GetValue(DasyncHttpHeaders.CorrelationId);
            var isRetry = context.Request.Headers.IsRetry();

            var rfc7240Preferences             = context.Request.Headers.GetRFC7240Preferences();
            var isHttpRequestBlockingExecution = !(rfc7240Preferences.RespondAsync == true);
            var waitTime = rfc7240Preferences.Wait;

            if (waitTime > MaxLongPollTime)
            {
                waitTime = MaxLongPollTime;
            }
            var waitForResult = isHttpRequestBlockingExecution || waitTime > TimeSpan.Zero;

            var communicatorMessage = new HttpCommunicatorMessage
            {
                IsRetry       = isRetry,
                RequestId     = externalRequestId,
                WaitForResult = waitForResult
            };

            if (invokeData != null)
            {
                if (invokeData.IntentId == null)
                {
                    invokeData.IntentId = intentId;
                }

                var invokeTask = _localTransitionRunner.RunAsync(invokeData, communicatorMessage);

                if (isHttpRequestBlockingExecution)
                {
                    var invocationResult = await invokeTask;

                    if (invocationResult.Outcome == InvocationOutcome.Complete)
                    {
                        await RespondWithMethodResult(context.Response, invocationResult.Result, serializer, respondWithEnvelope, compressResponse);

                        return;
                    }

                    if (waitForResult)
                    {
                        var taskResult = await TryWaitForResultAsync(
                            serviceReference.Id,
                            methodId, intentId, waitTime);

                        if (taskResult != null)
                        {
                            await RespondWithMethodResult(context.Response, taskResult, serializer, respondWithEnvelope, compressResponse);

                            return;
                        }
                    }
                }
                else
                {
                    // TODO: continue 'invokeTask' and handle exceptions in background
                }

                communicatorMessage.WaitForResult = false;

                var location = string.Concat(context.Request.Path, "/", intentId);
                context.Response.Headers.Add("Location", location);
                context.Response.Headers.Add(DasyncHttpHeaders.IntentId, intentId);
                context.Response.StatusCode = DasyncHttpCodes.Scheduled;
            }
            else if (continueData != null)
            {
                if (continueData.IntentId == null)
                {
                    continueData.IntentId = intentId;
                }

                var continueTask = _localTransitionRunner.ContinueAsync(continueData, communicatorMessage);
                // TODO: continue 'continueTask' in backgraound to handle exceptions

                context.Response.Headers.Add("Location", context.Request.Path.ToString());
                context.Response.Headers.Add(DasyncHttpHeaders.IntentId, continueData.Method.IntentId);
                context.Response.StatusCode = DasyncHttpCodes.Scheduled;
            }
        }