Files
pos-system/docs/en/templates/dotnet-template.md
Ho Ngoc Hai c851fd97eb docs: Revise architecture and template documentation for GoodGo Platform
- Updated the architecture documentation to enhance clarity with detailed diagrams and descriptions for the GoodGo Microservices Platform.
- Revised the .NET and Node.js template documentation to reflect new naming conventions, project structures, and setup instructions for local development.
- Improved the guide documentation with verification checklists, troubleshooting steps, and real-world examples to assist developers in deploying and managing services effectively.
- Ensured bilingual support in documentation to enhance accessibility for a wider audience.
2026-01-14 12:38:41 +07:00

16 KiB

.NET 10 Microservice Template

Enterprise-grade .NET 10 microservice template following DDD, CQRS, and Clean Architecture patterns.

Overview

This template provides a production-ready structure for .NET microservices based on the eShopOnContainers reference architecture with:

  • Domain-Driven Design (DDD) - Aggregates, Entities, Value Objects, Domain Events
  • CQRS Pattern - Separate Commands (write) and Queries (read) with MediatR
  • Clean Architecture - Domain, Infrastructure, API layered separation
  • EF Core 10 - PostgreSQL with connection resilience
  • FluentValidation - Request validation
  • API Versioning - URL segment versioning
  • Health Checks - Kubernetes-ready probes
  • Structured Logging - Serilog with console and Seq

Prerequisites

Requirement Version
.NET SDK 10.0.101+
Docker 24.0+
PostgreSQL 15+ (or use Docker)

Quick Start

1. Create New Service

# Copy template to new service
cp -r services/_template_dot_net services/your-service-name-net
cd services/your-service-name-net

# Rename all occurrences of "MyService" to "YourService"
# Example: MyService → StorageService, UserService, PaymentService
find . -type f -name "*.cs" -exec sed -i '' 's/MyService/YourService/g' {} +
find . -type f -name "*.csproj" -exec sed -i '' 's/MyService/YourService/g' {} +
find . -type f -name "*.sln" -exec sed -i '' 's/MyService/YourService/g' {} +

Naming Convention:

  • Service folder: {service-name}-net (e.g., storage-service-net, iam-service-net)
  • Projects: {ServiceName}.API, {ServiceName}.Domain, {ServiceName}.Infrastructure
  • DbContext: {ServiceName}Context (e.g., StorageServiceContext)
  • Namespace: GoodGo.Services.{ServiceName} (e.g., GoodGo.Services.StorageService)

2. Configure Environment

cp .env.example .env
nano .env

3. Run with Docker

docker-compose up -d
docker-compose logs -f myservice-api

4. Run Locally

dotnet restore
dotnet build
dotnet run --project src/MyService.API

Project Structure

{service-name}-net/                 # Example: storage-service-net
├── src/
│   ├── {ServiceName}.API/          # Presentation Layer (Controllers, CQRS)
│   │   ├── Controllers/            # API endpoints
│   │   ├── Application/            # CQRS Implementation
│   │   │   ├── Commands/           # Write operations (MediatR)
│   │   │   ├── Queries/            # Read operations
│   │   │   ├── Behaviors/          # MediatR pipeline behaviors
│   │   │   └── Validations/        # FluentValidation validators
│   │   ├── Middlewares/            # Custom middlewares
│   │   └── Program.cs              # Application entry point
│   │
│   ├── {ServiceName}.Domain/       # Domain Layer (Pure business logic)
│   │   ├── AggregatesModel/        # Aggregate roots and entities
│   │   ├── Events/                 # Domain events
│   │   ├── Exceptions/             # Domain exceptions
│   │   └── SeedWork/               # Base classes (Entity, IAggregateRoot)
│   │
│   └── {ServiceName}.Infrastructure/ # Infrastructure Layer
│       ├── EntityConfigurations/   # EF Core configurations (Fluent API)
│       ├── Repositories/           # Repository implementations
│       ├── Providers/              # External service providers (if any)
│       ├── Services/               # Infrastructure services
│       ├── Migrations/             # EF Core migrations
│       └── {ServiceName}Context.cs # DbContext with Unit of Work
│
├── tests/
│   ├── {ServiceName}.UnitTests/    # Unit tests
│   └── {ServiceName}.FunctionalTests/ # Integration tests
│
├── docs/
│   ├── en/                         # English documentation
│   │   ├── README.md
│   │   └── ARCHITECTURE.md
│   └── vi/                         # Vietnamese documentation
│       ├── README.md
│       └── ARCHITECTURE.md
│
├── Dockerfile                      # Multi-stage Docker build
├── docker-compose.yml              # Local development setup
└── README.md                       # Service overview (bilingual)

Real-World Examples:

Architecture

graph TB
    subgraph "API Layer"
        C[Controllers]
        CMD[Commands]
        Q[Queries]
        B[Behaviors]
        V[Validations]
    end
    
    subgraph "Domain Layer"
        AR[Aggregate Roots]
        E[Entities]
        VO[Value Objects]
        DE[Domain Events]
    end
    
    subgraph "Infrastructure Layer"
        DB[(PostgreSQL)]
        R[Repositories]
        CTX[DbContext]
    end
    
    C --> CMD
    C --> Q
    CMD --> B --> V
    CMD --> AR
    Q --> R
    R --> CTX --> DB
    
    style C fill:#4a90d9,stroke:#2d5986,color:#fff
    style AR fill:#50c878,stroke:#2d8659,color:#fff
    style DB fill:#ff6b6b,stroke:#c0392b,color:#fff

Layer Responsibilities

1. Domain Layer

  • ZERO external dependencies (except MediatR.Contracts)
  • Contains only POCO classes
  • Implements DDD tactical patterns
Component Purpose
SeedWork Base classes: Entity, ValueObject, IAggregateRoot
AggregatesModel Aggregate roots with entities and value objects
Events Domain events for cross-aggregate communication
Exceptions Domain-specific exceptions

2. Infrastructure Layer

  • Database access (EF Core)
  • Repository implementations
  • External service integrations

3. API Layer

  • Controllers for HTTP handling
  • Commands for write operations
  • Queries for read operations
  • MediatR behaviors for cross-cutting concerns

CQRS Pattern

Commands (Write Operations)

public record CreateSampleCommand(string Name, string? Description) 
    : IRequest<CreateSampleCommandResult>;

public class CreateSampleCommandHandler : IRequestHandler<CreateSampleCommand, CreateSampleCommandResult>
{
    public async Task<CreateSampleCommandResult> Handle(CreateSampleCommand request, CancellationToken ct)
    {
        var sample = new Sample(request.Name, request.Description);
        _repository.Add(sample);
        await _repository.UnitOfWork.SaveEntitiesAsync(ct);
        return new CreateSampleCommandResult(sample.Id);
    }
}

MediatR Pipeline

Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response

API Endpoints

Method Endpoint Description
GET /api/v1/samples Get all samples
GET /api/v1/samples/{id} Get sample by ID
POST /api/v1/samples Create new sample
PUT /api/v1/samples/{id} Update sample
DELETE /api/v1/samples/{id} Delete sample
PATCH /api/v1/samples/{id}/status Change status

Health Endpoints

Endpoint Purpose
/health Full health status
/health/live Liveness probe
/health/ready Readiness probe

Environment Variables

Required

Variable Description Default
ASPNETCORE_ENVIRONMENT Environment name Development
DATABASE_URL Neon PostgreSQL connection -
ConnectionStrings__DefaultConnection Standard connection string format -

Optional

Variable Description Default
REDIS_URL Redis connection (cache) -
RabbitMQ__Host RabbitMQ hostname localhost
RabbitMQ__Port RabbitMQ port 5672
RabbitMQ__Username RabbitMQ username guest
RabbitMQ__Password RabbitMQ password guest

Inter-Service Communication

Variable Description Default
IamService__BaseUrl IAM Service URL http://iam-service-net:5001
Services__InternalApiKey Shared secret for service-to-service auth -

Multi-Provider Pattern (Example: Storage Service)

Variable Description Default
Storage__Provider Provider: minio or aliyun minio
Storage__DefaultBucket Default bucket name storage
Storage__MinIO__Endpoint MinIO endpoint localhost:9000
Storage__MinIO__AccessKey MinIO access key -
Storage__MinIO__SecretKey MinIO secret key -
Storage__AliyunOSS__Endpoint Aliyun OSS endpoint -
Storage__AliyunOSS__AccessKeyId Aliyun access key -
Storage__AliyunOSS__AccessKeySecret Aliyun secret key -

Testing

# Run all tests
dotnet test

# Run with coverage
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura

# Run specific test project
dotnet test tests/MyService.UnitTests

Deployment

Docker Build

docker build -t myservice:latest .
docker run -p 5000:8080 --env-file .env myservice:latest

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myservice-api
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: api
        image: myservice:latest
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8080
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080

Advanced Patterns

Multi-Provider Pattern

Use when: Service needs to support multiple external providers (storage, payment, SMS)

Example: Storage Service with MinIO and Aliyun OSS

// Domain - Interface
public interface IStorageProvider
{
    Task<string> UploadFileAsync(Stream fileStream, string fileName, CancellationToken ct);
    Task<Stream> DownloadFileAsync(string objectKey, CancellationToken ct);
    Task DeleteFileAsync(string objectKey, CancellationToken ct);
    Task<string> GeneratePresignedUrlAsync(string objectKey, int expiryMinutes, CancellationToken ct);
}

// Infrastructure - MinIO Implementation
public class MinioStorageProvider : IStorageProvider
{
    private readonly MinioClient _client;
    // Implementation...
}

// Infrastructure - Aliyun OSS Implementation
public class AliyunOssStorageProvider : IStorageProvider
{
    private readonly OssClient _client;
    // Implementation...
}

// API - Provider Factory
public class StorageProviderFactory
{
    public IStorageProvider CreateProvider(IConfiguration config)
    {
        var provider = config["Storage:Provider"];
        return provider?.ToLower() switch
        {
            "minio" => new MinioStorageProvider(config),
            "aliyun" => new AliyunOssStorageProvider(config),
            _ => throw new InvalidOperationException($"Unknown provider: {provider}")
        };
    }
}

File Reference: StorageService.Infrastructure/Providers/


Inter-Service Authentication Pattern

Use when: Service needs to validate JWT tokens with IAM Service

// Infrastructure - IAM Service Client
public class IamServiceClient
{
    private readonly HttpClient _httpClient;
    private readonly IConfiguration _config;

    public IamServiceClient(HttpClient httpClient, IConfiguration config)
    {
        _httpClient = httpClient;
        _config = config;
        _httpClient.BaseAddress = new Uri(_config["IamService:BaseUrl"]);
    }

    public async Task<UserInfo> ValidateTokenAsync(string token, CancellationToken ct)
    {
        var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/auth/validate")
        {
            Headers = { { "Authorization", $"Bearer {token}" } }
        };

        var response = await _httpClient.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();
        
        return await response.Content.ReadFromJsonAsync<UserInfo>(ct);
    }
}

// API - Middleware
public class JwtValidationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IamServiceClient _iamClient;

    public async Task InvokeAsync(HttpContext context)
    {
        var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
        
        if (!string.IsNullOrEmpty(token))
        {
            var userInfo = await _iamClient.ValidateTokenAsync(token, context.RequestAborted);
            context.Items["UserId"] = userInfo.UserId;
        }

        await _next(context);
    }
}

What's New in .NET 10

  • C# 14 language features
  • Improved Native AOT support
  • Better async/await performance
  • Enhanced JSON serialization (System.Text.Json)
  • 3-year LTS support (until November 2028)
  • Entity Framework Core 10 with performance improvements

Platform Integration

Traefik Routing

Add labels to docker-compose.yml:

services:
  your-service-net:
    build:
      context: ../..
      dockerfile: services/your-service-net/Dockerfile
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.your-service.rule=PathPrefix(`/api/v1/your-service`)"
      - "traefik.http.services.your-service.loadbalancer.server.port=8080"
      - "traefik.http.routers.your-service.middlewares=strip-prefix@docker"

Neon PostgreSQL Configuration

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=ep-xxx.neon.tech;Database=your_service;Username=xxx;Password=xxx;SSL Mode=Require;Trust Server Certificate=true"
  }
}

Best Practices:

  • Use connection pooling: Pooling=true;Maximum Pool Size=100
  • Enable SSL: SSL Mode=Require
  • Set command timeout: Command Timeout=30

RabbitMQ Integration (instead of Kafka)

// Domain Event
public record FileUploadedEvent(string FileId, string UserId, long FileSize) : IDomainEvent;

// Infrastructure - Event Publisher
public class RabbitMqEventPublisher : IEventPublisher
{
    private readonly IConnection _connection;
    private readonly IModel _channel;

    public async Task PublishAsync<T>(T @event, CancellationToken ct) where T : IDomainEvent
    {
        var message = JsonSerializer.Serialize(@event);
        var body = Encoding.UTF8.GetBytes(message);
        
        _channel.BasicPublish(
            exchange: "domain_events",
            routingKey: typeof(T).Name,
            body: body
        );
    }
}

Architecture

Skills

Guides

Additional Resources

Microsoft Official

Architecture References

Libraries

Tools


Last Updated: 2026-01-14
Template Version: 2.0
Maintained By: GoodGo Architecture Team