Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions src/abstractions/MultipartBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,52 @@ public void AddOrReplacePart<T>(string partName, string contentType, T partValue
{
throw new ArgumentNullException(nameof(partValue));
}
var key = (partName, fileName ?? "");
var value = new Part(partName, partValue, contentType, fileName);
if(!_parts.TryAdd(partName, value))
if(!_parts.TryAdd(key, value))
{
_parts[partName] = value;
_parts[key] = value;
}
}
// TODO: Remove with next major release
/// <summary>
/// Gets the value of a part from the multipart body.
/// </summary>
/// <typeparam name="T">The type of the part value.</typeparam>
/// <param name="partName">The name of the part.</param>
/// <returns>The value of the part.</returns>
public T? GetPartValue<T>(string partName)
{
var value = GetPartValue<T>(partName, null);

if(EqualityComparer<T?>.Default.Equals(value, default))
{
foreach(var key in _parts.Keys)
{
if(key.Item1 == partName)
{
value = GetPartValue<T>(partName, key.Item2);
break;
}
}
}

return value;
}
/// <summary>
/// Gets the value of a part from the multipart body.
/// </summary>
/// <typeparam name="T">The type of the part value.</typeparam>
/// <param name="partName">The name of the part.</param>
/// <param name="fileName">An optional file name for the part.</param>
/// <returns>The value of the part.</returns>
public T? GetPartValue<T>(string partName, string? fileName)
{
if(string.IsNullOrEmpty(partName))
{
throw new ArgumentNullException(nameof(partName));
}
if(_parts.TryGetValue(partName, out var value))
if(_parts.TryGetValue((partName, fileName ?? ""), out var value))
{
if(value == null)
return default;
Expand All @@ -74,21 +101,47 @@ public void AddOrReplacePart<T>(string partName, string contentType, T partValue
}
return default;
}
// TODO: Remove with next major release
/// <summary>
/// Removes a part from the multipart body.
/// </summary>
/// <param name="partName">The name of the part.</param>
/// <returns>True if the part was removed, false otherwise.</returns>
public bool RemovePart(string partName)
Comment thread
romanett marked this conversation as resolved.
{
bool success = RemovePart(partName, null);

if(!success)
{
foreach(var key in _parts.Keys)
{
if(key.Item1 == partName)
{
success = RemovePart(partName, key.Item2);
break;
}
}
}

return success;
}

/// <summary>
/// Removes a part from the multipart body.
/// </summary>
/// <param name="partName">The name of the part.</param>
/// <param name="fileName">An optional file name for the part.</param>
/// <returns>True if the part was removed, false otherwise.</returns>
public bool RemovePart(string partName, string? fileName)
{
if(string.IsNullOrEmpty(partName))
{
throw new ArgumentNullException(nameof(partName));
}
return _parts.Remove(partName);
return _parts.Remove((partName, fileName ?? ""));
}

private readonly Dictionary<string, Part> _parts = new Dictionary<string, Part>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<ValueTuple<string, string>, Part> _parts = new Dictionary<ValueTuple<string, string>, Part>(new ValueTupleComparer());
/// <inheritdoc />
public IDictionary<string, Action<IParseNode>> GetFieldDeserializers() => throw new NotImplementedException();
private const char DoubleQuote = '"';
Expand Down Expand Up @@ -196,4 +249,20 @@ private sealed class Part(string name, object content, string contentType, strin
public string ContentType { get; } = contentType;
public string? FileName { get; } = fileName;
}

private sealed class ValueTupleComparer : IEqualityComparer<ValueTuple<string, string>>
{
public bool Equals((string, string) x, (string, string) y)
{
return StringComparer.Ordinal.Equals(x.Item1, y.Item1) &&
StringComparer.Ordinal.Equals(x.Item2, y.Item2);
}

public int GetHashCode(ValueTuple<string, string?> obj)
{
int hash1 = StringComparer.Ordinal.GetHashCode(obj.Item1);
int hash2 = obj.Item2 != null ? StringComparer.Ordinal.GetHashCode(obj.Item2) : 0;
return hash1 ^ hash2;
}
}
}
35 changes: 35 additions & 0 deletions tests/abstractions/MultipartBodyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,39 @@ public void WorksWithoutFilename()
requestAdapterMock.VerifyAll();
serializationFactoryMock.VerifyAll();
}


[Fact]
public void AllowsDuplicateEntries()
{
var body = new MultipartBody();

body.AddOrReplacePart("file", "application/json", "fileContent", "file.json");
body.AddOrReplacePart("file", "application/json", "fileContent2", "file2.json");

//Assert both files are stored in the body
Assert.Equal("fileContent", body.GetPartValue<string>("file", "file.json"));
Assert.Equal("fileContent2", body.GetPartValue<string>("file", "file2.json"));

//Assert part can be removed if fileName is specified
Assert.True(body.RemovePart("file", "file.json"));

//Assert file.json is removed and file2.json is still accessible
Assert.Null(body.GetPartValue<string>("file", "file.json"));
Assert.Equal("fileContent2", body.GetPartValue<string>("file", "file2.json"));
}

[Fact]
public void MultiPartBodyExistingGetAndRemoveStillWork()
{
var body = new MultipartBody();

body.AddOrReplacePart("file", "application/json", "fileContent", "file.json");

// existing usecase, file should still be able to be retreived
Assert.Equal("fileContent", body.GetPartValue<string>("file"));
Comment thread
romanett marked this conversation as resolved.

// existing usecase, file should be sucesfully removed
Assert.True(body.RemovePart("file"));
}
}