Mentor Feature Implementation Guide

Mentor Feature Implementation Guide

Course: ITI Graduation Project - Career Route Platform
Feature: User Story 1 - Mentor Profile Management
Tasks Covered: T029, T036, T039, T044, T047, T053, T056, T059, T060
Architecture: 3-Layer Clean Architecture (API → Core → Infrastructure)


Table of Contents

  1. Architecture Overview
  2. Implementation Roadmap
  3. Task T029: Create Mentor Entity
  4. Task T036: Create Mentor DTOs
  5. Task T039: Configure Entity Mapping
  6. Task T044: Create Repository Interface
  7. Task T047: Implement Repository
  8. Task T053: Create and Implement Service
  9. Task T056: Create Validator
  10. Task T059: Implement Controller
  11. Task T060: Create AutoMapper Profile
  12. Testing Your Implementation
  13. Common Issues and Solutions

Architecture Overview

Understanding 3-Layer Clean Architecture

Our project follows a 3-layer clean architecture pattern, which separates concerns and makes the code maintainable and testable.

┌─────────────────────────────────────────────────────┐
│         CareerRoute.API (Presentation Layer)        │
│  - Controllers (HTTP endpoints)                     │
│  - Middleware (logging, exception handling)         │
│  - Filters (authorization)                          │
└──────────────────┬──────────────────────────────────┘
                   │ Depends on ↓
┌─────────────────────────────────────────────────────┐
│         CareerRoute.Core (Domain + Application)     │
│  - Entities (domain models)                         │
│  - DTOs (data transfer objects)                     │
│  - Interfaces (contracts)                           │
│  - Services (business logic)                        │
│  - Validators (input validation)                    │
│  - Enums (constants)                                │
└──────────────────┬──────────────────────────────────┘
                   │ Depends on ↓
┌─────────────────────────────────────────────────────┐
│     CareerRoute.Infrastructure (Data Layer)         │
│  - DbContext (database access)                      │
│  - Repositories (data operations)                   │
│  - Migrations (database schema)                     │
│  - External Services (email, storage, etc.)         │
└─────────────────────────────────────────────────────┘

Key Principles:

  • Dependency Flow: API → Core → Infrastructure (one direction only)
  • Core is independent and contains business logic
  • Infrastructure implements interfaces defined in Core
  • API orchestrates everything through controllers

Implementation Roadmap

We'll implement the Mentor feature in this order:

graph TD
    A[Step 1: Create Mentor Entity] --> B[Step 2: Create DTOs]
    B --> C[Step 3: Configure EF Core Mapping]
    C --> D[Step 4: Create Repository Interface]
    D --> E[Step 5: Implement Repository]
    E --> F[Step 6: Create Service Interface & Implementation]
    F --> G[Step 7: Create Validator]
    G --> H[Step 8: Create Controller]
    H --> I[Step 9: Configure AutoMapper]
    I --> J[Step 10: Test]

Why this order?

  • Start with domain models (entities) - the foundation of your application
  • Create DTOs to define data contracts for APIs
  • Configure database mappings so EF Core knows how to persist entities
  • Build repository to abstract data access
  • Implement business logic in services
  • Add validation to ensure data integrity
  • Expose HTTP endpoints through controllers
  • Wire everything with AutoMapper for object transformations

Task T029: Create Mentor Entity

What is an Entity?

An entity represents a real-world business object that has an identity and is stored in the database. In our case, a Mentor is a user who provides mentorship services.

Understanding the Mentor Entity

According to the data model:

  • Mentor has a one-to-one relationship with User (a Mentor IS a User)
  • Mentor.Id is a foreign key pointing to User.Id
  • Contains mentor-specific fields: Bio, Expertise, Rates, Ratings, etc.

Implementation Steps

Location: Backend/CareerRoute.Core/Domain/Entities/Mentor.cs

Step 1: Create the Mentor Entity Class

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace CareerRoute.Core.Domain.Entities
{
    /// <summary>
    /// Represents a mentor in the system.
    /// A mentor is a user who has been approved to provide mentorship services.
    /// </summary>
    public class Mentor
    {
        // ============ PRIMARY KEY ============

        /// <summary>
        /// Primary Key and Foreign Key to ApplicationUser.
        /// This creates a one-to-one relationship: one User can be one Mentor.
        /// </summary>
        [Key]
        [ForeignKey(nameof(User))]
        public string Id { get; set; } = string.Empty;

        // ============ NAVIGATION PROPERTY ============

        /// <summary>
        /// Navigation property to the related User entity.
        /// This allows you to access mentor.User.Email, mentor.User.FirstName, etc.
        /// </summary>
        public virtual ApplicationUser User { get; set; } = null!;

        // ============ PROFILE INFORMATION ============

        /// <summary>
        /// Mentor's professional biography.
        /// This is displayed on their profile to attract mentees.
        /// </summary>
        [MaxLength(2000)]
        public string? Bio { get; set; }

        /// <summary>
        /// Comma-separated list of expertise areas (e.g., "C#, ASP.NET, Azure").
        /// Later we can enhance this to a separate ExpertiseTag entity.
        /// </summary>
        [MaxLength(500)]
        public string? ExpertiseTags { get; set; }

        /// <summary>
        /// Number of years of professional experience.
        /// Used for filtering and displaying credibility.
        /// </summary>
        public int? YearsOfExperience { get; set; }

        /// <summary>
        /// Mentor's certifications (e.g., "Microsoft Certified, AWS Solutions Architect").
        /// Can be enhanced to a separate Certification entity.
        /// </summary>
        [MaxLength(1000)]
        public string? Certifications { get; set; }

        // ============ PRICING ============

        /// <summary>
        /// Hourly rate for 30-minute sessions in USD.
        /// Example: 25.00 means $25 for 30 minutes.
        /// </summary>
        [Column(TypeName = "decimal(18,2)")]
        public decimal Rate30Min { get; set; }

        /// <summary>
        /// Hourly rate for 60-minute sessions in USD.
        /// Usually discounted compared to 2x the 30-min rate.
        /// </summary>
        [Column(TypeName = "decimal(18,2)")]
        public decimal Rate60Min { get; set; }

        // ============ RATING & STATISTICS ============

        /// <summary>
        /// Average rating calculated from all reviews (1-5 scale).
        /// This is computed and updated when reviews are submitted.
        /// </summary>
        [Column(TypeName = "decimal(3,2)")]
        public decimal AverageRating { get; set; } = 0;

        /// <summary>
        /// Total number of reviews received.
        /// Used to display social proof (e.g., "4.8 stars from 127 reviews").
        /// </summary>
        public int TotalReviews { get; set; } = 0;

        /// <summary>
        /// Total number of sessions this mentor has completed.
        /// Used for sorting by popularity and displaying experience.
        /// </summary>
        public int TotalSessionsCompleted { get; set; } = 0;

        // ============ APPROVAL STATUS ============

        /// <summary>
        /// Indicates if the mentor has been verified by admins.
        /// Verified mentors appear higher in search results.
        /// </summary>
        public bool IsVerified { get; set; } = false;

        /// <summary>
        /// Admin approval status: Pending, Approved, or Rejected.
        /// New mentor applications start as Pending.
        /// </summary>
        [MaxLength(20)]
        public string ApprovalStatus { get; set; } = "Pending";

        // ============ TIMESTAMPS ============

        /// <summary>
        /// When the mentor profile was created (application submitted).
        /// </summary>
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

        /// <summary>
        /// When the mentor profile was last updated.
        /// </summary>
        public DateTime? UpdatedAt { get; set; }

        // ============ FUTURE RELATIONSHIPS ============
        // These will be added in later tasks:
        // - public virtual ICollection<Session> Sessions { get; set; }
        // - public virtual ICollection<Review> Reviews { get; set; }
        // - public virtual ICollection<MentorCategory> MentorCategories { get; set; }
    }
}

Understanding the Code

1. Why string Id instead of int Id?

  • ASP.NET Identity uses string for user IDs (GUIDs)
  • Mentor.Id must match User.Id type for the foreign key relationship

2. What is [ForeignKey(nameof(User))]?

  • This tells EF Core that Id is a foreign key pointing to the User navigation property
  • Creates a one-to-one relationship: when you delete a User, the Mentor is also deleted (cascade)

3. Why virtual on navigation properties?

  • Enables lazy loading: EF Core can load related entities on-demand
  • Example: var userName = mentor.User.FirstName; automatically loads the User if not loaded yet

4. Why required or = null!?

  • C# 11+ nullable reference types require initialization
  • = null! tells the compiler "this will be set by EF Core, don't worry"

5. Why [MaxLength] attributes?

  • Defines database column sizes
  • Prevents users from submitting gigantic strings
  • Database will create nvarchar(2000) instead of nvarchar(MAX)

6. Why decimal(18,2) for prices?

  • decimal is precise for money (unlike float which has rounding errors)
  • 18,2 means 18 total digits, 2 after decimal point (e.g., 999999999999999.99)

7. Why DateTime.UtcNow instead of DateTime.Now?

  • Always use UTC for server timestamps
  • Prevents timezone confusion when users are in different countries
  • Frontend can convert to local time for display

Summary Checklist

  • <input type="checkbox" disabled=""> Create Mentor.cs in CareerRoute.Core/Domain/Entities/
  • <input type="checkbox" disabled=""> Define all properties according to data model
  • <input type="checkbox" disabled=""> Add XML comments for documentation
  • <input type="checkbox" disabled=""> Use proper data annotations ([Key], [MaxLength], etc.)
  • <input type="checkbox" disabled=""> Include navigation property to ApplicationUser
  • <input type="checkbox" disabled=""> Set default values for rating fields
  • <input type="checkbox" disabled=""> Use UTC timestamps

Task T036: Create Mentor DTOs

What are DTOs?

DTO = Data Transfer Object. DTOs are objects used to transfer data between layers (API ↔ Service ↔ Database).

Why not use Entities directly in API?

  • Security: Entities may contain sensitive fields you don't want to expose
  • Flexibility: API contracts can differ from database structure
  • Versioning: You can change entities without breaking API contracts
  • Performance: DTOs can be optimized for specific use cases (fewer fields)

Understanding DTOs for Mentor

We need two DTOs:

  1. MentorProfileDto: For reading mentor data (GET requests)
  2. UpdateMentorProfileDto: For updating mentor data (PUT requests)

Implementation Steps

Location: Backend/CareerRoute.Core/DTOs/Mentors/

Step 1: Create MentorProfileDto (Read DTO)

namespace CareerRoute.Core.DTOs.Mentors
{
    /// <summary>
    /// DTO for returning mentor profile information to clients.
    /// Used in GET requests to display mentor details.
    /// </summary>
    public class MentorProfileDto
    {
        // ============ IDENTITY ============

        /// <summary>
        /// Mentor's unique identifier (matches User.Id).
        /// </summary>
        public string Id { get; set; } = string.Empty;

        // ============ USER INFORMATION ============
        // We include user info here so clients don't need a separate request

        /// <summary>
        /// Mentor's first name (from User entity).
        /// </summary>
        public string FirstName { get; set; } = string.Empty;

        /// <summary>
        /// Mentor's last name (from User entity).
        /// </summary>
        public string LastName { get; set; } = string.Empty;

        /// <summary>
        /// Full name computed on the fly.
        /// </summary>
        public string FullName => $"{FirstName} {LastName}";

        /// <summary>
        /// Mentor's email address (from User entity).
        /// </summary>
        public string Email { get; set; } = string.Empty;

        /// <summary>
        /// URL to mentor's profile picture (from User entity).
        /// </summary>
        public string? ProfilePictureUrl { get; set; }

        // ============ MENTOR PROFILE ============

        /// <summary>
        /// Professional biography.
        /// </summary>
        public string? Bio { get; set; }

        /// <summary>
        /// List of expertise areas as an array.
        /// Example: ["C#", "ASP.NET", "Azure"]
        /// </summary>
        public List<string> ExpertiseTags { get; set; } = new();

        /// <summary>
        /// Years of professional experience.
        /// </summary>
        public int? YearsOfExperience { get; set; }

        /// <summary>
        /// Professional certifications.
        /// </summary>
        public string? Certifications { get; set; }

        // ============ PRICING ============

        /// <summary>
        /// Price for 30-minute session.
        /// </summary>
        public decimal Rate30Min { get; set; }

        /// <summary>
        /// Price for 60-minute session.
        /// </summary>
        public decimal Rate60Min { get; set; }

        // ============ STATISTICS ============

        /// <summary>
        /// Average rating (1-5 scale).
        /// </summary>
        public decimal AverageRating { get; set; }

        /// <summary>
        /// Total number of reviews.
        /// </summary>
        public int TotalReviews { get; set; }

        /// <summary>
        /// Total completed sessions.
        /// </summary>
        public int TotalSessionsCompleted { get; set; }

        /// <summary>
        /// Whether mentor is verified by admins.
        /// </summary>
        public bool IsVerified { get; set; }

        /// <summary>
        /// Current approval status.
        /// </summary>
        public string ApprovalStatus { get; set; } = string.Empty;

        /// <summary>
        /// When the mentor profile was created.
        /// </summary>
        public DateTime CreatedAt { get; set; }

        /// <summary>
        /// When the mentor profile was last updated.
        /// </summary>
        public DateTime? UpdatedAt { get; set; }
    }
}

Step 2: Create UpdateMentorProfileDto (Write DTO)

using System.ComponentModel.DataAnnotations;

namespace CareerRoute.Core.DTOs.Mentors
{
    /// <summary>
    /// DTO for updating an existing mentor profile.
    /// Used in PUT requests when mentors edit their profiles.
    /// Contains only fields that mentors are allowed to modify.
    /// </summary>
    public class UpdateMentorProfileDto
    {
        // ============ PROFILE INFORMATION ============

        /// <summary>
        /// Updated professional biography.
        /// </summary>
        [MaxLength(2000, ErrorMessage = "Bio cannot exceed 2000 characters")]
        public string? Bio { get; set; }

        /// <summary>
        /// Updated expertise areas as a comma-separated string.
        /// Example: "C#, ASP.NET, Azure"
        /// </summary>
        [MaxLength(500, ErrorMessage = "Expertise tags cannot exceed 500 characters")]
        public string? ExpertiseTags { get; set; }

        /// <summary>
        /// Updated years of experience.
        /// </summary>
        [Range(0, 60, ErrorMessage = "Years of experience must be between 0 and 60")]
        public int? YearsOfExperience { get; set; }

        /// <summary>
        /// Updated certifications.
        /// </summary>
        [MaxLength(1000, ErrorMessage = "Certifications cannot exceed 1000 characters")]
        public string? Certifications { get; set; }

        // ============ PRICING ============

        /// <summary>
        /// Updated price for 30-minute sessions.
        /// </summary>
        [Range(0.01, 10000, ErrorMessage = "30-minute rate must be between $0.01 and $10,000")]
        public decimal Rate30Min { get; set; }

        /// <summary>
        /// Updated price for 60-minute sessions.
        /// </summary>
        [Range(0.01, 10000, ErrorMessage = "60-minute rate must be between $0.01 and $10,000")]
        public decimal Rate60Min { get; set; }

        // NOTE: We DON'T include these fields because they should NOT be editable by mentors:
        // - AverageRating (calculated from reviews)
        // - TotalReviews (calculated from reviews)
        // - TotalSessionsCompleted (calculated from sessions)
        // - IsVerified (only admins can change)
        // - ApprovalStatus (only admins can change)
    }
}

Understanding the Code

1. Why separate Read and Write DTOs?

  • Read DTO includes everything clients need to display
  • Write DTO includes only fields users can modify
  • Example: AverageRating is in read DTO but NOT in write DTO (calculated field)

2. Why include User fields in MentorProfileDto?

  • Convenience: Frontend gets all data in one request
  • Avoids N+1 query problem (getting mentor, then user separately)
  • Alternative: Create a separate UserDto and nest it

3. Why validation attributes on DTOs?

  • First line of defense before data reaches the database
  • ASP.NET Core automatically validates DTOs and returns 400 Bad Request if invalid
  • You'll create custom validators in Task T056 for complex validation

4. Why List<string> for ExpertiseTags in read DTO but string? in write DTO?

  • Read: Converts comma-separated string to array for frontend convenience
  • Write: Accepts comma-separated string for simplicity (you'll split it in the service)

5. What about [Required] attribute?

  • Not used here because all fields are optional when updating
  • Mentors can update only Bio without changing pricing

Summary Checklist

  • <input type="checkbox" disabled=""> Create MentorProfileDto.cs in CareerRoute.Core/DTOs/Mentors/
  • <input type="checkbox" disabled=""> Create UpdateMentorProfileDto.cs in same folder
  • <input type="checkbox" disabled=""> Include User fields in MentorProfileDto for convenience
  • <input type="checkbox" disabled=""> Add validation attributes to UpdateMentorProfileDto
  • <input type="checkbox" disabled=""> Exclude computed/admin-only fields from UpdateMentorProfileDto
  • <input type="checkbox" disabled=""> Add XML comments for documentation

Task T039: Configure Entity Mapping

What is Entity Framework Core Configuration?

EF Core uses conventions to map entities to database tables. However, sometimes we need explicit configuration:

  • Define relationships (one-to-one, one-to-many)
  • Set up indexes for performance
  • Configure column types precisely
  • Add constraints (unique, check constraints)

Why Separate Configuration Files?

Instead of putting all configuration in DbContext.OnModelCreating(), we use IEntityTypeConfiguration<T>:

  • Separation of concerns: Each entity has its own configuration class
  • Maintainability: Easier to find and update configuration
  • Cleaner code: DbContext doesn't become a giant file

Implementation Steps

Location: Backend/CareerRoute.Infrastructure/Data/Configurations/MentorConfiguration.cs

Step 1: Create the Configurations Folder

First, ensure the folder exists:

Backend/CareerRoute.Infrastructure/Data/Configurations/

Step 2: Create MentorConfiguration Class

using CareerRoute.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace CareerRoute.Infrastructure.Data.Configurations
{
    /// <summary>
    /// Entity Framework Core configuration for the Mentor entity.
    /// Defines database schema, relationships, indexes, and constraints.
    /// </summary>
    public class MentorConfiguration : IEntityTypeConfiguration<Mentor>
    {
        public void Configure(EntityTypeBuilder<Mentor> builder)
        {
            // ============ TABLE CONFIGURATION ============

            /// <summary>
            /// Explicitly name the table "Mentors" in the database.
            /// Convention would create "Mentor" (singular), but we prefer plural.
            /// </summary>
            builder.ToTable("Mentors");

            // ============ PRIMARY KEY ============

            /// <summary>
            /// Define Id as the primary key.
            /// This is also a foreign key to AspNetUsers.Id.
            /// </summary>
            builder.HasKey(m => m.Id);

            // ============ ONE-TO-ONE RELATIONSHIP ============

            /// <summary>
            /// Configure one-to-one relationship between Mentor and ApplicationUser.
            /// - One Mentor has one User
            /// - One User can have zero or one Mentor (optional)
            /// - If User is deleted, Mentor is also deleted (cascade)
            /// </summary>
            builder.HasOne(m => m.User)
                .WithMany() // No navigation property from User to Mentor yet
                .HasForeignKey(m => m.Id)
                .OnDelete(DeleteBehavior.Cascade);

            // ============ PROPERTY CONFIGURATION ============

            /// <summary>
            /// Configure Bio column.
            /// </summary>
            builder.Property(m => m.Bio)
                .HasMaxLength(2000)
                .IsRequired(false); // Optional field

            /// <summary>
            /// Configure ExpertiseTags column.
            /// </summary>
            builder.Property(m => m.ExpertiseTags)
                .HasMaxLength(500)
                .IsRequired(false);

            /// <summary>
            /// Configure YearsOfExperience.
            /// Optional field (nullable int).
            /// </summary>
            builder.Property(m => m.YearsOfExperience)
                .IsRequired(false);

            /// <summary>
            /// Configure Certifications column.
            /// </summary>
            builder.Property(m => m.Certifications)
                .HasMaxLength(1000)
                .IsRequired(false);

            /// <summary>
            /// Configure Rate30Min with precise decimal type.
            /// SQL Server: decimal(18,2)
            /// </summary>
            builder.Property(m => m.Rate30Min)
                .HasColumnType("decimal(18,2)")
                .IsRequired();

            /// <summary>
            /// Configure Rate60Min with precise decimal type.
            /// </summary>
            builder.Property(m => m.Rate60Min)
                .HasColumnType("decimal(18,2)")
                .IsRequired();

            /// <summary>
            /// Configure AverageRating with decimal(3,2).
            /// Range: 0.00 to 5.00
            /// </summary>
            builder.Property(m => m.AverageRating)
                .HasColumnType("decimal(3,2)")
                .HasDefaultValue(0)
                .IsRequired();

            /// <summary>
            /// Configure TotalReviews with default value.
            /// </summary>
            builder.Property(m => m.TotalReviews)
                .HasDefaultValue(0)
                .IsRequired();

            /// <summary>
            /// Configure TotalSessionsCompleted with default value.
            /// </summary>
            builder.Property(m => m.TotalSessionsCompleted)
                .HasDefaultValue(0)
                .IsRequired();

            /// <summary>
            /// Configure IsVerified with default value.
            /// New mentors are not verified by default.
            /// </summary>
            builder.Property(m => m.IsVerified)
                .HasDefaultValue(false)
                .IsRequired();

            /// <summary>
            /// Configure ApprovalStatus.
            /// Default is "Pending" for new mentor applications.
            /// </summary>
            builder.Property(m => m.ApprovalStatus)
                .HasMaxLength(20)
                .HasDefaultValue("Pending")
                .IsRequired();

            /// <summary>
            /// Configure CreatedAt timestamp.
            /// Automatically set to current UTC time when record is created.
            /// </summary>
            builder.Property(m => m.CreatedAt)
                .HasDefaultValueSql("GETUTCDATE()")
                .IsRequired();

            /// <summary>
            /// Configure UpdatedAt timestamp.
            /// Optional field, set when record is updated.
            /// </summary>
            builder.Property(m => m.UpdatedAt)
                .IsRequired(false);

            // ============ INDEXES FOR PERFORMANCE ============

            /// <summary>
            /// Composite index for mentor search and filtering.
            /// Speeds up queries that filter by verification and availability.
            /// Example: SELECT * FROM Mentors WHERE IsVerified = 1
            /// </summary>
            builder.HasIndex(m => new { m.IsVerified, m.ApprovalStatus })
                .HasDatabaseName("IX_Mentor_IsVerified_ApprovalStatus");

            /// <summary>
            /// Index for sorting by average rating.
            /// Speeds up queries like: ORDER BY AverageRating DESC
            /// </summary>
            builder.HasIndex(m => m.AverageRating)
                .HasDatabaseName("IX_Mentor_AverageRating");

            /// <summary>
            /// Index for sorting by total sessions.
            /// Speeds up "most popular" queries.
            /// </summary>
            builder.HasIndex(m => m.TotalSessionsCompleted)
                .HasDatabaseName("IX_Mentor_TotalSessions");

            // ============ CHECK CONSTRAINTS ============

            // Note: Check constraints require EF Core 5.0+
            // These ensure data integrity at the database level

            /// <summary>
            /// Ensure average rating is between 0 and 5.
            /// </summary>
            builder.ToTable(t => t.HasCheckConstraint(
                "CK_Mentor_AverageRating",
                "[AverageRating] >= 0 AND [AverageRating] <= 5"));

            /// <summary>
            /// Ensure 30-minute rate is positive.
            /// </summary>
            builder.ToTable(t => t.HasCheckConstraint(
                "CK_Mentor_Rate30Min",
                "[Rate30Min] > 0"));

            /// <summary>
            /// Ensure 60-minute rate is positive.
            /// </summary>
            builder.ToTable(t => t.HasCheckConstraint(
                "CK_Mentor_Rate60Min",
                "[Rate60Min] > 0"));

            /// <summary>
            /// Ensure years of experience is non-negative.
            /// </summary>
            builder.ToTable(t => t.HasCheckConstraint(
                "CK_Mentor_YearsOfExperience",
                "[YearsOfExperience] >= 0"));
        }
    }
}

Step 3: Register Configuration in DbContext

Update ApplicationDbContext.cs:

using CareerRoute.Core.Domain.Entities;
using CareerRoute.Infrastructure.Data.Configurations;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace CareerRoute.Infrastructure.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        // ============ DBSETS ============

        public DbSet<RefreshToken> RefreshTokens { get; set; }

        /// <summary>
        /// Add DbSet for Mentor entity.
        /// This allows you to query: dbContext.Mentors.Where(...)
        /// </summary>
        public DbSet<Mentor> Mentors { get; set; }

        // ============ MODEL CONFIGURATION ============

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            // Existing RefreshToken configuration
            builder.Entity<RefreshToken>()
                .HasOne(rt => rt.User)
                .WithMany(u => u.RefreshTokens)
                .HasForeignKey(rt => rt.UserId);

            // ============ APPLY CONFIGURATIONS ============
            // Automatically applies all IEntityTypeConfiguration classes in the assembly
            builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);

            // Alternative: Apply manually
            // builder.ApplyConfiguration(new MentorConfiguration());
        }
    }
}

Understanding the Code

1. Why IEntityTypeConfiguration<Mentor>?

  • Keeps configuration separate from DbContext
  • Makes code modular and testable
  • Follows Single Responsibility Principle

2. What is .ToTable("Mentors")?

  • Sets the database table name
  • Without this, EF Core would create a table named "Mentor" (singular)

3. Why define .HasOne() and .WithMany()?

  • Explicitly configures the one-to-one relationship
  • .OnDelete(DeleteBehavior.Cascade) means: if User is deleted, Mentor is also deleted

4. What is HasMaxLength vs [MaxLength] attribute?

  • Both do the same thing!
  • Attribute: Applied on entity property
  • Fluent API: Applied in configuration class
  • Fluent API is more powerful (e.g., you can configure relationships)

5. Why decimal(18,2) for prices?

  • Prevents floating-point precision errors
  • Example: 9.99 + 0.01 = 10.00 (exact), unlike float which might give 10.000001

6. What are indexes?

  • Speed up database queries
  • Example: IX_Mentor_AverageRating speeds up ORDER BY AverageRating DESC
  • Trade-off: Indexes speed up reads but slow down writes slightly

7. What are check constraints?

  • Database-level validation
  • Example: [AverageRating] >= 0 AND [AverageRating] <= 5 prevents invalid data
  • Defense in depth: Even if C# validation fails, database rejects invalid data

8. Why GETUTCDATE() instead of DateTime.UtcNow?

  • GETUTCDATE(): Database server's UTC time (consistent)
  • DateTime.UtcNow: Application server's UTC time (could differ in distributed systems)

Creating the Migration

After creating the configuration, you need to create a database migration:

# Navigate to Infrastructure project
cd Backend/CareerRoute.Infrastructure

# Create migration
dotnet ef migrations add AddMentorEntity --startup-project ../CareerRoute.API

# Apply migration to database
dotnet ef database update --startup-project ../CareerRoute.API

Summary Checklist

  • <input type="checkbox" disabled=""> Create MentorConfiguration.cs in Infrastructure/Data/Configurations/
  • <input type="checkbox" disabled=""> Configure table name, primary key, and relationships
  • <input type="checkbox" disabled=""> Configure all properties with appropriate types and constraints
  • <input type="checkbox" disabled=""> Add performance indexes
  • <input type="checkbox" disabled=""> Add check constraints for data integrity
  • <input type="checkbox" disabled=""> Register DbSet in ApplicationDbContext
  • <input type="checkbox" disabled=""> Apply configurations using ApplyConfigurationsFromAssembly
  • <input type="checkbox" disabled=""> Create and apply EF Core migration

Task T044: Create Repository Interface

What is the Repository Pattern?

The Repository Pattern abstracts data access logic:

  • Separates business logic from data access
  • Makes code testable (you can mock repositories)
  • Centralizes data access logic
  • Follows Dependency Inversion Principle (depend on interfaces, not implementations)

Repository Architecture

┌──────────────────────────────────────┐
│    Service Layer (Business Logic)   │
│   Depends on IMentorRepository       │
└────────────┬─────────────────────────┘
             │ (Interface)
             ↓
┌──────────────────────────────────────┐
│         IMentorRepository            │
│  (Contract: what operations exist)   │
└────────────┬─────────────────────────┘
             │ (Implementation)
             ↓
┌──────────────────────────────────────┐
│       MentorRepository               │
│  (Actual database operations)        │
└────────────┬─────────────────────────┘
             │
             ↓
┌──────────────────────────────────────┐
│        ApplicationDbContext          │
│         (Entity Framework)           │
└──────────────────────────────────────┘

Implementation Steps

Location: Backend/CareerRoute.Core/Domain/Interfaces/IMentorRepository.cs

Step 1: Update IMentorRepository Interface

using CareerRoute.Core.Domain.Entities;

namespace CareerRoute.Core.Domain.Interfaces
{
    /// <summary>
    /// Repository interface for Mentor entity.
    /// Defines data access operations for mentors.
    /// Follows Repository Pattern to abstract database operations.
    /// </summary>
    public interface IMentorRepository : IBaseRepository<Mentor>
    {
        // ============ CUSTOM QUERIES ============
        // These methods go beyond basic CRUD operations

        /// <summary>
        /// Get a mentor by their user ID, including related User entity.
        /// Use this when you need mentor + user info in one query.
        /// </summary>
        /// <param name="userId">The ID of the user who is a mentor</param>
        /// <returns>Mentor with User navigation property loaded, or null if not found</returns>
        Task<Mentor?> GetMentorWithUserByIdAsync(string userId);

        /// <summary>
        /// Get all approved and verified mentors.
        /// Used for public mentor listings and search.
        /// </summary>
        /// <returns>List of approved mentors</returns>
        Task<IEnumerable<Mentor>> GetApprovedMentorsAsync();

        /// <summary>
        /// Get all mentors pending admin approval.
        /// Used in admin dashboard to review new applications.
        /// </summary>
        /// <returns>List of pending mentors</returns>
        Task<IEnumerable<Mentor>> GetPendingMentorsAsync();

        /// <summary>
        /// Search mentors by expertise tags or bio keywords.
        /// Supports full-text search for better user experience.
        /// </summary>
        /// <param name="searchTerm">Keywords to search for</param>
        /// <returns>List of matching mentors</returns>
        Task<IEnumerable<Mentor>> SearchMentorsAsync(string searchTerm);

        /// <summary>
        /// Get top mentors by average rating.
        /// Used for "Featured Mentors" or "Top Rated" sections.
        /// </summary>
        /// <param name="count">Number of top mentors to return</param>
        /// <returns>List of top-rated mentors</returns>
        Task<IEnumerable<Mentor>> GetTopRatedMentorsAsync(int count = 10);

        /// <summary>
        /// Check if a user is already a mentor.
        /// Used to prevent duplicate mentor applications.
        /// </summary>
        /// <param name="userId">User ID to check</param>
        /// <returns>True if user is a mentor, false otherwise</returns>
        Task<bool> IsMentorAsync(string userId);

        /// <summary>
        /// Update mentor's average rating and review count.
        /// Called after a new review is submitted.
        /// </summary>
        /// <param name="mentorId">Mentor ID</param>
        /// <param name="newAverageRating">Updated average rating</param>
        /// <param name="totalReviews">Updated total review count</param>
        Task UpdateRatingAsync(string mentorId, decimal newAverageRating, int totalReviews);

        /// <summary>
        /// Increment the total sessions completed counter.
        /// Called after a session is successfully completed.
        /// </summary>
        /// <param name="mentorId">Mentor ID</param>
        Task IncrementSessionCountAsync(string mentorId);
    }
}

Understanding the Code

1. Why extend IBaseRepository<Mentor>?

  • Inherits basic CRUD operations: GetAllAsync(), GetByIdAsync(), AddAsync(), Update(), Delete()
  • Avoids code duplication
  • We only define custom mentor-specific operations

2. Why Task<Mentor?> instead of Task<Mentor>?

  • The ? means nullable reference type (C# 11+)
  • Indicates the method may return null if mentor not found
  • Helps prevent null reference exceptions

3. Why GetMentorWithUserByIdAsync?

  • Eager loading: Loads mentor AND user in a single query
  • Without this, you'd need 2 queries: get mentor, then get user (N+1 problem)
  • Performance optimization

4. Why separate GetApprovedMentorsAsync and GetPendingMentorsAsync?

  • Different use cases: public listing vs. admin dashboard
  • Simplifies queries
  • Better performance (filtered at database level)

5. Why UpdateRatingAsync instead of just using Update()?

  • Encapsulates business logic: Rating calculation is complex
  • Atomic operation: Updates multiple fields together
  • Prevents errors: Ensures rating and review count stay in sync

6. What is Task<IEnumerable<Mentor>>?

  • Task: Asynchronous operation (doesn't block thread while waiting for database)
  • IEnumerable<Mentor>: Collection of mentors (could be list, array, etc.)
  • async/await pattern for better performance

Summary Checklist

  • <input type="checkbox" disabled=""> Update IMentorRepository.cs to extend IBaseRepository<Mentor>
  • <input type="checkbox" disabled=""> Define custom query methods for mentor-specific operations
  • <input type="checkbox" disabled=""> Use async/await pattern (Task<T>)
  • <input type="checkbox" disabled=""> Use nullable types (Mentor?) where appropriate
  • <input type="checkbox" disabled=""> Add XML comments for each method
  • <input type="checkbox" disabled=""> Include methods for common use cases (search, top-rated, pending approval)

Task T047: Implement Repository

What Goes in a Repository Implementation?

The repository implementation:

  • Uses Entity Framework Core to query the database
  • Implements the interface methods
  • Handles data access concerns (loading related entities, filtering, sorting)
  • Does NOT contain business logic (that's in services)

Implementation Steps

Location: Backend/CareerRoute.Infrastructure/Repositories/MentorRepository.cs

Step 1: Implement MentorRepository

using CareerRoute.Core.Domain.Entities;
using CareerRoute.Core.Domain.Interfaces;
using CareerRoute.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace CareerRoute.Infrastructure.Repositories
{
    /// <summary>
    /// Implementation of IMentorRepository using Entity Framework Core.
    /// Handles all database operations for Mentor entity.
    /// </summary>
    public class MentorRepository : GenericRepository<Mentor>, IMentorRepository
    {
        // Store DbContext for custom queries
        private readonly ApplicationDbContext _context;

        /// <summary>
        /// Constructor: Inject ApplicationDbContext
        /// </summary>
        public MentorRepository(ApplicationDbContext context) : base(context)
        {
            _context = context;
        }

        // ============ CUSTOM QUERIES ============

        /// <summary>
        /// Get mentor with related User entity loaded.
        /// Uses .Include() for eager loading.
        /// </summary>
        public async Task<Mentor?> GetMentorWithUserByIdAsync(string userId)
        {
            return await _context.Mentors
                .Include(m => m.User) // Eager load User navigation property
                .FirstOrDefaultAsync(m => m.Id == userId);
        }

        /// <summary>
        /// Get all approved and verified mentors.
        /// Includes User for displaying name/email.
        /// </summary>
        public async Task<IEnumerable<Mentor>> GetApprovedMentorsAsync()
        {
            return await _context.Mentors
                .Include(m => m.User)
                .Where(m => m.ApprovalStatus == "Approved" && m.IsVerified)
                .OrderByDescending(m => m.AverageRating)
                .ToListAsync();
        }

        /// <summary>
        /// Get all mentors with pending approval status.
        /// Used in admin dashboard.
        /// </summary>
        public async Task<IEnumerable<Mentor>> GetPendingMentorsAsync()
        {
            return await _context.Mentors
                .Include(m => m.User)
                .Where(m => m.ApprovalStatus == "Pending")
                .OrderBy(m => m.CreatedAt) // Oldest first
                .ToListAsync();
        }

        /// <summary>
        /// Search mentors by keywords in ExpertiseTags or Bio.
        /// Case-insensitive search.
        /// </summary>
        public async Task<IEnumerable<Mentor>> SearchMentorsAsync(string searchTerm)
        {
            if (string.IsNullOrWhiteSpace(searchTerm))
            {
                return await GetApprovedMentorsAsync();
            }

            var lowerSearchTerm = searchTerm.ToLower();

            return await _context.Mentors
                .Include(m => m.User)
                .Where(m => m.ApprovalStatus == "Approved"
                    && (m.ExpertiseTags!.ToLower().Contains(lowerSearchTerm)
                        || m.Bio!.ToLower().Contains(lowerSearchTerm)
                        || m.User.FirstName.ToLower().Contains(lowerSearchTerm)
                        || m.User.LastName.ToLower().Contains(lowerSearchTerm)))
                .OrderByDescending(m => m.AverageRating)
                .ToListAsync();
        }

        /// <summary>
        /// Get top-rated mentors.
        /// Used for featured mentors section.
        /// </summary>
        public async Task<IEnumerable<Mentor>> GetTopRatedMentorsAsync(int count = 10)
        {
            return await _context.Mentors
                .Include(m => m.User)
                .Where(m => m.ApprovalStatus == "Approved" 
                    && m.IsVerified 
                    && m.TotalReviews > 0) // Only mentors with reviews
                .OrderByDescending(m => m.AverageRating)
                .ThenByDescending(m => m.TotalReviews) // Break ties with review count
                .Take(count)
                .ToListAsync();
        }

        /// <summary>
        /// Check if a user is already a mentor.
        /// </summary>
        public async Task<bool> IsMentorAsync(string userId)
        {
            return await _context.Mentors
                .AnyAsync(m => m.Id == userId);
        }

        /// <summary>
        /// Update mentor's rating statistics.
        /// This is called by the service after calculating new averages.
        /// </summary>
        public async Task UpdateRatingAsync(string mentorId, decimal newAverageRating, int totalReviews)
        {
            var mentor = await _context.Mentors.FindAsync(mentorId);

            if (mentor == null)
            {
                throw new KeyNotFoundException($"Mentor with ID {mentorId} not found");
            }

            mentor.AverageRating = newAverageRating;
            mentor.TotalReviews = totalReviews;
            mentor.UpdatedAt = DateTime.UtcNow;

            await _context.SaveChangesAsync();
        }

        /// <summary>
        /// Increment completed session counter.
        /// Called after a session is marked as completed.
        /// </summary>
        public async Task IncrementSessionCountAsync(string mentorId)
        {
            var mentor = await _context.Mentors.FindAsync(mentorId);

            if (mentor == null)
            {
                throw new KeyNotFoundException($"Mentor with ID {mentorId} not found");
            }

            mentor.TotalSessionsCompleted++;
            mentor.UpdatedAt = DateTime.UtcNow;

            await _context.SaveChangesAsync();
        }
    }
}

Understanding the Code

1. Why extend GenericRepository<Mentor>?

  • Inherits base CRUD operations (GetAllAsync, AddAsync, etc.)
  • Avoids duplicating common code
  • We only implement additional mentor-specific methods

2. What is .Include(m => m.User)?

  • Eager loading: Loads related User entity in the same query
  • Without it: mentor.User would be null (lazy loading disabled by default)
  • Performance: One query instead of N+1 queries

3. Why FirstOrDefaultAsync vs SingleOrDefaultAsync?

  • FirstOrDefaultAsync: Returns first match or null (faster)
  • SingleOrDefaultAsync: Throws exception if multiple matches (safety)
  • Use First when you expect 0 or 1 result but don't want exception if >1

4. What is ToListAsync()?

  • Executes the query and materializes results into a list
  • Asynchronous: Doesn't block the thread
  • Without it, query is not executed (deferred execution)

5. Why check string.IsNullOrWhiteSpace(searchTerm)?

  • Defensive programming
  • If search term is empty, return all approved mentors
  • Prevents SQL errors from empty string queries

6. What is .ToLower().Contains()?

  • Case-insensitive search
  • Example: "C#" matches "c#", "C#", "C-Sharp"
  • Limitation: Not efficient for large datasets (consider full-text search later)

7. Why .ThenByDescending(m => m.TotalReviews)?

  • Secondary sort: If two mentors have same rating, sort by review count
  • Example: 5.0 stars with 100 reviews ranks higher than 5.0 with 10 reviews

8. Why await _context.SaveChangesAsync() in some methods?

  • Commits changes to the database
  • Required after .Update() or direct property changes
  • Not required after .AddAsync() (you'll call SaveChanges in service layer)

9. Why throw KeyNotFoundException?

  • Clearly indicates the problem
  • Service layer can catch and return appropriate HTTP status (404 Not Found)
  • Better than letting a null reference exception bubble up

Common Mistakes to Avoid

Mistake 1: Forgetting .Include()

var mentor = await _context.Mentors.FirstOrDefaultAsync(m => m.Id == id);
// mentor.User is NULL! (unless lazy loading is enabled)

Correct:

var mentor = await _context.Mentors
    .Include(m => m.User)
    .FirstOrDefaultAsync(m => m.Id == id);
// mentor.User is loaded

Mistake 2: Forgetting .ToListAsync()

var mentors = _context.Mentors.Where(m => m.IsVerified);
// This is an IQueryable, not executed yet!

Correct:

var mentors = await _context.Mentors
    .Where(m => m.IsVerified)
    .ToListAsync();
// Query executed, results in memory

Mistake 3: Not using await

public Task<Mentor?> GetByIdAsync(string id)
{
    return _context.Mentors.FindAsync(id); // ❌ Compiler error
}

Correct:

public async Task<Mentor?> GetByIdAsync(string id)
{
    return await _context.Mentors.FindAsync(id);
}

Summary Checklist

  • <input type="checkbox" disabled=""> Create MentorRepository.cs in Infrastructure/Repositories/
  • <input type="checkbox" disabled=""> Extend GenericRepository<Mentor> and implement IMentorRepository
  • <input type="checkbox" disabled=""> Inject ApplicationDbContext in constructor
  • <input type="checkbox" disabled=""> Use .Include() for eager loading related entities
  • <input type="checkbox" disabled=""> Use async/await pattern for all database operations
  • <input type="checkbox" disabled=""> Use .ToListAsync(), .FirstOrDefaultAsync(), etc. for async queries
  • <input type="checkbox" disabled=""> Throw appropriate exceptions for error cases
  • <input type="checkbox" disabled=""> Add defensive checks (null/empty parameters)

Task T053: Create and Implement Service

What is a Service Layer?

The Service Layer contains business logic:

  • Orchestrates operations across multiple repositories
  • Validates business rules
  • Transforms entities to DTOs and vice versa
  • Handles transactions
  • Does NOT directly access DbContext (uses repositories)

Service vs Repository

Repository Service
Data access Business logic
CRUD operations Complex workflows
Returns entities Returns DTOs
No validation Validates business rules
Single entity focus May use multiple repositories

Implementation Steps

Location: Backend/CareerRoute.Core/Services/

Step 1: Create IMentorService Interface

File: Backend/CareerRoute.Core/Services/Interfaces/IMentorService.cs

using CareerRoute.Core.DTOs.Mentors;

namespace CareerRoute.Core.Services.Interfaces
{
    /// <summary>
    /// Service interface for mentor-related business operations.
    /// Defines high-level operations that combine repository calls with business logic.
    /// </summary>
    public interface IMentorService
    {
        // ============ MENTOR PROFILE OPERATIONS ============

        /// <summary>
        /// Get a mentor's complete profile by ID.
        /// Returns DTO suitable for public display.
        /// </summary>
        /// <param name="mentorId">Mentor's unique ID</param>
        /// <returns>Mentor profile DTO or null if not found</returns>
        Task<MentorProfileDto?> GetMentorProfileAsync(string mentorId);

        /// <summary>
        /// Get all approved mentors (for public listing).
        /// Returns DTOs with basic info for search results.
        /// </summary>
        /// <returns>List of mentor profile DTOs</returns>
        Task<IEnumerable<MentorProfileDto>> GetAllApprovedMentorsAsync();

        /// <summary>
        /// Update a mentor's profile.
        /// Only the mentor themselves can update their profile.
        /// </summary>
        /// <param name="mentorId">Mentor ID</param>
        /// <param name="updateDto">Updated profile data</param>
        /// <returns>Updated mentor profile DTO</returns>
        Task<MentorProfileDto> UpdateMentorProfileAsync(string mentorId, UpdateMentorProfileDto updateDto);

        /// <summary>
        /// Create a new mentor profile (user applying to become a mentor).
        /// Creates a Mentor record linked to the User.
        /// </summary>
        /// <param name="userId">User ID of the applicant</param>
        /// <param name="mentorData">Initial mentor profile data</param>
        /// <returns>Created mentor profile DTO</returns>
        Task<MentorProfileDto> CreateMentorProfileAsync(string userId, UpdateMentorProfileDto mentorData);

        // ============ SEARCH & FILTERING ============

        /// <summary>
        /// Search mentors by keywords.
        /// Searches in bio, expertise tags, and name.
        /// </summary>
        /// <param name="searchTerm">Search keywords</param>
        /// <returns>List of matching mentor profiles</returns>
        Task<IEnumerable<MentorProfileDto>> SearchMentorsAsync(string searchTerm);

        /// <summary>
        /// Get top-rated mentors.
        /// Used for featured sections.
        /// </summary>
        /// <param name="count">Number of mentors to return</param>
        /// <returns>List of top-rated mentor profiles</returns>
        Task<IEnumerable<MentorProfileDto>> GetTopRatedMentorsAsync(int count = 10);

        // ============ ADMIN OPERATIONS ============

        /// <summary>
        /// Get all mentors pending admin approval.
        /// Admin-only operation.
        /// </summary>
        /// <returns>List of pending mentor profiles</returns>
        Task<IEnumerable<MentorProfileDto>> GetPendingMentorApplicationsAsync();

        /// <summary>
        /// Approve a mentor application.
        /// Admin-only operation.
        /// </summary>
        /// <param name="mentorId">Mentor ID to approve</param>
        Task ApproveMentorAsync(string mentorId);

        /// <summary>
        /// Reject a mentor application.
        /// Admin-only operation.
        /// </summary>
        /// <param name="mentorId">Mentor ID to reject</param>
        /// <param name="reason">Rejection reason (sent to applicant)</param>
        Task RejectMentorAsync(string mentorId, string reason);

        // ============ VALIDATION ============

        /// <summary>
        /// Check if a user is already a mentor.
        /// Prevents duplicate applications.
        /// </summary>
        /// <param name="userId">User ID to check</param>
        /// <returns>True if user is a mentor</returns>
        Task<bool> IsMentorAsync(string userId);
    }
}

Step 2: Implement MentorService

File: Backend/CareerRoute.Core/Services/Implementations/MentorService.cs

using AutoMapper;
using CareerRoute.Core.Domain.Entities;
using CareerRoute.Core.Domain.Interfaces;
using CareerRoute.Core.DTOs.Mentors;
using CareerRoute.Core.Exceptions;
using CareerRoute.Core.Services.Interfaces;
using Microsoft.Extensions.Logging;

namespace CareerRoute.Core.Services.Implementations
{
    /// <summary>
    /// Implementation of IMentorService.
    /// Contains business logic for mentor operations.
    /// </summary>
    public class MentorService : IMentorService
    {
        private readonly IMentorRepository _mentorRepository;
        private readonly IUserRepository _userRepository;
        private readonly IMapper _mapper;
        private readonly ILogger<MentorService> _logger;

        /// <summary>
        /// Constructor: Inject dependencies
        /// </summary>
        public MentorService(
            IMentorRepository mentorRepository,
            IUserRepository userRepository,
            IMapper mapper,
            ILogger<MentorService> logger)
        {
            _mentorRepository = mentorRepository;
            _userRepository = userRepository;
            _mapper = mapper;
            _logger = logger;
        }

        // ============ MENTOR PROFILE OPERATIONS ============

        /// <summary>
        /// Get mentor profile by ID.
        /// </summary>
        public async Task<MentorProfileDto?> GetMentorProfileAsync(string mentorId)
        {
            _logger.LogInformation("Fetching mentor profile for ID: {MentorId}", mentorId);

            var mentor = await _mentorRepository.GetMentorWithUserByIdAsync(mentorId);

            if (mentor == null)
            {
                _logger.LogWarning("Mentor with ID {MentorId} not found", mentorId);
                return null;
            }

            return _mapper.Map<MentorProfileDto>(mentor);
        }

        /// <summary>
        /// Get all approved mentors.
        /// </summary>
        public async Task<IEnumerable<MentorProfileDto>> GetAllApprovedMentorsAsync()
        {
            _logger.LogInformation("Fetching all approved mentors");

            var mentors = await _mentorRepository.GetApprovedMentorsAsync();

            return _mapper.Map<IEnumerable<MentorProfileDto>>(mentors);
        }

        /// <summary>
        /// Update mentor profile.
        /// Business rules:
        /// - Mentor must exist
        /// - 60-min rate should be >= 30-min rate (but we allow flexibility)
        /// </summary>
        public async Task<MentorProfileDto> UpdateMentorProfileAsync(
            string mentorId, 
            UpdateMentorProfileDto updateDto)
        {
            _logger.LogInformation("Updating mentor profile for ID: {MentorId}", mentorId);

            // Fetch existing mentor
            var mentor = await _mentorRepository.GetMentorWithUserByIdAsync(mentorId);

            if (mentor == null)
            {
                _logger.LogError("Mentor with ID {MentorId} not found", mentorId);
                throw new NotFoundException($"Mentor with ID {mentorId} not found");
            }

            // Business validation: 60-min rate should be reasonable
            if (updateDto.Rate60Min < updateDto.Rate30Min)
            {
                _logger.LogWarning("60-min rate is less than 30-min rate for mentor {MentorId}", mentorId);
                // We log but don't block - mentor might offer bulk discount
            }

            // Update properties
            mentor.Bio = updateDto.Bio;
            mentor.ExpertiseTags = updateDto.ExpertiseTags;
            mentor.YearsOfExperience = updateDto.YearsOfExperience;
            mentor.Certifications = updateDto.Certifications;
            mentor.Rate30Min = updateDto.Rate30Min;
            mentor.Rate60Min = updateDto.Rate60Min;
            mentor.UpdatedAt = DateTime.UtcNow;

            // Save changes
            _mentorRepository.Update(mentor);
            await _mentorRepository.SaveChangesAsync();

            _logger.LogInformation("Mentor profile updated successfully for ID: {MentorId}", mentorId);

            // Return updated DTO
            return _mapper.Map<MentorProfileDto>(mentor);
        }

        /// <summary>
        /// Create a new mentor profile.
        /// Business rules:
        /// - User must exist
        /// - User must not already be a mentor
        /// - New mentors start with "Pending" approval status
        /// </summary>
        public async Task<MentorProfileDto> CreateMentorProfileAsync(
            string userId, 
            UpdateMentorProfileDto mentorData)
        {
            _logger.LogInformation("Creating mentor profile for user ID: {UserId}", userId);

            // Validate user exists
            var user = await _userRepository.GetByIdAsync(userId);
            if (user == null)
            {
                _logger.LogError("User with ID {UserId} not found", userId);
                throw new NotFoundException($"User with ID {userId} not found");
            }

            // Check if user is already a mentor
            if (await _mentorRepository.IsMentorAsync(userId))
            {
                _logger.LogWarning("User {UserId} is already a mentor", userId);
                throw new BadRequestException("User is already a mentor");
            }

            // Create new mentor entity
            var mentor = new Mentor
            {
                Id = userId, // Same as User ID (one-to-one)
                Bio = mentorData.Bio,
                ExpertiseTags = mentorData.ExpertiseTags,
                YearsOfExperience = mentorData.YearsOfExperience,
                Certifications = mentorData.Certifications,
                Rate30Min = mentorData.Rate30Min,
                Rate60Min = mentorData.Rate60Min,
                ApprovalStatus = "Pending", // Requires admin approval
                IsVerified = false,
                CreatedAt = DateTime.UtcNow
            };

            // Save to database
            await _mentorRepository.AddAsync(mentor);
            await _mentorRepository.SaveChangesAsync();

            _logger.LogInformation("Mentor profile created successfully for user ID: {UserId}", userId);

            // Reload with user data for DTO mapping
            var createdMentor = await _mentorRepository.GetMentorWithUserByIdAsync(userId);

            return _mapper.Map<MentorProfileDto>(createdMentor!);
        }

        // ============ SEARCH & FILTERING ============

        /// <summary>
        /// Search mentors by keywords.
        /// </summary>
        public async Task<IEnumerable<MentorProfileDto>> SearchMentorsAsync(string searchTerm)
        {
            _logger.LogInformation("Searching mentors with term: {SearchTerm}", searchTerm);

            var mentors = await _mentorRepository.SearchMentorsAsync(searchTerm);

            return _mapper.Map<IEnumerable<MentorProfileDto>>(mentors);
        }

        /// <summary>
        /// Get top-rated mentors.
        /// </summary>
        public async Task<IEnumerable<MentorProfileDto>> GetTopRatedMentorsAsync(int count = 10)
        {
            _logger.LogInformation("Fetching top {Count} rated mentors", count);

            var mentors = await _mentorRepository.GetTopRatedMentorsAsync(count);

            return _mapper.Map<IEnumerable<MentorProfileDto>>(mentors);
        }

        // ============ ADMIN OPERATIONS ============

        /// <summary>
        /// Get pending mentor applications.
        /// </summary>
        public async Task<IEnumerable<MentorProfileDto>> GetPendingMentorApplicationsAsync()
        {
            _logger.LogInformation("Fetching pending mentor applications");

            var mentors = await _mentorRepository.GetPendingMentorsAsync();

            return _mapper.Map<IEnumerable<MentorProfileDto>>(mentors);
        }

        /// <summary>
        /// Approve a mentor application.
        /// Business rule: Changes status to "Approved" and sets IsVerified = true
        /// </summary>
        public async Task ApproveMentorAsync(string mentorId)
        {
            _logger.LogInformation("Approving mentor ID: {MentorId}", mentorId);

            var mentor = await _mentorRepository.GetMentorWithUserByIdAsync(mentorId);

            if (mentor == null)
            {
                _logger.LogError("Mentor with ID {MentorId} not found", mentorId);
                throw new NotFoundException($"Mentor with ID {mentorId} not found");
            }

            mentor.ApprovalStatus = "Approved";
            mentor.IsVerified = true;
            mentor.UpdatedAt = DateTime.UtcNow;

            _mentorRepository.Update(mentor);
            await _mentorRepository.SaveChangesAsync();

            _logger.LogInformation("Mentor {MentorId} approved successfully", mentorId);

            // TODO: Send approval email to mentor (implement in EmailService)
        }

        /// <summary>
        /// Reject a mentor application.
        /// </summary>
        public async Task RejectMentorAsync(string mentorId, string reason)
        {
            _logger.LogInformation("Rejecting mentor ID: {MentorId}, Reason: {Reason}", mentorId, reason);

            var mentor = await _mentorRepository.GetMentorWithUserByIdAsync(mentorId);

            if (mentor == null)
            {
                _logger.LogError("Mentor with ID {MentorId} not found", mentorId);
                throw new NotFoundException($"Mentor with ID {mentorId} not found");
            }

            mentor.ApprovalStatus = "Rejected";
            mentor.UpdatedAt = DateTime.UtcNow;

            _mentorRepository.Update(mentor);
            await _mentorRepository.SaveChangesAsync();

            _logger.LogInformation("Mentor {MentorId} rejected successfully", mentorId);

            // TODO: Send rejection email with reason (implement in EmailService)
        }

        // ============ VALIDATION ============

        /// <summary>
        /// Check if user is a mentor.
        /// </summary>
        public async Task<bool> IsMentorAsync(string userId)
        {
            return await _mentorRepository.IsMentorAsync(userId);
        }
    }
}

Understanding the Code

1. Why inject multiple dependencies?

  • IMentorRepository: For mentor data access
  • IUserRepository: To validate user exists when creating mentor
  • IMapper: To convert entities to DTOs
  • ILogger: For logging (debugging, monitoring)

2. What is IMapper?

  • Part of AutoMapper library
  • Automatically converts MentorMentorProfileDto
  • Eliminates boilerplate mapping code
  • You'll configure mappings in Task T060

3. Why throw custom exceptions?

  • NotFoundException: Returns 404 HTTP status
  • BadRequestException: Returns 400 HTTP status
  • Better than generic Exception (clearer intent)
  • You'll create these in CareerRoute.Core/Exceptions/

4. Why log at different levels?

  • LogInformation: Normal operations (audit trail)
  • LogWarning: Unusual but not error (e.g., duplicate mentor application)
  • LogError: Actual errors (entity not found)
  • Production debugging: Check logs to troubleshoot issues

5. Why reload entity after creation?

var createdMentor = await _mentorRepository.GetMentorWithUserByIdAsync(userId);
  • After AddAsync, navigation property User is not loaded
  • .Include(m => m.User) loads it
  • Needed for DTO mapping (MentorProfileDto includes user info)

6. Why check Rate60Min < Rate30Min?

  • Business validation: Usually 60-min should cost more (or at least equal)
  • We log but don't block (mentor might offer bulk discount)
  • Your choice: Strict (throw exception) or lenient (just warn)

7. Why UpdatedAt = DateTime.UtcNow?

  • Tracks when record was last modified
  • Useful for audit trails and optimistic concurrency

Step 3: Create Custom Exceptions

File: Backend/CareerRoute.Core/Exceptions/NotFoundException.cs

namespace CareerRoute.Core.Exceptions
{
    /// <summary>
    /// Exception thrown when a requested entity is not found.
    /// Maps to HTTP 404 Not Found.
    /// </summary>
    public class NotFoundException : Exception
    {
        public NotFoundException(string message) : base(message)
        {
        }

        public NotFoundException(string message, Exception innerException) 
            : base(message, innerException)
        {
        }
    }
}

File: Backend/CareerRoute.Core/Exceptions/BadRequestException.cs

namespace CareerRoute.Core.Exceptions
{
    /// <summary>
    /// Exception thrown when request data is invalid.
    /// Maps to HTTP 400 Bad Request.
    /// </summary>
    public class BadRequestException : Exception
    {
        public BadRequestException(string message) : base(message)
        {
        }

        public BadRequestException(string message, Exception innerException) 
            : base(message, innerException)
        {
        }
    }
}

Summary Checklist

  • <input type="checkbox" disabled=""> Create IMentorService.cs interface in Core/Services/Interfaces/
  • <input type="checkbox" disabled=""> Create MentorService.cs implementation in Core/Services/Implementations/
  • <input type="checkbox" disabled=""> Inject dependencies (repository, mapper, logger)
  • <input type="checkbox" disabled=""> Implement all interface methods
  • <input type="checkbox" disabled=""> Add business validation logic
  • <input type="checkbox" disabled=""> Use logging for debugging and audit
  • <input type="checkbox" disabled=""> Create custom exception classes (NotFoundException, BadRequestException)
  • <input type="checkbox" disabled=""> Return DTOs, not entities

Task T056: Create Validator

What is FluentValidation?

FluentValidation is a popular .NET library for building strongly-typed validation rules:

  • Fluent syntax: Chainable, readable validation rules
  • Reusable: Validators are classes that can be tested
  • Integrated with ASP.NET Core: Automatic validation before controller actions
  • Custom rules: You can create complex validation logic

Why Validate in Multiple Layers?

Layer Validation Type Example
DTO Basic constraints [MaxLength(2000)]
Validator Complex business rules "60-min rate must be > 30-min rate"
Service Cross-entity validation "User must exist before creating mentor"
Database Data integrity Check constraint: [AverageRating] <= 5

Defense in depth: Multiple layers catch different types of errors.

Implementation Steps

Location: Backend/CareerRoute.Core/Validators/

Step 1: Install FluentValidation

cd Backend/CareerRoute.Core
dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore

Step 2: Create UpdateMentorProfileValidator

File: Backend/CareerRoute.Core/Validators/UpdateMentorProfileValidator.cs

using CareerRoute.Core.DTOs.Mentors;
using FluentValidation;

namespace CareerRoute.Core.Validators
{
    /// <summary>
    /// Validator for UpdateMentorProfileDto.
    /// Defines business rules for mentor profile updates.
    /// </summary>
    public class UpdateMentorProfileValidator : AbstractValidator<UpdateMentorProfileDto>
    {
        public UpdateMentorProfileValidator()
        {
            // ============ BIO VALIDATION ============

            RuleFor(x => x.Bio)
                .MaximumLength(2000)
                    .WithMessage("Bio cannot exceed 2000 characters")
                .MinimumLength(50)
                    .WithMessage("Bio must be at least 50 characters to provide meaningful information")
                .When(x => !string.IsNullOrWhiteSpace(x.Bio)); // Only validate if provided

            // ============ EXPERTISE TAGS VALIDATION ============

            RuleFor(x => x.ExpertiseTags)
                .MaximumLength(500)
                    .WithMessage("Expertise tags cannot exceed 500 characters")
                .Must(BeValidTagsFormat)
                    .WithMessage("Expertise tags must be comma-separated (e.g., 'C#, ASP.NET, Azure')")
                .When(x => !string.IsNullOrWhiteSpace(x.ExpertiseTags));

            // ============ YEARS OF EXPERIENCE VALIDATION ============

            RuleFor(x => x.YearsOfExperience)
                .GreaterThanOrEqualTo(0)
                    .WithMessage("Years of experience cannot be negative")
                .LessThanOrEqualTo(60)
                    .WithMessage("Years of experience cannot exceed 60 years")
                .When(x => x.YearsOfExperience.HasValue);

            // ============ CERTIFICATIONS VALIDATION ============

            RuleFor(x => x.Certifications)
                .MaximumLength(1000)
                    .WithMessage("Certifications cannot exceed 1000 characters")
                .When(x => !string.IsNullOrWhiteSpace(x.Certifications));

            // ============ PRICING VALIDATION ============

            RuleFor(x => x.Rate30Min)
                .GreaterThan(0)
                    .WithMessage("30-minute rate must be greater than $0")
                .LessThanOrEqualTo(10000)
                    .WithMessage("30-minute rate cannot exceed $10,000");

            RuleFor(x => x.Rate60Min)
                .GreaterThan(0)
                    .WithMessage("60-minute rate must be greater than $0")
                .LessThanOrEqualTo(10000)
                    .WithMessage("60-minute rate cannot exceed $10,000");

            // ============ CROSS-PROPERTY VALIDATION ============

            RuleFor(x => x.Rate60Min)
                .GreaterThanOrEqualTo(x => x.Rate30Min)
                    .WithMessage("60-minute rate should be at least equal to 30-minute rate")
                    .WithSeverity(Severity.Warning); // Warning, not error

            // ============ CUSTOM VALIDATION ============

            RuleFor(x => x)
                .Must(HaveReasonablePricing)
                    .WithMessage("Pricing seems unrealistic. Please verify your rates.")
                    .When(x => x.Rate30Min > 0 && x.Rate60Min > 0);
        }

        // ============ CUSTOM VALIDATION METHODS ============

        /// <summary>
        /// Validate that expertise tags are comma-separated.
        /// </summary>
        private bool BeValidTagsFormat(string? tags)
        {
            if (string.IsNullOrWhiteSpace(tags))
                return true;

            // Check if tags contain valid characters (letters, numbers, commas, spaces)
            return tags.All(c => char.IsLetterOrDigit(c) || c == ',' || c == ' ' || c == '#' || c == '+' || c == '-');
        }

        /// <summary>
        /// Business rule: 60-min rate should not be more than 2.5x the 30-min rate.
        /// Example: If 30-min = $50, 60-min should be <= $125 (not $300).
        /// </summary>
        private bool HaveReasonablePricing(UpdateMentorProfileDto dto)
        {
            if (dto.Rate30Min <= 0 || dto.Rate60Min <= 0)
                return true; // Skip if rates not set

            var maxReasonableRate = dto.Rate30Min * 2.5m;
            return dto.Rate60Min <= maxReasonableRate;
        }
    }
}

Step 3: Register Validators in DependencyInjection

Update: Backend/CareerRoute.Core/DependencyInjection.cs

using CareerRoute.Core.Services.Implementations;
using CareerRoute.Core.Services.Interfaces;
using CareerRoute.Core.Validators;
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.Extensions.DependencyInjection;

namespace CareerRoute.Core
{
    public static class DependencyInjection
    {
        public static IServiceCollection AddCoreServices(this IServiceCollection services)
        {
            // ============ SERVICES ============
            services.AddScoped<IMentorService, MentorService>();
            // Add other services here...

            // ============ AUTOMAPPER ============
            services.AddAutoMapper(typeof(DependencyInjection).Assembly);

            // ============ FLUENTVALIDATION ============
            services.AddValidatorsFromAssemblyContaining<UpdateMentorProfileValidator>();
            services.AddFluentValidationAutoValidation();
            services.AddFluentValidationClientsideAdapters();

            return services;
        }
    }
}

Understanding the Code

1. What is AbstractValidator<T>?

  • Base class for FluentValidation validators
  • Generic type: <UpdateMentorProfileDto> means we're validating this DTO
  • Provides fluent API: RuleFor(), Must(), WithMessage(), etc.

2. What is RuleFor(x => x.Bio)?

  • Defines a validation rule for the Bio property
  • Lambda expression: x => refers to the DTO being validated

3. What is .MaximumLength(2000)?

  • Built-in validator: Checks string length
  • Chainable: You can add multiple validators

4. What is .WithMessage()?

  • Custom error message returned if validation fails
  • Overrides default message ("Bio must not exceed 2000 characters")

5. What is .When()?

  • Conditional validation: Only validates if condition is true
  • Example: .When(x => !string.IsNullOrWhiteSpace(x.Bio)) → Only validate Bio if it's not empty

6. What is .WithSeverity(Severity.Warning)?

  • Marks rule as a warning instead of error
  • Warning: Validation passes, but message is included
  • Error: Validation fails, request is rejected

7. What is .Must(HaveReasonablePricing)?

  • Custom validation method
  • Takes a predicate function that returns true (valid) or false (invalid)
  • Useful for complex business rules

8. Why validate tags format?

  • Prevents injection attacks: <script>alert('XSS')</script>
  • Ensures consistent format for parsing
  • Example valid: C#, ASP.NET, Azure
  • Example invalid: C#; DROP TABLE Users;--

9. Why check "reasonable pricing"?

  • User experience: Catches typos (e.g., $5000 instead of $50)
  • Data quality: Prevents obviously wrong data
  • Not a hard rule (just a warning)

10. How does ASP.NET Core use these validators?

  • Automatically validates DTOs before controller actions
  • If invalid: Returns 400 Bad Request with error messages
  • If valid: Controller action executes

Example Validation Output

Invalid Request:

POST /api/mentors/profile
{
  "bio": "Hi",
  "rate30Min": -10,
  "rate60Min": 5000
}

Response:

HTTP 400 Bad Request
{
  "errors": {
    "Bio": ["Bio must be at least 50 characters to provide meaningful information"],
    "Rate30Min": ["30-minute rate must be greater than $0"],
    "Rate60Min": ["60-minute rate should be at least equal to 30-minute rate"],
    "": ["Pricing seems unrealistic. Please verify your rates."]
  }
}

Summary Checklist

  • <input type="checkbox" disabled=""> Install FluentValidation NuGet packages
  • <input type="checkbox" disabled=""> Create UpdateMentorProfileValidator.cs in Core/Validators/
  • <input type="checkbox" disabled=""> Extend AbstractValidator<UpdateMentorProfileDto>
  • <input type="checkbox" disabled=""> Define validation rules for each property
  • <input type="checkbox" disabled=""> Add custom validation methods for complex rules
  • <input type="checkbox" disabled=""> Use .When() for conditional validation
  • <input type="checkbox" disabled=""> Register validators in DependencyInjection.cs
  • <input type="checkbox" disabled=""> Enable FluentValidationAutoValidation() for automatic validation

Task T059: Implement Controller

What is a Controller?

A Controller is the entry point for HTTP requests:

  • Receives HTTP requests (GET, POST, PUT, DELETE)
  • Calls service methods to execute business logic
  • Returns HTTP responses (200 OK, 404 Not Found, etc.)
  • Handles authentication and authorization

Controller Responsibilities

Responsibility Example
Route handling [HttpGet("{id}")]
Input validation Model binding, FluentValidation
Authorization [Authorize], [Authorize(Roles = "Admin")]
Call services await _mentorService.GetMentorProfileAsync()
Transform responses Return DTOs as JSON
Error handling Try-catch, return appropriate status codes

Controllers should be thin! Keep business logic in services.

Implementation Steps

Location: Backend/CareerRoute.API/Controllers/MentorsController.cs

Step 1: Create MentorsController

using CareerRoute.API.Filters;
using CareerRoute.Core.Constants;
using CareerRoute.Core.DTOs.Mentors;
using CareerRoute.Core.Exceptions;
using CareerRoute.Core.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace CareerRoute.API.Controllers
{
    /// <summary>
    /// Controller for mentor-related operations.
    /// Handles HTTP requests for mentor profiles, applications, and search.
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class MentorsController : ControllerBase
    {
        private readonly IMentorService _mentorService;
        private readonly ILogger<MentorsController> _logger;

        /// <summary>
        /// Constructor: Inject dependencies
        /// </summary>
        public MentorsController(
            IMentorService mentorService,
            ILogger<MentorsController> logger)
        {
            _mentorService = mentorService;
            _logger = logger;
        }

        // ============ PUBLIC ENDPOINTS (No Authentication Required) ============

        /// <summary>
        /// Get all approved mentors.
        /// Public endpoint - anyone can view approved mentors.
        /// </summary>
        /// <returns>List of mentor profiles</returns>
        [HttpGet]
        [ProducesResponseType(typeof(IEnumerable<MentorProfileDto>), StatusCodes.Status200OK)]
        public async Task<ActionResult<IEnumerable<MentorProfileDto>>> GetAllMentors()
        {
            _logger.LogInformation("GET /api/mentors - Fetching all approved mentors");

            var mentors = await _mentorService.GetAllApprovedMentorsAsync();

            return Ok(mentors);
        }

        /// <summary>
        /// Get a specific mentor by ID.
        /// Public endpoint - anyone can view mentor profiles.
        /// </summary>
        /// <param name="id">Mentor ID (same as User ID)</param>
        /// <returns>Mentor profile</returns>
        [HttpGet("{id}")]
        [ProducesResponseType(typeof(MentorProfileDto), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<MentorProfileDto>> GetMentorById(string id)
        {
            _logger.LogInformation("GET /api/mentors/{Id} - Fetching mentor profile", id);

            var mentor = await _mentorService.GetMentorProfileAsync(id);

            if (mentor == null)
            {
                _logger.LogWarning("Mentor with ID {Id} not found", id);
                return NotFound(new { message = $"Mentor with ID {id} not found" });
            }

            return Ok(mentor);
        }

        /// <summary>
        /// Search mentors by keywords.
        /// Public endpoint - anyone can search mentors.
        /// </summary>
        /// <param name="searchTerm">Search keywords (searches in bio, expertise, name)</param>
        /// <returns>List of matching mentors</returns>
        [HttpGet("search")]
        [ProducesResponseType(typeof(IEnumerable<MentorProfileDto>), StatusCodes.Status200OK)]
        public async Task<ActionResult<IEnumerable<MentorProfileDto>>> SearchMentors([FromQuery] string searchTerm)
        {
            _logger.LogInformation("GET /api/mentors/search?searchTerm={SearchTerm}", searchTerm);

            var mentors = await _mentorService.SearchMentorsAsync(searchTerm);

            return Ok(mentors);
        }

        /// <summary>
        /// Get top-rated mentors.
        /// Public endpoint - used for homepage "Featured Mentors".
        /// </summary>
        /// <param name="count">Number of mentors to return (default 10)</param>
        /// <returns>List of top-rated mentors</returns>
        [HttpGet("top-rated")]
        [ProducesResponseType(typeof(IEnumerable<MentorProfileDto>), StatusCodes.Status200OK)]
        public async Task<ActionResult<IEnumerable<MentorProfileDto>>> GetTopRatedMentors([FromQuery] int count = 10)
        {
            _logger.LogInformation("GET /api/mentors/top-rated?count={Count}", count);

            if (count <= 0 || count > 100)
            {
                return BadRequest(new { message = "Count must be between 1 and 100" });
            }

            var mentors = await _mentorService.GetTopRatedMentorsAsync(count);

            return Ok(mentors);
        }

        // ============ USER ENDPOINTS (Authentication Required) ============

        /// <summary>
        /// Apply to become a mentor (create mentor profile).
        /// User must be authenticated but not already a mentor.
        /// </summary>
        /// <param name="mentorData">Mentor profile data</param>
        /// <returns>Created mentor profile</returns>
        [HttpPost]
        [Authorize] // Requires logged-in user
        [ProducesResponseType(typeof(MentorProfileDto), StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
        public async Task<ActionResult<MentorProfileDto>> ApplyAsMentor([FromBody] UpdateMentorProfileDto mentorData)
        {
            // Get current user ID from JWT token
            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);

            if (string.IsNullOrEmpty(userId))
            {
                _logger.LogError("User ID not found in token claims");
                return Unauthorized(new { message = "Invalid authentication token" });
            }

            _logger.LogInformation("POST /api/mentors - User {UserId} applying as mentor", userId);

            try
            {
                var createdMentor = await _mentorService.CreateMentorProfileAsync(userId, mentorData);

                _logger.LogInformation("Mentor profile created for user {UserId}", userId);

                // Return 201 Created with Location header
                return CreatedAtAction(
                    nameof(GetMentorById),
                    new { id = createdMentor.Id },
                    createdMentor);
            }
            catch (BadRequestException ex)
            {
                _logger.LogWarning("Bad request: {Message}", ex.Message);
                return BadRequest(new { message = ex.Message });
            }
            catch (NotFoundException ex)
            {
                _logger.LogError("User not found: {Message}", ex.Message);
                return NotFound(new { message = ex.Message });
            }
        }

        /// <summary>
        /// Update mentor profile.
        /// Only the mentor themselves can update their profile.
        /// </summary>
        /// <param name="id">Mentor ID to update</param>
        /// <param name="updateDto">Updated profile data</param>
        /// <returns>Updated mentor profile</returns>
        [HttpPut("{id}")]
        [Authorize] // Requires logged-in user
        [ProducesResponseType(typeof(MentorProfileDto), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
        [ProducesResponseType(StatusCodes.Status403Forbidden)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<MentorProfileDto>> UpdateMentorProfile(
            string id,
            [FromBody] UpdateMentorProfileDto updateDto)
        {
            // Get current user ID from JWT token
            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);

            if (string.IsNullOrEmpty(userId))
            {
                return Unauthorized(new { message = "Invalid authentication token" });
            }

            // Check if user is updating their own profile
            if (userId != id)
            {
                _logger.LogWarning("User {UserId} attempted to update mentor {MentorId}'s profile", userId, id);
                return Forbid(); // 403 Forbidden
            }

            _logger.LogInformation("PUT /api/mentors/{Id} - Updating mentor profile", id);

            try
            {
                var updatedMentor = await _mentorService.UpdateMentorProfileAsync(id, updateDto);

                _logger.LogInformation("Mentor profile updated successfully for ID: {Id}", id);

                return Ok(updatedMentor);
            }
            catch (NotFoundException ex)
            {
                _logger.LogError("Mentor not found: {Message}", ex.Message);
                return NotFound(new { message = ex.Message });
            }
        }

        // ============ ADMIN ENDPOINTS (Admin Role Required) ============

        /// <summary>
        /// Get all mentors pending approval.
        /// Admin-only endpoint.
        /// </summary>
        /// <returns>List of pending mentor applications</returns>
        [HttpGet("pending")]
        [Authorize(Roles = "Admin")] // Only admins
        [ProducesResponseType(typeof(IEnumerable<MentorProfileDto>), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
        [ProducesResponseType(StatusCodes.Status403Forbidden)]
        public async Task<ActionResult<IEnumerable<MentorProfileDto>>> GetPendingMentors()
        {
            _logger.LogInformation("GET /api/mentors/pending - Fetching pending mentor applications");

            var mentors = await _mentorService.GetPendingMentorApplicationsAsync();

            return Ok(mentors);
        }

        /// <summary>
        /// Approve a mentor application.
        /// Admin-only endpoint.
        /// </summary>
        /// <param name="id">Mentor ID to approve</param>
        /// <returns>Success message</returns>
        [HttpPut("{id}/approve")]
        [Authorize(Roles = "Admin")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
        [ProducesResponseType(StatusCodes.Status403Forbidden)]
        public async Task<ActionResult> ApproveMentor(string id)
        {
            _logger.LogInformation("PUT /api/mentors/{Id}/approve - Approving mentor", id);

            try
            {
                await _mentorService.ApproveMentorAsync(id);

                _logger.LogInformation("Mentor {Id} approved successfully", id);

                return Ok(new { message = "Mentor approved successfully" });
            }
            catch (NotFoundException ex)
            {
                _logger.LogError("Mentor not found: {Message}", ex.Message);
                return NotFound(new { message = ex.Message });
            }
        }

        /// <summary>
        /// Reject a mentor application.
        /// Admin-only endpoint.
        /// </summary>
        /// <param name="id">Mentor ID to reject</param>
        /// <param name="reason">Rejection reason (sent to applicant)</param>
        /// <returns>Success message</returns>
        [HttpPut("{id}/reject")]
        [Authorize(Roles = "Admin")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
        [ProducesResponseType(StatusCodes.Status403Forbidden)]
        public async Task<ActionResult> RejectMentor(string id, [FromBody] string reason)
        {
            _logger.LogInformation("PUT /api/mentors/{Id}/reject - Rejecting mentor", id);

            if (string.IsNullOrWhiteSpace(reason))
            {
                return BadRequest(new { message = "Rejection reason is required" });
            }

            try
            {
                await _mentorService.RejectMentorAsync(id, reason);

                _logger.LogInformation("Mentor {Id} rejected successfully", id);

                return Ok(new { message = "Mentor rejected successfully" });
            }
            catch (NotFoundException ex)
            {
                _logger.LogError("Mentor not found: {Message}", ex.Message);
                return NotFound(new { message = ex.Message });
            }
        }
    }
}

Understanding the Code

1. What is [Route("api/[controller]")]?

  • Defines base route: /api/mentors (plural)
  • [controller] is replaced by controller name minus "Controller"
  • MentorsController → route is /api/mentors

2. What is [ApiController]?

  • Enables API-specific features:
    • Automatic model validation (FluentValidation)
    • Automatic 400 Bad Request for invalid models
    • Infers [FromBody], [FromQuery] attributes

3. What is [HttpGet], [HttpPost], etc.?

  • Defines HTTP method for the action
  • [HttpGet] → GET request
  • [HttpPost] → POST request (create)
  • [HttpPut] → PUT request (update)
  • [HttpDelete] → DELETE request (delete)

4. What is [HttpGet("{id}")]?

  • Route parameter: /api/mentors/{id}
  • Example: /api/mentors/abc123id = "abc123"
  • Binds id parameter to method argument

5. What is [Authorize]?

  • Requires authenticated user (JWT token)
  • Without it: Endpoint is public
  • Returns 401 Unauthorized if no token

6. What is [Authorize(Roles = "Admin")]?

  • Requires user to have "Admin" role
  • Returns 403 Forbidden if user is not admin

7. What is User.FindFirstValue(ClaimTypes.NameIdentifier)?

  • Gets current user's ID from JWT token
  • User: Represents authenticated user (from controller base class)
  • ClaimTypes.NameIdentifier: Standard claim for user ID

8. What is CreatedAtAction()?

  • Returns 201 Created with Location header
  • Example:
    HTTP/1.1 201 Created
    Location: /api/mentors/abc123
  • Points to the newly created resource

9. What is [ProducesResponseType]?

  • Documents possible response types for Swagger/OpenAPI
  • Example: [ProducesResponseType(typeof(MentorProfileDto), 200)]
  • Swagger shows "Returns 200 OK with MentorProfileDto"

10. Why wrap responses in anonymous objects?

return NotFound(new { message = "Mentor not found" });
  • Standardized error format
  • Frontend can display: error.message
  • Better than plain string: return NotFound("Mentor not found")

11. Why try-catch in controllers?

  • Catch service-layer exceptions
  • Convert to appropriate HTTP status codes:
    • NotFoundException → 404 Not Found
    • BadRequestException → 400 Bad Request
    • Generic Exception → 500 Internal Server Error (handled by middleware)

API Endpoints Summary

Method Endpoint Auth Description
GET /api/mentors None Get all approved mentors
GET /api/mentors/{id} None Get mentor by ID
GET /api/mentors/search None Search mentors by keywords
GET /api/mentors/top-rated None Get top-rated mentors
POST /api/mentors User Apply to become a mentor
PUT /api/mentors/{id} User Update own mentor profile
GET /api/mentors/pending Admin Get pending mentor applications
PUT /api/mentors/{id}/approve Admin Approve mentor application
PUT /api/mentors/{id}/reject Admin Reject mentor application

Summary Checklist

  • <input type="checkbox" disabled=""> Create MentorsController.cs in API/Controllers/
  • <input type="checkbox" disabled=""> Add [Route] and [ApiController] attributes
  • <input type="checkbox" disabled=""> Inject IMentorService and ILogger
  • <input type="checkbox" disabled=""> Implement public endpoints (no auth)
  • <input type="checkbox" disabled=""> Implement user endpoints ([Authorize])
  • <input type="checkbox" disabled=""> Implement admin endpoints ([Authorize(Roles = "Admin")])
  • <input type="checkbox" disabled=""> Use proper HTTP methods (GET, POST, PUT)
  • <input type="checkbox" disabled=""> Return appropriate status codes (200, 201, 400, 404)
  • <input type="checkbox" disabled=""> Add [ProducesResponseType] for Swagger documentation
  • <input type="checkbox" disabled=""> Handle exceptions and return error messages

Task T060: Create AutoMapper Profile

What is AutoMapper?

AutoMapper is a library that automates object-to-object mapping:

  • Eliminates boilerplate code
  • Example: Convert Mentor entity to MentorProfileDto
  • Convention-based: Maps properties with same names automatically
  • Customizable: You can define custom mappings

Why Use AutoMapper?

Without AutoMapper:

var dto = new MentorProfileDto
{
    Id = mentor.Id,
    FirstName = mentor.User.FirstName,
    LastName = mentor.User.LastName,
    Email = mentor.User.Email,
    Bio = mentor.Bio,
    ExpertiseTags = mentor.ExpertiseTags?.Split(',').ToList() ?? new(),
    // ... 15 more properties
};

With AutoMapper:

var dto = _mapper.Map<MentorProfileDto>(mentor);

Implementation Steps

Location: Backend/CareerRoute.Core/Mappings/MentorProfile.cs

Step 1: Update MentorProfile Mapping Class

using AutoMapper;
using CareerRoute.Core.Domain.Entities;
using CareerRoute.Core.DTOs.Mentors;

namespace CareerRoute.Core.Mappings
{
    /// <summary>
    /// AutoMapper profile for Mentor entity.
    /// Defines mappings between Mentor entity and DTOs.
    /// </summary>
    public class MentorProfile : Profile
    {
        public MentorProfile()
        {
            // ============ ENTITY → DTO MAPPINGS ============

            /// <summary>
            /// Map Mentor entity to MentorProfileDto.
            /// Used when reading mentor data from database.
            /// </summary>
            CreateMap<Mentor, MentorProfileDto>()
                // ---- Map User properties (from navigation property) ----
                .ForMember(dest => dest.FirstName,
                    opt => opt.MapFrom(src => src.User.FirstName))
                .ForMember(dest => dest.LastName,
                    opt => opt.MapFrom(src => src.User.LastName))
                .ForMember(dest => dest.Email,
                    opt => opt.MapFrom(src => src.User.Email))
                .ForMember(dest => dest.ProfilePictureUrl,
                    opt => opt.MapFrom(src => src.User.ProfilePictureUrl))

                // ---- Transform ExpertiseTags from comma-separated string to list ----
                .ForMember(dest => dest.ExpertiseTags,
                    opt => opt.MapFrom(src =>
                        string.IsNullOrWhiteSpace(src.ExpertiseTags)
                            ? new List<string>()
                            : src.ExpertiseTags.Split(',', StringSplitOptions.RemoveEmptyEntries)
                                .Select(tag => tag.Trim())
                                .ToList()))

                // ---- All other properties are mapped automatically by convention ----
                // AutoMapper maps: Id → Id, Bio → Bio, Rate30Min → Rate30Min, etc.
                ;

            /// <summary>
            /// Map UpdateMentorProfileDto to Mentor entity.
            /// Used when updating mentor profile.
            /// </summary>
            CreateMap<UpdateMentorProfileDto, Mentor>()
                // All properties match by name and type, so no custom mapping needed
                // AutoMapper will map: Bio → Bio, Rate30Min → Rate30Min, etc.
                ;

            // ============ REVERSE MAPPING (optional) ============

            /// <summary>
            /// Reverse mapping: MentorProfileDto → Mentor
            /// Useful if you need to reconstruct entity from DTO (rarely needed).
            /// </summary>
            // CreateMap<MentorProfileDto, Mentor>().ReverseMap();
        }
    }
}

Understanding the Code

1. Why extend Profile?

  • Profile is AutoMapper's base class for mapping configurations
  • Each entity/DTO group gets its own profile class
  • Profiles are automatically discovered and registered

2. What is CreateMap<Source, Destination>()?

  • Defines a mapping from Source type to Destination type
  • Example: CreateMap<Mentor, MentorProfileDto>() → Mentor to DTO

3. What is .ForMember()?

  • Configures how a specific destination property is mapped
  • Required when property names don't match or need transformation

4. What is .MapFrom()?

  • Specifies the source value for a property
  • Example:
    .ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.User.FirstName))

    Maps mentor.User.FirstNamedto.FirstName

5. Why split ExpertiseTags?

  • Entity: string? (comma-separated: "C#, ASP.NET, Azure")
  • DTO: List<string> (array: ["C#", "ASP.NET", "Azure"])
  • Frontend prefers arrays for easier rendering

6. What is StringSplitOptions.RemoveEmptyEntries?

  • Removes empty strings from split result
  • Example: "C#, , Azure"["C#", "Azure"] (skips empty)

7. What is .Trim()?

  • Removes leading/trailing spaces
  • Example: " C# ""C#"

8. Why map UpdateMentorProfileDtoMentor?

  • Used when updating entity from DTO
  • Example in service:
    var mentor = await _mentorRepository.GetByIdAsync(id);
    _mapper.Map(updateDto, mentor); // Updates mentor properties from DTO

9. What is .ReverseMap()?

  • Creates two-way mapping: A → B and B → A
  • Example:
    CreateMap<Mentor, MentorProfileDto>().ReverseMap();

    Creates both Mentor → MentorProfileDto and MentorProfileDto → Mentor

10. What if properties match by name?

  • AutoMapper maps them automatically!
  • Example: mentor.Iddto.Id (no configuration needed)

Using AutoMapper in Services

Example 1: Map single entity to DTO

var mentor = await _mentorRepository.GetByIdAsync(id);
var dto = _mapper.Map<MentorProfileDto>(mentor);
return dto;

Example 2: Map collection

var mentors = await _mentorRepository.GetAllAsync();
var dtos = _mapper.Map<IEnumerable<MentorProfileDto>>(mentors);
return dtos;

Example 3: Update entity from DTO

var mentor = await _mentorRepository.GetByIdAsync(id);
_mapper.Map(updateDto, mentor); // Updates mentor's properties
_mentorRepository.Update(mentor);

Testing AutoMapper Configuration

AutoMapper configurations can have errors (typos, missing mappings). Test them:

using Xunit;
using AutoMapper;
using CareerRoute.Core.Mappings;

public class MentorProfileMappingTests
{
    private readonly IMapper _mapper;

    public MentorProfileMappingTests()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.AddProfile<MentorProfile>();
        });

        _mapper = config.CreateMapper();
    }

    [Fact]
    public void AutoMapper_Configuration_IsValid()
    {
        // This will throw exception if configuration is invalid
        _mapper.ConfigurationProvider.AssertConfigurationIsValid();
    }

    [Fact]
    public void Map_Mentor_To_MentorProfileDto_Success()
    {
        // Arrange
        var mentor = new Mentor
        {
            Id = "abc123",
            Bio = "Experienced developer",
            ExpertiseTags = "C#, ASP.NET, Azure",
            Rate30Min = 50,
            User = new ApplicationUser
            {
                FirstName = "John",
                LastName = "Doe",
                Email = "[email protected]"
            }
        };

        // Act
        var dto = _mapper.Map<MentorProfileDto>(mentor);

        // Assert
        Assert.Equal("John", dto.FirstName);
        Assert.Equal("Doe", dto.LastName);
        Assert.Equal(3, dto.ExpertiseTags.Count);
        Assert.Contains("C#", dto.ExpertiseTags);
    }
}

Summary Checklist

  • <input type="checkbox" disabled=""> Update MentorProfile.cs in Core/Mappings/
  • <input type="checkbox" disabled=""> Extend Profile base class
  • <input type="checkbox" disabled=""> Create mapping: MentorMentorProfileDto
  • <input type="checkbox" disabled=""> Create mapping: UpdateMentorProfileDtoMentor
  • <input type="checkbox" disabled=""> Use .ForMember() for custom mappings (User properties, ExpertiseTags)
  • <input type="checkbox" disabled=""> Register AutoMapper in DependencyInjection.cs
  • <input type="checkbox" disabled=""> Test mapping configuration

Testing Your Implementation

Step 1: Create Database Migration

cd Backend/CareerRoute.Infrastructure
dotnet ef migrations add AddMentorEntity --startup-project ../CareerRoute.API
dotnet ef database update --startup-project ../CareerRoute.API

Step 2: Register Services in Dependency Injection

Update CareerRoute.Core/DependencyInjection.cs:

services.AddScoped<IMentorRepository, MentorRepository>();
services.AddScoped<IMentorService, MentorService>();

Update CareerRoute.API/Program.cs:

builder.Services.AddCoreServices(); // Registers Core services
builder.Services.AddInfrastructureServices(builder.Configuration); // Registers repositories

Step 3: Test with Postman or Swagger

1. Start the API:

cd Backend/CareerRoute.API
dotnet run

2. Open Swagger UI:

https://localhost:7001/swagger

3. Test Endpoints:

GET /api/mentors (should return empty array initially)

Response: []

POST /api/mentors (apply as mentor - requires authentication)

Headers:
Authorization: Bearer <your-jwt-token>

Body:
{
  "bio": "I am an experienced software engineer with 10 years in the industry...",
  "expertiseTags": "C#, ASP.NET Core, Azure, Docker",
  "yearsOfExperience": 10,
  "certifications": "Microsoft Certified: Azure Developer Associate",
  "rate30Min": 50.00,
  "rate60Min": 90.00
}

Response: 201 Created
{
  "id": "abc123",
  "firstName": "John",
  "lastName": "Doe",
  "bio": "I am an experienced software engineer...",
  ...
}

GET /api/mentors/abc123 (get specific mentor)

Response: 200 OK
{
  "id": "abc123",
  "firstName": "John",
  ...
}

PUT /api/mentors/abc123 (update own profile)

Headers:
Authorization: Bearer <your-jwt-token>

Body:
{
  "bio": "Updated bio...",
  "rate30Min": 55.00,
  ...
}

Response: 200 OK

Step 4: Verify Database

-- Check Mentors table
SELECT * FROM Mentors;

-- Check relationship with Users
SELECT m.Id, u.FirstName, u.LastName, m.Bio, m.ApprovalStatus
FROM Mentors m
INNER JOIN AspNetUsers u ON m.Id = u.Id;

Common Issues and Solutions

Issue 1: Migration Fails

Error:

The name 'Mentors' is already used by an object.

Solution:
Drop the old table and re-create migration:

dotnet ef migrations remove --startup-project ../CareerRoute.API
dotnet ef migrations add AddMentorEntity --startup-project ../CareerRoute.API
dotnet ef database update --startup-project ../CareerRoute.API

Issue 2: Foreign Key Constraint Error

Error:

The INSERT statement conflicted with the FOREIGN KEY constraint

Solution:

  • Ensure User exists before creating Mentor
  • In service: Check await _userRepository.GetByIdAsync(userId) is not null

Issue 3: AutoMapper Null Reference

Error:

AutoMapper.AutoMapperMappingException: Missing type map configuration

Solution:

  • Ensure MentorProfile is in the same assembly as DependencyInjection
  • Check registration: services.AddAutoMapper(typeof(DependencyInjection).Assembly);

Issue 4: Validation Not Working

Error:

FluentValidation not validating requests

Solution:

  • Ensure FluentValidationAutoValidation() is called in DependencyInjection
  • Check validator is registered: services.AddValidatorsFromAssemblyContaining<UpdateMentorProfileValidator>();

Issue 5: 401 Unauthorized

Error:

401 Unauthorized when calling POST /api/mentors

Solution:

  • Ensure JWT token is included: Authorization: Bearer <token>
  • Check token expiration
  • Verify authentication middleware is configured in Program.cs

Next Steps

Congratulations! You've completed the Mentor feature implementation. Here's what to do next:

  1. Test thoroughly using Postman or Swagger
  2. Create unit tests for MentorService methods
  3. Implement remaining DTOs (MentorListItemDto for search results)
  4. Add Category entity and many-to-many relationship (Task T030, T031)
  5. Enhance search with filters (price range, rating, category)
  6. Implement frontend components to consume these APIs

Summary

You've learned how to:

  • Create domain entities with relationships (Mentor → User)
  • Define DTOs for data transfer
  • Configure Entity Framework mappings
  • Implement Repository Pattern
  • Build Service Layer with business logic
  • Create validators with FluentValidation
  • Implement REST API controllers
  • Use AutoMapper for object transformations

This foundation will help you implement the remaining features (Sessions, Payments, Reviews, etc.) using the same patterns.

Keep coding and keep learning! 🚀