Skip to content

Commit 1685e7b

Browse files
committed
Merge branch 'develop' into stable
2 parents 785cf23 + 5971056 commit 1685e7b

27 files changed

+362
-209
lines changed

Client/Client.csproj

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,39 @@
55
<RootNamespace>Pathoschild.Http.Client</RootNamespace>
66
<PackageId>Pathoschild.Http.FluentClient</PackageId>
77
<Title>FluentHttpClient</Title>
8-
<Version>4.2.0</Version>
8+
<Version>4.3.0</Version>
99
<Authors>Pathoschild</Authors>
1010
<Description>A modern async HTTP client for REST APIs. Its fluent interface lets you send an HTTP request and parse the response in one go.</Description>
1111
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1212
<PackageProjectUrl>https://github.com/Pathoschild/FluentHttpClient#readme</PackageProjectUrl>
1313
<PackageIcon>images/package-icon.png</PackageIcon>
1414
<RepositoryType>git</RepositoryType>
1515
<RepositoryUrl>https://github.com/Pathoschild/FluentHttpClient.git</RepositoryUrl>
16+
<PackageReadmeFile>README.md</PackageReadmeFile>
1617
<PackageReleaseNotes>See release notes at https://github.com/Pathoschild/FluentHttpClient/blob/develop/RELEASE-NOTES.md</PackageReleaseNotes>
1718
<PackageTags>wcf;web;webapi;HttpClient;FluentHttp;FluentHttpClient</PackageTags>
1819
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1920
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
2021

2122
<LangVersion>latest</LangVersion>
2223
<Nullable>enable</Nullable>
24+
25+
<!-- suppress framework out of support warning (deliberate for compatibility with users' target frameworks, since .NET 5 is forward-compatible) -->
26+
<CheckEolTargetFramework>false</CheckEolTargetFramework>
2327
</PropertyGroup>
2428

2529
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard1.3' ">
26-
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
30+
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
2731
</ItemGroup>
2832

2933
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
30-
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
34+
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
3135
<PackageReference Include="System.Net.Http" Version="4.3.4" />
3236
<PackageReference Include="WinInsider.System.Net.Http.Formatting" Version="1.0.14" />
3337
</ItemGroup>
3438

3539
<ItemGroup>
40+
<None Include="../README.md" Pack="true" PackagePath="/" />
3641
<None Include="package-icon.png" Pack="true" PackagePath="images/" />
3742
</ItemGroup>
3843
</Project>

Client/FluentClient.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Linq;
45
using System.Net;
56
using System.Net.Http;
@@ -14,6 +15,7 @@
1415
namespace Pathoschild.Http.Client
1516
{
1617
/// <inheritdoc cref="IClient" />
18+
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is a public API.")]
1719
public class FluentClient : IClient
1820
{
1921
/*********
@@ -29,7 +31,7 @@ public class FluentClient : IClient
2931
private readonly IList<Func<IRequest, IRequest>> Defaults = new List<Func<IRequest, IRequest>>();
3032

3133
/// <summary>Options for the fluent client.</summary>
32-
private readonly FluentClientOptions Options = new FluentClientOptions();
34+
private readonly FluentClientOptions Options = new();
3335

3436

3537
/*********
@@ -42,9 +44,9 @@ public class FluentClient : IClient
4244
public HttpClient BaseClient { get; }
4345

4446
/// <inheritdoc />
45-
public MediaTypeFormatterCollection Formatters { get; } = new MediaTypeFormatterCollection();
47+
public MediaTypeFormatterCollection Formatters { get; } = new();
4648

47-
/// <summary>The request coordinator.</summary>
49+
/// <inheritdoc />
4850
public IRequestCoordinator? RequestCoordinator { get; private set; }
4951

5052

@@ -158,7 +160,7 @@ public virtual void Dispose()
158160
/// <summary>Set the default user agent header.</summary>
159161
private void SetDefaultUserAgent()
160162
{
161-
Version version = typeof(FluentClient).GetTypeInfo().Assembly.GetName().Version;
163+
Version version = typeof(FluentClient).GetTypeInfo().Assembly.GetName().Version!;
162164
this.SetUserAgent($"FluentHttpClient/{version} (+http://github.com/Pathoschild/FluentHttpClient)");
163165
}
164166

@@ -170,7 +172,7 @@ protected virtual async Task<HttpResponseMessage> SendImplAsync(IRequest request
170172
this.AssertNotDisposed();
171173

172174
// clone request (to avoid issues when resending messages)
173-
HttpRequestMessage requestMessage = await request.Message.CloneAsync().ConfigureAwait(false);
175+
HttpRequestMessage requestMessage = await request.Message.CloneAsync(request.CancellationToken).ConfigureAwait(false);
174176

175177
// dispatch request
176178
return await this.BaseClient
@@ -220,7 +222,7 @@ private static HttpClientHandler GetDefaultHandler()
220222
/// <remarks>Whereas <see cref="GetDefaultHandler()"/> leaves the default proxy unchanged, this method will explicitly override it (e.g. setting a null proxy will disable the default proxy).</remarks>
221223
private static HttpClientHandler GetDefaultHandler(IWebProxy? proxy)
222224
{
223-
var handler = FluentClient.GetDefaultHandler();
225+
HttpClientHandler handler = FluentClient.GetDefaultHandler();
224226
handler.Proxy = proxy;
225227
handler.UseProxy = proxy != null;
226228
return handler;

Client/FluentClientExtensions.cs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.IO;
45
using System.Linq;
56
using System.Net.Http;
67
using System.Text;
8+
using System.Threading;
79
using System.Threading.Tasks;
810
using Pathoschild.Http.Client.Extensibility;
911
using Pathoschild.Http.Client.Internal;
@@ -12,6 +14,7 @@
1214
namespace Pathoschild.Http.Client
1315
{
1416
/// <summary>Provides convenience methods for configuring the HTTP client.</summary>
17+
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is a public API.")]
1518
public static class FluentClientExtensions
1619
{
1720
/*********
@@ -127,8 +130,8 @@ public static IRequest PatchAsync<TBody>(this IClient client, string? resource,
127130
/// <exception cref="ObjectDisposedException">The instance has been disposed.</exception>
128131
public static IRequest SendAsync(this IClient client, HttpMethod method, string? resource)
129132
{
130-
var uri = FluentClientExtensions.ResolveFinalUrl(client.BaseClient.BaseAddress, resource);
131-
var message = Factory.GetRequestMessage(method, uri, client.Formatters);
133+
Uri uri = FluentClientExtensions.ResolveFinalUrl(client.BaseClient.BaseAddress, resource) ?? throw new InvalidOperationException("Can't send a request with a null URL.");
134+
HttpRequestMessage message = Factory.GetRequestMessage(method, uri, client.Formatters);
132135
return client.SendAsync(message);
133136
}
134137

@@ -286,17 +289,23 @@ public static IRequest WithOptions(this IRequest request, bool? ignoreHttpErrors
286289
*********/
287290
/// <summary>Get a copy of the request.</summary>
288291
/// <param name="request">The request to copy.</param>
292+
/// <param name="cancellationToken">The cancellation token.</param>
289293
/// <remarks>Note that cloning a request isn't possible after it's dispatched, because the content stream is automatically disposed after the request.</remarks>
290-
internal static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessage request)
294+
internal static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessage request, CancellationToken cancellationToken = default)
291295
{
292-
HttpRequestMessage clone = new HttpRequestMessage(request.Method, request.RequestUri)
296+
HttpRequestMessage clone = new(request.Method, request.RequestUri)
293297
{
294-
Content = await request.Content.CloneAsync().ConfigureAwait(false),
298+
Content = await request.Content.CloneAsync(cancellationToken).ConfigureAwait(false),
295299
Version = request.Version
296300
};
297301

302+
#if NET5_0_OR_GREATER
303+
foreach ((string key, object? value) in request.Options)
304+
clone.Options.Set(new HttpRequestOptionsKey<object?>(key), value);
305+
#else
298306
foreach (var prop in request.Properties)
299307
clone.Properties.Add(prop);
308+
#endif
300309
foreach (var header in request.Headers)
301310
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
302311

@@ -305,17 +314,24 @@ internal static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessag
305314

306315
/// <summary>Get a copy of the request content.</summary>
307316
/// <param name="content">The content to copy.</param>
317+
/// <param name="cancellationToken">The cancellation token.</param>
308318
/// <remarks>Note that cloning content isn't possible after it's dispatched, because the stream is automatically disposed after the request.</remarks>
309-
internal static async Task<HttpContent?> CloneAsync(this HttpContent? content)
319+
internal static async Task<HttpContent?> CloneAsync(this HttpContent? content, CancellationToken cancellationToken = default)
310320
{
311321
if (content == null)
312322
return null;
313323

314324
Stream stream = new MemoryStream();
315-
await content.CopyToAsync(stream).ConfigureAwait(false);
325+
await content
326+
.CopyToAsync(stream
327+
#if NET5_0_OR_GREATER
328+
, cancellationToken
329+
#endif
330+
)
331+
.ConfigureAwait(false);
316332
stream.Position = 0;
317333

318-
StreamContent clone = new StreamContent(stream);
334+
StreamContent clone = new(stream);
319335
foreach (var header in content.Headers)
320336
clone.Headers.Add(header.Key, header.Value);
321337

@@ -325,12 +341,12 @@ internal static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessag
325341
/// <summary>Resolve the final URL for a request.</summary>
326342
/// <param name="baseUrl">The base URL.</param>
327343
/// <param name="resource">The requested resource, or <c>null</c> to use the base URL (if set).</param>
328-
private static Uri ResolveFinalUrl(Uri baseUrl, string? resource)
344+
private static Uri? ResolveFinalUrl(Uri? baseUrl, string? resource)
329345
{
330346
// ignore if empty or already absolute
331347
if (string.IsNullOrWhiteSpace(resource))
332348
return baseUrl;
333-
if (Uri.TryCreate(resource, UriKind.Absolute, out Uri absoluteUrl))
349+
if (Uri.TryCreate(resource, UriKind.Absolute, out Uri? absoluteUrl))
334350
return absoluteUrl;
335351

336352
// can't combine if no base URL
@@ -339,7 +355,7 @@ private static Uri ResolveFinalUrl(Uri baseUrl, string? resource)
339355

340356
// parse URLs
341357
resource = resource!.Trim();
342-
UriBuilder builder = new UriBuilder(baseUrl);
358+
UriBuilder builder = new(baseUrl);
343359

344360
// special case: combine if either side is a fragment
345361
if (!string.IsNullOrWhiteSpace(builder.Fragment) || resource.StartsWith('#'))
@@ -349,11 +365,12 @@ private static Uri ResolveFinalUrl(Uri baseUrl, string? resource)
349365
if (resource.StartsWith('?') || resource.StartsWith('&'))
350366
{
351367
bool baseHasQuery = !string.IsNullOrWhiteSpace(builder.Query);
352-
if (baseHasQuery && resource.StartsWith('?'))
353-
throw new FormatException($"Can't add resource name '{resource}' to base URL '{baseUrl}' because the latter already has a query string.");
354-
if (!baseHasQuery && resource.StartsWith('&'))
355-
throw new FormatException($"Can't add resource name '{resource}' to base URL '{baseUrl}' because the latter doesn't have a query string.");
356-
return new Uri(baseUrl + resource);
368+
return baseHasQuery switch
369+
{
370+
true when resource.StartsWith('?') => throw new FormatException($"Can't add resource name '{resource}' to base URL '{baseUrl}' because the latter already has a query string."),
371+
false when resource.StartsWith('&') => throw new FormatException($"Can't add resource name '{resource}' to base URL '{baseUrl}' because the latter doesn't have a query string."),
372+
_ => new Uri(baseUrl + resource)
373+
};
357374
}
358375

359376
// else make absolute URL

Client/Formatters/PlainTextFormatter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ public override bool CanWriteType(Type type)
4242
/// <inheritdoc />
4343
public override object Deserialize(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
4444
{
45-
var reader = new StreamReader(stream); // don't dispose (stream disposal is handled elsewhere)
45+
StreamReader reader = new(stream); // don't dispose (stream disposal is handled elsewhere)
4646
return reader.ReadToEnd();
4747
}
4848

4949
/// <inheritdoc />
5050
public override void Serialize(Type type, object? value, Stream stream, HttpContent content, TransportContext transportContext)
5151
{
52-
var writer = new StreamWriter(stream); // don't dispose (stream disposal is handled elsewhere)
52+
StreamWriter writer = new(stream); // don't dispose (stream disposal is handled elsewhere)
5353
writer.Write(value != null ? value.ToString() : string.Empty);
5454
writer.Flush();
5555
}

Client/IBodyBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.IO;
45
using System.Net.Http;
56
using System.Net.Http.Formatting;
@@ -8,6 +9,7 @@
89
namespace Pathoschild.Http.Client
910
{
1011
/// <summary>Constructs HTTP request bodies.</summary>
12+
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is a public API.")]
1113
public interface IBodyBuilder
1214
{
1315
/*********

Client/IClient.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Net.Http;
45
using System.Net.Http.Formatting;
56
using Pathoschild.Http.Client.Extensibility;
@@ -8,6 +9,7 @@
89
namespace Pathoschild.Http.Client
910
{
1011
/// <summary>Sends HTTP requests and receives responses from REST URIs.</summary>
12+
[SuppressMessage("ReSharper", "UnusedMemberInSuper.Global", Justification = "This is a public API.")]
1113
public interface IClient : IDisposable
1214
{
1315
/*********
@@ -22,6 +24,9 @@ public interface IClient : IDisposable
2224
/// <summary>Interceptors which can read and modify HTTP requests and responses.</summary>
2325
ICollection<IHttpFilter> Filters { get; }
2426

27+
/// <summary>The request coordinator.</summary>
28+
IRequestCoordinator? RequestCoordinator { get; }
29+
2530

2631
/*********
2732
** Methods

Client/IRequest.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.IO;
45
using System.Net.Http;
56
using System.Net.Http.Formatting;
@@ -13,6 +14,8 @@
1314
namespace Pathoschild.Http.Client
1415
{
1516
/// <summary>Builds and dispatches an asynchronous HTTP request, and asynchronously parses the response.</summary>
17+
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is a public API.")]
18+
[SuppressMessage("ReSharper", "UnusedMemberInSuper.Global", Justification = "This is a public API.")]
1619
public interface IRequest
1720
{
1821
/*********

Client/IResponse.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.IO;
23
using System.Net;
34
using System.Net.Http;
45
using System.Net.Http.Formatting;
6+
using System.Threading;
57
using System.Threading.Tasks;
68
using Newtonsoft.Json.Linq;
79

810
namespace Pathoschild.Http.Client
911
{
1012
/// <summary>Asynchronously parses an HTTP response.</summary>
13+
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is a public API.")]
14+
[SuppressMessage("ReSharper", "UnusedMemberInSuper.Global", Justification = "This is a public API.")]
1115
public interface IResponse
1216
{
1317
/*********
@@ -25,10 +29,18 @@ public interface IResponse
2529
/// <summary>The formatters used for serializing and deserializing message bodies.</summary>
2630
MediaTypeFormatterCollection Formatters { get; }
2731

32+
/// <summary>The optional token used to cancel async operations.</summary>
33+
CancellationToken CancellationToken { get; }
34+
2835

2936
/*********
3037
** Methods
3138
*********/
39+
/// <summary>Specify the token that can be used to cancel the async operation.</summary>
40+
/// <param name="cancellationToken">The cancellation token.</param>
41+
/// <returns>Returns the response builder for chaining.</returns>
42+
IResponse WithCancellationToken(CancellationToken cancellationToken);
43+
3244
/// <summary>Asynchronously retrieve the response body as a deserialized model.</summary>
3345
/// <typeparam name="T">The response model to deserialize into.</typeparam>
3446
/// <exception cref="ApiException">An error occurred processing the response.</exception>

Client/Internal/BodyBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ public HttpContent FileUpload(IEnumerable<FileInfo> files)
8989
/// <inheritdoc />
9090
public HttpContent FileUpload(IEnumerable<KeyValuePair<string, Stream>> files)
9191
{
92-
var content = new MultipartFormDataContent();
92+
MultipartFormDataContent content = new();
9393

9494
foreach (var file in files)
9595
{
96-
StreamContent streamContent = new StreamContent(file.Value);
96+
StreamContent streamContent = new(file.Value);
9797
content.Add(streamContent, file.Key, file.Key);
9898
}
9999

Client/Internal/Factory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static MediaTypeFormatter GetFormatter(MediaTypeFormatterCollection forma
2424
if (!formatters.Any())
2525
throw new InvalidOperationException("No MediaTypeFormatters are available on the fluent client.");
2626

27-
MediaTypeFormatter formatter = contentType != null
27+
MediaTypeFormatter? formatter = contentType != null
2828
? formatters.FirstOrDefault(f => f.SupportedMediaTypes.Any(m => m.MediaType == contentType.MediaType))
2929
: formatters.FirstOrDefault();
3030
if (formatter == null)
@@ -39,7 +39,7 @@ public static MediaTypeFormatter GetFormatter(MediaTypeFormatterCollection forma
3939
/// <param name="formatters">The formatters used for serializing and deserializing message bodies.</param>
4040
public static HttpRequestMessage GetRequestMessage(HttpMethod method, Uri resource, MediaTypeFormatterCollection formatters)
4141
{
42-
HttpRequestMessage request = new HttpRequestMessage(method, resource);
42+
HttpRequestMessage request = new(method, resource);
4343

4444
// add default headers
4545
request.Headers.Add("accept", formatters.SelectMany(p => p.SupportedMediaTypes).Select(p => p.MediaType));

0 commit comments

Comments
 (0)