Queue
data structureyield return
and IAsyncEnumerable<T>
checked
and unchecked
keywordsIn 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.
If we look at which classes we need to implement to complete our service layer, an encouraging picture follows:
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.
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:
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.
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:
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.
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.
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.
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.
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)
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 |
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.
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:
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.
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:
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.
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.
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.
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.
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.
[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.
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.
[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
❼ 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!
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.
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.
True or false? For the endpoint GET /Band/Song
, we need to implement the BandService
class.
True or false? For the endpoint POST /Inventory/SKU
, we need to implement the SKUService
class.
What best describes the interactions with a Queue<T>
data structure?
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.
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?
What does this evaluate to? string.IsNullOrEmpty(string.empty
);
What does this evaluate to? string.IsNullOrWhitespace("
");
True or false? If you add a constructor to a struct, you can set only one property. The other properties must go unset.
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.
3.138.113.188