Clone the code repo and become familiar with it
In this unit, you'll clone the existing application's source code repository. By working with a local clone of the source, you can then familiarize yourself with the existing client-side polling functionality and evaluate how best to refactor the code.
Clone the repository
Whether you use Visual Studio, Visual Studio Code, or some other integrated development environment (IDE), you'll indirectly use Git to clone the repo. This module builds upon the Blazor workshop, which contains a pizza ordering application.
After you've successfully cloned the repository, it's best to build and run the app. You'll need to change directories into the blazor-workshop/src directory of the repo before you use the .NET CLI.
Run the application
You're free to use your IDE or the .NET CLI. From the CLI, use the dotnet run
command:
This application is intended for learning purposes. When you're authenticated, you can register any email address. While you're actively developing the app, you can complete the registration process by selecting a link to confirm your account, without having to validate the email address. For more information, see Blazor workshop: Register a user and log in.
Place a pizza order
After you've logged in, you can place an order for pizza. Select a pizza, choose the toppings, and add them to the order. As an example, consider the following image:
After you add extra toppings to your order, place it by selecting the Order button.
Immediately after you've created the order, the app redirects to the order status page, My Orders. This page displays the various order status details sequentially, from Preparing to Out for delivery and, finally, Delivered. While the order is Out for delivery, the live map updates by incrementally changing and emulating the delivery driver's location.
Consider the following series of images, which display on the live map a progression from the starting location to the ending location. Here are the statuses:
Finally, the order status page reflects the Delivered order status:
Stop the application
The console should output various logs, letting you know that the app has successfully built and that it's serving content at https://localhost:5001/
. To stop the application, close the browser and, from the command line session, select Ctrl+C.
Familiarize yourself with the code
The primary focus for this module is refactoring the client-side polling to instead use ASP.NET Core SignalR. The process of ordering a pizza redirects users to the order details page. This page performs the client-side polling. Make sure that you understand how this is currently implemented, so that you know what needs to be refactored. Consider the OrderDetails.razor file:
@page "/myorders/{orderId:int}"
@attribute [Authorize]
@using System.Threading
@inject OrdersClient OrdersClient
@implements IDisposable
<div class="main">
@if (invalidOrder)
{
<h2>Nope</h2>
<p>Sorry, this order could not be loaded.</p>
}
else if (orderWithStatus == null)
{
<text>Loading...</text>
}
else
{
<div class="track-order">
<div class="track-order-title">
<h2>
Order placed @orderWithStatus.Order.CreatedTime.ToLongDateString()
</h2>
<p class="ml-auto mb-0">
Status: <strong>@orderWithStatus.StatusText</strong>
</p>
</div>
<div class="track-order-body">
<div class="track-order-details">
<OrderReview Order="orderWithStatus.Order" />
</div>
<div class="track-order-map">
<Map Zoom="13" Markers="orderWithStatus.MapMarkers" />
</div>
</div>
</div>
}
</div>
@code {
[Parameter] public int OrderId { get; set; }
OrderWithStatus orderWithStatus;
bool invalidOrder;
CancellationTokenSource pollingCancellationToken;
protected override void OnParametersSet()
{
// If we were already polling for a different order, stop doing so
pollingCancellationToken?.Cancel();
// Start a new poll loop
PollForUpdates();
}
private async void PollForUpdates()
{
invalidOrder = false;
pollingCancellationToken = new CancellationTokenSource();
while (!pollingCancellationToken.IsCancellationRequested)
{
try
{
orderWithStatus = await OrdersClient.GetOrder(OrderId);
StateHasChanged();
if (orderWithStatus.IsDelivered)
{
pollingCancellationToken.Cancel();
}
else
{
await Task.Delay(4000);
}
}
catch (AccessTokenNotAvailableException ex)
{
pollingCancellationToken.Cancel();
ex.Redirect();
}
catch (Exception ex)
{
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
StateHasChanged();
}
}
}
void IDisposable.Dispose()
{
pollingCancellationToken?.Cancel();
}
}
The preceding Razor markup does the following:
- It binds values from the
orderWithStatus
object as part of the component template.- The created time and the status text values are bound in the order title markup.
- The
orderWithStatus.Order
is passed as an argument to theOrderReview
component. - The map markers (which represent the markings on the live map) are passed to the
Map
component.
- When the
OrderId
parameter is set, thePollForUpdates
is started.- This method will make an HTTP request to the server every four seconds.
- The latest order status details are reassigned to the
orderWithStatus
variable.
Note
The PollForUpdates
method is async void
, which means that it's "fire-and-forget." This can cause unexpected behavior and should be avoided if possible. It will be refactored as part of the changes.
Each time the order is received, it recalculates delivery status updates and corresponding map marker changes. This is achieved by calculating properties on the OrderWithStatus
object. Consider the following OrderWithStatus.cs C# file:
using BlazingPizza.ComponentsLibrary.Map;
using System;
using System.Collections.Generic;
namespace BlazingPizza
{
public class OrderWithStatus
{
public readonly static TimeSpan PreparationDuration = TimeSpan.FromSeconds(10);
public readonly static TimeSpan DeliveryDuration = TimeSpan.FromMinutes(1); // Unrealistic, but more interesting to watch
public Order Order { get; set; }
public string StatusText { get; set; }
public bool IsDelivered => StatusText == "Delivered";
public List<Marker> MapMarkers { get; set; }
public static OrderWithStatus FromOrder(Order order)
{
// To simulate a real backend process, we fake status updates based on the amount
// of time since the order was placed
string statusText;
List<Marker> mapMarkers;
var dispatchTime = order.CreatedTime.Add(PreparationDuration);
if (DateTime.Now < dispatchTime)
{
statusText = "Preparing";
mapMarkers = new List<Marker>
{
ToMapMarker("You", order.DeliveryLocation, showPopup: true)
};
}
else if (DateTime.Now < dispatchTime + DeliveryDuration)
{
statusText = "Out for delivery";
var startPosition = ComputeStartPosition(order);
var proportionOfDeliveryCompleted = Math.Min(1, (DateTime.Now - dispatchTime).TotalMilliseconds / DeliveryDuration.TotalMilliseconds);
var driverPosition = LatLong.Interpolate(startPosition, order.DeliveryLocation, proportionOfDeliveryCompleted);
mapMarkers = new List<Marker>
{
ToMapMarker("You", order.DeliveryLocation),
ToMapMarker("Driver", driverPosition, showPopup: true),
};
}
else
{
statusText = "Delivered";
mapMarkers = new List<Marker>
{
ToMapMarker("Delivery location", order.DeliveryLocation, showPopup: true),
};
}
return new OrderWithStatus
{
Order = order,
StatusText = statusText,
MapMarkers = mapMarkers,
};
}
private static LatLong ComputeStartPosition(Order order)
{
// Random but deterministic based on order ID
var rng = new Random(order.OrderId);
var distance = 0.01 + rng.NextDouble() * 0.02;
var angle = rng.NextDouble() * Math.PI * 2;
var offset = (distance * Math.Cos(angle), distance * Math.Sin(angle));
return new LatLong(order.DeliveryLocation.Latitude + offset.Item1, order.DeliveryLocation.Longitude + offset.Item2);
}
static Marker ToMapMarker(string description, LatLong coords, bool showPopup = false)
=> new Marker { Description = description, X = coords.Longitude, Y = coords.Latitude, ShowPopup = showPopup };
}
}
In the preceding C# code, FromOrder
calculates a new order status that's based on the current time. Based on your understanding of how this was implemented, you'll be able to reuse the OrderWithStatus
object, and you'll learn how the app was refactored.
Fetch the refactored code
The refactored code is in a separate branch that's named signalr
.
Use the git remote
command to determine the name of the https://github.com/MicrosoftDocs/mslearn-blazing-pizza-signalr
repo:
git remote -v
The remote name that corresponds to the https://github.com/MicrosoftDocs/mslearn-blazing-pizza-signalr
repo is the name you'll need to use. Next, use the git fetch
command to fetch the signalr
branch. This assumes that your remote is named upstream
, although the name might be origin
.
git fetch upstream signalr
Finally, use the git checkout
command to context-switch into the refactored source:
git checkout signalr