Skip to content

About

A modular RESTful API with ASP.NET Core 8, following Clean Architecture principles

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

E-Learning Platform

A fully-featured e-learning backend built with ASP.NET Core Web API, following Clean Architecture principles. Includes course & chapter/lesson management, cart, enrollments, quizzes & question banks, quiz attempts with auto-grading, and payment integration via Paymob.


🏗️ Architecture

This project follows Clean Architecture, separating concerns into distinct layers:

├── E-Learning.API             → Presentation Layer
│   ├── Controllers, Middleware, Program.cs
│   └── Extensions
│
├── E-Learning.Application     → Use Cases Layer
│   ├── Auth, Account, Courses, Chapters, Lessons
│   ├── Cart, Enrollments, Learn, Payment
│   ├── Quizzes, Questions, QuizAttempts
│   └── Extensions
│
├── E-Learning.Core            → Domain Layer
│   ├── Models, Repositories (Interfaces), Services (Interfaces)
│   ├── Shared, Specifications
│   └── (No dependency on any other layer)
│
└── E-Learning.Infrastructure  → Infrastructure Layer
    ├── Data (DbContext, Migrations, Seeding)
    ├── Repositories, Services (Paymob, Cloudinary, Local File Storage, Email)
    └── Extensions

Key Architectural Patterns

  • Clean Architecture: Separation of concerns with dependency inversion — outer layers depend on inner layers, never the reverse
  • CQRS (Command Query Responsibility Segregation): Separate models for read and write operations, keeping queries and commands isolated per module
  • Repository Pattern: Abstracted data access layer with generic and specific repositories defined in E-Learning.Core and implemented in E-Learning.Infrastructure
  • Specification Pattern : Encapsulated query logic in E-Learning.Core/Specifications for flexible and reusable filtering
  • Unit of Work: Coordinates multiple repository operations within a single transaction
  • Role-based Authorization: Distinct endpoint groups for anonymous visitors, authenticated students, instructors, and admins

⚙️ Tech Stack

Layer Technology
Framework ASP.NET Core Web API
ORM Entity Framework Core
Database SQL Server
Authentication ASP.NET Identity + JWT + Refresh Tokens
External Login Google OAuth
Media Storage Local server storage (images) + Cloudinary (videos)
Logging Serilog (Console + File sinks)
Payment Gateway Paymob
API Docs APIDog / Swagger

🗄️ Database Schema

erDiagram
    Users {
        string Id PK
        string FirstName
        string LastName
        string ImagePath
        string Qualifications
        string IntroductionBrief
        boolean IsDeleted
        string Email
        string EmailConfirmationCode
    }

    Roles {
        int Id PK
        string Name
    }

    UserRoles {
        int RoleId FK
        string UserId FK
    }

    RefreshTokens {
        bigint Id PK
        string UserId FK
        string Token
        datetime ExpirationOn
        datetime CreatedOn
        datetime RevokedOn
    }

    InstructorRequests {
        int Id PK
        string UserId FK
        string Status
        datetime CreatedAt
        datetime ApprovedAt
        datetime RejectedAt
        string RejectionReason
    }

    Courses {
        int Id PK
        string InstructorId FK
        string Title
        string Description
        decimal Price
        decimal DiscountPrice
        string ThumbnailPath
        string Prerequisites
        datetime CreatedAt
        boolean IsDeleted
        boolean IsPublished
        datetime UpdatedAt
    }

    Chapters {
        int Id PK
        int CourseId FK
        string Title
        string Description
        string ImagePath
        datetime CreatedAt
        datetime UpdatedAt
        int OrderNumber
    }

    Lessons {
        int Id PK
        int ChapterId FK
        string Title
        string ContentData
        string VideoUrl
        float Duration
        boolean IsMandatory
        boolean IsFree
        datetime CreatedAt
        datetime UpdatedAt
        int OrderNumber
    }

    Carts {
        int Id PK
        string UserId FK
        datetime CreatedAt
        datetime UpdatedAt
    }

    CartItems {
        int Id PK
        int CartId FK
        int CourseId FK
        datetime AddedAt
    }

    Enrollments {
        int Id PK
        string StudentId FK
        int CourseId FK
        datetime EnrollmentDate
        datetime LastAccessed
        string Status
        boolean IsPaid
        boolean IsActive
        datetime UnenrollmentDate
        datetime CompletionDate
    }

    LearningProgresses {
        int Id PK
        int EnrollmentId FK
        int LessonId FK
        string Status
        datetime BeginDate
        datetime CompletionDate
        boolean IsCompleted
        datetime LastAccessed
    }

    Quizzes {
        int Id PK
        int LessonId FK
        string Title
        string Description
        float Duration
        boolean IsActive
        datetime CreatedAt
        datetime UpdatedAt
    }

    QuestionTypes {
        int Id PK
        string TypeName
    }

    Questions {
        int Id PK
        int QuizId FK
        int QuestionTypeId FK
        string QuestionText
        decimal Points
        int OrderNumber
    }

    QuestionOptions {
        int Id PK
        int QuestionId FK
        string OptionText
        boolean IsCorrect
    }

    QuizAttempts {
        int Id PK
        int QuizId FK
        string StudentId FK
        int EnrollmentId FK
        datetime StartTime
        datetime EndTime
        datetime SubmissionTime
        string Status
        decimal TotalPossibleScore
        decimal Score
    }

    StudentAnswers {
        int Id PK
        string StudentId FK
        int QuestionId FK
        int QuizAttemptId FK
        int SelectedOptionId FK
        boolean IsCorrect
    }

    Payments {
        int Id PK
        string StudentId FK
        datetime PaymentDate
        string PaymentMethod
        decimal Amount
        string Status
        int TransactionId
    }

    PaymentItems {
        int Id PK
        int PaymentId FK
        int CourseId FK
    }

    Users ||--o{ RefreshTokens : "has many"
    Users ||--o{ InstructorRequests : "has many"
    Users ||--o{ Courses : "instructs many"
    Users ||--o{ Carts : "has one"
    Users ||--o{ Enrollments : "enrolls in many"
    Users ||--o{ QuizAttempts : "attempts many"
    Users ||--o{ StudentAnswers : "answers many"
    Users }o--o{ Roles : "UserRoles"

    Courses ||--o{ Chapters : "has many"
    Courses ||--o{ CartItems : "referenced in"
    Courses ||--o{ Enrollments : "has many"
    Courses ||--o{ PaymentItems : "referenced in"

    Chapters ||--o{ Lessons : "has many"

    Lessons ||--o{ LearningProgresses : "tracked in"
    Lessons ||--o| Quizzes : "has one"

    Carts ||--o{ CartItems : "contains"

    Enrollments ||--o{ LearningProgresses : "has many"
    Enrollments ||--o{ QuizAttempts : "has many"

    Quizzes ||--o{ Questions : "has many"
    Quizzes ||--o{ QuizAttempts : "attempted many"

    QuestionTypes ||--o{ Questions : "categorizes"
    Questions ||--o{ QuestionOptions : "has many"
    Questions ||--o{ StudentAnswers : "answered in"

    QuizAttempts ||--o{ StudentAnswers : "contains"
    QuestionOptions ||--o{ StudentAnswers : "selected in"

    Payments ||--o{ PaymentItems : "contains"
Loading

Entity Relationships

  • Users → Courses: One-to-Many (an instructor owns many courses)
  • Users → Enrollments: One-to-Many (a student enrolls in many courses; unique per student/course)
  • Courses → Chapters → Lessons: Nested one-to-many hierarchy, each with an OrderNumber for sequencing
  • Lessons → Quizzes: One-to-One (a lesson may have a single quiz)
  • Quizzes → Questions → QuestionOptions: One-to-Many chains for building quiz content
  • Enrollments → LearningProgresses: One-to-Many (progress is tracked per lesson, per enrollment)
  • Enrollments → QuizAttempts → StudentAnswers: Tracks each quiz attempt and the answers submitted
  • Carts → CartItems: One-to-Many (a user's cart holds multiple courses before checkout)
  • Payments → PaymentItems: One-to-Many (a single payment can cover multiple courses)
  • Users ↔ Roles: Many-to-Many via UserRoles (ASP.NET Identity roles: Student, Instructor, Admin)

🔐 Authentication Flow

  • Register → Email Confirmation → Login → JWT Access Token + Refresh Token
  • Google OAuth login supported (redirect-based and ID-token-based)
  • Access tokens are short-lived; refresh tokens allow silent re-authentication
  • Refresh tokens can be explicitly revoked
  • Password reset via email token
  • Users can request to become an Instructor; admins review, approve, or reject the request

🖼️ Media Storage

Media is split across two storage backends depending on type:

  • Local server storage — images are saved on the API server's file system:
    • Profile images (/account/image)
    • Course thumbnails (/instructor/courses/{courseId}/thumbnail)
    • Chapter images (/instructor/courses/{courseId}/chapters/{chapterId}/image)
  • Cloudinary — lesson videos are uploaded to Cloudinary:
    • Lesson videos (/instructor/courses/{courseId}/lessons/{lessonId}/video)

On upload, the resulting path/URL is persisted on the entity (e.g. ImagePath, ThumbnailPath as a local path; VideoUrl as a Cloudinary secure URL). On delete, the corresponding file is removed from its respective storage — local disk for images, Cloudinary for videos — to avoid orphaned files.


💳 Payment Integration

This project integrates with Paymob — an Egyptian payment gateway supporting cards, mobile wallets, and more.

A payment intention is created for the items in the student's cart, returning a checkout/redirect URL. Paymob confirms the transaction result via a callback, which is verified using HMAC SHA512 signature verification before enrollments are activated and payment status is updated.

Webhook Setup (Required for local development)

Paymob communicates payment results back to your API via webhooks. Since localhost is not publicly accessible, you need to expose your local server using ngrok.

1. Install and run ngrok:

ngrok http 5028

This gives you a public HTTPS URL like https://xxxx-xxxx.ngrok-free.app.

2. Configure webhooks in the Paymob dashboard:

Go to https://eg.dashboard.paymob.com/payment-integrations select the payment method and fill in the following fields using your ngrok URL:

Field Value
Webhook URL https://your-ngrok-url.ngrok-free.app/api/payment/callback-server
Redirect URL https://your-ngrok-url.ngrok-free.app/api/payment/callback-page

📡 API Endpoints

Full interactive docs: APIDog Documentation

🔑 Auth

Method Endpoint Description
POST /auth/register Register a new user
POST /auth/confirmEmail Confirm email address
POST /auth/resendConfirmationEmail Resend confirmation email
POST /auth/login Login and receive tokens
GET /auth/googleLogin Login with Google (redirect flow)
POST /auth/googleTokenLogin Login with Google ID token
POST /auth/forgetPassword Request password reset
POST /auth/resetPassword Reset password with token
POST /auth/refreshToken Refresh access token
POST /auth/revokeRefreshToken Revoke refresh token

👤 Account

Method Endpoint Description
GET /account Get current user profile
PUT /account Update user profile
DELETE /account Delete user account
PUT /account/changePassword Change password
POST /account/image Upload profile image
DELETE /account/image Delete profile image
POST /account/instructorRequest Request to become an instructor
GET /admin/account/users Get all users (Admin)
DELETE /admin/account/users/{userId} Delete user (Admin)
POST /admin/account/users/{userId}/roles/{roleName} Add user to role (Admin)
DELETE /admin/account/users/{userId}/roles/{roleName} Remove user from role (Admin)
GET /admin/account/instructorRequests Get all instructor requests (Admin)
PATCH /admin/account/approveInstructor Approve instructor request (Admin)
PATCH /admin/account/rejectInstructor Reject instructor request (Admin)

📚 Courses

Method Endpoint Description
GET /courses Get all published courses (Public)
GET /courses/{courseId} Get course by ID (Public)
GET /instructor/courses Get instructor's own courses
GET /instructor/courses/{courseId} Get instructor's course by ID
POST /instructor/courses Create a course
PUT /instructor/courses/{courseId} Update course info
PATCH /instructor/courses/{courseId}/publish Publish course
PATCH /instructor/courses/{courseId}/unpublish Unpublish course
DELETE /instructor/courses/{courseId} Delete course
POST /instructor/courses/{courseId}/thumbnail Upload course thumbnail
DELETE /instructor/courses/{courseId}/thumbnail Delete course thumbnail
GET /admin/courses Get all courses (Admin)
GET /admin/courses/{courseId} Get course by ID (Admin)
DELETE /admin/courses/{courseId} Soft-delete course (Admin)
PATCH /admin/courses/{courseId}/restore Restore a soft-deleted course (Admin)

📖 Chapters

Method Endpoint Description
GET /courses/{courseId}/chapters Get all chapters of a course (Public)
GET /courses/{courseId}/chapters/{chapterId} Get a chapter (Public)
GET /instructor/courses/{courseId}/chapters Get all chapters (Instructor)
GET /instructor/courses/{courseId}/chapters/{chapterId} Get a chapter (Instructor)
POST /instructor/courses/{courseId}/chapters Create a chapter
PUT /instructor/courses/{courseId}/chapters/{chapterId} Update a chapter
PATCH /instructor/courses/{courseId}/chapters/reorder Reorder chapters
DELETE /instructor/courses/{courseId}/chapters/{chapterId} Delete a chapter
POST /instructor/courses/{courseId}/chapters/{chapterId}/image Upload chapter image
DELETE /instructor/courses/{courseId}/chapters/{chapterId}/image Delete chapter image
GET /admin/courses/{courseId}/chapters Get all chapters (Admin)
GET /admin/courses/{courseId}/chapters/{chapterId} Get a chapter (Admin)

🎬 Lessons

Method Endpoint Description
GET /courses/{courseId}/lessons Get all lessons of a course (Public)
GET /courses/{courseId}/lessons/{lessonId} Get a lesson (Public)
GET /instructor/courses/{courseId}/lessons Get all lessons (Instructor)
GET /instructor/courses/{courseId}/lessons/{lessonId} Get a lesson (Instructor)
POST /instructor/courses/{courseId}/chapters/{chapterId}/lessons Create a lesson in a chapter
PUT /instructor/courses/{courseId}/lessons/{lessonId} Update a lesson
DELETE /instructor/courses/{courseId}/lessons/{lessonId} Delete a lesson
POST /instructor/courses/{courseId}/lessons/{lessonId}/video Upload lesson video
DELETE /instructor/courses/{courseId}/lessons/{lessonId}/video Delete lesson video
PATCH /instructor/courses/{courseId}/chapters/{chapterId}/lessons/reorder Reorder lessons in a chapter
GET /admin/courses/{courseId}/lessons Get all lessons (Admin)
GET /admin/courses/{courseId}/lessons/{lessonId} Get a lesson (Admin)

🧺 Cart

Method Endpoint Description
GET /cart Get current user's cart
POST /cart Add a course to the cart
DELETE /cart Remove an item from the cart

💳 Payment

Method Endpoint Description
POST /payment/payment-intention Create a Paymob payment intention for checkout
GET /payment/history Get current user's payment history

🎓 Enrollment

Method Endpoint Description
POST /enrollments/enroll Enroll in a course
PATCH /enrollments/unenroll Unenroll from a course
GET /enrollments/{courseId}/details Get details of an enrolled course
GET /enrollments/my-courses Get current student's enrolled courses
GET /instructor/enrollments/courses/{courseId}/students Get students enrolled in a course (Instructor)

▶️ Learn

Method Endpoint Description
GET /courses/{courseId}/learn/lessons/{lessonId} Get lesson content for an enrolled student
PATCH /courses/{courseId}/learn/lessons/{lessonId}/complete Mark a lesson as complete

📝 Quiz

Method Endpoint Description
POST /instructor/courses/{courseId}/lessons/{lessonId}/quiz Create a quiz
GET /instructor/courses/{courseId}/lessons/{lessonId}/quiz Get a quiz
PUT /instructor/courses/{courseId}/lessons/{lessonId}/quiz Update quiz info
DELETE /instructor/courses/{courseId}/lessons/{lessonId}/quiz Delete a quiz
PATCH /instructor/courses/{courseId}/lessons/{lessonId}/quiz/activate Activate a quiz
PATCH /instructor/courses/{courseId}/lessons/{lessonId}/quiz/deactivate Deactivate a quiz

❓ Question

Method Endpoint Description
GET /questions/question-types Get all question types
POST /instructor/courses/{courseId}/lessons/{lessonId}/quiz/questions Create a question
GET /instructor/courses/{courseId}/lessons/{lessonId}/quiz/questions/{questionId} Get a question
PUT /instructor/courses/{courseId}/lessons/{lessonId}/quiz/questions/{questionId} Update a question
DELETE /instructor/courses/{courseId}/lessons/{lessonId}/quiz/questions/{questionId} Delete a question
PATCH /instructor/courses/{courseId}/lessons/{lessonId}/quiz/questions/reorder Reorder questions

🧩 Quiz Attempt

Method Endpoint Description
POST /courses/{courseId}/learn/lessons/{lessonId}/quiz/{quizId}/start Start a quiz attempt
GET /courses/{courseId}/learn/lessons/{lessonId}/quiz/attempt/{quizAttemptId} Get the active quiz attempt
POST /courses/{courseId}/learn/lessons/{lessonId}/quiz/submit/{quizAttemptId} Submit a quiz attempt
GET /courses/{courseId}/learn/lessons/{lessonId}/quiz/result/{quizAttemptId} Get quiz attempt result
GET /courses/{courseId}/learn/lessons/{lessonId}/quiz/{quizId}/history Get quiz attempts history

🚀 Getting Started

Prerequisites

1. Clone the repository

2. Configure settings

Add the following configuration to your appsettings.json and fill in your values:

{
  "JwtOptions": {
    "SecretKey": "your-secret-key",
    "Issuer": "your-issuer",
    "Audience": "your-audience",
    "ExpirationInMinutes": 60,
    "ExpirationInDays": 1
  },
  "EmailSettings": {
    "SenderEmail": "your-email@gmail.com",
    "Password": "your-app-password",
    "DisplayName": "E-Learning",
    "Host": "smtp.gmail.com",
    "Port": 587
  },
  "Google": {
    "ClientId": "your-google-client-id",
    "ClientSecret": "your-google-client-secret"
  },
  "CloudinarySettings": {
    "CloudName": "your-cloudinary-cloud-name",
    "ApiKey": "your-cloudinary-cloud-api-key",
    "ApiSecret": "your-cloudinary-secret-key"
  },
  "PaymobOptions": {
    "APIKey": "your-paymob-api-key",
    "PublicKey": "your-paymob-public-key",
    "SecretKey": "your-paymob-secret-key",
    "HMAC": "your-paymob-hmac-secret",
    "CardIntegrationId": "your-integration-id"
  },
  "ConnectionStrings": {
    "DefaultConnection": "#{DB_CONNECTION_STRING}#"
  }
}

⚠️ Never commit real credentials to source control. Use environment variables or a secrets manager for sensitive values.

3. Run the project

✅ No need to apply migrations manually. The application automatically applies any pending migrations on startup.

dotnet run --project E-Learning.API

The API will be available at http://localhost:5028. Swagger UI: http://localhost:5028/swagger/index.html


About

A modular RESTful API with ASP.NET Core 8, following Clean Architecture principles

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages