Microshop.NET is a showcase for an e-commerce web shop with a MACH-based architecture. The system consists out of multiple microservices that communicate asynchronously.
The purpose of this application is to demo a complete setup of a MACH architecture using .NET and containerized applications. It will also contain different layers of testing and automated deployments, as well as automated resource provisioning.
Generally services are ASP.NET Core APIs. Even if they will not have endpoints, API projects are preferred due to their integration with testing utilities and their splendid Cloud support and containerization.
All communication between services happen asynchronously to prevent tight coupling and cascading outages of services. In order to achieve this, a RabbitMQ servicebus is used, leveraging the AMQP standard. Services will remain agnostic of each other and will only ever know about the data they request, creating clear boundaries between services and reducing coupling. For more information about this decision, please refer to the Architecture Decision Record (ADR)
Every microservice in this project has been based of the same Visual Studio solution template. This keeps the services consistent and allows for a clear structure. The template used is Microtemplate. Microtemplate has specifically been designed to make it easy to create a new .NET-based microservice on Clean Architecture.
More information about this choice can be found in an Architecture Decision Record (ADR)
Every service has the requirement of at least 70% code coverage for both lines and branches. This is verified upon push. The automatically triggered Github Action will fail if any of the relevant services does not adhere to the 70% minimum.
Additionally, every service has both Unit Tests and Integration Tests. These Test projects are written using XUnit as a testing framework and NSubstitute as a mocking framework.
In order to quickly view the test coverage locally, a Powershell script is available in the root (~
) folder of the repository: generateTestCoverageReport.ps1
. This file requires you to have the dotnet-coverage utility tool installed, as well as the ReportGenerator utility tool. To run the command, supply the name of a service as an argument, e.g. ./generateTestsCoverageReport.ps1 products
.
Unit Tests should test the smallest blocks of code possible. These tests can mock their dependencies.
Integration Tests should test the application's flow as close to real-life as possible. To facilitate this, the Integration Tests use Testcontainers .NET. These Testcontainers
allow the tests to spin up Docker containers and use them in their tests.
The Gateway is a reverse proxy that acts as an API aggregation layer to communicate with different downstream services. The Gateway is set-up using Microsoft's YARP
All inbound traffic to Microshop.NET will go through the Gateway, never directly to downstream services. Note that this does not apply to service-to-service communication, only for external communication (e.g. a front-end).
The Admin portal is a ASP.NET Razor Pages application that is responsible for interfacing with the API service and Authentication service to handle administrative actions (e.g. generate data or create a user).
The Authentication is an application written in Go as a backend to SuperTokens, a self-hosted open-source authentication service. To read more about this choice, please refer to the Architecture Decision Record (ADR).
This service is responsible for bootstrapping the authentication dashboard as well as all the authentication and authorization API endpoints that are being exposed through the YARP Gateway.
The API is responsible for any user interaction with the downstream services. For instance, this API is used to let the Admin portal interface with the downstream services using events.
- None
- GenerateProducts
The Products service is responsible for generating and storing product data. This only includes directly related product data, so things like prices and stock information is not part of this service.
- GenerateProducts
- ProductsGenerated
The Indexing service is responsible for receiving data and indexing it into the search index. The Indexing service listens for incoming messages. Once a message is received, the indexing service will store the data in an external search index.
- ProductsGenerated
- PricesGenerated
- None
The Price service is responsible for generating and storing price data. This includes price data only, so no other product assets - besides the product's identifier are available to this service.
The Price service listens to an incoming ProductsGenerated
message and uses the products inside that message to generate prices for. After the prices are generated, the price data is published as a PricesGenerated
message, including the price data, on the servicebus.
- ProductsGenerated
- PricesGenerated
The servicebus is a RabbitMQ application. The applications interface with the RabbitMQ servicebus through the MassTransit framework.
The search index is a MeiliSearch index. The applications interface using their .NET SDK.
The GenerateProducts message is sent out when the API endpoint to generate product data is called. This message doesn't contain any data directly but instead is used as a trigger for other services.
Example payload:
{
"messageId": "58010000-eace-e070-f3b8-08dba31489f0",
"requestId": null,
"correlationId": null,
"conversationId": "58010000-eace-e070-fb23-08dba31489f0",
"initiatorId": null,
"sourceAddress": "rabbitmq://localhost/NL524_API_bus_myyoyy8k35o8ywk1bdp4gr6rns?temporary=true",
"destinationAddress": "rabbitmq://localhost/Messaging.Messages:GenerateProducts",
"responseAddress": null,
"faultAddress": null,
"messageType": ["urn:message:Messaging.Messages:GenerateProducts"],
"message": {}
}
The ProductsGenerated message is sent out whenever product master data is generated. This message contains all products and their data.
Example payload:
{
"messageId": "07000000-ac12-0242-86c8-08db862bddb2",
"requestId": null,
"correlationId": null,
"conversationId": "07000000-ac12-0242-8ea9-08db862bddb2",
"initiatorId": null,
"sourceAddress": "rabbitmq://rabbitmq/76d617f6222b_API_bus_yhyyyyfcnebrfufqbdpack7wyh?temporary=true",
"destinationAddress": "rabbitmq://rabbitmq/Messaging.Messages:ProductsGenerated",
"responseAddress": null,
"faultAddress": null,
"messageType": ["urn:message:Messaging.Messages:ProductsGenerated"],
"message": {
"products": [
{
"code": "9132327080890",
"name": "Refined Steel Shoes",
"description": "The beautiful range of Apple Naturalé that has an exciting mix of natural ingredients. With the Goodness of 100% Natural Ingredients"
}
]
}
}
The PricesGenerated message is sent out whenever price data is generated. This message contains all prices per product.
Example payload:
{
"messageId": "07000000-ac12-0242-4d6b-08db862c2c2c",
"requestId": null,
"correlationId": null,
"conversationId": "07000000-ac12-0242-22d0-08db862c2c2b",
"initiatorId": null,
"sourceAddress": "rabbitmq://rabbitmq/pricing_products_generated",
"destinationAddress": "rabbitmq://rabbitmq/Messaging.Messages:PricesGenerated",
"responseAddress": null,
"faultAddress": null,
"messageType": ["urn:message:Messaging.Messages:PricesGenerated"],
"message": {
"prices": [
{
"productCode": "9132327080890",
"value": "595.32",
"currency": "EUR"
}
]
}
}
Every application or tool used for this project is runnable through a container. Every external application also has this prerequisite.
Microshop.NET's services are available as Docker images on the Docker Hub
The infrastructure is hosted in Azure through Azure Container Apps. Azure Container Apps is a managed solution for hosting microservices on a serverless platform. It's using Kubernetes and related technologies under the hood but they don't require intervention or managing.
Using Azure Container Apps over Kubernetes (or Azure Kubernetes Services) was a deliberate choice and the reasoning can be found in an Architecture Decision Record (ADR)
All infrastructure is setup as Infrastructure-as-Code by using Terraform.
The infrastructure is being provisioned automatically through the use of Github Actions.
Whenever a push is done to the main
branch, all necessary services are being built, tested and published to the Docker Hub.
These actions are done in parallel per service.
Whether or not a new version of the service is built and deployed depends on the changes files in the commit.
See the deploy.yaml workflow file for more information.
After these have all succeeded, the Azure Container Apps are provisioned with the latest Docker images through Terraform.
If you wish to run the platform locally, follow these steps:
- Clone the repository.
- Navigate to the root folder (
~
). - Copy
.env.example
and rename it to.env.
- Fill in the necessary values in the
.env
file - Run
docker-compose up
- See the Docker container overview and their logs for the results
The YARP gateway is now available with the following URLs:
http://auth.localhost
- routing to the Authentication servicehttp://api.localhost
- routing to the APIhttp://admin.localhost
- routing to the Admin portal web interfacehttp://index.localhost
- routing to the Meilisearch Index web interface
If you want to host this platform on your own Azure tenant, follow these steps:
Prerequisites:
- A Cloudflare account with a website to provision DNS records for
- An Azure account with a pre-configured storage account for storing Terraform state in
If these details are not provided, the applications won't fully be provisioned.
WIP A useful script to kickstart this locally is under development.
Do you have any questions or comments about this project? Feel free to open an issue. You can always contact me at alex_schouls@live.com.