diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b29acf4..0f61302 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,7 +41,7 @@ jobs: uses: docker/build-push-action@v7 with: context: . - file: Docker/Dockerfile.prod + file: docker/Dockerfile.prod push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 7bb3b2c..2a6f4c9 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -70,7 +70,7 @@ jobs: uses: docker/build-push-action@v7 with: context: . - file: Docker/Dockerfile.test + file: docker/Dockerfile.test push: false load: true tags: socialmedia-test:${{ github.sha }} diff --git a/README.md b/README.md index e16786f..177b276 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Social Media API -A comprehensive Social Media REST API built with ASP.NET Core following Clean Architecture principles. This API provides full social media functionality including user authentication, posts, comments, reactions, following system, and real-time communication. +A comprehensive Social Media REST API built with ASP.NET Core following Clean Architecture, CQRS, and MediatR, separating read and write models using table projections populated through domain events, the Outbox pattern, and background services. This API provides full social media functionality including user authentication, posts, comments, reactions, following system, and real-time communication. -## 🚀 Features +## Features ### Authentication & Authorization @@ -46,52 +46,91 @@ A comprehensive Social Media REST API built with ASP.NET Core following Clean Ar - Connection management for online/offline status - Message status tracking (sent, delivered, read) -## 🏗️ Architecture +## Architecture -The project follows Clean Architecture principles with the following layers: +The project follows Clean Architecture principles and uses physical CQRS with a clear separation between write models (EF Core) and read models (Dapper projections). Write operations are handled via commands, while read operations use optimized projection tables. Domain events, the Outbox pattern, and background services coordinate consistent side effects and populate read models. + +Project layout: ``` src/ ├── SocialMedia.WebApi/ # Presentation Layer │ ├── Controllers/ # API Controllers -│ ├── Hubs/ # SignalR Hubs for real-time communication -│ ├── Program.cs # Application entry point -│ └── appsettings.json # Configuration +│ ├── Hubs/ # SignalR Hubs for real-time communication +│ ├── Middlewares/ # Request pipeline and logging +│ ├── Filters/ # API filters +│ ├── Program.cs # Application entry point +│ └── appsettings.json # Configuration ├── SocialMedia.Application/ # Application Layer -│ ├── Services/ # Business logic services -│ ├── ServiceContracts/ # Service interfaces -│ ├── Dtos/ # Data Transfer Objects -│ └── CustomValidations/ # Custom validation attributes -├── SocialMedia.Core/ # Domain Layer -│ ├── Entities/ # Domain entities -│ ├── Enumerations/ # Domain enums -│ └── RepositoryContracts/ # Repository interfaces -└── SocialMedia.Infrastructure/ # Infrastructure Layer - ├── Database/ # Entity Framework DbContext - ├── Repositories/ # Repository implementations - ├── Auth/ # JWT & Password services - ├── Email/ # Email services - └── FileUploading/ # File upload services +│ ├── Abstractions/ # Cross-cutting contracts +│ ├── Auth/ # Auth commands, queries, and responses +│ ├── Comments/ # Comment commands, queries, and responses +│ ├── Posts/ # Post commands, queries, and responses +│ ├── Reacts/ # React commands, queries, and responses +│ ├── Users/ # User commands, queries, and responses +│ ├── Behaviors/ # MediatR pipeline behaviors +│ ├── Dtos/ # Cross-layer DTOs +│ └── Options/ # Options and configuration models +├── SocialMedia.Core/ # Domain Layer +│ ├── Entities/ # Domain entities +│ ├── Enumerations/ # Domain enums +│ ├── Events/ # Domain events +│ └── RepositoryContracts/ # Repository interfaces +└── SocialMedia.Infrastructure/ # Infrastructure Layer + ├── Database/ # Entity Framework DbContext + ├── Data/ # Dapper read models and projections + ├── Repositories/ # Repository implementations + ├── Auth/ # JWT & Password services + ├── Email/ # Email services + ├── FileUploading/ # File upload services + └── Outbox/ # Outbox processing + +tests/ +├── SocialMedia.Application.UnitTests/ +│ ├── Auth/ +│ ├── Posts/ +│ ├── Reacts/ +│ ├── Users/ +│ └── Comments/ +└── SocialMedia.IntegrationTests/ + ├── Auth/ + ├── Posts/ + ├── Reacts/ + ├── Users/ + └── Comments/ + +Docker/ +├── Dockerfile.dev +├── Dockerfile.prod +└── Dockerfile.test + +docker-compose/ +└── dev.yaml + ``` -## 🛠️ Technology Stack +## Technology Stack -- **Framework**: ASP.NET Core 8.0 +- **Framework**: ASP.NET Core 10.0 - **Database**: SQL Server with Entity Framework Core +- **Read Models**: Dapper - **Authentication**: JWT Bearer tokens - **Real-time Communication**: SignalR - **Email Service**: SMTP with Gmail - **File Storage**: Server storage + Supabase +- **Mediation & Validation**: MediatR + FluentValidation +- **Observability**: Serilog + Seq - **Architecture**: Clean Architecture -- **Patterns**: Repository Pattern, Unit of Work +- **Patterns**: CQRS, Repository Pattern, Outbox, Domain Events +- **Package Management**: Central Package Management (Directory.Packages.props) - **Containerization**: Docker & Docker Compose -## 📋 Prerequisites +## Prerequisites - Docker & Docker Compose - Git -## 🐳 Installation & Deployment +## Installation & Deployment The application is designed to run using Docker Compose, which provides a complete containerized environment with all necessary services. @@ -133,54 +172,44 @@ The application is designed to run using Docker Compose, which provides a comple "Url": "https://your-supabase-url.supabase.co", "Key": "your-supabase-anon-key" }, - "EmailVerificationTokenExpiryMinutes": 30, - "PasswordResetTokenExpiryMinutes": 30 + "durations": { + "EmailVerificationTokenExpiryMinutes": 30, + "PasswordResetTokenExpiryMinutes": 30 + }, + "FileUpload": { + "Provider": "Server" + } } ``` 3. **Run with Docker Compose** ```bash - docker-compose up -d + docker compose -f docker-compose/dev.yaml up -d ``` 4. **Verify Services** - - **API Service**: Available at `http://localhost:5001` - - **SQL Server**: Available at `localhost:1234` - - **Database**: Automatically created and migrated on startup + +- **API Service**: Available at `http://localhost:5039` +- **SQL Server**: Available at `localhost:1234` +- **Seq UI**: Available at `http://localhost:8081` +- **Database**: Automatically created and migrated on startup ### Docker Configuration Details -- **API Port**: 5001 (external) → 5000 (internal) +- **API Port**: 5039 (external) → 5000 (internal) - **Database Port**: 1234 (external) → 1433 (internal) +- **Seq UI Port**: 8081 (external) → 80 (internal) +- **Seq Ingestion Port**: 5341 (external) → 5341 (internal) - **Database Credentials**: SA user with password `YourStrong@Passw0rd` - **Persistent Volumes**: - Database data persisted in `db_data` volume - Uploaded files persisted in `api_wwwroot` volume -- **Environment**: Production environment by default - **Auto-migration**: Database migrations run automatically on container startup -## 📊 Database Schema - -### Core Entities - -- **User**: User profiles with authentication data -- **Post**: User posts with content and metadata -- **Comment**: Comments on posts with nested replies support -- **PostReact**: Reactions on posts -- **CommentReact**: Reactions on comments -- **FollowerFollowing**: Many-to-many relationship for user connections -- **Avatar**: User profile pictures -- **PostAttachment**: File attachments for posts -- **Message**: Real-time chat messages -- **MessageStatus**: Message delivery status tracking -- **Group**: Chat groups for real-time communication -- **UserConnection**: Active SignalR connections for users - -### Enumerations - -- **ReactType**: Like, Love, Laugh, Angry, Sad -- **AttachmentType**: Image, Video, Document -- **StorageProvider**: Server, Supabase -- **MessageStatusType**: Sent, Delivered, Read -- **GroupType**: Private, Group chat types +## Testing & CI + +- **Tests**: Unit and integration coverage across the codebase, with integration tests using real dependencies spun up via Testcontainers. +- **CI/CD**: GitHub Actions pipeline runs unit and integration tests and publishes results. +- **Deploy**: Automated Docker Hub image build and push after successful pipeline runs. +- **Containerization**: Docker + Docker Compose with environment-specific Dockerfiles for development and production. diff --git a/docker-compose/dev.yaml b/docker-compose/dev.yaml index 69affbc..f41b59b 100644 --- a/docker-compose/dev.yaml +++ b/docker-compose/dev.yaml @@ -2,7 +2,7 @@ services: social-api: build: context: ../ - dockerfile: Docker/Dockerfile.dev + dockerfile: docker/Dockerfile.dev container_name: SocialMedia.API ports: - 5039:5000 diff --git a/Docker/Dockerfile.dev b/docker/Dockerfile.dev similarity index 100% rename from Docker/Dockerfile.dev rename to docker/Dockerfile.dev diff --git a/Docker/Dockerfile.prod b/docker/Dockerfile.prod similarity index 100% rename from Docker/Dockerfile.prod rename to docker/Dockerfile.prod diff --git a/Docker/Dockerfile.test b/docker/Dockerfile.test similarity index 100% rename from Docker/Dockerfile.test rename to docker/Dockerfile.test diff --git a/src/SocialMedia.Application/CustomValidations/EnumValueAttribute.cs b/src/SocialMedia.Application/CustomValidations/EnumValueAttribute.cs deleted file mode 100644 index bfc7a79..0000000 --- a/src/SocialMedia.Application/CustomValidations/EnumValueAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace SocialMedia.Application.CustomValidations; - -public class EnumValueAttribute : ValidationAttribute -{ - - private Type _enumType; - private bool _isRequired; - public EnumValueAttribute(Type enumType, bool isRequired = false) - { - _enumType = enumType; - _isRequired = isRequired; - } - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) - { - if (value is not null) - { - if (Enum.IsDefined(_enumType, value)) - { - return ValidationResult.Success; - } - return new ValidationResult(ErrorMessage ?? $"Invlaid {validationContext.MemberName} field value"); - } - if (_isRequired) - { - return new ValidationResult(ErrorMessage ?? $"{validationContext.MemberName} field is required"); - } - return ValidationResult.Success; - } - -} diff --git a/src/SocialMedia.WebApi/Controllers/Reacts/Requests/ReactToCommentRequest.cs b/src/SocialMedia.WebApi/Controllers/Reacts/Requests/ReactToCommentRequest.cs index 91eb871..d535052 100644 --- a/src/SocialMedia.WebApi/Controllers/Reacts/Requests/ReactToCommentRequest.cs +++ b/src/SocialMedia.WebApi/Controllers/Reacts/Requests/ReactToCommentRequest.cs @@ -1,10 +1,8 @@ -using SocialMedia.Application.CustomValidations; using SocialMedia.Core.Enumerations; namespace SocialMedia.WebApi.Controllers.Reacts.Requests; public class ReactToCommentRequest { - [EnumValue(typeof(ReactType), true, ErrorMessage = "React type is required and valid values are from (1 - 5).")] public ReactType ReactType { get; set; } } diff --git a/src/SocialMedia.WebApi/Controllers/Reacts/Requests/ReactToPostRequest.cs b/src/SocialMedia.WebApi/Controllers/Reacts/Requests/ReactToPostRequest.cs index d592c95..078eb6a 100644 --- a/src/SocialMedia.WebApi/Controllers/Reacts/Requests/ReactToPostRequest.cs +++ b/src/SocialMedia.WebApi/Controllers/Reacts/Requests/ReactToPostRequest.cs @@ -1,10 +1,8 @@ -using SocialMedia.Application.CustomValidations; using SocialMedia.Core.Enumerations; namespace SocialMedia.WebApi.Controllers.Reacts.Requests; public class ReactToPostRequest { - [EnumValue(typeof(ReactType), true, ErrorMessage = "React type is required and valid values are from (1 - 5).")] public ReactType ReactType { get; set; } } \ No newline at end of file diff --git a/src/SocialMedia.WebApi/Hubs/ChatHub.cs b/src/SocialMedia.WebApi/Hubs/ChatHub.cs index fcb937d..5c68c30 100644 --- a/src/SocialMedia.WebApi/Hubs/ChatHub.cs +++ b/src/SocialMedia.WebApi/Hubs/ChatHub.cs @@ -10,7 +10,8 @@ namespace SocialMedia.WebApi.Hubs; [SignalRHub] -public class ChatHub : Hub +[Authorize] +public class ChatHub : Hub { private readonly IUnitOfWork _unitOfWork; private readonly IUserService _userService; @@ -39,7 +40,7 @@ public override async Task OnConnectedAsync() { await Groups.AddToGroupAsync(Context.ConnectionId, group.Id.ToString()); if (userFirstConnection) - await Clients.Group(group.Id.ToString()).SendAsync("DeliveredMessages", new DeliveredMessagesDto() + await Clients.Group(group.Id.ToString()).DeliveredMessages(new DeliveredMessagesDto() { GroudId = group.Id, RecieverId = user.Id @@ -99,7 +100,7 @@ public async Task SendFirstDirectMessage(SendFirstDirectMessageDto SendFirstDire public async Task ReadMessagesInGroup(ReadMessagesInGroupDto readMessagesInGroupDto) { await _unitOfWork.Users.UpdateDeliveredMessagesToSeen(readMessagesInGroupDto.RecieverId, readMessagesInGroupDto.GroupId); - await Clients.Group(readMessagesInGroupDto.GroupId.ToString()).SendAsync("SeenMessages", readMessagesInGroupDto); + await Clients.Group(readMessagesInGroupDto.GroupId.ToString()).SeenMessages(readMessagesInGroupDto); } private async Task _saveAndBroadcastMessage(Group group, UserDto tokenUser, string message) @@ -123,7 +124,7 @@ private async Task _saveAndBroadcastMessage(Group group, UserDto tokenUser, stri } group.Messages.Add(msg); await _unitOfWork.SaveChangesAsync(); - await Clients.Groups(group.Id.ToString()).SendAsync("NewMessage", new MessageDto + await Clients.Groups(group.Id.ToString()).NewMessage(new MessageDto { Id = msg.Id, GroupId = group.Id, diff --git a/src/SocialMedia.WebApi/Hubs/IChatClient.cs b/src/SocialMedia.WebApi/Hubs/IChatClient.cs new file mode 100644 index 0000000..0275082 --- /dev/null +++ b/src/SocialMedia.WebApi/Hubs/IChatClient.cs @@ -0,0 +1,10 @@ +using SocialMedia.Application.Dtos; + +namespace SocialMedia.WebApi.Hubs; + +public interface IChatClient +{ + Task DeliveredMessages(DeliveredMessagesDto deliveredMessagesDto); + Task SeenMessages(ReadMessagesInGroupDto readMessagesInGroupDto); + Task NewMessage(MessageDto messageDto); +}