Commit b59b614d authored by Daniel Roth's avatar Daniel Roth
Browse files

Update save points

parent 48f8129d
......@@ -85,7 +85,7 @@ Then add a `@code` block that makes an asynchronous request for the data we need
```csharp
@code {
List<OrderWithStatus> ordersWithStatus;
IEnumerable<OrderWithStatus> ordersWithStatus;
protected override async Task OnParametersSetAsync()
{
......@@ -108,7 +108,7 @@ It's simple to express this using `@if/else` blocks in Razor code. Update the ma
{
<text>Loading...</text>
}
else if (ordersWithStatus.Count == 0)
else if (!ordersWithStatus.Any())
{
<h2>No orders placed</h2>
<a class="btn btn-success" href="">Order some pizza</a>
......
......@@ -210,136 +210,125 @@ The user is logged in and redirected back to the home page.
## Request an access token
Even though you are now logged in, placing an order still fails because the HTTP request to place the order requires a valid access token. To request an access token use the `IAccessTokenProvider` service. If requesting an access token succeeds, add it to the request with a standard Authentication header with scheme Bearer. If the token request fails, use the `NavigationManager` service to redirect the user to the authorization service to request a new token.
Even though you are now logged in, placing an order still fails because the HTTP request to place the order requires a valid access token. To request access tokens and attach them to outbound requests, use the `BaseAddressAuthorizationMessageHandler` with the `HttpClient` that you're using to make the request. This message handler will acquire access tokens using the built-in `IAccessTokenProvider` service and attach them to each request using the standard Authorization header. If an access token cannot be acquired, an `AccessTokenNotAvailableException` is thrown, which can be used to redirect the user to the login page to authorize a new token.
*BlazingPizza.Client/Pages/Checkout.razor*
To add the `BaseAddressAuthorizationMessageHandler` to our `HttpClient` in our app, we'll use the [IHttpClientFactory` helpers from ASP.NET Core](https://docs.microsoft.com/aspnet/core/fundamentals/http-requests) with a strongly typed client.
```razor
@page "/checkout"
@attribute [Authorize]
@inject OrderState OrderState
@inject HttpClient HttpClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
To create the strongly typed client, add a new `OrdersClient` class to the client project. The class should take an `HttpClient` in its constructor, and provide methods getting and placing orders:
<div class="main">
...
</div>
*BlazingPizza.Client/OrdersClient.cs*
@code {
bool isSubmitting;
async Task PlaceOrder()
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace BlazingPizza.Client
{
public class OrdersClient
{
isSubmitting = true;
private readonly HttpClient httpClient;
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
public OrdersClient(HttpClient httpClient)
{
var request = new HttpRequestMessage(HttpMethod.Post, "orders");
request.Content = JsonContent.Create(OrderState.Order);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
var newOrderId = await response.Content.ReadFromJsonAsync<int>();
OrderState.ResetOrder();
NavigationManager.NavigateTo($"myorders/{newOrderId}");
this.httpClient = httpClient;
}
else
public async Task<IEnumerable<OrderWithStatus>> GetOrders() =>
await httpClient.GetFromJsonAsync<IEnumerable<OrderWithStatus>>("orders");
public async Task<OrderWithStatus> GetOrder(int orderId) =>
await httpClient.GetFromJsonAsync<OrderWithStatus>($"orders/{orderId}");
public async Task<int> PlaceOrder(Order order)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
var response = await httpClient.PostAsJsonAsync("orders", order);
response.EnsureSuccessStatusCode();
var orderId = await response.Content.ReadFromJsonAsync<int>();
return orderId;
}
}
}
```
Update the `MyOrders` and `OrderDetails` components to also make authenticated HTTP requests.
Register the `OrdersClient` as a typed client, with the underlying `HttpClient` configured with the correct base address and the `BaseAddressAuthorizationMessageHandler`.
*BlazingPizza.Client/Pages/MyOrders.razor*
```csharp
builder.Services.AddHttpClient<OrdersClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
```
```razor
@page "/myorders"
@inject HttpClient HttpClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
Update each page where an `HttpClient` is used to manage orders to use the new typed `OrdersClient`. Inject an `OrdersClient` instead of an `HttpClient` and use the new client to make the API call. Wrap each call in a `try-catch` that handles exceptions of type `AccessTokenNotAvailableException` by calling the provided `Redirect()` method.
<div class="main">
...
</div>
*Checkout.razor*
@code {
List<OrderWithStatus> ordersWithStatus;
```csharp
async Task PlaceOrder()
{
isSubmitting = true;
protected override async Task OnParametersSetAsync()
try
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
{
var request = new HttpRequestMessage(HttpMethod.Get, "orders");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
ordersWithStatus = await response.Content.ReadFromJsonAsync<List<OrderWithStatus>>();
}
else
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
}
var newOrderId = await OrdersClient.PlaceOrder(OrderState.Order);
OrderState.ResetOrder();
NavigationManager.NavigateTo($"myorders/{newOrderId}");
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
```
*BlazingPizza.Client/Pages/OrderDetails.razor*
*MyOrders.razor*
```razor
@page "/myorders/{orderId:int}"
@using System.Threading
@inject HttpClient HttpClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
@implements IDisposable
<div class="main">
....
</div>
```csharp
protected override async Task OnParametersSetAsync()
{
try
{
ordersWithStatus = await OrdersClient.GetOrders();
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
```
@code {
...
*OrderDetails.razor*
private async void PollForUpdates()
```csharp
private async void PollForUpdates()
{
invalidOrder = false;
pollingCancellationToken = new CancellationTokenSource();
while (!pollingCancellationToken.IsCancellationRequested)
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
try
{
orderWithStatus = await OrdersClient.GetOrder(OrderId);
StateHasChanged();
await Task.Delay(4000);
}
catch (AccessTokenNotAvailableException ex)
{
pollingCancellationToken = new CancellationTokenSource();
while (!pollingCancellationToken.IsCancellationRequested)
{
try
{
invalidOrder = false;
var request = new HttpRequestMessage(HttpMethod.Get, $"orders/{OrderId}");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
orderWithStatus = await response.Content.ReadFromJsonAsync<OrderWithStatus>();
}
catch (Exception ex)
{
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
}
StateHasChanged();
await Task.Delay(4000);
}
pollingCancellationToken.Cancel();
ex.Redirect();
}
else
catch (Exception ex)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
StateHasChanged();
}
}
...
}
```
......
......@@ -125,25 +125,27 @@ You'll then need to define `RequestNotificationSubscriptionAsync`. Add this else
```cs
async Task RequestNotificationSubscriptionAsync()
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>("blazorPushNotifications.requestSubscription");
if (subscription != null)
{
var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>("blazorPushNotifications.requestSubscription");
if (subscription != null)
try
{
var request = new HttpRequestMessage(HttpMethod.Put, "notifications/subscribe");
request.Content = JsonContent.Create(subscription);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
await HttpClient.SendAsync(request);
await OrdersClient.SubscribeToNotifications(subscription);
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
else
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
}
}
```
Also add the `SubscribeToNotifications` method to `OrdersClient`.
```csharp
```
You'll also need to inject the `IJSRuntime` service into the `Checkout` component.
```razor
......
......@@ -6,7 +6,7 @@
{
<text>Loading...</text>
}
else if (ordersWithStatus.Count == 0)
else if (!ordersWithStatus.Any())
{
<h2>No orders placed</h2>
<a class="btn btn-success" href="">Order some pizza</a>
......@@ -39,7 +39,7 @@
</div>
@code {
List<OrderWithStatus> ordersWithStatus;
IEnumerable<OrderWithStatus> ordersWithStatus;
protected override async Task OnParametersSetAsync()
{
......
......@@ -6,7 +6,7 @@
{
<text>Loading...</text>
}
else if (ordersWithStatus.Count == 0)
else if (!ordersWithStatus.Any())
{
<h2>No orders placed</h2>
<a class="btn btn-success" href="">Order some pizza</a>
......@@ -39,7 +39,7 @@
</div>
@code {
List<OrderWithStatus> ordersWithStatus;
IEnumerable<OrderWithStatus> ordersWithStatus;
protected override async Task OnParametersSetAsync()
{
......
......@@ -6,7 +6,7 @@
{
<text>Loading...</text>
}
else if (ordersWithStatus.Count == 0)
else if (!ordersWithStatus.Any())
{
<h2>No orders placed</h2>
<a class="btn btn-success" href="">Order some pizza</a>
......@@ -39,7 +39,7 @@
</div>
@code {
List<OrderWithStatus> ordersWithStatus;
IEnumerable<OrderWithStatus> ordersWithStatus;
protected override async Task OnParametersSetAsync()
{
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace BlazingPizza.Client
{
public class OrdersClient
{
private readonly HttpClient httpClient;
public OrdersClient(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public async Task<IEnumerable<OrderWithStatus>> GetOrders() =>
await httpClient.GetFromJsonAsync<IEnumerable<OrderWithStatus>>("orders");
public async Task<OrderWithStatus> GetOrder(int orderId) =>
await httpClient.GetFromJsonAsync<OrderWithStatus>($"orders/{orderId}");
public async Task<int> PlaceOrder(Order order)
{
var response = await httpClient.PostAsJsonAsync("orders", order);
response.EnsureSuccessStatusCode();
var orderId = await response.Content.ReadFromJsonAsync<int>();
return orderId;
}
}
}
\ No newline at end of file
@page "/checkout"
@attribute [Authorize]
@inject OrderState OrderState
@inject HttpClient HttpClient
@inject OrdersClient OrdersClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
<div class="main">
<EditForm Model="OrderState.Order.DeliveryAddress" OnValidSubmit="PlaceOrder">
......@@ -34,20 +33,15 @@
{
isSubmitting = true;
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
try
{
var request = new HttpRequestMessage(HttpMethod.Post, "orders");
request.Content = JsonContent.Create(OrderState.Order);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
var newOrderId = await response.Content.ReadFromJsonAsync<int>();
var newOrderId = await OrdersClient.PlaceOrder(OrderState.Order);
OrderState.ResetOrder();
NavigationManager.NavigateTo($"myorders/{newOrderId}");
}
else
catch (AccessTokenNotAvailableException ex)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
ex.Redirect();
}
}
}
@page "/myorders"
@attribute [Authorize]
@inject HttpClient HttpClient
@inject OrdersClient OrdersClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
<div class="main">
@if (ordersWithStatus == null)
{
<text>Loading...</text>
}
else if (ordersWithStatus.Count == 0)
else if (!ordersWithStatus.Any())
{
<h2>No orders placed</h2>
<a class="btn btn-success" href="">Order some pizza</a>
......@@ -42,21 +41,17 @@
</div>
@code {
List<OrderWithStatus> ordersWithStatus;
IEnumerable<OrderWithStatus> ordersWithStatus;
protected override async Task OnParametersSetAsync()
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
try
{
var request = new HttpRequestMessage(HttpMethod.Get, "orders");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
ordersWithStatus = await response.Content.ReadFromJsonAsync<List<OrderWithStatus>>();
ordersWithStatus = await OrdersClient.GetOrders();
}
else
catch (AccessTokenNotAvailableException ex)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
ex.Redirect();
}
}
}
@page "/myorders/{orderId:int}"
@attribute [Authorize]
@using System.Threading
@inject HttpClient HttpClient
@inject OrdersClient OrdersClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
@implements IDisposable
<div class="main">
......@@ -54,35 +53,28 @@
private async void PollForUpdates()
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
invalidOrder = false;
pollingCancellationToken = new CancellationTokenSource();
while (!pollingCancellationToken.IsCancellationRequested)
{
pollingCancellationToken = new CancellationTokenSource();
while (!pollingCancellationToken.IsCancellationRequested)
try
{
try
{
invalidOrder = false;
var request = new HttpRequestMessage(HttpMethod.Get, $"orders/{OrderId}");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
orderWithStatus = await response.Content.ReadFromJsonAsync<OrderWithStatus>();
}
catch (Exception ex)
{
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
}
orderWithStatus = await OrdersClient.GetOrder(OrderId);
StateHasChanged();
await Task.Delay(4000);
}
}
else
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
catch (AccessTokenNotAvailableException ex)
{
pollingCancellationToken.Cancel();
ex.Redirect();
}
catch (Exception ex)
{
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
StateHasChanged();
}
}
}
......
......@@ -15,6 +15,8 @@ namespace BlazingPizza.Client
builder.RootComponents.Add<App>("app");
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddHttpClient<OrdersClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddScoped<OrderState>();
// Add auth services
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace BlazingPizza.Client
{
public class OrdersClient
{
private readonly HttpClient httpClient;
public OrdersClient(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public async Task<IEnumerable<OrderWithStatus>> GetOrders() =>
await httpClient.GetFromJsonAsync<IEnumerable<OrderWithStatus>>("orders");
public async Task<OrderWithStatus> GetOrder(int orderId) =>
await httpClient.GetFromJsonAsync<OrderWithStatus>($"orders/{orderId}");
public async Task<int> PlaceOrder(Order order)
{
var response = await httpClient.PostAsJsonAsync("orders", order);
response.EnsureSuccessStatusCode();
var orderId = await response.Content.ReadFromJsonAsync<int>();
return orderId;
}
}
}
\ No newline at end of file
@page "/checkout"
@attribute [Authorize]
@inject OrderState OrderState
@inject HttpClient HttpClient
@inject OrdersClient OrdersClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
<div class="main">
<EditForm Model="OrderState.Order.DeliveryAddress" OnValidSubmit="PlaceOrder">
......@@ -34,20 +33,15 @@
{
isSubmitting = true;
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
try
{
var request = new HttpRequestMessage(HttpMethod.Post, "orders");
request.Content = JsonContent.Create(OrderState.Order);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
var newOrderId = await response.Content.ReadFromJsonAsync<int>();
var newOrderId = await OrdersClient.PlaceOrder(OrderState.Order);
OrderState.ResetOrder();
NavigationManager.NavigateTo($"myorders/{newOrderId}");
}
else
catch (AccessTokenNotAvailableException ex)