Skip to content

Commit 21bfc3f

Browse files
romanettRoman Ettlinger
andauthored
fix: MultiPartBody: Add Support for multiple Body Parts with the same name (#530)
* Add Support for multiple Body Parts with the same name - change MultiPartbody internal dictionary key from string to Tuple<string, string?) - Add additional method overloads for GetPartValue<T> RemovePart - Add EqualityComparer fro Tuple<string, string?> - Add failing Test showcasing the imminent breaking change - Add passing Test verifying the new intened behaviour * use ValueTuple instead of Tuple * add TODO for outdated methods * Make comparsions case sensitive * Make existing Methods compatible * fix Test assuming breaking behaviour --------- Co-authored-by: Roman Ettlinger <roman.ettlinger@zbfs.bayern.de>
1 parent 273e230 commit 21bfc3f

2 files changed

Lines changed: 109 additions & 5 deletions

File tree

src/abstractions/MultipartBody.cs

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,52 @@ public void AddOrReplacePart<T>(string partName, string contentType, T partValue
4747
{
4848
throw new ArgumentNullException(nameof(partValue));
4949
}
50+
var key = (partName, fileName ?? "");
5051
var value = new Part(partName, partValue, contentType, fileName);
51-
if(!_parts.TryAdd(partName, value))
52+
if(!_parts.TryAdd(key, value))
5253
{
53-
_parts[partName] = value;
54+
_parts[key] = value;
5455
}
5556
}
57+
// TODO: Remove with next major release
5658
/// <summary>
5759
/// Gets the value of a part from the multipart body.
5860
/// </summary>
5961
/// <typeparam name="T">The type of the part value.</typeparam>
6062
/// <param name="partName">The name of the part.</param>
6163
/// <returns>The value of the part.</returns>
6264
public T? GetPartValue<T>(string partName)
65+
{
66+
var value = GetPartValue<T>(partName, null);
67+
68+
if(EqualityComparer<T?>.Default.Equals(value, default))
69+
{
70+
foreach(var key in _parts.Keys)
71+
{
72+
if(key.Item1 == partName)
73+
{
74+
value = GetPartValue<T>(partName, key.Item2);
75+
break;
76+
}
77+
}
78+
}
79+
80+
return value;
81+
}
82+
/// <summary>
83+
/// Gets the value of a part from the multipart body.
84+
/// </summary>
85+
/// <typeparam name="T">The type of the part value.</typeparam>
86+
/// <param name="partName">The name of the part.</param>
87+
/// <param name="fileName">An optional file name for the part.</param>
88+
/// <returns>The value of the part.</returns>
89+
public T? GetPartValue<T>(string partName, string? fileName)
6390
{
6491
if(string.IsNullOrEmpty(partName))
6592
{
6693
throw new ArgumentNullException(nameof(partName));
6794
}
68-
if(_parts.TryGetValue(partName, out var value))
95+
if(_parts.TryGetValue((partName, fileName ?? ""), out var value))
6996
{
7097
if(value == null)
7198
return default;
@@ -74,21 +101,47 @@ public void AddOrReplacePart<T>(string partName, string contentType, T partValue
74101
}
75102
return default;
76103
}
104+
// TODO: Remove with next major release
77105
/// <summary>
78106
/// Removes a part from the multipart body.
79107
/// </summary>
80108
/// <param name="partName">The name of the part.</param>
81109
/// <returns>True if the part was removed, false otherwise.</returns>
82110
public bool RemovePart(string partName)
111+
{
112+
bool success = RemovePart(partName, null);
113+
114+
if(!success)
115+
{
116+
foreach(var key in _parts.Keys)
117+
{
118+
if(key.Item1 == partName)
119+
{
120+
success = RemovePart(partName, key.Item2);
121+
break;
122+
}
123+
}
124+
}
125+
126+
return success;
127+
}
128+
129+
/// <summary>
130+
/// Removes a part from the multipart body.
131+
/// </summary>
132+
/// <param name="partName">The name of the part.</param>
133+
/// <param name="fileName">An optional file name for the part.</param>
134+
/// <returns>True if the part was removed, false otherwise.</returns>
135+
public bool RemovePart(string partName, string? fileName)
83136
{
84137
if(string.IsNullOrEmpty(partName))
85138
{
86139
throw new ArgumentNullException(nameof(partName));
87140
}
88-
return _parts.Remove(partName);
141+
return _parts.Remove((partName, fileName ?? ""));
89142
}
90143

91-
private readonly Dictionary<string, Part> _parts = new Dictionary<string, Part>(StringComparer.OrdinalIgnoreCase);
144+
private readonly Dictionary<ValueTuple<string, string>, Part> _parts = new Dictionary<ValueTuple<string, string>, Part>(new ValueTupleComparer());
92145
/// <inheritdoc />
93146
public IDictionary<string, Action<IParseNode>> GetFieldDeserializers() => throw new NotImplementedException();
94147
private const char DoubleQuote = '"';
@@ -196,4 +249,20 @@ private sealed class Part(string name, object content, string contentType, strin
196249
public string ContentType { get; } = contentType;
197250
public string? FileName { get; } = fileName;
198251
}
252+
253+
private sealed class ValueTupleComparer : IEqualityComparer<ValueTuple<string, string>>
254+
{
255+
public bool Equals((string, string) x, (string, string) y)
256+
{
257+
return StringComparer.Ordinal.Equals(x.Item1, y.Item1) &&
258+
StringComparer.Ordinal.Equals(x.Item2, y.Item2);
259+
}
260+
261+
public int GetHashCode(ValueTuple<string, string?> obj)
262+
{
263+
int hash1 = StringComparer.Ordinal.GetHashCode(obj.Item1);
264+
int hash2 = obj.Item2 != null ? StringComparer.Ordinal.GetHashCode(obj.Item2) : 0;
265+
return hash1 ^ hash2;
266+
}
267+
}
199268
}

tests/abstractions/MultipartBodyTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,39 @@ public void WorksWithoutFilename()
9090
requestAdapterMock.VerifyAll();
9191
serializationFactoryMock.VerifyAll();
9292
}
93+
94+
95+
[Fact]
96+
public void AllowsDuplicateEntries()
97+
{
98+
var body = new MultipartBody();
99+
100+
body.AddOrReplacePart("file", "application/json", "fileContent", "file.json");
101+
body.AddOrReplacePart("file", "application/json", "fileContent2", "file2.json");
102+
103+
//Assert both files are stored in the body
104+
Assert.Equal("fileContent", body.GetPartValue<string>("file", "file.json"));
105+
Assert.Equal("fileContent2", body.GetPartValue<string>("file", "file2.json"));
106+
107+
//Assert part can be removed if fileName is specified
108+
Assert.True(body.RemovePart("file", "file.json"));
109+
110+
//Assert file.json is removed and file2.json is still accessible
111+
Assert.Null(body.GetPartValue<string>("file", "file.json"));
112+
Assert.Equal("fileContent2", body.GetPartValue<string>("file", "file2.json"));
113+
}
114+
115+
[Fact]
116+
public void MultiPartBodyExistingGetAndRemoveStillWork()
117+
{
118+
var body = new MultipartBody();
119+
120+
body.AddOrReplacePart("file", "application/json", "fileContent", "file.json");
121+
122+
// existing usecase, file should still be able to be retreived
123+
Assert.Equal("fileContent", body.GetPartValue<string>("file"));
124+
125+
// existing usecase, file should be sucesfully removed
126+
Assert.True(body.RemovePart("file"));
127+
}
93128
}

0 commit comments

Comments
 (0)