diff --git a/api/CourseRegistration.API/Controllers/DownloadController.cs b/api/CourseRegistration.API/Controllers/DownloadController.cs
new file mode 100644
index 0000000..3e944d5
--- /dev/null
+++ b/api/CourseRegistration.API/Controllers/DownloadController.cs
@@ -0,0 +1,143 @@
+using Microsoft.AspNetCore.Mvc;
+using CourseRegistration.Application.Interfaces;
+using System.Text;
+
+namespace CourseRegistration.API.Controllers;
+
+///
+/// Controller for downloading course, student, and registration data as CSV files
+///
+[ApiController]
+[Route("api/[controller]")]
+public class DownloadController : ControllerBase
+{
+ private readonly ICourseService _courseService;
+ private readonly IStudentService _studentService;
+ private readonly IRegistrationService _registrationService;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the DownloadController
+ ///
+ public DownloadController(
+ ICourseService courseService,
+ IStudentService studentService,
+ IRegistrationService registrationService,
+ ILogger logger)
+ {
+ _courseService = courseService ?? throw new ArgumentNullException(nameof(courseService));
+ _studentService = studentService ?? throw new ArgumentNullException(nameof(studentService));
+ _registrationService = registrationService ?? throw new ArgumentNullException(nameof(registrationService));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// Downloads all courses as a CSV file
+ ///
+ /// CSV file containing course data
+ [HttpGet("courses")]
+ [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
+ public async Task DownloadCourses()
+ {
+ _logger.LogInformation("Downloading courses as CSV");
+
+ var result = await _courseService.GetCoursesAsync(page: 1, pageSize: int.MaxValue);
+ var courses = result.Items;
+
+ var csv = new StringBuilder();
+ csv.AppendLine("CourseId,CourseName,Description,InstructorName,StartDate,EndDate,Schedule,CurrentEnrollment,IsActive,CreatedAt");
+
+ foreach (var course in courses)
+ {
+ csv.AppendLine(string.Join(",",
+ course.CourseId,
+ EscapeCsvField(course.CourseName),
+ EscapeCsvField(course.Description ?? string.Empty),
+ EscapeCsvField(course.InstructorName),
+ course.StartDate.ToString("yyyy-MM-dd"),
+ course.EndDate.ToString("yyyy-MM-dd"),
+ EscapeCsvField(course.Schedule),
+ course.CurrentEnrollment,
+ course.IsActive,
+ course.CreatedAt.ToString("yyyy-MM-dd")));
+ }
+
+ var bytes = Encoding.UTF8.GetBytes(csv.ToString());
+ return File(bytes, "text/csv", $"courses_{DateTime.UtcNow:yyyyMMdd}.csv");
+ }
+
+ ///
+ /// Downloads all students as a CSV file
+ ///
+ /// CSV file containing student data
+ [HttpGet("students")]
+ [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
+ public async Task DownloadStudents()
+ {
+ _logger.LogInformation("Downloading students as CSV");
+
+ var result = await _studentService.GetStudentsAsync(page: 1, pageSize: int.MaxValue);
+ var students = result.Items;
+
+ var csv = new StringBuilder();
+ csv.AppendLine("StudentId,FirstName,LastName,Email,PhoneNumber,DateOfBirth,CreatedAt");
+
+ foreach (var student in students)
+ {
+ csv.AppendLine(string.Join(",",
+ student.StudentId,
+ EscapeCsvField(student.FirstName),
+ EscapeCsvField(student.LastName),
+ EscapeCsvField(student.Email),
+ EscapeCsvField(student.PhoneNumber ?? string.Empty),
+ student.DateOfBirth.ToString("yyyy-MM-dd"),
+ student.CreatedAt.ToString("yyyy-MM-dd")));
+ }
+
+ var bytes = Encoding.UTF8.GetBytes(csv.ToString());
+ return File(bytes, "text/csv", $"students_{DateTime.UtcNow:yyyyMMdd}.csv");
+ }
+
+ ///
+ /// Downloads all registrations as a CSV file
+ ///
+ /// CSV file containing registration data
+ [HttpGet("registrations")]
+ [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
+ public async Task DownloadRegistrations()
+ {
+ _logger.LogInformation("Downloading registrations as CSV");
+
+ var result = await _registrationService.GetRegistrationsAsync(page: 1, pageSize: int.MaxValue);
+ var registrations = result.Items;
+
+ var csv = new StringBuilder();
+ csv.AppendLine("RegistrationId,StudentId,StudentName,CourseId,CourseName,RegistrationDate,Status,Grade,Notes");
+
+ foreach (var reg in registrations)
+ {
+ csv.AppendLine(string.Join(",",
+ reg.RegistrationId,
+ reg.StudentId,
+ EscapeCsvField(reg.Student != null ? $"{reg.Student.FirstName} {reg.Student.LastName}" : string.Empty),
+ reg.CourseId,
+ EscapeCsvField(reg.Course?.CourseName ?? string.Empty),
+ reg.RegistrationDate.ToString("yyyy-MM-dd"),
+ reg.Status,
+ reg.Grade?.ToString() ?? string.Empty,
+ EscapeCsvField(reg.Notes ?? string.Empty)));
+ }
+
+ var bytes = Encoding.UTF8.GetBytes(csv.ToString());
+ return File(bytes, "text/csv", $"registrations_{DateTime.UtcNow:yyyyMMdd}.csv");
+ }
+
+ private static string EscapeCsvField(string field)
+ {
+ if (field.Contains(',') || field.Contains('"') || field.Contains('\n') || field.Contains('\r'))
+ {
+ return $"\"{field.Replace("\"", "\"\"")}\"";
+ }
+ return field;
+ }
+}
diff --git a/api/CourseRegistration.Tests/Controllers/DownloadControllerTests.cs b/api/CourseRegistration.Tests/Controllers/DownloadControllerTests.cs
new file mode 100644
index 0000000..f08067b
--- /dev/null
+++ b/api/CourseRegistration.Tests/Controllers/DownloadControllerTests.cs
@@ -0,0 +1,408 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+using CourseRegistration.API.Controllers;
+using CourseRegistration.Application.DTOs;
+using CourseRegistration.Application.Interfaces;
+using CourseRegistration.Domain.Enums;
+
+namespace CourseRegistration.Tests.Controllers;
+
+///
+/// Unit tests for DownloadController CSV download endpoints
+///
+public class DownloadControllerTests
+{
+ private readonly Mock _mockCourseService;
+ private readonly Mock _mockStudentService;
+ private readonly Mock _mockRegistrationService;
+ private readonly Mock> _mockLogger;
+ private readonly DownloadController _controller;
+
+ public DownloadControllerTests()
+ {
+ _mockCourseService = new Mock();
+ _mockStudentService = new Mock();
+ _mockRegistrationService = new Mock();
+ _mockLogger = new Mock>();
+
+ _controller = new DownloadController(
+ _mockCourseService.Object,
+ _mockStudentService.Object,
+ _mockRegistrationService.Object,
+ _mockLogger.Object);
+ }
+
+ [Fact]
+ public async Task DownloadCourses_ReturnsFileResult_WithCsvContentType()
+ {
+ // Arrange
+ var courses = new PagedResponseDto
+ {
+ Items = new List
+ {
+ new CourseDto
+ {
+ CourseId = Guid.NewGuid(),
+ CourseName = "Introduction to CS",
+ Description = "Basics of CS",
+ InstructorName = "Dr. Smith",
+ StartDate = new DateTime(2024, 1, 15),
+ EndDate = new DateTime(2024, 5, 15),
+ Schedule = "MWF 9:00-10:30",
+ CurrentEnrollment = 25,
+ IsActive = true,
+ CreatedAt = new DateTime(2024, 1, 1)
+ }
+ },
+ TotalItems = 1
+ };
+
+ _mockCourseService
+ .Setup(s => s.GetCoursesAsync(1, int.MaxValue, null, null))
+ .ReturnsAsync(courses);
+
+ // Act
+ var result = await _controller.DownloadCourses();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ Assert.Equal("text/csv", fileResult.ContentType);
+ Assert.StartsWith("courses_", fileResult.FileDownloadName);
+ Assert.EndsWith(".csv", fileResult.FileDownloadName);
+ }
+
+ [Fact]
+ public async Task DownloadCourses_CsvContent_ContainsHeaderRow()
+ {
+ // Arrange
+ var courses = new PagedResponseDto
+ {
+ Items = Enumerable.Empty(),
+ TotalItems = 0
+ };
+
+ _mockCourseService
+ .Setup(s => s.GetCoursesAsync(1, int.MaxValue, null, null))
+ .ReturnsAsync(courses);
+
+ // Act
+ var result = await _controller.DownloadCourses();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ var content = System.Text.Encoding.UTF8.GetString(fileResult.FileContents);
+ Assert.Contains("CourseId,CourseName", content);
+ Assert.Contains("InstructorName", content);
+ }
+
+ [Fact]
+ public async Task DownloadCourses_CsvContent_ContainsCourseData()
+ {
+ // Arrange
+ var courseId = Guid.NewGuid();
+ var courses = new PagedResponseDto
+ {
+ Items = new List
+ {
+ new CourseDto
+ {
+ CourseId = courseId,
+ CourseName = "Web Development",
+ InstructorName = "Prof. Jones",
+ StartDate = new DateTime(2024, 2, 1),
+ EndDate = new DateTime(2024, 6, 1),
+ Schedule = "TTh 14:00-16:00",
+ CurrentEnrollment = 30,
+ IsActive = true,
+ CreatedAt = new DateTime(2024, 1, 10)
+ }
+ },
+ TotalItems = 1
+ };
+
+ _mockCourseService
+ .Setup(s => s.GetCoursesAsync(1, int.MaxValue, null, null))
+ .ReturnsAsync(courses);
+
+ // Act
+ var result = await _controller.DownloadCourses();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ var content = System.Text.Encoding.UTF8.GetString(fileResult.FileContents);
+ Assert.Contains("Web Development", content);
+ Assert.Contains("Prof. Jones", content);
+ Assert.Contains(courseId.ToString(), content);
+ }
+
+ [Fact]
+ public async Task DownloadStudents_ReturnsFileResult_WithCsvContentType()
+ {
+ // Arrange
+ var students = new PagedResponseDto
+ {
+ Items = new List
+ {
+ new StudentDto
+ {
+ StudentId = Guid.NewGuid(),
+ FirstName = "John",
+ LastName = "Doe",
+ Email = "john.doe@example.com",
+ PhoneNumber = "+1-555-0101",
+ DateOfBirth = new DateTime(1995, 5, 15),
+ CreatedAt = new DateTime(2024, 1, 1)
+ }
+ },
+ TotalItems = 1
+ };
+
+ _mockStudentService
+ .Setup(s => s.GetStudentsAsync(1, int.MaxValue))
+ .ReturnsAsync(students);
+
+ // Act
+ var result = await _controller.DownloadStudents();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ Assert.Equal("text/csv", fileResult.ContentType);
+ Assert.StartsWith("students_", fileResult.FileDownloadName);
+ Assert.EndsWith(".csv", fileResult.FileDownloadName);
+ }
+
+ [Fact]
+ public async Task DownloadStudents_CsvContent_ContainsHeaderRow()
+ {
+ // Arrange
+ var students = new PagedResponseDto
+ {
+ Items = Enumerable.Empty(),
+ TotalItems = 0
+ };
+
+ _mockStudentService
+ .Setup(s => s.GetStudentsAsync(1, int.MaxValue))
+ .ReturnsAsync(students);
+
+ // Act
+ var result = await _controller.DownloadStudents();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ var content = System.Text.Encoding.UTF8.GetString(fileResult.FileContents);
+ Assert.Contains("StudentId,FirstName,LastName,Email", content);
+ Assert.Contains("DateOfBirth", content);
+ }
+
+ [Fact]
+ public async Task DownloadStudents_CsvContent_ContainsStudentData()
+ {
+ // Arrange
+ var studentId = Guid.NewGuid();
+ var students = new PagedResponseDto
+ {
+ Items = new List
+ {
+ new StudentDto
+ {
+ StudentId = studentId,
+ FirstName = "Jane",
+ LastName = "Smith",
+ Email = "jane.smith@example.com",
+ DateOfBirth = new DateTime(1996, 8, 20),
+ CreatedAt = new DateTime(2024, 1, 5)
+ }
+ },
+ TotalItems = 1
+ };
+
+ _mockStudentService
+ .Setup(s => s.GetStudentsAsync(1, int.MaxValue))
+ .ReturnsAsync(students);
+
+ // Act
+ var result = await _controller.DownloadStudents();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ var content = System.Text.Encoding.UTF8.GetString(fileResult.FileContents);
+ Assert.Contains("Jane", content);
+ Assert.Contains("Smith", content);
+ Assert.Contains("jane.smith@example.com", content);
+ Assert.Contains(studentId.ToString(), content);
+ }
+
+ [Fact]
+ public async Task DownloadRegistrations_ReturnsFileResult_WithCsvContentType()
+ {
+ // Arrange
+ var registrations = new PagedResponseDto
+ {
+ Items = new List
+ {
+ new RegistrationDto
+ {
+ RegistrationId = Guid.NewGuid(),
+ StudentId = Guid.NewGuid(),
+ CourseId = Guid.NewGuid(),
+ RegistrationDate = DateTime.UtcNow.AddDays(-5),
+ Status = RegistrationStatus.Confirmed,
+ Student = new StudentDto { FirstName = "John", LastName = "Doe", Email = "john@example.com" },
+ Course = new CourseDto { CourseName = "CS101", InstructorName = "Dr. A" }
+ }
+ },
+ TotalItems = 1
+ };
+
+ _mockRegistrationService
+ .Setup(s => s.GetRegistrationsAsync(1, int.MaxValue, null, null, null))
+ .ReturnsAsync(registrations);
+
+ // Act
+ var result = await _controller.DownloadRegistrations();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ Assert.Equal("text/csv", fileResult.ContentType);
+ Assert.StartsWith("registrations_", fileResult.FileDownloadName);
+ Assert.EndsWith(".csv", fileResult.FileDownloadName);
+ }
+
+ [Fact]
+ public async Task DownloadRegistrations_CsvContent_ContainsHeaderRow()
+ {
+ // Arrange
+ var registrations = new PagedResponseDto
+ {
+ Items = Enumerable.Empty(),
+ TotalItems = 0
+ };
+
+ _mockRegistrationService
+ .Setup(s => s.GetRegistrationsAsync(1, int.MaxValue, null, null, null))
+ .ReturnsAsync(registrations);
+
+ // Act
+ var result = await _controller.DownloadRegistrations();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ var content = System.Text.Encoding.UTF8.GetString(fileResult.FileContents);
+ Assert.Contains("RegistrationId", content);
+ Assert.Contains("StudentName", content);
+ Assert.Contains("CourseName", content);
+ Assert.Contains("Status", content);
+ }
+
+ [Fact]
+ public async Task DownloadRegistrations_CsvContent_ContainsRegistrationData()
+ {
+ // Arrange
+ var registrationId = Guid.NewGuid();
+ var registrations = new PagedResponseDto
+ {
+ Items = new List
+ {
+ new RegistrationDto
+ {
+ RegistrationId = registrationId,
+ StudentId = Guid.NewGuid(),
+ CourseId = Guid.NewGuid(),
+ RegistrationDate = new DateTime(2024, 3, 1),
+ Status = RegistrationStatus.Confirmed,
+ Student = new StudentDto { FirstName = "Alice", LastName = "Brown", Email = "alice@example.com" },
+ Course = new CourseDto { CourseName = "Advanced Math", InstructorName = "Prof. Davis" }
+ }
+ },
+ TotalItems = 1
+ };
+
+ _mockRegistrationService
+ .Setup(s => s.GetRegistrationsAsync(1, int.MaxValue, null, null, null))
+ .ReturnsAsync(registrations);
+
+ // Act
+ var result = await _controller.DownloadRegistrations();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ var content = System.Text.Encoding.UTF8.GetString(fileResult.FileContents);
+ Assert.Contains(registrationId.ToString(), content);
+ Assert.Contains("Alice Brown", content);
+ Assert.Contains("Advanced Math", content);
+ Assert.Contains("Confirmed", content);
+ }
+
+ [Fact]
+ public async Task DownloadCourses_CsvField_EscapesCommasInFields()
+ {
+ // Arrange
+ var courses = new PagedResponseDto
+ {
+ Items = new List
+ {
+ new CourseDto
+ {
+ CourseId = Guid.NewGuid(),
+ CourseName = "Math, Science, and Art",
+ InstructorName = "Dr. Smith",
+ Description = "A course covering math, science",
+ StartDate = new DateTime(2024, 1, 1),
+ EndDate = new DateTime(2024, 6, 1),
+ Schedule = "MWF",
+ CreatedAt = new DateTime(2024, 1, 1)
+ }
+ },
+ TotalItems = 1
+ };
+
+ _mockCourseService
+ .Setup(s => s.GetCoursesAsync(1, int.MaxValue, null, null))
+ .ReturnsAsync(courses);
+
+ // Act
+ var result = await _controller.DownloadCourses();
+
+ // Assert
+ var fileResult = Assert.IsType(result);
+ var content = System.Text.Encoding.UTF8.GetString(fileResult.FileContents);
+ // Fields with commas should be quoted
+ Assert.Contains("\"Math, Science, and Art\"", content);
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenCourseServiceIsNull()
+ {
+ // Act & Assert
+ Assert.Throws(() => new DownloadController(
+ null!,
+ _mockStudentService.Object,
+ _mockRegistrationService.Object,
+ _mockLogger.Object));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenStudentServiceIsNull()
+ {
+ // Act & Assert
+ Assert.Throws(() => new DownloadController(
+ _mockCourseService.Object,
+ null!,
+ _mockRegistrationService.Object,
+ _mockLogger.Object));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenRegistrationServiceIsNull()
+ {
+ // Act & Assert
+ Assert.Throws(() => new DownloadController(
+ _mockCourseService.Object,
+ _mockStudentService.Object,
+ null!,
+ _mockLogger.Object));
+ }
+}
diff --git a/api/CourseRegistration.Tests/CourseRegistration.Tests.csproj b/api/CourseRegistration.Tests/CourseRegistration.Tests.csproj
index 17b82fa..f7a658d 100644
--- a/api/CourseRegistration.Tests/CourseRegistration.Tests.csproj
+++ b/api/CourseRegistration.Tests/CourseRegistration.Tests.csproj
@@ -10,6 +10,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/frontend/index.html b/frontend/index.html
index 9773824..bd3903d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -181,6 +181,10 @@
Refresh
+
+