private ServiceResult <HttpRequestMessage> TryCreateHttpRequest(HttpMethod httpMethod, string relativeUrlPattern, IEnumerable <KeyValuePair <string, string?> >?uriParameters, IEnumerable <KeyValuePair <string, string?> >?requestHeaders)
    {
        var url = m_baseUrl + relativeUrlPattern.TrimStart('/');

        if (uriParameters != null)
        {
            url = GetUrlFromPattern(url, uriParameters);
        }

        var requestMessage = new HttpRequestMessage(httpMethod, url);

        var headersResult = HttpServiceUtility.TryAddNonContentHeaders(requestMessage.Headers, requestHeaders);

        if (headersResult.IsFailure)
        {
            return(headersResult.ToFailure());
        }

        return(ServiceResult.Success(requestMessage));
    }
    /// <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);
                    if (m_disableChunkedTransfer)
                    {
                        await httpResponse.Content.LoadIntoBufferAsync().ConfigureAwait(false);
                    }
                }
            }
            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);
                if (m_disableChunkedTransfer)
                {
                    await httpResponse.Content.LoadIntoBufferAsync().ConfigureAwait(false);
                }
            }
        }

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

        return(httpResponse);
    }