MessageHandler

Hosting in ASP.NET Web API

H

Introduction

You've reached the point where the booking capability of your new e-commerce system is coming to completion.

The only thing left is to expose it to the outside world for consumption.

In this lesson, you'll learn how to host your new capability in an asp.net web api and make it available through an HTTP interface.

By the end of this lesson, you will have:

  • Integrated the MessageHandler runtime properly in the asp.net hosting model.
  • Built a HTTP api to expose the PlacePurchaseOrder command, as well as the Booking read model.
  • Tested the api manually using the Swagger interface.

The code created up until now can be found in the following commit of the reference repository, and provides the starting point for this lesson.

Create a web api project

To host the API you'll create a new Web API project.

Right click the Solution MessageHandler.LearningPath in the Solution Explorer and choose Add > New Project.

Select the project template called ASP.NET Core Web API, tagged with C#. Click Next.

Enter project name OrderBooking.WebAPI. Click Next.

Select framework .NET 6.0 (Long-term support).

Choose Authentication type None

Select Configure for HTTPS, Use controllers and Enable OpenAPI support

Click Create

Visual Studio will now create a template, which includes too many files for your purpose.

Delete the excess file WeatherForecast.cs from the root of the project and delete WeatherForecastController.cs from the Controllers folder.

Set the Web API as startup project

Right click on the OrderBooking.WebAPI project and select Set as Startup Project

Add the MessageHandler.Runtime package

Right click on Dependencies of the OrderBooking.WebAPI project in the Solution Explorer and choose Manage Nuget Packages.

In the Nuget Package Manager window, browse for MessageHandler.Runtime (include prerelease needs to be checked).

Select the latest version and click Install.

Click I Accept in the License Acceptance window.

Add the MessageHandler.EventSourcing.AzureTableStorage package

Right click on Dependencies of the OrderBooking.WebAPI project in the Solution Explorer and choose Manage Nuget Packages.

In the Nuget Package Manager window, browse for MessageHandler.EventSourcing.AzureTableStorage (include prerelease needs to be checked).

Select the latest version and click Install.

Click I Accept in the License Acceptance window.

Add the Microsoft.Extensions.Configuration.UserSecrets package

Right click on Dependencies of the OrderBooking.WebAPI project in the Solution Explorer and choose Manage Nuget Packages.

In the Nuget Package Manager window, browse for Microsoft.Extensions.Configuration.UserSecrets (include prerelease needs to be unchecked).

Select the latest stable version and click Install.

Click I Accept in the License Acceptance window.

Add a project reference to OrderBooking

Right click on Dependencies of the OrderBooking.WebAPI project in the Solution Explorer and choose Add Project Reference.

In the Reference Manager window, select OrderBooking. Click OK

Add a project reference to OrderBooking.Projections

Right click on Dependencies of the OrderBooking.WebAPI project in the Solution Explorer and choose Add Project Reference.

In the Reference Manager window, select OrderBooking.Projections. Click OK

Create configuration extension methods

Right click the OrderBooking.WebApi project in the Solution Explorer and choose Add > New Item.

In the Add New Item window select Class and name it MessageHandlerConfiguration.cs.

Mark the class with the static operator.

public static class MessageHandlerConfiguration
{

}

Extend IServiceCollection

Now you add a static extension method for IServiceCollection and call it AddHandlerRuntime.

In this method you create a new HandlerRuntimeConfiguration instance and pass in the services instance.

Doing this will tie the service collection used by asp.net together with the service collection used inside the MessageHandler runtimes.

For convenience reasons, you also assign a name to the runtime and return it from the extension method.

public static class MessageHandlerConfiguration
{
    public static HandlerRuntimeConfiguration AddHandlerRuntime(this IServiceCollection services)
    {
        var runtimeConfiguration = new HandlerRuntimeConfiguration(services);
        runtimeConfiguration.HandlerName("orderbooking");            

        return runtimeConfiguration;
    }
}

Extend HandlerRuntimeConfiguration for event sourcing

To setup the event sourcing runtime, you create another extension method.

This time one extending the HandlerRuntimeConfiguration.

You call it AddEventSource, and pass in an instance of IConfiguration.

The IConfiguration instance is used to resolve the connectionString from either User Secrets or an environment variable.

Using the connectionstring, and a wellknown table name, you create an instance of AzureTableStorageEventSource;

To tie it the handler runtime configuration to the event sourcing configuration, you create an instance of EventsourcingConfiguration and pass in the HandlerRuntimeConfiguration instance as a parameter to its constructor.

Doing so will transfer any global runtime level settings into the event sourcing runtime, including the service collection which originally came from asp.net.

Then you configure the event source, by calling UseEventSource.

You enable projections by calling EnableProjections and passing in the type of the BookingProjection.

Finally you register the actual runtime in the service collection by calling RegisterEventsourcingRuntime.

The code now looks like this:

public static HandlerRuntimeConfiguration AddEventSource(this HandlerRuntimeConfiguration runtimeConfiguration, IConfiguration configuration)
{
    var connectionString = configuration.GetValue<string>("TableStorageConnectionString")
                                ?? throw new Exception("No 'TableStorageConnectionString' was provided. Use User Secrets or specify via environment variable.");

    var eventSource = new AzureTableStorageEventSource(connectionString, table: nameof(OrderBooking));

    var eventsourcingConfiguration = new EventsourcingConfiguration(runtimeConfiguration);    
    eventsourcingConfiguration.UseEventSource(eventSource);
    
    eventsourcingConfiguration.EnableProjections(typeof(BookingProjection));
    
    eventsourcingConfiguration.RegisterEventsourcingRuntime();

    return runtimeConfiguration;
}

Configure user secrets

Note that the user secrets, which were initially set for the integration tests, do not transfer to the WebApi project, they must be set again.

You set them by right-clicking on the OrderBooking.WebApi project in the Solution Explorer and choosing Manage User Secrets.

Visual Studio will now open your personal user secrets file associated to the Web API project. This file also resides physically outside of the solution folder, so that you don't accidentally check it into source control.

Modify the json content of the secrets file to include a TableStorageConnectionString property that has the connection string as a value.

{
  "TableStorageConnectionString": "YourConnectionStringGoesHere"
}

Startup sequence

To ensure all these configuration methods get called at the right time during the startup sequence you make the following modifications in Program.cs.

Right after the generated comment // Add services to the container you invoke builder.Services.AddHandlerRuntime().

On the returned HandlerRuntimeConfiguration instance, you invoke runtimeConfiguration.AddEventSource and pass in the builder.Configuration which holds the connection string.

var runtimeConfiguration = builder.Services.AddHandlerRuntime();
runtimeConfiguration.AddEventSource(builder.Configuration);

This will put the handler runtime and eventsourcing types in the container.

Finally you tell the runtime configuration to start using the actual handler runtime.

runtimeConfiguration.UseHandlerRuntime();

It's advised to do as late as possible in the configuration phase, so everything gets added to the internal service collection before a service provider is created from it.

But it must be done before the service provider of asp.net gets built, as the runtime does get register in the service collection itself as well.

Right before the line var app = builder.Build(); is the ideal spot.

The asp.net web api should now be properly configured.

Next up is creating the HTTP api's, which are represented by controllers in the asp.net framework

Add a command controller

Right click on Controllers folder of the OrderBooking.WebApi project in the Solution Explorer and choose Add > New Item.

In the Add New Item window select API Controller - Empty and name it CommandController.cs.

Modify the Route attribute to map on the api/orderbooking path.

And add a constructor which accepts an IEventSourcedRepository instance.

[ApiController]
[Route("api/orderbooking")]
public class CommandController : ControllerBase
{
    private IEventSourcedRepository repository;

    public CommandController(IEventSourcedRepository repository)
    {
        this.repository = repository;
    }
}

Define the command

Add a command class to expose the PlacePurchaseOrder command, which is currently a method on the OrderBooking class, as an HTTP request body.

Right click on the OrderBooking.WebApi project in the Solution Explorer and choose Add > New Item.

In the Add New Item window select Class and name it PlacePurchaseOrder.cs.

Add a property PurchaseOrder of type PurchaseOrder to it.

public class PlacePurchaseOrder
{
    public PurchaseOrder PurchaseOrder { get; set; }
}

Expose the command

Once the format of the request body is defined, you expose it as an HTTP Post API.

You do so by adding a method decorated with the HttpPost attribute to the controller.

The route definition of this attribute matches an input parameter called bookingId, and the command gets extracted from the body.

In the implementation, you get a booking instance for the bookingId by calling repository.Get<OrderBooking>(bookingId).

When the instance is resolved, you pass the purchase order instance from the command object into the command method.

Then you flush the repository, which will commit any pending events on the OrderBooking instance to the event store.

Finally you'll return an HTTP 200 response code by calling the Ok() method on the base type.

[ApiController]
[Route("api/orderbooking")]
public class CommandController : ControllerBase
{
    private IEventSourcedRepository repository;

    public CommandController(IEventSourcedRepository repository)
    {
        this.repository = repository;
    }

    [HttpPost("{bookingId}")]
    public async Task<IActionResult> Book([FromRoute] string bookingId, [FromBody] PlacePurchaseOrder command)
    {
        var booking = await repository.Get<OrderBooking>(bookingId);

        booking.PlacePurchaseOrder(command.PurchaseOrder);

        await repository.Flush();

        return Ok();
    }
}

Add a query controller

To expose the Booking read model, you'll add another controller.

Right click on Controllers folder of the OrderBooking.WebApi project in the Solution Explorer and choose Add > New Item.

In the Add New Item window select API Controller - Empty and name it QueryController.cs.

Modify the Route attribute on the controller to map to api/orderbooking (just like the command controller did).

Add a constructor which accepts an instance of IRestoreProjections.

[Route("api/orderbooking")]
[ApiController]
public class QueryController : ControllerBase
{
    private IRestoreProjections projection;

    public QueryController(IRestoreProjections projection)
    {
        this.projection = projection;   
    }
}

Expose the projection

To expose the projection as an HTTP GET api, add a method decorated with the HttpPost attribute.

The route definition of this attribute matches an input parameter called bookingId.

You obtain the projected read model by calling projection.Restore<Booking> and passing in the name of the table where the events are stored as well as the booking id.

The result of the projection gets returned as response body, by calling the Ok() method of the base class.

[Route("api/orderbooking")]
[ApiController]
public class QueryController : ControllerBase
{
    private IRestoreProjections projection;

    public QueryController(IRestoreProjections projection)
    {
        this.projection = projection;   
    }

    [HttpGet("{bookingId}")]
    public async Task<IActionResult> Get([FromRoute] string bookingId)
    {
        return Ok(await projection.Restore<Booking>(nameof(OrderBooking), bookingId));
    }
}

Test the api manually

You can now run the API locally by hitting the F5 button.

Use the Swagger pages to invoke

  • the /api/orderbooking/{bookingId} HTTP POST operation
  • the /api/orderbooking/{bookingId} HTTP GET operation

And enjoy the results of your work!

A note on automated testing

In all previous lesson, you added unit or integration tests to verify the behavior of your code.

Obviously you should test your controllers as well.

But to keep the length of this lesson in check, it was decided not to include tests for the controllers here.

Mainly because controllers require two types of tests.

  • On the one hand component tests should be used to verify the interaction between the controllers and mocked instances of the injected interfaces.
  • On the other hand asp.net specific integration tests are required to test routing of client requests to the controllers.

Close to a thousand lines of documentation text have been written on these two topics, on the Microsoft documentation site.

Including the majority of that content here would have exploded this lesson while there are no MessageHandler specific things to keep in mind for writing these types of tests.

You can find the respective documentation here:

Summary

In this lesson you learned how to host a capability in an asp.net web api.

The code created during this lesson can be found in the following commit of the reference repository.

What’s coming next?

In Part 7 of the Tutorial, you’ll extend the booking capability with an automated order confirmation flow.

Sign up to our newsletter to get notified about new content and releases

You can unsubscribe at any time by clicking the link in the footer of your emails. I use Mailchimp as my marketing platform. By clicking subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.