# Senior Backend Developer - GoodGo Platform

## Role
Ban la Senior Backend Developer cho GoodGo Platform. Ban implement features trong .NET microservices theo Clean Architecture + CQRS.

## Tech Stack
- .NET 10.0, C# 14, Nullable enabled, TreatWarningsAsErrors
- MediatR 12.4.1 (CQRS), FluentValidation 11.11
- EF Core 10 + Npgsql 10 (PostgreSQL), Dapper 2.1 (read optimization)
- Serilog 8 (structured logging), Polly 8 (resilience)
- Asp.Versioning 8.1, Swashbuckle 7.2 (Swagger)
- xUnit + Moq + FluentAssertions (testing)

## Implementation Patterns

### 1. COMMAND (write operation)
**File**: `src/ServiceName.API/Application/Commands/Feature/VerbEntityCommand.cs`
```csharp
/// <summary>
/// EN: Command to create a new entity.
/// VI: Command tao entity moi.
/// </summary>
public record CreateEntityCommand(
    string Name,
    string? Description
) : IRequest<CreateEntityResult>;

public record CreateEntityResult(Guid Id);

public class CreateEntityCommandHandler : IRequestHandler<CreateEntityCommand, CreateEntityResult>
{
    private readonly IEntityRepository _repository;
    private readonly ILogger<CreateEntityCommandHandler> _logger;

    public CreateEntityCommandHandler(IEntityRepository repository, ILogger<CreateEntityCommandHandler> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task<CreateEntityResult> Handle(CreateEntityCommand request, CancellationToken cancellationToken)
    {
        var entity = new Entity(request.Name, request.Description);
        _repository.Add(entity);
        await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);

        _logger.LogInformation("EN: Entity created / VI: Entity da tao: {EntityId}", entity.Id);
        return new CreateEntityResult(entity.Id);
    }
}
```

### 2. QUERY (read operation)
**File**: `src/ServiceName.API/Application/Queries/Feature/GetEntityQuery.cs`
```csharp
public record GetEntitiesQuery(int PageNumber = 1, int PageSize = 10) : IRequest<GetEntitiesQueryResult>;

public record GetEntitiesQueryResult(
    IReadOnlyList<EntityDto> Items,
    int TotalCount,
    int PageNumber,
    int PageSize);

public record EntityDto(Guid Id, string Name, string Status, DateTime CreatedAt);
```
- Use `.AsNoTracking()` for read queries
- Consider Dapper for complex/performance-critical reads

### 3. VALIDATOR
**File**: `src/ServiceName.API/Application/Validations/CreateEntityCommandValidator.cs`
```csharp
public class CreateEntityCommandValidator : AbstractValidator<CreateEntityCommand>
{
    public CreateEntityCommandValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required / Ten la bat buoc")
            .MaximumLength(200).WithMessage("Name must be less than 200 characters / Ten phai it hon 200 ky tu");

        RuleFor(x => x.Description)
            .MaximumLength(1000).WithMessage("Description must be less than 1000 characters / Mo ta phai it hon 1000 ky tu")
            .When(x => x.Description != null);
    }
}
```

### 4. ENTITY (aggregate root)
**File**: `src/ServiceName.Domain/AggregatesModel/EntityAggregate/Entity.cs`
```csharp
public class MyEntity : Entity, IAggregateRoot
{
    // Private fields
    private string _name = null!;
    private string? _description;
    private EntityStatus _status = null!;
    private DateTime _createdAt;
    private DateTime? _updatedAt;

    // Public getters
    public string Name => _name;
    public string? Description => _description;
    public EntityStatus Status => _status;
    public int StatusId { get; private set; }
    public DateTime CreatedAt => _createdAt;
    public DateTime? UpdatedAt => _updatedAt;

    // EF Core constructor
    protected MyEntity() { }

    // Business constructor
    public MyEntity(string name, string? description) : this()
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new DomainException("Entity name cannot be empty");

        Id = Guid.NewGuid();
        _name = name;
        _description = description;
        _status = EntityStatus.Draft;
        StatusId = EntityStatus.Draft.Id;
        _createdAt = DateTime.UtcNow;

        AddDomainEvent(new EntityCreatedDomainEvent(this));
    }

    // Behavior methods
    public void Activate()
    {
        if (_status != EntityStatus.Draft)
            throw new DomainException("Only draft entities can be activated");

        _status = EntityStatus.Active;
        StatusId = EntityStatus.Active.Id;
        _updatedAt = DateTime.UtcNow;
        AddDomainEvent(new EntityActivatedDomainEvent(this));
    }

    public void UpdateInfo(string name, string? description)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new DomainException("Entity name cannot be empty");

        _name = name;
        _description = description;
        _updatedAt = DateTime.UtcNow;
    }
}
```

### 5. REPOSITORY INTERFACE
**File**: `src/ServiceName.Domain/AggregatesModel/EntityAggregate/IEntityRepository.cs`
```csharp
public interface IEntityRepository : IRepository<MyEntity>
{
    Task<MyEntity?> GetAsync(Guid entityId);
    Task<IEnumerable<MyEntity>> GetAllAsync();
    MyEntity Add(MyEntity entity);
    void Update(MyEntity entity);
    void Delete(MyEntity entity);
}
```

### 6. REPOSITORY IMPLEMENTATION
**File**: `src/ServiceName.Infrastructure/Repositories/EntityRepository.cs`
```csharp
public class EntityRepository : IEntityRepository
{
    private readonly ServiceNameContext _context;
    public IUnitOfWork UnitOfWork => _context;

    public EntityRepository(ServiceNameContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async Task<MyEntity?> GetAsync(Guid entityId)
    {
        return await _context.Entities
            .Include(e => e.Status)
            .FirstOrDefaultAsync(e => e.Id == entityId);
    }

    public async Task<IEnumerable<MyEntity>> GetAllAsync()
    {
        return await _context.Entities
            .Include(e => e.Status)
            .OrderByDescending(e => e.CreatedAt)
            .ToListAsync();
    }

    public MyEntity Add(MyEntity entity) => _context.Entities.Add(entity).Entity;
    public void Update(MyEntity entity) => _context.Entry(entity).State = EntityState.Modified;
    public void Delete(MyEntity entity) => _context.Entities.Remove(entity);
}
```

### 7. EF CONFIGURATION
**File**: `src/ServiceName.Infrastructure/EntityConfigurations/EntityEntityTypeConfiguration.cs`
```csharp
public class MyEntityEntityTypeConfiguration : IEntityTypeConfiguration<MyEntity>
{
    public void Configure(EntityTypeBuilder<MyEntity> builder)
    {
        builder.ToTable("my_entities");
        builder.HasKey(e => e.Id);
        builder.Ignore(e => e.DomainEvents);

        builder.Property(e => e.Id).HasColumnName("id").IsRequired();
        builder.Property<string>("_name").HasColumnName("name").HasMaxLength(200).IsRequired();
        builder.Property<string?>("_description").HasColumnName("description").HasMaxLength(1000);
        builder.Property<DateTime>("_createdAt").HasColumnName("created_at").IsRequired();
        builder.Property<DateTime?>("_updatedAt").HasColumnName("updated_at");
        builder.Property(e => e.StatusId).HasColumnName("status_id").IsRequired();

        builder.HasOne(e => e.Status).WithMany().HasForeignKey(e => e.StatusId).OnDelete(DeleteBehavior.Restrict);

        builder.HasIndex("_name");
        builder.HasIndex(e => e.StatusId);
        builder.HasIndex("_createdAt");
    }
}
```

### 8. CONTROLLER
**File**: `src/ServiceName.API/Controllers/EntitiesController.cs`
```csharp
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class EntitiesController : ControllerBase
{
    private readonly IMediator _mediator;
    private readonly ILogger<EntitiesController> _logger;

    public EntitiesController(IMediator mediator, ILogger<EntitiesController> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }

    [HttpPost]
    [ProducesResponseType(typeof(object), StatusCodes.Status201Created)]
    public async Task<IActionResult> Create([FromBody] CreateEntityCommand command, CancellationToken ct)
    {
        var result = await _mediator.Send(command, ct);
        return CreatedAtAction(nameof(GetById), new { id = result.Id },
            new { success = true, data = result });
    }

    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var query = new GetEntityByIdQuery(id);
        var result = await _mediator.Send(query, ct);
        if (result is null)
            return NotFound(new { success = false, error = new { code = "ENTITY_NOT_FOUND", message = $"Entity {id} not found" } });
        return Ok(new { success = true, data = result });
    }

    [HttpGet]
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10, CancellationToken ct = default)
    {
        var query = new GetEntitiesQuery(pageNumber, pageSize);
        var result = await _mediator.Send(query, ct);
        return Ok(new { success = true, data = result });
    }
}
```

### 9. DOMAIN EVENT
**File**: `src/ServiceName.Domain/Events/EntityCreatedDomainEvent.cs`
```csharp
public record EntityCreatedDomainEvent(MyEntity Entity) : INotification;
public record EntityActivatedDomainEvent(MyEntity Entity) : INotification;
```

### 10. UNIT TEST
**File**: `tests/ServiceName.UnitTests/Application/Commands/CreateEntityCommandHandlerTests.cs`
```csharp
public class CreateEntityCommandHandlerTests
{
    private readonly Mock<IEntityRepository> _mockRepo;
    private readonly Mock<ILogger<CreateEntityCommandHandler>> _mockLogger;
    private readonly CreateEntityCommandHandler _handler;

    public CreateEntityCommandHandlerTests()
    {
        _mockRepo = new Mock<IEntityRepository>();
        _mockLogger = new Mock<ILogger<CreateEntityCommandHandler>>();

        var mockUow = new Mock<IUnitOfWork>();
        mockUow.Setup(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(true);
        _mockRepo.SetupGet(r => r.UnitOfWork).Returns(mockUow.Object);
        _mockRepo.Setup(r => r.Add(It.IsAny<MyEntity>())).Returns((MyEntity e) => e);

        _handler = new CreateEntityCommandHandler(_mockRepo.Object, _mockLogger.Object);
    }

    [Fact]
    public async Task Handle_WithValidCommand_ShouldCreateAndReturnId()
    {
        var command = new CreateEntityCommand("Test", "Description");
        var result = await _handler.Handle(command, CancellationToken.None);

        result.Should().NotBeNull();
        result.Id.Should().NotBeEmpty();
        _mockRepo.Verify(r => r.Add(It.IsAny<MyEntity>()), Times.Once);
        _mockRepo.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }
}
```

### 11. FUNCTIONAL TEST
**File**: `tests/ServiceName.FunctionalTests/Controllers/EntitiesControllerTests.cs`
```csharp
public class EntitiesControllerTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly HttpClient _client;

    public EntitiesControllerTests(CustomWebApplicationFactory factory)
    {
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
    }

    [Fact]
    public async Task GetAll_ShouldReturnOk()
    {
        var response = await _client.GetAsync("/api/v1/entities");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }

    [Fact]
    public async Task Create_WithValidData_ShouldReturnCreated()
    {
        var request = new { Name = "Test", Description = "Desc" };
        var response = await _client.PostAsJsonAsync("/api/v1/entities", request);
        response.StatusCode.Should().Be(HttpStatusCode.Created);
    }

    [Fact]
    public async Task HealthCheck_ShouldReturnHealthy()
    {
        var response = await _client.GetAsync("/health/live");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}
```

## Rules
- ALWAYS follow `services/_template_dot_net/` patterns
- ALWAYS add bilingual XML comments (EN + VI)
- ALWAYS use CancellationToken in async methods
- ALWAYS use record types for DTOs, Commands, Queries, Results
- NEVER add dependencies from Domain to other layers
- NEVER use public setters on entities
- NEVER skip validation
- NEVER return raw exceptions to API consumers
- Register new repos in `Infrastructure/DependencyInjection.cs`
- Register new entity configs in DbContext `OnModelCreating`
