Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
145 changes: 87 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docker-compose/dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ services:
social-api:
build:
context: ../
dockerfile: Docker/Dockerfile.dev
dockerfile: docker/Dockerfile.dev
container_name: SocialMedia.API
ports:
- 5039:5000
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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; }
}
9 changes: 5 additions & 4 deletions src/SocialMedia.WebApi/Hubs/ChatHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
namespace SocialMedia.WebApi.Hubs;

[SignalRHub]
public class ChatHub : Hub
[Authorize]
public class ChatHub : Hub<IChatClient>
{
private readonly IUnitOfWork _unitOfWork;
private readonly IUserService _userService;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions src/SocialMedia.WebApi/Hubs/IChatClient.cs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading