Use real BCL System.Net.Http.HttpClient. Implements #159
This commit is contained in:
parent
dd831b4552
commit
649159e31d
|
|
@ -1,5 +1,4 @@
|
|||
@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
||||
@inject HttpClient Http
|
||||
@inject HttpClient Http
|
||||
|
||||
<h1>Weather forecast</h1>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
||||
@inject HttpClient Http
|
||||
@inject HttpClient Http
|
||||
|
||||
<h1>Weather forecast</h1>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue