12 Using IAsyncEnumerable<T> and yield return

This chapter covers

  • Using the generic Queue data structure
  • Using yield return and IAsyncEnumerable<T>
  • Creating views
  • Using private getters and setters with auto-properties
  • How structs differ from classes
  • Using checked and unchecked keywords

In the previous chapters, we examined the codebase we inherited and noted where we could make improvements. Then, we partially implemented our version of the codebase, adhering to FlyTomorrow’s OpenAPI specification. In chapters 10 and 11, we implemented the BookingService class and decided that there was no need for a CustomerService class. Figure 12.1 shows where we are in the scheme of the book.

Figure 12.1 In this chapter, we wrap up the services layer by implementing the AirportService and FlightService classes. By implementing those classes, we finish the service layer rewrite of the Flying Dutchman Airlines service.

If we look at which classes we need to implement to complete our service layer, an encouraging picture follows:

  • CustomerService (chapter 10)

  • BookingService (chapters 10 and 11)

  • AirportService (this chapter)

  • FlightService (this chapter)

We are halfway done with the service layer classes. In this chapter, we’ll wrap up the service layer implementation by writing code for the AirportService and FlightService classes. After this chapter, we are in an excellent spot to move on to our last architectural layer: the controller layer.

12.1 Do we need an AirportService class?

In section 10.2, we determined that if a service class would never get called by a controller class, we do not need to implement the service class. We also saw that you can determine whether you need a particular controller by checking the controller’s model name against the OpenAPI specification. If there is no need for a controller, there is no need for a service class. As humans, being creatures of habit, let’s repeat that process for the AirportService class.

The OpenAPI specification (as shown in figure 12.2) tells us we need to implement the following three endpoints:

  • GET /Flight

  • GET /Flight/{FlightNumber}

  • POST /Booking/{FlightNumber}

Figure 12.2 The OpenAPI specification from FlyTomorrow. We need to implement three endpoints: two GETs and one POST.

Do any of those endpoints have a controller related to the Airport model in their path? As shown in figure 12.3, I see two Flight controllers and one Booking controller, but no endpoints requiring an Airport controller. Well, that settles it then: we do not need to implement an AirportService class.

Figure 12.3 The Airport table has two incoming foreign key constraints. Both come from the Flight table and retrieve Airport.AirportID. These foreign key constraints can be used to retrieve information on a particular Airport.

On the other hand, we do have a use case to keep the AirportRepository class. If we look at the database schema of our deployed database, we see that the Airport table has the following two incoming foreign key constraints:

  • Flight.Origin to Airport.AirportID

  • Flight.Destination to Airport.AirportID

In section 12.2, we’ll dive deeper into those foreign key constraints and implement them. From our experiences in chapter 11, we know that we need to use the receiving table’s repository to trace down these foreign key constraints.

12.2 Implementing the FlightService class

So far, we implemented the BookingService and decided not to implement services for the Airport and Customer entities. In this section, we’ll finish the service layer by implementing the FlightService class. As with the prior sections, let’s ask ourselves, do we need to implement this class? We have two endpoints that require a Flight controller. Both the GET /Flight and the GET /Flight/{FlightNumber} endpoints make their requests against a Flight controller. Perfect—that means we need to implement a FlightService class. Both endpoints return data that already exists in the database and are fairly simple in their complexity. Let’s begin with the first: GET /Flight.

12.2.1 Getting information on a specific flight from the FlightRepository

In this section, we’ll implement the GET /Flight endpoint as discussed in section 12.1. FlyTomorrow uses the GET /Flight endpoint to query our service for all available flights. We don’t need to take into account (or validate) any special input parameters, but we have some foreign key restrictions to track down. We’ll also create a View class for Flight so we can return a combination of data stemming from the Flight and Airport tables.

But first, the starting point of all our endeavors: we need to create skeleton classes for both the FlightService and FlightServiceTests classes, as shown in figure 12.4. You know what to do.

Figure 12.4 To start the implementation of the FlightService, create two skeleton classes: FlightService and FlightServiceTests. These classes form the basis of our FlightService and FlightServiceTests implementations.

Now that we have the required classes in our projects, we can think about the things we need our method to do. Our method—let’s call it GetFlights—has to return data on every flight in the database. To do this, we should use an injected instance of the FlightRepository class. The FlightRepository class does not have a method to return all flights from the database, however, so we need to add that.

In FlightRepository, let’s add a virtual method called GetFlights. We don’t need to make the method asynchronous, because we do not query the actual database for the required information. Even though we want to get all the data from a specific table in the database, remember that Entity Framework Core stores a lot of metadata in memory. This brings us to one of the drawbacks of using an ORM: performance at scale. If you have a database that contains millions of records, Entity Framework Core still stores a lot of data locally. On the flip side, it means that we can query Entity Framework Core’s internal DbSet<Flight> and see all flights currently in the database.

The GetFlights method should return a collection of Flight, but which collection to use? We don’t need to access elements by some kind of key or index, so an Array, Dictionary, or List is unnecessary. Perhaps a simple Queue<Flight> would suffice.

A queue is a “first-in, first-out” (often abbreviated to FIFO) data structure. The first element to enter the queue is the first to come out, as shown in as shown in listing 12.1. Having a FIFO structure is helpful in our case because we can ensure we have an isomorphic relationship between how we represent the flights in our data structure and how they are represented in the database.

Listing 12.1 FlightRepository.GetFlights

public virtual Queue<Flight> GetFlights() {
  Queue<Flight> flights = new Queue<Flight>();    
  foreach (Flight flight in _context.Flight) {    
    flights.Enqueue(flight);                      
  }
 
  return flights;                                 
}

Creates a queue to store flights

Adds every flight to the queue (in order)

Returns the queue

  

EF Core FOREACH An alternative implementation to a foreach loop when dealing with an Entity Framework Core DbSet<T> collection is to use EF Core’s ForEachAsync method: _context.Flight.ForEachAsync(f => flights .Enqueue(f));. Depending on your readability preferences and asynchronous needs, this may be a good option for you.

That’s all for the FlightRepository.GetFlights method, but we need unit tests to back this up. I’ll walk you through the success case unit test, but I want you to think about some potential failure cases and write tests for them. If you find that you want to change the FlightRepository.GetFlights method for any reason, please do so!

If we look at the existing FlightRepositoryTest class’s TestInitialize method, we see that only one flight is added to the in-memory database before each test. In an ideal world, we would like to have at least two flights in the in-memory database so we can assert against the order in the returned Queue<Flight> as follows:

[TestInitialize]
public async Task TestInitialize() {
  DbContextOptions<FlyingDutchmanAirlinesContext> dbContextOptions = 
 new DbContextOptionsBuilder<FlyingDutchmanAirlinesContext>()
 .UseInMemoryDatabase("FlyingDutchman").Options;
  _context = new FlyingDutchmanAirlinesContext_Stub(dbContextOptions);
 
  Flight flight = new Flight {
    FlightNumber = 1,
    Origin = 1,
    Destination = 2
  };
 
 
  Flight flight2 = new Flight {
    FlightNumber = 10,
    Origin = 3,
    Destination = 4
  };
 
  _context.Flight.Add(flight);
  _context.Flight.Add(flight2);
  await _context.SaveChangesAsync();
 
  _repository = new FlightRepository(_context);
  Assert.IsNotNull(_repository);
}

  

YIELD RETURN keywords If you are okay with using a generic class instead of a concrete collection type such as Queue, List, or Dictionary, a neat concept to use is the yield return keywords.

When dealing with collections that implement the IEnumerable<T> interface, we can return the IEnumerable<T> type and not have to declare an actual collection inside a method. That may sound a bit confusing, so let me show you in the next code sample what the code in listing 12.1 looks like if we were to use this approach.

Listing 12.2 Using yield return and IEnumerable<Flight>

public virtual IEnumerable<Flight> GetFlights() {
  foreach (Flight flight in _context.Flight) { 
    yield return flight;
  }
}

The code in listing 12.2 does not explicitly declare a collection in which to store the Flight objects. Instead, by using the yield return keywords, we abstract away the collection initialization and let the compiler do its magic. (This is a simple example, and the code in listing 12.2 could also simply return the existing _context.Flight collection in this case.) This compiler magic consists of the compiler generating a class implementing the IEnumerable interface under the hood and returning that. The syntax suggests we are using the IEnumerable interface directly, but in fact, you are using the compiler-generated wrapper class.

You will also sometimes hear about the yield return keyword within the context of lazy evaluation. Lazy evaluation means that we delay all processing/iterating until it is absolutely necessary. The opposite of this is greedy evaluation, which does all processing up front and then lets us iterate over the results once it has all the information. By using the yield return keyword, we can come up with lazy logic that doesn’t operate on the returned results until they are returned. This is explained further in the discussion on IAsyncEnumerable<T> later in this section.

Now that we can get a queue of all the flights in the database by calling the FlightRepository.GetFlights method, we can start to assemble the view we want to return to the controller. By default, the Flight object has three properties: FlightNumber, OriginID, and DestinationID. This information is crucial to the customer, so we want to return it. However, simply returning IDs for the origin and destination airports is not very useful. If we look at the database schema, we see that we can use foreign keys to get more information about the origin and destination airports.

The Flight table has the following two outgoing foreign key constraints, as shown in figure 12.5:

  • Flight.Origin to Airport.AirportID

  • Flight.Destination to Airport.AirportID

Figure 12.5 The Flight table has two outgoing foreign key constraints. This figure does not show any other foreign key constraints (inbound or outbound). We’ll use these foreign key constraints to create a view in section 12.2.2.

If we were to trace down those foreign key constraints, we could get Airport information based on their IDs. The AirportRepository class has the following method that can help us here: GetAirportByID. The GetAirportByID method accepts an airport ID and returns the appropriate airport (if found in the database). We know that the Airport model has a property for its city name, so we can return the origin and destination city names along with the flight number to the controller. This amalgamation of two data sources forms the thinking behind our yet-to-be-created FlightView class.

12.2.2 Combining two data streams into a view

In section 10.1.1, we discussed views. We talked about how a view can give us a window into a model and combine data from various data sources. In this section, we’ll create the FlightView class and populate it with data from both the Flight model and the Airport model.

We can easily create the FlightView class. It is a public class with the following three public properties:

  • FlightNumber of type string

  • An Airport object containing OriginCity (type string) and Code (type string)

  • An Airport object containing DestinationCity (type string) and Code (type string)

The data for FlightNumber comes from the Flight model, whereas the data for the Airport object and the OriginCity and DestinationCity properties comes from the Airport model, as shown in figure 12.6. This information (for every flight in the database) is the data we ultimately return to FlyTomorrow when they query the GET /Flight endpoint.

Figure 12.6 The FlightView class combines the FlightNumber data from the Flight table with the city and code data from the Airport table. This allows us to present exactly the information we want from multiple sources to the end user.

To keep things organized, let’s create a new folder for the FlightView class called Views. The folder lives in the FlyingDutchmanAirlines project. Even though we don’t expect a lot of views in this project, it is always good to be somewhat organized.

  

Structs How do we deal with this Airport object we want to add inside the FlightView? Sure, we could add instances of Airport and ignore some fields, but that seems heavy handed to me. This is a prime chance to use the struct type. Many languages support either structs or classes, but C# does both. We can think of structs (within the context of C#) as light-weight classes that store simple information. Structs have less overhead than a full-fledged class, so when you just want to store a little information, use a struct.

Let’s add a struct called AirportInfo inside the FlightView.cs file (Note: Not inside the FlightView class.), as shown in the next code sample. The AirportInfo type should store information about the City and Code of a destination. We could use IATA instead of Code and reflect the database. However, because this is a view, we can change the name of things if we feel they better represent the data. IATA is correct, but Code is easier to understand for people unfamiliar with aviation terms.

public struct AirportInfo {
  public string City { get; set; }
  public string Code { get; set; }
 
  public AirportInfo((string city, string code) airport) {
    City = airport.city;
    Code = airport.code;
  }
}

The AirportInfo constructor accepts a tuple containing two fields: city and code. This brings us to the second cool thing about using a struct: when adding a constructor to a struct, you need to assign every property a value. In a class, you do not need to assign a value to all properties, but this does not fly with a struct! If we were to have an AirportInfo constructor that assigns a value only to the City property, the compiler would cry foul. By adding a constructor to a struct, you guarantee a complete setup of the respective struct. We can use this for future-proofing against a (well-intended) developer not completely initializing a struct as needed.

Coming back to the FlightView class, we can do something cool with the properties there as well. We can use private setters to make sure code only within the struct itself can change the value. We know that we don’t need to change the data once we retrieve it from the database, so let’s make the properties reflect that as much as possible. We don’t want just anybody to come in and try to set these properties, anyway.

ACCESS MODIFIERS AND AUTO-PROPERTIES When using auto-properties, we can have different access modifiers for setting and getting a property.

Let’s see what the FlightView class looks like with a split get and set system, as shown here:

public class FlightView {
  public string FlightNumber { get; private set; }
  public AirportInfo Origin { get; private set; }
  public AirportInfo Destination { get; private set; }
}

In this situation, only code that can access the properties through their private access modifiers can set new values, yet the get is still public. So where do we set these values? How about in a constructor? An alternative approach to using a private setter would be to make the properties readonly, because you can only set readonly properties in a constructor.

Let’s create a constructor that accesses the properties’ private setters and accepts arguments to set them to as follows:

public FlightView(string flightNumber, (string city, string code) origin, 
 (string city, string code) destination) {
  FlightNumber = flightNumber;
 
  Origin = new AirportInfo(origin);
  Destination = new AirportInfo(destination);
}

We should also do some input validation on the passed-in parameters. We can use the String.IsNullOrEmpty method to see whether any of the input arguments are a null pointer or an empty string. Alternatively, you can use the String.IsNullOrWhitespace, which checks whether the string is null, empty, or just consists of whitespace. If they are, we set them to appropriate values. We also use the ternary conditional operator, as shown next:

public class FlightView {
  public string FlightNumber { get; private set; }
  public AirportInfo Origin { get; private set; }
  public AirportInfo Destination { get; private set; }
 
  public FlightView(string flightNumber, 
 (string city, string code) origin,
 (string city, string code) destination) {
    FlightNumber = string.IsNullOrEmpty(flightNumber) ? 
      "No flight number found" : flightNumber;
 
    Origin = new AirportInfo(origin);
    Destination = new AirportInfo(destination);
  }
}
 
public struct AirportInfo {
  public string City { get; private set; }
  public string Code { get; private set; }
 
  public AirportInfo ((string city, string code) airport) {
    City = string.IsNullOrEmpty(airport.city) ? 
 "No city found" : airport.city;
    Code = string.IsNullOrEmpty(airport.code) ? 
 "No code found" : airport.code;
  }
}

NOTE Technically, we could make the FlightNumber, Origin, Destination, City, and Code properties to be “get” only and remove the private setter altogether. The compiler is smart enough to realize that we want to privately set the properties in the constructor. I like the verbosity of having the private setter, however. Your mileage may vary.

Of course, we should also create a test class and some unit tests to verify the FlightView constructor logic. Figure 12.7 shows the newly created files.

Figure 12.7 Two new files are created: FlightView in FlyingDutchmanAirlines/Views, and FlightViewTests in FlyingDutchmanAirlines_Tests/Views. Storing the classes in a separate Views folder helps us with organizing our codebase.

There are different trains of thought on testing constructors. Some people say that testing a constructor is simply testing the instantiation of a new object and, therefore, testing a language feature. Others say testing a constructor is useful because you never know what happens with the code. I fall into the latter camp. Especially when testing a constructor represents a minimal effort, having a test suite, such as the following code, to back up your code is the way to go:

[TestClass]
public class FlightViewTests {
  [TestMethod]
  public void Constructor_FlightView_Success() { 
    string flightNumber = "0";
    string originCity = "Amsterdam";
    string originCityCode = "AMS";
    string destinationCity = "Moscow";
    string destinationCityCode = "SVO";
 
    FlightView view = 
 new FlightView(flightNumber, (originCity, originCityCode), 
 (destinationCity, destinationCityCode));
    Assert.IsNotNull(view);
 
    Assert.AreEqual(view.FlightNumber, flightNumber);
    Assert.AreEqual(view.Origin.City, originCity);
    Assert.AreEqual(view.Origin.Code, originCityCode);
    Assert.AreEqual(view.Destination.City, destinationCity);
    Assert.AreEqual(view.Destination.Code, destinationCityCode);
    }
 
    [TestMethod]
    public void Constructor_FlightView_Success_FlightNumber_Null() {
      string originCity = "Athens";
      string originCityCode = "ATH";
      string destinationCity = "Dubai";
      string destinationCityCode = "DXB";
      FlightView view = 
 new FlightView(null, (originCity, originCityCode), 
 (destinationCity, destinationCityCode));
      Assert.IsNotNull(view);
 
      Assert.AreEqual(view.FlightNumber, "No flight number found");
      Assert.AreEqual(view.Origin.City, originCity);
      Assert.AreEqual(view.Destination.City, destinationCity);
    }
 
    [TestMethod]
    public void Constructor_AirportInfo_Success_City_EmptyString() {
      string destinationCity = string.Empty;
      string destinationCityCode = "SYD";
 
      AirportInfo airportInfo = 
 new AirportInfo((destinationCity, destinationCityCode));
      Assert.IsNotNull(airportInfo);
 
      Assert.AreEqual(airportInfo.City, "No city found");
      Assert.AreEqual(airportInfo.Code, destinationCityCode);
    }
 
    [TestMethod]
    public void Constructor_AirportInfo_Success_Code_EmptyString() {
      string destinationCity = "Ushuaia";
      string destinationCityCode = string.Empty;
 
      AirportInfo airportInfo = 
 new AirportInfo((destinationCity, destinationCityCode));
      Assert.IsNotNull(airportInfo);
 
      Assert.AreEqual(airportInfo.City, destinationCity);
      Assert.AreEqual(airportInfo.Code, "No code found");
    }
} 

We can now rest easy knowing that whatever may happen to the code in the FlightView class and the AirportInfo struct, we have tests to catch any changes that break existing functionality. We can now move on to populating the FlightView for every flight we get back from the FlightRepository. Of the five pieces of data we need for the FlightView (the flight number, the destination city, the destination code, the origin city, and the origin code), we know how to get the flight number. We just need to call the FlightRepository.GetFlights method. Of course, we need a GetFlights method in the FlightService first.

The GetFlights method returns an instance of FlightView wrapped in an IAsyncEnumerable. We discussed IEnumerable and how we can use the yield return keywords with it earlier in this section. The IAsyncEnumerable return type allows us to return an asynchronous collection implementing the IEnumerable interface. Because it is already asynchronous, we do not need to wrap it in a Task.

To start, let’s call the FlightRepository.GetFlights method and construct a FlightView for every flight returned from the database, as shown in the next listing. To do this, we also need to inject an instance of FlightRepository into the FlightService class. I leave that to you. You know what to do there. If you get stuck, see the provided source code. Note that the code in listing 12.3 does not compile, as explained after the listing.

Listing 12.3 FlightService.GetFlights asks for all flights in the database

public async Task<IAsyncEnumerable<FlightView>> GetFlights() {
  Queue<Flight> flights = _flightRepository.GetFlights();       
  foreach (Flight flight in flights) {                          
    FlightView view = 
 new FlightView(flight.FlightNumber.ToString(), ,);           
  }
}

Asks for all flights in the database

Loops over the returned flights

Creates a FlightView instance for every flight

Take a minute to read through listing 12.3 and see if you can spot why this code does not compile (besides not returning the correct type). Did you see it? The compiler throws an error because we did not provide enough arguments when instantiating the FlightView object for every flight. We don’t even have the correct information to give the view, though. The view wants us to pass in values for the flight number, origin city, and destination city. We passed in the flight number but neither of the cities. The closest thing we have to city names are the originAirportID and destinationAirportID properties on the returned Flight objects. We know how to take those and get the airport city names and codes: we call the AirportRepository.GetAirportByID method and take the Airport.City property (we also need an injected AirportRepository instance), as shown here:

public async IAsyncEnumerable<FlightView> GetFlights() {
     Queue<Flight> flights = _flightRepository.GetFlights();
  foreach (Flight flight in flights) {
  Airport originAirport = 
 await _airportRepository.GetAirportByID(flight.Origin);
  Airport destinationAirport = 
 await _airportRepository.GetAirportByID(flight.Destination);
 
  FlightView view = 
 new FlightView(flight.FlightNumber.ToString(), 
   (originAirport.City, originAirport.Code), 
   (destinationAirport.City, destinationAirport.Code)); 
  }
}

Now, here is where the real magic happens. Because we return a type of IAsyncEnumerable<FlightView>, we can use the yield return keywords to automatically add the created FlightView instances to a compiler-generated list as follows:

public async IAsyncEnumerable<FlightView> GetFlights() {
     Queue<Flight> flights = _flightRepository.GetFlights();
  foreach (Flight flight in flights) {
  Airport originAirport = 
 await _airportRepository.GetAirportByID(flight.Origin);
  Airport destinationAirport = 
 await _airportRepository.GetAirportByID(flight.Destination);
 
  yield return new FlightView(flight.FlightNumber.ToString(), 
 (originAirport.City, originAirport.Code), 
 (destinationAirport.City, destinationAirport.Code)); 
  }
}

We should also add a unit test in FlightServiceTests to verify we did a good job. Remember, we do not have to test the repository layers when testing service layer methods. Instead, we can use instances of Mock<FlightRepository> and Mock<AirportRepository> as the injected dependencies to the FlightService class. To mock the AirportRepository class, make the appropriate methods virtual and add a parameterless constructor, as shown in the next listing. We’ve done this a couple of times now, so I leave that to you.

Listing 12.4 Unit testing a method returning IAsyncEnumerable<T>

[TestMethod]
public async Task GetFlights_Success() {
  Flight flightInDatabase = new Flight {                           
    FlightNumber = 148,                                            
    Origin = 31,                                                   
    Destination = 92                                               
  };                                                               
 
  Queue<Flight> mockReturn = new Queue<Flight>(1);                 
  mockReturn.Enqueue(flightInDatabase);                            
 
  _mockFlightRepository.Setup(repository =>                        
 repository.GetFlights()).Returns(mockReturn);                   
 
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31)).ReturnsAsync(new Airport         
    {                                                              
      AirportId = 31,                                              
      City = "Mexico City",                                        
      Iata = "MEX"                                                 
    });                                                            
 
  _mockAirportRepository.Setup(repository =>                       
 repository.GetAirportByID(92)).ReturnsAsync(new Airport         
    {                                                              
      AirportId = 92,                                              
      City = "Ulaanbaataar",                                       
      Iata = "UBN"                                                 
    });                                                            
  FlightService service = new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);                                 
 
  await foreach (FlightView flightView in service.GetFlights()) {  
    Assert.IsNotNull(flightView);                                  
    Assert.AreEqual(flightView.FlightNumber, "148");               
    Assert.AreEqual(flightView.Origin.City, "Mexico City");        
    Assert.AreEqual(flightView.Origin.Code, "MEX");                
    Assert.AreEqual(flightView.Destination.City, "Ulaanbaatar");   
    Assert.AreEqual(flightView.Destination.Code, "UBN");           
  }
}

Sets up the FlightRepository.GetAllFlights mocked return

Sets up the AirportRepository.GetAirportByID mocked returns

Injects the mocked dependencies, and creates a new instance of FlightService

Receives flightViews as we construct them in the GetFlights method (one in this case)

Makes sure we received the correct flightView back

In listing 12.4, we get our first peek at how to use the returned IAsyncEnumerable type and can put together the puzzle of why it is such an outstanding feature. Instead of calling the FlightService.GetFlights method once, waiting for all the data to come back, and then operating on it, the IAsyncEnumerable type allows us to await on a foreach loop and operate on the returned data as it comes in.

12.2.3 Using the yield return keywords with try-catch code blocks

In section 12.2.2, we implemented the FlightService.GetFlights method. We did not, however, handle any exceptions coming out of the AirportRepository.GetAirportByID method. Unfortunately, we cannot simply add a try-catch code block and wrap the entire method in it because we cannot use the yield return keywords in such a code block. Not allowing yield statements in try-catch blocks has been a point of discussion within the C# language community for a while. Because adding yield statements to just try code blocks (without the catch) is allowed, and the only blocker for adding yield statement support to try-catch code blocks is added compiler complexity due to garbage collection difficulties, we may see this feature added in the future. The workaround is to add the calls to the AirportRepository.GetAirportByID method only in a try-catch block, so we can catch any outgoing exceptions, and then proceed as usual, as shown next:

public async IAsyncEnumerable<FlightView> GetFlights() {
     Queue<Flight> flights = _flightRepository.GetFlights();
  foreach (Flight flight in flights) {
    Airport originAirport;
    Airport destinationAirport;
 
    try {
      originAirport = 
 await _airportRepository.GetAirportByID(flight.Origin);
        destinationAirport = 
 await _airportRepository.GetAirportByID(flight.Destination);
    } catch (FlightNotFoundException) {
      throw new FlightNotFoundException();
    } catch (Exception) {
      throw new ArgumentException();
    }
 
    yield return new FlightView(flight.FlightNumber.ToString(), 
 (originAirport.City, originAirport.Code), 
 (destinationAirport.City, destinationAirport.Code)); 
  }
}

NOTE We have seen both IAsyncEnumerable and Task<IEnumerable> as return types. IAsyncEnumerable does not need to be wrapped in a Task<T> when returning from an asynchronous method, because IAsyncEnumerable is already asynchronous. Using a type with the generic Task<T> allows us to return a synchronous type from an asynchronous method.

This code allows us to catch any exception coming from the AirportRepository .GetAirportByID method. If the service class finds the repository method threw an exception of type FlightNotFoundException, it throws a new instance of FlightNotFoundException. If the code throws a different type of exception, the second catch block is entered and the code throws an ArgumentException. The controller calling the service layer handles this exception.

The last piece of our service layer implementations is to write a unit test that verifies our handling of the exception code we just wrote. Let’s look at that unit test shown next. It should be pretty straightforward.

Listing 12.5 Testing for exceptions in the FlightService

[TestMethod]
[ExpectedException(typeof(FlightNotFoundException))]          
public async Task GetFlights_Failure_RepositoryException() {
  Flight flightInDatabase = new Flight {                      
    FlightNumber = 148,                                       
    Origin = 31,                                              
    Destination = 92                                          
  };                                                          
    
  Queue<Flight> mockReturn = new Queue<Flight>(1);            
  mockReturn.Enqueue(flightInDatabase);                       
 
  _mockFlightRepository.Setup(repository =>                   
 repository.GetFlights()).Returns(mockReturn);              
 
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31))
 .ThrowsAsync(new FlightNotFoundException());               
 
  FlightService service = new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);                            
    
  await foreach (FlightView _ in service.GetFlights()) {      
    ;                                                         
  }
}
 
[TestMethod]
[ExpectedException(typeof(ArgumentException))]                
public async Task GetFlights_Failure_RegularException() {
  Flight flightInDatabase = new Flight {                      
    FlightNumber = 148,                                       
    Origin = 31,                                              
    Destination = 92                                          
  };                                                          
    
  Queue<Flight> mockReturn = new Queue<Flight>(1);            
  mockReturn.Enqueue(flightInDatabase);                       
 
  _mockFlightRepository.Setup(repository =>                   
 repository.GetFlights()).Returns(mockReturn);              
 
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31))
 .ThrowsAsync(new NullReferenceException());                
 
  FlightService service = new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);                            
    
  await foreach (FlightView _ in service.GetFlights()) {      
    ;                                                         
  }
}

Expects the executed logic in this test to throw an exception

Starts at the FlightRepository.GetAllFlights mocked return (same as listing 12.4)

Sets up the AirportRepository.GetAirportByID mocked returns (same as listing 12.4)

Creates a new instance of FlightService (same as listing 12.4)

Calls the GetFlights method, using the discard operator for the return assignment

Empty statement

Expects the executed logic in this test to throw an exception

Starts at the FlightRepository.GetAllFlights mocked return (same as listing 12.4)

Sets up the AirportRepository.GetAirportByID mocked returns (same as listing 12.4)

Creates a new instance of FlightService (same as listing 12.4)

Calls the GetFlights method, using the discard operator for the return assignment

Overall, the code in listing 12.5 should pose no challenges. It is good to point out that by using the discard operator in the foreach, we tell other developers that we do not need to use the returned values. In the same vein, inside the foreach loop, we added an empty statement (;). This does absolutely nothing but provide more readable code. By adding the empty statement, we say that having no logic inside the foreach loop was not a mistake.

We can do some further cleaning up: I am sure you noticed we have identical setup code for the Mock<Flight> and Mock<Airport> instances in both unit tests. Because this violates the DRY principle, we should refactor both unit tests and do this initialization in the TestInitialize method. This shortens our test methods considerably, as shown here:

[TestClass]
public class FlightServiceTests {
  private Mock<FlightRepository> _mockFlightRepository;
  private Mock<AirportRepository> _mockAirportRepository;
 
  [TestInitialize]
  public void Initialize() {
    _mockFlightRepository = new Mock<FlightRepository>();
    _mockAirportRepository = new Mock<AirportRepository>();
 
    Flight flightInDatabase = new Flight {
      FlightNumber = 148,
      Origin = 31,
      Destination = 92
    };
 
    Queue<Flight> mockReturn = new Queue<Flight>(1);
    mockReturn.Enqueue(flightInDatabase);
 
    _mockFlightRepository.Setup(repository => 
 repository.GetFlights()).Returns(mockReturn);
  }
 
  [TestMethod]
  public async Task GetFlights_Success() {
    _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31)).ReturnsAsync(new Airport
    {
      AirportId = 31,
      City = "Mexico City",
      Iata = "MEX"
    });
 
    _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(92)).ReturnsAsync(new Airport
    {
      AirportId = 92,
      City = "Ulaanbaatar",
      Iata = "UBN"
    });
 
    FlightService service = 
 new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);
 
    await foreach (FlightView flightView in service.GetFlights()) {
      Assert.IsNotNull(flightView);
      Assert.AreEqual(flightView.FlightNumber, "148");
      Assert.AreEqual(flightView.Origin.City, "Mexico City");
      Assert.AreEqual(flightView.Origin.Code, "MEX");
      Assert.AreEqual(flightView.Destination.City, "Ulaanbaatar");
      Assert.AreEqual(flightView.Destination.Code, "UBN");
    }
  }
 
  [TestMethod]
  [ExpectedException(typeof(FlightNotFoundException))]
  public async Task GetFlights_Failure_RepositoryException() {
    _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31)).ThrowsAsync(new Exception());
 
    FlightService service = 
 new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);
    await foreach (FlightView _ in service.GetFlights()) {
      ;
    }
  }
}

And that does it for the GetFlights method!

12.2.4 Implementing GetFlightByFlightNumber

All that is left is to add a similar method that retrieves only a single flight’s information when given a flight number. The patterns should be very familiar to you by now, as shown next:

public virtual async Task<FlightView> 
 GetFlightByFlightNumber(int flightNumber) {
  try {
    Flight flight = await 
 _flightRepository.GetFlightByFlightNumber(flightNumber);
    Airport originAirport = await 
 _airportRepository.GetAirportByID(flight.Origin);
    Airport destinationAirport = await 
 _airportRepository.GetAirportByID(flight.Destination);
 
    return new FlightView(flight.FlightNumber.ToString(),
     (originAirport.City, originAirport.Iata),
     (destinationAirport.City, destinationAirport.Iata));
  } catch (FlightNotFoundException) {
    throw new FlightNotFoundException();
  } catch (Exception) {
    throw new ArgumentException();
  }
}

We should also add some unit tests to verify we can get the correct flight from the database and handle the FlightNotFoundException and Exception error paths. To do this, we first have to add a new setup call to the TestInitalize method. Our mock currently does not return any data when we call FlightRepository.GetFlightByFlightNumber. Let’s fix that as follows:

[TestInitialize]
public void Initialize()  {
  ...
    
  _mockFlightRepository.Setup(repository => 
 repository.GetFlights()).Returns(mockReturn);
  _mockFlightRepository.Setup(repository => 
 repository.GetFlightByFlightNumber(148))
 .Returns(Task.FromResult(flightInDatabase));
}

When the mock’s GetFlightByFlightNumber returns data, we return the previously created flight instance. With that, we can add the GetFlightByFlightNumber_Success test case as follows:

[TestMethod]
public async Task GetFlightByFlightNumber_Success() {
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31)).ReturnsAsync(new Airport
      {
      AirportId = 31,
      City = "Mexico City",
      Iata = "MEX"
    });
 
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(92)).ReturnsAsync(new Airport
    {
      AirportId = 92,
      City = "Ulaanbaatar",
      Iata = "UBN"
    });
 
  FlightService service = new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);
  FlightView flightView = await service.GetFlightByFlightNumber(148);
 
  Assert.IsNotNull(flightView);
  Assert.AreEqual(flightView.FlightNumber, "148");
  Assert.AreEqual(flightView.Origin.City, "Mexico City");
  Assert.AreEqual(flightView.Origin.Code, "MEX");
  Assert.AreEqual(flightView.Destination.City, "Ulaanbaatar");
  Assert.AreEqual(flightView.Destination.Code, "UBN");
}

The unit test is pretty simple. We mimicked (read: copied and pasted) the airport setup code, so we added a flight to use in the in-memory database. Then we called FlightService.GetFlightByFlightNumber to check our service layer logic. Finally, we verified the return data. Now, when you saw the airport setup from the GetFlights_Success unit test in the code that we copied and pasted, alarm bells should have started to ring in your mind. Obviously, this repetition is a giant violation of the DRY principle, and we should refactor the test class to do this database setup in the TestInitialize method as follows:

[TestInitialize]
public void Initialize() {
  _mockFlightRepository = new Mock<FlightRepository>();
  _mockAirportRepository = new Mock<AirportRepository>();
    
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31)).ReturnsAsync(new Airport
    {
      AirportId = 31,
      City = "Mexico City",
      Iata = "MEX"
    });
 
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(92)).ReturnsAsync(new Airport
    {
      AirportId = 92,
      City = "Ulaanbaatar",
      Iata = "UBN"
    });
 
  ...
}

This shortens the GetFlights_Success and the GetFlightByFlightNumber_Success unit tests by a fair amount, as shown here:

[TestMethod]
public async Task GetFlights_Success() {
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31)).ReturnsAsync(new Airport
    {
      AirportId = 31,
      City = "Mexico City",
      Iata = "MEX"
    });
 
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(92)).ReturnsAsync(new Airport
    {
      AirportId = 92,
      City = "Ulaanbaatar",
      Iata = "UBN"
    });
 
  FlightService service = new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);
    
  await foreach (FlightView flightView in service.GetFlights()) {
    Assert.IsNotNull(flightView);
    Assert.AreEqual(flightView.FlightNumber, "148");
    Assert.AreEqual(flightView.Origin.City, "Mexico City");
    Assert.AreEqual(flightView.Origin.Code, "MEX");
    Assert.AreEqual(flightView.Destination.City, "Ulaanbaatar");
    Assert.AreEqual(flightView.Destination.Code, "UBN");
  }
}
 
[TestMethod]
public async Task GetFlightByFlightNumber_Success() {
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(31)).ReturnsAsync(new Airport
    {
      AirportId = 31,
      City = "Mexico City",
      Iata = "MEX"
    });
 
  _mockAirportRepository.Setup(repository => 
 repository.GetAirportByID(92)).ReturnsAsync(new Airport
    {
      AirportId = 92,
      City = "Ulaanbaatar",
      Iata = "UBN"
    });
 
  FlightService service = new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);
  FlightView flightView = await service.GetFlightByFlightNumber(148);
 
  Assert.IsNotNull(flightView);
  Assert.AreEqual(flightView.FlightNumber, "148");
  Assert.AreEqual(flightView.Origin.City, "Mexico City");
  Assert.AreEqual(flightView.Origin.Code, "MEX");
  Assert.AreEqual(flightView.Destination.City, "Ulaanbaatar");
  Assert.AreEqual(flightView.Destination.Code, "UBN");
}

Of course, all unit tests still pass. This gives us the confidence to know we broke nothing. Let’s add some failure case unit tests for the GetFlightByFlightNumber method and then we can call it a day.

Starting with the failure path where the service layer throws an exception of type FlightNotFoundException, we expect the service layer to throw another such exception, as shown next:

[TestMethod]
[ExpectedException(typeof(FlightNotFoundException))]
public async Task 
 GetFlightByFlightNumber_Failure_RepositoryException
 _FlightNotFoundException() {
  _mockFlightRepository.Setup(repository => 
 repository.GetFlightByFlightNumber(-1))
 .Throws(new FlightNotFoundException());
  FlightService service = new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);
 
  await service.GetFlightByFlightNumber(-1);
}

The GetFlightByFlightNumber_Failure_RepositoryException_Exception unit test sees our old friend the ExpectedException method attribute again. We are well aware of its usefulness by now and also use it in the unit test to check for the next (and last) exception path: the repository layer throws an exception of any type besides FlightNotFoundException. The FlightService.GetFlightByFlightNumber method catches the thrown exception and throws a new ArgumentException. Or so it says. Let’s see if it actually does:

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task 
 GetFlightByFlightNumber_Failure_RepositoryException_Exception() {
  _mockFlightRepository.Setup(repository => 
 repository.GetFlightByFlightNumber(-1))
 .Throws(new OverflowException());
  FlightService service = new FlightService(_mockFlightRepository.Object, 
 _mockAirportRepository.Object);
 
  await service.GetFlightByFlightNumber(-1);
}

The GetFlightByFlightNumber_Failure_RepositoryException_Exception unit test tells the Mock<FlightRepository> to throw an exception of type OverflowException when we call the FlightRepository.GetFlightByFlightNumber and pass in an input argument of -1. We could have used any exception class here because they are all derived from the base Exception class, and that is what the catch block in the method looks for. This is also the reason why the test is not more specific in its name regarding the exception type. We are testing the logic that happens if any type of Exception is thrown, not a specific one. Because Exception is the base class for all exceptions, we can test it with just that.

Overflows and underflows (checked and unchecked)

What do you get when you add two integers together? Let’s say 2147483647 and 1? You get a negative number. Similarly, what do you get when you subtract 1 from –2147483647? A positive number. This is what we call over- and underflow. When you go over the maximum value of a primitive type, or under the minimum value of a primitive type, you are greeted by a “wrapped around” value. Why is this, and how can we protect against this?

When there are not enough binary bits available in a type to represent your requested value, the type wraps around and flips (if it is an unsigned integer). This, depending on the context, is overflow and underflow. For example (albeit a simplistic one): an integer is a four-byte data type. This means we have 32 bits to play with (one byte contains eight bits, and 8 × 4 = 32). So, if we declare one variable that sets all 32 (31 if signed) bits to their “on” value, we have the maximum value we can represent in a 32-bit (or four-byte) type (in C#, we can use decimal, hexadecimal, or binary representations directly in our code; this is binary):

int maxVal = 0b11111111_11111111_11111111_1111111;
int oneVal = 0b00000000_00000000_00000000_0000001;
 
int overflow = maxVal + oneVal;

In C#, when using direct binary representation, you have to prefix your value with either 0b or 0B (with hexadecimal, use 0x or 0X). You can opt, as in the code snippet, to include underscores in the binary representation for readability. We prefix these values so the compiler knows how to treat the value. In this code snippet, we do the equivalent of adding 1 to the max 2147483647. So, what does the overflow variable resolve to? It resolves to –2147483648. And if we were to subtract a value of 1 from that, we would end up with a positive value: 2147483647. Often, when you know you are dealing with values over a particular type’s capacity, you use a different one. For example, you may use a long instead of an integer, or a BigInteger instead of a long. But what if you are, for whatever reason, restricted to a specific type, yet can see overflows and underflows as a realistic scenario?

  

BIGINTEGER is an immutable, nonprimitive “type” that grows with your data and is effectively capped only by your memory. A BigInteger acts as an integer but is actually a cleverly designed struct. Java developers may be familiar with BigInteger.

C# provides us with a keyword and compilation mode that can somewhat prevent unexpected overflows and underflows: checked. By default, C# compiles in unchecked mode. This means that the CLR does not throw any exceptions on arithmetic overflow and underflow. This is fine for most use cases, because we have some additional overhead with checking for this possibility, and it is not a common occurrence in a lot of programs. But, if we use checked mode, the CLR throws an exception when it detects under- or overflow. To use checked mode, we can either compile the entire codebase with these checks in place, by adding the -checked compiler option to the build instructions, or we can use the checked keyword.

To have the CLR throw an exception when it sees under- or overflow in a specific code block, we can wrap code in a checked block as follows:

checked {
  int maxVal = 0b_11111111_11111111_11111111_1111111;
  int oneVal = 0b_00000000_00000000_00000000_0000001;
 
  int overflow = maxVal + oneVal;
}

Now, when we add the maxVal and oneVal variables, the CLR throws an OverflowException! Consequently, if you compiled the entire codebase in checked mode, you can use the unchecked code block to tell the CLR not to throw any OverflowExceptions for that block’s scope.

And that does it for the service layer classes. I hope you learned some valuable things, and if not, the end of the book is in sight. In chapter 13, we’ll look at implementing the controller layer and integration testing.

Exercises

Exercise 12.1

True or false? For the endpoint GET /Band/Song, we need to implement the BandService class.

Exercise 12.2

True or false? For the endpoint POST /Inventory/SKU, we need to implement the SKUService class.

Exercise 12.3

What best describes the interactions with a Queue<T> data structure?

a. First-in, last-out (FILO)

b. First-in, first-out (FIFO)

c. Last-in, first-out (LIFO)

d. Last-in, last-out (LILO)

Exercise 12.4

If we use the yield return keywords inside a foreach loop embedded in a method with a return type of IEnumerable<T>, what could we expect to get back as a value from the method?

a. A collection implementing the IEnumerable interface containing all the data from the foreach loop.

b. A collection implementing the IEnumerable interface containing only the first piece of data to be processed in the foreach loop.

c. A collection not implementing the IEnumerable interface returning a reference to the original collection.

Exercise 12.5

Imagine a class called Food with a Boolean property IsFruit. This property has a public getter and a protected setter. Can the class Dragonfruit, which derives from the Food class, set the IsFruit value?

Exercise 12.6

What does this evaluate to? string.IsNullOrEmpty(string.empty);

Exercise 12.7

What does this evaluate to? string.IsNullOrWhitespace(" ");

Exercise 12.8

True or false? If you add a constructor to a struct, you can set only one property. The other properties must go unset.

Summary

  • To determine whether we need to implement a particular service, we can look at the required API endpoints. If there is no need for a particular model’s controller, we don’t need a service for that model, either. This saves us from implementing unnecessary code.

  • A Queue<T> is a “first-in, first-out” (FIFO) data structure. Queues are very helpful when we want to preserve order and deal with information as if we are dealing with a queue of people. The first one there is the first one processed, or, “the early bird gets the worm.”

  • We can use the yield return keywords to asynchronously return an IEnumerable<T> implementation if we iterate over some data. This can make our code more readable and concise.

  • A struct can be thought of as a “lightweight” class. We often use them to store small amounts of information and typically do not do any processing of data in a struct. Structs are a great way to signify to our fellow developers that this piece of code acts as a data storage device.

  • When adding a constructor to a struct, the compiler requires us to assign every property in the struct to a value. This is to prevent structs that are only partly initialized and stops us from accidentally forgetting to set values.

  • We can have different access modifiers for getters and setters in an auto-property. This allows us to make a property that can be publicly accessed but only set inside its respective class (private). Any combination of access modifiers is allowed. Because encapsulation is often our goal, by using these access modifiers, we can better control the encapsulation story.

  • We can only set readonly values at their declaration or in a constructor. Because we can set a readonly value only once, and declaring a field means the compiler automatically assigns a default value to its spot in memory, we need to set it at the earliest possible moment. A readonly field can greatly reduce the amount of data manipulation others can do on our code.

  • By using an IAsyncEnumerable<T> along with the yield return keywords, we can create code that asynchronously awaits on data and processes it as it receives the data. This is very helpful when dealing with external interactions such as database queries.

  • Overflows and underflows happened when we try to represent a value that needs more bits than a specific type has access to. When this happens, your variable values suddenly become incorrect, which can have unexpected side effects.

  • By default, C# code is compiled using unchecked mode. This means the CLR does not throw an OverflowException if it encounters an overflow or underflow. Similarly, checked mode means the CLR does throw such an exception.

  • We can use checked and unchecked code blocks to change the compilation mode on a per-code-block basis. This is helpful when wanting to control the Exception story.

  • In C#, we can represent integer values with decimal, hexadecimal, or binary representation. When using hexadecimal, we need to prefix our values with either 0x or 0X. With binary representation, use 0b or 0B. These different representations allow us to pick and choose what makes the most sense for our code’s readability.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.113.188