Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
52 changes: 47 additions & 5 deletions src/abstractions/MultipartBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ public void AddOrReplacePart<T>(string partName, string contentType, T partValue
{
throw new ArgumentNullException(nameof(partValue));
}
var key = Tuple.Create(partName, fileName);
Comment thread
romanett marked this conversation as resolved.
Outdated
Comment thread
romanett marked this conversation as resolved.
Outdated
var value = new Part(partName, partValue, contentType, fileName);
if(!_parts.TryAdd(partName, value))
if(!_parts.TryAdd(key, value))
{
_parts[partName] = value;
_parts[key] = value;
}
}
/// <summary>
Expand All @@ -60,12 +61,23 @@ public void AddOrReplacePart<T>(string partName, string contentType, T partValue
/// <param name="partName">The name of the part.</param>
/// <returns>The value of the part.</returns>
public T? GetPartValue<T>(string partName)
{
return GetPartValue<T>(partName, null);
}
/// <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(Tuple.Create(partName, fileName), out var value))
Comment thread
baywet marked this conversation as resolved.
Outdated
{
if(value == null)
return default;
Expand All @@ -80,15 +92,26 @@ public void AddOrReplacePart<T>(string partName, string contentType, T partValue
/// <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.
{
return RemovePart(partName, null);
}

/// <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(Tuple.Create(partName, fileName));
}

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

private sealed class TupleComparer : IEqualityComparer<Tuple<string, string?>>
{
public bool Equals(Tuple<string, string?>? x, Tuple<string, string?>? y)
{
if(x == null && y == null) return true;
if(x == null || y == null) return false;
return StringComparer.OrdinalIgnoreCase.Equals(x.Item1, y.Item1) &&
StringComparer.OrdinalIgnoreCase.Equals(x.Item2, y.Item2);
Comment thread
baywet marked this conversation as resolved.
Outdated
}

public int GetHashCode(Tuple<string, string?> obj)
{
if(obj == null) throw new ArgumentNullException(nameof(obj));
int hash1 = StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Item1);
int hash2 = obj.Item2 != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Item2) : 0;
Comment thread
romanett marked this conversation as resolved.
Outdated
return hash1 ^ hash2;
}
}
}
33 changes: 33 additions & 0 deletions tests/abstractions/MultipartBodyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,37 @@ 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 only be removed if fileName is specified
Assert.False(body.RemovePart("file"));
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 ImplementationBreaksExisting()
{
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.
}
}