Clone the code repo and become familiar with it

Completed

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.

git clone https://github.com/MicrosoftDocs/mslearn-blazing-pizza-signalr

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.

cd ./mslearn-blazing-pizza-signalr/blazing-pizza/src

Run the application

You're free to use your IDE or the .NET CLI. From the CLI, use the dotnet run command:

dotnet run --project ./BlazingPizza.Server/BlazingPizza.Server.csproj

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:

Screenshot of the Blazing Pizza window for adding extra toppings to a pizza order.

After you add extra toppings to your order, place it by selecting the Order button.

Screenshot of the Blazing Pizza window for placing order.

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:

Preparing Screenshot of the Blazing Pizza 'My Orders' window with an order status of 'Preparing'.

Out for delivery 1 Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver is just leaving.

Out for delivery 2 Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver has gone about a quarter of the way.

Out for delivery 3 Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver has gone about halfway.

Out for delivery 4 Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver has gone about three quarters of the way.

Out for delivery 5 Screenshot of the 'My Orders' window with an order status of 'Out for delivery'. The live map shows that the driver has arrived at the destination.

Finally, the order status page reflects the Delivered order status:

Screenshot of the 'My Orders' window with an order status of 'Delivered'. The live map shows that the driver has arrived at the delivery location.

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 the OrderReview component.
    • The map markers (which represent the markings on the live map) are passed to the Map component.
  • When the OrderId parameter is set, the PollForUpdates 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