private ServiceResult <T> Execute <T>(ServiceDto request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (m_testInfo == null) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest("Facility test name is missing; set the FacilityTest HTTP header."))); } string uncapitalize(string value) => value.Substring(0, 1).ToLowerInvariant() + value.Substring(1); string methodName = uncapitalize(request.GetType().Name.Substring(0, request.GetType().Name.Length - "RequestDto".Length)); if (methodName != m_testInfo.Method) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest($"Unexpected method name for test {m_testInfo.Test}. expected={m_testInfo.Method} actual={methodName}"))); } var actualRequest = (JObject)ServiceJsonUtility.ToJToken(request); if (!JToken.DeepEquals(m_testInfo.Request, actualRequest)) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest($"Request did not match for test {m_testInfo.Test}. expected={ServiceJsonUtility.ToJson(m_testInfo.Request)} actual={ServiceJsonUtility.ToJson(actualRequest)}"))); } if (m_testInfo.Error != null) { var error = ServiceJsonUtility.FromJToken <ServiceErrorDto>(m_testInfo.Error); var errorRoundTrip = ServiceJsonUtility.ToJToken(error); if (!JToken.DeepEquals(m_testInfo.Error, errorRoundTrip)) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest($"Error round trip failed for test {m_testInfo.Test}. expected={ServiceJsonUtility.ToJson(m_testInfo.Error)} actual={ServiceJsonUtility.ToJson(errorRoundTrip)}"))); } return(ServiceResult.Failure(error)); } else { var response = ServiceJsonUtility.FromJToken <T>(m_testInfo.Response); var responseRoundTrip = ServiceJsonUtility.ToJToken(response); if (!JToken.DeepEquals(m_testInfo.Response, responseRoundTrip)) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest($"Response round trip failed for test {m_testInfo.Test}. expected={ServiceJsonUtility.ToJson(m_testInfo.Response)} actual={ServiceJsonUtility.ToJson(responseRoundTrip)}"))); } return(ServiceResult.Success(response)); } }
private ServiceResult <T> Execute <T>(ServiceDto request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } var methodName = Uncapitalize(request.GetType().Name.Substring(0, request.GetType().Name.Length - "RequestDto".Length)); var testsWithMethodName = m_tests.Where(x => x.Method == methodName).ToList(); if (testsWithMethodName.Count == 0) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest($"No tests found for method {methodName}."))); } var actualRequest = m_jsonSerializer.ToServiceObject(request); var testsWithMatchingRequest = testsWithMethodName.Where(x => ServiceObjectUtility.DeepEquals(x.Request, actualRequest)).ToList(); if (testsWithMatchingRequest.Count != 1) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest( $"{testsWithMatchingRequest.Count} of {testsWithMethodName.Count} tests for method {methodName} matched request: " + $"{m_jsonSerializer.ToJson(actualRequest)} ({string.Join(", ", testsWithMethodName.Select(x => m_jsonSerializer.ToJson(x.Request)))})"))); } var testInfo = testsWithMatchingRequest[0]; if (testInfo.Error != null) { var error = m_jsonSerializer.FromServiceObject <ServiceErrorDto>(testInfo.Error); var errorRoundTrip = m_jsonSerializer.ToServiceObject(error); if (!ServiceObjectUtility.DeepEquals(testInfo.Error, errorRoundTrip)) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest($"Error round trip failed for test {testInfo.Test}. expected={m_jsonSerializer.ToJson(testInfo.Error)} actual={m_jsonSerializer.ToJson(errorRoundTrip)}"))); } return(ServiceResult.Failure(error)); } else { var response = m_jsonSerializer.FromServiceObject <T>(testInfo.Response !); var responseRoundTrip = m_jsonSerializer.ToServiceObject(response); if (!ServiceObjectUtility.DeepEquals(testInfo.Response, responseRoundTrip)) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest($"Response round trip failed for test {testInfo.Test}. expected={m_jsonSerializer.ToJson(testInfo.Response)} actual={m_jsonSerializer.ToJson(responseRoundTrip)}"))); } return(ServiceResult.Success(response)); } }
public async Task <IActionResult> GenerateZip([FromForm] string definitionName, [FromForm] string definitionText, [FromForm] string generatorName, CancellationToken cancellationToken) { var request = new GenerateRequestDto { Definition = new NamedTextDto { Name = definitionName, Text = definitionText, }, Generator = new GeneratorDto { Name = generatorName, }, }; var result = await m_api.GenerateAsync(request, cancellationToken).ConfigureAwait(true); if (result.IsFailure) { return(CreateActionResultFromError(result.Error)); } var response = result.Value; var failure = response.Failure; if (failure != null) { return(CreateActionResultFromError(ServiceErrors.CreateInvalidRequest($"({failure.Line},{failure.Column}): {failure.Message}"))); } return(new FileCallbackResult("application/zip", async(outputStream, _) => { using (var zipArchive = new ZipArchive(new WriteOnlyStreamWrapper(outputStream), ZipArchiveMode.Create)) { foreach (var namedText in response.Output) { var zipEntry = zipArchive.CreateEntry(namedText.Name); using (var zipStream = zipEntry.Open()) using (var writer = new StreamWriter(zipStream)) await writer.WriteAsync(namedText.Text).ConfigureAwait(false); } } }) { FileDownloadName = $"{request.Generator.Name}.zip" }); }
public async Task <ServiceResult <SetPreferenceResponseDto> > SetPreferenceAsync(SetPreferenceRequestDto request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.Key)) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest("Missing preference key."))); } await m_repository.SetPreferenceAsync(request.Key, request.Value, cancellationToken).ConfigureAwait(false); return(ServiceResult.Success(new SetPreferenceResponseDto { Value = request.Value })); }
public async Task <ServiceResult <GetPreferenceResponseDto> > GetPreferenceAsync(GetPreferenceRequestDto request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.Key)) { return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest("Missing preference key."))); } var value = await m_repository.TryGetPreferenceAsync(request.Key, cancellationToken).ConfigureAwait(false); if (value == null) { return(ServiceResult.Failure(ServiceErrors.CreateNotFound("Preference key not found."))); } return(ServiceResult.Success(new GetPreferenceResponseDto { Value = value })); }
/// <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> /// 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))); } }
public async Task <ServiceResult <GenerateResponseDto> > GenerateAsync(GenerateRequestDto request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException(nameof(request)); } try { var input = new NamedText(request.Definition?.Name ?? "", request.Definition?.Text ?? ""); bool isSwagger = ServiceDefinitionUtility.DetectFormat(input) == ServiceDefinitionFormat.Swagger; var service = isSwagger ? new SwaggerParser().ParseDefinition(input) : new FsdParser().ParseDefinition(input); var generatorName = request.Generator?.Name; switch (generatorName) { case "csharp": return(ServiceResult.Success(GenerateCode(() => new CSharpGenerator(), g => g.GenerateOutput(service)))); case "javascript": return(ServiceResult.Success(GenerateCode(() => new JavaScriptGenerator(), g => g.GenerateOutput(service)))); case "typescript": return(ServiceResult.Success(GenerateCode(() => new JavaScriptGenerator { TypeScript = true }, g => g.GenerateOutput(service)))); case "markdown": return(ServiceResult.Success(GenerateCode(() => new MarkdownGenerator(), g => g.GenerateOutput(service)))); case "fsd": return(ServiceResult.Success(GenerateCode(() => new FsdGenerator(), g => g.GenerateOutput(service)))); case "swagger-json": return(ServiceResult.Success(GenerateCode(() => new SwaggerGenerator(), g => g.GenerateOutput(service)))); case "swagger-yaml": return(ServiceResult.Success(GenerateCode(() => new SwaggerGenerator { Yaml = true }, g => g.GenerateOutput(service)))); case "asp-net-web-api": return(ServiceResult.Success(GenerateCode(() => new AspNetGenerator(), g => g.GenerateOutput(service)))); case "crash": throw new InvalidOperationException("Intentional exception for diagnostic purposes."); default: return(ServiceResult.Failure(ServiceErrors.CreateInvalidRequest($"Unrecognized generator '{generatorName}'."))); } } catch (ServiceDefinitionException exception) { return(ServiceResult.Success(new GenerateResponseDto { Failure = new FailureDto { Message = exception.Error, Line = exception.Position.LineNumber, Column = exception.Position.ColumnNumber, }, })); } }
/// <summary> /// The HTTP header is not supported. /// </summary> public static ServiceErrorDto CreateHeaderNotSupported(string headerName) => ServiceErrors.CreateInvalidRequest($"HTTP header '{headerName}' is not supported.");
/// <summary> /// HTTP header has an invalid format. /// </summary> public static ServiceErrorDto CreateHeaderInvalidFormat(string headerName) => ServiceErrors.CreateInvalidRequest($"HTTP header '{headerName}' has an invalid format.");
/// <summary> /// HTTP content is invalid. /// </summary> public static ServiceErrorDto CreateInvalidContent(string message) => ServiceErrors.CreateInvalidRequest($"HTTP content is invalid: {message}");
public static ServiceErrorDto CreateInvalidRequestMissingWidgetId() { return(ServiceErrors.CreateInvalidRequest("The widget ID is missing.")); }
/// <summary> /// HTTP content missing Content-Type. /// </summary> public static ServiceErrorDto CreateMissingContentType() => ServiceErrors.CreateInvalidRequest("HTTP content missing Content-Type.");
private async Task HostAsync(HttpContext httpContext) { var httpRequest = httpContext.Request; var requestUrl = httpRequest.GetEncodedUrl(); var apiHandler = new ConformanceApiHttpHandler(new ConformanceApiService(m_tests)); var requestMessage = new HttpRequestMessage(new HttpMethod(httpRequest.Method), requestUrl) { Content = new StreamContent(httpRequest.Body), }; foreach (var header in httpRequest.Headers) { // Every header should be able to fit into one of the two header collections. // Try message.Headers first since that accepts more of them. if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable())) { requestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); } } HttpResponseMessage?responseMessage = null; ServiceErrorDto? error = null; try { responseMessage = await apiHandler.TryHandleHttpRequestAsync(requestMessage, httpContext.RequestAborted).ConfigureAwait(false); if (responseMessage == null) { error = ServiceErrors.CreateInvalidRequest($"Test not found for {httpRequest.Method} {requestUrl}"); } } catch (Exception exception) { error = ServiceErrorUtility.CreateInternalErrorForException(exception); } if (error != null) { var statusCode = HttpServiceErrors.TryGetHttpStatusCode(error.Code) ?? HttpStatusCode.InternalServerError; responseMessage = new HttpResponseMessage(statusCode) { Content = JsonHttpContentSerializer.Instance.CreateHttpContent(error) }; } if (responseMessage != null) { using (responseMessage) { var response = httpContext.Response; response.StatusCode = (int)responseMessage.StatusCode; var responseHeaders = responseMessage.Headers; // Ignore the Transfer-Encoding header if it is just "chunked". // We let the host decide about whether the response should be chunked or not. if (responseHeaders.TransferEncodingChunked == true && responseHeaders.TransferEncoding.Count == 1) { responseHeaders.TransferEncoding.Clear(); } foreach (var header in responseHeaders) { response.Headers.Append(header.Key, header.Value.ToArray()); } // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (responseMessage.Content != null) { var contentHeaders = responseMessage.Content.Headers; // Copy the response content headers only after ensuring they are complete. // We ask for Content-Length first because HttpContent lazily computes this // and only afterwards writes the value into the content headers. _ = contentHeaders.ContentLength; foreach (var header in contentHeaders) { response.Headers.Append(header.Key, header.Value.ToArray()); } await responseMessage.Content.CopyToAsync(response.Body).ConfigureAwait(false); } } } }
public static ServiceErrorDto CreateInvalidRequestMissingWidgetIds() { return(ServiceErrors.CreateInvalidRequest("Must specify at least one widget ID.")); }
/// <summary> /// Called to create an error object from an unexpected exception. /// </summary> protected virtual ServiceErrorDto CreateErrorFromException(Exception exception) => ServiceErrors.CreateInvalidRequest("Unexpected error while reading request body.");
/// <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> /// HTTP content has unsupported Content-Type. /// </summary> public static ServiceErrorDto CreateUnsupportedContentType(string contentType) => ServiceErrors.CreateInvalidRequest($"HTTP content has unsupported Content-Type: {contentType}");