/// <summary> /// Validates requests and responses. /// </summary> public static ServiceDelegator Validate(object inner) { if (inner is null) { throw new ArgumentNullException(nameof(inner)); } return(async(method, request, cancellationToken) => { if (!request.Validate(out var requestErrorMessage)) { return ServiceResult.Failure(ServiceErrors.CreateInvalidRequest(requestErrorMessage)); } var response = await method.InvokeAsync(inner, request, cancellationToken).ConfigureAwait(false); if (!response.Validate(out var responseErrorMessage)) { return ServiceResult.Failure(ServiceErrors.CreateInvalidResponse(responseErrorMessage)); } return response; }); }
/// <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); }
/// <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))); } }