Imagine you are tasked with making a flight ticket reservation website. The first page of the website should show a list of available flights from different flight providers aka airlines through receiving the data from their web APIs. Unfortunately, some flight providers might take considerable time to provide a list of available flights or cannot return flights immediately. In this situation, making our website’s users wait for a long period to see the available flights might hurt their user experience. This tutorial illustrates how you can design the available flights page with the mentioned constraints. 

1- Creating domain entities

The first thing that we should think about is the properties of the flight we are going to display. We define the flight class as follows.

public class Flight
{
  public string FlightNumber { get; set; }
  public DateTime BoardingDateTime { get; set; } 
  public string SourceCity { get; set; }
  public string DestinationCity { get; set; } 
  public byte AvailableSeatNumbers { get; set; }
  public byte AllowableCargoWeight { get; set; } 
  public string SourceAirport { get; set; }
  public string DestinationAirport { get; set; } 
  public decimal Price { get; set; }
}

2- Creating a contract for data providers aka airlines

The following contract has a method returning the result containing a list of available flights along with the name of the airline. The reason for creating such a contract is that we want to design a system adhering the open-closed principle of SOLID principles. The Open-Closed Principle simply means that you can add new functionality to your software without having to change the existing code. In this situation, we can add more data providers later without changing our code just by adding an instance of the new data provider in the dependency injection configuration file.

public interface IAirlineApiProvider
{
  public Task<AirlineApiResult> GetAvailableFlightsAsync();
}

public class AirlineApiResult
{
  public Guid Id { get; set; }
  public string AirlineName { get; set; }
  public List<Flight> AvailableFlights { get; set; }
    = new List<Flight>();
}

3- Implementing the contract

After that, we need to implement the contract, for example, for three airlines. In reality, each implementation should contain external API call logic, but for the sake of simplicity, we return dummy data. For brevity, we just show TurkishAirlineApiProvider. Note that we have added a delay of two to six seconds in each implementation to mimic the behavior of real web APIs. You can replace the dummy data with the actual web API call in your project.

public class TurkishAirlineApiProvider : IAirlineApiProvider
{
  public async Task<AirlineApiResult> GetAvailableFlightsAsync()
  {
   var random = new Random();
   var sleepInterval = random.Next(2000, 6000);
   Thread.Sleep(sleepInterval);
 
   return new AirlineApiResult
   {
     AirlineName = "Turkish",
     AvailableFlights = new List<Flight>
     {
      new Flight
      {
        FlightNumber ="ABC123",
        BoardingDateTime =DateTime.Now,
        SourceCity= "Stockholm ",
        DestinationCity = "Hamburg",
        AvailableSeatNumbers= 35,
        AllowableCargoWeight= 21,
        SourceAirport="Stockholm Airport",
        DestinationAirport = "Hamburg Airport",
        Price=50
      },
      new Flight…,
      new Flight…
     }
   };
  }
}

4- Adding dependencies

The dependencies we need in this project are SignalR and Newtonsoft.Json. We just add SignalR dependency to our front-end because the asp.net core already has SignalR dependency in Microsoft.AspNetCore.SignalR namespace. Regarding Newtonsoft.Json, we add it through the Nugget package manager.

5- Adding SignalR to the back-end

The first step in this stage is to create a hub. We add the GetConnectionId method to let the client ask connection identifier from the server. The SignalR Hubs API enables connected clients to call methods on the server. The server defines methods that are called from the client and the client defines methods that are called from the server. SignalR takes care of everything required to make real-time client-to-server and server-to-client communication possible.

public class AvailableFlightsHub : Hub
  public string GetConnectionId()
  {
   return Context.ConnectionId;
  }
}

Then, To register the services required by SignalR hubs, call AddSignalR in Program.cs.

builder.Services.AddSignalR(); 

Next, to configure SignalR endpoints, call MapHub, also in Program.cs.

app.MapHub<AvailableFlightsHub>("/availableFlightsHub");

After that, create an API endpoint to let the client get available flights from the server. In the following code, we get the list of data providers aka airlines and call them in an asynchronous and parallel fashion. Calling each provider in the loop asynchronously prevents the code from getting blocked by a lengthy API call. We also iterate through the loop in parallel to let each airline send its data to the client as soon as it receives the data from an external API. If we did not do so in parallel, we had to wait for airlines to call their APIs one by one in sequential order, something that harms the user experience. After getting data from an airline, we serialize the data and send it to the respective client identified by a connection identifier.  

[HttpPost]
public async Task PublishAvailableFlightsToClient
  (string connectionId, string signalMethodName)
{
  await Parallel.ForEachAsync(_airlineApiProviders,
  async (airlineApiProvider, cancellationToken) =>
  {
    var airlineApiResult = 
      await airlineApiProvider.GetAvailableFlightsAsync();
    if (airlineApiResult is null)
    {
      return;
    }

    JsonConvert.SerializeObject
    (airlineApiResult, new JsonSerializerSettings
    {
      ContractResolver = new CamelCasePropertyNamesContractResolver()
    });

    await _availableFlightsHubContext.Clients.Client(connectionId)
      .SendAsync(signalMethodName, airlineApiResult);
  });
}

6- Adding SignalR to the front-end

We create a file named available-flights-hub.js to add client codes needed to connect to the server. We first create a connection with the following code. Note that the URL here and the URL that we specify in the back-end’s program.cs should be the same. Otherwise, the connection cannot be made.

const connection = new signalR.HubConnectionBuilder()
  .withUrl("/availableFlightsHub")
  .configureLogging(signalR.LogLevel.Information)
  .withAutomaticReconnect()
  .build();

Then, we start the connection. In part of the connection code, it calls the getConnectionId() method to get the connection identifier from the server. This method itself calls the GetConnectionId method from the hub in the back-end. Note that the name of the method in the back-end hub, which is GetConnectionId, should be the same as the method we mention in the front-end for making a connection.

connection
  .start()
  .then(() => this.getConnectionId())
  .catch(err => console.error(err.toString()));

function getConnectionId(){
   connection.invoke("GetConnectionId")
    .then(function (connectionId) {
      publishAvailableFlightsToClient(connectionId);
    }
   );
}

If obtaining the connection identifier is successful, the code makes an Ajax call to publishAvailableFlightsToClient API endpoint we created earlier and sends the connection identifier and the signal method name along with that. The signal method gets executed if the server sends data to the client.

function publishAvailableFlightsToClient(connectinoId) {
  $.ajax({
    type: "POST",
    url: "/home/PublishAvailableFlightsToClient",
    data: {
      "connectionId": connectinoId,
      "signalMethodName": "publishAvailableFlightsToClientSignal"
    },
    dataType: "text",
    success: function (msg) {
      console.log(msg);
    },
    error: function (req, status, error) {
      console.log(error);
    }
  });
}

Now, whenever the server successfully sends data to the client, the code finds the placeholder for printing the result with the help of jQuery. Then, it renders necessary HTML elements from the airline’s available flights. Finally, it appends all HTML elements to the placeholder.

connection.on("publishAvailableFlightsToClientSignal", (jsonAirlineApiResult) => {
  var airlineApiResultsPlaceholder = $("#airlineApiResultsPlaceholder");  
  var htmlAirlineApiResult = renderAirlineApiResult(jsonAirlineApiResult);    
  airlineApiResultsPlaceholder.append(htmlAirlineApiResult);
});

function renderAirlineApiResult(jsonAirlineApiResult) {
    var htmlAvailableFlights = '';

    for (var counter = 0; counter < jsonAirlineApiResult.availableFlights.length; counter++) {
        var currentAvailableFlight = jsonAirlineApiResult.availableFlights[counter];
        htmlAvailableFlights +=
            `<ul class="list-group my-2 mx-2"> 
                <li class="list-group-item list-group-item-secondary">${currentAvailableFlight.flightNumber}</li> 
                <li class="list-group-item ">${currentAvailableFlight.boardingDateTime}</li>                
                <li class="list-group-item ">${currentAvailableFlight.sourceCity}</li>
                <li class="list-group-item ">${currentAvailableFlight.destinationCity}</li>
                <li class="list-group-item ">${currentAvailableFlight.availableSeatNumbers}</li>
                <li class="list-group-item ">${currentAvailableFlight.allowableCargoWeight}</li>
                <li class="list-group-item ">${currentAvailableFlight.sourceAirport}</li>
                <li class="list-group-item ">${currentAvailableFlight.destinationAirport}</li>
                <li class="list-group-item ">${currentAvailableFlight.price}</li>
            </ul>`;
    }

    var htmlAvailableFlightsConainer =
        `<div class="card-body">
            <div class="card">
                <div class="card-header">
                    <h3>
                        ${jsonAirlineApiResult.airlineName}
                    </h3>                
                </div> 
                    ${htmlAvailableFlights}
            </div>
        </div>`;

    return htmlAvailableFlightsConainer;
}

7- Wiring up software components

Create the following configuration file. 

public static class ConfigureAirlineApiProviders
{
  public static IServiceCollection AddAirlineApiProviders
    (this IServiceCollection serviceCollection)
  {
    serviceCollection.AddSingleton(
    new List<IAirlineApiProvider>
    {
      new TurkishAirlineApiProvider(),
      new LufthansaAirlineApiProvider(),
      new QatarAirlineApiProvider(),
    });

    return serviceCollection;
  }
}

Then, To add your configuration file, call it in Program.cs somewhere before builder.Build().

builder.Services.AddAirlineApiProviders();