TimeSlot Management - API Contract & Implementation Guide
TimeSlot Management - API Contract & Implementation Guide
Frontend Framework: Angular 20.3.0
Expected Backend: ASP.NET Core 8.0 Web API
Base URL: http://localhost:5000/api
Project: Career Route Mentorship Platform
Purpose: Simple slot-based mentor availability system
Date: 2025-11-12
Table of Contents
PART 1: ENTITY CLASS DEFINITION
TimeSlot Entity Class
File: Backend/CareerRoute.Core/Entities/TimeSlot.cs
namespace CareerRoute.Core.Entities
{
public class TimeSlot
{
// PRIMARY KEY
public int Id { get; set; }
// FOREIGN KEYS (Relations to other tables)
public string MentorId { get; set; } // FK to Mentor (which extends User)
public int? SessionId { get; set; } // FK to Session (nullable - only set when booked)
// DATA COLUMNS
public DateTime StartDateTime { get; set; } // When the slot starts (UTC)
public int DurationMinutes { get; set; } // Duration: 30 or 60 minutes
public bool IsBooked { get; set; } // Quick flag: true if booked, false if available
public DateTime CreatedAt { get; set; } // When slot was created
// NAVIGATION PROPERTIES (EF Core uses these to JOIN tables)
public Mentor Mentor { get; set; } // Relationship: TimeSlot belongs to ONE Mentor
public Session Session { get; set; } // Relationship: TimeSlot linked to ONE Session (when booked)
}
}
Field Descriptions
| Field | Type | Description | Required | Constraints |
|---|---|---|---|---|
Id |
int | Primary key, auto-increment | Yes | Auto-generated |
MentorId |
string (GUID) | Foreign key to Mentor/User | Yes | Max 450 chars |
SessionId |
int? | Foreign key to Session | No | Nullable - only set when booked |
StartDateTime |
DateTime | Slot start time in UTC | Yes | Must be future date/time |
DurationMinutes |
int | Session duration | Yes | Must be 30 or 60 |
IsBooked |
bool | Booking status flag | Yes | Default: false |
CreatedAt |
DateTime | Creation timestamp | Yes | Auto-set on creation |
PART 2: API CONTRACT
Overview
TimeSlot endpoints enable mentors to manage their availability by creating specific bookable time slots, and allow mentees to view available slots when booking sessions. This simple slot-based system eliminates complex recurring patterns and provides clear, explicit availability management.
Authorization Rules:
- Public endpoints: View available slots (no authentication required)
- Mentor endpoints: Create, view, and delete own time slots (authentication required)
- Business rule: Mentors can only delete slots that are NOT booked
Key Business Rules:
- Mentors create specific time slots (date/time + duration)
- Time slots can be 30 or 60 minutes
- Slots must be at least 24 hours in the future
- Once booked (
isBooked = true), slot cannot be deleted - When session is cancelled, slot becomes available again (
isBooked = false,sessionId = null) - Mentees can only book from available slots (not booked, at least 24h ahead)
TimeSlot Lifecycle:
- Available - Created by mentor,
isBooked = false,sessionId = null - Booked - User books session,
isBooked = true,sessionIdset to session ID - Available (again) - Session cancelled,
isBooked = false,sessionId = null
Related Documentation
-
📖 Session Booking: See Session-Payment-Endpoints.md for:
- POST /api/sessions (booking flow updated to use timeSlotId)
- Session cancellation logic (releases time slot)
-
📖 Mentor Profiles: See Mentor-Endpoints.md for:
- GET /api/mentors/{id} (includes availabilityPreview calculated from TimeSlots)
Public Endpoints (No Authentication Required)
1. Get Available Time Slots for a Mentor
Endpoint: GET /api/mentors/{mentorId}/available-slots
Requires: None (public access)
Roles: Public
Path Parameters:
mentorId(string, GUID): Mentor ID
Query Parameters:
startDate(string, ISO 8601 date, optional): Filter slots from this date (default: today)endDate(string, ISO 8601 date, optional): Filter slots until this date (default: 7 days from startDate)durationMinutes(integer, optional): Filter by duration (30 or 60)
Success Response (200):
{
"success": true,
"message": "Available slots retrieved successfully",
"data": {
"mentorId": "cc0e8400-e29b-41d4-a716-446655440007",
"mentorName": "Sarah Johnson",
"availableSlots": [
{
"id": 123,
"startDateTime": "2025-11-15T14:00:00Z",
"endDateTime": "2025-11-15T15:00:00Z",
"durationMinutes": 60,
"price": 45.00
},
{
"id": 124,
"startDateTime": "2025-11-15T16:00:00Z",
"endDateTime": "2025-11-15T16:30:00Z",
"durationMinutes": 30,
"price": 25.00
},
{
"id": 125,
"startDateTime": "2025-11-16T10:00:00Z",
"endDateTime": "2025-11-16T11:00:00Z",
"durationMinutes": 60,
"price": 45.00
}
],
"totalCount": 3,
"dateRange": {
"startDate": "2025-11-15",
"endDate": "2025-11-22"
}
}
}
Error Responses:
-
400 Bad Request:
{ "success": false, "message": "Validation failed", "errors": { "StartDate": ["Start date must be a valid ISO 8601 date"], "DurationMinutes": ["Duration must be 30 or 60 minutes"] }, "statusCode": 400 } -
404 Not Found (Mentor):
{ "success": false, "message": "Mentor not found", "statusCode": 404 } -
404 Not Found (No Slots):
{ "success": false, "message": "No available slots found for the specified date range", "statusCode": 404 }
Backend Behavior:
- Validate mentor exists and is approved
- Default date range: today to 7 days from today if not specified
- Filter TimeSlots WHERE:
- MentorId = {mentorId}
- IsBooked = false
- StartDateTime >= {startDate}
- StartDateTime <= {endDate}
- StartDateTime > DateTime.UtcNow + 24 hours (must be bookable - 24h advance)
- If durationMinutes specified: DurationMinutes = {durationMinutes}
- Order by StartDateTime ASC
- Calculate endDateTime (StartDateTime + DurationMinutes)
- Include price from mentor's rate30Min or rate60Min
- Return 404 if no slots match criteria
Authenticated Endpoints (Mentor Only)
2. Create Time Slot(s)
Endpoint: POST /api/mentors/{mentorId}/time-slots
Requires: Authorization: Bearer {token}
Roles: Mentor (own profile) or Admin
Path Parameters:
mentorId(string, GUID): Mentor ID
Request Body (Single Slot):
{
"startDateTime": "2025-11-15T14:00:00Z",
"durationMinutes": 60
}
Request Body (Batch Create - Multiple Slots):
{
"slots": [
{
"startDateTime": "2025-11-15T14:00:00Z",
"durationMinutes": 60
},
{
"startDateTime": "2025-11-15T16:00:00Z",
"durationMinutes": 30
},
{
"startDateTime": "2025-11-16T10:00:00Z",
"durationMinutes": 60
}
]
}
Field Requirements:
startDateTime(required): ISO 8601 datetime, must be at least 24 hours in futuredurationMinutes(required): Integer, must be 30 or 60slots(for batch): Array of slot objects, max 50 slots per request
Success Response (201) - Single Slot:
{
"success": true,
"message": "Time slot created successfully",
"data": {
"id": 123,
"mentorId": "cc0e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-11-15T14:00:00Z",
"endDateTime": "2025-11-15T15:00:00Z",
"durationMinutes": 60,
"isBooked": false,
"sessionId": null,
"createdAt": "2025-11-12T10:00:00Z"
}
}
Success Response (201) - Batch Create:
{
"success": true,
"message": "3 time slots created successfully",
"data": {
"createdSlots": [
{
"id": 123,
"mentorId": "cc0e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-11-15T14:00:00Z",
"endDateTime": "2025-11-15T15:00:00Z",
"durationMinutes": 60,
"isBooked": false,
"sessionId": null,
"createdAt": "2025-11-12T10:00:00Z"
},
{
"id": 124,
"mentorId": "cc0e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-11-15T16:00:00Z",
"endDateTime": "2025-11-15T16:30:00Z",
"durationMinutes": 30,
"isBooked": false,
"sessionId": null,
"createdAt": "2025-11-12T10:00:00Z"
},
{
"id": 125,
"mentorId": "cc0e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-11-16T10:00:00Z",
"endDateTime": "2025-11-16T11:00:00Z",
"durationMinutes": 60,
"isBooked": false,
"sessionId": null,
"createdAt": "2025-11-12T10:00:00Z"
}
],
"totalCreated": 3
}
}
Error Responses:
-
400 Bad Request (Validation):
{ "success": false, "message": "Validation failed", "errors": { "StartDateTime": ["Time slot must be at least 24 hours in the future"], "DurationMinutes": ["Duration must be 30 or 60 minutes"], "Slots": ["Cannot create more than 50 slots in one request"] }, "statusCode": 400 } -
401 Unauthorized:
{ "success": false, "message": "Invalid authentication token", "statusCode": 401 } -
403 Forbidden:
{ "success": false, "message": "You can only create time slots for your own mentor profile", "statusCode": 403 } -
409 Conflict:
{ "success": false, "message": "A time slot already exists at this date/time", "statusCode": 409 }
Backend Behavior:
- Extract user ID from JWT token
- Verify user owns this mentor profile OR user is Admin
- Validate startDateTime is at least 24 hours in future
- Validate durationMinutes is 30 or 60
- For batch: Validate max 50 slots per request
- Check for existing slot at same startDateTime for this mentor (return 409 if exists)
- Create TimeSlot entity/entities with:
- MentorId = {mentorId}
- IsBooked = false
- SessionId = null
- CreatedAt = DateTime.UtcNow
- Return created slot(s) with calculated endDateTime
- Return 403 if user doesn't own profile and is not Admin
3. Get All Time Slots for Mentor
Endpoint: GET /api/mentors/{mentorId}/time-slots
Requires: Authorization: Bearer {token}
Roles: Mentor (own profile) or Admin
Path Parameters:
mentorId(string, GUID): Mentor ID
Query Parameters:
startDate(string, ISO 8601 date, optional): Filter slots from this date (default: today)endDate(string, ISO 8601 date, optional): Filter slots until this date (default: 30 days from startDate)isBooked(boolean, optional): Filter by booking status (true/false)page(integer, optional): Page number (default: 1, min: 1)pageSize(integer, optional): Items per page (default: 20, min: 1, max: 100)
Success Response (200):
{
"success": true,
"message": "Time slots retrieved successfully",
"data": {
"timeSlots": [
{
"id": 123,
"mentorId": "cc0e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-11-15T14:00:00Z",
"endDateTime": "2025-11-15T15:00:00Z",
"durationMinutes": 60,
"isBooked": false,
"sessionId": null,
"createdAt": "2025-11-12T10:00:00Z",
"canDelete": true
},
{
"id": 124,
"mentorId": "cc0e8400-e29b-41d4-a716-446655440007",
"startDateTime": "2025-11-15T16:00:00Z",
"endDateTime": "2025-11-15T16:30:00Z",
"durationMinutes": 30,
"isBooked": true,
"sessionId": 456,
"session": {
"id": 456,
"menteeFirstName": "John",
"menteeLastName": "Doe",
"status": "Confirmed",
"topic": "Career Planning"
},
"createdAt": "2025-11-12T10:00:00Z",
"canDelete": false
}
],
"pagination": {
"totalCount": 2,
"currentPage": 1,
"pageSize": 20,
"totalPages": 1,
"hasNextPage": false,
"hasPreviousPage": false
},
"summary": {
"totalSlots": 2,
"availableSlots": 1,
"bookedSlots": 1
}
}
}
Error Responses:
-
401 Unauthorized:
{ "success": false, "message": "Invalid authentication token", "statusCode": 401 } -
403 Forbidden:
{ "success": false, "message": "You can only view time slots for your own mentor profile", "statusCode": 403 } -
404 Not Found:
{ "success": false, "message": "No time slots found", "statusCode": 404 }
Backend Behavior:
- Extract user ID from JWT token
- Verify user owns this mentor profile OR user is Admin
- Default date range: today to 30 days from today if not specified
- Filter TimeSlots WHERE:
- MentorId = {mentorId}
- StartDateTime >= {startDate}
- StartDateTime <= {endDate}
- If isBooked specified: IsBooked = {isBooked}
- Order by StartDateTime ASC
- Apply pagination
- For booked slots (isBooked = true), include session details via JOIN
- Calculate canDelete (true if isBooked = false)
- Calculate summary statistics (total, available, booked counts)
- Return 403 if user doesn't own profile and is not Admin
- Return 404 if no slots found
4. Delete Time Slot
Endpoint: DELETE /api/mentors/{mentorId}/time-slots/{slotId}
Requires: Authorization: Bearer {token}
Roles: Mentor (own profile) or Admin
Path Parameters:
mentorId(string, GUID): Mentor IDslotId(integer): Time slot ID to delete
Success Response (200):
{
"success": true,
"message": "Time slot deleted successfully"
}
Error Responses:
-
401 Unauthorized:
{ "success": false, "message": "Invalid authentication token", "statusCode": 401 } -
403 Forbidden (Ownership):
{ "success": false, "message": "You can only delete time slots from your own mentor profile", "statusCode": 403 } -
404 Not Found:
{ "success": false, "message": "Time slot not found", "statusCode": 404 } -
409 Conflict:
{ "success": false, "message": "Cannot delete a booked time slot. Please cancel the session first.", "statusCode": 409 }
Backend Behavior:
- Extract user ID from JWT token
- Verify user owns this mentor profile OR user is Admin
- Fetch TimeSlot by slotId and mentorId
- Check if TimeSlot.IsBooked = true (return 409 if booked)
- Delete TimeSlot from database
- Return success message
- Return 403 if user doesn't own profile and is not Admin
- Return 404 if slot doesn't exist or doesn't belong to this mentor
- Return 409 if slot is booked (prevent deletion)
Model Structures (DTOs)
TimeSlotDto
{
"id": "number",
"mentorId": "string (GUID)",
"startDateTime": "ISO 8601 datetime",
"endDateTime": "ISO 8601 datetime", // Calculated: startDateTime + durationMinutes
"durationMinutes": "number", // 30 or 60
"isBooked": "boolean",
"sessionId": "number | null", // Null if available, session ID if booked
"session": "SessionPreviewDto | null", // Only included for mentor viewing their slots
"createdAt": "ISO 8601 datetime",
"canDelete": "boolean | optional" // Only for mentor view: true if not booked
}
AvailableSlotDto (Lightweight for Public)
{
"id": "number",
"startDateTime": "ISO 8601 datetime",
"endDateTime": "ISO 8601 datetime",
"durationMinutes": "number", // 30 or 60
"price": "decimal" // Mentor's rate30Min or rate60Min
}
CreateTimeSlotDto
{
"startDateTime": "ISO 8601 datetime", // Required
"durationMinutes": "number" // Required: 30 or 60
}
BatchCreateTimeSlotsDto
{
"slots": "CreateTimeSlotDto[]" // Array of slot objects, max 50
}
TimeSlotListResponseDto
{
"timeSlots": "TimeSlotDto[]",
"pagination": {
"totalCount": "number",
"currentPage": "number",
"pageSize": "number",
"totalPages": "number",
"hasNextPage": "boolean",
"hasPreviousPage": "boolean"
},
"summary": {
"totalSlots": "number",
"availableSlots": "number",
"bookedSlots": "number"
}
}
SessionPreviewDto (Nested in TimeSlotDto when booked)
{
"id": "number",
"menteeFirstName": "string",
"menteeLastName": "string",
"status": "string (enum: Pending, Confirmed, InProgress, Completed, Cancelled)",
"topic": "string | null"
}
Validation Rules
StartDateTime
- Required for creation
- Must be ISO 8601 format
- Must be at least 24 hours in the future from current UTC time
- Cannot be in the past
DurationMinutes
- Required for creation
- Must be integer
- Must be exactly 30 or 60 (no other values allowed)
MentorId
- Must be valid GUID format
- Mentor must exist in database
- Mentor must have "Approved" status
Batch Creation
- Max 50 slots per batch request
- All slots must pass individual validation
- If any slot validation fails, entire batch fails (atomic operation)
- Cannot create duplicate slots (same mentorId + startDateTime)
Deletion
- Can only delete own slots (or Admin can delete any)
- Cannot delete if IsBooked = true
- Cannot delete if slot doesn't exist
Date Range Filters
- startDate and endDate must be valid ISO 8601 dates
- endDate must be after startDate if both provided
- Max date range: 90 days
Security Considerations
Authorization
- Verify JWT token on all authenticated endpoints
- Extract user ID from JWT claims
- Verify mentor ownership for create/delete operations
- Admin can manage any mentor's slots
Business Logic Protection
- Prevent deletion of booked slots (409 Conflict)
- Prevent creating slots in the past
- Enforce 24-hour minimum advance booking
- Validate slot doesn't overlap with existing slot (same mentor, same time)
Rate Limiting
- 100 requests per minute per user for slot creation
- 200 requests per minute for public available-slots endpoint
Data Integrity
- Use database transactions for batch operations
- Atomic batch create: all or nothing
- When session is cancelled, automatically update TimeSlot (IsBooked = false, SessionId = null)
Performance
- Index on (MentorId, StartDateTime, IsBooked) for fast filtering
- Cache mentor's available slots for 5 minutes
- Pagination prevents large result sets
Testing Checklist
GET /api/mentors/{mentorId}/available-slots (Public)
- <input type="checkbox" disabled=""> Get available slots without query params (default 7-day range)
- <input type="checkbox" disabled=""> Get slots with custom date range
- <input type="checkbox" disabled=""> Filter by durationMinutes (30 or 60)
- <input type="checkbox" disabled=""> Verify only available slots returned (isBooked = false)
- <input type="checkbox" disabled=""> Verify only future slots returned (>24h from now)
- <input type="checkbox" disabled=""> Verify slots ordered by startDateTime ASC
- <input type="checkbox" disabled=""> Get slots for non-existent mentor (404)
- <input type="checkbox" disabled=""> Get slots when no slots available (404)
- <input type="checkbox" disabled=""> Verify price matches mentor's rates
POST /api/mentors/{mentorId}/time-slots (Create Single)
- <input type="checkbox" disabled=""> Create slot with valid data as mentor
- <input type="checkbox" disabled=""> Create slot with startDateTime <24h in future (400)
- <input type="checkbox" disabled=""> Create slot with invalid duration (not 30 or 60) (400)
- <input type="checkbox" disabled=""> Create slot as different mentor (403)
- <input type="checkbox" disabled=""> Create slot without authentication (401)
- <input type="checkbox" disabled=""> Create duplicate slot at same time (409)
- <input type="checkbox" disabled=""> Verify slot created with isBooked = false
POST /api/mentors/{mentorId}/time-slots (Batch Create)
- <input type="checkbox" disabled=""> Create multiple slots (batch) with valid data
- <input type="checkbox" disabled=""> Create batch >50 slots (400)
- <input type="checkbox" disabled=""> Create batch with one invalid slot - verify all fail (400)
- <input type="checkbox" disabled=""> Verify all slots created in batch have same createdAt
GET /api/mentors/{mentorId}/time-slots (Mentor View)
- <input type="checkbox" disabled=""> Get own slots as mentor
- <input type="checkbox" disabled=""> Get slots with date range filter
- <input type="checkbox" disabled=""> Filter by isBooked status
- <input type="checkbox" disabled=""> Verify pagination works correctly
- <input type="checkbox" disabled=""> Verify booked slots include session details
- <input type="checkbox" disabled=""> Verify canDelete flag is correct
- <input type="checkbox" disabled=""> Verify summary statistics are accurate
- <input type="checkbox" disabled=""> Try to view another mentor's slots (403)
- <input type="checkbox" disabled=""> Get slots without authentication (401)
DELETE /api/mentors/{mentorId}/time-slots/{slotId}
- <input type="checkbox" disabled=""> Delete available slot (isBooked = false) as mentor
- <input type="checkbox" disabled=""> Try to delete booked slot (409)
- <input type="checkbox" disabled=""> Try to delete another mentor's slot (403)
- <input type="checkbox" disabled=""> Delete non-existent slot (404)
- <input type="checkbox" disabled=""> Delete without authentication (401)
- <input type="checkbox" disabled=""> Verify slot removed from database
Sample API Requests
Get Available Slots (Public):
GET http://localhost:5000/api/mentors/cc0e8400-e29b-41d4-a716-446655440007/available-slots
Get Available Slots with Date Range:
GET http://localhost:5000/api/mentors/cc0e8400-e29b-41d4-a716-446655440007/available-slots?startDate=2025-11-15&endDate=2025-11-22
Get Available 60-Minute Slots Only:
GET http://localhost:5000/api/mentors/cc0e8400-e29b-41d4-a716-446655440007/available-slots?durationMinutes=60
Create Single Time Slot:
POST http://localhost:5000/api/mentors/cc0e8400-e29b-41d4-a716-446655440007/time-slots
Authorization: Bearer {access-token}
Content-Type: application/json
{
"startDateTime": "2025-11-15T14:00:00Z",
"durationMinutes": 60
}
Create Multiple Time Slots (Batch):
POST http://localhost:5000/api/mentors/cc0e8400-e29b-41d4-a716-446655440007/time-slots
Authorization: Bearer {access-token}
Content-Type: application/json
{
"slots": [
{
"startDateTime": "2025-11-15T14:00:00Z",
"durationMinutes": 60
},
{
"startDateTime": "2025-11-15T16:00:00Z",
"durationMinutes": 30
},
{
"startDateTime": "2025-11-16T10:00:00Z",
"durationMinutes": 60
}
]
}
Get All Slots for Mentor:
GET http://localhost:5000/api/mentors/cc0e8400-e29b-41d4-a716-446655440007/time-slots?page=1&pageSize=20
Authorization: Bearer {access-token}
Get Only Available Slots for Mentor:
GET http://localhost:5000/api/mentors/cc0e8400-e29b-41d4-a716-446655440007/time-slots?isBooked=false
Authorization: Bearer {access-token}
Delete Time Slot:
DELETE http://localhost:5000/api/mentors/cc0e8400-e29b-41d4-a716-446655440007/time-slots/123
Authorization: Bearer {access-token}
PART 3: BACKEND IMPLEMENTATION GUIDE
Understanding the Relationships
Relationship 1: TimeSlot → Mentor (Many-to-One)
Meaning:
- One mentor can have many time slots
- Each time slot belongs to one mentor
In Code:
// TimeSlot.cs
public class TimeSlot
{
public string MentorId { get; set; } // Foreign Key
public Mentor Mentor { get; set; } // Navigation property
}
// Mentor.cs (you'll need to add this)
public class Mentor
{
public string Id { get; set; }
// ... existing properties ...
// NEW: Add this collection property
public ICollection<TimeSlot> TimeSlots { get; set; } // One mentor has many slots
}
In Database:
Mentors Table (AspNetUsers) TimeSlots Table
┌─────────────┐ ┌─────────────────┐
│ Id (PK) │◄─────────────────────│ MentorId (FK) │
│ FirstName │ │ Id (PK) │
│ LastName │ │ StartDateTime │
└─────────────┘ └─────────────────┘
1 *
(One Mentor) (Many TimeSlots)
How it works:
// EF Core can now do this automatically:
var mentor = await _context.Mentors
.Include(m => m.TimeSlots) // JOIN with TimeSlots table
.FirstOrDefaultAsync(m => m.Id == mentorId);
// Now you can access:
var allMentorSlots = mentor.TimeSlots; // List of TimeSlot objects
Relationship 2: TimeSlot → Session (One-to-One, Optional)
Meaning:
- Each time slot can be linked to zero or one session (nullable)
- When booked,
SessionIdis set; when available,SessionIdis null
In Code:
// TimeSlot.cs
public class TimeSlot
{
public int? SessionId { get; set; } // Nullable FK (null = available)
public Session Session { get; set; } // Navigation property
}
// Session.cs (you'll need to add this)
public class Session
{
public int Id { get; set; }
// ... existing properties ...
// NEW: Add this property
public TimeSlot TimeSlot { get; set; } // One session uses one slot
}
In Database:
TimeSlots Table Sessions Table
┌─────────────────┐ ┌─────────────┐
│ Id (PK) │ │ Id (PK) │
│ SessionId (FK) │──────────────────►│ MentorId │
│ IsBooked │ │ MenteeId │
└─────────────────┘ └─────────────┘
1 1
(One TimeSlot) (One Session)
How it works:
// Get slot with its session details
var slot = await _context.TimeSlots
.Include(ts => ts.Session) // JOIN with Sessions table
.FirstOrDefaultAsync(ts => ts.Id == slotId);
if (slot.SessionId != null)
{
Console.WriteLine($"Booked by: {slot.Session.MenteeId}");
}
Complete Relationship Diagram
User (AspNetUsers)
↓ (1:1)
Mentor
↓ (1:many)
TimeSlot ←──────┐
↓ (1:1) │
Session │
↓ │
Payment │
│
(When cancelled, SessionId = null, slot becomes available again)
Entity Configuration (EF Core)
Complete Configuration Class
File: Backend/CareerRoute.Infrastructure/Data/Configurations/TimeSlotConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using CareerRoute.Core.Entities;
namespace CareerRoute.Infrastructure.Data.Configurations
{
public class TimeSlotConfiguration : IEntityTypeConfiguration<TimeSlot>
{
public void Configure(EntityTypeBuilder<TimeSlot> builder)
{
// Table name
builder.ToTable("TimeSlots");
// Primary Key
builder.HasKey(ts => ts.Id);
// Properties
builder.Property(ts => ts.MentorId)
.IsRequired()
.HasMaxLength(450); // Same as AspNetUsers.Id
builder.Property(ts => ts.StartDateTime)
.IsRequired();
builder.Property(ts => ts.DurationMinutes)
.IsRequired();
builder.Property(ts => ts.IsBooked)
.IsRequired()
.HasDefaultValue(false);
builder.Property(ts => ts.SessionId)
.IsRequired(false); // Nullable
builder.Property(ts => ts.CreatedAt)
.IsRequired();
// RELATIONSHIP 1: TimeSlot → Mentor (Many-to-One)
builder.HasOne(ts => ts.Mentor) // Each TimeSlot has one Mentor
.WithMany(m => m.TimeSlots) // Each Mentor has many TimeSlots
.HasForeignKey(ts => ts.MentorId) // Foreign key column
.OnDelete(DeleteBehavior.Cascade); // If mentor deleted, delete their slots
// RELATIONSHIP 2: TimeSlot → Session (One-to-One, Optional)
builder.HasOne(ts => ts.Session) // Each TimeSlot has one Session
.WithOne(s => s.TimeSlot) // Each Session has one TimeSlot
.HasForeignKey<TimeSlot>(ts => ts.SessionId) // Foreign key column
.OnDelete(DeleteBehavior.SetNull); // If session deleted, keep slot (set SessionId = null)
// INDEXES (for performance)
builder.HasIndex(ts => new { ts.MentorId, ts.StartDateTime })
.HasDatabaseName("IX_TimeSlots_MentorId_StartDateTime");
builder.HasIndex(ts => ts.IsBooked)
.HasDatabaseName("IX_TimeSlots_IsBooked");
builder.HasIndex(ts => ts.SessionId)
.HasDatabaseName("IX_TimeSlots_SessionId");
}
}
}
Updating Existing Entities
1. Update Mentor Entity
File: Backend/CareerRoute.Core/Entities/Mentor.cs
public class Mentor
{
public string Id { get; set; }
// ... all existing properties (Bio, Rate30Min, Rate60Min, etc.) ...
// ADD THIS: Navigation property for TimeSlots
public ICollection<TimeSlot> TimeSlots { get; set; } // One mentor, many slots
}
2. Update Session Entity
File: Backend/CareerRoute.Core/Entities/Session.cs
public class Session
{
public int Id { get; set; }
// ... all existing properties (MentorId, MenteeId, Status, etc.) ...
// ADD THIS: Navigation property for TimeSlot
public TimeSlot TimeSlot { get; set; } // One session, one slot
}
ApplicationDbContext Registration
File: Backend/CareerRoute.Infrastructure/Data/ApplicationDbContext.cs
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
// Existing DbSets
public DbSet<Mentor> Mentors { get; set; }
public DbSet<Session> Sessions { get; set; }
public DbSet<Payment> Payments { get; set; }
// ADD THIS: New DbSet for TimeSlots
public DbSet<TimeSlot> TimeSlots { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Existing configurations
modelBuilder.ApplyConfiguration(new MentorConfiguration());
modelBuilder.ApplyConfiguration(new SessionConfiguration());
// ADD THIS: Apply TimeSlot configuration
modelBuilder.ApplyConfiguration(new TimeSlotConfiguration());
}
}
Database Migration Result
Commands to Run
dotnet ef migrations add AddTimeSlotEntity --project Backend/CareerRoute.Infrastructure --startup-project Backend/CareerRoute.API
dotnet ef database update --project Backend/CareerRoute.Infrastructure --startup-project Backend/CareerRoute.API
Generated SQL
-- Creates the table
CREATE TABLE [TimeSlots] (
[Id] INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
[MentorId] NVARCHAR(450) NOT NULL,
[SessionId] INT NULL,
[StartDateTime] DATETIME2 NOT NULL,
[DurationMinutes] INT NOT NULL,
[IsBooked] BIT NOT NULL DEFAULT 0,
[CreatedAt] DATETIME2 NOT NULL,
-- Foreign key constraints
CONSTRAINT [FK_TimeSlots_Mentors] FOREIGN KEY ([MentorId])
REFERENCES [AspNetUsers]([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_TimeSlots_Sessions] FOREIGN KEY ([SessionId])
REFERENCES [Sessions]([Id]) ON DELETE SET NULL
);
-- Creates indexes for performance
CREATE INDEX [IX_TimeSlots_MentorId_StartDateTime]
ON [TimeSlots]([MentorId], [StartDateTime]);
CREATE INDEX [IX_TimeSlots_IsBooked]
ON [TimeSlots]([IsBooked]);
CREATE INDEX [IX_TimeSlots_SessionId]
ON [TimeSlots]([SessionId]);
Query Examples with Relations
Example 1: Get All Available Slots for a Mentor
var availableSlots = await _context.TimeSlots
.Where(ts => ts.MentorId == mentorId)
.Where(ts => ts.IsBooked == false)
.Where(ts => ts.StartDateTime > DateTime.UtcNow.AddHours(24))
.OrderBy(ts => ts.StartDateTime)
.ToListAsync();
// SQL Generated:
// SELECT * FROM TimeSlots
// WHERE MentorId = @p0 AND IsBooked = 0 AND StartDateTime > @p1
// ORDER BY StartDateTime
Example 2: Book a Slot (Using Relations)
var slot = await _context.TimeSlots
.Include(ts => ts.Mentor) // JOIN to get mentor details
.FirstOrDefaultAsync(ts => ts.Id == slotId);
if (!slot.IsBooked)
{
var session = new Session
{
MentorId = slot.MentorId,
ScheduledStartTime = slot.StartDateTime,
// ... other properties
};
_context.Sessions.Add(session);
await _context.SaveChangesAsync(); // Session gets Id assigned
// Link slot to session
slot.IsBooked = true;
slot.SessionId = session.Id; // Set the foreign key
await _context.SaveChangesAsync(); // Updates TimeSlot row
}
// SQL Generated:
// INSERT INTO Sessions (...) VALUES (...)
// UPDATE TimeSlots SET IsBooked = 1, SessionId = @sessionId WHERE Id = @slotId
Example 3: Cancel Session (Release Slot)
var session = await _context.Sessions
.Include(s => s.TimeSlot) // JOIN to get the time slot
.FirstOrDefaultAsync(s => s.Id == sessionId);
if (session != null)
{
session.Status = SessionStatus.Cancelled;
// Release the time slot (make it available again)
if (session.TimeSlot != null)
{
session.TimeSlot.IsBooked = false;
session.TimeSlot.SessionId = null; // Unlink from session
}
await _context.SaveChangesAsync();
}
// SQL Generated:
// UPDATE Sessions SET Status = 'Cancelled' WHERE Id = @sessionId
// UPDATE TimeSlots SET IsBooked = 0, SessionId = NULL WHERE SessionId = @sessionId
Example 4: Get Mentor with All Slots and Sessions
var mentor = await _context.Mentors
.Include(m => m.TimeSlots) // Include all time slots
.ThenInclude(ts => ts.Session) // For each slot, include linked session
.FirstOrDefaultAsync(m => m.Id == mentorId);
// Now you can access:
foreach (var slot in mentor.TimeSlots)
{
if (slot.IsBooked && slot.Session != null)
{
Console.WriteLine($"Slot booked by: {slot.Session.MenteeId}");
}
else
{
Console.WriteLine($"Slot available at: {slot.StartDateTime}");
}
}
Summary of Relations
| Relationship | Type | Meaning | Delete Behavior |
|---|---|---|---|
| TimeSlot → Mentor | Many-to-One | One mentor has many slots | Cascade (delete slots if mentor deleted) |
| TimeSlot → Session | One-to-One (Optional) | Slot linked to session when booked | Set Null (keep slot if session deleted) |
Key Implementation Points
- TimeSlot entity is the core of the availability system
- MentorId (FK) links slot to mentor (required, non-nullable)
- SessionId (FK) links slot to session (optional, nullable - only set when booked)
- IsBooked flag provides quick filtering for available slots
- Navigation properties enable EF Core to automatically JOIN tables
- Cascade delete for Mentor → TimeSlot (if mentor deleted, slots deleted)
- Set null delete for Session → TimeSlot (if session deleted, slot becomes available again)
- Indexes improve query performance for common filters (mentor, booking status, date)
Next Steps
After implementing this entity:
- Create TimeSlot repository (ITimeSlotRepository, TimeSlotRepository)
- Create TimeSlot service (ITimeSlotService, TimeSlotService)
- Create TimeSlot DTOs (TimeSlotDto, CreateTimeSlotDto, etc.)
- Create TimeSlotController with CRUD endpoints
- Update SessionBookingService to validate against TimeSlot
- Update cancellation logic to release TimeSlot when session cancelled