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 8.0 (Long-term support).
  • Choose Authentication type None
  • Select Configure for HTTPS and Enable OpenAPI support, disable Use controllers
  • Click Create

Visual Studio will now create a template, which includes too much code for your purpose.

In the file Program.cs delete the excess class WeatherForecast delete the MapGet callback which returns these objects, as well as the summaries variable declaration.

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 api routes in the asp.net framework

Add a command api route

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 the item ApiRoutingConfiguration.cs.

And add a method UseOrderBooking which accepts an IEndpointRouteBuilder and a Func<IEndpointRouteBuilder, RouteGroupBuilder> instance.

This function allows callers of the UseOrderBooking function to set common values for all the api endpoints exposed.

Invoke this callback function, by passing in the builder, followed by returning the result.

public static class ApiRoutingConfiguration
{
    public static RouteGroupBuilder UseOrderBooking(
        this IEndpointRouteBuilder builder,
        Func<IEndpointRouteBuilder, RouteGroupBuilder> groupBuilder)
    {
        var orderBookings = groupBuilder(builder);       

        return orderBookings;
    }
}

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 calling MapPost on the route group builder.

The route definition of the first parameter 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). The repository is injected in the api method as an instance of IEventSourcedRepository<Booking>

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 201 response code by calling the Created() method on the aspnet Results factory.

public static class ApiRoutingConfiguration
{
    public static RouteGroupBuilder UseOrderBooking(
        this IEndpointRouteBuilder builder,
        Func<IEndpointRouteBuilder, RouteGroupBuilder> groupBuilder)
    {
        var orderBookings = groupBuilder(builder);

        orderBookings.MapPost("{bookingId}",
        async (IEventSourcedRepository<Booking> repo, string bookingId, PlacePurchaseOrder cmd) =>
        {
            var booking = await repo.Get(bookingId);
            booking.PlacePurchaseOrder(cmd.PurchaseOrder, cmd.Name);

            await repo.Flush();

            return Results.Created($"api/orderbooking/{bookingId}", booking.Id);
        })
        .Produces(StatusCodes.Status201Created);

        return orderBookings;
    }
}

Add a query route

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

In the ApiRoutingConfiguration add another MapGet call to map the route {bookingId} to a projection.

To obtain the projection, you inject an instance of IRestoreProjections in the route method and run the method Restore.

Finally you'll return an HTTP 200 response code by calling the Ok() method on the aspnet Results factory.

public static RouteGroupBuilder UseOrderBooking(
    this IEndpointRouteBuilder builder,
    Func<IEndpointRouteBuilder, RouteGroupBuilder> groupBuilder)
{
    var orderBookings = groupBuilder(builder);

    orderBookings.MapGet("{bookingId}", async(IRestoreProjections<Booking> projector, string bookingId) =>
        Results.Ok(await projector.Restore(nameof(Booking), bookingId))
    ).Produces(StatusCodes.Status200OK);

    return orderBookings;
}

Call UseOrderBooking

To set up these api routes you open Program.cs and call UseOrderBooking and use the group builder callback to set the base route and Open Api tag for both the routes.

app.UseOrderBooking((builder) => builder.MapGroup("api/orderbooking").WithTags("OrderBooking"));

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 api routes as well.

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

Mainly because api routes require two types of tests.

  • On the one hand component tests should be used to verify the interaction between the route implemenations 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 api routes.

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.