From 891f2a14d0063cf94fbc9a38637eafe7f007122d Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 23 Feb 2018 12:29:53 +0000 Subject: [PATCH] Basic implementation of temporary HttpClient. Currently only supports GET requests and doesn't return HTTP headers. --- samples/StandaloneApp/Pages/FetchData.cshtml | 28 ++++++ samples/StandaloneApp/Shared/NavMenu.cshtml | 5 + .../src/Boot.ts | 3 +- .../src/Platform/Platform.ts | 4 +- .../src/Services/Http.ts | 40 ++++++++ .../src/{Routing => Services}/UriHelper.ts | 0 .../Services/BrowserServiceProvider.cs | 2 + .../Services/Temporary/HttpClient.cs | 99 +++++++++++++++++++ 8 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 samples/StandaloneApp/Pages/FetchData.cshtml create mode 100644 src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts rename src/Microsoft.AspNetCore.Blazor.Browser.JS/src/{Routing => Services}/UriHelper.ts (100%) create 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 new file mode 100644 index 0000000000..2da2bcdfd4 --- /dev/null +++ b/samples/StandaloneApp/Pages/FetchData.cshtml @@ -0,0 +1,28 @@ +@using Microsoft.AspNetCore.Blazor.Browser.Services.Temporary +@inject HttpClient Http + +

Fetch data

+ +Response: @responseText + +@functions { + private string responseText; + + // TODO: Move to OnInitAsync + protected override void OnParametersSet() + { + Http.GetStringAsync("/").ContinueWith(task => + { + try + { + responseText = task.Result; + } + catch (Exception ex) + { + Console.Error.WriteLine(ex.InnerException.Message); + Console.Error.WriteLine(ex.InnerException.StackTrace); + } + StateHasChanged(); + }); + } +} diff --git a/samples/StandaloneApp/Shared/NavMenu.cshtml b/samples/StandaloneApp/Shared/NavMenu.cshtml index 3983d01b5b..f11d7ec1c2 100644 --- a/samples/StandaloneApp/Shared/NavMenu.cshtml +++ b/samples/StandaloneApp/Shared/NavMenu.cshtml @@ -23,6 +23,11 @@ Counter +
  • + + Fetch data + +
  • diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.ts index e4ba205083..28d395decc 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Boot.ts @@ -1,7 +1,8 @@ import { platform } from './Environment'; import { getAssemblyNameFromUrl } from './Platform/DotNet'; import './Rendering/Renderer'; -import './Routing/UriHelper'; +import './Services/Http'; +import './Services/UriHelper'; import './GlobalExports'; async function boot() { diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Platform.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Platform.ts index 1e1272152e..b41850a32a 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Platform.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Platform/Platform.ts @@ -1,9 +1,9 @@ export interface Platform { start(loadAssemblyUrls: string[]): Promise; - callEntryPoint(assemblyName: string, entrypointMethod: string, args: System_Object[]); + callEntryPoint(assemblyName: string, entrypointMethod: string, args: (System_Object | null)[]); findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle; - callMethod(method: MethodHandle, target: System_Object | null, args: System_Object[]): System_Object; + callMethod(method: MethodHandle, target: System_Object | null, args: (System_Object | null)[]): System_Object; toJavaScriptString(dotNetString: System_String): string; toDotNetString(javaScriptString: string): System_String; diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts new file mode 100644 index 0000000000..4f230662c3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts @@ -0,0 +1,40 @@ +import { registerFunction } from '../Interop/RegisteredFunction'; +import { platform } from '../Environment'; +import { MethodHandle } from '../Platform/Platform'; +const httpClientAssembly = 'Microsoft.AspNetCore.Blazor.Browser'; +const httpClientNamespace = `${httpClientAssembly}.Services.Temporary`; +const httpClientTypeName = 'HttpClient'; +const httpClientFullTypeName = `${httpClientNamespace}.${httpClientTypeName}`; +let receiveResponseMethod: MethodHandle; + +registerFunction(`${httpClientFullTypeName}.Send`, (id: number, requestUri: string) => { + sendAsync(id, requestUri); +}); + +async function sendAsync(id: number, requestUri: string) { + try { + const response = await fetch(requestUri); + const responseText = await response.text(); + dispatchResponse(id, response.status, responseText, null); + } catch (ex) { + dispatchResponse(id, 0, null, ex.toString()); + } +} + +function dispatchResponse(id: number, statusCode: number, responseText: string | null, errorInfo: string | null) { + if (!receiveResponseMethod) { + receiveResponseMethod = platform.findMethod( + httpClientAssembly, + httpClientNamespace, + httpClientTypeName, + 'ReceiveResponse' + ); + } + + platform.callMethod(receiveResponseMethod, null, [ + platform.toDotNetString(id.toString()), + platform.toDotNetString(statusCode.toString()), + responseText === null ? null : platform.toDotNetString(responseText), + errorInfo === null ? null : platform.toDotNetString(errorInfo.toString()) + ]); +} diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Routing/UriHelper.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts similarity index 100% rename from src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Routing/UriHelper.ts rename to src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserServiceProvider.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserServiceProvider.cs index 81aee97eed..11302a913d 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserServiceProvider.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserServiceProvider.cs @@ -1,6 +1,7 @@ // 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.Services; using Microsoft.Extensions.DependencyInjection; using System; @@ -41,6 +42,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services private void AddDefaultServices(ServiceCollection serviceCollection) { serviceCollection.AddSingleton(new BrowserUriHelper()); + serviceCollection.AddSingleton(new HttpClient()); } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Services/Temporary/HttpClient.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Services/Temporary/HttpClient.cs new file mode 100644 index 0000000000..bc28d147dd --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Services/Temporary/HttpClient.cs @@ -0,0 +1,99 @@ +// 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.Collections.Generic; +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>(); + + // 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() + { + } + + /// + /// 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) + { + var tcs = new TaskCompletionSource(); + int id; + lock (_idLock) + { + id = _nextRequestId++; + _pendingRequests.Add(id, tcs); + } + + RegisteredFunction.Invoke($"{typeof(HttpClient).FullName}.Send", id, requestUri); + + return tcs.Task; + } + + private static void ReceiveResponse(string id, string statusCode, string responseText, string errorText) + { + TaskCompletionSource tcs; + var idVal = int.Parse(id); + lock (_idLock) + { + tcs = _pendingRequests[idVal]; + _pendingRequests.Remove(idVal); + } + + if (errorText == null) + { + tcs.SetResult(new HttpResponseMessage + { + StatusCode = (HttpStatusCode)int.Parse(statusCode), + Content = new StringContent(responseText) + }); + } + else + { + tcs.SetException(new HttpRequestException(errorText)); + } + } + } +}