Last active
April 7, 2024 20:27
-
-
Save PascalSenn/9b623a439426fa361552632d8bd7972a to your computer and use it in GitHub Desktop.
MultiPartRquestMiddlware made with ❤️ by https://github.com/acelot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
descriptor.Field("uploadImage") | |
.Type<NonNullType<UploadImageResultType>>() | |
.Argument("file", a => a.Type<UploadType>()) | |
.Resolver(ctx => | |
{ | |
var file = ctx.GetFile("file"); | |
<...> // do stuff with uploaded file | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
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