using System.Net; using System.Runtime.CompilerServices; using Elsa.Extensions; using Elsa.Http.ContentWriters; using Elsa.Http.UIHints; using Elsa.Workflows; using Elsa.Workflows.Attributes; using Elsa.Workflows.UIHints; using Elsa.Workflows.Exceptions; using Elsa.Workflows.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Elsa.Http.Options; namespace Elsa.Http; /// /// Write a response to the current HTTP response object. /// [Activity("Elsa", "HTTP", "Write a response to the current HTTP response object.", DisplayName = "HTTP Response")] public class WriteHttpResponse : Activity { /// public WriteHttpResponse([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } /// public WriteHttpResponse(Input content, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { Content = content; } /// public WriteHttpResponse(Input content, Input contentType, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { Content = content; ContentType = contentType; } /// /// The status code to return. /// [Input( DefaultValue = HttpStatusCode.OK, Description = "The status code to return.", UIHint = InputUIHints.DropDown )] public Input StatusCode { get; set; } = new(HttpStatusCode.OK); /// /// The content to write back. /// [Input(Description = "The content to write back. String values will be sent as-is, while objects will be serialized to a JSON string. Byte arrays and streams will be sent as files.")] public Input Content { get; set; } = null!; /// /// The content type to use when returning the response. /// [Input( Description = "The content type to write when sending the response.", UIHandler = typeof(HttpContentTypeOptionsProvider), UIHint = InputUIHints.DropDown )] public Input ContentType { get; set; } = null!; /// /// The headers to return along with the response. /// [Input( Description = "The headers to send along with the response.", UIHint = InputUIHints.JsonEditor, Category = "Advanced" )] public Input ResponseHeaders { get; set; } = new(new HttpHeaders()); /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { var httpContextAccessor = context.GetRequiredService(); var httpContext = httpContextAccessor.HttpContext; if (httpContext == null) { throw new FaultException( HttpFaultCodes.NoHttpContext, HttpFaultCategories.Http, DefaultFaultTypes.System, "The HTTP context was lost during workflow execution. This typically occurs when a workflow initiated from an HTTP endpoint is suspended and later resumed in a different execution context (e.g., background processing, virtual actor, or after a workflow transition). The original HTTP request context that expects a response is no longer available."); } await WriteResponseAsync(context, httpContext.Response); } private async Task WriteResponseAsync(ActivityExecutionContext context, HttpResponse response) { // Set status code. var statusCode = StatusCode.GetOrDefault(context, () => HttpStatusCode.OK); response.StatusCode = (int)statusCode; // Add headers. var headers = context.GetHeaders(ResponseHeaders); foreach (var header in headers) response.Headers[header.Key] = header.Value; // Get content and content type. var content = context.Get(Content); if (content != null) { var contentType = ContentType.GetOrDefault(context); if (string.IsNullOrWhiteSpace(contentType)) contentType = DetermineContentType(content); var factories = context.GetServices(); var factory = factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == contentType)) ?? new TextContentFactory(); var httpContent = factory.CreateHttpContent(content, contentType); // Set content type. response.ContentType = httpContent.Headers.ContentType?.ToString() ?? contentType; // Write content. if (statusCode != HttpStatusCode.NoContent) { try { await httpContent.CopyToAsync(response.Body); } catch (NotSupportedException) { // This can happen the Content property is a type that cannot be serialized or contains a type that cannot be serialized. await response.WriteAsync("The response includes a type that cannot be serialized."); } } } // Check if the configuration is set to flush immediatly the response to the caller. var options = context.GetRequiredService>(); if (options.Value.WriteHttpResponseSynchronously) await response.CompleteAsync(); // Complete activity. await context.CompleteActivityAsync(); } private string DetermineContentType(object? content) => content is byte[] or Stream ? "application/octet-stream" : content is string ? "text/plain" : "application/json"; }