Skip to content

Commit 6c9a01b

Browse files
committed
feat(body): configure the request compressor directly
- Add RefitSettings.RequestCompressionOptions so a client can set the compressor's own knobs - window size, strategy, a Zstandard dictionary - which a CompressionLevel cannot express. - Options set for a coding replace the level for that coding; the codings left unset still compress by level. - The options types arrived with .NET 9.0, and Zstandard's with .NET 11.0, so the surface only exists from net9.0 onward.
1 parent aebf479 commit 6c9a01b

10 files changed

Lines changed: 167 additions & 9 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,24 @@ var settings = new RefitSettings
10421042
};
10431043
```
10441044

1045+
For knobs a level cannot express — window size, strategy, a Zstandard dictionary — set the compressor's own options.
1046+
Options set for a coding replace the level for that coding; the codings left unset still compress by level:
1047+
1048+
```csharp
1049+
var settings = new RefitSettings
1050+
{
1051+
RequestCompression = RequestCompression.Brotli,
1052+
RequestCompressionOptions = new()
1053+
{
1054+
Brotli = new() { Quality = 9, WindowLog2 = 22 },
1055+
GZip = new() { CompressionLevel = 6 },
1056+
},
1057+
};
1058+
```
1059+
1060+
`RequestCompressionOptions` needs .NET 9.0 or later, where `ZLibCompressionOptions` and `BrotliCompressionOptions`
1061+
were introduced; its `Zstandard` property needs .NET 11.0.
1062+
10451063
There is no negotiation for a compressed request body, so only turn this on against a server you know accepts one.
10461064
The compressed length is unknown until the body has been written, so these requests are sent chunked.
10471065

src/Refit/CompressedContent.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,22 @@ internal CompressedContent(HttpContent inner, RequestCompression compression, Co
4747
Headers.ContentEncoding.Add(RequestContentCoding.Token(compression));
4848
}
4949

50+
#if NET9_0_OR_GREATER
51+
/// <summary>Gets the per-coding compressor settings that override the level, or <see langword="null"/>.</summary>
52+
internal RequestCompressionOptions? Options { get; init; }
53+
#endif
54+
5055
/// <inheritdoc/>
5156
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
5257
{
5358
// leaveOpen, because the request stream belongs to the transport; only the compressor is finished here, and
5459
// finishing it is what writes the trailer that makes the body readable.
5560
Stream compressor = _compression switch
5661
{
62+
#if NET9_0_OR_GREATER
63+
RequestCompression.GZip when Options?.GZip is { } gzip => new GZipStream(stream, gzip, leaveOpen: true),
64+
RequestCompression.Brotli when Options?.Brotli is { } brotli => new BrotliStream(stream, brotli, leaveOpen: true),
65+
#endif
5766
RequestCompression.GZip => new GZipStream(stream, _level, leaveOpen: true),
5867
#if NET8_0_OR_GREATER
5968
RequestCompression.Brotli => new BrotliStream(stream, _level, leaveOpen: true),

src/Refit/GeneratedRequestRunner.BodyContent.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public static HttpContent CompressBodyContent(
4242

4343
return resolved is RequestCompression.Default or RequestCompression.None
4444
? content
45-
: RequestContentCoding.Wrap(content, resolved, resolvedLevel);
45+
: RequestContentCoding.Wrap(content, settings, resolved, resolvedLevel);
4646
}
4747

4848
/// <summary>Serializes a generated request body using Refit body rules.</summary>

src/Refit/PublicAPI/net10.0/PublicAPI.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ public class RefitSettings
622622
public Refit.RequestBodySerializationMode RequestBodySerialization { get; set; }
623623
public Refit.RequestCompression RequestCompression { get; set; }
624624
public System.IO.Compression.CompressionLevel RequestCompressionLevel { get; set; }
625+
public Refit.RequestCompressionOptions? RequestCompressionOptions { get; set; }
625626
public System.Collections.Generic.IList<System.Type> ReturnTypeAdapters { get; }
626627
public System.Func<System.Net.Http.HttpRequestMessage, System.Exception, System.Threading.CancellationToken, System.Exception> TransportExceptionFactory { get; set; }
627628
public Refit.IUrlParameterFormatter UrlParameterFormatter { get; set; }
@@ -667,6 +668,13 @@ public enum RequestCompression
667668
Brotli = 3,
668669
Zstandard = 4,
669670
}
671+
[System.Diagnostics.DebuggerDisplay("{GZip} {Brotli}")]
672+
public sealed class RequestCompressionOptions
673+
{
674+
public RequestCompressionOptions() { }
675+
public System.IO.Compression.BrotliCompressionOptions? Brotli { get; set; }
676+
public System.IO.Compression.ZLibCompressionOptions? GZip { get; set; }
677+
}
670678
[System.Diagnostics.DebuggerDisplay("{ToString(),nq}")]
671679
public record RestMethodInfo : System.IEquatable<Refit.RestMethodInfo>
672680
{

src/Refit/PublicAPI/net11.0/PublicAPI.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ public class RefitSettings
622622
public Refit.RequestBodySerializationMode RequestBodySerialization { get; set; }
623623
public Refit.RequestCompression RequestCompression { get; set; }
624624
public System.IO.Compression.CompressionLevel RequestCompressionLevel { get; set; }
625+
public Refit.RequestCompressionOptions? RequestCompressionOptions { get; set; }
625626
public System.Collections.Generic.IList<System.Type> ReturnTypeAdapters { get; }
626627
public System.Func<System.Net.Http.HttpRequestMessage, System.Exception, System.Threading.CancellationToken, System.Exception> TransportExceptionFactory { get; set; }
627628
public Refit.IUrlParameterFormatter UrlParameterFormatter { get; set; }
@@ -667,6 +668,14 @@ public enum RequestCompression
667668
Brotli = 3,
668669
Zstandard = 4,
669670
}
671+
[System.Diagnostics.DebuggerDisplay("{GZip} {Brotli}")]
672+
public sealed class RequestCompressionOptions
673+
{
674+
public RequestCompressionOptions() { }
675+
public System.IO.Compression.BrotliCompressionOptions? Brotli { get; set; }
676+
public System.IO.Compression.ZLibCompressionOptions? GZip { get; set; }
677+
public System.IO.Compression.ZstandardCompressionOptions? Zstandard { get; set; }
678+
}
670679
[System.Diagnostics.DebuggerDisplay("{ToString(),nq}")]
671680
public record RestMethodInfo : System.IEquatable<Refit.RestMethodInfo>
672681
{

src/Refit/PublicAPI/net9.0/PublicAPI.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@ public class RefitSettings
621621
public Refit.RequestBodySerializationMode RequestBodySerialization { get; set; }
622622
public Refit.RequestCompression RequestCompression { get; set; }
623623
public System.IO.Compression.CompressionLevel RequestCompressionLevel { get; set; }
624+
public Refit.RequestCompressionOptions? RequestCompressionOptions { get; set; }
624625
public System.Collections.Generic.IList<System.Type> ReturnTypeAdapters { get; }
625626
public System.Func<System.Net.Http.HttpRequestMessage, System.Exception, System.Threading.CancellationToken, System.Exception> TransportExceptionFactory { get; set; }
626627
public Refit.IUrlParameterFormatter UrlParameterFormatter { get; set; }
@@ -666,6 +667,13 @@ public enum RequestCompression
666667
Brotli = 3,
667668
Zstandard = 4,
668669
}
670+
[System.Diagnostics.DebuggerDisplay("{GZip} {Brotli}")]
671+
public sealed class RequestCompressionOptions
672+
{
673+
public RequestCompressionOptions() { }
674+
public System.IO.Compression.BrotliCompressionOptions? Brotli { get; set; }
675+
public System.IO.Compression.ZLibCompressionOptions? GZip { get; set; }
676+
}
669677
[System.Diagnostics.DebuggerDisplay("{ToString(),nq}")]
670678
public record RestMethodInfo : System.IEquatable<Refit.RestMethodInfo>
671679
{

src/Refit/RefitSettings.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,13 @@ public Func<
276276
public System.IO.Compression.CompressionLevel RequestCompressionLevel { get; set; } =
277277
System.IO.Compression.CompressionLevel.Optimal;
278278

279+
#if NET9_0_OR_GREATER
280+
/// <summary>Gets or sets the per-coding compressor settings, or null to compress by level alone.</summary>
281+
/// <remarks>Options set for a coding override the level for that coding, including where a <c>[Body]</c> parameter
282+
/// named the coding and its own level.</remarks>
283+
public RequestCompressionOptions? RequestCompressionOptions { get; set; }
284+
#endif
285+
279286
/// <summary>Gets optional Key-Value pairs, which are displayed in the property <see cref="HttpRequestMessage.Properties"/>.</summary>
280287
public Dictionary<string, object>? HttpRequestMessageOptions { get; init; }
281288

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
2+
// ReactiveUI and Contributors licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
#if NET9_0_OR_GREATER
5+
using System.IO.Compression;
6+
7+
namespace Refit;
8+
9+
/// <summary>The per-coding compressor settings used in place of <see cref="RefitSettings.RequestCompressionLevel"/>.</summary>
10+
/// <remarks>
11+
/// Each coding has its own options type carrying knobs a level cannot express - window size, strategy, a Zstandard
12+
/// dictionary. Setting the options for a coding overrides the level for that coding only; the codings left null still
13+
/// compress by level. The options types themselves arrived with .NET 9.0, and Zstandard's with .NET 11.0, so this type
14+
/// does not exist on earlier targets.
15+
/// </remarks>
16+
[System.Diagnostics.DebuggerDisplay("{GZip} {Brotli}")]
17+
public sealed class RequestCompressionOptions
18+
{
19+
/// <summary>Gets or sets the gzip compressor settings, or <see langword="null"/> to compress by level.</summary>
20+
public ZLibCompressionOptions? GZip { get; set; }
21+
22+
/// <summary>Gets or sets the Brotli compressor settings, or <see langword="null"/> to compress by level.</summary>
23+
public BrotliCompressionOptions? Brotli { get; set; }
24+
25+
#if NET11_0_OR_GREATER
26+
/// <summary>Gets or sets the Zstandard compressor settings, or <see langword="null"/> to compress by level.</summary>
27+
public ZstandardCompressionOptions? Zstandard { get; set; }
28+
#endif
29+
}
30+
#endif

src/Refit/RequestContentCoding.cs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,26 +44,49 @@ internal static (RequestCompression Compression, CompressionLevel Level) Resolve
4444

4545
/// <summary>Wraps content in the compressor for a coding.</summary>
4646
/// <param name="content">The content to compress.</param>
47+
/// <param name="settings">The Refit settings supplying the per-coding compressor options.</param>
4748
/// <param name="compression">The resolved coding, never <see cref="RequestCompression.Default"/> or <see cref="RequestCompression.None"/>.</param>
48-
/// <param name="level">How hard to compress.</param>
49+
/// <param name="level">How hard to compress, used for any coding the settings left without options.</param>
4950
/// <returns>The compressing content.</returns>
5051
/// <exception cref="PlatformNotSupportedException">This framework cannot produce the coding.</exception>
51-
internal static HttpContent Wrap(HttpContent content, RequestCompression compression, CompressionLevel level) =>
52+
internal static HttpContent Wrap(
53+
HttpContent content,
54+
RefitSettings settings,
55+
RequestCompression compression,
56+
CompressionLevel level)
57+
{
5258
#if NET11_0_OR_GREATER
53-
compression switch
59+
var options = settings.RequestCompressionOptions;
60+
61+
return compression switch
5462
{
55-
RequestCompression.GZip => new GZipCompressedContent(content, level),
56-
RequestCompression.Brotli => new BrotliCompressedContent(content, level),
57-
RequestCompression.Zstandard => new ZstandardCompressedContent(content, level),
63+
RequestCompression.GZip => options?.GZip is { } gzip
64+
? new GZipCompressedContent(content, gzip)
65+
: new GZipCompressedContent(content, level),
66+
RequestCompression.Brotli => options?.Brotli is { } brotli
67+
? new BrotliCompressedContent(content, brotli)
68+
: new BrotliCompressedContent(content, level),
69+
RequestCompression.Zstandard => options?.Zstandard is { } zstandard
70+
? new ZstandardCompressedContent(content, zstandard)
71+
: new ZstandardCompressedContent(content, level),
5872
_ => throw Unsupported(compression),
5973
};
74+
#elif NET9_0_OR_GREATER
75+
return compression is RequestCompression.GZip or RequestCompression.Brotli
76+
? new CompressedContent(content, compression, level) { Options = settings.RequestCompressionOptions }
77+
: throw Unsupported(compression);
6078
#elif NET8_0_OR_GREATER
61-
compression is RequestCompression.GZip or RequestCompression.Brotli
79+
_ = settings;
80+
81+
return compression is RequestCompression.GZip or RequestCompression.Brotli
6282
? new CompressedContent(content, compression, level)
6383
: throw Unsupported(compression);
6484
#else
65-
compression is RequestCompression.GZip
85+
_ = settings;
86+
87+
return compression is RequestCompression.GZip
6688
? new CompressedContent(content, compression, level)
6789
: throw Unsupported(compression);
6890
#endif
91+
}
6992
}

src/tests/Refit.Tests/RequestCompressionTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,34 @@ public async Task CompressedBodyRoundTrips()
139139
await Assert.That(await reader.ReadToEndAsync()).IsEqualTo(BodyText);
140140
}
141141

142+
#if NET9_0_OR_GREATER
143+
/// <summary>Verifies per-coding options replace the level for the coding they name.</summary>
144+
/// <returns>A task that represents the asynchronous operation.</returns>
145+
[Test]
146+
public async Task PerCodingOptionsReplaceTheLevel()
147+
{
148+
// Zero is the zlib "store, do not compress" level, and the declared level asks for the smallest output.
149+
var settings = new RefitSettings { RequestCompressionOptions = new() { GZip = new() { CompressionLevel = 0 } } };
150+
151+
var byOptions = await CompressAsync(settings, RequestCompression.GZip, CompressionLevel.SmallestSize);
152+
var byLevel = await CompressAsync(new(), RequestCompression.GZip, CompressionLevel.SmallestSize);
153+
154+
await Assert.That(byOptions.Length).IsGreaterThan(byLevel.Length);
155+
}
156+
157+
/// <summary>Verifies a coding the options left unset still compresses by level.</summary>
158+
/// <returns>A task that represents the asynchronous operation.</returns>
159+
[Test]
160+
public async Task CodingWithoutOptionsStillCompressesByLevel()
161+
{
162+
var settings = new RefitSettings { RequestCompressionOptions = new() { Brotli = new() { Quality = 0 } } };
163+
164+
var bytes = await CompressAsync(settings, RequestCompression.GZip, CompressionLevel.SmallestSize);
165+
166+
await Assert.That(bytes.Length).IsLessThan(BodyText.Length);
167+
}
168+
#endif
169+
142170
/// <summary>Verifies the compressed content reports no length, so the request is sent chunked.</summary>
143171
/// <returns>A task that represents the asynchronous operation.</returns>
144172
[Test]
@@ -153,4 +181,22 @@ public async Task CompressedBodyReportsNoContentLength()
153181

154182
await Assert.That(compressed.Headers.ContentLength).IsNull();
155183
}
184+
185+
#if NET9_0_OR_GREATER
186+
/// <summary>Compresses the shared body and returns the bytes that would reach the wire.</summary>
187+
/// <param name="settings">The settings supplying any per-coding options.</param>
188+
/// <param name="compression">The coding to apply.</param>
189+
/// <param name="level">The level to apply where the options do not override it.</param>
190+
/// <returns>The compressed bytes.</returns>
191+
private static async Task<byte[]> CompressAsync(
192+
RefitSettings settings,
193+
RequestCompression compression,
194+
CompressionLevel level)
195+
{
196+
using var content = new StringContent(BodyText);
197+
using var compressed = GeneratedRequestRunner.CompressBodyContent(content, settings, compression, level);
198+
199+
return await compressed.ReadAsByteArrayAsync();
200+
}
201+
#endif
156202
}

0 commit comments

Comments
 (0)