Skip to content

Commit

Permalink
Add support for content streaming
Browse files Browse the repository at this point in the history
Add support for streaming content as well as from arrays of bytes.
Fix NullReferenceException if a null synchronous content factory delegate was registered, and instead correctly de-register it.
  • Loading branch information
martincostello committed Sep 23, 2017
1 parent ae8c771 commit dc06483
Show file tree
Hide file tree
Showing 8 changed files with 432 additions and 16 deletions.
72 changes: 70 additions & 2 deletions src/HttpClientInterception/HttpClientInterceptorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
Expand Down Expand Up @@ -192,6 +193,66 @@ public HttpClientInterceptorOptions Register(
return this;
}

/// <summary>
/// Registers an HTTP request interception, replacing any existing registration.
/// </summary>
/// <param name="method">The HTTP method to register an interception for.</param>
/// <param name="uri">The request URI to register an interception for.</param>
/// <param name="contentStream">A delegate to a method that returns the response stream.</param>
/// <param name="statusCode">The optional HTTP status code to return.</param>
/// <param name="mediaType">The optional media type for the content-type.</param>
/// <param name="responseHeaders">The optional HTTP response headers for the response.</param>
/// <param name="contentHeaders">The optional HTTP response headers for the content.</param>
/// <param name="onIntercepted">An optional delegate to invoke when the HTTP message is intercepted.</param>
/// <returns>
/// The current <see cref="HttpClientInterceptorOptions"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="method"/>, <paramref name="uri"/> or <paramref name="contentStream"/> is <see langword="null"/>.
/// </exception>
public HttpClientInterceptorOptions Register(
HttpMethod method,
Uri uri,
Func<Task<Stream>> contentStream,
HttpStatusCode statusCode = HttpStatusCode.OK,
string mediaType = JsonMediaType,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders = null,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> contentHeaders = null,
Func<HttpRequestMessage, Task> onIntercepted = null)
{
if (method == null)
{
throw new ArgumentNullException(nameof(method));
}

if (uri == null)
{
throw new ArgumentNullException(nameof(uri));
}

if (contentStream == null)
{
throw new ArgumentNullException(nameof(contentStream));
}

var interceptor = new HttpInterceptionResponse()
{
ContentStream = contentStream,
ContentHeaders = contentHeaders,
ContentMediaType = mediaType,
Method = method,
OnIntercepted = onIntercepted,
RequestUri = uri,
ResponseHeaders = responseHeaders,
StatusCode = statusCode
};

string key = BuildKey(method, uri);
_mappings[key] = interceptor;

return this;
}

/// <summary>
/// Registers an HTTP request interception, replacing any existing registration.
/// </summary>
Expand Down Expand Up @@ -263,8 +324,15 @@ public async virtual Task<HttpResponseMessage> GetResponseAsync(HttpRequestMessa
result.Version = options.Version;
}

byte[] content = await options.ContentFactory() ?? Array.Empty<byte>();
result.Content = new ByteArrayContent(content);
if (options.ContentStream != null)
{
result.Content = new StreamContent(await options.ContentStream() ?? Stream.Null);
}
else
{
byte[] content = await options.ContentFactory() ?? Array.Empty<byte>();
result.Content = new ByteArrayContent(content);
}

if (options.ContentHeaders != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
Expand Down Expand Up @@ -117,6 +118,63 @@ public static HttpClientInterceptorOptions Register(
multivalueHeaders);
}

/// <summary>
/// Registers an HTTP request interception, replacing any existing registration.
/// </summary>
/// <param name="options">The <see cref="HttpClientInterceptorOptions"/> to set up.</param>
/// <param name="method">The HTTP method to register an interception for.</param>
/// <param name="uri">The request URI to register an interception for.</param>
/// <param name="contentStream">A delegate to a method that returns the response stream.</param>
/// <param name="statusCode">The optional HTTP status code to return.</param>
/// <param name="mediaType">The optional media type for the content-type.</param>
/// <param name="responseHeaders">The optional HTTP response headers.</param>
/// <returns>
/// The current <see cref="HttpClientInterceptorOptions"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="options"/>, <paramref name="method"/>, <paramref name="uri"/> or
/// <paramref name="contentStream"/> is <see langword="null"/>.
/// </exception>
public static HttpClientInterceptorOptions Register(
this HttpClientInterceptorOptions options,
HttpMethod method,
Uri uri,
Func<Stream> contentStream,
HttpStatusCode statusCode = HttpStatusCode.OK,
string mediaType = HttpClientInterceptorOptions.JsonMediaType,
IEnumerable<KeyValuePair<string, string>> responseHeaders = null)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

if (contentStream == null)
{
throw new ArgumentNullException(nameof(contentStream));
}

IDictionary<string, IEnumerable<string>> multivalueHeaders = null;

if (responseHeaders != null)
{
multivalueHeaders = new Dictionary<string, IEnumerable<string>>();

foreach (var pair in responseHeaders)
{
multivalueHeaders[pair.Key] = new[] { pair.Value };
}
}

return options.Register(
method,
uri,
() => Task.FromResult(contentStream()),
statusCode,
mediaType,
multivalueHeaders);
}

/// <summary>
/// Registers an HTTP GET request for the specified JSON content.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/HttpClientInterception/HttpInterceptionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
Expand All @@ -21,6 +22,8 @@ internal sealed class HttpInterceptionResponse

internal Func<Task<byte[]>> ContentFactory { get; set; }

internal Func<Task<Stream>> ContentStream { get; set; }

internal string ContentMediaType { get; set; }

internal IEnumerable<KeyValuePair<string, IEnumerable<string>>> ContentHeaders { get; set; }
Expand Down
48 changes: 47 additions & 1 deletion src/HttpClientInterception/HttpRequestInterceptionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
Expand All @@ -20,6 +21,8 @@ public class HttpRequestInterceptionBuilder

private Func<Task<byte[]>> _contentFactory;

private Func<Task<Stream>> _contentStream;

private IDictionary<string, ICollection<string>> _contentHeaders;

private IDictionary<string, ICollection<string>> _responseHeaders;
Expand Down Expand Up @@ -157,7 +160,15 @@ public HttpRequestInterceptionBuilder ForUri(UriBuilder uriBuilder)
/// </returns>
public HttpRequestInterceptionBuilder WithContent(Func<byte[]> contentFactory)
{
_contentFactory = () => Task.FromResult(contentFactory());
if (contentFactory == null)
{
_contentFactory = null;
}
else
{
_contentFactory = () => Task.FromResult(contentFactory());
}

return this;
}

Expand All @@ -174,6 +185,40 @@ public HttpRequestInterceptionBuilder WithContent(Func<Task<byte[]>> contentFact
return this;
}

/// <summary>
/// Sets the function to use to build the response stream.
/// </summary>
/// <param name="contentStream">A delegate to a method that returns the response stream for the content.</param>
/// <returns>
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
/// </returns>
public HttpRequestInterceptionBuilder WithContentStream(Func<Stream> contentStream)
{
if (contentStream == null)
{
_contentStream = null;
}
else
{
_contentStream = () => Task.FromResult(contentStream());
}

return this;
}

/// <summary>
/// Sets the function to use to asynchronously build the response stream.
/// </summary>
/// <param name="contentStream">A delegate to a method that returns the response stream for the content asynchronously.</param>
/// <returns>
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
/// </returns>
public HttpRequestInterceptionBuilder WithContentStream(Func<Task<Stream>> contentStream)
{
_contentStream = contentStream;
return this;
}

/// <summary>
/// Sets a custom HTTP content header to use with a single value.
/// </summary>
Expand Down Expand Up @@ -445,6 +490,7 @@ internal HttpInterceptionResponse Build()
var response = new HttpInterceptionResponse()
{
ContentFactory = _contentFactory ?? EmptyContentFactory,
ContentStream = _contentStream,
ContentMediaType = _mediaType,
Method = _method,
OnIntercepted = _onIntercepted,
Expand Down
2 changes: 1 addition & 1 deletion tests/HttpClientInterception.Tests/Examples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ public static async Task Intercept_Http_Get_To_Stream_Content_From_Disk()
var builder = new HttpRequestInterceptionBuilder()
.ForHost("xunit.github.io")
.ForPath("settings.json")
.WithContent(() => File.ReadAllBytesAsync("xunit.runner.json"));
.WithContentStream(() => File.OpenRead("xunit.runner.json"));

var options = new HttpClientInterceptorOptions()
.Register(builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Shouldly;
Expand Down Expand Up @@ -116,7 +117,13 @@ public static void Register_Throws_If_Options_Is_Null()
IEnumerable<KeyValuePair<string, string>> headers = null;

// Act and Assert
Assert.Throws<ArgumentNullException>("options", () => options.Register(method, uri, Array.Empty<byte>, responseHeaders: headers));
Assert.Throws<ArgumentNullException>(
"options",
() => options.Register(method, uri, contentFactory: Array.Empty<byte>, responseHeaders: headers));

Assert.Throws<ArgumentNullException>(
"options",
() => options.Register(method, uri, contentStream: () => Stream.Null, responseHeaders: headers));
}

[Fact]
Expand Down
Loading

0 comments on commit dc06483

Please sign in to comment.