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.