From 649159e31dc12ab1a8456dcf4ea071970a9977ee Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 28 Feb 2018 17:16:22 +0000 Subject: [PATCH] Use real BCL System.Net.Http.HttpClient. Implements #159 --- samples/StandaloneApp/Pages/FetchData.cshtml | 3 +- samples/StandaloneApp/_ViewImports.cshtml | 3 +- .../src/Services/Http.ts | 4 +- .../Http/BrowserHttpMessageHandler.cs | 117 +++++++++++ .../Services/BrowserServiceProvider.cs | 8 +- .../Services/Temporary/HttpClient.cs | 183 ------------------ .../Pages/FetchData.cshtml | 3 +- .../_ViewImports.cshtml | 3 +- .../Pages/FetchData.cshtml | 3 +- .../_ViewImports.cshtml | 3 +- .../HttpRequestsComponent.cshtml | 2 +- 11 files changed, 135 insertions(+), 197 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Browser/Services/Temporary/HttpClient.cs diff --git a/samples/StandaloneApp/Pages/FetchData.cshtml b/samples/StandaloneApp/Pages/FetchData.cshtml index 2cc8cbfcb6..ef6efd2958 100644 --- a/samples/StandaloneApp/Pages/FetchData.cshtml +++ b/samples/StandaloneApp/Pages/FetchData.cshtml @@ -1,5 +1,4 @@ -@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary -@inject HttpClient Http +@inject HttpClient Http

Weather forecast

diff --git a/samples/StandaloneApp/_ViewImports.cshtml b/samples/StandaloneApp/_ViewImports.cshtml index caea86a11a..eae35968c9 100644 --- a/samples/StandaloneApp/_ViewImports.cshtml +++ b/samples/StandaloneApp/_ViewImports.cshtml @@ -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 diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts index 26b68c7166..170761646f 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts @@ -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; diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs new file mode 100644 index 0000000000..de00e0a3a5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs @@ -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 +{ + /// + /// A browser-compatible implementation of + /// + public class BrowserHttpMessageHandler : HttpMessageHandler + { + static object _idLock = new object(); + static int _nextRequestId = 0; + static IDictionary> _pendingRequests + = new Dictionary>(); + + /// + protected override async Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + cancellationToken.Register(() => tcs.TrySetCanceled()); + + int id; + lock (_idLock) + { + id = _nextRequestId++; + _pendingRequests.Add(id, tcs); + } + + RegisteredFunction.Invoke( + $"{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>>()) + 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 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 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(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; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserServiceProvider.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserServiceProvider.cs index 52f7c1b5e9..a3351eebe1 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserServiceProvider.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserServiceProvider.cs @@ -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(uriHelper); - serviceCollection.AddSingleton(new HttpClient(uriHelper)); + serviceCollection.AddSingleton(new HttpClient(new BrowserHttpMessageHandler()) + { + BaseAddress = new Uri(uriHelper.GetBaseUriPrefix()) + }); } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Services/Temporary/HttpClient.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Services/Temporary/HttpClient.cs deleted file mode 100644 index 24af600334..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Services/Temporary/HttpClient.cs +++ /dev/null @@ -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 -{ - /// - /// Provides mechanisms for sending HTTP requests. - /// - /// This is intended to serve as an equivalent to - /// until we're able to use the real inside Mono - /// for WebAssembly. - /// - public class HttpClient - { - static object _idLock = new object(); - static int _nextRequestId = 0; - static IDictionary> _pendingRequests - = new Dictionary>(); - 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)); - } - - /// - /// Sends a GET request to the specified URI and returns the response body as - /// a string in an asynchronous operation. - /// - /// The URI the request is sent to. - /// A task representing the asynchronous operation. - public async Task 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(); - } - - /// - /// Sends a GET request to the specified URI and returns the response as - /// an instance of in an asynchronous - /// operation. - /// - /// The URI the request is sent to. - /// A task representing the asynchronous operation. - public Task GetAsync(string requestUri) - => SendAsync(new HttpRequestMessage(HttpMethod.Get, CreateUri(requestUri))); - - /// - /// Sends a POST request to the specified URI and returns the response as - /// an instance of in an asynchronous - /// operation. - /// - /// The URI the request is sent to. - /// The content for the request. - /// A task representing the asynchronous operation. - public Task PostAsync(string requestUri, HttpContent content) - => SendAsync(new HttpRequestMessage(HttpMethod.Post, CreateUri(requestUri)) - { - Content = content - }); - - /// - /// Sends an HTTP request to the specified URI and returns the response as - /// an instance of in an asynchronous - /// operation. - /// - /// The request to be sent. - /// A task representing the asynchronous operation. - public async Task SendAsync(HttpRequestMessage request) - { - var tcs = new TaskCompletionSource(); - int id; - lock (_idLock) - { - id = _nextRequestId++; - _pendingRequests.Add(id, tcs); - } - - RegisteredFunction.Invoke( - $"{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>>()) - 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 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 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(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; - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted.CSharp/BlazorHosted.CSharp.Client/Pages/FetchData.cshtml b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted.CSharp/BlazorHosted.CSharp.Client/Pages/FetchData.cshtml index e5e0344557..97ec765bbd 100644 --- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted.CSharp/BlazorHosted.CSharp.Client/Pages/FetchData.cshtml +++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted.CSharp/BlazorHosted.CSharp.Client/Pages/FetchData.cshtml @@ -1,5 +1,4 @@ -@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary -@using BlazorHosted.CSharp.Shared +@using BlazorHosted.CSharp.Shared @inject HttpClient Http

Weather forecast

diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted.CSharp/BlazorHosted.CSharp.Client/_ViewImports.cshtml b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted.CSharp/BlazorHosted.CSharp.Client/_ViewImports.cshtml index 12019f24b6..0377253a83 100644 --- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted.CSharp/BlazorHosted.CSharp.Client/_ViewImports.cshtml +++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted.CSharp/BlazorHosted.CSharp.Client/_ViewImports.cshtml @@ -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 diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone.CSharp/Pages/FetchData.cshtml b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone.CSharp/Pages/FetchData.cshtml index 3ca1823f10..bf9bd40e49 100644 --- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone.CSharp/Pages/FetchData.cshtml +++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone.CSharp/Pages/FetchData.cshtml @@ -1,5 +1,4 @@ -@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary -@inject HttpClient Http +@inject HttpClient Http

Weather forecast

diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone.CSharp/_ViewImports.cshtml b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone.CSharp/_ViewImports.cshtml index 9c36108975..0cf69b273d 100644 --- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone.CSharp/_ViewImports.cshtml +++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone.CSharp/_ViewImports.cshtml @@ -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 diff --git a/test/testapps/BasicTestApp/HttpClientTest/HttpRequestsComponent.cshtml b/test/testapps/BasicTestApp/HttpClientTest/HttpRequestsComponent.cshtml index 34af7864a8..5a632491f5 100644 --- a/test/testapps/BasicTestApp/HttpClientTest/HttpRequestsComponent.cshtml +++ b/test/testapps/BasicTestApp/HttpClientTest/HttpRequestsComponent.cshtml @@ -1,6 +1,6 @@ @using System.Net @using System.Net.Http -@inject Microsoft.AspNetCore.Blazor.Browser.Services.Temporary.HttpClient Http +@inject HttpClient Http

HTTP request tester