Skip to content
Open
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
17 changes: 4 additions & 13 deletions Client/Client.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard1.3;netstandard2.0;net452;net5.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<AssemblyName>Pathoschild.Http.Client</AssemblyName>
<RootNamespace>Pathoschild.Http.Client</RootNamespace>
<PackageId>Pathoschild.Http.FluentClient</PackageId>
<Title>FluentHttpClient</Title>
<Version>4.4.2</Version>
<Version>5.0.0</Version>
<Authors>Pathoschild</Authors>
<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>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand All @@ -14,26 +14,17 @@
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Pathoschild/FluentHttpClient.git</RepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>See release notes at https://github.com/Pathoschild/FluentHttpClient/blob/develop/RELEASE-NOTES.md#442</PackageReleaseNotes>
<PackageReleaseNotes>See release notes at https://github.com/Pathoschild/FluentHttpClient/blob/develop/RELEASE-NOTES.md#500</PackageReleaseNotes>
<PackageTags>wcf;web;webapi;HttpClient;FluentHttp;FluentHttpClient</PackageTags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>

<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>

<!-- suppress framework out of support warning (deliberate for compatibility with users' target frameworks, since .NET 5 is forward-compatible) -->
<CheckEolTargetFramework>false</CheckEolTargetFramework>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard1.3' ">
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="WinInsider.System.Net.Http.Formatting" Version="1.0.14" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions Client/FluentClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
using Pathoschild.Http.Client.Extensibility;
using Pathoschild.Http.Client.Formatters;
using Pathoschild.Http.Client.Internal;
using Pathoschild.Http.Client.Retry;

Expand Down Expand Up @@ -44,7 +44,7 @@ public class FluentClient : IClient
public HttpClient BaseClient { get; }

/// <inheritdoc />
public MediaTypeFormatterCollection Formatters { get; } = [];
public MediaTypeFormatterCollection Formatters { get; } = [new JsonMediaTypeFormatter(), new XmlMediaTypeFormatter()];

/// <inheritdoc />
public IRequestCoordinator? RequestCoordinator { get; private set; }
Expand Down
15 changes: 8 additions & 7 deletions Client/FluentClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using Pathoschild.Http.Client.Extensibility;
using Pathoschild.Http.Client.Formatters;
using Pathoschild.Http.Client.Internal;
using Pathoschild.Http.Client.Retry;

Expand Down Expand Up @@ -303,7 +304,7 @@ internal static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessag
Version = request.Version
};

#if NET5_0_OR_GREATER
#if NET8_0_OR_GREATER
foreach ((string key, object? value) in request.Options)
clone.Options.Set(new HttpRequestOptionsKey<object?>(key), value);
#else
Expand All @@ -328,7 +329,7 @@ internal static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessag
Stream stream = new MemoryStream();
await content
.CopyToAsync(stream
#if NET5_0_OR_GREATER
#if NET8_0_OR_GREATER
, cancellationToken
#endif
)
Expand Down Expand Up @@ -362,23 +363,23 @@ await content
UriBuilder builder = new(baseUrl);

// special case: combine if either side is a fragment
if (!string.IsNullOrWhiteSpace(builder.Fragment) || resource.StartsWith('#'))
if (!string.IsNullOrWhiteSpace(builder.Fragment) || resource.StartsWith("#", StringComparison.Ordinal))
return new Uri(baseUrl + resource);

// special case: if resource is a query string, validate and append it
if (resource.StartsWith('?') || resource.StartsWith('&'))
if (resource.StartsWith("?", StringComparison.Ordinal) || resource.StartsWith("&", StringComparison.Ordinal))
{
bool baseHasQuery = !string.IsNullOrWhiteSpace(builder.Query);
return baseHasQuery switch
{
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."),
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."),
true when resource.StartsWith("?", StringComparison.Ordinal) => throw new FormatException($"Can't add resource name '{resource}' to base URL '{baseUrl}' because the latter already has a query string."),
false when resource.StartsWith("&", StringComparison.Ordinal) => throw new FormatException($"Can't add resource name '{resource}' to base URL '{baseUrl}' because the latter doesn't have a query string."),
_ => new Uri(baseUrl + resource)
};
}

// else make absolute URL
if (!builder.Path.EndsWith('/'))
if (!builder.Path.EndsWith("/", StringComparison.Ordinal))
{
builder.Path += "/";
baseUrl = builder.Uri;
Expand Down
38 changes: 38 additions & 0 deletions Client/Formatters/IMediaTypeFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Pathoschild.Http.Client.Formatters;

/// <summary>Defines methods for serializing and deserializing HTTP message bodies.</summary>
public interface IMediaTypeFormatter
{
/// <summary>The media types supported by this formatter.</summary>
IReadOnlyCollection<string> SupportedMediaTypes { get; }

/// <summary>Determines whether this formatter can deserialize an object of the specified type.</summary>
/// <param name="type">The type of object that will be deserialized.</param>
bool CanReadType(Type type);

/// <summary>Determines whether this formatter can serialize an object of the specified type.</summary>
/// <param name="type">The type of object that will be serialized.</param>
bool CanWriteType(Type type);

/// <summary>Reads an object from the stream asynchronously.</summary>
/// <param name="type">The type of object to read.</param>
/// <param name="stream">The stream from which to read.</param>
/// <param name="content">The HTTP content being read.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task<object?> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, CancellationToken cancellationToken);

/// <summary>Writes an object to the stream asynchronously.</summary>
/// <param name="type">The type of object to write.</param>
/// <param name="value">The object instance to write.</param>
/// <param name="stream">The stream to which to write.</param>
/// <param name="content">The HTTP content being written.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task WriteToStreamAsync(Type type, object? value, Stream stream, HttpContent content, CancellationToken cancellationToken);
}
65 changes: 65 additions & 0 deletions Client/Formatters/JsonMediaTypeFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Pathoschild.Http.Client.Formatters;

/// <summary>Serializes and deserializes data as JSON using Newtonsoft.Json.</summary>
public class JsonMediaTypeFormatter : IMediaTypeFormatter
{
/*********
** Accessors
*********/
/// <inheritdoc />
public IReadOnlyCollection<string> SupportedMediaTypes { get; } = ["application/json", "text/json"];

/// <summary>The JSON serializer settings.</summary>
public JsonSerializerSettings SerializerSettings { get; set; } = new();


/*********
** Public methods
*********/
/// <inheritdoc />
public bool CanReadType(Type type)
{
return true;
}

/// <inheritdoc />
public bool CanWriteType(Type type)
{
return true;
}

/// <inheritdoc />
public async Task<object?> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, CancellationToken cancellationToken)
{
using StreamReader reader = new(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);
string json = await reader.ReadToEndAsync(
#if NET8_0_OR_GREATER
cancellationToken
#endif
).ConfigureAwait(false);
return JsonConvert.DeserializeObject(json, type, this.SerializerSettings);
}

/// <inheritdoc />
public async Task WriteToStreamAsync(Type type, object? value, Stream stream, HttpContent content, CancellationToken cancellationToken)
{
string json = JsonConvert.SerializeObject(value, type, this.SerializerSettings);
byte[] bytes = new UTF8Encoding(false).GetBytes(json);
await stream.WriteAsync(
#if NET8_0_OR_GREATER
bytes.AsMemory(), cancellationToken
#else
bytes, 0, bytes.Length, cancellationToken
#endif
).ConfigureAwait(false);
}
}
109 changes: 35 additions & 74 deletions Client/Formatters/MediaTypeFormatterBase.cs
Original file line number Diff line number Diff line change
@@ -1,94 +1,67 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Pathoschild.Http.Client.Formatters;

/// <summary>Base implementation of an HTTP <see cref="MediaTypeFormatter"/> for serialization providers.</summary>
/// <summary>Base implementation of an <see cref="IMediaTypeFormatter"/> for serialization providers.</summary>
/// <remarks>This class handles the common code for implementing a media type formatter, so most subclasses only need to implement the <see cref="Serialize"/> and <see cref="Deserialize"/> methods.</remarks>
public abstract class MediaTypeFormatterBase : MediaTypeFormatter
public abstract class MediaTypeFormatterBase : IMediaTypeFormatter
{
/*********
** Fields
*********/
/// <summary>The supported media types.</summary>
private readonly List<string> MediaTypes = [];


/*********
** Accessors
*********/
/// <inheritdoc />
public IReadOnlyCollection<string> SupportedMediaTypes => this.MediaTypes;


/*********
** Public methods
*********/
/***
** Generic
***/
/// <summary>Determines whether this <see cref="MediaTypeFormatter"/> can deserialize an object of the specified type.</summary>
/// <param name="type">The type of object that will be deserialized.</param>
/// <returns>true if this <see cref="MediaTypeFormatter"/> can deserialize an object of that type; otherwise false.</returns>
public override bool CanReadType(Type type)
/// <inheritdoc />
public virtual bool CanReadType(Type type)
{
return true;
}

/// <summary>Determines whether this <see cref="MediaTypeFormatter"/> can serialize an object of the specified type. </summary>
/// <param name="type">The type of object that will be serialized.</param>
/// <returns>true if this <see cref="MediaTypeFormatter"/> can serialize an object of that type; otherwise false.</returns>
public override bool CanWriteType(Type type)
/// <inheritdoc />
public virtual bool CanWriteType(Type type)
{
return true;
}

/// <summary>Reads an object from the stream asynchronously.</summary>
/// <param name="type">The type of object to read.</param>
/// <param name="stream">The stream from which to read.</param>
/// <param name="content">The HTTP content being read.</param>
/// <param name="formatterLogger">The trace message logger.</param>
/// <returns>A task which writes the object to the stream asynchronously.</returns>
public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
/// <inheritdoc />
public virtual Task<object?> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, CancellationToken cancellationToken)
{
var completionSource = new TaskCompletionSource<object>();
try
{
object result = this.Deserialize(type, stream, content, formatterLogger);
completionSource.SetResult(result);
}
catch (Exception ex)
{
completionSource.SetException(ex);
}

return completionSource.Task;
object result = this.Deserialize(type, stream, content);
return Task.FromResult<object?>(result);
}

/// <summary>Writes an object to the stream asynchronously.</summary>
/// <param name="type">The type of object to write.</param>
/// <param name="value">The object instance to write.</param>
/// <param name="stream">The stream to which to write.</param>
/// <param name="content">The HTTP content being written.</param>
/// <param name="transportContext">The <see cref="TransportContext"/>.</param>
/// <returns>A task which writes the object to the stream asynchronously.</returns>
public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContent content, TransportContext transportContext)
/// <inheritdoc />
public virtual Task WriteToStreamAsync(Type type, object? value, Stream stream, HttpContent content, CancellationToken cancellationToken)
{
var completionSource = new TaskCompletionSource<object?>();
try
{
this.Serialize(type, value, stream, content, transportContext);
completionSource.SetResult(null);
}
catch (Exception ex)
{
completionSource.SetException(ex);
}

return completionSource.Task;
this.Serialize(type, value, stream, content);
return Task.CompletedTask;
}

/// <summary>Add a media type which can be read or written by this formatter.</summary>
/// <param name="mediaType">The media type string.</param>
/// <param name="quality">The relative quality factor.</param>
public MediaTypeFormatterBase AddMediaType(string mediaType, double? quality = null)
public MediaTypeFormatterBase AddMediaType(string mediaType)
{
this.SupportedMediaTypes.Add(quality.HasValue
? new MediaTypeWithQualityHeaderValue(mediaType, quality.Value)
: new MediaTypeHeaderValue(mediaType)
);
this.MediaTypes.Add(mediaType);
return this;
}

Expand All @@ -99,25 +72,13 @@ public MediaTypeFormatterBase AddMediaType(string mediaType, double? quality = n
/// <param name="type">The type of object to read.</param>
/// <param name="stream">The stream from which to read.</param>
/// <param name="content">The HTTP content being read.</param>
/// <param name="formatterLogger">The trace message logger.</param>
/// <returns>Returns a deserialized object.</returns>
public abstract object Deserialize(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger);
public abstract object Deserialize(Type type, Stream stream, HttpContent content);

/// <summary>Serialize an object into the stream.</summary>
/// <param name="type">The type of object to write.</param>
/// <param name="value">The object instance to write.</param>
/// <param name="stream">The stream to which to write.</param>
/// <param name="content">The HTTP content being written.</param>
/// <param name="transportContext">The <see cref="TransportContext"/>.</param>
public abstract void Serialize(Type type, object? value, Stream stream, HttpContent content, TransportContext transportContext);


/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
protected MediaTypeFormatterBase()
{
this.SupportedEncodings.Add(new UTF8Encoding());
}
}
public abstract void Serialize(Type type, object? value, Stream stream, HttpContent content);
}
Loading