Skip to content

Instantly share code, notes, and snippets.

@PascalSenn
Last active February 11, 2021 21:45
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save PascalSenn/9b623a439426fa361552632d8bd7972a to your computer and use it in GitHub Desktop.
Save PascalSenn/9b623a439426fa361552632d8bd7972a to your computer and use it in GitHub Desktop.
MultiPartRquestMiddlware made with ❤️ by https://github.com/acelot
descriptor.Field("uploadImage")
.Type<NonNullType<UploadImageResultType>>()
.Argument("file", a => a.Type<UploadType>())
.Resolver(ctx =>
{
var file = ctx.GetFile("file");
<...> // do stuff with uploaded file
});
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ServiceMedia.Common;
namespace ServiceMedia.Middleware
{
public class MultipartRequestMiddleware
{
private const string OPERATIONS_PART_KEY = "operations";
private const string MAP_PART_KEY = "map";
private readonly RequestDelegate _next;
private readonly Regex _jsonPathPattern
= new Regex(@"^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*$", RegexOptions.Compiled | RegexOptions.Singleline);
public MultipartRequestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Preconditions
if (context.Request.Path.Value != "/" || !context.Request.HasFormContentType)
{
await _next(context);
return;
}
// Validating form data
if (!context.Request.Form.ContainsKey(OPERATIONS_PART_KEY))
{
await InvalidRequest(context, $"Request must contain `{OPERATIONS_PART_KEY}` part!");
return;
}
if (!context.Request.Form.ContainsKey(MAP_PART_KEY))
{
await InvalidRequest(context, $"Request must contain `{MAP_PART_KEY}` part!");
return;
}
// Mapping parsing
IReadOnlyDictionary<string, string[]> map;
try
{
map = ParseMap(context.Request.Form[MAP_PART_KEY][0]);
}
catch (ArgumentException e)
{
await InvalidRequest(context, $"Map is invalid: {e.Message}");
return;
}
// Validating mapping
foreach (var key in map.Keys)
{
if (context.Request.Form.Files[key] is null)
{
await InvalidRequest(context, $"File with key `{key}` not found");
return;
}
}
// Variables substitution
JObject parsedOperations;
try
{
parsedOperations = JObject.Parse(context.Request.Form[OPERATIONS_PART_KEY][0]);
foreach (var (key, paths) in map)
{
foreach (var path in paths)
{
var token = parsedOperations.SelectToken(path);
if (token is null)
{
await InvalidRequest(context, $"Path `{path}` not found");
return;
}
token.Replace(key);
}
}
}
catch (JsonReaderException e)
{
await InvalidRequest(context, $"Operations is invalid: {e.Message}");
return;
}
// Passing next a regular JSON request
context.Request.ContentType = "application/json";
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(parsedOperations.ToString()));
await _next(context);
}
protected IReadOnlyDictionary<string, string[]> ParseMap(string raw)
{
try
{
var json = JsonDocument.Parse(raw);
if (json.RootElement.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("Map root element must be an object");
}
var map = new Dictionary<string, string[]>();
foreach (var prop in json.RootElement.EnumerateObject())
{
if (prop.Value.ValueKind != JsonValueKind.Array)
{
throw new ArgumentException("Map item value must be an array");
}
var paths = new List<string>();
foreach (var jsonPath in prop.Value.EnumerateArray())
{
if (jsonPath.ValueKind != JsonValueKind.String)
{
throw new ArgumentException("Map item value JSON path must be a string");
}
if (!_jsonPathPattern.IsMatch(jsonPath.GetString()))
{
throw new ArgumentException($"Map item value JSON path should match `{_jsonPathPattern}`");
}
paths.Add(jsonPath.GetString());
}
map[prop.Name] = paths.ToArray();
}
return map;
}
catch (System.Text.Json.JsonException e)
{
throw new ArgumentException("Cannot parse map", e);
}
}
protected Task InvalidRequest(HttpContext context, string reason) =>
context.Response.WriteText(
$"Invalid multipart request. {reason}",
HttpStatusCode.BadRequest
);
}
}
using HotChocolate;
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Http;
namespace ServiceMedia.Common
{
public static class ResolverExtensions
{
public static IFormFile GetFile(this IResolverContext ctx, NameString name)
{
var contextAccessor = ctx.Service<IHttpContextAccessor>();
return contextAccessor.HttpContext.Request.Form.Files[name.Value];
}
}
@CoryBall
Copy link

CoryBall commented Jan 6, 2021

This looks great, but what is the UploadType you are using as the Argument? That has been my inherent problem with any implementation I've tried to make for this. I try to use FormFile as the input type as I'd use in a REST endpoint, but HotChocolate is not allowing it as an input type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment