GraphQL.Attachments
GraphQL.Attachments copied to clipboard
Provides access to a HTTP stream in GraphQL
GraphQL.Attachments
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.
NuGet package
https://nuget.org/packages/GraphQL.Attachments/
PM> Install-Package GraphQL.Attachments
Usage in Graphs
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"),
};
});
snippet source | anchor
Server-side Middleware
RequestReader instead of binding
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;
}
snippet source | anchor
Query Execution
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);
snippet source | anchor
Result Writing
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);
snippet source | anchor
Client - JavaScript
The JavaScript that submits the query does so through by building up a FormData object and POSTing that via the Fetch API.
Helper method for builgin post settings
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
};
}
snippet source | anchor
Post mutation and download result
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;
});
});
}
snippet source | anchor
Post mutation and display text result
function PostMutationWithTextResult() {
var postSettings = BuildPostSettings();
return fetch('graphql', postSettings)
.then(function (data) {
return data.text().then(x => {
result.innerHTML = x;
});
});
}
snippet source | anchor
Client - .NET
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);
}
}
snippet source | anchor
This can be useful when performing Integration testing in ASP.NET Core.
Icon
memory designed by H Alberto Gongora from The Noun Project