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