Course Content
Evolution to Microservices
Throughout our first chapter, we will gain a shared understanding of what a Microservice is, and cover some of the main benefits as well as drawbacks. Finally, we’ll explore some of the situations where we would want to choose a Microservices architecture compared to a monolithic application.
0/6
Designing Our First Microservice
Now that we have a shared understanding of microservices, including the benefits and drawbacks, let’s dive into creating our first microservice. We’ll start by setting up our environment, and covering the domain we’ll be working in throughout the rest of the book. From there, we’ll scaffold our first microservice and implement the business logic to allow other microservices to communicate with it.
0/7
Communication Between Microservices in .NET
So far, we have implemented a Basket microservice as part of our e-commerce application. However, this is only one component of an e-commerce application, so we need to introduce more functionality, which we achieve by creating new microservices. In this chapter, we will cover communication methods between microservices, introduce our second microservice, and implement communication between it and our Basket microservice.
0/7
Cross-Cutting Concerns
In our previous chapter, we introduced some duplicated code around the connections for RabbitMQ. Duplicated code isn’t the end of the world, but as developers, we must ask ourselves whether code can be re-used. Throughout this chapter, we are going to discuss duplication of code in the realm of microservices, as well as some common concerns that affect all microservices.
0/10
Data Ownership
Our e-commerce application is starting to take shape. We have a Basket and Order microservice, along with events that allow for asynchronous communication between the two. However, we currently use an in-memory store for both microservices. This is not reflective of a production microservices environment. Whenever we write applications, be it a monolith or a microservices-based architecture, data is a core component. Without data, most of our applications wouldn’t be very useful. So, throughout this chapter, we are going to understand data ownership in the context of microservices, exploring the replication of data to build more efficient and robust applications.
0/9
Extend Basket Microservice Functionality
Now that we’ve introduced a new service, the Product microservice, to our E-Commerce application, we can extend the functionality of our Basket microservice. We want to be able to display product price information in our baskets, so we need to consume the new ProductPriceUpdatedEvent we introduced in the previous chapter. At the same time, we can introduce a persistent data store for our basket microservice, and tick off another part of our overall architecture.
0/7
Testing Distributed Applications
At this stage, our E-Commerce application is starting to come together, with quite a few moving pieces. However, any time we introduce a new service or change some functionality, we need to manually run tests via Postman or curl, which isn’t very efficient. Furthermore, we cannot easily automate this type of testing, so whenever we get to a stage of continuously deploying our microservices, we’ll be slowed down by this manual testing. As developers, testing is something we should be very comfortable with doing and implementing. Throughout this chapter, we’ll briefly cover the types of tests we can write, focusing on our microservices, as well as implementing various levels of tests to ensure we can continuously add new microservices and functionality to our E-Commerce application.
0/4
Integration Testing With Order Microservice
So far, we’ve covered the base of the testing pyramid with unit tests in our Basket microservice. The next level we need to cover is integration testing, which we’ll pick up with our Order microservice. It is worth noting that we previously asked you to implement a data store for the Order microservice, so things may differ slightly. Of course, the source code is available with an SQL implementation, so feel free to follow along using that configuration. We’ve already covered the scope of integration testing in the previous chapter, so let’s waste no time getting into the code!
0/5
Application Observability
Throughout our journey of building an E-Commerce application using microservices, we’ve composed quite a complex system. So far, we’ve got 3 separate microservices, each with its own data store. Furthermore, we’ve got an external message broker (RabbitMQ) that allows us to communicate asynchronously between microservices. We’ve been able to test each microservice individually, both manually via Postman or curl and in an automated fashion with unit and integration tests. All of these processes are great to help us during local development and provide confidence whenever writing new features, but what about whenever our application is in production? Right now, if we deployed our application and allowed customers to use our E-Commerce platform, we’d have no insight into the performance of our application. We’d also have no idea how data flows through our application beyond what we have tested ourselves. This is where observability comes into play.
0/7
Monitoring Microservices With Metrics
In the previous chapter, we started considering how to monitor our microservices whenever deployed in a production environment. The first telemetry component we covered was tracing, which gives us contextual information for our microservices and external infrastructure. This information is useful when we need to dive deep into a problem, but it doesn’t provide an easy-to-understand overview of our service’s performance. This is where metrics come into play, which we’ve already gained an understanding of, so let’s waste no time implementing metrics in our Order microservice.
0/3
Building Resilient Microservices
So far, we’ve designed a system that provides us confidence when releasing new features thanks to testing. We’ve also gained insight into how our application performs when deployed with the help of OpenTelemetry tracing and metrics. With this last component, we’re likely to see recurring failures between microservices and our external infrastructure such as SQL or RabbitMQ.
0/6
Securing Microservices
Throughout the development of our microservices, every request we executed was unauthenticated. However, this needs to be revised for a production-level E-Commerce application. Although we can allow anonymous access to create baskets and add products to them, we cannot allow everyone access to create products or update product pricing. Therefore, we need a mechanism to secure certain endpoints, which we’ll achieve by introducing two new services to our system. Let’s start with an understanding of the different components of security.
0/6
Microservice Deployment
We’re now at a stage where we have a pretty sophisticated system, with many components, tests, and features. The next logical step for any application, microservice-based or not, is to tackle deployment. We need a solution to help us with microservice deployment complexities. But first, let’s briefly touch on the differences between monolithic and microservice deployments.
0/5
Microservices in .NET

With integration testing, we need to run an instance of our application to properly test the API. Furthermore, we want to be able to override configuration, such as our SQL database, so that we control the environment we test against and don’t pollute our actual database with test data.

Within the .NET ecosystem, we can achieve this with the WebApplicationFactory class from the Microsoft.AspNetCore.Mvc.Testing NuGet package, so let’s start with installing that in our test project:

dotnet add package Microsoft.AspNetCore.Mvc.Testing -v 8.0.6

With this installed, let’s understand the WebApplicationFactory class better.

Bootstrap Application With WebApplicationFactory

The WebApplicationFactory class allows us to bootstrap our application by taking an instance of the Program class, with methods to override specific parts of the startup process. To allow our test project to reference the Program class correctly from our Order.Service project, we need to add a line to the bottom of our Program class:

app.Run();

public partial class Program { }

Now we’re ready to bootstrap our application. Back in our test project, let’s create a new class:

using Microsoft.AspNetCore.Mvc.Testing;

namespace Order.Tests;

public class OrderWebApplicationFactory : WebApplicationFactory<Program>
{
}

We start by including a reference to the Microsoft.AspNetCore.Mvc.Testing namespace, which gives us access to the WebApplicationFactory class. Now, we have access to override a bunch of methods for different stages of our application startup. So what do we need to override? Our Order microservice has a database connection to the Order database running in Docker. But we don’t want to use this database for test purposes, so let’s override this. We can still use Docker to create our test databases so we can replicate our actual implementation, but we want to use a different database. In our test project, let’s add a new appsettings.Tests.json file:

{
  "ConnectionStrings": {
    "Default": "Server=host.docker.internal,1433;Database=Order_Tests;User Id=sa;Password=micR0S3rvice$;TrustServerCertificate=True"
  }
}

Although this looks very similar to our appsettings.json file in our Order microservice, it differs when we define the database name, Order_Tests. If we take a look at the EntityFrameworkExtensions class:

public static class EntityFrameworkExtensions
{
    public static void AddSqlServerDatastore(this IServiceCollection services, 
        IConfigurationManager configuration)
    {
        services.AddDbContext<OrderContext>(options =>
            options.UseSqlServer(configuration.GetConnectionString("Default")));

        services.AddScoped<IOrderStore, OrderContext>();
    }
}

We use the GetConnectionString() method, looking for the Default key. With the WebApplicationFactory class, we can provide our test configuration file as an override, so let’s do that:

protected override IHost CreateHost(IHostBuilder builder)
{
    builder.ConfigureHostConfiguration(configurationBuilder =>
    {
        configurationBuilder.AddConfiguration(new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.Tests.json")
            .Build());
    });

    return base.CreateHost(builder);
}

With a using statement added for Microsoft.Extensions.Hosting and Microsoft.Extensions.Configuration, we override the CreateHost() method, and call the ConfigureHostConfiguration() method. Instead of using the appsettings.json file from our Order microservice, we use the appsettings.Tests.json file from our test project. We have a further change to ensure our appsettings.Tests.json file gets copied to the output directory when running the tests. In our Order.Tests.csproj file, let’s add a new <ItemGroup> section:

<ItemGroup>
  <None Update="appsettings.Tests.json">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>

We specify the name of our JSON file and set the CopyToOutputDirectory element to Always. With this, we can move on to our next step.

Scaffold Test Database

We need a way to scaffold our test database and apply migrations so the correct database schema is present. Before we can reference our OrderContext class, which is marked as internal, we need to add a new section to our Order.Service.csproj file:

<ItemGroup>
  <InternalsVisibleTo Include="Order.Tests" />
</ItemGroup>

With this, we can override another method from the WebApplicationFactory class:

public class OrderWebApplicationFactory : WebApplicationFactory<Program>
{
    private OrderContext? _orderContext;

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(ApplyMigrations);
    }

    private void ApplyMigrations(IServiceCollection services)
    {
        var serviceProvider = services.BuildServiceProvider();
        var scope = serviceProvider.CreateScope();
        _orderContext = scope.ServiceProvider.GetRequiredService<OrderContext>();

        _orderContext.Database.Migrate();
    }
}

First, we include using statements for Microsoft.AspNetCore.Hosting, Microsoft.AspNetCore.TestHost, Microsoft.EntityFrameworkCore, Microsoft.Extensions.DependencyInjection and Order.Service.Infrastructure.Data.EntityFramework. With this, we override the ConfigureWebHost() method, and call the ConfigureTestServices() method. This method can be used to manipulate the service collection, adding or removing registrations as we need to correctly configure our test environment. We don’t need to add or remove any services, but we do need to apply database migrations, so we define a private method ApplyMigrations().

In this method, we build the service collection to retrieve an instance of the ServiceProvider class. With this, we call the CreateScope() method and retrieve an instance of our OrderContext class, assigning it to a private field and calling the Migrate() method to create the database and apply any migrations. This is great for setting up our database, but we need to remove it whenever we finish a test so we have a clean environment each time. For this, we can utilize the IAsyncLifetime interface from xUnit, which works similarly to the IDisposable interface from the .NET base library.

So, let’s inherit this interface and implement the required methods:

public class OrderWebApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private OrderContext? _orderContext;

    public Task InitializeAsync() => Task.CompletedTask;

    public new Task DisposeAsync()
    {
        if (_orderContext is not null)
        {
            return _orderContext.Database.EnsureDeletedAsync();
        }

        return Task.CompletedTask;
    }
}

We have two methods to implement. First, we don’t need to do any initialization in the InitializeAsync() method, so we return Task.CompletedTask. In the DisposeAsync() method, we check if our private field is not null, calling the EnsureDeletedAsync() method and returning a CompletedTask once again. It is worth noting we add the new keyword in the method declaration, otherwise, we hide the base class implementation. With this, our database is ready to go. We’re ready to get started with our tests.

0% Complete