Skip to content

Commit 9fa7cf6

Browse files
Added the possibility to specify the content type of the uploaded file (#8474)
Co-authored-by: Tobias Tengler <[email protected]>
1 parent cb777e1 commit 9fa7cf6

File tree

9 files changed

+192
-81
lines changed

9 files changed

+192
-81
lines changed

src/HotChocolate/AspNetCore/src/Transport.Abstractions/FileReference.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ public sealed class FileReference
1010
{
1111
private readonly Func<Stream> _openRead;
1212

13+
/// <inheritdoc cref="FileReference(Stream,string,string)"/>
14+
public FileReference(Stream stream, string fileName)
15+
: this(stream, fileName, null)
16+
{
17+
}
18+
1319
/// <summary>
1420
/// Creates a new instance of <see cref="FileReference" />
1521
/// </summary>
@@ -19,13 +25,16 @@ public sealed class FileReference
1925
/// <param name="fileName">
2026
/// The file name.
2127
/// </param>
28+
/// <param name="contentType">
29+
/// The file content type.
30+
/// </param>
2231
/// <exception cref="ArgumentNullException">
2332
/// <paramref name="stream"/> is <c>null</c>.
2433
/// </exception>
2534
/// <exception cref="ArgumentException">
2635
/// <paramref name="fileName"/> is <c>null</c>, empty or white space.
2736
/// </exception>
28-
public FileReference(Stream stream, string fileName)
37+
public FileReference(Stream stream, string fileName, string? contentType)
2938
{
3039
ArgumentNullException.ThrowIfNull(stream);
3140

@@ -38,6 +47,13 @@ public FileReference(Stream stream, string fileName)
3847

3948
_openRead = () => stream;
4049
FileName = fileName;
50+
ContentType = contentType;
51+
}
52+
53+
/// <inheritdoc cref="FileReference(Func{Stream},string,string)"/>
54+
public FileReference(Func<Stream> openRead, string fileName)
55+
: this(openRead, fileName, null)
56+
{
4157
}
4258

4359
/// <summary>
@@ -49,13 +65,16 @@ public FileReference(Stream stream, string fileName)
4965
/// <param name="fileName">
5066
/// The file name.
5167
/// </param>
68+
/// <param name="contentType">
69+
/// The file content type.
70+
/// </param>
5271
/// <exception cref="ArgumentException">
5372
/// <paramref name="fileName"/> is <c>null</c>, empty or white space.
5473
/// </exception>
5574
/// <exception cref="ArgumentNullException">
5675
/// <paramref name="openRead"/> is <c>null</c>.
5776
/// </exception>
58-
public FileReference(Func<Stream> openRead, string fileName)
77+
public FileReference(Func<Stream> openRead, string fileName, string? contentType)
5978
{
6079
if (string.IsNullOrWhiteSpace(fileName))
6180
{
@@ -66,13 +85,19 @@ public FileReference(Func<Stream> openRead, string fileName)
6685

6786
_openRead = openRead ?? throw new ArgumentNullException(nameof(openRead));
6887
FileName = fileName;
88+
ContentType = contentType;
6989
}
7090

7191
/// <summary>
72-
/// The file name eg. <c>foo.txt</c>.
92+
/// The file name e.g. <c>"foo.txt"</c>.
7393
/// </summary>
7494
public string FileName { get; }
7595

96+
/// <summary>
97+
/// The content type of the file e.g. <c>"application/pdf"</c>.
98+
/// </summary>
99+
public string? ContentType { get; }
100+
76101
/// <summary>
77102
/// Opens the file stream.
78103
/// </summary>

src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,13 @@ private static HttpContent CreateMultipartContent(
212212

213213
foreach (var fileInfo in fileInfos)
214214
{
215-
var file = new StreamContent(fileInfo.File.OpenRead());
216-
form.Add(file, fileInfo.Name, fileInfo.File.FileName);
215+
var fileContent = new StreamContent(fileInfo.File.OpenRead());
216+
if (!string.IsNullOrEmpty(fileInfo.File.ContentType))
217+
{
218+
fileContent.Headers.ContentType = new MediaTypeHeaderValue(fileInfo.File.ContentType);
219+
}
220+
221+
form.Add(fileContent, fileInfo.Name, fileInfo.File.FileName);
217222
}
218223

219224
return form;

src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Net;
2+
using System.Text;
23
using System.Text.Json;
34
using HotChocolate.AspNetCore.Tests.Utilities;
45
using HotChocolate.Language;
@@ -881,29 +882,39 @@ public async Task Get_Subscription_Over_SSE_With_Errors()
881882
await snapshot.MatchMarkdownAsync(cts.Token);
882883
}
883884

884-
[Fact]
885-
public async Task Post_GraphQL_FileUpload()
885+
[Theory]
886+
[InlineData((string?)null)]
887+
[InlineData("application/pdf")]
888+
public async Task Post_GraphQL_FileUpload(string? contentType)
886889
{
887890
// arrange
888891
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5000));
889-
using var testServer = CreateStarWarsServer();
890-
var httpClient = testServer.CreateClient();
892+
var server = CreateStarWarsServer(
893+
configureServices: s => s
894+
.AddGraphQLServer("test")
895+
.AddType<UploadType>()
896+
.AddQueryType<UploadTestQuery>());
897+
var httpClient = server.CreateClient();
891898
var client = new DefaultGraphQLHttpClient(httpClient);
892899

893900
var stream = new MemoryStream("abc"u8.ToArray());
894901

895902
var operation = new OperationRequest(
896903
"""
897904
query ($upload: Upload!) {
898-
singleUpload(file: $upload)
905+
singleInfoUpload(file: $upload) {
906+
name
907+
content
908+
contentType
909+
}
899910
}
900911
""",
901912
variables: new Dictionary<string, object?>
902913
{
903-
["upload"] = new FileReference(() => stream, "test.txt")
914+
["upload"] = new FileReference(() => stream, "test.txt", contentType)
904915
});
905916

906-
var requestUri = new Uri(CreateUrl("/upload"));
917+
var requestUri = new Uri(CreateUrl("/test"));
907918

908919
var request = new GraphQLHttpRequest(operation, requestUri)
909920
{
@@ -917,10 +928,14 @@ public async Task Post_GraphQL_FileUpload()
917928
// assert
918929
using var body = await response.ReadAsResultAsync(cts.Token);
919930
body.MatchInlineSnapshot(
920-
"""
931+
$$$"""
921932
{
922933
"data": {
923-
"singleUpload": "abc"
934+
"singleInfoUpload": {
935+
"name": "test.txt",
936+
"content": "abc",
937+
"contentType": "{{{contentType}}}"
938+
}
924939
}
925940
}
926941
""");
@@ -1074,4 +1089,26 @@ public async IAsyncEnumerable<string> CreateStream()
10741089
public string OnError([EventMessage] string message)
10751090
=> message;
10761091
}
1092+
1093+
public class UploadTestQuery
1094+
{
1095+
public async Task<FileInfoOutput> SingleInfoUpload(IFile file)
1096+
{
1097+
await using var stream = file.OpenReadStream();
1098+
using var sr = new StreamReader(stream, Encoding.UTF8);
1099+
return new FileInfoOutput
1100+
{
1101+
Content = await sr.ReadToEndAsync(),
1102+
ContentType = file.ContentType ?? string.Empty,
1103+
Name = file.Name
1104+
};
1105+
}
1106+
1107+
public class FileInfoOutput
1108+
{
1109+
public string? Content { get; init; }
1110+
public string? ContentType { get; init; }
1111+
public string? Name { get; init; }
1112+
}
1113+
}
10771114
}

src/StrawberryShake/Client/src/Core/Upload.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@ namespace StrawberryShake;
55
/// </summary>
66
public readonly struct Upload
77
{
8+
/// <inheritdoc cref="Upload(Stream, string, string?)"/>
9+
public Upload(Stream content, string fileName)
10+
: this(content, fileName, null)
11+
{
12+
}
13+
814
/// <summary>
9-
/// Creates a new instance of Upload
15+
/// Creates a new instance of the Upload-scalar.
1016
/// </summary>
11-
public Upload(Stream content, string fileName)
17+
public Upload(Stream content, string fileName, string? contentType)
1218
{
1319
Content = content;
1420
FileName = fileName;
21+
ContentType = contentType;
1522
}
1623

1724
/// <summary>
@@ -23,4 +30,12 @@ public Upload(Stream content, string fileName)
2330
/// The name of the file
2431
/// </summary>
2532
public string FileName { get; }
33+
34+
/// <summary>
35+
/// The optional MIME type of the file.
36+
/// </summary>
37+
/// <remarks>
38+
/// If specified, this value is applied as the HTTP Content-Type header.
39+
/// </remarks>
40+
public string? ContentType { get; }
2641
}

src/StrawberryShake/Client/src/Transport.Http/HttpConnection.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ private static void MapVariables(List<object?> variables)
241241
{
242242
case Dictionary<string, object> result:
243243
result[currentPath] =
244-
new FileReference(upload.Value.Content, upload.Value.FileName);
244+
new FileReference(upload.Value.Content, upload.Value.FileName, upload.Value.ContentType);
245245
break;
246246

247247
case List<object> array:
@@ -258,7 +258,7 @@ private static void MapVariables(List<object?> variables)
258258
}
259259

260260
array[arrayIndex] =
261-
new FileReference(upload.Value.Content, upload.Value.FileName);
261+
new FileReference(upload.Value.Content, upload.Value.FileName, upload.Value.ContentType);
262262

263263
break;
264264

src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ await interceptor
129129
default:
130130
if (fileValue is Upload upload)
131131
{
132-
return new StreamFile(upload.FileName, () => upload.Content);
132+
return new StreamFile(upload.FileName, () => upload.Content, null, upload.ContentType);
133133
}
134134

135135
return variables;

0 commit comments

Comments
 (0)