MessageHandler MH Sign Up

Host 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 04 persistence folder 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

Add MessageHandler

To setup MessageHandler, open program.cs and add a call to AddMessageHandler on the ServiceCollection of the builder.

builder.Services.AddMessageHandler("orderbooking", runtimeConfiguration =>
{
    
});

It needs to be called before the line var app = builder.Build(); is reached.

The MessageHandler runtime is now integrated into asp.net.

Configure event sourcing

To setup the event sourcing runtime, you call the EventSourcing extension method on the HandlerRuntimeConfiguration instance exposed by AddMessageHandler.

Resolve the connectionString from either User Secrets or an environment variable using builder.Configuration.

Using the Stream extension method, you configure streaming the OrderBooking stream from an AzureTableStorage event source.

The AzureTableStorageEventSource requires a connectionstring, and a table name, where the events on the stream will be persisted;

The events will be streamed into both an Aggregate called OrderBooking and a Projection called BookingProjection

The code now looks like this:

builder.Services.AddMessageHandler("orderbooking", runtimeConfiguration =>
{
    var connectionString = builder.Configuration.GetValue<string>("TableStorageConnectionString")
                                   ?? throw new Exception("No 'TableStorageConnectionString' was provided. Use User Secrets or specify via environment variable.");

    runtimeConfiguration.EventSourcing(source =>
    {
        source.Stream(nameof(OrderBooking),
            from => from.AzureTableStorage(connectionString, nameof(OrderBooking)),
            into =>
            {
                into.Aggregate<OrderBooking>();
                into.Projection<BookingProjection>();
            });
    });
});

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"
}

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<OrderBooking> instance.

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

    public CommandController(IEventSourcedRepository<OrderBooking> 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 and a Name property of type string to it.

public class PlacePurchaseOrder
{
    public string Name { get; set; }

    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(bookingId).

When the instance is resolved, you pass the purchase order instance and name value 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<OrderBooking> repository;

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

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

        booking.PlacePurchaseOrder(command.PurchaseOrder, command.Name);

        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<Booking> projection;

    public QueryController(IRestoreProjections<Booking> 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 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<Booking> projection;

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

    [HttpGet("{bookingId}")]
    public async Task<IActionResult> Get([FromRoute] string bookingId)
    {
        return Ok(await projection.Restore(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:

Expose the API for consumption from other domains

In future lessons you will be designing user interfaces on top of these API's, for respectively the buyer and the seller.

These user interfaces will be hosted on a different domain then your API and by default they won't be allowed to call your API due to a feature called Cross Origin Resource Sharing.

To allow any user interface to call into it, you configure a CORS policy that allows more origins, methods and headers then would default be the case.

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", policy =>
    {
        policy.AllowAnyMethod()
              .AllowAnyOrigin()
              .AllowAnyHeader();
    });
});

Before running the app, you instruct the aspnet runtime to use this new policy.

app.UseCors("AllowAll");

Now you're ready for the next step!

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 05 aspnet folder of the reference repository.

What’s coming next?

In Part 7 of the Tutorial, you'll design a task oriented user interface on top of the API just created.

TO PART 7

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.