/// <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);
                if (m_disableChunkedTransfer)
                {
                    await httpRequest.Content.LoadIntoBufferAsync().ConfigureAwait(false);
                }
            }

            // 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);
                var 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)));
        }
    }