Skip to content

This is my PET project — ASP.NET Core web APIs built as microservices. I used DDD, Clean Architecture CQRS and event-driven communication principles.

Notifications You must be signed in to change notification settings

TeseySTD/asp-web-api-ecommerce-app

Repository files navigation

🚀 Microservices Web API Project

📋Overview

This is a PET project — a set of ASP.NET Core web APIs built as microservices.
The main goal of the project is to gain hands-on experience with modern architectural practices (Clean Architecture, DDD, CQRS, event-driven communication, SAGA) and libraries or frameworks (EF Core, MediatR, MassTransit, FluentValidation, etc.) . Each service follows DDD principles and is designed to be independently developed, tested, and deployed.

🛠️ Tech Stack

.NET ASP.NET Core Postgres Redis RabbitMQ Docker EF Core Marten DB MassTransit MediatR FluentValidation Mapster Carter YARP xUnit NSubstitute Testcontainers Respawn

📁 Project Structure

Server/
│   ├── src/
│   │   ├── Services/
│   │   │   ├── Basket/
│   │   │   │   └── Basket.API/
│   │   │   ├── Catalog/
│   │   │   │   ├── Catalog.API/
│   │   │   │   ├── Catalog.Application/
│   │   │   │   ├── Catalog.Core/
│   │   │   │   └── Catalog.Persistence/
│   │   │   ├── Ordering/
│   │   │   │   ├── Ordering.API/
│   │   │   │   ├── Ordering.Application/
│   │   │   │   ├── Ordering.Core/
│   │   │   │   └── Ordering.Persistence/
│   │   │   ├── Users/
│   │   │   │   ├── Users.API/
│   │   │   │   ├── Users.Application/
│   │   │   │   ├── Users.Core/
│   │   │   │   ├── Users.Infrastructure/
│   │   │   └── └── Users.Persistence/
│   │   ├── Shared/
│   │   │   ├── Shared.Core/
│   │   │   └── Shared.Messaging/
│   │   ├── APIGateways/
│   │   │   └── YarpGateway/
│   │   └── KeyManager/
│   ├── tests/
│   │   ├── Basket
│   │   │   ├── Basket.Tests.Unit/
│   │   │   └── Basket.Tests.Integration/
│   │   ├── Catalog
│   │   │   ├── Catalog.Tests.Unit/
│   │   │   └── Catalog.Tests.Integration/
│   │   ├── Ordering
│   │   │   ├── Ordering.Tests.Unit/
│   │   │   └── Ordering.Tests.Integration/
│   │   ├── Users
│   │   │   ├── Users.Tests.Unit/
│   │   │   └── Users.Tests.Integration/
│   │   ├── Shared
│   └── └── └── Shared.Core.Tests
├── docker-compose.yml
└── docker-compose.override.yml

Services

🛒 Basket Service

Basket is a lightweight microservice responsible for product cart actions. Stores minimal info about Product and Category in one ProductCart object as JSON, using MartenDb ORM. Reacts to product/category events in Catalog service. Because its responsibility is small, I decided to implement it as a single project but separate all contexts using the Clean Architecture principle. For this reason I also used MartenDb ORM because of the business logic simplicity and lightweight service conception.

📦 Catalog Service

Catalog is the largest microservice in the project responsible for Product and Category actions. Stores all info about Product and Category aggregates in DB, using EF Core ORM. Also acts as a CDN server by storing Product/Category images in the separate table Images as an aggregate with its own actions and endpoints. Reacts to Ordering ReserveProduct event.

📋 Ordering Service

Ordering is a microservice that handles the order lifecycle. Stores all info about Orders and minimal info about Product aggregates in DB, using EF Core ORM. It also controls all stages of ordering, using Event-Driven approach and SAGA pattern. Reacts to product events in Catalog service.

👥 Users Service

Users is a microservice responsible for all user and auth actions. Plays the role of the authentication server because it contains all necessary information about User aggregate. Stores all info about the user in the DB, using EF Core ORM. Performs authentication and authorization using JWT token with RSA signing and role-based hierarchy policies. Here is the hierarchy:

Admin->Seller->Default

Works with SMTP server and sends emails with verification links to users. Reacts to the CheckCustomer event in the Ordering service.

🔑 KeyManager

KeyManager is a simple console project, that has only one purpose - create public and private keys for Users service. Create keys in Server/secrets folder.

🌐 YARP Gateway

Gateway of all microservices. Has rate limiter and SwaggerUI for development. Each microservice has its swagger Open Api scheme, Yarp Gateway service unite all this schemes in one, format their routes to gateway and enable developer to use one SwaggerUI for all microservices.

🔧 Shared Libraries

Has two projects - Shared.Core and Shared.Messaging.

  • Shared.Messaging contains only events class, shared DTOs and all data objects that are used for communication between services. It also has extension for adding MassTransit into api.
  • Shared.Core contains shared logic and base classes. It has Fluent Validation abstractions, Result pattern implementation, and custom validators. Shared.Core describes all DDD abstractions like Entity<> and AggregateRoot<>, adds MediatR abstractions for CQRS implementation, configurations for logging and validation pipelines, pagination and envelope records and extension methods for shared authentication, authorization, swagger, etc.

🏗️ Architectural Principles

DDD

I made an accent in Rich Domain Model approach. My models have all their domain logic, like creating, updating, event dispathcing, etc. Value objects have their logic too. They all have factory method Create, that returns Result<TypeOfValueObject>. This method contains all validation logic , and if the validation process fails, it returns a Result object with all errors; if not, it returns success Result with ValueObject in the Value field. I decided to make ValueObject responsible for their own valid state becaus of:

  1. Single Responsablity Principle - Domain models take responsibility only for their state, not all ValueObjects that they consist of.
  2. Always-Valid Domain Model - Domain model consist of ValueObjects or entities, which are consist of ValueObjects too, so if all ValueObject are valid, the model is valid too.
  3. Infomation Expert Principle (GRASP) - Value object contains all necessary information about itself to validate its state.

This type of validation is perfectly connected with the FluentValdation library using a special custom validator that checks input using a factory method in a value object.

For example:

public class UpdateOrderCommandValidator : AbstractValidator<UpdateOrderCommand>
{
    public UpdateOrderCommandValidator()
    {
        RuleFor(x => x.CustomerId)
            .MustBeCreatedWith(CustomerId.Create); // This is the custom validator that takes
                                                   // ValueObjects factory method, and checks
                                                   // it result.
        
        RuleFor(x => x.OrderId)
            .MustBeCreatedWith(OrderId.Create);
        
        RuleFor(x => x.Value.Payment)
            .MustBeCreatedWith((p) => Payment.Create(
                cardName: p.cardName,
                cardNumber: p.cardNumber,
                paymentMethod: p.paymentMethod,
                expiration: p.expiration,
                cvv: p.cvv
            )
        );
    }
}

I also was inspired by this article1 and this video2 about DDD validation.

Clean Architecture

All services are built using this principle but with a feature for practical purposes. This feature is the EF Core dependencies between the Application and Persistence layers. Mostly when implementing the business logic (Application) layer with Clean Architecture, developers make an IRepository interface that describes all necessary methods to work with DB andPresistence layer implements it and places it in the dependency injection pool, so the Application layer is not dependent on the Persistence layer - which Clean Architecture needs.

I did the same in the Basket service, but in rest of the services I made a trick. Because EF Core DbSet and DbContext implement Repository and UnitOfWork patterns already, in the Application layer I would have to implement all interfaces for it, so a lot of boilerplate code had to be written, and also my repositories cannot contain necessary EF Core logic like Include and ThenInclude methods. I could write all that boilerplate, but I decided that it's not practical.

Instead, I add to the Application layer one package - Microsoft.EntityFrameworkCore.Relational and define IApplicationDbContext interfaces - where DbSets and SaveChangesAsync methods are defined. Persistence layer implements this interface and adds it to the dependency injection pool. This approach adds ORM dependency to the Application layer, so it is harder to test, but it doesn't add a database one - Application layer still can use any DB; ORM makes abstraction of the database itself.

CQRS

I implement CQRS using MediatR library for making requests between API and Application layers. I defined some interfaces for my commands and queries:

  1.  public interface ICommandBase;
     public interface ICommand: IRequest<Result>, ICommandBase;
     public interface ICommand<TResponse> : IRequest<Result<TResponse>>, ICommandBase
     where TResponse : notnull;
  2.  public interface ICommandHandler<in TCommand> 
         : IRequestHandler<TCommand, Result>
         where TCommand : ICommand {}
    
     public interface ICommandHandler<in TCommand, TResponse> 
         : IRequestHandler<TCommand, Result<TResponse>>
         where TCommand : ICommand<TResponse>
         where TResponse : notnull {}
  3.  public interface IQuery<TResponse> : IRequest<Result<TResponse>>
         where TResponse : notnull {}
  4.  public interface IQueryHandler<in TQuery, TResponse> :
         IRequestHandler<TQuery, Result<TResponse>>
         where TQuery : IQuery<TResponse>
         where TResponse : notnull {}

ICommandHandler accepts the ICommand interface, and if command has generic response - returns Result<TypeOfResponse> object otherwise - just Result.

IQueryHandler instead can return only Result<TResponse> because query always returns some type.

I also defined some behaviors in the MediatR pipeline: ValidationBehaviour and LoggingBehaviour.

LoggingBehaviour logs all commands and queries with their props, handle time, and result.

ValidationBehaviour takes all commands and queries validators from FluentValidation library, checks their results before execution, and if result is failure - returns it instead.

Event-Driven Architecture

I implement Event-Driven Architecture using RabbitMQ as a message broker and MassTransit library to interact with it.

Most of the time, events need only one action to handle them, so it's enough to have single event handlers in microservices.

To orchestrate much more complicated workflows, I used the SAGA Pattern and stored the saga state in the DB, using EF Core. In my API it was Ordering Workflow that had actions across many microservices.

🧪 Testing

For each service, I wrote two types of tests: Integration and Unit tests. All of them was written in Arrange Act Assert pattern and their names in MethodName_ScenarioUnderTest_ExpectedBehavior pattern. I used these patterns because they defined as best3 for .NET testing.

Unit Tests

I decided to use xUnit as main test framework and NSubtitute as mock library, I didn't use Moq because of security vulnerabilities4 that were, so I think it is not safe to trust them.

In all projects, the Core or Domain layer is tested using unit tests, but the Application layer only in the Basket service. This is because of ORM (for details, look in the Integration Tests section) and test unreliability.

Integration Tests

I decided to use Testcontainers in this type of tests, because:

  1. It is more reliable to test with real infrastructure elements.
  2. Almost any CI/CD has Docker and Docker is the lightweight way to use different services.
  3. No need to write any Dockerfiles - all containers are up from code.

All services have API layer integration tests, where endpoint responses are tested, and almost all (despite Basket service) have Application layer integration tests. The reason for it is the EF Core dependency in each of these services.

I think it is better to test EF Core using an integration test approach than a unit test because:

  1. It is hard to mock. Mocking DbContext is quite hard and requires making some test abstractions. And even if it's done, the developer must think about any raw sql logic or methods that directly work with SQL, like ExecuteDeleteAsync and ExecuteUpdateAsync.
  2. Any mocks or fake realizations do not guarantee5 that provider-specific translations will pass and LINQ methods will work correctly.
  3. In-Memory Providers also have limitations, like not being able to execute any raw sql.

All of it pushed me to make the decision to choose integration tests for the Application layer in services, where EF Core was. Other dependencies in such tests were mocked if it could be done.

For clearing datebase after each test I used Respawn libary - it generates sql script that cleans DB so this is lighter than manual DB clearing.

About

This is my PET project — ASP.NET Core web APIs built as microservices. I used DDD, Clean Architecture CQRS and event-driven communication principles.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published