Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ddieppa/035e7e4cd699df473edb1d70963dc131 to your computer and use it in GitHub Desktop.
Save ddieppa/035e7e4cd699df473edb1d70963dc131 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];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment