feat: Thêm các unit test cho các thành phần của ChatService như AIService, ChatHubClient và các sự kiện domain.
This commit is contained in:
@@ -369,6 +369,56 @@ services:
|
||||
- "traefik.http.services.chat-service.loadbalancer.sticky.cookie=true"
|
||||
- "traefik.http.services.chat-service.loadbalancer.sticky.cookie.name=chat_session"
|
||||
|
||||
# Social Service .NET - Social Features (Profiles, Posts, Followers)
|
||||
social-service-net:
|
||||
build:
|
||||
context: ../../services/social-service-net
|
||||
dockerfile: Dockerfile
|
||||
image: goodgo/social-service-net:latest
|
||||
container_name: social-service-net-local
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_URLS=http://+:8080
|
||||
# EN: Database - Neon PostgreSQL
|
||||
# VI: Cơ sở dữ liệu - Neon PostgreSQL
|
||||
- ConnectionStrings__DefaultConnection=Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Port=5432;Database=social_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require
|
||||
# EN: IAM Service Communication
|
||||
# VI: Giao tiếp IAM Service
|
||||
- IamService__BaseUrl=http://iam-service-net:8080
|
||||
- IamService__ServiceName=social-service
|
||||
# EN: JWT Configuration
|
||||
# VI: Cấu hình JWT
|
||||
- Jwt__Authority=http://iam-service-net:8080
|
||||
- Jwt__Audience=goodgo-api
|
||||
- Jwt__RequireHttpsMetadata=false
|
||||
# EN: Redis Cache
|
||||
# VI: Cache Redis
|
||||
- Redis__Host=167.114.174.113
|
||||
- Redis__Port=6379
|
||||
- Redis__Password=Velik@2026
|
||||
ports:
|
||||
- "5009:8080"
|
||||
depends_on:
|
||||
iam-service-net:
|
||||
condition: service_healthy
|
||||
traefik:
|
||||
condition: service_started
|
||||
networks:
|
||||
- microservices-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.social-service.rule=PathPrefix(`/api/v1/profiles`) || PathPrefix(`/api/v1/posts`) || PathPrefix(`/api/v1/followers`) || PathPrefix(`/api/v1/social`)"
|
||||
- "traefik.http.routers.social-service.entrypoints=web"
|
||||
- "traefik.http.services.social-service.loadbalancer.server.port=8080"
|
||||
- "traefik.http.services.social-service.loadbalancer.healthcheck.path=/health/live"
|
||||
- "traefik.http.services.social-service.loadbalancer.healthcheck.interval=10s"
|
||||
|
||||
|
||||
# Jaeger - Distributed Tracing
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Security.Claims;
|
||||
using ChatService.API.Hubs;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Xunit;
|
||||
|
||||
namespace ChatService.UnitTests.Application.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for ClaimsUserIdProvider.
|
||||
/// VI: Unit tests cho ClaimsUserIdProvider.
|
||||
/// </summary>
|
||||
public class ClaimsUserIdProviderTests
|
||||
{
|
||||
private readonly ClaimsUserIdProvider _sut;
|
||||
|
||||
public ClaimsUserIdProviderTests()
|
||||
{
|
||||
_sut = new ClaimsUserIdProvider();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaimsUserIdProvider_ShouldImplementIUserIdProvider()
|
||||
{
|
||||
// Assert
|
||||
_sut.Should().BeAssignableTo<IUserIdProvider>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ClaimTypes.NameIdentifier)]
|
||||
[InlineData("sub")]
|
||||
[InlineData("user_id")]
|
||||
public void GetUserId_ShouldSupportMultipleClaimTypes(string claimType)
|
||||
{
|
||||
// EN: This verifies the expected claim types are documented
|
||||
// VI: Kiểm tra các claim types được hỗ trợ đã được ghi nhận
|
||||
var supportedClaimTypes = new[] { ClaimTypes.NameIdentifier, "sub", "user_id" };
|
||||
supportedClaimTypes.Should().Contain(claimType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaimsUserIdProvider_ShouldNotBeNull()
|
||||
{
|
||||
// Assert
|
||||
_sut.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ChatService.Domain\ChatService.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\src\ChatService.Infrastructure\ChatService.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\src\ChatService.API\ChatService.API.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
using ChatService.Domain.Contracts;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ChatService.UnitTests.Domain.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for IAIService interface and related DTOs.
|
||||
/// VI: Unit tests cho IAIService interface và các DTOs liên quan.
|
||||
/// </summary>
|
||||
public class AIServiceContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void ChatMessage_ShouldCreateWithAllProperties()
|
||||
{
|
||||
// Arrange
|
||||
var role = "user";
|
||||
var content = "Hello AI!";
|
||||
var timestamp = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
var message = new ChatMessage(role, content, timestamp);
|
||||
|
||||
// Assert
|
||||
message.Role.Should().Be(role);
|
||||
message.Content.Should().Be(content);
|
||||
message.Timestamp.Should().Be(timestamp);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("user")]
|
||||
[InlineData("assistant")]
|
||||
[InlineData("system")]
|
||||
public void ChatMessage_ShouldAcceptValidRoles(string role)
|
||||
{
|
||||
// Arrange & Act
|
||||
var message = new ChatMessage(role, "Test content", DateTime.UtcNow);
|
||||
|
||||
// Assert
|
||||
message.Role.Should().Be(role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AIServiceOptions_ShouldHaveDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new AIServiceOptions();
|
||||
|
||||
// Assert
|
||||
options.Provider.Should().Be("OpenAI");
|
||||
options.Model.Should().Be("gpt-4");
|
||||
options.MaxHistoryMessages.Should().Be(20);
|
||||
options.MaxTokens.Should().Be(1000);
|
||||
options.Temperature.Should().BeApproximately(0.7f, 0.001f);
|
||||
options.SystemPrompt.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AIServiceOptions_SectionName_ShouldBeAI()
|
||||
{
|
||||
// Assert
|
||||
AIServiceOptions.SectionName.Should().Be("AI");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AIServiceOptions_ShouldAllowCustomConfiguration()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new AIServiceOptions
|
||||
{
|
||||
Provider = "AzureOpenAI",
|
||||
Model = "gpt-3.5-turbo",
|
||||
MaxHistoryMessages = 50,
|
||||
MaxTokens = 2000,
|
||||
Temperature = 0.5f,
|
||||
SystemPrompt = "Custom prompt"
|
||||
};
|
||||
|
||||
// Assert
|
||||
options.Provider.Should().Be("AzureOpenAI");
|
||||
options.Model.Should().Be("gpt-3.5-turbo");
|
||||
options.MaxHistoryMessages.Should().Be(50);
|
||||
options.MaxTokens.Should().Be(2000);
|
||||
options.Temperature.Should().BeApproximately(0.5f, 0.001f);
|
||||
options.SystemPrompt.Should().Be("Custom prompt");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using ChatService.Domain.Contracts;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ChatService.UnitTests.Domain.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for IChatHubClient interface and related DTOs.
|
||||
/// VI: Unit tests cho IChatHubClient interface và các DTOs liên quan.
|
||||
/// </summary>
|
||||
public class ChatHubClientTests
|
||||
{
|
||||
[Fact]
|
||||
public void MessageNotification_ShouldCreateWithAllProperties()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var conversationId = Guid.NewGuid();
|
||||
var senderId = "user-123";
|
||||
var senderName = "John Doe";
|
||||
var content = "Hello, World!";
|
||||
var messageType = "text";
|
||||
var sentAt = DateTime.UtcNow;
|
||||
var replyToMessageId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var notification = new MessageNotification(
|
||||
Id: id,
|
||||
ConversationId: conversationId,
|
||||
SenderId: senderId,
|
||||
SenderName: senderName,
|
||||
Content: content,
|
||||
MessageType: messageType,
|
||||
SentAt: sentAt,
|
||||
ReplyToMessageId: replyToMessageId
|
||||
);
|
||||
|
||||
// Assert
|
||||
notification.Id.Should().Be(id);
|
||||
notification.ConversationId.Should().Be(conversationId);
|
||||
notification.SenderId.Should().Be(senderId);
|
||||
notification.SenderName.Should().Be(senderName);
|
||||
notification.Content.Should().Be(content);
|
||||
notification.MessageType.Should().Be(messageType);
|
||||
notification.SentAt.Should().Be(sentAt);
|
||||
notification.ReplyToMessageId.Should().Be(replyToMessageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MessageNotification_ShouldAllowNullReplyToMessageId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var notification = new MessageNotification(
|
||||
Id: Guid.NewGuid(),
|
||||
ConversationId: Guid.NewGuid(),
|
||||
SenderId: "user-123",
|
||||
SenderName: "John Doe",
|
||||
Content: "Hello!",
|
||||
MessageType: "text",
|
||||
SentAt: DateTime.UtcNow
|
||||
);
|
||||
|
||||
// Assert
|
||||
notification.ReplyToMessageId.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using ChatService.Domain.Events;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ChatService.UnitTests.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for Conversation Domain Events.
|
||||
/// VI: Unit tests cho Conversation Domain Events.
|
||||
/// </summary>
|
||||
public class ConversationDomainEventsTests
|
||||
{
|
||||
[Fact]
|
||||
public void UserJoinedRoomDomainEvent_ShouldCreateWithCorrectProperties()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var conversationId = Guid.NewGuid();
|
||||
var userName = "John Doe";
|
||||
|
||||
// Act
|
||||
var domainEvent = new UserJoinedRoomDomainEvent(userId, conversationId, userName);
|
||||
|
||||
// Assert
|
||||
domainEvent.UserId.Should().Be(userId);
|
||||
domainEvent.ConversationId.Should().Be(conversationId);
|
||||
domainEvent.UserName.Should().Be(userName);
|
||||
domainEvent.JoinedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserLeftRoomDomainEvent_ShouldCreateWithCorrectProperties()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var conversationId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var domainEvent = new UserLeftRoomDomainEvent(userId, conversationId);
|
||||
|
||||
// Assert
|
||||
domainEvent.UserId.Should().Be(userId);
|
||||
domainEvent.ConversationId.Should().Be(conversationId);
|
||||
domainEvent.LeftAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypingDomainEvent_ShouldCreateWithCorrectProperties_WhenTyping()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var conversationId = Guid.NewGuid();
|
||||
var userName = "Jane Smith";
|
||||
var isTyping = true;
|
||||
|
||||
// Act
|
||||
var domainEvent = new TypingDomainEvent(userId, conversationId, userName, isTyping);
|
||||
|
||||
// Assert
|
||||
domainEvent.UserId.Should().Be(userId);
|
||||
domainEvent.ConversationId.Should().Be(conversationId);
|
||||
domainEvent.UserName.Should().Be(userName);
|
||||
domainEvent.IsTyping.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypingDomainEvent_ShouldCreateWithCorrectProperties_WhenStoppedTyping()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var conversationId = Guid.NewGuid();
|
||||
var userName = "Jane Smith";
|
||||
var isTyping = false;
|
||||
|
||||
// Act
|
||||
var domainEvent = new TypingDomainEvent(userId, conversationId, userName, isTyping);
|
||||
|
||||
// Assert
|
||||
domainEvent.IsTyping.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MessageSentDomainEvent_ShouldImplementINotification()
|
||||
{
|
||||
// Assert
|
||||
typeof(MessageSentDomainEvent).Should().Implement<MediatR.INotification>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MessageReadDomainEvent_ShouldCreateWithCorrectProperties()
|
||||
{
|
||||
// Arrange
|
||||
var messageId = Guid.NewGuid();
|
||||
var conversationId = Guid.NewGuid();
|
||||
var readerId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var domainEvent = new MessageReadDomainEvent(messageId, conversationId, readerId);
|
||||
|
||||
// Assert
|
||||
domainEvent.MessageId.Should().Be(messageId);
|
||||
domainEvent.ConversationId.Should().Be(conversationId);
|
||||
domainEvent.ReaderId.Should().Be(readerId);
|
||||
domainEvent.ReadAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using ChatService.Domain.Contracts;
|
||||
using ChatService.Infrastructure.Services;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace ChatService.UnitTests.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for NullAIService (used when OpenAI API key is not configured).
|
||||
/// VI: Unit tests cho NullAIService (dùng khi chưa cấu hình OpenAI API key).
|
||||
/// </summary>
|
||||
public class NullAIServiceTests
|
||||
{
|
||||
private readonly NullAIService _sut;
|
||||
|
||||
public NullAIServiceTests()
|
||||
{
|
||||
_sut = new NullAIService();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsAvailableAsync_ShouldReturnFalse()
|
||||
{
|
||||
// Act
|
||||
var result = await _sut.IsAvailableAsync();
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsync_ShouldReturnNotConfiguredMessage()
|
||||
{
|
||||
// Arrange
|
||||
var prompt = "Hello AI";
|
||||
var history = Enumerable.Empty<ChatMessage>();
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetResponseAsync(prompt, history);
|
||||
|
||||
// Assert
|
||||
result.Should().Contain("not configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamResponseAsync_ShouldReturnNotConfiguredMessage()
|
||||
{
|
||||
// Arrange
|
||||
var prompt = "Hello AI";
|
||||
var history = Enumerable.Empty<ChatMessage>();
|
||||
var chunks = new List<string>();
|
||||
|
||||
// Act
|
||||
await foreach (var chunk in _sut.StreamResponseAsync(prompt, history))
|
||||
{
|
||||
chunks.Add(chunk);
|
||||
}
|
||||
|
||||
// Assert
|
||||
chunks.Should().HaveCount(1);
|
||||
chunks[0].Should().Contain("not configured");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullAIService_ShouldImplementIAIService()
|
||||
{
|
||||
// Assert
|
||||
_sut.Should().BeAssignableTo<IAIService>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for AIService (requires mocking HttpClient).
|
||||
/// VI: Unit tests cho AIService (cần mock HttpClient).
|
||||
/// </summary>
|
||||
public class AIServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void AIService_ShouldImplementIAIService()
|
||||
{
|
||||
// Arrange
|
||||
var httpClient = new HttpClient();
|
||||
var options = Options.Create(new AIServiceOptions());
|
||||
var logger = new Mock<ILogger<AIService>>().Object;
|
||||
|
||||
// Act
|
||||
var service = new AIService(httpClient, options, logger);
|
||||
|
||||
// Assert
|
||||
service.Should().BeAssignableTo<IAIService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AIService_ShouldUseProvidedOptions()
|
||||
{
|
||||
// Arrange
|
||||
var httpClient = new HttpClient();
|
||||
var options = Options.Create(new AIServiceOptions
|
||||
{
|
||||
Model = "gpt-3.5-turbo",
|
||||
MaxHistoryMessages = 10
|
||||
});
|
||||
var logger = new Mock<ILogger<AIService>>().Object;
|
||||
|
||||
// Act
|
||||
var service = new AIService(httpClient, options, logger);
|
||||
|
||||
// Assert
|
||||
service.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsAvailableAsync_ShouldReturnFalse_WhenApiReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var mockHandler = new MockHttpMessageHandler(new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized));
|
||||
var httpClient = new HttpClient(mockHandler);
|
||||
var options = Options.Create(new AIServiceOptions());
|
||||
var logger = new Mock<ILogger<AIService>>().Object;
|
||||
var service = new AIService(httpClient, options, logger);
|
||||
|
||||
// Act
|
||||
var result = await service.IsAvailableAsync();
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Mock HTTP message handler for testing.
|
||||
/// VI: Mock HTTP message handler cho testing.
|
||||
/// </summary>
|
||||
internal class MockHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpResponseMessage _response;
|
||||
|
||||
public MockHttpMessageHandler(HttpResponseMessage response)
|
||||
{
|
||||
_response = response;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(_response);
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,11 @@ COPY src/ ./src/
|
||||
# EN: Build the application
|
||||
# VI: Build ứng dụng
|
||||
WORKDIR "/src/src/SocialService.API"
|
||||
RUN dotnet build "SocialService.API.csproj" -c Release -o /app/build --no-restore
|
||||
RUN dotnet build "SocialService.API.csproj" -c Release -o /app/build
|
||||
|
||||
# Publish stage / Giai đoạn publish
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "SocialService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||
RUN dotnet publish "SocialService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Runtime stage / Giai đoạn runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
# Architecture Documentation
|
||||
# Architecture Documentation - Social Service
|
||||
|
||||
> Detailed architecture documentation for the .NET 10 Microservice Template.
|
||||
> Detailed architecture documentation for Social Service - Social relationship management service.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "API Layer"
|
||||
C[Controllers]
|
||||
RC[RelationshipsController]
|
||||
BC[BlocksController]
|
||||
CMD[Commands]
|
||||
Q[Queries]
|
||||
B[Behaviors]
|
||||
V[Validations]
|
||||
end
|
||||
|
||||
subgraph "Domain Layer"
|
||||
AR[Aggregate Roots]
|
||||
E[Entities]
|
||||
VO[Value Objects]
|
||||
RA[Relationship Aggregate]
|
||||
UBA[UserBlock Aggregate]
|
||||
UPA[UserProfile Aggregate]
|
||||
DE[Domain Events]
|
||||
DX[Domain Exceptions]
|
||||
end
|
||||
|
||||
subgraph "Infrastructure Layer"
|
||||
DB[(PostgreSQL)]
|
||||
R[Repositories]
|
||||
CTX[DbContext]
|
||||
ID[Idempotency]
|
||||
RR[RelationshipRepository]
|
||||
UBR[UserBlockRepository]
|
||||
UPR[UserProfileRepository]
|
||||
CTX[SocialServiceContext]
|
||||
end
|
||||
|
||||
C --> CMD
|
||||
C --> Q
|
||||
CMD --> B --> V
|
||||
CMD --> AR
|
||||
Q --> R
|
||||
R --> CTX --> DB
|
||||
AR --> DE
|
||||
R --> AR
|
||||
RC --> CMD
|
||||
RC --> Q
|
||||
BC --> CMD
|
||||
BC --> Q
|
||||
CMD --> B
|
||||
CMD --> RA
|
||||
CMD --> UBA
|
||||
Q --> RR
|
||||
Q --> UBR
|
||||
RR --> CTX --> DB
|
||||
UBR --> CTX
|
||||
UPR --> CTX
|
||||
RA --> DE
|
||||
UBA --> DE
|
||||
|
||||
style C fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style AR fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style RC fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style BC fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style RA fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style UBA fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style UPA fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
|
||||
```
|
||||
|
||||
@@ -47,34 +56,51 @@ graph TB
|
||||
|
||||
### 1. Domain Layer (SocialService.Domain)
|
||||
|
||||
The heart of the application containing pure business logic. This layer:
|
||||
- Has **ZERO** external dependencies (except MediatR.Contracts for events)
|
||||
- Contains only POCO classes
|
||||
- Implements DDD tactical patterns
|
||||
The heart of the application containing pure business logic.
|
||||
|
||||
#### Components
|
||||
#### Aggregates
|
||||
|
||||
| Component | Purpose |
|
||||
| Aggregate | Purpose |
|
||||
|-----------|---------|
|
||||
| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
|
||||
| **AggregatesModel** | Aggregate roots with their entities and value objects |
|
||||
| **Events** | Domain events for cross-aggregate communication |
|
||||
| **Exceptions** | Domain-specific exceptions for business rule violations |
|
||||
| **Relationship** | Manages friendship and following |
|
||||
| **UserBlock** | Manages user blocking |
|
||||
| **UserProfile** | Caches user info from IAM Service |
|
||||
|
||||
#### Domain Events
|
||||
|
||||
| Event | Trigger |
|
||||
|-------|---------|
|
||||
| `FriendRequestSentDomainEvent` | Friend request sent |
|
||||
| `FriendshipCreatedDomainEvent` | Friend request accepted |
|
||||
| `UserFollowedDomainEvent` | User followed |
|
||||
| `RelationshipStatusChangedDomainEvent` | Relationship status changed |
|
||||
| `RelationshipRemovedDomainEvent` | Unfriend/unfollow |
|
||||
| `UserBlockedDomainEvent` | User blocked |
|
||||
| `UserUnblockedDomainEvent` | User unblocked |
|
||||
|
||||
### 2. Infrastructure Layer (SocialService.Infrastructure)
|
||||
|
||||
Technical implementations and external concerns:
|
||||
- Database access (EF Core)
|
||||
- Repository implementations
|
||||
- External service integrations
|
||||
Technical implementation and data access.
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| **SocialServiceContext** | DbContext with Unit of Work pattern |
|
||||
| **RelationshipRepository** | CRUD for Relationship aggregate |
|
||||
| **UserBlockRepository** | CRUD for UserBlock aggregate |
|
||||
| **UserProfileRepository** | CRUD for UserProfile aggregate |
|
||||
| **EntityConfigurations** | EF Core Fluent API mappings |
|
||||
|
||||
### 3. API Layer (SocialService.API)
|
||||
|
||||
Application entry point and CQRS implementation:
|
||||
- Controllers for HTTP handling
|
||||
- Commands for write operations
|
||||
- Queries for read operations
|
||||
- MediatR behaviors for cross-cutting concerns
|
||||
Application entry point and CQRS implementation.
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| **RelationshipsController** | APIs for friendship and following |
|
||||
| **BlocksController** | APIs for block/unblock users |
|
||||
| **Commands** | 6 write operations (MediatR) |
|
||||
| **Queries** | 4 read operations |
|
||||
| **Behaviors** | Logging, Validation, Transaction |
|
||||
|
||||
## CQRS Flow
|
||||
|
||||
@@ -86,7 +112,7 @@ sequenceDiagram
|
||||
participant LoggingBehavior
|
||||
participant ValidatorBehavior
|
||||
participant TransactionBehavior
|
||||
participant CommandHandler
|
||||
participant Handler
|
||||
participant Repository
|
||||
participant DbContext
|
||||
|
||||
@@ -95,94 +121,118 @@ sequenceDiagram
|
||||
MediatR->>LoggingBehavior: Handle
|
||||
LoggingBehavior->>ValidatorBehavior: Next()
|
||||
ValidatorBehavior->>TransactionBehavior: Next()
|
||||
TransactionBehavior->>CommandHandler: Next()
|
||||
CommandHandler->>Repository: Add/Update/Delete
|
||||
TransactionBehavior->>Handler: Next()
|
||||
Handler->>Repository: Add/Update/Delete
|
||||
Repository->>DbContext: SaveEntitiesAsync()
|
||||
DbContext-->>Repository: Success
|
||||
Repository-->>CommandHandler: Result
|
||||
CommandHandler-->>Controller: Response
|
||||
DbContext-->>Repository: Success + Dispatch Events
|
||||
Repository-->>Handler: Result
|
||||
Handler-->>Controller: Response
|
||||
Controller-->>Client: HTTP Response
|
||||
```
|
||||
|
||||
## Domain Events
|
||||
## Relationship State Machine
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
AR[Aggregate Root] -->|Raises| DE[Domain Event]
|
||||
DE -->|Dispatched by| CTX[DbContext]
|
||||
CTX -->|Publishes to| M[MediatR]
|
||||
M -->|Handled by| H1[Handler 1]
|
||||
M -->|Handled by| H2[Handler 2]
|
||||
stateDiagram-v2
|
||||
[*] --> Pending: SendFriendRequest
|
||||
Pending --> Accepted: Accept()
|
||||
Pending --> Rejected: Reject()
|
||||
Pending --> Cancelled: Cancel()
|
||||
Accepted --> Cancelled: Remove()
|
||||
|
||||
style AR fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style DE fill:#f39c12,stroke:#d68910,color:#fff
|
||||
style M fill:#9b59b6,stroke:#7d3c98,color:#fff
|
||||
[*] --> Accepted: FollowUser (auto-accept)
|
||||
|
||||
note right of Pending: For Friendship type only
|
||||
note right of Accepted: Following is auto-Accepted
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Sample Aggregate
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
samples {
|
||||
relationships {
|
||||
uuid id PK
|
||||
varchar(200) name
|
||||
varchar(1000) description
|
||||
uuid requester_id FK
|
||||
uuid addressee_id FK
|
||||
int type_id FK
|
||||
int status_id FK
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
sample_statuses {
|
||||
relationship_types {
|
||||
int id PK
|
||||
varchar(50) name
|
||||
}
|
||||
|
||||
samples ||--o{ sample_statuses : has
|
||||
relationship_statuses {
|
||||
int id PK
|
||||
varchar(50) name
|
||||
}
|
||||
|
||||
user_blocks {
|
||||
uuid id PK
|
||||
uuid blocker_id FK
|
||||
uuid blocked_id FK
|
||||
varchar(500) reason
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
user_profiles {
|
||||
uuid id PK
|
||||
uuid user_id UK
|
||||
varchar(100) display_name
|
||||
varchar(500) avatar_url
|
||||
varchar(500) bio
|
||||
timestamp last_synced_at
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
relationships ||--o| relationship_types : has_type
|
||||
relationships ||--o| relationship_statuses : has_status
|
||||
```
|
||||
|
||||
## MediatR Pipeline
|
||||
### Enumeration Values
|
||||
|
||||
```
|
||||
Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Log start/end Validate Begin/Commit
|
||||
+ timing with Transaction
|
||||
FluentValidation
|
||||
```
|
||||
**RelationshipType:**
|
||||
| ID | Name |
|
||||
|----|------|
|
||||
| 1 | Friendship |
|
||||
| 2 | Following |
|
||||
|
||||
### Behavior Order
|
||||
**RelationshipStatus:**
|
||||
| ID | Name |
|
||||
|----|------|
|
||||
| 1 | Pending |
|
||||
| 2 | Accepted |
|
||||
| 3 | Rejected |
|
||||
| 4 | Cancelled |
|
||||
|
||||
1. **LoggingBehavior** - Logs request handling with timing
|
||||
2. **ValidatorBehavior** - Validates request using FluentValidation
|
||||
3. **TransactionBehavior** - Wraps command handlers in database transactions
|
||||
## Domain Events Flow
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Exception Hierarchy
|
||||
|
||||
```
|
||||
Exception
|
||||
└── DomainException
|
||||
└── SampleDomainException
|
||||
```
|
||||
|
||||
### Problem Details (RFC 7807)
|
||||
|
||||
All errors are returned in Problem Details format:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7807",
|
||||
"title": "Validation Error",
|
||||
"status": 400,
|
||||
"detail": "One or more validation errors occurred.",
|
||||
"errors": {
|
||||
"Name": ["Name is required"]
|
||||
}
|
||||
}
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "Relationship Aggregate"
|
||||
R[Relationship] -->|Creates| E1[FriendRequestSentEvent]
|
||||
R -->|Accept| E2[FriendshipCreatedEvent]
|
||||
R -->|Follow| E3[UserFollowedEvent]
|
||||
R -->|Any Change| E4[StatusChangedEvent]
|
||||
R -->|Remove| E5[RemovedEvent]
|
||||
end
|
||||
|
||||
subgraph "UserBlock Aggregate"
|
||||
UB[UserBlock] -->|Creates| E6[UserBlockedEvent]
|
||||
end
|
||||
|
||||
subgraph "Event Handlers"
|
||||
E1 --> H1[Notification Handler]
|
||||
E2 --> H2[Notification Handler]
|
||||
E3 --> H3[Feed Handler]
|
||||
E6 --> H4[Cleanup Handler]
|
||||
end
|
||||
|
||||
style R fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style UB fill:#50c878,stroke:#2d8659,color:#fff
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
@@ -250,18 +300,38 @@ spec:
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Authentication**: JWT Bearer token (configure in production)
|
||||
2. **Authorization**: Role-based access control
|
||||
1. **Authentication**: JWT Bearer token from IAM Service
|
||||
2. **Authorization**: Ownership check in business logic
|
||||
3. **Input Validation**: FluentValidation on all requests
|
||||
4. **SQL Injection**: EF Core parameterized queries
|
||||
5. **Secrets**: Environment variables, never in code
|
||||
4. **Block Enforcement**: Blocking a user prevents all interactions
|
||||
5. **SQL Injection**: EF Core parameterized queries
|
||||
|
||||
## Integration with Other Services
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
SS[Social Service] <-->|Integration Events| EB[Event Bus]
|
||||
EB <--> IAM[IAM Service]
|
||||
EB <--> NS[Notification Service]
|
||||
EB <--> FS[Feed Service]
|
||||
|
||||
IAM -->|UserCreated/Updated| SS
|
||||
SS -->|FriendshipCreated| NS
|
||||
SS -->|UserFollowed| FS
|
||||
|
||||
style SS fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style EB fill:#9b59b6,stroke:#7d3c98,color:#fff
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
1. **Connection Pooling**: EF Core with Npgsql connection resilience
|
||||
2. **Async/Await**: All I/O operations are async
|
||||
3. **Response Caching**: Add caching headers for queries
|
||||
4. **Database Indexes**: Configure in EntityConfigurations
|
||||
3. **Pagination**: All list queries support skip/take
|
||||
4. **Indexes**:
|
||||
- `relationships`: (requester_id, addressee_id, type_id)
|
||||
- `user_blocks`: (blocker_id, blocked_id)
|
||||
- `user_profiles`: (user_id) UNIQUE
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
# .NET 10 Microservice Template
|
||||
# Social Service - Social Relationship Management
|
||||
|
||||
> Enterprise-grade .NET 10 microservice template following DDD, CQRS, and Clean Architecture patterns.
|
||||
> .NET 10 microservice for managing social relationships between users: friendship, following, and blocking.
|
||||
|
||||
## Overview
|
||||
|
||||
This template provides a production-ready structure for .NET microservices based on the eShopOnContainers reference architecture with:
|
||||
Social Service manages all social relationships between users in the GoodGo Platform:
|
||||
|
||||
- **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
|
||||
- **Friendship** - Send/accept/reject friend requests, unfriend
|
||||
- **Following** - Follow/unfollow users
|
||||
- **Blocking** - Block/unblock users with optional reason
|
||||
- **User Profile** - Sync user info from IAM Service
|
||||
|
||||
### Architecture & Patterns
|
||||
|
||||
| Pattern | Implementation |
|
||||
|---------|---------------|
|
||||
| **DDD** | Aggregates, Entities, Value Objects, Domain Events |
|
||||
| **CQRS** | Commands/Queries separated with MediatR |
|
||||
| **Clean Architecture** | 3-layer: Domain, Infrastructure, API |
|
||||
| **EF Core 10** | PostgreSQL with connection resilience |
|
||||
| **Structured Logging** | Serilog with console and Seq |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -21,41 +27,11 @@ This template provides a production-ready structure for .NET microservices based
|
||||
|-------------|---------|
|
||||
| .NET SDK | 10.0.101+ |
|
||||
| Docker | 24.0+ |
|
||||
| PostgreSQL | 15+ (or use Docker) |
|
||||
|
||||
```bash
|
||||
# Check .NET version
|
||||
dotnet --version
|
||||
# Should output: 10.0.xxx
|
||||
```
|
||||
| PostgreSQL | 15+ |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create New Service
|
||||
|
||||
```bash
|
||||
# Copy template to new service
|
||||
cp -r services/_template_dot_net services/your-service-name
|
||||
|
||||
# Navigate to service directory
|
||||
cd services/your-service-name
|
||||
|
||||
# Rename all occurrences of "SocialService" to "YourService"
|
||||
find . -type f -name "*.cs" -exec sed -i '' 's/SocialService/YourService/g' {} +
|
||||
find . -type f -name "*.csproj" -exec sed -i '' 's/SocialService/YourService/g' {} +
|
||||
```
|
||||
|
||||
### 2. Configure Environment
|
||||
|
||||
```bash
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your configuration
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 3. Run with Docker
|
||||
### Run with Docker
|
||||
|
||||
```bash
|
||||
# Start all services (API + PostgreSQL + Redis)
|
||||
@@ -65,7 +41,7 @@ docker-compose up -d
|
||||
docker-compose logs -f socialservice-api
|
||||
```
|
||||
|
||||
### 4. Run Locally
|
||||
### Run Locally
|
||||
|
||||
```bash
|
||||
# Restore dependencies
|
||||
@@ -78,53 +54,101 @@ dotnet build
|
||||
dotnet run --project src/SocialService.API
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
## Domain Model
|
||||
|
||||
### Aggregates
|
||||
|
||||
#### 1. Relationship Aggregate
|
||||
Manages friendship and following relationships between users.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Relationship {
|
||||
+Guid Id
|
||||
+Guid RequesterId
|
||||
+Guid AddresseeId
|
||||
+RelationshipType Type
|
||||
+RelationshipStatus Status
|
||||
+DateTime CreatedAt
|
||||
+DateTime? UpdatedAt
|
||||
+Accept()
|
||||
+Reject()
|
||||
+Cancel()
|
||||
+Remove()
|
||||
}
|
||||
|
||||
class RelationshipType {
|
||||
<<Enumeration>>
|
||||
Friendship
|
||||
Following
|
||||
}
|
||||
|
||||
class RelationshipStatus {
|
||||
<<Enumeration>>
|
||||
Pending
|
||||
Accepted
|
||||
Rejected
|
||||
Cancelled
|
||||
}
|
||||
|
||||
Relationship --> RelationshipType
|
||||
Relationship --> RelationshipStatus
|
||||
```
|
||||
_template_dot_net/
|
||||
├── src/
|
||||
│ ├── SocialService.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
|
||||
│ │ ├── Middleware/ # Custom middleware
|
||||
│ │ └── Program.cs # Application entry point
|
||||
│ │
|
||||
│ ├── SocialService.Domain/ # Domain Layer (Pure business logic)
|
||||
│ │ ├── AggregatesModel/ # Aggregate roots and entities
|
||||
│ │ ├── Events/ # Domain events
|
||||
│ │ ├── Exceptions/ # Domain exceptions
|
||||
│ │ └── SeedWork/ # Base classes (Entity, ValueObject, etc.)
|
||||
│ │
|
||||
│ └── SocialService.Infrastructure/ # Infrastructure Layer (Data access)
|
||||
│ ├── EntityConfigurations/ # EF Core Fluent API configurations
|
||||
│ ├── Repositories/ # Repository implementations
|
||||
│ ├── Idempotency/ # Request idempotency handling
|
||||
│ └── SocialServiceContext.cs # DbContext with Unit of Work
|
||||
│
|
||||
├── tests/
|
||||
│ ├── SocialService.UnitTests/ # Unit tests (Domain, Application)
|
||||
│ └── SocialService.FunctionalTests/ # Integration tests (API endpoints)
|
||||
│
|
||||
├── Dockerfile # Multi-stage Docker build
|
||||
├── docker-compose.yml # Local development setup
|
||||
├── global.json # .NET SDK version pinning
|
||||
└── Directory.Build.props # Common MSBuild properties
|
||||
```
|
||||
|
||||
**Business Rules:**
|
||||
- Following is auto-accepted
|
||||
- Friendship requires confirmation from recipient
|
||||
- Cannot create relationship with yourself
|
||||
- Only Pending requests can be Accept/Reject/Cancel
|
||||
- Only Accepted relationships can be Removed
|
||||
|
||||
#### 2. UserBlock Aggregate
|
||||
Manages user blocking.
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `BlockerId` | ID of blocking user |
|
||||
| `BlockedId` | ID of blocked user |
|
||||
| `Reason` | Block reason (optional) |
|
||||
| `CreatedAt` | Block timestamp |
|
||||
|
||||
#### 3. UserProfile Aggregate
|
||||
Caches user info synced from IAM Service.
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `UserId` | User ID from IAM |
|
||||
| `DisplayName` | Display name |
|
||||
| `AvatarUrl` | Avatar URL |
|
||||
| `Bio` | User bio |
|
||||
| `LastSyncedAt` | Last sync time |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Relationships - Friendship
|
||||
|
||||
| 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 |
|
||||
| `GET` | `/api/v1/relationships/users/{userId}/friends` | Get friend list |
|
||||
| `POST` | `/api/v1/relationships/friend-requests` | Send friend request |
|
||||
| `PUT` | `/api/v1/relationships/friend-requests/{id}` | Respond (accept/reject) |
|
||||
| `GET` | `/api/v1/relationships/users/{id1}/mutual-friends/{id2}` | Get mutual friends |
|
||||
| `GET` | `/api/v1/relationships/users/{userId}/suggestions` | Friend suggestions |
|
||||
|
||||
### Relationships - Following
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/v1/relationships/follow` | Follow user |
|
||||
| `DELETE` | `/api/v1/relationships/follow` | Unfollow user |
|
||||
|
||||
### Block Users
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/v1/blocks` | Block user |
|
||||
| `DELETE` | `/api/v1/blocks` | Unblock user |
|
||||
| `GET` | `/api/v1/blocks/users/{userId}` | Get blocked users list |
|
||||
|
||||
### Health Endpoints
|
||||
|
||||
@@ -134,74 +158,79 @@ _template_dot_net/
|
||||
| `/health/live` | Liveness probe |
|
||||
| `/health/ready` | Readiness probe |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
social-service-net/
|
||||
├── src/
|
||||
│ ├── SocialService.API/
|
||||
│ │ ├── Controllers/
|
||||
│ │ │ ├── RelationshipsController.cs # Friends + Following APIs
|
||||
│ │ │ └── BlocksController.cs # Block/Unblock APIs
|
||||
│ │ ├── Application/
|
||||
│ │ │ ├── Commands/ # 6 commands
|
||||
│ │ │ ├── Queries/ # 4 queries
|
||||
│ │ │ ├── Behaviors/ # MediatR pipeline
|
||||
│ │ │ └── Validations/ # FluentValidation
|
||||
│ │ └── Program.cs
|
||||
│ │
|
||||
│ ├── SocialService.Domain/
|
||||
│ │ ├── AggregatesModel/
|
||||
│ │ │ ├── RelationshipAggregate/ # Relationship, Status, Type
|
||||
│ │ │ ├── UserBlockAggregate/ # UserBlock
|
||||
│ │ │ └── UserProfileAggregate/ # UserProfile (IAM sync)
|
||||
│ │ ├── Events/ # 7 domain events
|
||||
│ │ ├── Exceptions/ # SocialDomainException
|
||||
│ │ └── SeedWork/ # Base classes
|
||||
│ │
|
||||
│ └── SocialService.Infrastructure/
|
||||
│ ├── EntityConfigurations/ # EF Core mappings
|
||||
│ ├── Repositories/ # 3 repositories
|
||||
│ ├── Idempotency/ # Request deduplication
|
||||
│ └── SocialServiceContext.cs # DbContext
|
||||
│
|
||||
├── tests/
|
||||
│ ├── SocialService.UnitTests/
|
||||
│ └── SocialService.FunctionalTests/
|
||||
│
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── docs/
|
||||
```
|
||||
|
||||
## CQRS Pattern
|
||||
|
||||
### Commands (Write Operations)
|
||||
|
||||
```csharp
|
||||
// Define command
|
||||
public record CreateSampleCommand(string Name, string? Description)
|
||||
: IRequest<CreateSampleCommandResult>;
|
||||
|
||||
// Handle command
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `SendFriendRequestCommand` | Send friend request |
|
||||
| `RespondToFriendRequestCommand` | Accept/Reject friend request |
|
||||
| `FollowUserCommand` | Follow user |
|
||||
| `UnfollowUserCommand` | Unfollow user |
|
||||
| `BlockUserCommand` | Block user with optional reason |
|
||||
| `UnblockUserCommand` | Unblock user |
|
||||
|
||||
### Queries (Read Operations)
|
||||
|
||||
```csharp
|
||||
// Define query
|
||||
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
|
||||
```
|
||||
| Query | Description |
|
||||
|-------|-------------|
|
||||
| `GetFriendsQuery` | Friend list with pagination |
|
||||
| `GetMutualFriendsQuery` | Mutual friends between 2 users |
|
||||
| `GetFriendSuggestionsQuery` | Friend suggestions |
|
||||
| `GetBlockedUsersQuery` | Blocked users list |
|
||||
|
||||
## Domain Model
|
||||
## Domain Events
|
||||
|
||||
### Aggregate Root
|
||||
|
||||
```csharp
|
||||
public class Sample : Entity, IAggregateRoot
|
||||
{
|
||||
public string Name => _name;
|
||||
public SampleStatus Status => _status;
|
||||
|
||||
public Sample(string name, string? description) {
|
||||
// Business logic validation
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new SampleDomainException("Sample name cannot be empty");
|
||||
|
||||
// Domain event
|
||||
AddDomainEvent(new SampleCreatedDomainEvent(this));
|
||||
}
|
||||
|
||||
public void Activate() {
|
||||
if (_status != SampleStatus.Draft)
|
||||
throw new SampleDomainException("Only draft samples can be activated");
|
||||
// State transition
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run with coverage
|
||||
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura
|
||||
|
||||
# Run specific test project
|
||||
dotnet test tests/SocialService.UnitTests
|
||||
```
|
||||
| Event | Trigger |
|
||||
|-------|---------|
|
||||
| `FriendRequestSentDomainEvent` | Friend request sent |
|
||||
| `FriendshipCreatedDomainEvent` | Friend request accepted |
|
||||
| `UserFollowedDomainEvent` | User followed |
|
||||
| `RelationshipStatusChangedDomainEvent` | Status changed |
|
||||
| `RelationshipRemovedDomainEvent` | Unfriend/unfollow |
|
||||
| `UserBlockedDomainEvent` | User blocked |
|
||||
| `UserUnblockedDomainEvent` | User unblocked |
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -212,7 +241,6 @@ dotnet test tests/SocialService.UnitTests
|
||||
| `ASPNETCORE_ENVIRONMENT` | Environment name | `Development` |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | - |
|
||||
| `REDIS_URL` | Redis connection string | - |
|
||||
| `JWT_SECRET` | JWT signing secret (min 32 chars) | - |
|
||||
|
||||
### appsettings.json
|
||||
|
||||
@@ -227,6 +255,19 @@ dotnet test tests/SocialService.UnitTests
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run with coverage
|
||||
dotnet test /p:CollectCoverage=true
|
||||
|
||||
# Run specific test project
|
||||
dotnet test tests/SocialService.UnitTests
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Build
|
||||
@@ -239,26 +280,11 @@ docker build -t socialservice:latest .
|
||||
docker run -p 5000:8080 --env-file .env socialservice:latest
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md) for Kubernetes deployment manifests.
|
||||
|
||||
## What's New in .NET 10
|
||||
|
||||
- **C# 14** language features
|
||||
- Improved **Native AOT** support
|
||||
- Better **async/await** performance
|
||||
- Enhanced **JSON serialization**
|
||||
- Performance improvements across the board
|
||||
- 3-year **LTS** support (until November 2028)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Architecture Documentation](./ARCHITECTURE.md)
|
||||
- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers) - Reference architecture
|
||||
- [.NET 10 Documentation](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10)
|
||||
- [DDD with .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)
|
||||
- [MediatR](https://github.com/jbogard/MediatR) - CQRS library
|
||||
- [FluentValidation](https://docs.fluentvalidation.net/) - Validation library
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
# Tài Liệu Kiến Trúc
|
||||
# Tài Liệu Kiến Trúc - Social Service
|
||||
|
||||
> Tài liệu kiến trúc chi tiết cho Template Microservice .NET 10.
|
||||
> Tài liệu kiến trúc chi tiết cho Social Service - Dịch vụ quản lý quan hệ xã hội.
|
||||
|
||||
## Tổng Quan Kiến Trúc
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Lớp API"
|
||||
C[Controllers]
|
||||
RC[RelationshipsController]
|
||||
BC[BlocksController]
|
||||
CMD[Commands]
|
||||
Q[Queries]
|
||||
B[Behaviors]
|
||||
V[Validations]
|
||||
end
|
||||
|
||||
subgraph "Lớp Domain"
|
||||
AR[Aggregate Roots]
|
||||
E[Entities]
|
||||
VO[Value Objects]
|
||||
RA[Relationship Aggregate]
|
||||
UBA[UserBlock Aggregate]
|
||||
UPA[UserProfile Aggregate]
|
||||
DE[Domain Events]
|
||||
DX[Domain Exceptions]
|
||||
end
|
||||
|
||||
subgraph "Lớp Infrastructure"
|
||||
DB[(PostgreSQL)]
|
||||
R[Repositories]
|
||||
CTX[DbContext]
|
||||
ID[Idempotency]
|
||||
RR[RelationshipRepository]
|
||||
UBR[UserBlockRepository]
|
||||
UPR[UserProfileRepository]
|
||||
CTX[SocialServiceContext]
|
||||
end
|
||||
|
||||
C --> CMD
|
||||
C --> Q
|
||||
CMD --> B --> V
|
||||
CMD --> AR
|
||||
Q --> R
|
||||
R --> CTX --> DB
|
||||
AR --> DE
|
||||
R --> AR
|
||||
RC --> CMD
|
||||
RC --> Q
|
||||
BC --> CMD
|
||||
BC --> Q
|
||||
CMD --> B
|
||||
CMD --> RA
|
||||
CMD --> UBA
|
||||
Q --> RR
|
||||
Q --> UBR
|
||||
RR --> CTX --> DB
|
||||
UBR --> CTX
|
||||
UPR --> CTX
|
||||
RA --> DE
|
||||
UBA --> DE
|
||||
|
||||
style C fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style AR fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style RC fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style BC fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style RA fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style UBA fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style UPA fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
|
||||
```
|
||||
|
||||
@@ -47,34 +56,51 @@ graph TB
|
||||
|
||||
### 1. Lớp Domain (SocialService.Domain)
|
||||
|
||||
Trái tim của ứng dụng chứa business logic thuần túy. Lớp này:
|
||||
- Có **ZERO** phụ thuộc bên ngoài (ngoại trừ MediatR.Contracts cho events)
|
||||
- Chỉ chứa các class POCO
|
||||
- Triển khai các tactical patterns của DDD
|
||||
Trái tim của ứng dụng chứa business logic thuần túy.
|
||||
|
||||
#### Thành Phần
|
||||
#### Aggregates
|
||||
|
||||
| Thành phần | Mục Đích |
|
||||
|------------|----------|
|
||||
| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
|
||||
| **AggregatesModel** | Aggregate roots với entities và value objects |
|
||||
| **Events** | Domain events cho giao tiếp cross-aggregate |
|
||||
| **Exceptions** | Domain exceptions cho vi phạm business rules |
|
||||
| Aggregate | Mục Đích |
|
||||
|-----------|----------|
|
||||
| **Relationship** | Quản lý kết bạn (Friendship) và theo dõi (Following) |
|
||||
| **UserBlock** | Quản lý việc block users |
|
||||
| **UserProfile** | Cache thông tin user từ IAM Service |
|
||||
|
||||
#### Domain Events
|
||||
|
||||
| Event | Trigger |
|
||||
|-------|---------|
|
||||
| `FriendRequestSentDomainEvent` | Gửi yêu cầu kết bạn |
|
||||
| `FriendshipCreatedDomainEvent` | Chấp nhận kết bạn |
|
||||
| `UserFollowedDomainEvent` | Follow user |
|
||||
| `RelationshipStatusChangedDomainEvent` | Thay đổi trạng thái quan hệ |
|
||||
| `RelationshipRemovedDomainEvent` | Hủy kết bạn/bỏ theo dõi |
|
||||
| `UserBlockedDomainEvent` | Block user |
|
||||
| `UserUnblockedDomainEvent` | Unblock user |
|
||||
|
||||
### 2. Lớp Infrastructure (SocialService.Infrastructure)
|
||||
|
||||
Triển khai kỹ thuật và các mối quan tâm bên ngoài:
|
||||
- Truy cập database (EF Core)
|
||||
- Triển khai repositories
|
||||
- Tích hợp external services
|
||||
Triển khai kỹ thuật và truy cập dữ liệu.
|
||||
|
||||
| Component | Mục Đích |
|
||||
|-----------|----------|
|
||||
| **SocialServiceContext** | DbContext với Unit of Work pattern |
|
||||
| **RelationshipRepository** | CRUD cho Relationship aggregate |
|
||||
| **UserBlockRepository** | CRUD cho UserBlock aggregate |
|
||||
| **UserProfileRepository** | CRUD cho UserProfile aggregate |
|
||||
| **EntityConfigurations** | EF Core Fluent API mappings |
|
||||
|
||||
### 3. Lớp API (SocialService.API)
|
||||
|
||||
Điểm vào ứng dụng và triển khai CQRS:
|
||||
- Controllers để xử lý HTTP
|
||||
- Commands cho các thao tác ghi
|
||||
- Queries cho các thao tác đọc
|
||||
- MediatR behaviors cho cross-cutting concerns
|
||||
Điểm vào ứng dụng và triển khai CQRS.
|
||||
|
||||
| Component | Mục Đích |
|
||||
|-----------|----------|
|
||||
| **RelationshipsController** | APIs cho kết bạn và theo dõi |
|
||||
| **BlocksController** | APIs cho block/unblock users |
|
||||
| **Commands** | 6 write operations (MediatR) |
|
||||
| **Queries** | 4 read operations |
|
||||
| **Behaviors** | Logging, Validation, Transaction |
|
||||
|
||||
## Luồng CQRS
|
||||
|
||||
@@ -86,7 +112,7 @@ sequenceDiagram
|
||||
participant LoggingBehavior
|
||||
participant ValidatorBehavior
|
||||
participant TransactionBehavior
|
||||
participant CommandHandler
|
||||
participant Handler
|
||||
participant Repository
|
||||
participant DbContext
|
||||
|
||||
@@ -95,94 +121,118 @@ sequenceDiagram
|
||||
MediatR->>LoggingBehavior: Handle
|
||||
LoggingBehavior->>ValidatorBehavior: Next()
|
||||
ValidatorBehavior->>TransactionBehavior: Next()
|
||||
TransactionBehavior->>CommandHandler: Next()
|
||||
CommandHandler->>Repository: Add/Update/Delete
|
||||
TransactionBehavior->>Handler: Next()
|
||||
Handler->>Repository: Add/Update/Delete
|
||||
Repository->>DbContext: SaveEntitiesAsync()
|
||||
DbContext-->>Repository: Success
|
||||
Repository-->>CommandHandler: Result
|
||||
CommandHandler-->>Controller: Response
|
||||
DbContext-->>Repository: Success + Dispatch Events
|
||||
Repository-->>Handler: Result
|
||||
Handler-->>Controller: Response
|
||||
Controller-->>Client: HTTP Response
|
||||
```
|
||||
|
||||
## Domain Events
|
||||
## Relationship State Machine
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
AR[Aggregate Root] -->|Phát sinh| DE[Domain Event]
|
||||
DE -->|Dispatch bởi| CTX[DbContext]
|
||||
CTX -->|Publish tới| M[MediatR]
|
||||
M -->|Xử lý bởi| H1[Handler 1]
|
||||
M -->|Xử lý bởi| H2[Handler 2]
|
||||
stateDiagram-v2
|
||||
[*] --> Pending: SendFriendRequest
|
||||
Pending --> Accepted: Accept()
|
||||
Pending --> Rejected: Reject()
|
||||
Pending --> Cancelled: Cancel()
|
||||
Accepted --> Cancelled: Remove()
|
||||
|
||||
style AR fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style DE fill:#f39c12,stroke:#d68910,color:#fff
|
||||
style M fill:#9b59b6,stroke:#7d3c98,color:#fff
|
||||
[*] --> Accepted: FollowUser (auto-accept)
|
||||
|
||||
note right of Pending: Chỉ cho Friendship type
|
||||
note right of Accepted: Follow tự động Accepted
|
||||
```
|
||||
|
||||
## Schema Database
|
||||
|
||||
### Sample Aggregate
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
samples {
|
||||
relationships {
|
||||
uuid id PK
|
||||
varchar(200) name
|
||||
varchar(1000) description
|
||||
uuid requester_id FK
|
||||
uuid addressee_id FK
|
||||
int type_id FK
|
||||
int status_id FK
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
sample_statuses {
|
||||
relationship_types {
|
||||
int id PK
|
||||
varchar(50) name
|
||||
}
|
||||
|
||||
samples ||--o{ sample_statuses : has
|
||||
relationship_statuses {
|
||||
int id PK
|
||||
varchar(50) name
|
||||
}
|
||||
|
||||
user_blocks {
|
||||
uuid id PK
|
||||
uuid blocker_id FK
|
||||
uuid blocked_id FK
|
||||
varchar(500) reason
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
user_profiles {
|
||||
uuid id PK
|
||||
uuid user_id UK
|
||||
varchar(100) display_name
|
||||
varchar(500) avatar_url
|
||||
varchar(500) bio
|
||||
timestamp last_synced_at
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
relationships ||--o| relationship_types : has_type
|
||||
relationships ||--o| relationship_statuses : has_status
|
||||
```
|
||||
|
||||
## Pipeline MediatR
|
||||
### Enumeration Values
|
||||
|
||||
```
|
||||
Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Log start/end Validate Begin/Commit
|
||||
+ timing với Transaction
|
||||
FluentValidation
|
||||
```
|
||||
**RelationshipType:**
|
||||
| ID | Name |
|
||||
|----|------|
|
||||
| 1 | Friendship |
|
||||
| 2 | Following |
|
||||
|
||||
### Thứ Tự Behaviors
|
||||
**RelationshipStatus:**
|
||||
| ID | Name |
|
||||
|----|------|
|
||||
| 1 | Pending |
|
||||
| 2 | Accepted |
|
||||
| 3 | Rejected |
|
||||
| 4 | Cancelled |
|
||||
|
||||
1. **LoggingBehavior** - Ghi log xử lý request với timing
|
||||
2. **ValidatorBehavior** - Validate request sử dụng FluentValidation
|
||||
3. **TransactionBehavior** - Bao bọc command handlers trong database transactions
|
||||
## Domain Events Flow
|
||||
|
||||
## Xử Lý Lỗi
|
||||
|
||||
### Phân Cấp Exceptions
|
||||
|
||||
```
|
||||
Exception
|
||||
└── DomainException
|
||||
└── SampleDomainException
|
||||
```
|
||||
|
||||
### Problem Details (RFC 7807)
|
||||
|
||||
Tất cả lỗi được trả về theo định dạng Problem Details:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7807",
|
||||
"title": "Lỗi Validation",
|
||||
"status": 400,
|
||||
"detail": "Một hoặc nhiều lỗi validation đã xảy ra.",
|
||||
"errors": {
|
||||
"Name": ["Tên là bắt buộc"]
|
||||
}
|
||||
}
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "Relationship Aggregate"
|
||||
R[Relationship] -->|Creates| E1[FriendRequestSentEvent]
|
||||
R -->|Accept| E2[FriendshipCreatedEvent]
|
||||
R -->|Follow| E3[UserFollowedEvent]
|
||||
R -->|Any Change| E4[StatusChangedEvent]
|
||||
R -->|Remove| E5[RemovedEvent]
|
||||
end
|
||||
|
||||
subgraph "UserBlock Aggregate"
|
||||
UB[UserBlock] -->|Creates| E6[UserBlockedEvent]
|
||||
end
|
||||
|
||||
subgraph "Event Handlers"
|
||||
E1 --> H1[Notification Handler]
|
||||
E2 --> H2[Notification Handler]
|
||||
E3 --> H3[Feed Handler]
|
||||
E6 --> H4[Cleanup Handler]
|
||||
end
|
||||
|
||||
style R fill:#50c878,stroke:#2d8659,color:#fff
|
||||
style UB fill:#50c878,stroke:#2d8659,color:#fff
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
@@ -250,18 +300,38 @@ spec:
|
||||
|
||||
## Cân Nhắc Bảo Mật
|
||||
|
||||
1. **Authentication**: JWT Bearer token (cấu hình trong production)
|
||||
2. **Authorization**: Role-based access control
|
||||
1. **Authentication**: JWT Bearer token từ IAM Service
|
||||
2. **Authorization**: Kiểm tra ownership trong business logic
|
||||
3. **Input Validation**: FluentValidation trên tất cả requests
|
||||
4. **SQL Injection**: EF Core parameterized queries
|
||||
5. **Secrets**: Biến môi trường, không bao giờ trong code
|
||||
4. **Block Enforcement**: Block một user sẽ ngăn mọi tương tác
|
||||
5. **SQL Injection**: EF Core parameterized queries
|
||||
|
||||
## Tích Hợp Với Các Services Khác
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
SS[Social Service] <-->|Integration Events| EB[Event Bus]
|
||||
EB <--> IAM[IAM Service]
|
||||
EB <--> NS[Notification Service]
|
||||
EB <--> FS[Feed Service]
|
||||
|
||||
IAM -->|UserCreated/Updated| SS
|
||||
SS -->|FriendshipCreated| NS
|
||||
SS -->|UserFollowed| FS
|
||||
|
||||
style SS fill:#4a90d9,stroke:#2d5986,color:#fff
|
||||
style EB fill:#9b59b6,stroke:#7d3c98,color:#fff
|
||||
```
|
||||
|
||||
## Tối Ưu Hiệu Năng
|
||||
|
||||
1. **Connection Pooling**: EF Core với Npgsql connection resilience
|
||||
2. **Async/Await**: Tất cả I/O operations đều async
|
||||
3. **Response Caching**: Thêm caching headers cho queries
|
||||
4. **Database Indexes**: Cấu hình trong EntityConfigurations
|
||||
3. **Pagination**: Tất cả list queries hỗ trợ skip/take
|
||||
4. **Indexes**:
|
||||
- `relationships`: (requester_id, addressee_id, type_id)
|
||||
- `user_blocks`: (blocker_id, blocked_id)
|
||||
- `user_profiles`: (user_id) UNIQUE
|
||||
|
||||
## Tài Liệu Tham Khảo
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
# Template Microservice .NET 10
|
||||
# Social Service - Dịch Vụ Quản Lý Quan Hệ Xã Hội
|
||||
|
||||
> Template microservice .NET 10 cấp doanh nghiệp theo các pattern DDD, CQRS và Clean Architecture.
|
||||
> Microservice .NET 10 quản lý quan hệ xã hội giữa users: kết bạn, theo dõi và block.
|
||||
|
||||
## Tổng Quan
|
||||
|
||||
Template này cung cấp cấu trúc sẵn sàng production cho microservices .NET dựa trên kiến trúc tham chiếu eShopOnContainers với:
|
||||
Social Service là microservice quản lý tất cả các mối quan hệ xã hội giữa users trong GoodGo Platform:
|
||||
|
||||
- **Domain-Driven Design (DDD)** - Aggregates, Entities, Value Objects, Domain Events
|
||||
- **CQRS Pattern** - Tách biệt Commands (ghi) và Queries (đọc) với MediatR
|
||||
- **Clean Architecture** - Phân tầng Domain, Infrastructure, API
|
||||
- **EF Core 10** - PostgreSQL với connection resilience
|
||||
- **FluentValidation** - Validation request
|
||||
- **API Versioning** - Versioning theo URL segment
|
||||
- **Health Checks** - Probes sẵn sàng cho Kubernetes
|
||||
- **Structured Logging** - Serilog với console và Seq
|
||||
- **Kết bạn (Friendship)** - Gửi/chấp nhận/từ chối yêu cầu kết bạn, hủy kết bạn
|
||||
- **Theo dõi (Following)** - Follow/unfollow users
|
||||
- **Block (User Blocking)** - Block/unblock users với lý do tùy chọn
|
||||
- **User Profile** - Đồng bộ thông tin user từ IAM Service
|
||||
|
||||
### Kiến Trúc & Patterns
|
||||
|
||||
| Pattern | Triển khai |
|
||||
|---------|-----------|
|
||||
| **DDD** | Aggregates, Entities, Value Objects, Domain Events |
|
||||
| **CQRS** | Commands/Queries tách biệt với MediatR |
|
||||
| **Clean Architecture** | 3-layer: Domain, Infrastructure, API |
|
||||
| **EF Core 10** | PostgreSQL với connection resilience |
|
||||
| **Structured Logging** | Serilog với console và Seq |
|
||||
|
||||
## Yêu Cầu
|
||||
|
||||
@@ -21,41 +27,11 @@ Template này cung cấp cấu trúc sẵn sàng production cho microservices .N
|
||||
|---------|-----------|
|
||||
| .NET SDK | 10.0.101+ |
|
||||
| Docker | 24.0+ |
|
||||
| PostgreSQL | 15+ (hoặc dùng Docker) |
|
||||
|
||||
```bash
|
||||
# Kiểm tra phiên bản .NET
|
||||
dotnet --version
|
||||
# Kết quả nên là: 10.0.xxx
|
||||
```
|
||||
| PostgreSQL | 15+ |
|
||||
|
||||
## Bắt Đầu Nhanh
|
||||
|
||||
### 1. Tạo Service Mới
|
||||
|
||||
```bash
|
||||
# Sao chép template sang service mới
|
||||
cp -r services/_template_dot_net services/your-service-name
|
||||
|
||||
# Di chuyển đến thư mục service
|
||||
cd services/your-service-name
|
||||
|
||||
# Đổi tên tất cả "SocialService" thành "YourService"
|
||||
find . -type f -name "*.cs" -exec sed -i '' 's/SocialService/YourService/g' {} +
|
||||
find . -type f -name "*.csproj" -exec sed -i '' 's/SocialService/YourService/g' {} +
|
||||
```
|
||||
|
||||
### 2. Cấu Hình Môi Trường
|
||||
|
||||
```bash
|
||||
# Sao chép template môi trường
|
||||
cp .env.example .env
|
||||
|
||||
# Chỉnh sửa với cấu hình của bạn
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 3. Chạy với Docker
|
||||
### Chạy với Docker
|
||||
|
||||
```bash
|
||||
# Khởi động tất cả services (API + PostgreSQL + Redis)
|
||||
@@ -65,7 +41,7 @@ docker-compose up -d
|
||||
docker-compose logs -f socialservice-api
|
||||
```
|
||||
|
||||
### 4. Chạy Local
|
||||
### Chạy Local
|
||||
|
||||
```bash
|
||||
# Khôi phục dependencies
|
||||
@@ -78,53 +54,101 @@ dotnet build
|
||||
dotnet run --project src/SocialService.API
|
||||
```
|
||||
|
||||
## Cấu Trúc Dự Án
|
||||
## Domain Model
|
||||
|
||||
```
|
||||
_template_dot_net/
|
||||
├── src/
|
||||
│ ├── SocialService.API/ # Lớp Presentation (Controllers, CQRS)
|
||||
│ │ ├── Controllers/ # Các API endpoints
|
||||
│ │ ├── Application/ # Triển khai CQRS
|
||||
│ │ │ ├── Commands/ # Thao tác ghi (MediatR)
|
||||
│ │ │ ├── Queries/ # Thao tác đọc
|
||||
│ │ │ ├── Behaviors/ # MediatR pipeline behaviors
|
||||
│ │ │ └── Validations/ # FluentValidation validators
|
||||
│ │ ├── Middleware/ # Custom middleware
|
||||
│ │ └── Program.cs # Điểm vào ứng dụng
|
||||
│ │
|
||||
│ ├── SocialService.Domain/ # Lớp Domain (Business logic thuần túy)
|
||||
│ │ ├── AggregatesModel/ # Aggregate roots và entities
|
||||
│ │ ├── Events/ # Domain events
|
||||
│ │ ├── Exceptions/ # Domain exceptions
|
||||
│ │ └── SeedWork/ # Base classes (Entity, ValueObject, etc.)
|
||||
│ │
|
||||
│ └── SocialService.Infrastructure/ # Lớp Infrastructure (Truy cập dữ liệu)
|
||||
│ ├── EntityConfigurations/ # Cấu hình EF Core Fluent API
|
||||
│ ├── Repositories/ # Triển khai repositories
|
||||
│ ├── Idempotency/ # Xử lý idempotency request
|
||||
│ └── SocialServiceContext.cs # DbContext với Unit of Work
|
||||
│
|
||||
├── tests/
|
||||
│ ├── SocialService.UnitTests/ # Unit tests (Domain, Application)
|
||||
│ └── SocialService.FunctionalTests/ # Integration tests (API endpoints)
|
||||
│
|
||||
├── Dockerfile # Multi-stage Docker build
|
||||
├── docker-compose.yml # Thiết lập phát triển local
|
||||
├── global.json # Pin phiên bản .NET SDK
|
||||
└── Directory.Build.props # Thuộc tính MSBuild chung
|
||||
### Aggregates
|
||||
|
||||
#### 1. Relationship Aggregate
|
||||
Quản lý quan hệ kết bạn và theo dõi giữa users.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Relationship {
|
||||
+Guid Id
|
||||
+Guid RequesterId
|
||||
+Guid AddresseeId
|
||||
+RelationshipType Type
|
||||
+RelationshipStatus Status
|
||||
+DateTime CreatedAt
|
||||
+DateTime? UpdatedAt
|
||||
+Accept()
|
||||
+Reject()
|
||||
+Cancel()
|
||||
+Remove()
|
||||
}
|
||||
|
||||
class RelationshipType {
|
||||
<<Enumeration>>
|
||||
Friendship
|
||||
Following
|
||||
}
|
||||
|
||||
class RelationshipStatus {
|
||||
<<Enumeration>>
|
||||
Pending
|
||||
Accepted
|
||||
Rejected
|
||||
Cancelled
|
||||
}
|
||||
|
||||
Relationship --> RelationshipType
|
||||
Relationship --> RelationshipStatus
|
||||
```
|
||||
|
||||
## Các Endpoint API
|
||||
**Business Rules:**
|
||||
- Following tự động được chấp nhận
|
||||
- Friendship yêu cầu xác nhận từ người nhận
|
||||
- Không thể tạo quan hệ với chính mình
|
||||
- Chỉ Pending requests mới có thể Accept/Reject/Cancel
|
||||
- Chỉ Accepted relationships mới có thể Remove
|
||||
|
||||
#### 2. UserBlock Aggregate
|
||||
Quản lý việc block users.
|
||||
|
||||
| Field | Mô tả |
|
||||
|-------|-------|
|
||||
| `BlockerId` | ID user thực hiện block |
|
||||
| `BlockedId` | ID user bị block |
|
||||
| `Reason` | Lý do block (tùy chọn) |
|
||||
| `CreatedAt` | Thời gian block |
|
||||
|
||||
#### 3. UserProfile Aggregate
|
||||
Cache thông tin user được đồng bộ từ IAM Service.
|
||||
|
||||
| Field | Mô tả |
|
||||
|-------|-------|
|
||||
| `UserId` | ID user từ IAM |
|
||||
| `DisplayName` | Tên hiển thị |
|
||||
| `AvatarUrl` | URL avatar |
|
||||
| `Bio` | Tiểu sử |
|
||||
| `LastSyncedAt` | Thời gian sync cuối |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Relationships - Kết Bạn
|
||||
|
||||
| Method | Endpoint | Mô Tả |
|
||||
|--------|----------|-------|
|
||||
| `GET` | `/api/v1/samples` | Lấy tất cả samples |
|
||||
| `GET` | `/api/v1/samples/{id}` | Lấy sample theo ID |
|
||||
| `POST` | `/api/v1/samples` | Tạo sample mới |
|
||||
| `PUT` | `/api/v1/samples/{id}` | Cập nhật sample |
|
||||
| `DELETE` | `/api/v1/samples/{id}` | Xóa sample |
|
||||
| `PATCH` | `/api/v1/samples/{id}/status` | Thay đổi trạng thái |
|
||||
| `GET` | `/api/v1/relationships/users/{userId}/friends` | Lấy danh sách bạn bè |
|
||||
| `POST` | `/api/v1/relationships/friend-requests` | Gửi yêu cầu kết bạn |
|
||||
| `PUT` | `/api/v1/relationships/friend-requests/{id}` | Phản hồi yêu cầu (accept/reject) |
|
||||
| `GET` | `/api/v1/relationships/users/{id1}/mutual-friends/{id2}` | Lấy bạn chung |
|
||||
| `GET` | `/api/v1/relationships/users/{userId}/suggestions` | Gợi ý kết bạn |
|
||||
|
||||
### Relationships - Theo Dõi
|
||||
|
||||
| Method | Endpoint | Mô Tả |
|
||||
|--------|----------|-------|
|
||||
| `POST` | `/api/v1/relationships/follow` | Theo dõi user |
|
||||
| `DELETE` | `/api/v1/relationships/follow` | Bỏ theo dõi user |
|
||||
|
||||
### Block Users
|
||||
|
||||
| Method | Endpoint | Mô Tả |
|
||||
|--------|----------|-------|
|
||||
| `POST` | `/api/v1/blocks` | Block user |
|
||||
| `DELETE` | `/api/v1/blocks` | Unblock user |
|
||||
| `GET` | `/api/v1/blocks/users/{userId}` | Lấy danh sách blocked users |
|
||||
|
||||
### Health Endpoints
|
||||
|
||||
@@ -134,74 +158,79 @@ _template_dot_net/
|
||||
| `/health/live` | Kiểm tra sống |
|
||||
| `/health/ready` | Kiểm tra sẵn sàng |
|
||||
|
||||
## Pattern CQRS
|
||||
## Cấu Trúc Dự Án
|
||||
|
||||
```
|
||||
social-service-net/
|
||||
├── src/
|
||||
│ ├── SocialService.API/
|
||||
│ │ ├── Controllers/
|
||||
│ │ │ ├── RelationshipsController.cs # Friends + Following APIs
|
||||
│ │ │ └── BlocksController.cs # Block/Unblock APIs
|
||||
│ │ ├── Application/
|
||||
│ │ │ ├── Commands/ # 6 commands
|
||||
│ │ │ ├── Queries/ # 4 queries
|
||||
│ │ │ ├── Behaviors/ # MediatR pipeline
|
||||
│ │ │ └── Validations/ # FluentValidation
|
||||
│ │ └── Program.cs
|
||||
│ │
|
||||
│ ├── SocialService.Domain/
|
||||
│ │ ├── AggregatesModel/
|
||||
│ │ │ ├── RelationshipAggregate/ # Relationship, Status, Type
|
||||
│ │ │ ├── UserBlockAggregate/ # UserBlock
|
||||
│ │ │ └── UserProfileAggregate/ # UserProfile (IAM sync)
|
||||
│ │ ├── Events/ # 7 domain events
|
||||
│ │ ├── Exceptions/ # SocialDomainException
|
||||
│ │ └── SeedWork/ # Base classes
|
||||
│ │
|
||||
│ └── SocialService.Infrastructure/
|
||||
│ ├── EntityConfigurations/ # EF Core mappings
|
||||
│ ├── Repositories/ # 3 repositories
|
||||
│ ├── Idempotency/ # Request deduplication
|
||||
│ └── SocialServiceContext.cs # DbContext
|
||||
│
|
||||
├── tests/
|
||||
│ ├── SocialService.UnitTests/
|
||||
│ └── SocialService.FunctionalTests/
|
||||
│
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── docs/
|
||||
```
|
||||
|
||||
## CQRS Pattern
|
||||
|
||||
### Commands (Thao Tác Ghi)
|
||||
|
||||
```csharp
|
||||
// Định nghĩa command
|
||||
public record CreateSampleCommand(string Name, string? Description)
|
||||
: IRequest<CreateSampleCommandResult>;
|
||||
|
||||
// Xử lý command
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
| Command | Mô Tả |
|
||||
|---------|-------|
|
||||
| `SendFriendRequestCommand` | Gửi yêu cầu kết bạn |
|
||||
| `RespondToFriendRequestCommand` | Accept/Reject friend request |
|
||||
| `FollowUserCommand` | Theo dõi user |
|
||||
| `UnfollowUserCommand` | Bỏ theo dõi |
|
||||
| `BlockUserCommand` | Block user với lý do tùy chọn |
|
||||
| `UnblockUserCommand` | Unblock user |
|
||||
|
||||
### Queries (Thao Tác Đọc)
|
||||
|
||||
```csharp
|
||||
// Định nghĩa query
|
||||
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
|
||||
```
|
||||
| Query | Mô Tả |
|
||||
|-------|-------|
|
||||
| `GetFriendsQuery` | Danh sách bạn bè với phân trang |
|
||||
| `GetMutualFriendsQuery` | Bạn chung giữa 2 users |
|
||||
| `GetFriendSuggestionsQuery` | Gợi ý kết bạn |
|
||||
| `GetBlockedUsersQuery` | Danh sách users bị block |
|
||||
|
||||
## Domain Model
|
||||
## Domain Events
|
||||
|
||||
### Aggregate Root
|
||||
|
||||
```csharp
|
||||
public class Sample : Entity, IAggregateRoot
|
||||
{
|
||||
public string Name => _name;
|
||||
public SampleStatus Status => _status;
|
||||
|
||||
public Sample(string name, string? description) {
|
||||
// Validation business logic
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new SampleDomainException("Tên sample không được để trống");
|
||||
|
||||
// Domain event
|
||||
AddDomainEvent(new SampleCreatedDomainEvent(this));
|
||||
}
|
||||
|
||||
public void Activate() {
|
||||
if (_status != SampleStatus.Draft)
|
||||
throw new SampleDomainException("Chỉ sample draft mới có thể kích hoạt");
|
||||
// Chuyển đổi trạng thái
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kiểm Thử
|
||||
|
||||
```bash
|
||||
# Chạy tất cả tests
|
||||
dotnet test
|
||||
|
||||
# Chạy với coverage
|
||||
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura
|
||||
|
||||
# Chạy project test cụ thể
|
||||
dotnet test tests/SocialService.UnitTests
|
||||
```
|
||||
| Event | Trigger |
|
||||
|-------|---------|
|
||||
| `FriendRequestSentDomainEvent` | Khi gửi friend request |
|
||||
| `FriendshipCreatedDomainEvent` | Khi friend request được accept |
|
||||
| `UserFollowedDomainEvent` | Khi follow user |
|
||||
| `RelationshipStatusChangedDomainEvent` | Khi status thay đổi |
|
||||
| `RelationshipRemovedDomainEvent` | Khi unfriend/unfollow |
|
||||
| `UserBlockedDomainEvent` | Khi block user |
|
||||
| `UserUnblockedDomainEvent` | Khi unblock user |
|
||||
|
||||
## Cấu Hình
|
||||
|
||||
@@ -210,9 +239,8 @@ dotnet test tests/SocialService.UnitTests
|
||||
| Biến | Mô Tả | Mặc định |
|
||||
|------|-------|----------|
|
||||
| `ASPNETCORE_ENVIRONMENT` | Tên môi trường | `Development` |
|
||||
| `DATABASE_URL` | Connection string PostgreSQL | - |
|
||||
| `REDIS_URL` | Connection string Redis | - |
|
||||
| `JWT_SECRET` | Secret ký JWT (tối thiểu 32 ký tự) | - |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | - |
|
||||
| `REDIS_URL` | Redis connection string | - |
|
||||
|
||||
### appsettings.json
|
||||
|
||||
@@ -227,6 +255,19 @@ dotnet test tests/SocialService.UnitTests
|
||||
}
|
||||
```
|
||||
|
||||
## Kiểm Thử
|
||||
|
||||
```bash
|
||||
# Chạy tất cả tests
|
||||
dotnet test
|
||||
|
||||
# Chạy với coverage
|
||||
dotnet test /p:CollectCoverage=true
|
||||
|
||||
# Chạy project test cụ thể
|
||||
dotnet test tests/SocialService.UnitTests
|
||||
```
|
||||
|
||||
## Triển Khai
|
||||
|
||||
### Docker Build
|
||||
@@ -239,26 +280,11 @@ docker build -t socialservice:latest .
|
||||
docker run -p 5000:8080 --env-file .env socialservice:latest
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Xem [ARCHITECTURE.md](./ARCHITECTURE.md) để biết manifests triển khai Kubernetes.
|
||||
|
||||
## Có Gì Mới Trong .NET 10
|
||||
|
||||
- Tính năng ngôn ngữ **C# 14**
|
||||
- Hỗ trợ **Native AOT** được cải thiện
|
||||
- Hiệu suất **async/await** tốt hơn
|
||||
- **JSON serialization** được nâng cao
|
||||
- Cải thiện hiệu suất toàn diện
|
||||
- Hỗ trợ **LTS** 3 năm (đến tháng 11/2028)
|
||||
|
||||
## Tài Nguyên
|
||||
|
||||
- [Tài liệu Kiến trúc](./ARCHITECTURE.md)
|
||||
- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers) - Kiến trúc tham chiếu
|
||||
- [Tài liệu .NET 10](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10)
|
||||
- [DDD với .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)
|
||||
- [MediatR](https://github.com/jbogard/MediatR) - Thư viện CQRS
|
||||
- [FluentValidation](https://docs.fluentvalidation.net/) - Thư viện validation
|
||||
|
||||
## Giấy Phép
|
||||
|
||||
|
||||
Reference in New Issue
Block a user