Current version of CoffeeSpace uses Avalonia UI framework and was tested on MacOS
and Windows
. However, older versions have been using MAUI as the main framework.
In order to start use client you must start the API. After that start the client and register or login into an account.
Now, you should be able to create you first orders.
Be sure to provide correct address, otherwise you will get an error.
API | Libraries | Services |
---|---|---|
OrderingAPI | Shared | PaymentService |
ProductsAPI | Messages | ShipmentService |
IdentityAPI | Domain |
Order State | Description |
---|---|
Submitted |
Initial State Of Order |
StockConfirmed |
Product API Confirmed Order's Stock |
Paid |
Payment Service Captured Payment Response |
Shipped |
Shipment Service Completed |
To create messaging through MassTransit, all message models were moved to the Coffeespace.Messages project.
Lets start with services, that some microservices can use.
Service registration is implemented using the Scrutor library, which gives a great opportunity to add your services using reflection, as shown below.
public static IServiceCollection AddApplicationService<TInterface>(
this IServiceCollection services,
ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)
{
services.Scan(scan => scan
.FromAssemblyOf<TInterface>()
.AddClasses(classes =>
{
classes.AssignableTo<TInterface>()
.WithoutAttribute<Decorator>();
})
.AsImplementedInterfaces()
.WithLifetime(serviceLifetime));
return services;
}
Basically, when an interface is parsed to a parameter, it scans all classes from its assembly and implements each of the derived classes as an implementation.
builder.Services.AddApplicationService(typeof(ICacheService<>));
builder.Services.AddApplicationService<IOrderService>();
ICacheService
is coming from the Shared
library. There are generic services and settings, which can be used across microservices.
Currently, there is two approaches on cache invalidation. Ordering API
usesMediator's notifications to move cache invalidation logic into Notification Handler
.
public Task<IEnumerable<Order>> GetAllByBuyerIdAsync(Guid buyerId, CancellationToken cancellationToken)
{
return _cacheService.GetAllOrCreateAsync(CacheKeys.Order.GetAll(buyerId.ToString()), () =>
{
var orders = _orderService.GetAllByBuyerIdAsync(buyerId, cancellationToken);
return orders;
}, cancellationToken);
}
public async Task<bool> CreateAsync(Order order, CancellationToken cancellationToken)
{
bool isCreated = await _orderService.CreateAsync(order, cancellationToken);
if (isCreated)
{
await _publisher.Publish(new CreateOrderNotification
{
Id = order.Id,
BuyerId = order.BuyerId
}, cancellationToken).ConfigureAwait(false);
}
return isCreated;
}
Example is from Cached Order Service.
While Product API
using different approach. Instead of moving cache invalidation logic to a different file, it invalidates 'on-place'.
public Task<IEnumerable<Product>> GetAllProductsAsync(CancellationToken cancellationToken)
{
return _cacheService.GetAllOrCreateAsync(CacheKeys.Products.GetAll, () =>
{
var products = _productRepository.GetAllProductsAsync(cancellationToken);
return products;
}, cancellationToken);
}
public async Task<bool> CreateProductAsync(Product product, CancellationToken cancellationToken)
{
bool created = await _productRepository.CreateProductAsync(product, cancellationToken);
if (created)
{
await _cacheService.RemoveAsync(CacheKeys.Products.GetAll, cancellationToken);
}
return created;
}
Example is from Cached Product Repository.
API that works as an orchestrator for all messages. Also, exposes API endpoints for order and buyer management.
Here is the workflow of the Ordering API
after an order is submitted.
sequenceDiagram
participant Customer
participant Ordering
participant Product
participant Payment
participant Shipping
Customer->>Ordering: Submit Order
alt Stock Confirmed?
Ordering->>Product: Check Stock Availability
alt Product Available
Product-->>Ordering: Confirm Stock
Ordering->>Payment: Request Payment
Payment-->>Ordering: Payment Success
alt Payment Valid
Ordering->>Shipping: Ship Order
Shipping-->>Ordering: Shipping Complete
Ordering->>Customer: Order Complete
else Payment Not Valid
Ordering->>Customer: Cancel Order
end
else Stock Not Available
Ordering->>Customer: Cancel Order
end
else Stock Not Confirmed
Ordering->>Customer: Cancel Order
end
The Ordering API
can both publish and consume messages from other microservices. It uses the Masstransit StateMachine, which provides great opportunities to manage order state. There are five states: Submitted
, StockConfirmed
, Paid
, Shipped
, and Canceled
. When an order receives a new state, the Order State Machine changes its state in the database. When an order is submitted, it is saved in the Ordering DB, and then the Order State Machine sends all the necessary messages.
Furthermore, the Ordering API
has one message for the Identity API
when someone deletes a buyer, it sends a message to the Identity API
to remove the buyer from its database. It also has a consumer that creates a new buyer if someone completes registration.
All requests have a timeout value. If a request exceeds this timeout value, it will automatically be moved into a canceled state.
The Product API
provides CRUD operations for managing products, and it is responsible for verifying the stock of orders.If the order item's title is not found in the ProductApi database, the product cannot be fulfilled, and the order is moved to the cancel state.
The Identity API
allows users to log in or register as new users, using IdentityDbContext
for authentication and identity storage purposes.
sequenceDiagram
participant A as Client
participant B as API Gateway
participant C as Identity API
participant D as Authentication & Authorization Provider
participant E as Data Store
participant F as External Services
A->>B: Sends request
B->>C: Routes request
alt Request is for registration
C->>D: Registers user
D->>E: Stores user data
C->>F: Interacts with External Services
else Request is for login
C->>D: Authenticates user
end
alt Authentication/Registration succeeded
C-->>B: Responds with JWT Token
B-->>A: Responds with JWT Token
else Authentication/Registration failed
C-->>B: Responds with 404 status code
B-->>A: Responds with 404 status code
end
Once a user registers with the Identity API
, it sends a message to the message provider, and the Ordering API
acts as an external service and receives the message. This process results in the creation of a buyer after registration.
This service is designed to seamlessly handle the payment processing for orders received from an ordering API. It integrates with the PayPal API to facilitate secure and efficient transactions.
sequenceDiagram
participant OrderingAPI as Ordering API
participant PaymentService as Payment Service
participant PayPalAPI as PayPal API
participant Client as Client
OrderingAPI ->> PaymentService: Create Payment
PaymentService ->> PayPalAPI: Create Order Request
PayPalAPI -->> PaymentService: PayPal Response
PaymentService ->> Client: Send Payment Approval Link (SignalR)
Client ->> PaymentService: Proceed with Payment
PaymentService ->> PayPalAPI: Capture Payment
PayPalAPI -->> PaymentService: Payment Captured
PaymentService ->> OrderingAPI: Send Payment Captured Response
In order to create request more readable, I've implemented Builder
pattern for OrderRequest
.
With PayPalOrderRequestBuilder order creation process can be simplified using Payment Constants.
The Shipment Service
is a simple service that returns a successful result. However, in a real-world scenario, it would redirect to an external shipment service to handle the actual shipment.
By default, all logs with open telemetry are being sent to the Seq
. There is also Prometheus
endpoint.
In order to run this project, you need to configure the following environment variables in your secrets:
AZURE_CLIENT_ID
: The client ID of Azure application.AZURE_CLIENT_SECRET
: The client secret of Azure application.AZURE_TENANT_ID
: The ID of Azure tenant.AZURE_VAULT_NAME
: The name of Azure Key Vault.
These environment variables are necessary for authentication and accessing the Azure Key Vault in the project. Make sure to set the values of these variables appropriately in your secret management system or environment configuration.
To configure each of the microservices manually for local running, follow these steps:
- Clone Git repo
git clone https://github.com/Marsik424/CoffeeSpace.git
- Set up the required environment variables by creating a .env file or utilizing Kubernetes secrets. Refer to the "Environment Variables" section for more information on the specific variables needed.
To run the microservices in K8s, follow these steps:
-
Go to the root of the project and open terminal.
-
Create the deployment using
kubeclt
orhelm
kubectl apply -R -f ./deploy/kubeclt
helm install CoffeeSpace ./deploy/helm/CoffeeSpace
- To delete all pods, execute the following command in your terminal:
kubectl delete -R -f ./deploy/kubectl
helm uninstall CoffeeSpace
All requests should be made to the API Gateway, which will eventually redirect them to the appropriate controller.
You will find the Postman collection file named "CoffeeSpace.postman_collection" in the root of the project. This file contains the collection of API endpoints and associated requests that can be imported into Postman for testing and interacting with the project's APIs.
To import the Postman collection:
- Launch Postman.
- Click on the "Import" button located in the top-left corner.
- Select the option to "Import File" and choose the "CoffeeSpace.postman_collection" file from the project's root directory.
- Postman will import the collection, and you will be able to see the available requests and their associated details.
After importing the collection, you can explore the endpoints, customize the request parameters, and execute the requests against the project's APIs directly from Postman. This allows you to easily test and interact with the functionality provided by the project.
I was inspired by this project