Example #1
0
        /// <summary>
        /// Sends an HTTP request and processes the response.
        /// </summary>
        protected async Task <ServiceResult <TResponse> > TrySendRequestAsync <TRequest, TResponse>(HttpMethodMapping <TRequest, TResponse> mapping, TRequest request, CancellationToken cancellationToken)
            where TRequest : ServiceDto, new()
            where TResponse : ServiceDto, new()
        {
            if (mapping == null)
            {
                throw new ArgumentNullException(nameof(mapping));
            }
            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            try
            {
                // validate the request DTO
                if (!m_skipRequestValidation && !request.Validate(out var requestErrorMessage))
                {
                    return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest(requestErrorMessage)));
                }

                // make sure the request DTO doesn't violate any HTTP constraints
                var requestValidation = mapping.ValidateRequest(request);
                if (requestValidation.IsFailure)
                {
                    return(requestValidation.ToFailure());
                }

                // create the HTTP request with the right method, path, query string, and headers
                var requestHeaders    = mapping.GetRequestHeaders(request);
                var httpRequestResult = TryCreateHttpRequest(mapping.HttpMethod, mapping.Path, mapping.GetUriParameters(request), requestHeaders);
                if (httpRequestResult.IsFailure)
                {
                    return(httpRequestResult.ToFailure());
                }

                // create the request body if necessary
                using var httpRequest = httpRequestResult.Value;
                var requestBody = mapping.GetRequestBody(request);
                if (requestBody != null)
                {
                    var contentType = mapping.RequestBodyContentType ?? requestHeaders?.GetContentType();
                    httpRequest.Content = GetHttpContentSerializer(requestBody.GetType()).CreateHttpContent(requestBody, contentType);
                }

                // send the HTTP request and get the HTTP response
                using var httpResponse = await SendRequestAsync(httpRequest, request, cancellationToken).ConfigureAwait(false);

                // find the response mapping for the status code
                var statusCode      = httpResponse.StatusCode;
                var responseMapping = mapping.ResponseMappings.FirstOrDefault(x => x.StatusCode == statusCode);

                // fail if no response mapping can be found for the status code
                if (responseMapping == null)
                {
                    return(ServiceResult.Failure(await CreateErrorFromHttpResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false)));
                }

                // read the response body if necessary
                object?responseBody = null;
                if (responseMapping.ResponseBodyType != null)
                {
                    var serializer = GetHttpContentSerializer(responseMapping.ResponseBodyType);
                    ServiceResult <object> responseResult = await serializer.ReadHttpContentAsync(
                        responseMapping.ResponseBodyType, httpResponse.Content, cancellationToken).ConfigureAwait(false);

                    if (responseResult.IsFailure)
                    {
                        var error = responseResult.Error !;
                        error.Code = ServiceErrors.InvalidResponse;
                        return(ServiceResult.Failure(error));
                    }
                    responseBody = responseResult.Value;
                }

                // create the response DTO
                var response = responseMapping.CreateResponse(responseBody);
                response = mapping.SetResponseHeaders(response, HttpServiceUtility.CreateDictionaryFromHeaders(httpResponse.Headers, httpResponse.Content.Headers) !);

                // validate the response DTO
                if (!m_skipResponseValidation && !response.Validate(out var responseErrorMessage))
                {
                    return(ServiceResult.Failure(ServiceErrors.CreateInvalidResponse(responseErrorMessage)));
                }

                return(ServiceResult.Success(response));
            }
            catch (OperationCanceledException) when(!cancellationToken.IsCancellationRequested)
            {
                // HttpClient timeout
                return(ServiceResult.Failure(ServiceErrors.CreateTimeout()));
            }
            catch (Exception exception) when(ShouldCreateErrorFromException(exception))
            {
                // cancellation can cause the wrong exception
                cancellationToken.ThrowIfCancellationRequested();

                // error contacting service
                return(ServiceResult.Failure(CreateErrorFromException(exception)));
            }
        }
        /// <summary>
        /// Attempts to handle a service method.
        /// </summary>
        protected async Task <HttpResponseMessage> TryHandleServiceMethodAsync <TRequest, TResponse>(HttpMethodMapping <TRequest, TResponse> mapping, HttpRequestMessage httpRequest, Func <TRequest, CancellationToken, Task <ServiceResult <TResponse> > > invokeMethodAsync, CancellationToken cancellationToken)
            where TRequest : ServiceDto, new()
            where TResponse : ServiceDto, new()
        {
            if (httpRequest.Method != mapping.HttpMethod)
            {
                return(null);
            }

            var pathParameters = TryMatchHttpRoute(httpRequest.RequestUri, m_rootPath + mapping.Path);

            if (pathParameters == null)
            {
                return(null);
            }

            var context = new ServiceHttpContext();

            ServiceHttpContext.SetContext(httpRequest, context);

            var aspectHttpResponse = await AdaptTask(RequestReceivedAsync(httpRequest, cancellationToken)).ConfigureAwait(true);

            if (aspectHttpResponse != null)
            {
                return(aspectHttpResponse);
            }

            string mediaType = GetResponseMediaType(httpRequest);

            ServiceErrorDto error = null;

            object requestBody = null;

            if (mapping.RequestBodyType != null)
            {
                var requestResult = await AdaptTask(m_contentSerializer.ReadHttpContentAsync(mapping.RequestBodyType, httpRequest.Content, cancellationToken)).ConfigureAwait(true);

                if (requestResult.IsFailure)
                {
                    error = requestResult.Error;
                }
                else
                {
                    requestBody = requestResult.Value;
                }
            }

            TResponse response = null;

            if (error == null)
            {
                var request = mapping.CreateRequest(requestBody);

                var uriParameters = new Dictionary <string, string>();
                foreach (var queryParameter in ParseQueryString(httpRequest.RequestUri.Query))
                {
                    uriParameters[queryParameter.Key] = queryParameter.Value[0];
                }
                foreach (var pathParameter in pathParameters)
                {
                    uriParameters[pathParameter.Key] = pathParameter.Value;
                }
                request = mapping.SetUriParameters(request, uriParameters);
                request = mapping.SetRequestHeaders(request, HttpServiceUtility.CreateDictionaryFromHeaders(httpRequest.Headers));

                context.Request = request;

                var methodResult = await invokeMethodAsync(request, cancellationToken).ConfigureAwait(true);

                if (methodResult.IsFailure)
                {
                    error = methodResult.Error;
                }
                else
                {
                    response = methodResult.Value;
                }

                context.Result = error != null?ServiceResult.Failure(error) : ServiceResult.Success <ServiceDto>(response);
            }

            HttpResponseMessage httpResponse;

            if (error == null)
            {
                var responseMappingGroups = mapping.ResponseMappings
                                            .GroupBy(x => x.MatchesResponse(response))
                                            .Where(x => x.Key != false)
                                            .OrderByDescending(x => x.Key)
                                            .ToList();
                if (responseMappingGroups.Count >= 1 && responseMappingGroups[0].Count() == 1)
                {
                    var responseMapping = responseMappingGroups[0].Single();
                    httpResponse = new HttpResponseMessage(responseMapping.StatusCode);

                    var headersResult = HttpServiceUtility.TryAddHeaders(httpResponse.Headers, mapping.GetResponseHeaders(response));
                    if (headersResult.IsFailure)
                    {
                        throw new InvalidOperationException(headersResult.Error.Message);
                    }

                    if (responseMapping.ResponseBodyType != null)
                    {
                        httpResponse.Content = m_contentSerializer.CreateHttpContent(responseMapping.GetResponseBody(response), mediaType);
                    }
                }
                else
                {
                    throw new InvalidOperationException($"Found {responseMappingGroups.Sum(x => x.Count())} valid HTTP responses for {typeof(TResponse).Name}: {response}");
                }
            }
            else
            {
                var statusCode = error.Code == null ? HttpStatusCode.InternalServerError :
                                 (TryGetCustomHttpStatusCode(error.Code) ?? HttpServiceErrors.TryGetHttpStatusCode(error.Code) ?? HttpStatusCode.InternalServerError);
                httpResponse = new HttpResponseMessage(statusCode);
                if (statusCode != HttpStatusCode.NoContent && statusCode != HttpStatusCode.NotModified)
                {
                    httpResponse.Content = m_contentSerializer.CreateHttpContent(error, mediaType);
                }
            }

            httpResponse.RequestMessage = httpRequest;
            await AdaptTask(ResponseReadyAsync(httpResponse, cancellationToken)).ConfigureAwait(true);

            return(httpResponse);
        }
Example #3
0
        /// <summary>
        /// Attempts to handle a service method.
        /// </summary>
        protected async Task <HttpResponseMessage?> TryHandleServiceMethodAsync <TRequest, TResponse>(HttpMethodMapping <TRequest, TResponse> mapping, HttpRequestMessage httpRequest, Func <TRequest, CancellationToken, Task <ServiceResult <TResponse> > > invokeMethodAsync, CancellationToken cancellationToken)
            where TRequest : ServiceDto, new()
            where TResponse : ServiceDto, new()
        {
            if (mapping == null)
            {
                throw new ArgumentNullException(nameof(mapping));
            }
            if (httpRequest == null)
            {
                throw new ArgumentNullException(nameof(httpRequest));
            }
            if (invokeMethodAsync == null)
            {
                throw new ArgumentNullException(nameof(invokeMethodAsync));
            }
            if (httpRequest.RequestUri == null)
            {
                throw new ArgumentException("RequestUri must be specified.", nameof(httpRequest));
            }

            if (httpRequest.Method != mapping.HttpMethod)
            {
                return(null);
            }

            var pathParameters = TryMatchHttpRoute(httpRequest.RequestUri, m_rootPath + mapping.Path);

            if (pathParameters == null)
            {
                return(null);
            }

            var context = new ServiceHttpContext();

            ServiceHttpContext.SetContext(httpRequest, context);

            var aspectHttpResponse = await AdaptTask(RequestReceivedAsync(httpRequest, cancellationToken)).ConfigureAwait(true);

            if (aspectHttpResponse != null)
            {
                return(aspectHttpResponse);
            }

            ServiceErrorDto?error = null;

            object?requestBody = null;

            if (mapping.RequestBodyType != null)
            {
                try
                {
                    var serializer    = GetHttpContentSerializer(mapping.RequestBodyType);
                    var requestResult = await AdaptTask(serializer.ReadHttpContentAsync(mapping.RequestBodyType, httpRequest.Content, cancellationToken)).ConfigureAwait(true);

                    if (requestResult.IsFailure)
                    {
                        error = requestResult.Error;
                    }
                    else
                    {
                        requestBody = requestResult.Value;
                    }
                }
                catch (Exception exception) when(ShouldCreateErrorFromException(exception))
                {
                    // cancellation can cause the wrong exception
                    cancellationToken.ThrowIfCancellationRequested();

                    // error reading request body
                    error = CreateErrorFromException(exception);
                }
            }

            TResponse?response = null;

            if (error == null)
            {
                var request = mapping.CreateRequest(requestBody);

                var uriParameters = new Dictionary <string, string?>(StringComparer.OrdinalIgnoreCase);
                foreach (var queryParameter in ParseQueryString(httpRequest.RequestUri.Query))
                {
                    uriParameters[queryParameter.Key] = queryParameter.Value[0];
                }
                foreach (var pathParameter in pathParameters)
                {
                    uriParameters[pathParameter.Key] = pathParameter.Value;
                }
                request = mapping.SetUriParameters(request, uriParameters);
                request = mapping.SetRequestHeaders(request, HttpServiceUtility.CreateDictionaryFromHeaders(httpRequest.Headers, httpRequest.Content?.Headers) !);

                context.Request = request;

                if (!m_skipRequestValidation && !request.Validate(out var requestErrorMessage))
                {
                    error = ServiceErrors.CreateInvalidRequest(requestErrorMessage);
                }
                else
                {
                    var methodResult = await invokeMethodAsync(request, cancellationToken).ConfigureAwait(true);

                    if (methodResult.IsFailure)
                    {
                        error = methodResult.Error;
                    }
                    else
                    {
                        response = methodResult.Value;

                        if (!m_skipResponseValidation && !response.Validate(out var responseErrorMessage))
                        {
                            error    = ServiceErrors.CreateInvalidResponse(responseErrorMessage);
                            response = null;
                        }
                    }
                }

                context.Result = error != null?ServiceResult.Failure(error) : ServiceResult.Success <ServiceDto>(response !);
            }

            HttpResponseMessage httpResponse;

            if (error == null)
            {
                var responseMappingGroups = mapping.ResponseMappings
                                            .GroupBy(x => x.MatchesResponse(response !))
                                            .Where(x => x.Key != false)
                                            .OrderByDescending(x => x.Key)
                                            .ToList();
                if (responseMappingGroups.Count >= 1 && responseMappingGroups[0].Count() == 1)
                {
                    var responseMapping = responseMappingGroups[0].Single();
                    httpResponse = new HttpResponseMessage(responseMapping.StatusCode);

                    var responseHeaders = mapping.GetResponseHeaders(response !);
                    var headersResult   = HttpServiceUtility.TryAddNonContentHeaders(httpResponse.Headers, responseHeaders);
                    if (headersResult.IsFailure)
                    {
                        throw new InvalidOperationException(headersResult.Error !.Message);
                    }

                    if (responseMapping.ResponseBodyType != null)
                    {
                        var serializer = GetHttpContentSerializer(responseMapping.ResponseBodyType);
                        var mediaType  = responseMapping.ResponseBodyContentType ?? responseHeaders?.GetContentType() ?? GetAcceptedMediaType(httpRequest, serializer);
                        httpResponse.Content = serializer.CreateHttpContent(responseMapping.GetResponseBody(response !) !, mediaType);
                    }
                }
                else
                {
                    throw new InvalidOperationException($"Found {responseMappingGroups.Sum(x => x.Count())} valid HTTP responses for {typeof(TResponse).Name}: {response}");
                }
            }
            else
            {
                var statusCode = error.Code == null ? HttpStatusCode.InternalServerError :
                                 (TryGetCustomHttpStatusCode(error.Code) ?? HttpServiceErrors.TryGetHttpStatusCode(error.Code) ?? HttpStatusCode.InternalServerError);
                httpResponse = new HttpResponseMessage(statusCode);
                if (statusCode != HttpStatusCode.NoContent && statusCode != HttpStatusCode.NotModified)
                {
                    var mediaType = GetAcceptedMediaType(httpRequest, m_contentSerializer);
                    httpResponse.Content = m_contentSerializer.CreateHttpContent(error, mediaType);
                }
            }

            httpResponse.RequestMessage = httpRequest;
            await AdaptTask(ResponseReadyAsync(httpResponse, cancellationToken)).ConfigureAwait(true);

            return(httpResponse);
        }