Provides access to a HTTP stream (via JavaScript on a web page) in GraphQL Mutations or Queries. Attachments are transferred via a multipart form.
See Milestones for release notes.
https://nuget.org/packages/GraphQL.Attachments/
PM> Install-Package GraphQL.Attachments
Incoming and Outgoing attachments can be accessed via the ResolveFieldContext
:
Field<ResultGraph>("withAttachment")
.Argument<NonNullGraphType<StringGraphType>>("argument")
.Resolve(context =>
{
var incomingAttachments = context.IncomingAttachments();
var outgoingAttachments = context.OutgoingAttachments();
foreach (var incoming in incomingAttachments.Values)
{
// For sample purpose echo the incoming request
// stream to the outgoing response stream
var memoryStream = new MemoryStream();
incoming.CopyTo(memoryStream);
memoryStream.Position = 0;
outgoingAttachments.AddStream(incoming.Name, memoryStream);
}
return new Result
{
Argument = context.GetArgument<string>("argument"),
};
});
When using Attachments the incoming request also requires the incoming form data to be parse. To facilitate this RequestReader is used.:
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var cancel = context.RequestAborted;
var response = context.Response;
var request = context.Request;
var isGet = HttpMethods.IsGet(request.Method);
var isPost = HttpMethods.IsPost(request.Method);
if (isGet)
{
var (query, inputs, operation) = readerWriter.ReadGet(request);
await Execute(response, query, operation, null, inputs, cancel);
return;
}
if (isPost)
{
var (query, inputs, attachments, operation) = await readerWriter.ReadPost(request, cancel);
await Execute(response, query, operation, attachments, inputs, cancel);
return;
}
response.Headers.Allow = "GET, POST";
response.StatusCode = (int) HttpStatusCode.BadRequest;
}
To expose the attachments to the queries, the attachment context needs to be added to the IDocumentExecuter
. This is done using AttachmentsExtensions.ExecuteWithAttachments
:
var result = await executer.ExecuteWithAttachments(options, attachments);
As with RequestReader for the incoming data, the outgoing data needs to be written with any resulting attachments. To facilitate this ResponseWriter is used.
await readerWriter.WriteResult(response, result, cancel);
The JavaScript that submits the query does so through by building up a FormData object and POSTing that via the Fetch API.
function BuildPostSettings() {
var data = new FormData();
var files = document.getElementById("files").files;
for (var i = 0; i < files.length; i++) {
data.append('files[]', files[i], files[i].name);
}
data.append(
"query",
'mutation{ withAttachment (argument: "argumentValue"){argument}}'
);
return {
method: 'POST',
body: data
};
}
function PostMutationAndDownloadFile() {
var postSettings = BuildPostSettings();
return fetch('graphql', postSettings)
.then(function (data) {
return data.formData().then(x => {
var resultContent = '';
x.forEach(e => {
// This is the attachments
if (e.name) {
var a = document.createElement('a');
var blob = new Blob([e]);
a.href = window.URL.createObjectURL(blob);
a.download = e.name;
a.click();
}
else {
resultContent += JSON.stringify(e);
}
});
result.innerHTML = resultContent;
});
});
}
function PostMutationWithTextResult() {
var postSettings = BuildPostSettings();
return fetch('graphql', postSettings)
.then(function (data) {
return data.text().then(x => {
result.innerHTML = x;
});
});
}
Creating and posting a multipart form can be done using a combination of MultipartFormDataContent and HttpClient.PostAsync. To simplify this action the ClientQueryExecutor
class can be used:
namespace GraphQL.Attachments;
public class QueryExecutor
{
HttpClient client;
string uri;
public QueryExecutor(HttpClient client, string uri = "graphql")
{
Guard.AgainstNullWhiteSpace(uri);
this.client = client;
this.uri = uri;
}
public Task<QueryResult> ExecutePost(string query, Cancel cancel = default)
{
Guard.AgainstNullWhiteSpace(query);
return ExecutePost(new PostRequest(query), cancel);
}
public async Task<QueryResult> ExecutePost(PostRequest request, Cancel cancel = default)
{
using var content = new MultipartFormDataContent();
content.AddQueryAndVariables(request.Query, request.Variables, request.OperationName);
if (request.Action != null)
{
var postContext = new PostContext(content);
request.Action?.Invoke(postContext);
postContext.HeadersAction?.Invoke(content.Headers);
}
var response = await client.PostAsync(uri, content, cancel);
var result = await response.ProcessResponse(cancel);
return new(result.Stream, result.Attachments, response.Content.Headers, response.Headers, response.StatusCode);
}
public Task<QueryResult> ExecuteGet(string query, Cancel cancel = default)
{
Guard.AgainstNullWhiteSpace(query);
return ExecuteGet(new GetRequest(query), cancel);
}
public async Task<QueryResult> ExecuteGet(GetRequest request, Cancel cancel = default)
{
var compressed = Compress.Query(request.Query);
var variablesString = RequestAppender.ToJson(request.Variables);
var getUri = UriBuilder.GetUri(uri, variablesString, compressed, request.OperationName);
using var getRequest = new HttpRequestMessage(HttpMethod.Get, getUri);
request.HeadersAction?.Invoke(getRequest.Headers);
var response = await client.SendAsync(getRequest, cancel);
return await response.ProcessResponse(cancel);
}
}
This can be useful when performing Integration testing in ASP.NET Core.
memory designed by H Alberto Gongora from The Noun Project