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.
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
- 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.Coreand implemented inE-Learning.Infrastructure - Specification Pattern : Encapsulated query logic in
E-Learning.Core/Specificationsfor 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
| 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 |
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"
- 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
OrderNumberfor 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)
- 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 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)
- Profile images (
- Cloudinary — lesson videos are uploaded to Cloudinary:
- Lesson videos (
/instructor/courses/{courseId}/lessons/{lessonId}/video)
- Lesson videos (
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.
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.
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 5028This 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 |
Full interactive docs: APIDog Documentation
| 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 |
| 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) |
| 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) |
| 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) |
| 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) |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
| POST | /payment/payment-intention |
Create a Paymob payment intention for checkout |
| GET | /payment/history |
Get current user's payment history |
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
- .NET SDK
- SQL Server
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.
✅ No need to apply migrations manually. The application automatically applies any pending migrations on startup.
dotnet run --project E-Learning.APIThe API will be available at http://localhost:5028.
Swagger UI: http://localhost:5028/swagger/index.html