Skip to content

Instantly share code, notes, and snippets.

@PascalSenn
Last active April 7, 2024 20:27
Show Gist options
  • Select an option

  • Save PascalSenn/9b623a439426fa361552632d8bd7972a to your computer and use it in GitHub Desktop.

Select an option

Save PascalSenn/9b623a439426fa361552632d8bd7972a to your computer and use it in GitHub Desktop.

Revisions

  1. PascalSenn revised this gist Jul 21, 2020. No changes.
  2. PascalSenn created this gist Jul 21, 2020.
    158 changes: 158 additions & 0 deletions MultipartRequestMiddleware
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,158 @@
    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
    );
    }
    }
    14 changes: 14 additions & 0 deletions ResolverExtensions
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,14 @@
    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];
    }
    }
    8 changes: 8 additions & 0 deletions example
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    descriptor.Field("uploadImage")
    .Type<NonNullType<UploadImageResultType>>()
    .Argument("file", a => a.Type<UploadType>())
    .Resolver(ctx =>
    {
    var file = ctx.GetFile("file");
    <...> // do stuff with uploaded file
    });