Use real BCL System.Net.Http.HttpClient. Implements #159

This commit is contained in:
Steve Sanderson 2018-02-28 17:16:22 +00:00
parent dd831b4552
commit 649159e31d
11 changed files with 135 additions and 197 deletions

View File

@ -1,5 +1,4 @@
@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
@inject HttpClient Http
@inject HttpClient Http
<h1>Weather forecast</h1>

View File

@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Blazor
@using System.Net.Http
@using Microsoft.AspNetCore.Blazor
@using Microsoft.AspNetCore.Blazor.Layouts
@using Microsoft.AspNetCore.Blazor.Routing
@using StandaloneApp

View File

@ -2,8 +2,8 @@
import { platform } from '../Environment';
import { MethodHandle, System_String } from '../Platform/Platform';
const httpClientAssembly = 'Microsoft.AspNetCore.Blazor.Browser';
const httpClientNamespace = `${httpClientAssembly}.Services.Temporary`;
const httpClientTypeName = 'HttpClient';
const httpClientNamespace = `${httpClientAssembly}.Http`;
const httpClientTypeName = 'BrowserHttpMessageHandler';
const httpClientFullTypeName = `${httpClientNamespace}.${httpClientTypeName}`;
let receiveResponseMethod: MethodHandle;

View File

@ -0,0 +1,117 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Browser.Interop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Blazor.Browser.Http
{
/// <summary>
/// A browser-compatible implementation of <see cref="HttpMessageHandler"/>
/// </summary>
public class BrowserHttpMessageHandler : HttpMessageHandler
{
static object _idLock = new object();
static int _nextRequestId = 0;
static IDictionary<int, TaskCompletionSource<HttpResponseMessage>> _pendingRequests
= new Dictionary<int, TaskCompletionSource<HttpResponseMessage>>();
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<HttpResponseMessage>();
cancellationToken.Register(() => tcs.TrySetCanceled());
int id;
lock (_idLock)
{
id = _nextRequestId++;
_pendingRequests.Add(id, tcs);
}
RegisteredFunction.Invoke<object>(
$"{typeof(BrowserHttpMessageHandler).FullName}.Send",
id,
request.Method.Method,
request.RequestUri,
request.Content == null ? null : await GetContentAsString(request.Content),
SerializeHeadersAsJson(request));
return await tcs.Task;
}
private string SerializeHeadersAsJson(HttpRequestMessage request)
=> JsonUtil.Serialize(
(from header in request.Headers.Concat(request.Content?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
from headerValue in header.Value // There can be more than one value for each name
select new[] { header.Key, headerValue }).ToList()
);
private static async Task<string> GetContentAsString(HttpContent content)
=> content is StringContent stringContent
? await stringContent.ReadAsStringAsync()
: throw new InvalidOperationException($"Currently, {typeof(HttpClient).FullName} " +
$"only supports contents of type {nameof(StringContent)}, but you supplied " +
$"{content.GetType().FullName}.");
private static void ReceiveResponse(
string id,
string responseDescriptorJson,
string responseBodyText,
string errorText)
{
TaskCompletionSource<HttpResponseMessage> tcs;
var idVal = int.Parse(id);
lock (_idLock)
{
tcs = _pendingRequests[idVal];
_pendingRequests.Remove(idVal);
}
if (errorText != null)
{
tcs.SetException(new HttpRequestException(errorText));
}
else
{
var responseDescriptor = JsonUtil.Deserialize<ResponseDescriptor>(responseDescriptorJson);
var responseContent = responseBodyText == null ? null : new StringContent(responseBodyText);
var responseMessage = responseDescriptor.ToResponseMessage(responseContent);
tcs.SetResult(responseMessage);
}
}
// Keep in sync with TypeScript class in Http.ts
private class ResponseDescriptor
{
#pragma warning disable 0649
public int StatusCode { get; set; }
public string[][] Headers { get; set; }
#pragma warning restore 0649
public HttpResponseMessage ToResponseMessage(HttpContent content)
{
var result = new HttpResponseMessage((HttpStatusCode)StatusCode);
result.Content = content;
var headers = result.Headers;
var contentHeaders = result.Content?.Headers;
foreach (var pair in Headers)
{
if (!headers.TryAddWithoutValidation(pair[0], pair[1]))
{
contentHeaders?.TryAddWithoutValidation(pair[0], pair[1]);
}
}
return result;
}
}
}
}

View File

@ -1,10 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary;
using Microsoft.AspNetCore.Blazor.Browser.Http;
using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
namespace Microsoft.AspNetCore.Blazor.Browser.Services
{
@ -43,7 +44,10 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
{
var uriHelper = new BrowserUriHelper();
serviceCollection.AddSingleton<IUriHelper>(uriHelper);
serviceCollection.AddSingleton(new HttpClient(uriHelper));
serviceCollection.AddSingleton(new HttpClient(new BrowserHttpMessageHandler())
{
BaseAddress = new Uri(uriHelper.GetBaseUriPrefix())
});
}
}
}

View File

@ -1,183 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Browser.Interop;
using Microsoft.AspNetCore.Blazor.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
{
/// <summary>
/// Provides mechanisms for sending HTTP requests.
///
/// This is intended to serve as an equivalent to <see cref="System.Net.Http.HttpClient"/>
/// until we're able to use the real <see cref="System.Net.Http.HttpClient"/> inside Mono
/// for WebAssembly.
/// </summary>
public class HttpClient
{
static object _idLock = new object();
static int _nextRequestId = 0;
static IDictionary<int, TaskCompletionSource<HttpResponseMessage>> _pendingRequests
= new Dictionary<int, TaskCompletionSource<HttpResponseMessage>>();
IUriHelper _uriHelper;
// Making the constructor internal to be sure people only get instances from
// the service provider. It doesn't make any difference right now, but when
// we switch to System.Net.Http.HttpClient, there may be a period where it
// only works when you get an instance from the service provider because it
// has to be configured with a browser-specific HTTP handler. In the long
// term, it should be possible to use System.Net.Http.HttpClient directly
// without any browser-specific constructor args.
internal HttpClient(IUriHelper uriHelper)
{
_uriHelper = uriHelper ?? throw new ArgumentNullException(nameof(uriHelper));
}
/// <summary>
/// Sends a GET request to the specified URI and returns the response body as
/// a string in an asynchronous operation.
/// </summary>
/// <param name="requestUri">The URI the request is sent to.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task<string> GetStringAsync(string requestUri)
{
var response = await GetAsync(requestUri);
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"The response status code was {response.StatusCode}");
}
return await response.Content.ReadAsStringAsync();
}
/// <summary>
/// Sends a GET request to the specified URI and returns the response as
/// an instance of <see cref="HttpResponseMessage"/> in an asynchronous
/// operation.
/// </summary>
/// <param name="requestUri">The URI the request is sent to.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public Task<HttpResponseMessage> GetAsync(string requestUri)
=> SendAsync(new HttpRequestMessage(HttpMethod.Get, CreateUri(requestUri)));
/// <summary>
/// Sends a POST request to the specified URI and returns the response as
/// an instance of <see cref="HttpResponseMessage"/> in an asynchronous
/// operation.
/// </summary>
/// <param name="requestUri">The URI the request is sent to.</param>
/// <param name="content">The content for the request.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
=> SendAsync(new HttpRequestMessage(HttpMethod.Post, CreateUri(requestUri))
{
Content = content
});
/// <summary>
/// Sends an HTTP request to the specified URI and returns the response as
/// an instance of <see cref="HttpResponseMessage"/> in an asynchronous
/// operation.
/// </summary>
/// <param name="request">The request to be sent.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
var tcs = new TaskCompletionSource<HttpResponseMessage>();
int id;
lock (_idLock)
{
id = _nextRequestId++;
_pendingRequests.Add(id, tcs);
}
RegisteredFunction.Invoke<object>(
$"{typeof(HttpClient).FullName}.Send",
id,
request.Method.Method,
ResolveRequestUri(request.RequestUri),
request.Content == null ? null : await GetContentAsString(request.Content),
SerializeHeadersAsJson(request));
return await tcs.Task;
}
private string SerializeHeadersAsJson(HttpRequestMessage request)
=> JsonUtil.Serialize(
(from header in request.Headers.Concat(request.Content?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
from headerValue in header.Value // There can be more than one value for each name
select new[] { header.Key, headerValue }).ToList()
);
private static async Task<string> GetContentAsString(HttpContent content)
=> content is StringContent stringContent
? await stringContent.ReadAsStringAsync()
: throw new InvalidOperationException($"Currently, {typeof(HttpClient).FullName} " +
$"only supports contents of type {nameof(StringContent)}, but you supplied " +
$"{content.GetType().FullName}.");
private Uri CreateUri(String uri)
=> new Uri(uri, UriKind.RelativeOrAbsolute);
private static void ReceiveResponse(
string id,
string responseDescriptorJson,
string responseBodyText,
string errorText)
{
TaskCompletionSource<HttpResponseMessage> tcs;
var idVal = int.Parse(id);
lock (_idLock)
{
tcs = _pendingRequests[idVal];
_pendingRequests.Remove(idVal);
}
if (errorText != null)
{
tcs.SetException(new HttpRequestException(errorText));
}
else
{
var responseDescriptor = JsonUtil.Deserialize<ResponseDescriptor>(responseDescriptorJson);
var responseContent = responseBodyText == null ? null : new StringContent(responseBodyText);
var responseMessage = responseDescriptor.ToResponseMessage(responseContent);
tcs.SetResult(responseMessage);
}
}
private string ResolveRequestUri(Uri requestUri)
=> _uriHelper.ToAbsoluteUri(requestUri.OriginalString).AbsoluteUri;
// Keep in sync with TypeScript class in Http.ts
private class ResponseDescriptor
{
#pragma warning disable 0649
public int StatusCode { get; set; }
public string[][] Headers { get; set; }
#pragma warning restore 0649
public HttpResponseMessage ToResponseMessage(HttpContent content)
{
var result = new HttpResponseMessage((HttpStatusCode)StatusCode);
result.Content = content;
var headers = result.Headers;
var contentHeaders = result.Content?.Headers;
foreach (var pair in Headers)
{
if (!headers.TryAddWithoutValidation(pair[0], pair[1]))
{
contentHeaders?.TryAddWithoutValidation(pair[0], pair[1]);
}
}
return result;
}
}
}
}

View File

@ -1,5 +1,4 @@
@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
@using BlazorHosted.CSharp.Shared
@using BlazorHosted.CSharp.Shared
@inject HttpClient Http
<h1>Weather forecast</h1>

View File

@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Blazor
@using System.Net.Http
@using Microsoft.AspNetCore.Blazor
@using Microsoft.AspNetCore.Blazor.Layouts
@using Microsoft.AspNetCore.Blazor.Routing
@using BlazorHosted.CSharp.Client

View File

@ -1,5 +1,4 @@
@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
@inject HttpClient Http
@inject HttpClient Http
<h1>Weather forecast</h1>

View File

@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Blazor
@using System.Net.Http
@using Microsoft.AspNetCore.Blazor
@using Microsoft.AspNetCore.Blazor.Layouts
@using Microsoft.AspNetCore.Blazor.Routing
@using BlazorStandalone.CSharp

View File

@ -1,6 +1,6 @@
@using System.Net
@using System.Net.Http
@inject Microsoft.AspNetCore.Blazor.Browser.Services.Temporary.HttpClient Http
@inject HttpClient Http
<h1>HTTP request tester</h1>