From d9f1a5ac0fd9e4653237e7efb159f4e5dd1b7df3 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Mon, 20 May 2019 15:48:00 -0500 Subject: [PATCH 1/8] Blazor call web API topic with sample Updates --- aspnetcore/blazor/call-web-api.md | 230 ++++++++++++++++++ .../Components/HTTPRequestTester.razor | 178 ++++++++++++++ .../3.x/BlazorSample/Pages/CallWebAPI.razor | 129 ++++++++++ .../common/samples/3.x/BlazorSample/README.md | 18 ++ .../3.x/BlazorSample/Shared/NavMenu.razor | 5 + .../3.x/BlazorSample/wwwroot/css/site.css | 21 ++ aspnetcore/toc.yml | 2 + 7 files changed, 583 insertions(+) create mode 100644 aspnetcore/blazor/call-web-api.md create mode 100644 aspnetcore/blazor/common/samples/3.x/BlazorSample/Components/HTTPRequestTester.razor create mode 100644 aspnetcore/blazor/common/samples/3.x/BlazorSample/Pages/CallWebAPI.razor diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md new file mode 100644 index 000000000000..0e160bd6ebf7 --- /dev/null +++ b/aspnetcore/blazor/call-web-api.md @@ -0,0 +1,230 @@ +--- +title: Call a web API +author: guardrex +description: Learn how to call a web API from a Blazor app using JSON helpers, including making cross-origin resource sharing (CORS) requests. +monikerRange: '>= aspnetcore-3.0' +ms.author: riande +ms.custom: mvc +ms.date: 05/20/2019 +uid: blazor/call-web-api +--- +# Call a web API + +By [Luke Latham](https://github.com/guardrex) + +Blazor apps call web API services using [HttpClient](xref:fundamentals/http-requests). Compose requests using Blazor JSON helpers or with , which can include JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) request options. + +[View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/blazor/common/samples/) ([how to download](xref:index#how-to-download-a-sample)) + +See the following components in the sample app: + +* Call Web API (*Pages/CallWebAPI.razor*) +* HTTP Request Tester (*Components/HTTPRequestTester.razor*) + +## HttpClient and JSON helpers + +Use [HttpClient](xref:fundamentals/http-requests) and JSON helpers to call a web API service endpoint in a Razor component. Blazor client-side uses the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API), while Blazor server-side uses . + +Include an `@using` statement for for asynchronous web API calls and inject an `HttpClient` instance: + +```cshtml +@using System.Threading.Tasks +@inject HttpClient Http +``` + +In the following examples, a Todo web API service processes create, read, update, and delete (CRUD) operations. The examples are based on a `TodoItem` class that stores the: + +* ID (`Id`, `long`) – Unique ID of the item. +* Name (`Name`, `string`) – Name of the item. +* Status (`IsComplete`, `bool`) – Indication if the Todo item is finished. + +```csharp +private class TodoItem +{ + public long Id { get; set; } + public string Name { get; set; } + public bool IsComplete { get; set; } +} +``` + +JSON helper methods send requests to a URI (a web API in the following examples) and process the response: + +* `GetJsonAsync` – Sends a GET request and parses the JSON response body to create an object. + + In the following code, the `_todoItems` are displayed by the component. The `GetTodoItems` method is triggered when the component is finished rendering ([OnInitAsync](xref:blazor/components#lifecycle-methods)). See the sample app for a complete example. + + ```cshtml + @using Microsoft.AspNetCore.Blazor.Http + @inject HttpClient Http + + @functions { + private const string ServiceEndpoint = "https://localhost:10000/api/todo"; + private TodoItem[] _todoItems; + + protected override async Task OnInitAsync() => + await GetTodoItems(); + + private async Task GetTodoItems() => + _todoItems = await Http.GetJsonAsync(ServiceEndpoint); + } + ``` + +* `PostJsonAsync` – Sends a POST request, including JSON-encoded content, and parses the JSON response body to create an object. + + In the following code, `_newItemName` is provided by a bound element of the component. The `AddItem` method is triggered by selecting a ` + + @functions { + private const string ServiceEndpoint = "https://localhost:10000/api/todo"; + private string _newItemName; + + private async Task AddItem() + { + var addItem = new TodoItem { Name = _newItemName, IsComplete = false }; + await Http.PostJsonAsync(ServiceEndpoint, addItem); + } + } + ``` + +* `PutJsonAsync` – Sends a PUT request, including JSON-encoded content. + + In the following code, `_editItem` values (`Id`, `Name`, `IsCompleted`) are provided by bound elements of the component. The `SaveItem` method is triggered by selecting the Save ` + + @functions { + private const string ServiceEndpoint = "https://localhost:10000/api/todo"; + private TodoItem _editItem = new TodoItem(); + + private async Task SaveItem() + { + await Http.PutJsonAsync( + Path.Combine(ServiceEndpoint, _editItem.Id.ToString()), _editItem); + } + } + ``` + + includes additional extension methods for sending HTTP requests and receiving HTTP responses. [HttpClient.DeleteAsync](xref:System.Net.Http.HttpClient.DeleteAsync*) is used to send a DELETE request to a web API. + +In the following code, the Delete ` + +@functions { + private const string ServiceEndpoint = "https://localhost:10000/api/todo"; + private long _id; + + private async Task DeleteItem() + { + await Http.DeleteAsync(Path.Combine(ServiceEndpoint, _id.ToString())); + } +} +``` + +## HttpClient and HttpRequestMessage with Fetch API request options + +Use [HttpClient](xref:fundamentals/http-requests) and to supply request options to the underlying JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API). + +In the following example: + +* JSON serialization and deserialization must be handled by user code (*not shown*). +* The `credentials` property is set to any of the following values: + + * `FetchCredentialsOption.Include` ("include") – Advises the browser to send credentials (such as cookies or HTTP auth headers) even for cross-origin requests. Only allowed when the CORS Middleware policy in the web API is configured to . + * `FetchCredentialsOption.Omit` ("omit") – Advises the browser never to send credentials (such as cookies or HTTP auth headers). + * `FetchCredentialsOption.SameOrigin` ("same-origin") – Advises the browser to send credentials (such as cookies or HTTP auth headers) only if the target URL is on the same origin as the calling application. + +```cshtml +@using System.Net.Http.Headers +@using Microsoft.AspNetCore.Blazor.Http +@inject HttpClient Http + +@functions { + private async Task PostRequest() + { + Http.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", "{OAUTH TOKEN}"); + + var requestMessage = new HttpRequestMessage() + { + Method = new HttpMethod("POST"), + RequestUri = new Uri("https://localhost:10000/api/todo"), + Content = + new StringContent( + @"{""name"":""A New Todo Item"",""isComplete"":false}") + }; + + requestMessage.Content.Headers.ContentType = + new System.Net.Http.Headers.MediaTypeHeaderValue( + "application/json"); + + requestMessage.Content.Headers.TryAddWithoutValidation( + "x-custom-header", "value"); + + requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new + { + credentials = FetchCredentialsOption.Include + }; + + var response = await Http.SendAsync(requestMessage); + var responseStatusCode = response.StatusCode; + var responseBody = await response.Content.ReadAsStringAsync(); + } +} +``` + +For more information on Fetch API options, see [MDN web docs: Window​OrWorker​Global​Scope​.fetch():Parameters](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters). + +When sending credentials (authorization cookies/headers) on CORS requests, allow the `Authorization` header when creating the CORS policy for CORS middleware. The following policy includes configuration for: + +* Request origins (`http://localhost:5000`, `https://localhost:5001`). +* Any method (verb). +* `Content-Type` and `Authorization` headers. To allow a custom header (for example, `x-custom-header`), list the header when calling . +* Credentials set by client-side JavaScript code (`credentials` property set to `include`). + +```csharp +app.UseCors(policy => + policy.WithOrigins("http://localhost:5000", "https://localhost:5001") + .AllowAnyMethod() + .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, "x-custom-header") + .AllowCredentials()); +``` + +For more information, see and the sample app's HTTP Request Tester component (*Components/HTTPRequestTester.razor*). + +## Cross-origin resource sharing (CORS) + +For more information on making cross-origin resource sharing (CORS) requests, see . The sample app demonstrates CORS. See the Call Web API component (*Pages/CallWebAPI.razor*). + +## HTTP Request Tester + +The sample app includes an HTTP Request Tester component (*Components/HTTPRequestTester.razor*). The component is included in the Call Web API component: + +```cshtml + +``` + +Use the component to test request-responses against web API service endpoints. + +## Additional resources + +* +* [Cross Origin Resource Sharing (CORS) at W3C](https://www.w3.org/TR/cors/) diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/Components/HTTPRequestTester.razor b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Components/HTTPRequestTester.razor new file mode 100644 index 000000000000..b75c95fa0449 --- /dev/null +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Components/HTTPRequestTester.razor @@ -0,0 +1,178 @@ +@using System.Net +@using System.Net.Http +@using Microsoft.AspNetCore.Blazor.Http +@using Microsoft.AspNetCore.Components +@inject HttpClient Http +@inject IUriHelper UriHelper + +

HTTP Request Tester

+ +

The default values of the following form POST (add a Todo item) to the web API service created in the Tutorial: Create a web API with ASP.NET Core MVC topic with:

+ +
    +
  • An endpoint URI of https://localhost:10000/api/todo.
  • +
  • The Content-Type header set to application/json, which describes the payload of a POST/PUT request to the service.
  • +
  • Fetch API credentials of include, which is permitted by the service's CORS Middleware policy with an Authorization header included in the request.
  • +
+ +

Add the following CORS middleware configuration to the web API's service's Startup.Configure method before it calls UseMvc:

+ +
app.UseCors(policy =>
+    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
+    .AllowAnyMethod()
+    .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization)
+    .AllowCredentials());
+ +

Adjust the domains and ports of WithOrigins as needed for the Blazor app.

+ +

The web API service is configured for CORS to permit authorization cookies/headers and requests from client code, but the service as created by the tutorial doesn't actually authorize requests. See the ASP.NET Core Security and Identity articles for implementation guidance.

+ +

+

URI:
+ +

+ +

+

Method:
+ +

+ +

+

Credentials:
+ +

+ +

+

Request body:
+ +

+ +

+

Request headers:
+ @foreach (var header in _requestHeaders) + { +
+ Name: + Value: + +
+ } + +

+ +

+

Request referrer:
+ +

+ + + +@if (_responseStatusCode.HasValue) +{ +

Response

+

Status:
@_responseStatusCode

+

Body:

+

Headers:

+} + +@functions { + private string _uri = "https://localhost:10000/api/todo"; + private string _method = "POST"; + private string _requestBody = @"{""name"":""A New Todo Item"",""isComplete"":false}"; + private List _requestHeaders = new List() +{ + new RequestHeader() { Name = "Content-Type", Value = "application/json" }, + new RequestHeader() { Name = "Authorization", Value = "Bearer MHrHDcEfxjoYZgeFONFh7HgQ" } + }; + private string _requestReferrer; + private string _credentials = "include"; + private HttpStatusCode? _responseStatusCode; + private string _responseBody; + private string _responseHeaders; + + protected override void OnInit() + { + _requestReferrer = UriHelper.GetAbsoluteUri(); + } + + private async void DoRequest() + { + _responseStatusCode = null; + + try + { + var requestMessage = new HttpRequestMessage() + { + Method = new HttpMethod(_method), + RequestUri = new Uri(_uri), + Content = string.IsNullOrEmpty(_requestBody) + ? null + : new StringContent(_requestBody) + }; + + foreach (var header in _requestHeaders) + { + // StringContent automatically adds its own Content-Type header with default value "text/plain" + // If the developer is trying to specify a content type explicitly, we need to replace the default value, + // rather than adding a second Content-Type header. + if (header.Name.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) && requestMessage.Content != null) + { + requestMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(header.Value); + continue; + } + + if (!requestMessage.Headers.TryAddWithoutValidation(header.Name, header.Value)) + { + requestMessage.Content?.Headers.TryAddWithoutValidation(header.Name, header.Value); + } + } + + requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new + { + referrer = _requestReferrer, + credentials = _credentials + }; + + var response = await Http.SendAsync(requestMessage); + _responseStatusCode = response.StatusCode; + _responseBody = await response.Content.ReadAsStringAsync(); + var allHeaders = response.Headers.Concat(response.Content?.Headers + ?? Enumerable.Empty>>()); + _responseHeaders = string.Join( + Environment.NewLine, + allHeaders.Select(pair => $"{pair.Key}: {pair.Value.First()}").ToArray()); + } + catch (Exception ex) + { + if (ex is AggregateException) + { + ex = ex.InnerException; + } + _responseStatusCode = HttpStatusCode.SeeOther; + _responseBody = ex.Message + Environment.NewLine + ex.StackTrace; + } + + StateHasChanged(); + } + + private void AddHeader() + => _requestHeaders.Add(new RequestHeader()); + + private void RemoveHeader(RequestHeader header) + => _requestHeaders.Remove(header); + + private class RequestHeader + { + public string Name { get; set; } + public string Value { get; set; } + } +} diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/Pages/CallWebAPI.razor b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Pages/CallWebAPI.razor new file mode 100644 index 000000000000..119d8ab4adf0 --- /dev/null +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Pages/CallWebAPI.razor @@ -0,0 +1,129 @@ +@page "/CallWebAPI" +@using System.IO +@using System.Linq +@using System.Threading.Tasks +@inject HttpClient Http + +

Call a Web API from Blazor

+ +

Blazor Example

+ +

This example requires a running web API service based on the sample app for the Tutorial: Create a web API with ASP.NET Core MVC topic. This Blazor sample app makes requests to the web API service at https://localhost:10000/api/todo. If a different service address is used, update the ServiceEndpoint constant value in the Razor component's @@functions block.

+ +

The Blazor sample app makes a cross-origin resource sharing (CORS) request from http://localhost:5000 or https://localhost:5001 to the web API service app. Add the following CORS middleware configuration to the web API's service's Startup.Configure method before it calls UseMvc:

+ +
app.UseCors(policy => 
+    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
+    .AllowAnyMethod()
+    .WithHeaders(HeaderNames.ContentType));
+ +

Adjust the domains and ports of WithOrigins as needed for the Blazor app.

+ +

Todo Items

+ +@if (_todoItems == null) +{ +

No Todo Items found.

+} +else +{ + + + + + + + + + + + + + + + @foreach (var item in _todoItems) + { + + + + + + } + + + + + + +
CompleteName
+ + + + + + +
+ @if (item.IsComplete) + { + + } + @item.Name + + +
+ + + +
+} + + + +@functions { + private const string ServiceEndpoint = "https://localhost:10000/api/todo"; + private TodoItem[] _todoItems; + private TodoItem _editItem = new TodoItem(); + private string _editRowStyle = "none"; + private string _newItemName; + + protected override async Task OnInitAsync() => await GetTodoItems(); + + private async Task GetTodoItems() => _todoItems = await Http.GetJsonAsync(ServiceEndpoint); + + private void EditItem(long id) + { + var editItem = _todoItems.Single(i => i.Id == id); + _editItem = new TodoItem { Id = editItem.Id, Name = editItem.Name, IsComplete = editItem.IsComplete }; + _editRowStyle = "table-row"; + } + + private async Task AddItem() + { + var addItem = new TodoItem { Name = _newItemName, IsComplete = false }; + await Http.PostJsonAsync(ServiceEndpoint, addItem); + _newItemName = string.Empty; + await GetTodoItems(); + _editRowStyle = "none"; + } + + private async Task SaveItem() + { + await Http.PutJsonAsync(Path.Combine(ServiceEndpoint, _editItem.Id.ToString()), _editItem); + await GetTodoItems(); + _editRowStyle = "none"; + } + + private async Task DeleteItem(long id) + { + await Http.DeleteAsync(Path.Combine(ServiceEndpoint, id.ToString())); + await GetTodoItems(); + _editRowStyle = "none"; + } + + private class TodoItem + { + public long Id { get; set; } + public string Name { get; set; } + public bool IsComplete { get; set; } + } +} diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/README.md b/aspnetcore/blazor/common/samples/3.x/BlazorSample/README.md index b98ea521e350..eabbb5516430 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/README.md +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/README.md @@ -1,3 +1,21 @@ # Blazor (client-side) Sample App This sample illustrates the use of Blazor scenarios described in the Blazor documentation. + +## Call web API example + +The web API example requires a running web API service based on the sample app for the Tutorial: Create a web API with ASP.NET Core MVC topic. The sample app makes requests to the web API service at `https://localhost:10000/api/todo`. If a different service address is used, update the `ServiceEndpoint` constant value in the Razor component's `@functions` block.

+ +The sample app makes a cross-origin resource sharing (CORS) request from `http://localhost:5000` or `https://localhost:5001` to the web API service app. Credentials (authorization cookies/headers) are permitted. Add the following CORS middleware configuration to the web API service's `Startup.Configure` method before it calls `UseMvc`:

+ +```csharp +app.UseCors(policy => + policy.WithOrigins("http://localhost:5000", "https://localhost:5001") + .AllowAnyMethod() + .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization) + .AllowCredentials()); +``` + +Adjust the domains and ports of `WithOrigins` as needed for the Blazor app. + +The web API service is configured for CORS to permit authorization cookies/headers and requests from client code, but the service as created by the tutorial doesn't actually authorize requests. See the ASP.NET Core Security and Identity articles for implementation guidance. diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/Shared/NavMenu.razor b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Shared/NavMenu.razor index 13ed562052e7..4786f13a7644 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/Shared/NavMenu.razor +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Shared/NavMenu.razor @@ -64,6 +64,11 @@ JavaScript Interop + diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/wwwroot/css/site.css b/aspnetcore/blazor/common/samples/3.x/BlazorSample/wwwroot/css/site.css index 24e247ab262e..f19e5a0a1234 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/wwwroot/css/site.css +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/wwwroot/css/site.css @@ -4,6 +4,11 @@ html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } +textarea { + width: 100%; + height: 60px; +} + app { position: relative; display: flex; @@ -155,6 +160,22 @@ app { border-top-width: 0; } +.table td { + vertical-align: middle; +} + +.table input { + width: 100%; +} + +.table #editRow { + background-color: lightyellow; +} + +.table #addRow { + background-color: palegreen; +} + @media (max-width: 767.98px) { .main .top-row { display: none; diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 6bb6a9e457ef..13e30414ef80 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -299,6 +299,8 @@ uid: blazor/javascript-interop - name: Debug uid: blazor/debug + - name: Call a web API + uid: blazor/call-web-api - name: Host and deploy items: - name: Overview From 48f7bd450f3dfe8dc8f54b884ea14a86adbab441 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Fri, 14 Jun 2019 12:51:55 -0500 Subject: [PATCH 2/8] Title update --- aspnetcore/blazor/call-web-api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index 0e160bd6ebf7..5d2d679b8847 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -1,14 +1,14 @@ --- -title: Call a web API +title: Call a web API from ASP.NET Core Blazor author: guardrex description: Learn how to call a web API from a Blazor app using JSON helpers, including making cross-origin resource sharing (CORS) requests. monikerRange: '>= aspnetcore-3.0' ms.author: riande ms.custom: mvc -ms.date: 05/20/2019 +ms.date: 06/14/2019 uid: blazor/call-web-api --- -# Call a web API +# Call a web API from ASP.NET Core Blazor By [Luke Latham](https://github.com/guardrex) From d0f6126d3a1949b83dd8bb803753c98c99338bfe Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Fri, 21 Jun 2019 14:21:02 -0500 Subject: [PATCH 3/8] React to feedback --- aspnetcore/blazor/call-web-api.md | 106 ++++++++---------- .../common/samples/3.x/BlazorSample/App.razor | 6 +- .../3.x/BlazorSample/BlazorSample.csproj | 12 +- .../Components/HTTPRequestTester.razor | 24 ++-- .../JsInteropClasses/ExampleJsInterop.cs | 2 +- .../3.x/BlazorSample/Pages/CallWebAPI.razor | 27 ++--- .../3.x/BlazorSample/wwwroot/css/site.css | 12 ++ 7 files changed, 91 insertions(+), 98 deletions(-) diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index 5d2d679b8847..56e5f4efa9b2 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -5,12 +5,12 @@ description: Learn how to call a web API from a Blazor app using JSON helpers, i monikerRange: '>= aspnetcore-3.0' ms.author: riande ms.custom: mvc -ms.date: 06/14/2019 +ms.date: 06/21/2019 uid: blazor/call-web-api --- # Call a web API from ASP.NET Core Blazor -By [Luke Latham](https://github.com/guardrex) +By [Luke Latham](https://github.com/guardrex) and [Daniel Roth](https://github.com/danroth27) Blazor apps call web API services using [HttpClient](xref:fundamentals/http-requests). Compose requests using Blazor JSON helpers or with , which can include JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) request options. @@ -21,14 +21,19 @@ See the following components in the sample app: * Call Web API (*Pages/CallWebAPI.razor*) * HTTP Request Tester (*Components/HTTPRequestTester.razor*) +> [!NOTE] +> This topic applies to Blazor client-side apps that call web APIs in a Razor Component. +> +> In Blazor server-side apps, the implementation is provided by .NET Core. An `HttpClient` service for Blazor server-side apps is in-design. For more information, see [Making HTTP requests from Blazor apps](https://github.com/aspnet/AspNetCore/issues/10397) in the aspnet/AspNetCore GitHub repository. + ## HttpClient and JSON helpers -Use [HttpClient](xref:fundamentals/http-requests) and JSON helpers to call a web API service endpoint in a Razor component. Blazor client-side uses the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API), while Blazor server-side uses . +Use [HttpClient](xref:fundamentals/http-requests) and JSON helpers to call a web API service endpoint from a Blazor app. In Blazor client-side apps, `HttpClient` is implemented using the browser [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) and is subject to its limitations, including enforcement of the same origin policy. In Blazor server-side apps, the implementation is provided by .NET Core and can be used when making web API requests in server-side code. -Include an `@using` statement for for asynchronous web API calls and inject an `HttpClient` instance: +In Blazor client-side apps, `HttpClient` is available as a service. The client's base address is set to the originating server's address. Inject an `HttpClient` instance using the `@inject` directive. If the component isn't routable and thus doesn't include the `@page` directive, import with an `@using` directive: ```cshtml -@using System.Threading.Tasks +@using System.Net.Http @inject HttpClient Http ``` @@ -54,18 +59,14 @@ JSON helper methods send requests to a URI (a web API in the following examples) In the following code, the `_todoItems` are displayed by the component. The `GetTodoItems` method is triggered when the component is finished rendering ([OnInitAsync](xref:blazor/components#lifecycle-methods)). See the sample app for a complete example. ```cshtml - @using Microsoft.AspNetCore.Blazor.Http + @using System.Net.Http @inject HttpClient Http - @functions { - private const string ServiceEndpoint = "https://localhost:10000/api/todo"; + @code { private TodoItem[] _todoItems; protected override async Task OnInitAsync() => - await GetTodoItems(); - - private async Task GetTodoItems() => - _todoItems = await Http.GetJsonAsync(ServiceEndpoint); + _todoItems = await Http.GetJsonAsync("api/todo"); } ``` @@ -74,20 +75,19 @@ JSON helper methods send requests to a URI (a web API in the following examples) In the following code, `_newItemName` is provided by a bound element of the component. The `AddItem` method is triggered by selecting a ` + + - @functions { - private const string ServiceEndpoint = "https://localhost:10000/api/todo"; + @code { private string _newItemName; private async Task AddItem() { var addItem = new TodoItem { Name = _newItemName, IsComplete = false }; - await Http.PostJsonAsync(ServiceEndpoint, addItem); + await Http.PostJsonAsync("api/todo", addItem); } } ``` @@ -97,67 +97,67 @@ JSON helper methods send requests to a URI (a web API in the following examples) In the following code, `_editItem` values (`Id`, `Name`, `IsCompleted`) are provided by bound elements of the component. The `SaveItem` method is triggered by selecting the Save ` + + + + - @functions { - private const string ServiceEndpoint = "https://localhost:10000/api/todo"; + @code { private TodoItem _editItem = new TodoItem(); - private async Task SaveItem() - { - await Http.PutJsonAsync( - Path.Combine(ServiceEndpoint, _editItem.Id.ToString()), _editItem); - } + private async Task SaveItem() => + await Http.PutJsonAsync($"api/todo/{_editItem.Id}, _editItem); } ``` includes additional extension methods for sending HTTP requests and receiving HTTP responses. [HttpClient.DeleteAsync](xref:System.Net.Http.HttpClient.DeleteAsync*) is used to send a DELETE request to a web API. -In the following code, the Delete ` + + @functions { - private const string ServiceEndpoint = "https://localhost:10000/api/todo"; private long _id; - private async Task DeleteItem() - { - await Http.DeleteAsync(Path.Combine(ServiceEndpoint, _id.ToString())); - } + private async Task DeleteItem() => + await Http.DeleteAsync($"api/todo/{_id}"); } ``` +## Cross-origin resource sharing (CORS) + +Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction is called the *same-origin policy*. The same-origin policy prevents a malicious site from reading sensitive data from another site. Sometimes, you might want to allow other sites make cross-origin resource sharing (CORS) requests to your app. + +The sample app demonstrates the use of CORS in the Call Web API component (*Pages/CallWebAPI.razor*). + +For more information, see . + ## HttpClient and HttpRequestMessage with Fetch API request options -Use [HttpClient](xref:fundamentals/http-requests) and to supply request options to the underlying JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API). +Use [HttpClient](xref:fundamentals/http-requests) and to customize requests. In the following example: -* JSON serialization and deserialization must be handled by user code (*not shown*). -* The `credentials` property is set to any of the following values: +When running on WebAssembly in a Blazor client-side app, supply request options to the underlying JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) using the `WebAssemblyHttpMessageHandler.FetchArgs` property on the request. As shown in the following example, the `credentials` property is set to any of the following values: - * `FetchCredentialsOption.Include` ("include") – Advises the browser to send credentials (such as cookies or HTTP auth headers) even for cross-origin requests. Only allowed when the CORS Middleware policy in the web API is configured to . + * `FetchCredentialsOption.Include` ("include") – Advises the browser to send credentials (such as cookies or HTTP authentication headers) even for cross-origin requests. Only allowed when the CORS policy is configured to allow credentials. * `FetchCredentialsOption.Omit` ("omit") – Advises the browser never to send credentials (such as cookies or HTTP auth headers). * `FetchCredentialsOption.SameOrigin` ("same-origin") – Advises the browser to send credentials (such as cookies or HTTP auth headers) only if the target URL is on the same origin as the calling application. ```cshtml +@using System.Net.Http @using System.Net.Http.Headers -@using Microsoft.AspNetCore.Blazor.Http @inject HttpClient Http -@functions { +@code { private async Task PostRequest() { Http.DefaultRequestHeaders.Authorization = @@ -191,7 +191,7 @@ In the following example: } ``` -For more information on Fetch API options, see [MDN web docs: Window​OrWorker​Global​Scope​.fetch():Parameters](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters). +For more information on Fetch API options, see [MDN web docs: WindowOrWorkerGlobalScope.fetch():Parameters](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters). When sending credentials (authorization cookies/headers) on CORS requests, allow the `Authorization` header when creating the CORS policy for CORS middleware. The following policy includes configuration for: @@ -210,20 +210,6 @@ app.UseCors(policy => For more information, see and the sample app's HTTP Request Tester component (*Components/HTTPRequestTester.razor*). -## Cross-origin resource sharing (CORS) - -For more information on making cross-origin resource sharing (CORS) requests, see . The sample app demonstrates CORS. See the Call Web API component (*Pages/CallWebAPI.razor*). - -## HTTP Request Tester - -The sample app includes an HTTP Request Tester component (*Components/HTTPRequestTester.razor*). The component is included in the Call Web API component: - -```cshtml - -``` - -Use the component to test request-responses against web API service endpoints. - ## Additional resources * diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/App.razor b/aspnetcore/blazor/common/samples/3.x/BlazorSample/App.razor index 80fb778674c0..eb5c93b6dee4 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/App.razor +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/App.razor @@ -1 +1,5 @@ - + + +

Sorry, there's nothing at this address.

+
+
diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/BlazorSample.csproj b/aspnetcore/blazor/common/samples/3.x/BlazorSample/BlazorSample.csproj index 1ac342710b4b..d32a6889b973 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/BlazorSample.csproj +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/BlazorSample.csproj @@ -1,19 +1,15 @@ - + netstandard2.0 - - https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; - https://dotnet.myget.org/F/blazor-dev/api/v3/index.json; - 7.3 3.0 - - - + + + diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/Components/HTTPRequestTester.razor b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Components/HTTPRequestTester.razor index b75c95fa0449..1c7b20d1903e 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/Components/HTTPRequestTester.razor +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Components/HTTPRequestTester.razor @@ -1,6 +1,4 @@ -@using System.Net @using System.Net.Http -@using Microsoft.AspNetCore.Blazor.Http @using Microsoft.AspNetCore.Components @inject HttpClient Http @inject IUriHelper UriHelper @@ -29,12 +27,12 @@

URI:
- +

Method:
- @@ -44,7 +42,7 @@

Credentials:
- @@ -53,7 +51,7 @@

Request body:
- +

@@ -61,20 +59,20 @@ @foreach (var header in _requestHeaders) {

- Name: - Value: - + Name: + Value: +
} - +

Request referrer:
- +

- + @if (_responseStatusCode.HasValue) { @@ -84,7 +82,7 @@

Headers:

} -@functions { +@code { private string _uri = "https://localhost:10000/api/todo"; private string _method = "POST"; private string _requestBody = @"{""name"":""A New Todo Item"",""isComplete"":false}"; diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/JsInteropClasses/ExampleJsInterop.cs b/aspnetcore/blazor/common/samples/3.x/BlazorSample/JsInteropClasses/ExampleJsInterop.cs index d19d16170939..b873a5c6e204 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/JsInteropClasses/ExampleJsInterop.cs +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/JsInteropClasses/ExampleJsInterop.cs @@ -22,7 +22,7 @@ public Task CallHelloHelperSayHello(string name) // sayHello is implemented in wwwroot/exampleJsInterop.js return _jsRuntime.InvokeAsync( "exampleJsFunctions.sayHello", - new DotNetObjectRef(new HelloHelper(name))); + DotNetObjectRef.Create(new HelloHelper(name))); } } #endregion diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/Pages/CallWebAPI.razor b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Pages/CallWebAPI.razor index 119d8ab4adf0..f45692e2526c 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/Pages/CallWebAPI.razor +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/Pages/CallWebAPI.razor @@ -1,14 +1,11 @@ @page "/CallWebAPI" -@using System.IO -@using System.Linq -@using System.Threading.Tasks @inject HttpClient Http

Call a Web API from Blazor

Blazor Example

-

This example requires a running web API service based on the sample app for the Tutorial: Create a web API with ASP.NET Core MVC topic. This Blazor sample app makes requests to the web API service at https://localhost:10000/api/todo. If a different service address is used, update the ServiceEndpoint constant value in the Razor component's @@functions block.

+

This example requires a running web API service based on the sample app for the Tutorial: Create a web API with ASP.NET Core MVC topic. This Blazor sample app makes requests to the web API service at https://localhost:10000/api/todo. If a different service address is used, update the ServiceEndpoint constant value in the Razor component's @@code block.

The Blazor sample app makes a cross-origin resource sharing (CORS) request from http://localhost:5000 or https://localhost:5001 to the web API service app. Add the following CORS middleware configuration to the web API's service's Startup.Configure method before it calls UseMvc:

@@ -38,14 +35,14 @@ else - + - + - - + + @foreach (var item in _todoItems) @@ -59,18 +56,18 @@ else @item.Name - - + + } - + - + @@ -79,7 +76,7 @@ else -@functions { +@code { private const string ServiceEndpoint = "https://localhost:10000/api/todo"; private TodoItem[] _todoItems; private TodoItem _editItem = new TodoItem(); @@ -108,14 +105,14 @@ else private async Task SaveItem() { - await Http.PutJsonAsync(Path.Combine(ServiceEndpoint, _editItem.Id.ToString()), _editItem); + await Http.PutJsonAsync($"{ServiceEndpoint}/{_editItem.Id}", _editItem); await GetTodoItems(); _editRowStyle = "none"; } private async Task DeleteItem(long id) { - await Http.DeleteAsync(Path.Combine(ServiceEndpoint, id.ToString())); + await Http.DeleteAsync($"{ServiceEndpoint}/{id}"); await GetTodoItems(); _editRowStyle = "none"; } diff --git a/aspnetcore/blazor/common/samples/3.x/BlazorSample/wwwroot/css/site.css b/aspnetcore/blazor/common/samples/3.x/BlazorSample/wwwroot/css/site.css index f19e5a0a1234..9ff278b44fcd 100644 --- a/aspnetcore/blazor/common/samples/3.x/BlazorSample/wwwroot/css/site.css +++ b/aspnetcore/blazor/common/samples/3.x/BlazorSample/wwwroot/css/site.css @@ -89,6 +89,18 @@ app { background-color: rgba(255, 255, 255, 0.1); } +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + .panel { margin-bottom: 20px; border: 1px solid transparent; From e5d3cdcaec7974f791e4687284f19aa6b0c203f4 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Fri, 21 Jun 2019 14:35:56 -0500 Subject: [PATCH 4/8] Update --- aspnetcore/blazor/call-web-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index 56e5f4efa9b2..c1feb03bf4f7 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -124,7 +124,7 @@ In the following code, the Delete ` -@functions { +@code { private long _id; private async Task DeleteItem() => From 9f09831a0ed52f46da1416e23aaaf17625aa283b Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Mon, 24 Jun 2019 08:25:18 -0500 Subject: [PATCH 5/8] React to feedback --- aspnetcore/blazor/call-web-api.md | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index c1feb03bf4f7..a031cc0e84b3 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -5,32 +5,29 @@ description: Learn how to call a web API from a Blazor app using JSON helpers, i monikerRange: '>= aspnetcore-3.0' ms.author: riande ms.custom: mvc -ms.date: 06/21/2019 +ms.date: 06/24/2019 uid: blazor/call-web-api --- # Call a web API from ASP.NET Core Blazor By [Luke Latham](https://github.com/guardrex) and [Daniel Roth](https://github.com/danroth27) -Blazor apps call web API services using [HttpClient](xref:fundamentals/http-requests). Compose requests using Blazor JSON helpers or with , which can include JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) request options. +Blazor client-side apps call web APIs using a preconfigured `HttpClient` service. Compose requests, which can include JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) options, using Blazor JSON helpers or with . + +Blazor server-side apps call web APIs using , which implements . For more information, see . [View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/blazor/common/samples/) ([how to download](xref:index#how-to-download-a-sample)) -See the following components in the sample app: +For Blazor client-side examples, see the following components in the sample app: * Call Web API (*Pages/CallWebAPI.razor*) * HTTP Request Tester (*Components/HTTPRequestTester.razor*) -> [!NOTE] -> This topic applies to Blazor client-side apps that call web APIs in a Razor Component. -> -> In Blazor server-side apps, the implementation is provided by .NET Core. An `HttpClient` service for Blazor server-side apps is in-design. For more information, see [Making HTTP requests from Blazor apps](https://github.com/aspnet/AspNetCore/issues/10397) in the aspnet/AspNetCore GitHub repository. - ## HttpClient and JSON helpers -Use [HttpClient](xref:fundamentals/http-requests) and JSON helpers to call a web API service endpoint from a Blazor app. In Blazor client-side apps, `HttpClient` is implemented using the browser [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) and is subject to its limitations, including enforcement of the same origin policy. In Blazor server-side apps, the implementation is provided by .NET Core and can be used when making web API requests in server-side code. +In Blazor client-side apps, [HttpClient](xref:fundamentals/http-requests) is available as a preconfigured service for making requests back to the origin server. `HttpClient` and JSON helpers are also used to call third party web API service endpoints. `HttpClient` is implemented using the browser [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) and is subject to its limitations, including enforcement of the same origin policy. -In Blazor client-side apps, `HttpClient` is available as a service. The client's base address is set to the originating server's address. Inject an `HttpClient` instance using the `@inject` directive. If the component isn't routable and thus doesn't include the `@page` directive, import with an `@using` directive: +The client's base address is set to the originating server's address. Inject an `HttpClient` instance using the `@inject` directive: ```cshtml @using System.Net.Http @@ -94,13 +91,12 @@ JSON helper methods send requests to a URI (a web API in the following examples) * `PutJsonAsync` – Sends a PUT request, including JSON-encoded content. - In the following code, `_editItem` values (`Id`, `Name`, `IsCompleted`) are provided by bound elements of the component. The `SaveItem` method is triggered by selecting the Save ` @@ -108,6 +104,13 @@ JSON helper methods send requests to a URI (a web API in the following examples) @code { private TodoItem _editItem = new TodoItem(); + private void EditItem(long id) + { + var editItem = _todoItems.Single(i => i.Id == id); + _editItem = new TodoItem { Id = editItem.Id, Name = editItem.Name, + IsComplete = editItem.IsComplete }; + } + private async Task SaveItem() => await Http.PutJsonAsync($"api/todo/{_editItem.Id}, _editItem); } @@ -142,15 +145,13 @@ For more information, see . ## HttpClient and HttpRequestMessage with Fetch API request options -Use [HttpClient](xref:fundamentals/http-requests) and to customize requests. - -In the following example: +When running on WebAssembly in a Blazor client-side app, use [HttpClient](xref:fundamentals/http-requests) and to customize requests. For example, you can specify the request URI, HTTP method, and any desired request headers. -When running on WebAssembly in a Blazor client-side app, supply request options to the underlying JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) using the `WebAssemblyHttpMessageHandler.FetchArgs` property on the request. As shown in the following example, the `credentials` property is set to any of the following values: +Supply request options to the underlying JavaScript [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) using the `WebAssemblyHttpMessageHandler.FetchArgs` property on the request. As shown in the following example, the `credentials` property is set to any of the following values: - * `FetchCredentialsOption.Include` ("include") – Advises the browser to send credentials (such as cookies or HTTP authentication headers) even for cross-origin requests. Only allowed when the CORS policy is configured to allow credentials. - * `FetchCredentialsOption.Omit` ("omit") – Advises the browser never to send credentials (such as cookies or HTTP auth headers). - * `FetchCredentialsOption.SameOrigin` ("same-origin") – Advises the browser to send credentials (such as cookies or HTTP auth headers) only if the target URL is on the same origin as the calling application. +* `FetchCredentialsOption.Include` ("include") – Advises the browser to send credentials (such as cookies or HTTP authentication headers) even for cross-origin requests. Only allowed when the CORS policy is configured to allow credentials. +* `FetchCredentialsOption.Omit` ("omit") – Advises the browser never to send credentials (such as cookies or HTTP auth headers). +* `FetchCredentialsOption.SameOrigin` ("same-origin") – Advises the browser to send credentials (such as cookies or HTTP auth headers) only if the target URL is on the same origin as the calling application. ```cshtml @using System.Net.Http From e395e37e2b6fd98b284f5fb9d5b3aec4583cabe2 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Mon, 24 Jun 2019 11:12:17 -0500 Subject: [PATCH 6/8] Apply suggestions from code review Co-Authored-By: Scott Addie <10702007+scottaddie@users.noreply.github.com> --- aspnetcore/blazor/call-web-api.md | 16 ++++++++-------- .../Components/HTTPRequestTester.razor | 6 +++--- .../3.x/BlazorSample/Pages/CallWebAPI.razor | 2 +- .../common/samples/3.x/BlazorSample/README.md | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index a031cc0e84b3..971004c3a925 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -25,7 +25,7 @@ For Blazor client-side examples, see the following components in the sample app: ## HttpClient and JSON helpers -In Blazor client-side apps, [HttpClient](xref:fundamentals/http-requests) is available as a preconfigured service for making requests back to the origin server. `HttpClient` and JSON helpers are also used to call third party web API service endpoints. `HttpClient` is implemented using the browser [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) and is subject to its limitations, including enforcement of the same origin policy. +In Blazor client-side apps, [HttpClient](xref:fundamentals/http-requests) is available as a preconfigured service for making requests back to the origin server. `HttpClient` and JSON helpers are also used to call third-party web API endpoints. `HttpClient` is implemented using the browser [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) and is subject to its limitations, including enforcement of the same origin policy. The client's base address is set to the originating server's address. Inject an `HttpClient` instance using the `@inject` directive: @@ -34,7 +34,7 @@ The client's base address is set to the originating server's address. Inject an @inject HttpClient Http ``` -In the following examples, a Todo web API service processes create, read, update, and delete (CRUD) operations. The examples are based on a `TodoItem` class that stores the: +In the following examples, a Todo web API processes create, read, update, and delete (CRUD) operations. The examples are based on a `TodoItem` class that stores the: * ID (`Id`, `long`) – Unique ID of the item. * Name (`Name`, `string`) – Name of the item. @@ -51,7 +51,7 @@ private class TodoItem JSON helper methods send requests to a URI (a web API in the following examples) and process the response: -* `GetJsonAsync` – Sends a GET request and parses the JSON response body to create an object. +* `GetJsonAsync` – Sends an HTTP GET request and parses the JSON response body to create an object. In the following code, the `_todoItems` are displayed by the component. The `GetTodoItems` method is triggered when the component is finished rendering ([OnInitAsync](xref:blazor/components#lifecycle-methods)). See the sample app for a complete example. @@ -67,7 +67,7 @@ JSON helper methods send requests to a URI (a web API in the following examples) } ``` -* `PostJsonAsync` – Sends a POST request, including JSON-encoded content, and parses the JSON response body to create an object. +* `PostJsonAsync` – Sends an HTTP POST request, including JSON-encoded content, and parses the JSON response body to create an object. In the following code, `_newItemName` is provided by a bound element of the component. The `AddItem` method is triggered by selecting a `