├── LSC.SmartCertify.API ├── LSC.SmartCertify.API.http ├── Filters │ ├── AdminRoleAttribute.cs │ ├── ValidationFilter.cs │ └── GlobalExceptionFilter.cs ├── MissingImplementations │ └── CURD_Courses_QuestionsAndChoices_Missing_Implementation.txt ├── appsettings.Production.json ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── Middlewares │ ├── ResponseBodyLoggingMiddleware.cs │ ├── RequestBodyLoggingMiddleware.cs │ └── RequestResponseLoggingMiddleware.cs ├── appsettings.json ├── RequestPayloads │ └── Course.txt ├── LSC.SmartCertify.API.csproj ├── LSC.SmartCertify.API.sln ├── Controllers │ ├── ChoicesController.cs │ ├── QuestionsController.cs │ ├── ExamController.cs │ ├── UserController.cs │ └── CourseController.cs └── Program.cs ├── LSC.SmartCertify.Application ├── Interfaces │ ├── Graph │ │ ├── IGraphAuthService.cs │ │ └── IGraphService.cs │ ├── Common │ │ └── IUserClaims.cs │ ├── Storage │ │ └── IStorageService.cs │ ├── ManageUser │ │ ├── IUserProfileService.cs │ │ └── IUserProfileRepository.cs │ ├── Courses │ │ ├── ICourseRepository.cs │ │ └── ICourseService.cs │ ├── QuestionsChoice │ │ ├── IChoiceRepository.cs │ │ ├── IQuestionService.cs │ │ ├── IChoiceService.cs │ │ └── IQuestionRepository.cs │ └── Certification │ │ ├── IExamService.cs │ │ └── IExamRepository.cs ├── Services │ ├── Storage │ │ └── StorageService.cs │ ├── ManageUser │ │ └── UserProfileService.cs │ ├── Common │ │ └── UserClaims.cs │ ├── Graph │ │ ├── GraphAuthService.cs │ │ └── GraphService.cs │ ├── ChoiceService.cs │ ├── QuestionService.cs │ ├── CourseService.cs │ └── Certification │ │ └── ExamService.cs ├── Common │ └── NotFoundException.cs ├── DTOs │ ├── ContactMessage.cs │ ├── ExamResponseDto.cs │ ├── CourseDto.cs │ ├── ChoiceDto.cs │ ├── ExamDto.cs │ ├── UserModel.cs │ └── QuestionDto.cs ├── DTOValidations │ ├── StartExamRequestValidator.cs │ ├── UpdateCourseValidator.cs │ ├── QuestionChoiceValidator.cs │ ├── CreateCourseValidator.cs │ ├── ChoiceValidator.cs │ └── QuestionValidator.cs ├── LSC.SmartCertify.Application.csproj └── MappingProfile.cs ├── LSC.SmartCertify.Domain ├── Entities │ ├── Role.cs │ ├── SmartApp.cs │ ├── ContactU.cs │ ├── BannerInfo.cs │ ├── UserActivityLog.cs │ ├── Choice.cs │ ├── Notification.cs │ ├── UserRole.cs │ ├── Course.cs │ ├── ExamQuestion.cs │ ├── Question.cs │ ├── UserNotification.cs │ ├── Exam.cs │ └── UserProfile.cs └── LSC.SmartCertify.Domain.csproj ├── LSC.SmartCertify.Infrastructure ├── UserProfileRepository.cs ├── LSC.SmartCertify.Infrastructure.csproj ├── ChoiceRepository.cs ├── ReadMe.txt ├── CourseRepository.cs ├── Storage │ └── StorageService.cs ├── BackgroundServices │ ├── OnboardUserBackgroundService.cs │ └── NotificationBackgroundService.cs ├── QuestionRepository.cs ├── SmartCertifyContext.cs ├── Database_Scripts │ └── SmartCertify.sql └── ExamRepository.cs ├── .gitattributes ├── .gitignore └── README.md /LSC.SmartCertify.API/LSC.SmartCertify.API.http: -------------------------------------------------------------------------------- 1 | @LSC.SmartCertify.API_HostAddress = http://localhost:5063 2 | 3 | GET {{LSC.SmartCertify.API_HostAddress}}/weatherforecast/ 4 | Accept: application/json 5 | 6 | ### 7 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/Graph/IGraphAuthService.cs: -------------------------------------------------------------------------------- 1 | namespace LSC.SmartCertify.Application.Interfaces.Graph 2 | { 3 | public interface IGraphAuthService 4 | { 5 | Task GetAccessTokenAsync(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/Storage/StorageService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSC.SmartCertify.Application.Services.Storage 8 | { 9 | internal class StorageService 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Common/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSC.SmartCertify.Application.Common 8 | { 9 | public class NotFoundException : Exception 10 | { 11 | public NotFoundException(string message) : base(message) 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/Graph/IGraphService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.Interfaces.Graph 9 | { 10 | public interface IGraphService 11 | { 12 | public Task> GetADB2CUsersAsync(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/Common/IUserClaims.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSC.SmartCertify.Application.Interfaces.Common 8 | { 9 | public interface IUserClaims 10 | { 11 | string GetCurrentUserEmail(); 12 | string GetCurrentUserId(); 13 | List GetUserRoles(); 14 | int GetUserId(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOs/ContactMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSC.SmartCertify.Application.DTOs 8 | { 9 | public class ContactMessageDto 10 | { 11 | public string Name { get; set; } 12 | public string Email { get; set; } 13 | public string Subject { get; set; } 14 | public string Message { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/Storage/IStorageService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSC.SmartCertify.Application.Interfaces.Storage 8 | { 9 | public interface IStorageService 10 | { 11 | Task GenerateSasTokenAsync(string fileName); 12 | Task UploadAsync(byte[] fileData, string fileName, string containerName = ""); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/ManageUser/IUserProfileService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Domain.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.Interfaces.ManageUser 9 | { 10 | public interface IUserProfileService 11 | { 12 | Task UpdateUserProfilePicture(int userId, string pictureUrl); 13 | 14 | Task GetUserInfoAsync(int userId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/ManageUser/IUserProfileRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Domain.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.Interfaces.ManageUser 9 | { 10 | public interface IUserProfileRepository 11 | { 12 | Task UpdateUserProfilePicture(int userId, string pictureUrl); 13 | 14 | Task GetUserInfoAsync(int userId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/Role.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | public partial class Role 10 | { 11 | [Key] 12 | public int RoleId { get; set; } 13 | 14 | [StringLength(50)] 15 | public string RoleName { get; set; } = null!; 16 | 17 | [InverseProperty("Role")] 18 | public virtual ICollection UserRoles { get; set; } = new List(); 19 | } 20 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/SmartApp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | [Table("SmartApp")] 10 | public partial class SmartApp 11 | { 12 | [Key] 13 | public int SmartAppId { get; set; } 14 | 15 | [StringLength(50)] 16 | public string AppName { get; set; } = null!; 17 | 18 | [InverseProperty("SmartApp")] 19 | public virtual ICollection UserRoles { get; set; } = new List(); 20 | } 21 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/Courses/ICourseRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Domain.Entities; 2 | 3 | namespace LSC.SmartCertify.Application.Interfaces.Courses 4 | { 5 | public interface ICourseRepository 6 | { 7 | Task> GetAllCoursesAsync(); 8 | Task GetCourseByIdAsync(int courseId); 9 | Task IsTitleDuplicateAsync(string title); 10 | Task AddCourseAsync(Course course); 11 | Task UpdateCourseAsync(Course course); 12 | Task DeleteCourseAsync(Course course); 13 | Task UpdateDescriptionAsync(int courseId, string description); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/QuestionsChoice/IChoiceRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Domain.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.Interfaces.QuestionsChoice 9 | { 10 | public interface IChoiceRepository 11 | { 12 | Task> GetAllChoicesAsync(int questionId); 13 | Task GetChoiceByIdAsync(int id); 14 | Task AddChoiceAsync(Choice choice); 15 | Task UpdateChoiceAsync(Choice choice); 16 | Task DeleteChoiceAsync(Choice choice); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/ContactU.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | public partial class ContactU 10 | { 11 | [Key] 12 | public int ContactUsId { get; set; } 13 | 14 | [StringLength(100)] 15 | public string UserName { get; set; } = null!; 16 | 17 | [StringLength(100)] 18 | public string UserEmail { get; set; } = null!; 19 | 20 | [StringLength(2000)] 21 | public string MessageDetail { get; set; } = null!; 22 | } 23 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOs/ExamResponseDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSC.SmartCertify.Application.DTOs 8 | { 9 | // DTO for encapsulated exam data 10 | public class ExamResponseDto 11 | { 12 | public int ExamId { get; set; } 13 | public string Title { get; set; } 14 | public string Status { get; set; } 15 | public DateTime StartedOn { get; set; } 16 | public DateTime? FinishedOn { get; set; } 17 | public List Questions { get; set; } = new(); 18 | } 19 | 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/QuestionsChoice/IQuestionService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | 3 | namespace LSC.SmartCertify.Application.Interfaces.QuestionsChoice 4 | { 5 | public interface IQuestionService 6 | { 7 | Task> GetAllQuestionsAsync(); 8 | Task GetQuestionByIdAsync(int id); 9 | Task AddQuestionAsync(CreateQuestionDto dto); 10 | Task UpdateQuestionAsync(int id, UpdateQuestionDto dto); 11 | Task DeleteQuestionAsync(int id); 12 | Task AddQuestionAndChoicesAsync(QuestionDto dto); 13 | Task UpdateQuestionAndChoicesAsync(int id, QuestionDto dto); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/QuestionsChoice/IChoiceService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.Interfaces.QuestionsChoice 9 | { 10 | public interface IChoiceService 11 | { 12 | Task> GetAllChoicesAsync(int questionId); 13 | Task GetChoiceByIdAsync(int choiceId); 14 | Task AddChoiceAsync(CreateChoiceDto dto); 15 | Task UpdateChoiceAsync(int choiceId, UpdateChoiceDto dto); 16 | Task UpdateUserChoiceAsync(int choiceId, UpdateUserChoice dto); 17 | Task DeleteChoiceAsync(int choiceId); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/QuestionsChoice/IQuestionRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using LSC.SmartCertify.Domain.Entities; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace LSC.SmartCertify.Application.Interfaces.QuestionsChoice 10 | { 11 | public interface IQuestionRepository 12 | { 13 | Task> GetAllQuestionsAsync(); 14 | Task GetQuestionByIdAsync(int id); 15 | Task AddQuestionAsync(Question question); 16 | Task UpdateQuestionAsync(Question question); 17 | Task DeleteQuestionAsync(Question question); 18 | Task UpdateQuestionAndChoicesAsync(int id, QuestionDto dto); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/Courses/ICourseService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.Interfaces.Courses 9 | { 10 | public interface ICourseService 11 | { 12 | Task> GetAllCoursesAsync(); 13 | Task GetCourseByIdAsync(int courseId); 14 | Task IsTitleDuplicateAsync(string title); 15 | Task AddCourseAsync(CreateCourseDto createCourseDto); 16 | Task UpdateCourseAsync(int courseId, UpdateCourseDto updateCourseDto); 17 | Task DeleteCourseAsync(int courseId); 18 | Task UpdateDescriptionAsync(int courseId, string description); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/LSC.SmartCertify.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/BannerInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | [Table("BannerInfo")] 10 | public partial class BannerInfo 11 | { 12 | [Key] 13 | public int BannerId { get; set; } 14 | 15 | [StringLength(100)] 16 | public string Title { get; set; } = null!; 17 | 18 | public string Content { get; set; } = null!; 19 | 20 | [StringLength(500)] 21 | public string? ImageUrl { get; set; } 22 | 23 | public bool IsActive { get; set; } 24 | 25 | public DateTime DisplayFrom { get; set; } 26 | 27 | public DateTime DisplayTo { get; set; } 28 | 29 | public DateTime CreatedOn { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/UserActivityLog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | [Table("UserActivityLog")] 10 | public partial class UserActivityLog 11 | { 12 | [Key] 13 | public int LogId { get; set; } 14 | 15 | public int? UserId { get; set; } 16 | 17 | [StringLength(50)] 18 | public string ActivityType { get; set; } = null!; 19 | 20 | public string? ActivityDescription { get; set; } 21 | 22 | [Column(TypeName = "datetime")] 23 | public DateTime LogDate { get; set; } 24 | 25 | [ForeignKey("UserId")] 26 | [InverseProperty("UserActivityLogs")] 27 | public virtual UserProfile? User { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/Certification/IExamService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.Interfaces.Certification 9 | { 10 | public interface IExamService 11 | { 12 | Task StartExamAsync(int courseId, int userId); 13 | Task UpdateUserChoiceAsync(int id, UpdateUserQuestionChoiceDto dto); 14 | Task> GetExamQuestionsAsync(int examId); 15 | Task> GetUserExamsAsync(int userId); 16 | 17 | Task GetExamMetaData(int examId); 18 | Task SaveExamStatus(ExamFeedbackDto examFeedback); 19 | 20 | Task GetExamDetailsAsync(int examId); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/Choice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | public partial class Choice 10 | { 11 | [Key] 12 | public int ChoiceId { get; set; } 13 | 14 | public int QuestionId { get; set; } 15 | 16 | public string ChoiceText { get; set; } = null!; 17 | 18 | public bool IsCode { get; set; } 19 | 20 | public bool IsCorrect { get; set; } 21 | 22 | [InverseProperty("SelectedChoice")] 23 | public virtual ICollection ExamQuestions { get; set; } = new List(); 24 | 25 | [ForeignKey("QuestionId")] 26 | [InverseProperty("Choices")] 27 | public virtual Question Question { get; set; } = null!; 28 | } 29 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/Notification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | [Table("Notification")] 10 | public partial class Notification 11 | { 12 | [Key] 13 | public int NotificationId { get; set; } 14 | 15 | [StringLength(200)] 16 | public string Subject { get; set; } = null!; 17 | 18 | public string Content { get; set; } = null!; 19 | 20 | public DateTime CreatedOn { get; set; } 21 | 22 | public DateTime ScheduledSendTime { get; set; } 23 | 24 | public bool IsActive { get; set; } 25 | 26 | [InverseProperty("Notification")] 27 | public virtual ICollection UserNotifications { get; set; } = new List(); 28 | } 29 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOValidations/StartExamRequestValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using LSC.SmartCertify.Application.DTOs; 3 | 4 | namespace LSC.SmartCertify.Application.DTOValidations 5 | { 6 | public class StartExamRequestValidator : AbstractValidator 7 | { 8 | public StartExamRequestValidator() 9 | { 10 | RuleFor(x => x.CourseId).GreaterThan(0).WithMessage("CourseId must be greater than 0."); 11 | 12 | RuleFor(x => x.UserId).GreaterThan(0).WithMessage("UserId must be greater than 0."); 13 | 14 | /* 15 | Pending validations for StartExamRequest. 16 | 1. incoming CourseId should be validated against the existing courses in DB 17 | 2. incoming UserId should be validated against the existing users in DB or take it from IUserClaimsPrincipal 18 | */ 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOValidations/UpdateCourseValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Application.Interfaces.Courses; 4 | 5 | namespace LSC.SmartCertify.Application.DTOValidations 6 | { 7 | public class UpdateCourseValidator : AbstractValidator 8 | { 9 | public UpdateCourseValidator(ICourseRepository repository) 10 | { 11 | RuleFor(x => x.Title).NotNull() 12 | .NotEmpty() 13 | .MaximumLength(100) 14 | .MustAsync(async (title, cancellation) => 15 | title == null || !await repository.IsTitleDuplicateAsync(title)) 16 | .WithMessage("The course title must be unique."); 17 | RuleFor(x => x.Description) 18 | .NotNull() 19 | .NotEmpty() 20 | .MaximumLength(500); 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Interfaces/Certification/IExamRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using LSC.SmartCertify.Domain.Entities; 3 | 4 | namespace LSC.SmartCertify.Application.Interfaces.Certification 5 | { 6 | public interface IExamRepository 7 | { 8 | Task> GetRandomQuestionsAsync(int courseId, int count); 9 | Task CreateExamWithQuestionsAsync(Exam exam, List questions); 10 | Task UpdateExamQuestionAsync(ExamQuestion examQuestion); 11 | Task GetExamQuestionAsync(int examId, int examQuestionId); 12 | Task> GetExamQuestionsAsync(int examId); 13 | Task> GetUserExamsAsync(int userId); 14 | Task GetExamMetaDataAsync(int examId); 15 | Task SaveExamStatusAsync(int examId, string feedback); 16 | 17 | Task GetExamDetailsAsync(int examId); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOValidations/QuestionChoiceValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace LSC.SmartCertify.Application.DTOValidations 10 | { 11 | public class QChoiceValidator : AbstractValidator 12 | { 13 | public QChoiceValidator() 14 | { 15 | RuleFor(x => x.ChoiceText).NotEmpty().WithMessage("Choice text is required."); 16 | } 17 | } 18 | 19 | public class CQuestionValidator : AbstractValidator 20 | { 21 | public CQuestionValidator() 22 | { 23 | RuleFor(x => x.QuestionText).NotEmpty().WithMessage("Question text is required."); 24 | RuleFor(x => x.DifficultyLevel).NotEmpty().WithMessage("Difficulty level is required."); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/ManageUser/UserProfileService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.Interfaces.ManageUser; 2 | using LSC.SmartCertify.Domain.Entities; 3 | 4 | namespace LSC.SmartCertify.Application.Services.ManageUser 5 | { 6 | public class UserProfileService : IUserProfileService 7 | { 8 | private readonly IUserProfileRepository userProfileRepository; 9 | 10 | public UserProfileService(IUserProfileRepository userProfileRepository) 11 | { 12 | this.userProfileRepository = userProfileRepository; 13 | } 14 | 15 | public async Task UpdateUserProfilePicture(int userId, string pictureUrl) 16 | { 17 | await userProfileRepository.UpdateUserProfilePicture(userId, pictureUrl); 18 | } 19 | 20 | public Task GetUserInfoAsync(int userId) 21 | { 22 | return userProfileRepository.GetUserInfoAsync(userId); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/UserRole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | [Table("UserRole")] 10 | public partial class UserRole 11 | { 12 | [Key] 13 | public int UserRoleId { get; set; } 14 | 15 | public int RoleId { get; set; } 16 | 17 | public int UserId { get; set; } 18 | 19 | public int SmartAppId { get; set; } 20 | 21 | [ForeignKey("RoleId")] 22 | [InverseProperty("UserRoles")] 23 | public virtual Role Role { get; set; } = null!; 24 | 25 | [ForeignKey("SmartAppId")] 26 | [InverseProperty("UserRoles")] 27 | public virtual SmartApp SmartApp { get; set; } = null!; 28 | 29 | [ForeignKey("UserId")] 30 | [InverseProperty("UserRoles")] 31 | public virtual UserProfile User { get; set; } = null!; 32 | } 33 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Filters/AdminRoleAttribute.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.Interfaces.Common; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using System.Linq; 5 | 6 | namespace LSC.SmartCertify.API.Filters 7 | { 8 | 9 | namespace LSC.OnlineCourse.API.Common 10 | { 11 | public class AdminRoleAttribute : Attribute, IAuthorizationFilter 12 | { 13 | public void OnAuthorization(AuthorizationFilterContext context) 14 | { 15 | var userClaims = context.HttpContext.RequestServices.GetService(); 16 | var userRoles = userClaims?.GetUserRoles(); 17 | if (userRoles == null || !userRoles.Contains("Admin")) 18 | { 19 | // Return 403 Forbidden if the user does not have the Admin role 20 | context.Result = new ForbidResult(); 21 | } 22 | } 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOValidations/CreateCourseValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Application.Interfaces.Courses; 4 | 5 | namespace LSC.SmartCertify.Application.DTOValidations 6 | { 7 | public class CreateCourseValidator : AbstractValidator 8 | { 9 | public CreateCourseValidator(ICourseRepository repository) 10 | { 11 | RuleFor(x => x.Title) 12 | .NotNull() 13 | .NotEmpty() 14 | .MaximumLength(100) 15 | .MaximumLength(100) 16 | .MustAsync(async (title, cancellation) => 17 | !await repository.IsTitleDuplicateAsync(title)) 18 | .WithMessage("The course title must be unique."); 19 | 20 | RuleFor(x => x.Description) 21 | .NotNull() 22 | .NotEmpty() 23 | .MaximumLength(500); 24 | } 25 | 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/MissingImplementations/CURD_Courses_QuestionsAndChoices_Missing_Implementation.txt: -------------------------------------------------------------------------------- 1 | -- Episode 12 - Work for Viewers -- 2 | 3 | As part of episode 12, we started with our CURD operations for Courses, Questions, and Choices. 4 | We have implemented the CURD operations for Courses and Questions and Choices. With courses, we implemented fluent validation and automapper. 5 | 6 | Now, it's time to implement the missing check on fluent validation Questions and Choices. 7 | 8 | 1. We need to check whether the incoming model's questionid, choiceid present in the database or not. 9 | 2. We need to check the model's questionid, choiceid present in the database or not and if not present, we should send 10 | 404 not found response. 11 | 3. Add the missing API response details as part of API documentation. 12 | 4. We need to create a new end point where the question's choice can be an array. 13 | Instead of inserting the choice one by one, we can insert multiple choices at once. 14 | 15 | 16 | -- End of Episode 12 -- 17 | 18 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/Course.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | public partial class Course 10 | { 11 | [Key] 12 | public int CourseId { get; set; } 13 | 14 | [StringLength(100)] 15 | public string Title { get; set; } = null!; 16 | 17 | public string? Description { get; set; } 18 | 19 | public int CreatedBy { get; set; } 20 | 21 | public DateTime CreatedOn { get; set; } 22 | 23 | [ForeignKey("CreatedBy")] 24 | [InverseProperty("Courses")] 25 | public virtual UserProfile CreatedByNavigation { get; set; } = null!; 26 | 27 | [InverseProperty("Course")] 28 | public virtual ICollection Exams { get; set; } = new List(); 29 | 30 | [InverseProperty("Course")] 31 | public virtual ICollection Questions { get; set; } = new List(); 32 | } 33 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "ConnectionStrings": { 9 | "DbContext": "" 10 | }, 11 | "AzureStorage": { 12 | "ConnectionString": "your_storage_Account_connection_String", 13 | "UserContainerName": "userimages", 14 | "ContainerName": "images" 15 | }, 16 | "AzureAdB2C": { 17 | "Instance": "https://smartlearnbykarthik.b2clogin.com", 18 | "ClientId": "f6a6fc5a-1a47-4fdf-9797-d086599be7fe", 19 | "Domain": "smartlearnbykarthik.onmicrosoft.com", 20 | "SignUpSignInPolicyId": "b2c_1_smartcertify_susi", 21 | "Scopes": { 22 | "Read": [ "User.Read", "User.Write" ], 23 | "Write": [ "User.Write" ] 24 | } 25 | }, 26 | "AzureAdB2CGraph": { 27 | "Instance": "https://login.microsoftonline.com/", 28 | "TenantId": "", 29 | "ClientId": "", 30 | "ClientSecret": "", 31 | "GraphEndpoint": "https://graph.microsoft.com/v1.0/" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/LSC.SmartCertify.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/ExamQuestion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | public partial class ExamQuestion 10 | { 11 | [Key] 12 | public int ExamQuestionId { get; set; } 13 | 14 | public int ExamId { get; set; } 15 | 16 | public int QuestionId { get; set; } 17 | 18 | public int? SelectedChoiceId { get; set; } 19 | 20 | public bool? IsCorrect { get; set; } 21 | 22 | public bool? ReviewLater { get; set; } 23 | 24 | [ForeignKey("ExamId")] 25 | [InverseProperty("ExamQuestions")] 26 | public virtual Exam Exam { get; set; } = null!; 27 | 28 | [ForeignKey("QuestionId")] 29 | [InverseProperty("ExamQuestions")] 30 | public virtual Question Question { get; set; } = null!; 31 | 32 | [ForeignKey("SelectedChoiceId")] 33 | [InverseProperty("ExamQuestions")] 34 | public virtual Choice? SelectedChoice { get; set; } 35 | } 36 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "ASPNETCORE_ENVIRONMENT": "Development" 7 | }, 8 | "dotnetRunMessages": true, 9 | "applicationUrl": "http://localhost:5063" 10 | }, 11 | "https": { 12 | "commandName": "Project", 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | }, 16 | "dotnetRunMessages": true, 17 | "applicationUrl": "https://localhost:7209;http://localhost:5063" 18 | }, 19 | "IIS Express": { 20 | "commandName": "IISExpress", 21 | "launchBrowser": true, 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | }, 27 | "$schema": "https://json.schemastore.org/launchsettings.json", 28 | "iisSettings": { 29 | "windowsAuthentication": false, 30 | "anonymousAuthentication": true, 31 | "iisExpress": { 32 | "applicationUrl": "http://localhost:53320/", 33 | "sslPort": 44390 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/Question.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | public partial class Question 10 | { 11 | [Key] 12 | public int QuestionId { get; set; } 13 | 14 | public int CourseId { get; set; } 15 | 16 | public string QuestionText { get; set; } = null!; 17 | 18 | [StringLength(20)] 19 | public string DifficultyLevel { get; set; } = null!; 20 | 21 | public bool IsCode { get; set; } 22 | 23 | public bool HasMultipleAnswers { get; set; } 24 | 25 | [InverseProperty("Question")] 26 | public virtual ICollection Choices { get; set; } = new List(); 27 | 28 | [ForeignKey("CourseId")] 29 | [InverseProperty("Questions")] 30 | public virtual Course Course { get; set; } = null!; 31 | 32 | [InverseProperty("Question")] 33 | public virtual ICollection ExamQuestions { get; set; } = new List(); 34 | } 35 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOs/CourseDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.DTOs 9 | { 10 | public class CourseDto 11 | { 12 | public int CourseId { get; set; } 13 | public string Title { get; set; } = null!; 14 | public string? Description { get; set; } 15 | public bool QuestionsAvailable { get; set; } = false; 16 | public int QuestionCount { get; set; } 17 | } 18 | public class CreateCourseDto 19 | { 20 | public string Title { get; set; } = null!; 21 | public string? Description { get; set; } 22 | } 23 | public class UpdateCourseDto 24 | { 25 | public string? Title { get; set; } 26 | public string? Description { get; set; } 27 | } 28 | public class CourseUpdateDescriptionDto 29 | { 30 | [Required] 31 | [StringLength(500)] 32 | public string Description { get; set; } = string.Empty; 33 | } 34 | 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/UserNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | public partial class UserNotification 10 | { 11 | [Key] 12 | public int UserNotificationId { get; set; } 13 | 14 | public int NotificationId { get; set; } 15 | 16 | public int UserId { get; set; } 17 | 18 | [StringLength(200)] 19 | public string EmailSubject { get; set; } = null!; 20 | 21 | public string EmailContent { get; set; } = null!; 22 | 23 | public bool NotificationSent { get; set; } 24 | 25 | public DateTime? SentOn { get; set; } 26 | 27 | public DateTime CreatedOn { get; set; } 28 | 29 | [ForeignKey("NotificationId")] 30 | [InverseProperty("UserNotifications")] 31 | public virtual Notification Notification { get; set; } = null!; 32 | 33 | [ForeignKey("UserId")] 34 | [InverseProperty("UserNotifications")] 35 | public virtual UserProfile User { get; set; } = null!; 36 | } 37 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/UserProfileRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.Interfaces.ManageUser; 2 | using LSC.SmartCertify.Domain.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace LSC.SmartCertify.Infrastructure 6 | { 7 | public class UserProfileRepository : IUserProfileRepository 8 | { 9 | private readonly SmartCertifyContext _context; 10 | 11 | public UserProfileRepository(SmartCertifyContext context) 12 | { 13 | _context = context; 14 | } 15 | 16 | public async Task UpdateUserProfilePicture(int userId, string pictureUrl) 17 | { 18 | var user = await _context.UserProfiles.FindAsync(userId); 19 | if (user != null) 20 | { 21 | user.ProfileImageUrl = pictureUrl; 22 | await _context.SaveChangesAsync(); 23 | } 24 | } 25 | 26 | public async Task GetUserInfoAsync(int userId) 27 | { 28 | var user = await _context.UserProfiles.FirstOrDefaultAsync(f => f.UserId == userId); 29 | return user; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/Exam.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | public partial class Exam 10 | { 11 | [Key] 12 | public int ExamId { get; set; } 13 | 14 | public int CourseId { get; set; } 15 | 16 | public int UserId { get; set; } 17 | 18 | [StringLength(20)] 19 | public string Status { get; set; } = null!; 20 | 21 | public DateTime StartedOn { get; set; } 22 | 23 | public DateTime? FinishedOn { get; set; } 24 | 25 | [StringLength(2000)] 26 | public string? Feedback { get; set; } 27 | 28 | [ForeignKey("CourseId")] 29 | [InverseProperty("Exams")] 30 | public virtual Course Course { get; set; } = null!; 31 | 32 | [InverseProperty("Exam")] 33 | public virtual ICollection ExamQuestions { get; set; } = new List(); 34 | 35 | [ForeignKey("UserId")] 36 | [InverseProperty("Exams")] 37 | public virtual UserProfile User { get; set; } = null!; 38 | } 39 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "ConnectionStrings": { 9 | "DbContext": "Server=localhost\\SQLEXPRESS;Initial Catalog=SmartCertify;Integrated Security=SSPI; MultipleActiveResultSets=true;TrustServerCertificate=True;" 10 | }, 11 | "AzureStorage": { 12 | "ConnectionString": "your_storage_Account_connection_String", 13 | "UserContainerName": "userimages", 14 | "ContainerName": "images" 15 | }, 16 | "AzureAdB2C": { 17 | "Instance": "https://smartlearnbykarthik.b2clogin.com", 18 | "ClientId": "45dd4eae-55a2-440e-a0f3-c1572232ab50", 19 | "Domain": "smartlearnbykarthik.onmicrosoft.com", 20 | "SignUpSignInPolicyId": "b2c_1_smartcertify_susi", 21 | "Scopes": { 22 | "Read": [ "User.Read", "User.Write" ], 23 | "Write": [ "User.Write" ] 24 | } 25 | }, 26 | "AzureAdB2CGraph": { 27 | "Instance": "https://login.microsoftonline.com/", 28 | "TenantId": "", 29 | "ClientId": "", 30 | "ClientSecret": "", 31 | "GraphEndpoint": "https://graph.microsoft.com/v1.0/" 32 | } 33 | } -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/LSC.SmartCertify.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOValidations/ChoiceValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace LSC.SmartCertify.Application.DTOValidations 10 | { 11 | public class ChoiceValidator : AbstractValidator 12 | { 13 | public ChoiceValidator() 14 | { 15 | RuleFor(c => c.ChoiceText) 16 | .NotEmpty() 17 | .WithMessage("Choice text is required.") 18 | .MaximumLength(200) 19 | .WithMessage("Choice text cannot exceed 200 characters."); 20 | 21 | RuleFor(c => c.QuestionId) 22 | .GreaterThan(0) 23 | .WithMessage("QuestionId must be greater than 0."); 24 | } 25 | } 26 | 27 | public class UpdateChoiceValidator : AbstractValidator 28 | { 29 | public UpdateChoiceValidator() 30 | { 31 | RuleFor(c => c.ChoiceText) 32 | .NotEmpty() 33 | .WithMessage("Choice text is required.") 34 | .MaximumLength(200) 35 | .WithMessage("Choice text cannot exceed 200 characters."); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Filters/ValidationFilter.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | 5 | namespace LSC.SmartCertify.API.Filters 6 | { 7 | public class ValidationFilter : IAsyncActionFilter 8 | { 9 | public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 10 | { 11 | foreach (var argument in context.ActionArguments.Values) 12 | { 13 | if (argument is null) continue; 14 | 15 | var validatorType = typeof(IValidator<>).MakeGenericType(argument.GetType()); 16 | var validator = context.HttpContext.RequestServices.GetService(validatorType) as IValidator; 17 | 18 | if (validator is not null) 19 | { 20 | var validationResult = await validator.ValidateAsync(new ValidationContext(argument)); 21 | if (!validationResult.IsValid) 22 | { 23 | var errors = validationResult.Errors.Select(e => new { e.PropertyName, e.ErrorMessage }); 24 | context.Result = new BadRequestObjectResult(errors); 25 | return; 26 | } 27 | } 28 | } 29 | 30 | await next(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOs/ChoiceDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.DTOs 9 | { 10 | public class ChoiceDto 11 | { 12 | public int ChoiceId { get; set; } 13 | public int QuestionId { get; set; } 14 | public string ChoiceText { get; set; } = string.Empty; 15 | public bool IsCode { get; set; } 16 | public bool IsCorrect { get; set; } 17 | } 18 | 19 | public class CreateChoiceDto 20 | { 21 | [Required] 22 | public int QuestionId { get; set; } 23 | 24 | [Required] 25 | [StringLength(200, ErrorMessage = "Choice text cannot exceed 200 characters.")] 26 | public string ChoiceText { get; set; } = string.Empty; 27 | 28 | public bool IsCode { get; set; } 29 | public bool IsCorrect { get; set; } 30 | } 31 | 32 | public class UpdateChoiceDto: UpdateUserChoice 33 | { 34 | [Required] 35 | [StringLength(200, ErrorMessage = "Choice text cannot exceed 200 characters.")] 36 | public string ChoiceText { get; set; } = string.Empty; 37 | 38 | public bool IsCode { get; set; } 39 | 40 | } 41 | 42 | public class UpdateUserChoice 43 | { 44 | public int ChoiceId { get; set; } 45 | public bool IsCorrect { get; set; } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/ChoiceRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.Interfaces.QuestionsChoice; 2 | using LSC.SmartCertify.Domain.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace LSC.SmartCertify.Infrastructure 6 | { 7 | public class ChoiceRepository : IChoiceRepository 8 | { 9 | private readonly SmartCertifyContext _context; 10 | 11 | public ChoiceRepository(SmartCertifyContext context) 12 | { 13 | _context = context; 14 | } 15 | 16 | public async Task> GetAllChoicesAsync(int questionId) 17 | { 18 | return await _context.Choices.Where(c => c.QuestionId == questionId).ToListAsync(); 19 | } 20 | 21 | public async Task GetChoiceByIdAsync(int id) 22 | { 23 | return await _context.Choices.FirstOrDefaultAsync(c => c.ChoiceId == id); 24 | } 25 | 26 | public async Task AddChoiceAsync(Choice choice) 27 | { 28 | await _context.Choices.AddAsync(choice); 29 | await _context.SaveChangesAsync(); 30 | } 31 | 32 | public async Task UpdateChoiceAsync(Choice choice) 33 | { 34 | _context.Choices.Update(choice); 35 | await _context.SaveChangesAsync(); 36 | } 37 | 38 | public async Task DeleteChoiceAsync(Choice choice) 39 | { 40 | _context.Choices.Remove(choice); 41 | await _context.SaveChangesAsync(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Middlewares/ResponseBodyLoggingMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights.DataContracts; 2 | 3 | namespace LSC.SmartCertify.API.Middlewares 4 | { 5 | public class ResponseBodyLoggingMiddleware : IMiddleware 6 | { 7 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 8 | { 9 | var originalBodyStream = context.Response.Body; 10 | try 11 | { 12 | // Swap out stream with one that is buffered and suports seeking 13 | using var memoryStream = new MemoryStream(); 14 | context.Response.Body = memoryStream; 15 | // hand over to the next middleware and wait for the call to return 16 | 17 | await next(context); 18 | // Read response body from memory stream 19 | memoryStream.Position = 0; 20 | var reader = new StreamReader(memoryStream); 21 | var responseBody = await reader.ReadToEndAsync(); 22 | // Copy body back to so its available to the user agent 23 | memoryStream.Position = 0; 24 | await memoryStream.CopyToAsync(originalBodyStream); 25 | // Write response body to App Insights 26 | var requestTelemetry = context.Features.Get(); 27 | requestTelemetry?.Properties.Add("ResponseBody", responseBody); 28 | } 29 | finally 30 | { 31 | context.Response.Body = originalBodyStream; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Domain/Entities/UserProfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Domain.Entities; 8 | 9 | [Table("UserProfile")] 10 | public partial class UserProfile 11 | { 12 | [Key] 13 | public int UserId { get; set; } 14 | 15 | [StringLength(100)] 16 | public string DisplayName { get; set; } = null!; 17 | 18 | [StringLength(50)] 19 | public string FirstName { get; set; } = null!; 20 | 21 | [StringLength(50)] 22 | public string LastName { get; set; } = null!; 23 | 24 | [StringLength(100)] 25 | public string Email { get; set; } = null!; 26 | 27 | [StringLength(128)] 28 | public string AdObjId { get; set; } = null!; 29 | 30 | [StringLength(500)] 31 | public string? ProfileImageUrl { get; set; } 32 | 33 | public DateTime CreatedOn { get; set; } 34 | 35 | [InverseProperty("CreatedByNavigation")] 36 | public virtual ICollection Courses { get; set; } = new List(); 37 | 38 | [InverseProperty("User")] 39 | public virtual ICollection Exams { get; set; } = new List(); 40 | 41 | [InverseProperty("User")] 42 | public virtual ICollection UserActivityLogs { get; set; } = new List(); 43 | 44 | [InverseProperty("User")] 45 | public virtual ICollection UserNotifications { get; set; } = new List(); 46 | 47 | [InverseProperty("User")] 48 | public virtual ICollection UserRoles { get; set; } = new List(); 49 | } 50 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOs/ExamDto.cs: -------------------------------------------------------------------------------- 1 | namespace LSC.SmartCertify.Application.DTOs 2 | { 3 | public class ExamFeedbackDto 4 | { 5 | public int ExamId { get; set; } 6 | public string Feedback { get; set; } 7 | } 8 | public class ExamDto 9 | { 10 | public int ExamId { get; set; } 11 | public int CourseId { get; set; } 12 | public int UserId { get; set; } 13 | public string Status { get; set; } = null!; 14 | public DateTime StartedOn { get; set; } 15 | public DateTime? FinishedOn { get; set; } 16 | public List QuestionIds { get; set; } = new List(); 17 | } 18 | 19 | public class StartExamRequest 20 | { 21 | public int CourseId { get; set; } 22 | public int UserId { get; set; } 23 | } 24 | 25 | public class UserExamQuestionsDto : UpdateUserQuestionChoiceDto 26 | { 27 | public int QuestionId { get; set; } 28 | public bool IsCorrect { get; set; } 29 | } 30 | public class UpdateUserQuestionChoiceDto 31 | { 32 | public int ExamId { get; set; } 33 | public int ExamQuestionId { get; set; } 34 | public int SelectedChoiceId { get; set; } 35 | public bool ReviewLater { get; set; } 36 | 37 | } 38 | 39 | public class UserExam 40 | { 41 | public int ExamId { get; set; } 42 | public int CourseId { get; set; } 43 | public string Title { get; set; } = null!; 44 | 45 | public string? Description { get; set; } 46 | public string Status { get; set; } = null!; 47 | 48 | public DateTime StartedOn { get; set; } 49 | 50 | public DateTime? FinishedOn { get; set; } 51 | } 52 | } -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Middlewares/RequestBodyLoggingMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights.DataContracts; 2 | using Serilog; 3 | using System.Text; 4 | 5 | namespace LSC.SmartCertify.API.Middlewares 6 | { 7 | public class RequestBodyLoggingMiddleware : IMiddleware 8 | { 9 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 10 | { 11 | var method = context.Request.Method; 12 | // Ensure the request body can be read multiple times 13 | context.Request.EnableBuffering(); 14 | // Only if we are dealing with POST or PUT, GET and others shouldn't have a body 15 | if (context.Request.Body.CanRead && (method == HttpMethods.Post || method == HttpMethods.Put)) 16 | { 17 | // Leave stream open so next middleware can read it 18 | using var reader = new StreamReader( 19 | context.Request.Body, 20 | Encoding.UTF8, 21 | detectEncodingFromByteOrderMarks: false, 22 | bufferSize: 512, leaveOpen: true); 23 | var requestBody = await reader.ReadToEndAsync(); 24 | // Reset stream position, so next middleware can read it 25 | context.Request.Body.Position = 0; 26 | // Write request body to App Insights 27 | var requestTelemetry = context.Features.Get(); 28 | requestTelemetry?.Properties.Add("RequestBody", requestBody); 29 | Log.Information("Request:" + requestBody); 30 | } 31 | // Call next middleware in the pipeline 32 | await next(context); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | }, 7 | "ApplicationInsights": { 8 | "LogLevel": { 9 | "Default": "Information" 10 | } 11 | } 12 | }, 13 | "AllowedHosts": "*", 14 | "ApplicationInsights": { 15 | "ConnectionString": "InstrumentationKey=f947cbea-e6c0-4670-922e-ac2b837c0e9f;IngestionEndpoint=https://eastus2-3.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus2.livediagnostics.monitor.azure.com/;ApplicationId=55867fd1-f35c-4815-9fad-6d14e384aa4a" 16 | }, 17 | "Serilog": { 18 | "Using": [ 19 | "Serilog.Sinks.ApplicationInsights" 20 | ], 21 | "MinimumLevel": { 22 | "Default": "Information", 23 | "Override": { 24 | "Microsoft": "Information", 25 | "System": "Information" 26 | } 27 | }, 28 | "WriteTo": [ 29 | { 30 | "Name": "ApplicationInsights", 31 | "Args": { 32 | "connectionString": "InstrumentationKey=f947cbea-e6c0-4670-922e-ac2b837c0e9f;IngestionEndpoint=https://eastus2-3.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus2.livediagnostics.monitor.azure.com/;ApplicationId=55867fd1-f35c-4815-9fad-6d14e384aa4a", 33 | "telemetryConverter": "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights" 34 | } 35 | }, 36 | { 37 | "Name": "File", 38 | "Args": { 39 | "path": "./bin/logs/log-.txt", 40 | "rollingInterval": "Day" 41 | } 42 | } 43 | ], 44 | "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId", "WithThreadName", "WithEventType" ], 45 | "Properties": { 46 | "Application": "SmartCertify By Karthik | API" 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Domain.Entities; 4 | 5 | namespace LSC.SmartCertify.Application 6 | { 7 | public class MappingProfile : Profile 8 | { 9 | public MappingProfile() 10 | { 11 | CreateMap().ReverseMap(); 12 | CreateMap(); 13 | CreateMap().ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null)); 14 | 15 | 16 | //CreateMap().ReverseMap(); 17 | CreateMap(); 18 | CreateMap(); 19 | 20 | CreateMap().ReverseMap(); 21 | CreateMap(); 22 | CreateMap(); 23 | 24 | CreateMap().ReverseMap(); 25 | CreateMap().ReverseMap(); 26 | 27 | CreateMap() 28 | .ForMember(dest => dest.Questions, opt => opt.Ignore()); // Populate manually 29 | // 30 | // Map Question to QuestionDto and vice versa 31 | CreateMap() 32 | .ForMember(dest => dest.Choices, opt => opt.MapFrom(src => src.Choices)); 33 | 34 | CreateMap() 35 | .ForMember(dest => dest.Choices, opt => opt.Ignore()); // Ignore to handle manually 36 | 37 | // Map Choice to ChoiceDto and vice versa 38 | //CreateMap().ReverseMap(); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/Common/UserClaims.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.Interfaces.Common; 2 | using Microsoft.AspNetCore.Http; 3 | using System.Security.Claims; 4 | 5 | namespace LSC.SmartCertify.Application.Services.Common 6 | { 7 | public class UserClaims : IUserClaims 8 | { 9 | public UserClaims(IHttpContextAccessor httpContextAccessor) 10 | { 11 | HttpContextAccessor = httpContextAccessor; 12 | } 13 | 14 | public IHttpContextAccessor HttpContextAccessor { get; } 15 | 16 | public string GetCurrentContextUserId() 17 | { 18 | return GetCurrentUserId(); 19 | } 20 | private string GetClaimInfo(string property) 21 | { 22 | var propertyData = ""; 23 | var identity = HttpContextAccessor.HttpContext.User.Identity as ClaimsIdentity; 24 | if (identity != null) 25 | { 26 | IEnumerable claims = identity.Claims; 27 | // or 28 | propertyData = identity.Claims.FirstOrDefault(d => d.Type.Contains(property))?.Value; 29 | 30 | } 31 | return propertyData; 32 | } 33 | 34 | public string GetCurrentUserEmail() 35 | { 36 | return GetClaimInfo("emails"); 37 | } 38 | 39 | public string GetCurrentUserId() 40 | { 41 | return GetClaimInfo("objectidentifier"); 42 | } 43 | public List GetUserRoles() 44 | { 45 | var roles = GetClaimInfo("extension_userRoles"); ; 46 | return string.IsNullOrEmpty(roles) ? new List() : roles.Split(',').ToList(); 47 | } 48 | 49 | public int GetUserId() 50 | { 51 | var userId = GetClaimInfo("extension_userId"); 52 | return Convert.ToInt32(userId); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOValidations/QuestionValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace LSC.SmartCertify.Application.DTOValidations 10 | { 11 | public class QuestionValidator : AbstractValidator 12 | { 13 | public QuestionValidator() 14 | { 15 | RuleFor(q => q.QuestionText) 16 | .NotEmpty() 17 | .WithMessage("Question text is required.") 18 | .MaximumLength(500) 19 | .WithMessage("Question text cannot exceed 500 characters."); 20 | 21 | RuleFor(q => q.DifficultyLevel) 22 | .NotEmpty() 23 | .WithMessage("Difficulty level is required.") 24 | .MaximumLength(20) 25 | .WithMessage("Difficulty level cannot exceed 20 characters."); 26 | 27 | RuleFor(q => q.CourseId) 28 | .GreaterThan(0) 29 | .WithMessage("CourseId must be greater than 0."); 30 | } 31 | } 32 | 33 | public class UpdateQuestionValidator : AbstractValidator 34 | { 35 | public UpdateQuestionValidator() 36 | { 37 | RuleFor(q => q.QuestionText) 38 | .NotEmpty() 39 | .WithMessage("Question text is required.") 40 | .MaximumLength(500) 41 | .WithMessage("Question text cannot exceed 500 characters."); 42 | 43 | RuleFor(q => q.DifficultyLevel) 44 | .NotEmpty() 45 | .WithMessage("Difficulty level is required.") 46 | .MaximumLength(20) 47 | .WithMessage("Difficulty level cannot exceed 20 characters."); 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Middlewares/RequestResponseLoggingMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | 3 | namespace LSC.SmartCertify.API.Middlewares 4 | { 5 | public class RequestResponseLoggingMiddleware 6 | { 7 | private readonly RequestDelegate _next; 8 | 9 | 10 | public RequestResponseLoggingMiddleware(RequestDelegate next 11 | ) 12 | { 13 | _next = next; 14 | } 15 | 16 | public async Task Invoke(HttpContext context) 17 | { 18 | // Log the request 19 | Log.Information($"Request: {context.Request.Method} {context.Request.Path}"); 20 | 21 | // Copy the original response body stream 22 | var originalBodyStream = context.Response.Body; 23 | 24 | // Create a new memory stream to capture the response 25 | using (var responseBody = new MemoryStream()) 26 | { 27 | // Set the response body stream to the memory stream 28 | context.Response.Body = responseBody; 29 | 30 | // Continue processing the request 31 | await _next(context); 32 | 33 | // Log the response 34 | var response = await FormatResponse(context.Response); 35 | Log.Information($"Response: {response}"); 36 | 37 | // Copy the captured response to the original response body stream 38 | responseBody.Seek(0, SeekOrigin.Begin); 39 | await responseBody.CopyToAsync(originalBodyStream); 40 | } 41 | } 42 | 43 | private async Task FormatResponse(HttpResponse response) 44 | { 45 | response.Body.Seek(0, SeekOrigin.Begin); 46 | var text = await new StreamReader(response.Body).ReadToEndAsync(); 47 | response.Body.Seek(0, SeekOrigin.Begin); 48 | return $"{response.StatusCode}: {text}"; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/Graph/GraphAuthService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.Interfaces.Graph; 2 | using Microsoft.Extensions.Configuration; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace LSC.SmartCertify.Application.Services.Graph 6 | { 7 | 8 | public class GraphAuthService: IGraphAuthService 9 | { 10 | private readonly IConfiguration _configuration; 11 | private readonly HttpClient _httpClient; 12 | 13 | public GraphAuthService(IConfiguration configuration, HttpClient httpClient) 14 | { 15 | _configuration = configuration; 16 | _httpClient = httpClient; 17 | } 18 | 19 | public async Task GetAccessTokenAsync() 20 | { 21 | var tenantId = _configuration["AzureAdB2CGraph:TenantId"]; 22 | var clientId = _configuration["AzureAdB2CGraph:ClientId"]; 23 | var clientSecret = _configuration["AzureAdB2CGraph:ClientSecret"]; 24 | var tokenUrl = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token"; 25 | 26 | var body = new FormUrlEncodedContent(new[] 27 | { 28 | new KeyValuePair("grant_type", "client_credentials"), 29 | new KeyValuePair("client_id", clientId), 30 | new KeyValuePair("client_secret", clientSecret), 31 | new KeyValuePair("scope", "https://graph.microsoft.com/.default") 32 | }); 33 | 34 | var response = await _httpClient.PostAsync(tokenUrl, body); 35 | var responseContent = await response.Content.ReadAsStringAsync(); 36 | 37 | if (!response.IsSuccessStatusCode) 38 | throw new Exception($"Error getting token: {responseContent}"); 39 | 40 | var token = JObject.Parse(responseContent)["access_token"]?.ToString(); 41 | return token ?? throw new Exception("Token not found in response"); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOs/UserModel.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace LSC.SmartCertify.Application.DTOs 10 | { 11 | public class AdB2CUserModel 12 | { 13 | public string Id { get; set; } 14 | public string GivenName { get; set; } 15 | public string Surname { get; set; } 16 | public string DisplayName { get; set; } 17 | public string UserPrincipalName { get; set; } 18 | public string Mail { get; set; } 19 | public List OtherMails { get; set; } 20 | } 21 | 22 | // Wrapper model for JSON structure 23 | public class GraphApiResponse 24 | { 25 | [JsonProperty("value")] // The key in the API response that holds the list of users 26 | public List Users { get; set; } 27 | } 28 | 29 | public class UpdateUserProfileModel 30 | { 31 | public required int UserId { get; set; } 32 | public IFormFile? Picture { get; set; } 33 | } 34 | 35 | public class UserModel 36 | { 37 | public int UserId { get; set; } 38 | 39 | public string DisplayName { get; set; } = null!; 40 | 41 | public string FirstName { get; set; } = null!; 42 | 43 | public string LastName { get; set; } = null!; 44 | 45 | public string Email { get; set; } = null!; 46 | 47 | public string AdObjId { get; set; } = null!; 48 | public string? ProfilePictureUrl { get; set; } 49 | public string? Bio { get; set; } 50 | public required List UserRoleModel { get; set; } 51 | } 52 | public class UserRoleModel 53 | { 54 | public int UserRoleId { get; set; } 55 | 56 | public int RoleId { get; set; } 57 | public string RoleName { get; set; } = null!; 58 | public int UserId { get; set; } 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/ReadMe.txt: -------------------------------------------------------------------------------- 1 | To check the installed Entity Framework (EF) versions on your machine, you can use the following steps: 2 | 3 | Open a command prompt (cmd) or PowerShell window. 4 | Use the following command to list all installed EF packages for all .NET Core/.NET projects on your machine: 5 | 6 | dotnet tool list --global | findstr "dotnet-ef" 7 | 8 | if you already have some version and wanted to upgrade use this cmd 9 | dotnet tool update --global dotnet-ef --version 9.0.0 10 | or 11 | dotnet tool update --global dotnet-ef 12 | (this installs latest version) 13 | 14 | To install latest EF use 15 | dotnet tool install --global dotnet-ef 16 | 17 | To un install 18 | dotnet tool uninstall --global dotnet-ef 19 | 20 | ********************************* 21 | 22 | To Scaffold database as model to local project use below cmd. 23 | 24 | 25 | use this for locally installed SQL Express Dev DB 26 | dotnet ef dbcontext scaffold "Server=localhost\SQLEXPRESS;Initial Catalog=SmartCertify;Integrated Security=SSPI; MultipleActiveResultSets=true;TrustServerCertificate=True;" Microsoft.EntityFrameworkCore.SqlServer -o EntitiesNew -d 27 | 28 | 29 | 30 | 31 | using (var connection = new SqlConnection(connectionString)) 32 | { 33 | var command = new SqlCommand("UpdateExamCorrectness", connection) 34 | { 35 | CommandType = CommandType.StoredProcedure 36 | }; 37 | command.Parameters.AddWithValue("@ExamId", examId); 38 | connection.Open(); 39 | command.ExecuteNonQuery(); 40 | } 41 | 42 | 43 | CREATE PROCEDURE UpdateExamResults 44 | @ExamId INT 45 | AS 46 | BEGIN 47 | UPDATE eq 48 | SET eq.IsCorrect = CASE 49 | WHEN c.ChoiceId = eq.SelectedChoiceId AND c.IsCorrect = 1 THEN 1 50 | ELSE 0 51 | END 52 | FROM ExamQuestions eq 53 | INNER JOIN Choices c 54 | ON eq.QuestionId = c.QuestionId 55 | WHERE eq.ExamId = @ExamId; 56 | END; 57 | 58 | 59 | await _context.Database.ExecuteSqlRawAsync("EXEC UpdateExamResults @ExamId = {0}", examId); 60 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/CourseRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.Interfaces.Courses; 2 | using LSC.SmartCertify.Domain.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace LSC.SmartCertify.Infrastructure 6 | { 7 | public class CourseRepository : ICourseRepository 8 | { 9 | private readonly SmartCertifyContext _dbContext; 10 | 11 | public CourseRepository(SmartCertifyContext dbContext) 12 | { 13 | _dbContext = dbContext; 14 | } 15 | 16 | public async Task> GetAllCoursesAsync() 17 | { 18 | return await _dbContext.Courses.Include(i=>i.Questions).ToListAsync(); 19 | } 20 | 21 | public async Task GetCourseByIdAsync(int courseId) 22 | { 23 | return await _dbContext.Courses.FindAsync(courseId); 24 | } 25 | 26 | public async Task IsTitleDuplicateAsync(string title) 27 | { 28 | return await _dbContext.Courses.AnyAsync(c => c.Title == title); 29 | } 30 | 31 | public async Task AddCourseAsync(Course course) 32 | { 33 | _dbContext.Courses.Add(course); 34 | await _dbContext.SaveChangesAsync(); 35 | } 36 | 37 | public async Task UpdateCourseAsync(Course course) 38 | { 39 | _dbContext.Courses.Update(course); 40 | await _dbContext.SaveChangesAsync(); 41 | } 42 | 43 | public async Task DeleteCourseAsync(Course course) 44 | { 45 | _dbContext.Courses.Remove(course); 46 | await _dbContext.SaveChangesAsync(); 47 | } 48 | 49 | public async Task UpdateDescriptionAsync(int courseId, string description) 50 | { 51 | var course = await _dbContext.Courses.FindAsync(courseId); 52 | if (course == null) throw new KeyNotFoundException("Course not found."); 53 | 54 | course.Description = description; 55 | await _dbContext.SaveChangesAsync(); 56 | } 57 | 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/RequestPayloads/Course.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Angular", 4 | "description": "Angular is a powerful TypeScript-based platform and framework for building single-page client applications using HTML and TypeScript. It provides tools to build scalable, testable, and maintainable applications with features like two-way data binding, dependency injection, and a modular structure." 5 | }, 6 | { 7 | "title": "React", 8 | "description": "React is a JavaScript library for building fast, responsive user interfaces for web applications. Developed by Facebook, React allows developers to create reusable UI components, manage application state effectively, and render dynamic views efficiently." 9 | }, 10 | { 11 | "title": "DotNet Core", 12 | "description": ".NET Core is a cross-platform, high-performance, and open-source framework for building modern, cloud-based, and internet-connected applications. It supports multiple languages like C#, F#, and VB.NET and enables developers to deploy applications across Windows, Linux, and macOS." 13 | }, 14 | { 15 | "title": "EF Core", 16 | "description": "Entity Framework Core (EF Core) is a lightweight, extensible, and cross-platform Object-Relational Mapper (ORM) for .NET. It simplifies database access by allowing developers to work with data as strongly-typed objects and LINQ queries rather than raw SQL queries." 17 | }, 18 | { 19 | "title": "JavaScript", 20 | "description": "JavaScript is the language of the web, enabling developers to create dynamic and interactive content. It powers front-end frameworks, back-end development (via Node.js), and is essential for building modern, responsive, and scalable applications." 21 | }, 22 | { 23 | "title": "C#", 24 | "description": "C# is a modern, object-oriented programming language developed by Microsoft. It is widely used for building Windows applications, web applications, games using Unity, and enterprise-grade software with .NET technologies." 25 | }, 26 | { 27 | "title": "Azure", 28 | "description": "Microsoft Azure is a cloud computing platform offering services like virtual machines, storage, AI tools, and serverless computing. It helps businesses scale their applications, manage resources efficiently, and deploy cloud-native solutions with ease." 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/LSC.SmartCertify.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/DTOs/QuestionDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSC.SmartCertify.Application.DTOs 9 | { 10 | public class ExamQuestionDto 11 | { 12 | public int QuestionId { get; set; } 13 | public string QuestionText { get; set; } = string.Empty; 14 | public string DifficultyLevel { get; set; } = string.Empty; 15 | public bool IsCode { get; set; } 16 | public bool HasMultipleAnswers { get; set; } 17 | public List Choices { get; set; } = new(); 18 | } 19 | 20 | public class QuestionDto 21 | { 22 | public int QuestionId { get; set; } 23 | public int CourseId { get; set; } 24 | public string QuestionText { get; set; } = string.Empty; 25 | public string DifficultyLevel { get; set; } = string.Empty; 26 | public bool IsCode { get; set; } 27 | public bool HasMultipleAnswers { get; set; } 28 | public List Choices { get; set; } = new(); 29 | 30 | } 31 | 32 | public class UserExamQuestionDto 33 | { 34 | public bool IsCorrect { get; set; } 35 | public string QuestionText { get; set; } = string.Empty; 36 | public string DifficultyLevel { get; set; } = string.Empty; 37 | } 38 | 39 | public class CreateQuestionDto 40 | { 41 | [Required] 42 | public int CourseId { get; set; } 43 | 44 | [Required] 45 | [StringLength(500, ErrorMessage = "Question text cannot exceed 500 characters.")] 46 | public string QuestionText { get; set; } = string.Empty; 47 | 48 | [Required] 49 | [StringLength(20, ErrorMessage = "Difficulty level cannot exceed 20 characters.")] 50 | public string DifficultyLevel { get; set; } = string.Empty; 51 | 52 | public bool IsCode { get; set; } 53 | public bool HasMultipleAnswers { get; set; } 54 | } 55 | 56 | public class UpdateQuestionDto 57 | { 58 | [Required] 59 | [StringLength(500, ErrorMessage = "Question text cannot exceed 500 characters.")] 60 | public string QuestionText { get; set; } = string.Empty; 61 | 62 | [Required] 63 | [StringLength(20, ErrorMessage = "Difficulty level cannot exceed 20 characters.")] 64 | public string DifficultyLevel { get; set; } = string.Empty; 65 | 66 | public bool IsCode { get; set; } 67 | public bool HasMultipleAnswers { get; set; } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/LSC.SmartCertify.API.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.12.35527.113 d17.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LSC.SmartCertify.API", "LSC.SmartCertify.API.csproj", "{2A9F9A74-7B96-434E-A62B-9E50C2FFAC40}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LSC.SmartCertify.Domain", "..\LSC.SmartCertify.Domain\LSC.SmartCertify.Domain.csproj", "{D98FEF01-0B5F-449A-9CCD-74D12E93E37D}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LSC.SmartCertify.Infrastructure", "..\LSC.SmartCertify.Infrastructure\LSC.SmartCertify.Infrastructure.csproj", "{8C8E8845-05AB-4038-9E86-EB682A2D0B8A}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LSC.SmartCertify.Application", "..\LSC.SmartCertify.Application\LSC.SmartCertify.Application.csproj", "{48E5C502-B19A-4B4B-B2D4-CC68256DC26F}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {2A9F9A74-7B96-434E-A62B-9E50C2FFAC40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2A9F9A74-7B96-434E-A62B-9E50C2FFAC40}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2A9F9A74-7B96-434E-A62B-9E50C2FFAC40}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2A9F9A74-7B96-434E-A62B-9E50C2FFAC40}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {D98FEF01-0B5F-449A-9CCD-74D12E93E37D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {D98FEF01-0B5F-449A-9CCD-74D12E93E37D}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {D98FEF01-0B5F-449A-9CCD-74D12E93E37D}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {D98FEF01-0B5F-449A-9CCD-74D12E93E37D}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {8C8E8845-05AB-4038-9E86-EB682A2D0B8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {8C8E8845-05AB-4038-9E86-EB682A2D0B8A}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {8C8E8845-05AB-4038-9E86-EB682A2D0B8A}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {8C8E8845-05AB-4038-9E86-EB682A2D0B8A}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {48E5C502-B19A-4B4B-B2D4-CC68256DC26F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {48E5C502-B19A-4B4B-B2D4-CC68256DC26F}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {48E5C502-B19A-4B4B-B2D4-CC68256DC26F}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {48E5C502-B19A-4B4B-B2D4-CC68256DC26F}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/ChoiceService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Application.Interfaces.QuestionsChoice; 4 | using LSC.SmartCertify.Domain.Entities; 5 | 6 | namespace LSC.SmartCertify.Application.Services 7 | { 8 | public class ChoiceService : IChoiceService 9 | { 10 | private readonly IChoiceRepository _choiceRepository; 11 | private readonly IMapper _mapper; 12 | 13 | public ChoiceService(IChoiceRepository choiceRepository, IMapper mapper) 14 | { 15 | _choiceRepository = choiceRepository; 16 | _mapper = mapper; 17 | } 18 | 19 | public async Task> GetAllChoicesAsync(int questionId) 20 | { 21 | var choices = await _choiceRepository.GetAllChoicesAsync(questionId); 22 | return _mapper.Map>(choices); 23 | } 24 | 25 | public async Task GetChoiceByIdAsync(int choiceId) 26 | { 27 | var choice = await _choiceRepository.GetChoiceByIdAsync(choiceId); 28 | return choice != null ? _mapper.Map(choice) : null; 29 | } 30 | 31 | public async Task AddChoiceAsync(CreateChoiceDto dto) 32 | { 33 | var choice = _mapper.Map(dto); 34 | await _choiceRepository.AddChoiceAsync(choice); 35 | } 36 | 37 | public async Task UpdateChoiceAsync(int choiceId, UpdateChoiceDto dto) 38 | { 39 | var existingChoice = await _choiceRepository.GetChoiceByIdAsync(choiceId); 40 | if (existingChoice == null) 41 | throw new KeyNotFoundException($"Choice with ID {choiceId} not found."); 42 | 43 | _mapper.Map(dto, existingChoice); 44 | await _choiceRepository.UpdateChoiceAsync(existingChoice); 45 | } 46 | 47 | public async Task UpdateUserChoiceAsync(int choiceId, UpdateUserChoice dto) 48 | { 49 | var existingChoice = await _choiceRepository.GetChoiceByIdAsync(choiceId); 50 | if (existingChoice == null) 51 | throw new KeyNotFoundException($"Choice with ID {choiceId} not found."); 52 | 53 | _mapper.Map(dto, existingChoice); 54 | await _choiceRepository.UpdateChoiceAsync(existingChoice); 55 | } 56 | 57 | public async Task DeleteChoiceAsync(int choiceId) 58 | { 59 | var choice = await _choiceRepository.GetChoiceByIdAsync(choiceId); 60 | if (choice == null) 61 | throw new KeyNotFoundException($"Choice with ID {choiceId} not found."); 62 | 63 | await _choiceRepository.DeleteChoiceAsync(choice); 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Controllers/ChoicesController.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using LSC.SmartCertify.Application.Interfaces.QuestionsChoice; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Identity.Web.Resource; 6 | 7 | namespace LSC.SmartCertify.API.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | [ApiController] 11 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Read")] 12 | [Authorize] 13 | public class ChoicesController : ControllerBase 14 | { 15 | private readonly IChoiceService _service; 16 | 17 | public ChoicesController(IChoiceService service) 18 | { 19 | _service = service; 20 | } 21 | 22 | [HttpGet("{questionId}")] 23 | public async Task>> GetChoices(int questionId) 24 | { 25 | return Ok(await _service.GetAllChoicesAsync(questionId)); 26 | } 27 | 28 | [HttpGet("{questionId}/{id}")] 29 | public async Task> GetChoice(int questionId, int id) 30 | { 31 | var choice = await _service.GetChoiceByIdAsync(id); 32 | return choice == null ? NotFound() : Ok(choice); 33 | } 34 | 35 | [HttpPost] 36 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 37 | [Authorize] 38 | public async Task CreateChoice([FromBody] CreateChoiceDto dto) 39 | { 40 | await _service.AddChoiceAsync(dto); 41 | return Created(); //CreatedAtAction(nameof(GetChoices), new { questionId = dto.QuestionId }); 42 | } 43 | 44 | [HttpPut("{id}")] 45 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 46 | [Authorize] 47 | public async Task UpdateChoice(int id, [FromBody] UpdateChoiceDto dto) 48 | { 49 | await _service.UpdateChoiceAsync(id, dto); 50 | return NoContent(); 51 | } 52 | 53 | [HttpPatch("{id}")] 54 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 55 | [Authorize] 56 | public async Task UpdateUserChoice(int id, [FromBody] UpdateUserChoice dto) 57 | { 58 | await _service.UpdateUserChoiceAsync(id, dto); 59 | return NoContent(); 60 | } 61 | 62 | [HttpDelete("{id}")] 63 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 64 | [Authorize] 65 | public async Task DeleteChoice(int id) 66 | { 67 | await _service.DeleteChoiceAsync(id); 68 | return NoContent(); 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/QuestionService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Application.Interfaces.QuestionsChoice; 4 | using LSC.SmartCertify.Domain.Entities; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace LSC.SmartCertify.Application.Services 8 | { 9 | public class QuestionService : IQuestionService 10 | { 11 | private readonly IQuestionRepository _repository; 12 | private readonly IMapper _mapper; 13 | 14 | public QuestionService(IQuestionRepository repository, IMapper mapper) 15 | { 16 | _repository = repository; 17 | _mapper = mapper; 18 | } 19 | 20 | public async Task> GetAllQuestionsAsync() 21 | { 22 | var questions = await _repository.GetAllQuestionsAsync(); 23 | return _mapper.Map>(questions); 24 | } 25 | 26 | public async Task GetQuestionByIdAsync(int id) 27 | { 28 | var question = await _repository.GetQuestionByIdAsync(id); 29 | return question == null ? null : _mapper.Map(question); 30 | } 31 | 32 | public async Task AddQuestionAsync(CreateQuestionDto dto) 33 | { 34 | var question = _mapper.Map(dto); 35 | await _repository.AddQuestionAsync(question); 36 | } 37 | 38 | public async Task UpdateQuestionAsync(int id, UpdateQuestionDto dto) 39 | { 40 | var question = await _repository.GetQuestionByIdAsync(id); 41 | if (question == null) 42 | throw new KeyNotFoundException("Question not found"); 43 | 44 | _mapper.Map(dto, question); 45 | await _repository.UpdateQuestionAsync(question); 46 | } 47 | 48 | public async Task DeleteQuestionAsync(int id) 49 | { 50 | var question = await _repository.GetQuestionByIdAsync(id); 51 | if (question == null) 52 | throw new KeyNotFoundException("Question not found"); 53 | 54 | await _repository.DeleteQuestionAsync(question); 55 | } 56 | 57 | public async Task AddQuestionAndChoicesAsync(QuestionDto dto) 58 | { 59 | var question = _mapper.Map(dto); 60 | question.Choices = dto.Choices.Select(c => _mapper.Map(c)).ToList(); 61 | 62 | await _repository.AddQuestionAsync(question); 63 | _mapper.Map(question, dto); 64 | return dto; 65 | } 66 | 67 | public async Task UpdateQuestionAndChoicesAsync(int id, QuestionDto dto) 68 | { 69 | await _repository.UpdateQuestionAndChoicesAsync(id, dto); 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/Storage/StorageService.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Azure.Storage.Sas; 3 | using LSC.SmartCertify.Application.Interfaces.Storage; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace LSC.SmartCertify.Infrastructure.Services.Storage 7 | { 8 | public class StorageService : IStorageService 9 | { 10 | private readonly BlobServiceClient _blobServiceClient; 11 | private readonly string _containerName; 12 | private readonly IConfiguration configuration; 13 | 14 | public StorageService(IConfiguration configuration) 15 | { 16 | string connectionString = configuration["AzureStorage:ConnectionString"]; 17 | _blobServiceClient = new BlobServiceClient(connectionString); 18 | _containerName = configuration["AzureStorage:UserContainerName"]; 19 | this.configuration = configuration; 20 | } 21 | 22 | public async Task GenerateSasTokenAsync(string fileName) 23 | { 24 | if (string.IsNullOrEmpty(fileName)) 25 | { 26 | throw new ArgumentException("File name is required."); 27 | } 28 | 29 | var blobContainerClient = _blobServiceClient.GetBlobContainerClient(_containerName); 30 | var blobClient = blobContainerClient.GetBlobClient(fileName.Split('/').LastOrDefault()); 31 | 32 | if (!await blobClient.ExistsAsync()) 33 | { 34 | throw new Exception("File not found in storage."); 35 | } 36 | 37 | return GenerateBlobSasToken(blobClient); 38 | } 39 | 40 | private string GenerateBlobSasToken(BlobClient blobClient) 41 | { 42 | if (blobClient.CanGenerateSasUri) 43 | { 44 | var sasBuilder = new BlobSasBuilder 45 | { 46 | BlobContainerName = _containerName, 47 | BlobName = blobClient.Name, 48 | Resource = "b", // 'b' for blob, 'c' for container 49 | ExpiresOn = DateTime.UtcNow.AddSeconds(8000) // Token expires in 1 hour 50 | }; 51 | 52 | sasBuilder.SetPermissions(BlobSasPermissions.Read); 53 | 54 | return blobClient.GenerateSasUri(sasBuilder).Query; 55 | } 56 | 57 | return ""; 58 | } 59 | 60 | public async Task UploadAsync(byte[] fileData, string fileName, string containerName = "images") 61 | { 62 | var containerClient = _blobServiceClient.GetBlobContainerClient(string.IsNullOrEmpty(containerName) ? _containerName : containerName); 63 | await containerClient.CreateIfNotExistsAsync(); 64 | 65 | var blobClient = containerClient.GetBlobClient(fileName); 66 | 67 | using (var stream = new MemoryStream(fileData)) 68 | { 69 | await blobClient.UploadAsync(stream, true); 70 | } 71 | 72 | return blobClient.Uri.ToString(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/Graph/GraphService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using LSC.SmartCertify.Application.Interfaces.Graph; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | using Newtonsoft.Json; 6 | using System.Net.Http.Headers; 7 | 8 | namespace LSC.SmartCertify.Application.Services.Graph 9 | { 10 | public class GraphService : IGraphService 11 | { 12 | private readonly IGraphAuthService graphAuthService; 13 | private readonly IConfiguration configuration; 14 | private readonly HttpClient httpClient; 15 | private readonly ILogger logger; 16 | 17 | public GraphService(IGraphAuthService graphAuthService, IConfiguration configuration, HttpClient httpClient, 18 | ILogger logger) 19 | { 20 | this.graphAuthService = graphAuthService; 21 | this.configuration = configuration; 22 | this.httpClient = httpClient; 23 | this.logger = logger; 24 | } 25 | public async Task> GetADB2CUsersAsync() 26 | { 27 | var accessToken = await graphAuthService.GetAccessTokenAsync(); 28 | var graphEndpoint = configuration["AzureAdB2CGraph:GraphEndpoint"] + "users?$select=id,givenName,surname,displayName,userPrincipalName,mail,otherMails"; 29 | 30 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 31 | var response = await httpClient.GetAsync(graphEndpoint); 32 | var content = await response.Content.ReadAsStringAsync(); 33 | 34 | if (!response.IsSuccessStatusCode) 35 | { 36 | logger.LogError($"IsSuccessStatusCode : {(int)response.StatusCode}, {content}"); 37 | return new List(); 38 | } 39 | 40 | // Deserialize using GraphApiResponse wrapper class 41 | var graphResponse = JsonConvert.DeserializeObject(content); 42 | 43 | // Ensure we have users before filtering 44 | if (graphResponse?.Users == null) 45 | { 46 | logger.LogInformation("graphResponse?.Users received as null"); 47 | return new List(); 48 | } 49 | 50 | // Filter users that have an email 51 | List usersWithEmail = graphResponse.Users 52 | .Where(u => !string.IsNullOrEmpty(u.Mail) || (u.OtherMails != null && u.OtherMails.Any())) 53 | .Select(u => new AdB2CUserModel 54 | { 55 | Id = u.Id, 56 | GivenName = u.GivenName, 57 | Surname = u.Surname, 58 | DisplayName = u.DisplayName, 59 | UserPrincipalName = u.UserPrincipalName, 60 | Mail = u.Mail, 61 | OtherMails = u.OtherMails 62 | }) 63 | .ToList(); 64 | 65 | return usersWithEmail; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/CourseService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Application.Interfaces.Courses; 4 | using LSC.SmartCertify.Domain.Entities; 5 | 6 | namespace LSC.SmartCertify.Application.Services 7 | { 8 | public class CourseService : ICourseService 9 | { 10 | private readonly ICourseRepository _courseRepository; 11 | private readonly IMapper _mapper; 12 | 13 | public CourseService(ICourseRepository repository, IMapper mapper) 14 | { 15 | _courseRepository = repository; 16 | _mapper = mapper; 17 | } 18 | 19 | public async Task> GetAllCoursesAsync() 20 | { 21 | var courses = await _courseRepository.GetAllCoursesAsync(); 22 | var courseData = _mapper.Map>(courses); 23 | courseData.ToList().ForEach(c => 24 | { 25 | c.QuestionsAvailable = courses.Any(w => w.CourseId == c.CourseId && w.Questions.Count > 0); 26 | c.QuestionCount = courses.Where(w => w.CourseId == c.CourseId) 27 | .SelectMany(s => s.Questions) 28 | .Count(); 29 | }); 30 | 31 | return courseData; 32 | } 33 | 34 | public async Task GetCourseByIdAsync(int courseId) 35 | { 36 | var course = await _courseRepository.GetCourseByIdAsync(courseId); 37 | return course == null ? null : _mapper.Map(course); 38 | } 39 | 40 | public async Task IsTitleDuplicateAsync(string title) 41 | { 42 | return await _courseRepository.IsTitleDuplicateAsync(title); 43 | } 44 | 45 | public async Task AddCourseAsync(CreateCourseDto createCourseDto) 46 | { 47 | var course = _mapper.Map(createCourseDto); 48 | course.CreatedBy = 1; // Replace with actual user context 49 | course.CreatedOn = DateTime.UtcNow; 50 | 51 | await _courseRepository.AddCourseAsync(course); 52 | } 53 | 54 | public async Task UpdateCourseAsync(int courseId, UpdateCourseDto updateCourseDto) 55 | { 56 | var course = await _courseRepository.GetCourseByIdAsync(courseId); 57 | if (course == null) throw new KeyNotFoundException("Course not found"); 58 | 59 | _mapper.Map(updateCourseDto, course); 60 | await _courseRepository.UpdateCourseAsync(course); 61 | } 62 | 63 | public async Task DeleteCourseAsync(int courseId) 64 | { 65 | var course = await _courseRepository.GetCourseByIdAsync(courseId); 66 | if (course == null) throw new KeyNotFoundException($"Course with id {courseId} not found"); 67 | 68 | await _courseRepository.DeleteCourseAsync(course); 69 | } 70 | 71 | public async Task UpdateDescriptionAsync(int courseId, string description) 72 | { 73 | await _courseRepository.UpdateDescriptionAsync(courseId, description); 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.rsuser 3 | *.suo 4 | *.user 5 | *.userosscache 6 | *.sln.docstates 7 | 8 | # User-specific files (MonoDevelop/Xamarin Studio) 9 | *.userprefs 10 | 11 | # Build results 12 | [Dd]ebug/ 13 | [Dd]ebugPublic/ 14 | [Rr]elease/ 15 | [Rr]eleases/ 16 | x64/ 17 | x86/ 18 | [Aa][Rr][Mm]/ 19 | [Aa][Rr][Mm]64/ 20 | bld/ 21 | 22 | # Ignore bin and obj directories in all subfolders 23 | **/[Bb]in/ 24 | **/[Oo]bj/ 25 | 26 | # Additional directories and files to ignore 27 | .vs/ 28 | Generated\ Files/ 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | *.VisualState.xml 32 | TestResult.xml 33 | [Dd]ebugPS/ 34 | [Rr]eleasePS/ 35 | dlldata.c 36 | BenchmarkDotNet.Artifacts/ 37 | project.lock.json 38 | project.fragment.lock.json 39 | artifacts/ 40 | StyleCopReport.xml 41 | *_i.c 42 | *_p.c 43 | *_h.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.iobj 48 | *.pch 49 | *.pdb 50 | *.ipdb 51 | *.pgc 52 | *.pgd 53 | *.rsp 54 | *.sbr 55 | *.tlb 56 | *.tli 57 | *.tlh 58 | *.tmp 59 | *.tmp_proj 60 | *_wpftmp.csproj 61 | *.log 62 | *.vspscc 63 | *.vssscc 64 | .builds 65 | *.pidb 66 | *.svclog 67 | *.scc 68 | _Chutzpah* 69 | ipch/ 70 | *.aps 71 | *.ncb 72 | *.opendb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | *.VC.db 77 | *.VC.VC.opendb 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | *.sap 82 | *.e2e 83 | $tf/ 84 | *.gpState 85 | _ReSharper*/ 86 | *.[Rr]e[Ss]harper 87 | *.DotSettings.user 88 | .JustCode 89 | _TeamCity* 90 | *.dotCover 91 | .axoCover/* 92 | !.axoCover/settings.json 93 | *.coverage 94 | *.coveragexml 95 | _NCrunch_* 96 | .*crunch*.local.xml 97 | nCrunchTemp_* 98 | *.mm.* 99 | AutoTest.Net/ 100 | .sass-cache/ 101 | [Ee]xpress/ 102 | DocProject/buildhelp/ 103 | DocProject/Help/*.HxT 104 | DocProject/Help/*.HxC 105 | DocProject/Help/*.hhc 106 | DocProject/Help/*.hhk 107 | DocProject/Help/*.hhp 108 | DocProject/Help/Html2 109 | DocProject/Help/html 110 | publish/ 111 | *.[Pp]ublish.xml 112 | *.azurePubxml 113 | *.pubxml 114 | *.publishproj 115 | PublishScripts/ 116 | *.nupkg 117 | **/[Pp]ackages/* 118 | !**/[Pp]ackages/build/ 119 | *.nuget.props 120 | *.nuget.targets 121 | csx/ 122 | *.build.csdef 123 | ecf/ 124 | rcf/ 125 | AppPackages/ 126 | BundleArtifacts/ 127 | Package.StoreAssociation.xml 128 | _pkginfo.txt 129 | *.appx 130 | *.[Cc]ache 131 | !?*.[Cc]ache/ 132 | ClientBin/ 133 | ~$* 134 | *~ 135 | *.dbmdl 136 | *.dbproj.schemaview 137 | *.jfm 138 | *.pfx 139 | *.publishsettings 140 | orleans.codegen.cs 141 | _UpgradeReport_Files/ 142 | Backup*/ 143 | UpgradeLog*.XML 144 | UpgradeLog*.htm 145 | ServiceFabricBackup/ 146 | *.rptproj.bak 147 | *.mdf 148 | *.ldf 149 | *.ndf 150 | *.rdl.data 151 | *.bim.layout 152 | *.bim_*.settings 153 | *.rptproj.rsuser 154 | *- Backup*.rdl 155 | FakesAssemblies/ 156 | *.GhostDoc.xml 157 | .ntvs_analysis.dat 158 | node_modules/ 159 | *.plg 160 | *.opt 161 | *.vbw 162 | **/*.HTMLClient/GeneratedArtifacts 163 | **/*.DesktopClient/GeneratedArtifacts 164 | **/*.DesktopClient/ModelManifest.xml 165 | **/*.Server/GeneratedArtifacts 166 | **/*.Server/ModelManifest.xml 167 | _Pvt_Extensions 168 | .paket/paket.exe 169 | paket-files/ 170 | .fake/ 171 | .idea/ 172 | *.sln.iml 173 | .cr/personal 174 | __pycache__/ 175 | *.pyc 176 | *.btp.cs 177 | *.btm.cs 178 | *.odx.cs 179 | *.xsd.cs 180 | OpenCover/ 181 | ASALocalRun/ 182 | *.binlog 183 | *.nvuser 184 | .mfractor/ 185 | .localhistory/ 186 | healthchecksdb 187 | /bin/ 188 | /obj/ 189 | /.vscode/ 190 | *.dll 191 | *.exe 192 | /logs/ 193 | /logs 194 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/BackgroundServices/OnboardUserBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.Interfaces.Graph; 2 | using LSC.SmartCertify.Domain.Entities; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace LSC.SmartCertify.Infrastructure.BackgroundServices 8 | { 9 | public class OnboardUserBackgroundService : BackgroundService 10 | { 11 | private readonly IServiceScopeFactory _serviceScopeFactory; 12 | private readonly ILogger _logger; 13 | 14 | public OnboardUserBackgroundService(IServiceScopeFactory serviceScopeFactory, ILogger logger) 15 | { 16 | _serviceScopeFactory = serviceScopeFactory; 17 | _logger = logger; 18 | } 19 | 20 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 21 | { 22 | while (!stoppingToken.IsCancellationRequested) 23 | { 24 | try 25 | { 26 | _logger.LogInformation("Running notification background service..."); 27 | 28 | using (var scope = _serviceScopeFactory.CreateScope()) 29 | { 30 | var dbContext = scope.ServiceProvider.GetRequiredService(); 31 | var graphService = scope.ServiceProvider.GetRequiredService(); 32 | 33 | var adb2cUsers= await graphService.GetADB2CUsersAsync(); 34 | if (adb2cUsers.Any()) 35 | { 36 | foreach (var user in adb2cUsers) 37 | { 38 | if (user.OtherMails.Any()) 39 | { 40 | var userExists = dbContext.UserProfiles 41 | .Any(u => u.Email == user.OtherMails.FirstOrDefault()); 42 | 43 | if (!userExists) 44 | { 45 | var userProfile = new UserProfile 46 | { 47 | Email = user.OtherMails.FirstOrDefault(), 48 | FirstName = Convert.ToString(user.GivenName), 49 | LastName = user.Surname ?? "", 50 | DisplayName = user.DisplayName, 51 | AdObjId = "", 52 | ProfileImageUrl = "" 53 | }; 54 | 55 | dbContext.UserProfiles.Add(userProfile); 56 | } 57 | } 58 | } 59 | await dbContext.SaveChangesAsync(); 60 | _logger.LogInformation($"Processed {adb2cUsers.Count} new users."); 61 | } 62 | 63 | 64 | } 65 | } 66 | catch (Exception ex) 67 | { 68 | _logger.LogError(ex, "Error in onboardUser background service"); 69 | } 70 | 71 | // Wait for an hour before next execution 72 | await Task.Delay(TimeSpan.FromDays(1), stoppingToken); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Application/Services/Certification/ExamService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Application.Interfaces.Certification; 4 | using LSC.SmartCertify.Domain.Entities; 5 | 6 | namespace LSC.SmartCertify.Application.Services.Certification 7 | { 8 | public class ExamService : IExamService 9 | { 10 | private readonly IExamRepository _examRepository; 11 | private readonly IMapper mapper; 12 | 13 | public ExamService(IExamRepository examRepository, IMapper mapper) 14 | { 15 | _examRepository = examRepository; 16 | this.mapper = mapper; 17 | } 18 | 19 | public async Task StartExamAsync(int courseId, int userId) 20 | { 21 | // Fetch 10 random questions for the course 22 | var questions = await _examRepository.GetRandomQuestionsAsync(courseId, 10); 23 | 24 | if (!questions.Any()) 25 | { 26 | throw new Exception("No questions found for the specified course."); 27 | } 28 | 29 | // Create a new exam 30 | var exam = new Exam 31 | { 32 | CourseId = courseId, 33 | UserId = userId, 34 | Status = "In Progress", 35 | StartedOn = DateTime.UtcNow 36 | }; 37 | 38 | // Save exam and associate questions 39 | await _examRepository.CreateExamWithQuestionsAsync(exam, questions); 40 | 41 | // Map to DTO and return 42 | return new ExamDto 43 | { 44 | ExamId = exam.ExamId, 45 | CourseId = courseId, 46 | UserId = userId, 47 | Status = exam.Status, 48 | StartedOn = exam.StartedOn, 49 | QuestionIds = questions.Select(q => q.QuestionId).ToList() 50 | }; 51 | } 52 | 53 | public async Task UpdateUserChoiceAsync(int id, UpdateUserQuestionChoiceDto dto) 54 | { 55 | var examQuestion = await _examRepository.GetExamQuestionAsync(dto.ExamId, dto.ExamQuestionId); 56 | if (examQuestion == null) 57 | throw new KeyNotFoundException($"Exam ID {dto.ExamId} with ExamQuestionId {dto.ExamQuestionId} not found."); 58 | 59 | mapper.Map(dto, examQuestion); 60 | await _examRepository.UpdateExamQuestionAsync(examQuestion); 61 | } 62 | 63 | public async Task> GetExamQuestionsAsync(int examId) 64 | { 65 | // Fetch the exam questions from the repository 66 | var examQuestions = await _examRepository.GetExamQuestionsAsync(examId); 67 | 68 | // Map the result to a list of UserExamQuestionsDto if not null; otherwise, return an empty list 69 | return examQuestions != null 70 | ? mapper.Map>(examQuestions) 71 | : new List(); 72 | } 73 | 74 | public async Task> GetUserExamsAsync(int userId) 75 | { 76 | return await _examRepository.GetUserExamsAsync(userId); 77 | } 78 | 79 | public Task GetExamMetaData(int examId) 80 | { 81 | return _examRepository.GetExamMetaDataAsync(examId); 82 | } 83 | 84 | public async Task SaveExamStatus(ExamFeedbackDto examFeedback) 85 | { 86 | await _examRepository.SaveExamStatusAsync(examFeedback.ExamId, examFeedback.Feedback.ToString()); 87 | } 88 | 89 | public async Task GetExamDetailsAsync(int examId) 90 | { 91 | return await _examRepository.GetExamDetailsAsync(examId); 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Controllers/QuestionsController.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.API.Filters.LSC.OnlineCourse.API.Common; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Application.Interfaces.Common; 4 | using LSC.SmartCertify.Application.Interfaces.QuestionsChoice; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Identity.Web.Resource; 8 | 9 | namespace LSC.SmartCertify.API.Controllers 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Read")] 14 | [Authorize] 15 | public class QuestionsController : ControllerBase 16 | { 17 | private readonly IQuestionService _service; 18 | private readonly IUserClaims userClaims; 19 | 20 | public QuestionsController(IQuestionService service, IUserClaims userClaims) 21 | { 22 | _service = service; 23 | this.userClaims = userClaims; 24 | } 25 | 26 | [HttpGet] 27 | public async Task>> GetQuestions() 28 | { 29 | return Ok(await _service.GetAllQuestionsAsync()); 30 | } 31 | 32 | [HttpGet("{id}")] 33 | public async Task> GetQuestion(int id) 34 | { 35 | var question = await _service.GetQuestionByIdAsync(id); 36 | var isAdmin = this.userClaims.GetUserRoles().Any(s => s.ToLower().Equals("admin")); 37 | if (!isAdmin) 38 | { 39 | //let's mark choice's answer as false so we dont let user know the answer 40 | question?.Choices.ForEach(c => c.IsCorrect = false); 41 | } 42 | return question == null ? NotFound() : Ok(question); 43 | } 44 | 45 | [HttpPost] 46 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 47 | [Authorize] 48 | [AdminRole] 49 | public async Task CreateQuestion([FromBody] CreateQuestionDto dto) 50 | { 51 | await _service.AddQuestionAsync(dto); 52 | return CreatedAtAction(nameof(GetQuestion), new { id = dto.CourseId }, dto); 53 | } 54 | 55 | [HttpPut("{id}")] 56 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 57 | [Authorize] 58 | [AdminRole] 59 | public async Task UpdateQuestion(int id, [FromBody] UpdateQuestionDto dto) 60 | { 61 | await _service.UpdateQuestionAsync(id, dto); 62 | return NoContent(); 63 | } 64 | 65 | [HttpDelete("{id}")] 66 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 67 | [Authorize] 68 | [AdminRole] 69 | public async Task DeleteQuestion(int id) 70 | { 71 | await _service.DeleteQuestionAsync(id); 72 | return NoContent(); 73 | } 74 | 75 | 76 | [HttpPost("CreateQuestionChoices")] 77 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 78 | [Authorize] 79 | [AdminRole] 80 | public async Task CreateQuestionChoices([FromBody] QuestionDto dto) 81 | { 82 | var createdResource = await _service.AddQuestionAndChoicesAsync(dto); 83 | return CreatedAtAction(nameof(GetQuestion), new { id = createdResource.QuestionId }, createdResource); 84 | } 85 | 86 | [HttpPut("UpdateQuestionAndChoices/{id}")] 87 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 88 | [Authorize] 89 | [AdminRole] 90 | public async Task UpdateQuestionAndChoices(int id, [FromBody] QuestionDto dto) 91 | { 92 | await _service.UpdateQuestionAndChoicesAsync(id, dto); 93 | return NoContent(); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/BackgroundServices/NotificationBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Domain.Entities; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace LSC.SmartCertify.Infrastructure.BackgroundServices 7 | { 8 | public class NotificationBackgroundService : BackgroundService 9 | { 10 | private readonly IServiceScopeFactory _serviceScopeFactory; 11 | private readonly ILogger _logger; 12 | 13 | public NotificationBackgroundService(IServiceScopeFactory serviceScopeFactory, 14 | ILogger logger) 15 | { 16 | _serviceScopeFactory = serviceScopeFactory; 17 | _logger = logger; 18 | } 19 | 20 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 21 | { 22 | while (!stoppingToken.IsCancellationRequested) 23 | { 24 | try 25 | { 26 | _logger.LogInformation("Running notification background service..."); 27 | 28 | using (var scope = _serviceScopeFactory.CreateScope()) 29 | { 30 | var dbContext = scope.ServiceProvider.GetRequiredService(); 31 | 32 | 33 | // Retrieve new notifications that are not processed 34 | var newNotifications = dbContext.Notifications 35 | .Where(n => n.IsActive && n.ScheduledSendTime >= DateTime.Now.AddHours(-1) 36 | && n.ScheduledSendTime < DateTime.Now.AddHours(1)) 37 | .ToList(); 38 | 39 | if (newNotifications.Any()) 40 | { 41 | var users = dbContext.UserProfiles.ToList(); 42 | 43 | foreach (var notification in newNotifications) 44 | { 45 | var userNotificationExists = dbContext.UserNotifications 46 | .Any(un => un.NotificationId == notification.NotificationId); 47 | if (!userNotificationExists) 48 | { 49 | foreach (var user in users) 50 | { 51 | dbContext.UserNotifications.Add(new UserNotification 52 | { 53 | CreatedOn = DateTime.Now, 54 | NotificationId = notification.NotificationId, 55 | UserId = user.UserId, 56 | EmailContent = notification.Content, 57 | EmailSubject = notification.Subject, 58 | NotificationSent = false, 59 | SentOn = null 60 | }); 61 | } 62 | } 63 | 64 | notification.IsActive = false; 65 | 66 | } 67 | 68 | await dbContext.SaveChangesAsync(); 69 | _logger.LogInformation($"Processed {newNotifications.Count} new notifications."); 70 | } 71 | } 72 | } 73 | catch (Exception ex) 74 | { 75 | _logger.LogError(ex, "Error in notification background service"); 76 | } 77 | 78 | // Wait for an hour before next execution 79 | await Task.Delay(TimeSpan.FromHours(1), stoppingToken); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/QuestionRepository.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using LSC.SmartCertify.Application.DTOs; 3 | using LSC.SmartCertify.Application.Interfaces.QuestionsChoice; 4 | using LSC.SmartCertify.Domain.Entities; 5 | using Microsoft.EntityFrameworkCore; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace LSC.SmartCertify.Infrastructure 13 | { 14 | 15 | public class QuestionRepository : IQuestionRepository 16 | { 17 | private readonly SmartCertifyContext _context; 18 | private readonly IMapper mapper; 19 | 20 | public QuestionRepository(SmartCertifyContext context, IMapper mapper) 21 | { 22 | _context = context; 23 | this.mapper = mapper; 24 | } 25 | 26 | public async Task> GetAllQuestionsAsync() 27 | { 28 | return await _context.Questions.Include(q => q.Choices).ToListAsync(); 29 | } 30 | 31 | public async Task GetQuestionByIdAsync(int id) 32 | { 33 | return await _context.Questions.Include(q => q.Choices).FirstOrDefaultAsync(q => q.QuestionId == id); 34 | } 35 | 36 | public async Task AddQuestionAsync(Question question) 37 | { 38 | await _context.Questions.AddAsync(question); 39 | await _context.SaveChangesAsync(); 40 | return question; 41 | } 42 | 43 | public async Task UpdateQuestionAsync(Question question) 44 | { 45 | _context.Questions.Update(question); 46 | await _context.SaveChangesAsync(); 47 | } 48 | 49 | public async Task DeleteQuestionAsync(Question question) 50 | { 51 | //currently case delete not enable hence we use like this, if we enable case deleteing in table 52 | // Remove all choices associated with the question 53 | var choices = _context.Choices.Where(c => c.QuestionId == question.QuestionId); 54 | _context.Choices.RemoveRange(choices); 55 | 56 | // Now remove the question 57 | _context.Questions.Remove(question); 58 | 59 | await _context.SaveChangesAsync(); 60 | } 61 | 62 | public async Task UpdateQuestionAndChoicesAsync(int id, QuestionDto dto) 63 | { 64 | var question = await GetQuestionByIdAsync(id); 65 | if (question == null) 66 | throw new KeyNotFoundException("Question not found"); 67 | 68 | // Map basic properties (excluding choices) 69 | mapper.Map(dto, question); 70 | 71 | // Handle Choices separately 72 | var existingChoiceIds = question.Choices.Select(c => c.ChoiceId).ToList(); 73 | var incomingChoiceIds = dto.Choices.Select(c => c.ChoiceId).ToList(); 74 | 75 | // Find choices to delete 76 | var choicesToDelete = question.Choices.Where(c => !incomingChoiceIds.Contains(c.ChoiceId)).ToList(); 77 | foreach (var choice in choicesToDelete) 78 | { 79 | _context.Choices.Remove(choice); 80 | } 81 | 82 | // Update existing choices and add new ones 83 | foreach (var choiceDto in dto.Choices) 84 | { 85 | var existingChoice = question.Choices.FirstOrDefault(c => c.ChoiceId == choiceDto.ChoiceId); 86 | if (existingChoice != null) 87 | { 88 | // Update existing choice 89 | mapper.Map(choiceDto, existingChoice); 90 | } 91 | else 92 | { 93 | // Add new choice 94 | var newChoice = mapper.Map(choiceDto); 95 | question.Choices.Add(newChoice); 96 | } 97 | } 98 | 99 | await UpdateQuestionAsync(question); 100 | } 101 | 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Controllers/ExamController.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using LSC.SmartCertify.Application.DTOValidations; 3 | using LSC.SmartCertify.Application.Interfaces.Certification; 4 | using LSC.SmartCertify.Application.Interfaces.Common; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace LSC.SmartCertify.API.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/[controller]")] 12 | public class ExamController : ControllerBase 13 | { 14 | private readonly IExamService _examService; 15 | private readonly IUserClaims userClaims; 16 | 17 | public ExamController(IExamService examService, IUserClaims userClaims) 18 | { 19 | _examService = examService; 20 | this.userClaims = userClaims; 21 | } 22 | 23 | [HttpPost("start-exam")] 24 | public async Task StartExam([FromBody] StartExamRequest request) 25 | { 26 | // Validate the input using FluentValidation 27 | var validator = new StartExamRequestValidator(); 28 | var validationResult = validator.Validate(request); 29 | 30 | if (!validationResult.IsValid) 31 | { 32 | return BadRequest(validationResult.Errors.Select(e => e.ErrorMessage)); 33 | } 34 | 35 | try 36 | { 37 | var result = await _examService.StartExamAsync(request.CourseId, request.UserId); 38 | return Ok(result); 39 | } 40 | catch (Exception ex) 41 | { 42 | return StatusCode(500, $"Internal server error: {ex.Message}"); 43 | } 44 | } 45 | 46 | 47 | /// 48 | /// id is examQuestionId 49 | /// 50 | /// 51 | /// 52 | /// 53 | [HttpPut("update-user-choice/{id}")] 54 | public async Task UpdateUserChoice(int id, [FromBody] UpdateUserQuestionChoiceDto dto) 55 | { 56 | //TODO Validation of request model pending 57 | await _examService.UpdateUserChoiceAsync(id, dto); 58 | return NoContent(); 59 | } 60 | 61 | [HttpGet("get-user-exam-questions/{examId}")] 62 | public async Task GetUserExamQuestions(int examId) 63 | { 64 | //TODO Validation of request model pending 65 | var result = await _examService.GetExamQuestionsAsync(examId); 66 | return Ok(result); 67 | } 68 | 69 | 70 | [HttpGet("get-user-exams/{userId}")] 71 | public async Task GetUserExams(int userId = 0) 72 | { 73 | userId = userId == 0 ? userClaims.GetUserId() : userId; 74 | 75 | //TODO Validation of request model pending 76 | var result = await _examService.GetUserExamsAsync(userId); 77 | return Ok(result); 78 | } 79 | 80 | [HttpGet("exam-meta-data/{examId}")] 81 | public async Task GetExamMetaData(int examId) 82 | { 83 | //TODO Validation of request model pending 84 | var result = await _examService.GetExamMetaData(examId); 85 | 86 | if (result == null) 87 | return NotFound(); 88 | 89 | if (result.UserId != userClaims.GetUserId()) 90 | { 91 | return new ForbidResult(); 92 | } 93 | return Ok(result); 94 | } 95 | 96 | [HttpPut("update-exam-status/{examId}")] 97 | public async Task UpdateExamStatus(int examId, [FromBody] ExamFeedbackDto feedback) 98 | { 99 | //TODO Validation of request model pending 100 | await _examService.SaveExamStatus(feedback); 101 | return NoContent(); 102 | } 103 | 104 | [HttpGet("exam-details/{examId}")] 105 | public async Task GetExamDetails(int examId) 106 | { 107 | var result = await _examService.GetExamDetailsAsync(examId); 108 | 109 | if (result == null) 110 | return NotFound(new { Message = "Exam not found." }); 111 | 112 | return Ok(result); 113 | } 114 | 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using LSC.SmartCertify.Application.Interfaces.Common; 3 | using LSC.SmartCertify.Application.Interfaces.Graph; 4 | using LSC.SmartCertify.Application.Interfaces.ManageUser; 5 | using LSC.SmartCertify.Application.Interfaces.Storage; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace LSC.SmartCertify.API.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class UserController : ControllerBase 13 | { 14 | private readonly IStorageService storageService; 15 | private readonly IUserClaims userClaims; 16 | private readonly IUserProfileService userProfileService; 17 | private readonly IGraphService graphService; 18 | 19 | public UserController( 20 | IStorageService storageService, 21 | IUserClaims userClaims, 22 | IUserProfileService userProfileService, 23 | IGraphService graphService) 24 | { 25 | this.storageService = storageService; 26 | this.userClaims = userClaims; 27 | this.userProfileService = userProfileService; 28 | this.graphService = graphService; 29 | } 30 | 31 | [HttpPost("updateProfile")] 32 | 33 | public async Task UpdateUserProfile([FromForm] UpdateUserProfileModel model) 34 | { 35 | string pictureUrl = null; 36 | 37 | if (model.Picture != null) 38 | { 39 | using (var stream = new MemoryStream()) 40 | { 41 | await model.Picture.CopyToAsync(stream); 42 | 43 | // Upload the byte array or stream to Azure Blob Storage 44 | pictureUrl = await storageService.UploadAsync(stream.ToArray(), 45 | $"{model.UserId}_profile_picture.{model.Picture.FileName.Split('.').LastOrDefault()}"); 46 | } 47 | 48 | // Update the profile picture URL in the database 49 | await userProfileService.UpdateUserProfilePicture(model.UserId, pictureUrl); 50 | } 51 | 52 | 53 | return Ok(model); 54 | } 55 | 56 | 57 | [HttpGet("generate-sas")] 58 | public async Task GenerateSasToken() 59 | { 60 | try 61 | { 62 | var userid = userClaims.GetUserId(); 63 | var userinfo = await userProfileService.GetUserInfoAsync(userid); 64 | 65 | string sasToken = await storageService.GenerateSasTokenAsync(userinfo?.ProfileImageUrl ?? ""); 66 | if (string.IsNullOrEmpty(sasToken)) 67 | { 68 | return StatusCode(500, "Failed to generate SAS token."); 69 | } 70 | 71 | return Ok(new 72 | { 73 | FileUrl = $"{userinfo?.ProfileImageUrl}?{sasToken}" 74 | }); 75 | } 76 | catch (Exception ex) 77 | { 78 | return BadRequest(ex.Message); 79 | } 80 | } 81 | 82 | [HttpGet("{id}")] 83 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserModel))] 84 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 85 | [ProducesResponseType(StatusCodes.Status404NotFound)] 86 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 87 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 88 | public async Task GetUserProfile([FromRoute] int id) 89 | { 90 | var userInfo = await userProfileService.GetUserInfoAsync(id); 91 | 92 | if (userInfo == null) 93 | { 94 | return NotFound(); 95 | } 96 | if (!string.IsNullOrEmpty(userInfo?.ProfileImageUrl)) 97 | { 98 | string sasToken = 99 | await storageService.GenerateSasTokenAsync(userInfo?.ProfileImageUrl ?? ""); 100 | 101 | if (string.IsNullOrEmpty(sasToken)) 102 | { 103 | return StatusCode(500, "Failed to generate SAS token."); 104 | } 105 | 106 | userInfo.ProfileImageUrl = $"{userInfo?.ProfileImageUrl}{sasToken}"; 107 | } 108 | 109 | return Ok(userInfo); 110 | } 111 | 112 | [HttpGet("users-with-email")] 113 | public async Task GetUsersWithEmail() 114 | { 115 | var adb2cUserModel = await graphService.GetADB2CUsersAsync(); 116 | return Ok(adb2cUserModel); 117 | } 118 | 119 | 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Filters/GlobalExceptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using LSC.SmartCertify.Application.Common; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | using System.Diagnostics; 6 | using System.Net; 7 | 8 | 9 | namespace TodoApp.WebAPI.Filters 10 | { 11 | public class GlobalExceptionFilter : IExceptionFilter 12 | { 13 | private readonly ILogger _logger; 14 | private readonly IWebHostEnvironment env; 15 | 16 | public GlobalExceptionFilter(ILogger logger, IWebHostEnvironment env) 17 | { 18 | _logger = logger; 19 | this.env = env; 20 | } 21 | 22 | //public void OnException(ExceptionContext context) 23 | //{ 24 | // _logger.LogError(context.Exception, "An unhandled exception occurred"); 25 | // _logger.LogError(new EventId(context.Exception.HResult), 26 | // context.Exception, 27 | // context.Exception.Message); 28 | 29 | // // Handle specific exceptions 30 | // if (context.Exception is NotFoundException or KeyNotFoundException) 31 | // { 32 | // context.Result = new ObjectResult(new { message = context.Exception.Message }) 33 | // { 34 | // StatusCode = StatusCodes.Status404NotFound 35 | // }; 36 | // } 37 | // else if (context.Exception is FluentValidation.ValidationException validationException) 38 | // { 39 | // var errors = validationException.Errors.Select(e => new { e.PropertyName, e.ErrorMessage }); 40 | // context.Result = new BadRequestObjectResult(errors); 41 | // context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; 42 | // } 43 | // else 44 | // { 45 | // // This is often very handy information for tracing the specific request 46 | // var traceId = Activity.Current?.Id ?? context.HttpContext?.TraceIdentifier; 47 | 48 | // var json = new JsonErrorResponse 49 | // { 50 | // Messages = new[] { "An error ocurred." }, 51 | // TraceId = traceId ?? string.Empty 52 | // }; 53 | 54 | // if (env.IsDevelopment()) 55 | // { 56 | // json.DeveloperMessage = context.Exception; 57 | // } 58 | 59 | // // General exception handling 60 | // context.Result = new ObjectResult(new { message = "An error occurred while processing your request" }) 61 | // { 62 | // StatusCode = StatusCodes.Status500InternalServerError 63 | // }; 64 | 65 | // } 66 | 67 | // // Mark the exception as handled 68 | // context.ExceptionHandled = true; 69 | //} 70 | 71 | public void OnException(ExceptionContext context) 72 | { 73 | _logger.LogError(context.Exception, "An unhandled exception occurred"); 74 | _logger.LogError(new EventId(context.Exception.HResult), 75 | context.Exception, 76 | context.Exception.Message); 77 | 78 | // Capture the Trace ID (Request ID) for debugging 79 | var traceId = Activity.Current?.Id ?? context.HttpContext?.TraceIdentifier; 80 | var requestId = context.HttpContext?.TraceIdentifier; 81 | 82 | // Handle specific exceptions 83 | if (context.Exception is NotFoundException or KeyNotFoundException) 84 | { 85 | context.Result = new ObjectResult(new { message = context.Exception.Message, traceId }) 86 | { 87 | StatusCode = StatusCodes.Status404NotFound 88 | }; 89 | } 90 | else if (context.Exception is FluentValidation.ValidationException validationException) 91 | { 92 | var errors = validationException.Errors.Select(e => new { e.PropertyName, e.ErrorMessage }); 93 | context.Result = new BadRequestObjectResult(new { errors, traceId }); 94 | context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; 95 | } 96 | else 97 | { 98 | var json = new JsonErrorResponse 99 | { 100 | Messages = new[] { "An error occurred." }, 101 | TraceId = traceId ?? string.Empty 102 | }; 103 | 104 | if (env.IsDevelopment()) 105 | { 106 | json.DeveloperMessage = context.Exception; 107 | } 108 | 109 | // General 500 error response with Request ID 110 | context.Result = new ObjectResult(new 111 | { 112 | message = "An error occurred while processing your request. Please contact support with the Request ID.", 113 | traceId, 114 | requestId 115 | }) 116 | { 117 | StatusCode = StatusCodes.Status500InternalServerError 118 | }; 119 | } 120 | 121 | // Mark the exception as handled 122 | context.ExceptionHandled = true; 123 | } 124 | 125 | private class JsonErrorResponse 126 | { 127 | public string[] Messages { get; set; } 128 | public string TraceId { get; set; } 129 | public object DeveloperMessage { get; set; } 130 | } 131 | } 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/SmartCertifyContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using LSC.SmartCertify.Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace LSC.SmartCertify.Infrastructure; 7 | 8 | public partial class SmartCertifyContext : DbContext 9 | { 10 | public SmartCertifyContext() 11 | { 12 | } 13 | 14 | public SmartCertifyContext(DbContextOptions options) 15 | : base(options) 16 | { 17 | } 18 | 19 | public virtual DbSet BannerInfos { get; set; } 20 | 21 | public virtual DbSet Choices { get; set; } 22 | 23 | public virtual DbSet ContactUs { get; set; } 24 | 25 | public virtual DbSet Courses { get; set; } 26 | 27 | public virtual DbSet Exams { get; set; } 28 | 29 | public virtual DbSet ExamQuestions { get; set; } 30 | 31 | public virtual DbSet Notifications { get; set; } 32 | 33 | public virtual DbSet Questions { get; set; } 34 | 35 | public virtual DbSet Roles { get; set; } 36 | 37 | public virtual DbSet SmartApps { get; set; } 38 | 39 | public virtual DbSet UserActivityLogs { get; set; } 40 | 41 | public virtual DbSet UserNotifications { get; set; } 42 | 43 | public virtual DbSet UserProfiles { get; set; } 44 | 45 | public virtual DbSet UserRoles { get; set; } 46 | 47 | 48 | protected override void OnModelCreating(ModelBuilder modelBuilder) 49 | { 50 | modelBuilder.Entity(entity => 51 | { 52 | entity.HasKey(e => e.BannerId).HasName("PK_BannerInfo_BannerId"); 53 | 54 | entity.Property(e => e.CreatedOn).HasDefaultValueSql("(getdate())"); 55 | entity.Property(e => e.IsActive).HasDefaultValue(true); 56 | }); 57 | 58 | modelBuilder.Entity(entity => 59 | { 60 | entity.HasKey(e => e.ChoiceId).HasName("PK_Choices_ChoiceId"); 61 | 62 | entity.HasOne(d => d.Question).WithMany(p => p.Choices) 63 | .OnDelete(DeleteBehavior.ClientSetNull) 64 | .HasConstraintName("FK_Choices_QuestionId"); 65 | }); 66 | 67 | modelBuilder.Entity(entity => 68 | { 69 | entity.HasKey(e => e.ContactUsId).HasName("PK_ContactUs_ContactUsId"); 70 | }); 71 | 72 | modelBuilder.Entity(entity => 73 | { 74 | entity.HasKey(e => e.CourseId).HasName("PK_Courses_CourseId"); 75 | 76 | entity.Property(e => e.CreatedOn).HasDefaultValueSql("(getdate())"); 77 | 78 | entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.Courses) 79 | .OnDelete(DeleteBehavior.ClientSetNull) 80 | .HasConstraintName("FK_Courses_CreatedBy"); 81 | }); 82 | 83 | modelBuilder.Entity(entity => 84 | { 85 | entity.HasKey(e => e.ExamId).HasName("PK_Exams_ExamId"); 86 | 87 | entity.Property(e => e.StartedOn).HasDefaultValueSql("(getdate())"); 88 | entity.Property(e => e.Status).HasDefaultValue("In Progress"); 89 | 90 | entity.HasOne(d => d.Course).WithMany(p => p.Exams) 91 | .OnDelete(DeleteBehavior.ClientSetNull) 92 | .HasConstraintName("FK_Exams_CourseId"); 93 | 94 | entity.HasOne(d => d.User).WithMany(p => p.Exams) 95 | .OnDelete(DeleteBehavior.ClientSetNull) 96 | .HasConstraintName("FK_Exams_UserId"); 97 | }); 98 | 99 | modelBuilder.Entity(entity => 100 | { 101 | entity.HasKey(e => e.ExamQuestionId).HasName("PK_ExamQuestions_ExamQuestionId"); 102 | 103 | entity.Property(e => e.ReviewLater).HasDefaultValue(false); 104 | 105 | entity.HasOne(d => d.Exam).WithMany(p => p.ExamQuestions) 106 | .OnDelete(DeleteBehavior.ClientSetNull) 107 | .HasConstraintName("FK_ExamQuestions_ExamId"); 108 | 109 | entity.HasOne(d => d.Question).WithMany(p => p.ExamQuestions) 110 | .OnDelete(DeleteBehavior.ClientSetNull) 111 | .HasConstraintName("FK_ExamQuestions_QuestionId"); 112 | 113 | entity.HasOne(d => d.SelectedChoice).WithMany(p => p.ExamQuestions).HasConstraintName("FK_ExamQuestions_SelectedChoiceId"); 114 | }); 115 | 116 | modelBuilder.Entity(entity => 117 | { 118 | entity.HasKey(e => e.NotificationId).HasName("PK_Notification_NotificationId"); 119 | 120 | entity.Property(e => e.CreatedOn).HasDefaultValueSql("(getdate())"); 121 | entity.Property(e => e.IsActive).HasDefaultValue(true); 122 | }); 123 | 124 | modelBuilder.Entity(entity => 125 | { 126 | entity.HasKey(e => e.QuestionId).HasName("PK_Questions_QuestionId"); 127 | 128 | entity.HasOne(d => d.Course).WithMany(p => p.Questions) 129 | .OnDelete(DeleteBehavior.ClientSetNull) 130 | .HasConstraintName("FK_Questions_CourseId"); 131 | }); 132 | 133 | modelBuilder.Entity(entity => 134 | { 135 | entity.HasKey(e => e.RoleId).HasName("PK_Roles_RoleId"); 136 | }); 137 | 138 | modelBuilder.Entity(entity => 139 | { 140 | entity.HasKey(e => e.SmartAppId).HasName("PK_SmartApp_SmartAppId"); 141 | }); 142 | 143 | modelBuilder.Entity(entity => 144 | { 145 | entity.HasKey(e => e.LogId).HasName("PK_UserActivityLog_LogId"); 146 | 147 | entity.HasOne(d => d.User).WithMany(p => p.UserActivityLogs).HasConstraintName("FK_UserActivityLog_UserProfile"); 148 | }); 149 | 150 | modelBuilder.Entity(entity => 151 | { 152 | entity.HasKey(e => e.UserNotificationId).HasName("PK_UserNotifications_UserNotificationId"); 153 | 154 | entity.Property(e => e.CreatedOn).HasDefaultValueSql("(getdate())"); 155 | 156 | entity.HasOne(d => d.Notification).WithMany(p => p.UserNotifications) 157 | .OnDelete(DeleteBehavior.ClientSetNull) 158 | .HasConstraintName("FK_UserNotifications_NotificationId"); 159 | 160 | entity.HasOne(d => d.User).WithMany(p => p.UserNotifications) 161 | .OnDelete(DeleteBehavior.ClientSetNull) 162 | .HasConstraintName("FK_UserNotifications_UserId"); 163 | }); 164 | 165 | modelBuilder.Entity(entity => 166 | { 167 | entity.HasKey(e => e.UserId).HasName("PK_UserProfile_UserId"); 168 | //entity.Property(e => e.ProfilePictureUrl).HasMaxLength(500); 169 | entity.Property(e => e.CreatedOn).HasDefaultValueSql("(getutcdate())"); 170 | entity.Property(e => e.DisplayName).HasDefaultValue("Guest"); 171 | }); 172 | 173 | modelBuilder.Entity(entity => 174 | { 175 | entity.HasKey(e => e.UserRoleId).HasName("PK_UserRole_UserRoleId"); 176 | 177 | entity.HasOne(d => d.Role).WithMany(p => p.UserRoles) 178 | .OnDelete(DeleteBehavior.ClientSetNull) 179 | .HasConstraintName("FK_UserRole_Roles"); 180 | 181 | entity.HasOne(d => d.SmartApp).WithMany(p => p.UserRoles) 182 | .OnDelete(DeleteBehavior.ClientSetNull) 183 | .HasConstraintName("FK_UserRole_SmartApp"); 184 | 185 | entity.HasOne(d => d.User).WithMany(p => p.UserRoles) 186 | .OnDelete(DeleteBehavior.ClientSetNull) 187 | .HasConstraintName("FK_UserRole_UserProfile"); 188 | }); 189 | 190 | OnModelCreatingPartial(modelBuilder); 191 | } 192 | 193 | partial void OnModelCreatingPartial(ModelBuilder modelBuilder); 194 | } 195 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/Database_Scripts/SmartCertify.sql: -------------------------------------------------------------------------------- 1 | use [master] 2 | go 3 | 4 | IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = 'SmartCertify') 5 | BEGIN 6 | -- Create the database 7 | CREATE DATABASE SmartCertify; 8 | END 9 | ELSE 10 | BEGIN 11 | DROP DATABASE SmartCertify; 12 | END 13 | 14 | Go 15 | use SmartCertify 16 | go 17 | 18 | -- User Profile Table 19 | CREATE TABLE UserProfile ( 20 | UserId INT IDENTITY(1,1), 21 | DisplayName NVARCHAR(100) NOT NULL CONSTRAINT DF_UserProfile_DisplayName DEFAULT 'Guest', 22 | FirstName NVARCHAR(50) NOT NULL, 23 | LastName NVARCHAR(50) NOT NULL, 24 | Email NVARCHAR(100) NOT NULL, 25 | AdObjId NVARCHAR(128) NOT NULL, 26 | ProfileImageUrl NVARCHAR(500) NULL, 27 | CreatedOn DATETIME2 NOT NULL CONSTRAINT DF_UserProfile_CreatedOn DEFAULT GETUTCDATE(), 28 | -- Add other user-related fields as needed 29 | CONSTRAINT PK_UserProfile_UserId PRIMARY KEY (UserId) 30 | ); 31 | 32 | --Roles 33 | CREATE TABLE Roles ( 34 | RoleId INT IDENTITY(1,1), 35 | RoleName NVARCHAR(50) NOT NULL, --Admin, ReadOnly, Support, Customer etc 36 | CONSTRAINT PK_Roles_RoleId PRIMARY KEY (RoleId) 37 | ); 38 | 39 | -- SmartApp Table. We can use this databse as centralized and add more tables for future apps. that is why this table is added 40 | CREATE TABLE SmartApp ( 41 | SmartAppId INT IDENTITY(1,1), 42 | AppName NVARCHAR(50) NOT NULL, 43 | CONSTRAINT PK_SmartApp_SmartAppId PRIMARY KEY (SmartAppId) 44 | ); 45 | 46 | -- UserRole Table 47 | CREATE TABLE UserRole ( 48 | UserRoleId INT IDENTITY(1,1), 49 | RoleId INT NOT NULL, 50 | UserId INT NOT NULL, 51 | SmartAppId INT NOT NULL, 52 | CONSTRAINT PK_UserRole_UserRoleId PRIMARY KEY (UserRoleId), 53 | CONSTRAINT FK_UserRole_UserProfile FOREIGN KEY (UserId) REFERENCES UserProfile(UserId), 54 | CONSTRAINT FK_UserRole_Roles FOREIGN KEY (RoleId) REFERENCES Roles(RoleId), 55 | CONSTRAINT FK_UserRole_SmartApp FOREIGN KEY (SmartAppId) REFERENCES SmartApp(SmartAppId) 56 | ); 57 | 58 | CREATE TABLE Courses ( 59 | CourseId INT IDENTITY(1,1), 60 | Title NVARCHAR(100) NOT NULL, 61 | Description NVARCHAR(MAX) NULL, 62 | CreatedBy INT NOT NULL, 63 | CreatedOn DATETIME2 NOT NULL CONSTRAINT DF_Courses_CreatedOn DEFAULT GETDATE(), 64 | CONSTRAINT PK_Courses_CourseId PRIMARY KEY (CourseId), 65 | CONSTRAINT FK_Courses_CreatedBy FOREIGN KEY (CreatedBy) REFERENCES UserProfile(UserId) 66 | ); 67 | 68 | CREATE TABLE Questions ( 69 | QuestionId INT IDENTITY(1,1), 70 | CourseId INT NOT NULL, 71 | QuestionText NVARCHAR(MAX) NOT NULL, -- Question content 72 | DifficultyLevel NVARCHAR(20) NOT NULL, -- Easy, Medium, Advance 73 | IsCode BIT NOT NULL DEFAULT 0, -- Marks if the question includes a code sample 74 | HasMultipleAnswers BIT NOT NULL DEFAULT 0, 75 | CONSTRAINT PK_Questions_QuestionId PRIMARY KEY (QuestionId), 76 | CONSTRAINT FK_Questions_CourseId FOREIGN KEY (CourseId) REFERENCES Courses(CourseId) 77 | ); 78 | 79 | CREATE TABLE Choices ( 80 | ChoiceId INT IDENTITY(1,1), 81 | QuestionId INT NOT NULL, 82 | ChoiceText NVARCHAR(MAX) NOT NULL, -- Text for the choice 83 | IsCode BIT NOT NULL DEFAULT 0, -- Marks if the choice is a code snippet 84 | IsCorrect BIT NOT NULL, -- Indicates the correct answer 85 | CONSTRAINT PK_Choices_ChoiceId PRIMARY KEY (ChoiceId), 86 | CONSTRAINT FK_Choices_QuestionId FOREIGN KEY (QuestionId) REFERENCES Questions(QuestionId) 87 | ); 88 | 89 | CREATE TABLE Exams ( 90 | ExamId INT IDENTITY(1,1), 91 | CourseId INT NOT NULL, 92 | UserId INT NOT NULL, 93 | Status NVARCHAR(20) NOT NULL DEFAULT 'In Progress', -- e.g., In Progress, Completed 94 | StartedOn DATETIME2 NOT NULL CONSTRAINT DF_Exams_StartedOn DEFAULT GETDATE(), 95 | FinishedOn DATETIME2 NULL, 96 | Feedback NVARCHAR(2000) NULL, 97 | CONSTRAINT PK_Exams_ExamId PRIMARY KEY (ExamId), 98 | CONSTRAINT FK_Exams_CourseId FOREIGN KEY (CourseId) REFERENCES Courses(CourseId), 99 | CONSTRAINT FK_Exams_UserId FOREIGN KEY (UserId) REFERENCES UserProfile(UserId) 100 | ); 101 | 102 | CREATE TABLE ExamQuestions ( 103 | ExamQuestionId INT IDENTITY(1,1), 104 | ExamId INT NOT NULL, 105 | QuestionId INT NOT NULL, 106 | SelectedChoiceId INT NULL, -- User's selected answer 107 | IsCorrect BIT NULL, -- Indicates whether the user's answer is correct 108 | ReviewLater BIT NULL DEFAULT 0, 109 | CONSTRAINT PK_ExamQuestions_ExamQuestionId PRIMARY KEY (ExamQuestionId), 110 | CONSTRAINT FK_ExamQuestions_ExamId FOREIGN KEY (ExamId) REFERENCES Exams(ExamId), 111 | CONSTRAINT FK_ExamQuestions_QuestionId FOREIGN KEY (QuestionId) REFERENCES Questions(QuestionId), 112 | CONSTRAINT FK_ExamQuestions_SelectedChoiceId FOREIGN KEY (SelectedChoiceId) REFERENCES Choices(ChoiceId) 113 | ); 114 | 115 | CREATE TABLE Notification ( 116 | NotificationId INT IDENTITY(1,1), 117 | Subject NVARCHAR(200) NOT NULL, -- Subject of the notification/email 118 | Content NVARCHAR(MAX) NOT NULL, -- Email body or notification content 119 | CreatedOn DATETIME2 NOT NULL CONSTRAINT DF_Notification_CreatedOn DEFAULT GETDATE(), 120 | ScheduledSendTime DATETIME2 NOT NULL, -- When the notification is scheduled to be sent 121 | IsActive BIT NOT NULL DEFAULT 1, -- If active, it will trigger user notifications 122 | CONSTRAINT PK_Notification_NotificationId PRIMARY KEY (NotificationId) 123 | ); 124 | 125 | CREATE TABLE UserNotifications ( 126 | UserNotificationId INT IDENTITY(1,1), 127 | NotificationId INT NOT NULL, 128 | UserId INT NOT NULL, 129 | EmailSubject NVARCHAR(200) NOT NULL, -- Personalized email subject 130 | EmailContent NVARCHAR(MAX) NOT NULL, -- Personalized email body 131 | NotificationSent BIT NOT NULL DEFAULT 0, -- Flag to indicate if the email was sent 132 | SentOn DATETIME2 NULL, -- Time when the email was sent 133 | CreatedOn DATETIME2 NOT NULL CONSTRAINT DF_UserNotifications_CreatedOn DEFAULT GETDATE(), 134 | CONSTRAINT PK_UserNotifications_UserNotificationId PRIMARY KEY (UserNotificationId), 135 | CONSTRAINT FK_UserNotifications_NotificationId FOREIGN KEY (NotificationId) REFERENCES Notification(NotificationId), 136 | CONSTRAINT FK_UserNotifications_UserId FOREIGN KEY (UserId) REFERENCES UserProfile(UserId) 137 | ); 138 | 139 | CREATE TABLE BannerInfo ( 140 | BannerId INT IDENTITY(1,1), 141 | Title NVARCHAR(100) NOT NULL, -- Banner title or heading 142 | Content NVARCHAR(MAX) NOT NULL, -- Banner content or description 143 | ImageUrl NVARCHAR(500) NULL, -- Optional URL for banner image 144 | IsActive BIT NOT NULL DEFAULT 1, -- Only active banners are displayed in the app 145 | DisplayFrom DATETIME2 NOT NULL, -- Start date for displaying the banner 146 | DisplayTo DATETIME2 NOT NULL, -- End date for displaying the banner 147 | CreatedOn DATETIME2 NOT NULL CONSTRAINT DF_BannerInfo_CreatedOn DEFAULT GETDATE(), 148 | CONSTRAINT PK_BannerInfo_BannerId PRIMARY KEY (BannerId) 149 | ); 150 | 151 | 152 | -- UserActivityLog Table 153 | CREATE TABLE UserActivityLog ( 154 | LogId INT IDENTITY(1,1), 155 | UserId INT, 156 | ActivityType NVARCHAR(50) NOT NULL, 157 | ActivityDescription NVARCHAR(MAX), 158 | LogDate DATETIME NOT NULL, 159 | -- Add other log-related fields as needed 160 | CONSTRAINT PK_UserActivityLog_LogId PRIMARY KEY (LogId), 161 | CONSTRAINT FK_UserActivityLog_UserProfile FOREIGN KEY (UserId) REFERENCES UserProfile(UserId) 162 | ); 163 | 164 | --Contact Us table 165 | CREATE TABLE ContactUs ( 166 | ContactUsId INT IDENTITY(1,1), 167 | UserName NVARCHAR(100) NOT NULL, 168 | UserEmail NVARCHAR(100) NOT NULL, 169 | MessageDetail NVARCHAR(2000) NOT NULL, 170 | CONSTRAINT PK_ContactUs_ContactUsId PRIMARY KEY (ContactUsId) 171 | ); 172 | 173 | 174 | -- Insert default roles 175 | USE [SmartCertify] 176 | GO 177 | 178 | INSERT INTO [dbo].[Roles] 179 | ([RoleName]) 180 | VALUES 181 | ('Admin') 182 | ,('Support') 183 | ,('Customer') 184 | ,('ReadOnly') 185 | GO 186 | 187 | INSERT INTO [dbo].[SmartApp] 188 | ([AppName]) 189 | VALUES 190 | ('SmartCertify') 191 | GO 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Controllers/CourseController.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using LSC.SmartCertify.API.Filters.LSC.OnlineCourse.API.Common; 3 | using LSC.SmartCertify.Application.DTOs; 4 | using LSC.SmartCertify.Application.Interfaces.Courses; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Identity.Web.Resource; 8 | 9 | namespace LSC.SmartCertify.API.Controllers 10 | { 11 | [ApiController] 12 | [Route("api/[controller]")] 13 | public class CoursesController : ControllerBase 14 | { 15 | private readonly ICourseService _service; 16 | private readonly IValidator validator; 17 | private readonly IValidator updateValidator; 18 | 19 | public CoursesController( 20 | ICourseService service, 21 | IValidator validator, 22 | IValidator updateValidator) 23 | { 24 | _service = service; 25 | this.validator = validator; 26 | this.updateValidator = updateValidator; 27 | } 28 | 29 | /// 30 | /// Retrieves all courses. 31 | /// 32 | /// A list of courses. 33 | /// Returns the list of courses. 34 | [HttpGet] 35 | [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] 36 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 37 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 38 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 39 | [AllowAnonymous] 40 | public async Task>> GetCourses() 41 | { 42 | var mainCourses = new List { "Angular", ".NET Core", "Azure" }; 43 | var model = await _service.GetAllCoursesAsync(); 44 | 45 | foreach (var course in mainCourses) 46 | { 47 | var mainCourseItem = model.FirstOrDefault(w => 48 | w.Title.Equals(course, StringComparison.OrdinalIgnoreCase)); 49 | 50 | if (mainCourseItem != null) 51 | { 52 | mainCourseItem.QuestionCount = model 53 | .Where(w => w.Title.StartsWith(course, StringComparison.OrdinalIgnoreCase)) 54 | .Sum(s => s.QuestionCount); 55 | } 56 | } 57 | 58 | 59 | return Ok(model); 60 | } 61 | 62 | /// 63 | /// Retrieves a specific course by ID. 64 | /// 65 | /// The ID of the course to retrieve. 66 | /// The course with the specified ID. 67 | /// Returns the course if found. 68 | /// If the course is not found. 69 | [HttpGet("{id}")] 70 | [ProducesResponseType(typeof(CourseDto), StatusCodes.Status200OK)] 71 | [ProducesResponseType(StatusCodes.Status404NotFound)] 72 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 73 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 74 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 75 | [AllowAnonymous] 76 | public async Task> GetCourse(int id) 77 | { 78 | var course = await _service.GetCourseByIdAsync(id); 79 | return course == null ? NotFound() : Ok(course); 80 | } 81 | 82 | /// 83 | /// Creates a new course. 84 | /// 85 | /// The details of the course to create. 86 | /// The newly created course. 87 | /// Returns the created course. 88 | /// If the input is invalid. 89 | [HttpPost] 90 | [ProducesResponseType(typeof(CreateCourseDto), StatusCodes.Status201Created)] 91 | [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status400BadRequest)] 92 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 93 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 94 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 95 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 96 | [AdminRole] 97 | public async Task CreateCourse([FromBody] CreateCourseDto createCourseDto) 98 | { 99 | var validationResult = await validator.ValidateAsync(createCourseDto); 100 | 101 | if (!validationResult.IsValid) 102 | { 103 | return BadRequest(validationResult.Errors); 104 | } 105 | 106 | await _service.AddCourseAsync(createCourseDto); 107 | return CreatedAtAction(nameof(GetCourse), new { id = createCourseDto.Title }, createCourseDto); 108 | } 109 | 110 | /// 111 | /// Updates an existing course. 112 | /// 113 | /// The ID of the course to update. 114 | /// The updated course details. 115 | /// Indicates the update was successful. 116 | /// If the input is invalid. 117 | [HttpPut("{id}")] 118 | [ProducesResponseType(StatusCodes.Status204NoContent)] 119 | [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status400BadRequest)] 120 | [ProducesResponseType(StatusCodes.Status404NotFound)] 121 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 122 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 123 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 124 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 125 | [AdminRole] 126 | public async Task UpdateCourse(int id, [FromBody] UpdateCourseDto updateCourseDto) 127 | { 128 | var validationResult = await updateValidator.ValidateAsync(updateCourseDto); 129 | 130 | if (!validationResult.IsValid) 131 | { 132 | return BadRequest(validationResult.Errors); 133 | } 134 | 135 | await _service.UpdateCourseAsync(id, updateCourseDto); 136 | return NoContent(); 137 | } 138 | 139 | /// 140 | /// Deletes a course. 141 | /// 142 | /// The ID of the course to delete. 143 | /// Indicates the deletion was successful. 144 | [HttpDelete("{id}")] 145 | [ProducesResponseType(StatusCodes.Status204NoContent)] 146 | [ProducesResponseType(StatusCodes.Status404NotFound)] 147 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 148 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 149 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 150 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 151 | [AdminRole] 152 | public async Task DeleteCourse(int id) 153 | { 154 | await _service.DeleteCourseAsync(id); 155 | return NoContent(); 156 | } 157 | 158 | /// 159 | /// Updates the description of a course. 160 | /// 161 | /// The ID of the course to update. 162 | /// The updated course description. 163 | /// Indicates the update was successful. 164 | [HttpPatch("{id}")] 165 | [ProducesResponseType(StatusCodes.Status204NoContent)] 166 | [ProducesResponseType(StatusCodes.Status404NotFound)] 167 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 168 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 169 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 170 | [RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes:Write")] 171 | [AdminRole] 172 | public async Task UpdateDescription([FromRoute] int id, [FromBody] CourseUpdateDescriptionDto model) 173 | { 174 | await _service.UpdateDescriptionAsync(id, model.Description); 175 | return NoContent(); 176 | } 177 | } 178 | 179 | 180 | } 181 | -------------------------------------------------------------------------------- /LSC.SmartCertify.Infrastructure/ExamRepository.cs: -------------------------------------------------------------------------------- 1 | using LSC.SmartCertify.Application.DTOs; 2 | using LSC.SmartCertify.Application.Interfaces.Certification; 3 | using LSC.SmartCertify.Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace LSC.SmartCertify.Infrastructure 7 | { 8 | public class ExamRepository : IExamRepository 9 | { 10 | private readonly SmartCertifyContext _context; 11 | 12 | public ExamRepository(SmartCertifyContext context) 13 | { 14 | _context = context; 15 | } 16 | 17 | public async Task> GetRandomQuestionsAsync(int courseId, int count) 18 | { 19 | var mainCourses = new List() { "Angular", ".NET Core", "Azure" }; 20 | var courseIds = new List(); 21 | var courses = await _context.Courses.FindAsync(courseId); 22 | mainCourses.ForEach(e => 23 | { 24 | if (e.ToLower().Equals(courses?.Title.ToLower())) 25 | { 26 | courseIds = _context.Courses.Where(w => w.Title.ToLower(). 27 | StartsWith(courses.Title.ToLower())).Select(s => s.CourseId).ToList(); 28 | } 29 | }); 30 | 31 | if (courseIds.Any()) 32 | { 33 | return await _context.Questions 34 | .Where(q => courseIds.Contains(q.CourseId)) 35 | .OrderBy(q => Guid.NewGuid()) 36 | .Take(count) 37 | .ToListAsync(); 38 | } 39 | else 40 | { 41 | return await _context.Questions 42 | .Where(q => q.CourseId == courseId) 43 | .OrderBy(q => Guid.NewGuid()) 44 | .Take(count) 45 | .ToListAsync(); 46 | } 47 | } 48 | 49 | public async Task CreateExamWithQuestionsAsync(Exam exam, List questions) 50 | { 51 | if (!await _context.UserProfiles.AnyAsync(u => u.UserId == exam.UserId)) 52 | { 53 | throw new Exception($"UserId {exam.UserId} does not exist in the database."); 54 | } 55 | 56 | 57 | await _context.Exams.AddAsync(exam); 58 | await _context.SaveChangesAsync(); 59 | 60 | var examQuestions = questions.Select(q => new ExamQuestion 61 | { 62 | ExamId = exam.ExamId, 63 | QuestionId = q.QuestionId 64 | }).ToList(); 65 | 66 | await _context.ExamQuestions.AddRangeAsync(examQuestions); 67 | await _context.SaveChangesAsync(); 68 | } 69 | 70 | public Task GetExamQuestionAsync(int examId, int examQuestionId) 71 | { 72 | return _context.ExamQuestions 73 | .FirstOrDefaultAsync(eq => eq.ExamId == examId && eq.ExamQuestionId == examQuestionId); 74 | } 75 | 76 | public async Task UpdateExamQuestionAsync(ExamQuestion examQuestion) 77 | { 78 | _context.ExamQuestions.Update(examQuestion); 79 | await _context.SaveChangesAsync(); 80 | } 81 | 82 | public async Task> GetExamQuestionsAsync(int examId) 83 | { 84 | return await _context.ExamQuestions 85 | .Where(eq => eq.ExamId == examId) 86 | .ToListAsync(); 87 | } 88 | 89 | public async Task> GetUserExamsAsync(int userId) 90 | { 91 | var result = await _context.Exams 92 | .Join(_context.Courses, 93 | exam => exam.CourseId, 94 | course => course.CourseId, 95 | (exam, course) => new { exam, course }) 96 | .Where(e => e.exam.UserId == userId) 97 | .Select(e => new UserExam() 98 | { 99 | ExamId = e.exam.ExamId, 100 | CourseId = e.exam.CourseId, 101 | Title = e.course.Title, 102 | Description = e.course.Description, 103 | Status = e.exam.Status, 104 | StartedOn = e.exam.StartedOn, 105 | FinishedOn = e.exam.FinishedOn 106 | }).ToListAsync(); 107 | 108 | 109 | return result; 110 | 111 | } 112 | 113 | public async Task GetExamMetaDataAsync(int examId) 114 | { 115 | return await _context.Exams.Include(i => i.ExamQuestions).Select(s => new ExamDto() 116 | { 117 | ExamId = s.ExamId, 118 | CourseId = s.CourseId, 119 | UserId = s.UserId, 120 | Status = s.Status, 121 | StartedOn = s.StartedOn, 122 | FinishedOn = s.FinishedOn, 123 | QuestionIds = s.ExamQuestions.Select(s => s.QuestionId).ToList() 124 | }).FirstOrDefaultAsync(w => w.ExamId == examId); 125 | } 126 | 127 | public async Task SaveExamStatusAsync(int examId, string feedback) 128 | { 129 | var existingExam = await _context.Exams.FirstOrDefaultAsync(e => e.ExamId == examId); 130 | if (existingExam != null) 131 | { 132 | existingExam.Feedback = feedback; 133 | existingExam.Status = "Completed"; 134 | existingExam.FinishedOn = DateTime.UtcNow; 135 | _context.Exams.Update(existingExam); 136 | await _context.SaveChangesAsync(); 137 | await UpdateExamResults(examId); 138 | } 139 | } 140 | 141 | protected internal async Task UpdateExamResults(int examId) 142 | { 143 | // Fetch all exam questions for the given ExamId 144 | var examQuestions = await _context.ExamQuestions 145 | .Where(eq => eq.ExamId == examId) 146 | .ToListAsync(); 147 | 148 | // Fetch all relevant choices for the questions in the exam 149 | var questionIds = examQuestions.Select(eq => eq.QuestionId).Distinct(); 150 | var correctChoices = await _context.Choices 151 | .Where(c => questionIds.Contains(c.QuestionId) && c.IsCorrect) 152 | .ToListAsync(); 153 | 154 | // Create a dictionary for quick lookup of correct choices by QuestionId 155 | var correctChoiceMap = correctChoices 156 | .GroupBy(c => c.QuestionId) 157 | .ToDictionary( 158 | g => g.Key, 159 | g => g.Select(c => c.ChoiceId).ToList() 160 | ); 161 | 162 | // Update the IsCorrect field for each ExamQuestion 163 | foreach (var eq in examQuestions) 164 | { 165 | if (eq.SelectedChoiceId.HasValue && correctChoiceMap.ContainsKey(eq.QuestionId)) 166 | { 167 | eq.IsCorrect = correctChoiceMap[eq.QuestionId].Contains(eq.SelectedChoiceId.Value); 168 | } 169 | else 170 | { 171 | eq.IsCorrect = false; // No correct choice or no selection made 172 | } 173 | } 174 | 175 | // Save changes to the database 176 | await _context.SaveChangesAsync(); 177 | } 178 | 179 | 180 | public async Task GetExamDetailsAsync(int examId) 181 | { 182 | var examData = await (from exam in _context.Exams 183 | join course in _context.Courses on exam.CourseId equals course.CourseId 184 | join eq in _context.ExamQuestions on exam.ExamId equals eq.ExamId 185 | join question in _context.Questions on eq.QuestionId equals question.QuestionId 186 | where exam.ExamId == examId 187 | select new 188 | { 189 | exam.ExamId, 190 | course.Title, 191 | exam.Status, 192 | exam.StartedOn, 193 | exam.FinishedOn, 194 | question.QuestionText, 195 | eq.IsCorrect, 196 | question.DifficultyLevel 197 | }).ToListAsync(); 198 | 199 | if (!examData.Any()) return null; 200 | 201 | // Encapsulate shared data 202 | var firstRow = examData.First(); 203 | var response = new ExamResponseDto 204 | { 205 | ExamId = firstRow.ExamId, 206 | Title = firstRow.Title, 207 | Status = firstRow.Status, 208 | StartedOn = firstRow.StartedOn, 209 | FinishedOn = firstRow.FinishedOn, 210 | Questions = examData.Select(e => new UserExamQuestionDto 211 | { 212 | QuestionText = e.QuestionText, 213 | IsCorrect = Convert.ToBoolean(e.IsCorrect), 214 | DifficultyLevel = e.DifficultyLevel 215 | }).ToList() 216 | }; 217 | 218 | return response; 219 | } 220 | 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /LSC.SmartCertify.API/Program.cs: -------------------------------------------------------------------------------- 1 | 2 | using FluentValidation; 3 | using LSC.SmartCertify.API.Filters; 4 | using LSC.SmartCertify.API.Middlewares; 5 | using LSC.SmartCertify.Application; 6 | using LSC.SmartCertify.Application.DTOValidations; 7 | using LSC.SmartCertify.Application.Interfaces.Certification; 8 | using LSC.SmartCertify.Application.Interfaces.Common; 9 | using LSC.SmartCertify.Application.Interfaces.Courses; 10 | using LSC.SmartCertify.Application.Interfaces.Graph; 11 | using LSC.SmartCertify.Application.Interfaces.ManageUser; 12 | using LSC.SmartCertify.Application.Interfaces.QuestionsChoice; 13 | using LSC.SmartCertify.Application.Interfaces.Storage; 14 | using LSC.SmartCertify.Application.Services; 15 | using LSC.SmartCertify.Application.Services.Certification; 16 | using LSC.SmartCertify.Application.Services.Common; 17 | using LSC.SmartCertify.Application.Services.Graph; 18 | using LSC.SmartCertify.Application.Services.ManageUser; 19 | using LSC.SmartCertify.Infrastructure; 20 | using LSC.SmartCertify.Infrastructure.BackgroundServices; 21 | using LSC.SmartCertify.Infrastructure.Services.Storage; 22 | using Microsoft.ApplicationInsights.Extensibility; 23 | using Microsoft.AspNetCore.Authentication.JwtBearer; 24 | using Microsoft.AspNetCore.Diagnostics; 25 | using Microsoft.EntityFrameworkCore; 26 | using Microsoft.Identity.Web; 27 | using Microsoft.IdentityModel.Logging; 28 | using Scalar.AspNetCore; 29 | using Serilog; 30 | using Serilog.Templates; 31 | using System.Net; 32 | using System.Text.Json.Serialization; 33 | using TodoApp.WebAPI.Filters; 34 | 35 | namespace LSC.SmartCertify.API 36 | { 37 | public class Program 38 | { 39 | public static void Main(string[] args) 40 | { 41 | // Configure Serilog with the settings 42 | Log.Logger = new LoggerConfiguration() 43 | .WriteTo.Console() 44 | .WriteTo.Debug() 45 | .MinimumLevel.Information() 46 | .Enrich.FromLogContext() 47 | .CreateBootstrapLogger(); 48 | 49 | try 50 | { 51 | 52 | var builder = WebApplication.CreateBuilder(args); 53 | 54 | builder.Services.AddApplicationInsightsTelemetry(); 55 | 56 | builder.Host.UseSerilog((context, services, loggerConfiguration) => loggerConfiguration 57 | .ReadFrom.Configuration(context.Configuration) 58 | .ReadFrom.Services(services) 59 | .WriteTo.Console(new ExpressionTemplate( 60 | // Include trace and span ids when present. 61 | "[{@t:HH:mm:ss} {@l:u3}{#if @tr is not null} ({substring(@tr,0,4)}:{substring(@sp,0,4)}){#end}] {@m}\n{@x}")) 62 | .WriteTo.ApplicationInsights( 63 | services.GetRequiredService(), 64 | TelemetryConverter.Traces)); 65 | 66 | Log.Information("Starting the SmartCertify API..."); 67 | 68 | 69 | 70 | // Add services to the container. 71 | 72 | //use this for real database on your sql server 73 | builder.Services.AddDbContext(options => 74 | { 75 | options.UseSqlServer( 76 | builder.Configuration.GetConnectionString("DbContext"), 77 | providerOptions => providerOptions.EnableRetryOnFailure() 78 | ).EnableSensitiveDataLogging().EnableDetailedErrors(); 79 | } 80 | ); 81 | 82 | builder.Services.AddControllers(options => 83 | { 84 | options.Filters.Add(); // Add your custom validation filter 85 | options.Filters.Add(); 86 | }).ConfigureApiBehaviorOptions(options => 87 | { 88 | options.SuppressModelStateInvalidFilter = true; // Disable automatic validation 89 | }) 90 | .AddJsonOptions(options => 91 | { 92 | options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; 93 | options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); 94 | }); 95 | 96 | //builder.Services.AddHttpClient(); 97 | //builder.Services.Configure(builder.Configuration.GetSection("YouTube")); 98 | 99 | 100 | // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi 101 | builder.Services.AddEndpointsApiExplorer(); 102 | 103 | builder.Services.AddOpenApi(); 104 | 105 | 106 | builder.Services.AddScoped(); 107 | builder.Services.AddScoped(); 108 | builder.Services.AddScoped(); 109 | builder.Services.AddScoped(); 110 | builder.Services.AddScoped(); 111 | builder.Services.AddScoped(); 112 | builder.Services.AddScoped(); 113 | builder.Services.AddScoped(); 114 | builder.Services.AddScoped(); 115 | builder.Services.AddScoped(); 116 | builder.Services.AddScoped(); 117 | builder.Services.AddTransient(); 118 | builder.Services.AddTransient(); 119 | 120 | // Add FluentValidation 121 | builder.Services.AddValidatorsFromAssemblyContaining(); 122 | builder.Services.AddValidatorsFromAssemblyContaining(); 123 | builder.Services.AddAutoMapper(typeof(MappingProfile)); 124 | 125 | #region AD B2C configuration 126 | builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 127 | .AddMicrosoftIdentityWebApi(options => 128 | { 129 | builder.Configuration.Bind("AzureAdB2C", options); 130 | 131 | options.Events = new JwtBearerEvents 132 | { 133 | 134 | OnTokenValidated = context => 135 | { 136 | var logger = context.HttpContext.RequestServices.GetRequiredService>(); 137 | 138 | // Access the scope claim (scp) directly 139 | var scopeClaim = context.Principal?.Claims.FirstOrDefault(c => c.Type == "scp")?.Value; 140 | 141 | if (scopeClaim != null) 142 | { 143 | logger.LogInformation("Scope found in token: {Scope}", scopeClaim); 144 | } 145 | else 146 | { 147 | logger.LogWarning("Scope claim not found in token."); 148 | } 149 | 150 | 151 | return Task.CompletedTask; 152 | }, 153 | OnAuthenticationFailed = context => 154 | { 155 | var logger = context.HttpContext.RequestServices.GetRequiredService>(); 156 | logger.LogError("Authentication failed: {Message}", context.Exception.Message); 157 | return Task.CompletedTask; 158 | }, 159 | OnChallenge = context => 160 | { 161 | var logger = context.HttpContext.RequestServices.GetRequiredService>(); 162 | logger.LogError("Challenge error: {ErrorDescription}", context.ErrorDescription); 163 | return Task.CompletedTask; 164 | } 165 | }; 166 | }, options => { builder.Configuration.Bind("AzureAdB2C", options); }); 167 | 168 | // The following flag can be used to get more descriptive errors in development environments 169 | IdentityModelEventSource.ShowPII = true; 170 | #endregion AD B2C configuration 171 | 172 | builder.Services.AddHttpClient(); 173 | 174 | builder.Services.AddScoped(); 175 | builder.Services.AddSingleton(); 176 | builder.Services.AddScoped(); 177 | builder.Services.AddScoped(); 178 | 179 | // Register the background service 180 | builder.Services.AddHostedService(); 181 | builder.Services.AddHostedService(); 182 | 183 | // In production, modify this with the actual domains you want to allow 184 | builder.Services.AddCors(options => 185 | { 186 | options.AddPolicy("default", policy => 187 | { 188 | policy.AllowAnyOrigin() 189 | .AllowAnyHeader() 190 | .AllowAnyMethod(); 191 | }); 192 | }); 193 | 194 | 195 | var app = builder.Build(); 196 | 197 | // Configure the HTTP request pipeline. 198 | app.UseCors("default"); 199 | 200 | app.UseExceptionHandler(errorApp => 201 | { 202 | errorApp.Run(async context => 203 | { 204 | var exceptionHandlerPathFeature = context.Features.Get(); 205 | var exception = exceptionHandlerPathFeature?.Error; 206 | 207 | Log.Error(exception, "Unhandled exception occurred. {ExceptionDetails}", exception?.ToString()); 208 | Console.WriteLine(exception?.ToString()); 209 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 210 | await context.Response.WriteAsync("An unexpected error occurred. Please try again later."); 211 | }); 212 | }); 213 | 214 | 215 | app.UseMiddleware(); 216 | app.UseMiddleware(); 217 | app.UseMiddleware(); 218 | 219 | // Configure the HTTP request pipeline. 220 | //if (app.Environment.IsDevelopment()) 221 | { 222 | app.MapOpenApi(); 223 | app.MapScalarApiReference(options => 224 | { 225 | options.WithTitle("My API"); 226 | options.WithTheme(ScalarTheme.BluePlanet); 227 | options.WithSidebar(true); 228 | }); 229 | 230 | app.UseSwaggerUi(options => 231 | { 232 | options.DocumentPath = "openapi/v1.json"; 233 | }); 234 | 235 | } 236 | 237 | app.UseHttpsRedirection(); 238 | 239 | app.UseAuthorization(); 240 | 241 | 242 | app.MapControllers(); 243 | 244 | app.Run(); 245 | } 246 | catch (Exception ex) 247 | { 248 | Log.Fatal(ex, "Host terminated unexpectedly"); 249 | } 250 | finally 251 | { 252 | Log.CloseAndFlush(); 253 | } 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartCertify Clean Architecture .NET 9 API 2 | 3 | Welcome to the **SmartCertify Clean Architecture .NET 9 API** project! This repository showcases how to build a robust API with a clean architecture using .NET 9, following best practices to design scalable and maintainable applications. 4 | 5 | The project is structured with a focus on **Clean Architecture**, utilizing the **Entity Framework Core (EF Core)** for database access and **Swagger, Scalar,and NSwag** for API documentation. 6 | 7 | --- 8 | 9 | ## Episode Overview 10 | 11 | ### **How to Build an Online Course Certification Platform | Architecture & App Overview** 12 | Welcome to Episode 1 of the SmartCertify series! 🚀 In this video, we’ll kick off by exploring the architecture and overview of the SmartCertify App, a full-stack web application designed to revolutionize online course certification. 13 | 14 | Discover the clean architecture design, learn how the app is structured, and see how we’ll use cutting-edge technologies like Angular 19, .NET Core 9 Web API, and Azure. Whether you’re a beginner or an experienced developer, this series will guide you step-by-step to build a powerful, scalable platform for the future of online education. 15 | 16 | **Key Topics:** 17 | - Project architecture and structure 18 | - App overview and high-level design 19 | - Exploring heavy Azure Services that we will use in this Application 20 | 21 | --- 22 | 23 | ### **Install SQL Express, SSMS, Azure Data Studio & Connect to SQL Server | Step-by-Step Guide** 24 | In this video, learn how to install SQL Express, SQL Server Management Studio (SSMS), and Azure Data Studio. We’ll guide you through connecting to a SQLExpress server and accessing your database. Perfect for beginners starting with SQL databases! Stay tuned for more episodes in our SmartCertify series, where we build a robust online course certification platform. 25 | 26 | **Key Topics:** 27 | - Installation of SQL Tools 28 | - Azure Data Studio Installation 29 | - SQL Express Installation, SQL Server Management Studio (SSMS) Installation 30 | 31 | - ### **Install SQL Server and Tools** 32 | 33 | 1. **SQL Server Management Studio (SSMS)** 34 | Download and install SQL Server Management Studio (SSMS) to manage your SQL Server databases. 35 | - [SSMS Download (Official)](https://learn.microsoft.com/en-us/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver16) 36 | - [Direct SSMS Full Setup](https://aka.ms/ssmsfullsetup) 37 | - [SQL Server Downloads](https://www.microsoft.com/en-us/sql-server/sql-server-downloads) 38 | 39 | 2. **SQL Server Express Edition** 40 | SQL Server 2022 Express is a free edition suitable for development and small applications. 41 | - [SQL Server Express 2022 Download](https://go.microsoft.com/fwlink/p/?linkid=2216019&clcid=0x409&culture=en-us&country=us) 42 | 43 | 3. **Azure Data Studio** 44 | An alternative to SSMS for working with SQL databases, designed for data professionals who prefer a cross-platform tool. 45 | - [Azure Data Studio](https://azure.microsoft.com/en-us/products/data-studio) 46 | - [Azure Data Studio Download Link](https://go.microsoft.com/fwlink/?linkid=2216158&clcid=0x409) 47 | 48 | 49 | --- 50 | 💬 Got questions or stuck? Drop a comment or join our Telegram community for quick help! 51 | 52 | 💡 Want to ask questions, discuss coding, and interact with others? 53 | 👉 Join our community group here: t.me/LearnSmartCodingYTCommunity 54 | 55 | ## Video Timestamp: 56 | - 00:00 - App Demo & Features Overview 57 | - 06:35 - Architecture & Tech Stack Explained 58 | - 10:30 - Install SQL Tools & Connect to DB 59 | - 19:22 - SQL Basics for Beginners 60 | - 44:23 - Database Design: Tables, Keys, Constraints 61 | - 1:00:16 - .NET Core 9 Web API Setup (Clean Architecture) 62 | - 1:25:41 - Git & Azure DevOps for Beginners 63 | - 1:42:31 - Azure Policy to Save Costs 64 | - 2:01:29 - Secure Azure SQL Connections 65 | - 2:09:45 - Angular 19 Standalone Components 66 | - 2:31:51 - Angular Data Binding & Directives 67 | - 2:55:18 - Lazy Loading & Routing in Angular 68 | - 3:09:16 - Angular Signals & State Management 69 | - 3:26:52 - Full CRUD API with Patch + Validation 70 | - 4:51:00 - Reusable API Service in Angular 71 | - 5:02:18 - Input/Output Filters & Component Comm 72 | - 5:24:14 - Secure Apps with Azure AD B2C 73 | - 6:12:20 - Deploy .NET API to Azure with CI/CD 74 | - 6:51:24 - Deploy Angular 19 to Azure with CI/CD 75 | - 7:18:01 - Monitor APIs with App Insights 76 | - 7:36:44 - Handle 500 Errors in Angular with Toast 77 | - 7:41:33 - Set Alerts for 500 Errors in App Insights 78 | - 7:52:13 - Azure Functions HTTP Trigger (Email Alerts) 79 | - 8:15:28 - CI/CD for Azure Functions with DevOps 80 | - 8:34:20 - Enrich JWT with Azure AD B2C API Connector 81 | - 8:51:56 - Background Service for DB Tasks 82 | - 9:00:13 - Email Automation via Timer Trigger 83 | - 9:08:12 - Public vs Private Azure Containers 84 | - 9:23:37 - Secure Image Upload: Angular + .NET 85 | - 9:33:03 - Generate Secure SAS Tokens in .NET 86 | - 9:39:55 - Fetch Azure AD B2C Data with Graph API 87 | - 9:57:14 - Role-Based Auth with AD B2C & Angular 88 | - 10:04:20 - Azure Key Vault + Managed Identity 89 | - 10:33:35 - Managed Identity vs Service Principal 90 | 91 | Demo App: https://smartcertify-web.azurewebsites.net/home 92 | --- 93 | 94 | ### **SQL Basics for Beginners: Step-by-Step Guide with Examples** 95 | In this episode, we dive into the basics of SQL, including database creation, table design, constraints, and SQL commands like SELECT, INSERT, UPDATE, DELETE, etc. This is a must-watch for beginners who want to understand how databases work. 96 | 97 | **Key Topics:** 98 | - SQL commands (SELECT, INSERT, UPDATE, DELETE) 99 | - Database design and normalization 100 | - Primary/Foreign Keys and Constraints 101 | 102 | --- 103 | 104 | ### **Beginner's Guide to Database Design: Tables, Keys, and Constraints** 105 | Unlock the foundations of database design in this beginner-friendly guide. In this video, we explore key concepts like tables, primary keys, foreign keys, and constraints while walking through a real-world schema for a course certification app. Learn about designing efficient databases, creating relationships between tables, and using constraints to maintain data integrity. This practical, step-by-step guide is perfect for developers, students, or anyone interested in SQL and database management. 106 | 107 | **Key Topics:** 108 | - Understanding database design 109 | - Creating tables with constraints 110 | - Primary/Foreign Keys and Indexes 111 | - Setting up databases 112 | - Creating and managing tables 113 | - Understanding primary keys and foreign keys 114 | - Adding constraints for data validation 115 | - SQL basics: SELECT, INSERT, UPDATE, DELETE 116 | 117 | --- 118 | 119 | ### **Build .NET Core 9 Web API with Clean Architecture | Project Setup** 120 | In this video, we’ll walk through the process of creating a .NET Core 9 Web API project using Clean Architecture principles. We'll set up the project structure, configure Entity Framework (EF) Core, and generate entities from the database we built in the previous episode. Additionally, we’ll show how to configure Swagger alternatives in .NET Core 9, as Swagger's default scaffold is removed. Perfect for anyone looking to structure their Web API efficiently and learn about the latest .NET Core 9 changes. 121 | 122 | **Key Topics:** 123 | - .NET 9 Web API project setup using Clean Architecture Princeples 124 | - Scaffold database entities with EF Core 125 | - Swagger setup and configuration. NSwag and Scalar for API documentation. 126 | 127 | --- 128 | 129 | ### **Git & Azure DevOps for absolute beginners!** 130 | Are you an absolute beginner in Git or Azure DevOps? This video is your ultimate guide! We’ll walk you through the basics of Git, including essential commands, and show you how to set up an Azure DevOps account from scratch. Learn how to create repositories, push your local code to the Azure DevOps repo, and manage your projects seamlessly. Whether you're new to version control or looking to understand Azure DevOps, this tutorial has everything you need to get started. 131 | 132 | **Key Topics:** 133 | - Git basics and setup 134 | - Azure DevOps account and repository creation 135 | - Pushing code to Git with Git commands 136 | - Setting up your Azure DevOps account. 137 | - Overview of Azure DevOps and its features. 138 | - Step-by-step repo creation in Azure DevOps. 139 | - Using Git commands to push code from local to remote repositories. 140 | - Pro tips for managing your code with Git and Azure DevOps. 141 | 142 | - ### **Git Commands to Initialize and Push Code to Repository** 143 | To get started with version control, follow these Git commands to set up a new Git repository and push your code to Azure DevOps. 144 | 145 | ``` 146 | # Initialize a new Git repository in your local project 147 | git init 148 | 149 | # Link your local repository to the remote repository on Azure DevOps 150 | git remote add origin 151 | 152 | # Fetch the latest information from the remote repository 153 | git fetch origin 154 | 155 | # Create a new branch named 'main' and switch to it 156 | git checkout -b main 157 | 158 | # Stage all changes in your local project for the next commit 159 | git add . 160 | 161 | # Commit the staged changes with a message 162 | git commit -m "Initial commit" 163 | 164 | # Push the 'main' branch to the remote repository 165 | git push -u origin main 166 | ``` 167 | 168 | ## Git Commands Explanation 169 | git init 170 | Initializes a new Git repository in your local project directory. 171 | 172 | git remote add origin 173 | Links your local Git repository to a remote repository. In this case, it connects to the Azure DevOps repository. 174 | 175 | git fetch origin 176 | Fetches updates from the remote repository without merging them into your local branch. 177 | 178 | git checkout -b main 179 | Creates a new branch named main and switches to it. This step aligns your local branch naming with the remote repository's naming convention. 180 | 181 | git add . 182 | Stages all the changes you've made in your local repository for the next commit. 183 | 184 | git commit -m "Initial commit" 185 | Commits the staged changes to the local repository with a descriptive message. 186 | 187 | git push -u origin main 188 | Pushes the main branch to the remote repository and sets it as the default upstream branch for future pushes/pulls. 189 | 190 | --- 191 | - ### **Save Money with Azure Policy: Restrict SQL Database to Basic Tier | Full Step-by-Step Guide** 192 | In this step-by-step Azure tutorial, learn how to create and assign a custom Azure Policy to restrict SQL Database creation to the cost-effective Basic tier. Watch as we: 193 | 194 | Create a new Azure Subscription 195 | Define a custom Azure Policy with restrictions for SQL Databases 196 | Assign the policy to the new subscription 197 | Create an SQL Server and Database under the subscription 198 | Test the policy enforcement by attempting to select a pricing tier other than Basic 199 | This tutorial is perfect for Azure beginners, cloud administrators, and developers looking to manage resources effectively and control costs. 200 | 201 | 📌 What You’ll Learn: 202 | 203 | - What is Azure Policy? 204 | - How to define and assign a custom policy 205 | - Enforcing SQL Database SKU restrictions in Azure 206 | - Testing policy functionality in real time 207 | 208 | **Sample Policy Shown in the Video** 209 | ``` 210 | { 211 | "properties": { 212 | "displayName": "Restrict SQL Database SKU to Basic", 213 | "policyType": "Custom", 214 | "mode": "All", 215 | "description": "This policy restricts the creation of Azure SQL Databases to the Basic SKU only.", 216 | "metadata": { 217 | "category": "SQL", 218 | "createdBy": "your userid guid here", 219 | "createdOn": "2025-01-10T19:16:33.2197985Z", 220 | "updatedBy": null, 221 | "updatedOn": null 222 | }, 223 | "version": "1.0.0", 224 | "parameters": {}, 225 | "policyRule": { 226 | "if": { 227 | "allOf": [ 228 | { 229 | "field": "type", 230 | "equals": "Microsoft.Sql/servers/databases" 231 | }, 232 | { 233 | "not": { 234 | "field": "Microsoft.Sql/servers/databases/sku.name", 235 | "equals": "Basic" 236 | } 237 | } 238 | ] 239 | }, 240 | "then": { 241 | "effect": "deny" 242 | } 243 | }, 244 | "versions": [ 245 | "1.0.0" 246 | ] 247 | }, 248 | "id": "/subscriptions/your-subscription-id/providers/Microsoft.Authorization/policyDefinitions/yourid", 249 | "type": "Microsoft.Authorization/policyDefinitions", 250 | "name": "ee15c7b5-dc76-43f2-aa9e-876f286a460c", 251 | "systemData": { 252 | "createdBy": "learnsmartcoding@gmail.com", 253 | "createdByType": "User", 254 | "createdAt": "2025-01-10T19:16:33.2054339Z", 255 | "lastModifiedBy": "learnsmartcoding@gmail.com", 256 | "lastModifiedByType": "User", 257 | "lastModifiedAt": "2025-01-10T19:16:33.2054339Z" 258 | } 259 | } 260 | ``` 261 | 262 | 263 | 264 | - ### **Mastering Secure Azure SQL Database Connections Effortlessly** 265 | Learn how to connect your Azure SQL Database to Azure Data Studio in this step-by-step tutorial! We’ll guide you through configuring the Azure SQL server firewall to allow your local system's IP address, ensuring secure and seamless access. Perfect for developers and cloud enthusiasts looking to manage Azure SQL databases efficiently. 266 | 267 | 📌 Topics Covered: 268 | 269 | - Configuring the Azure SQL server firewall 270 | - Finding your local IP address for firewall rules 271 | - Connecting to Azure SQL Database using Azure Data Studio 272 | - Troubleshooting common connectivity issues 273 | 274 | 275 | - ### **Mastering Angular 19 Standalone Components Quickly | Angular 19 first Episode** 276 | Kickstart your Angular journey with this comprehensive beginner's guide! Learn how to set up your local environment by installing Node.js and Angular, create your first Angular project, and understand the basics of Angular components. This video simplifies key concepts and walks you through everything you need to know to start building Angular applications. Perfect for developers at any level! 277 | 278 | - [Angular 19 Repo]( https://github.com/learnsmartcoding/smartcertify-ui-angular19) 279 | - [YT Video](https://youtu.be/SwJ4RMQnJOo) 280 | 281 | - ### **Master Angular Data Binding and Directives | Complete Beginner's Guide** 282 | Unlock the power of Angular with this comprehensive guide to data binding and directives. Learn how to efficiently bind data between your components and templates using interpolation, property binding, event binding, and two-way binding. Dive into Angular's powerful built-in directives like *ngIf, *ngFor, and explore the latest @if() syntax for cleaner and modern conditional rendering. Whether you're new to Angular or looking to sharpen your skills, this tutorial is perfect for beginners. Start building interactive and dynamic web apps today! 283 | 284 | - [Angular 19 Repo]( https://github.com/learnsmartcoding/smartcertify-ui-angular19) 285 | - [YT Video](https://youtu.be/gnGPcTCn81w) 286 | 287 | - ### **Supercharge Your Angular 19 App with Lazy Loading Techniques & Routing** 288 | Learn how to implement efficient and modern Angular Routing with Lazy Loading in Angular 19, designed for standalone components. In this video, we’ll cover how to set up dynamic routes, lazy load your components to optimize performance, and handle 404 pages with wildcard routes. Whether you're building small apps or large-scale enterprise solutions, understanding routing and lazy loading is essential for creating seamless, fast-loading applications. Perfect for beginners and intermediate Angular developers! 289 | 290 | - [Angular 19 Repo]( https://github.com/learnsmartcoding/smartcertify-ui-angular19) 291 | - [YT Video](https://youtu.be/pu44fnIompg) 292 | 293 | - ### **Build a Full Course App using Angular 19 - Interactive Demo!** 294 | 🚀 Explore the Complete User Journey in Our Course App! 295 | 296 | In this demo, we walk you through how to create an interactive, real-world course app where users can: 297 | ✅ Browse and select available courses 298 | ✅ Start, pause, and resume tests 299 | ✅ Complete tests and receive certifications 300 | ✅ Access video references for deeper understanding 301 | 302 | This app is built using the latest Angular 18 for the frontend and .NET Core 8 for the backend, integrating powerful features like Azure AD B2C for secure authentication and dynamic UI/UX design. 303 | 304 | Whether you're looking to build a similar app or test your skills, this video is your perfect guide. Watch now and get ready to create an engaging and functional application for online learning! 305 | 306 | - [Angular 19 Repo]( https://github.com/learnsmartcoding/smartcertify-ui-angular19) 307 | - [YT Video](https://youtu.be/M_lCcGwrYlo) 308 | 309 | - ### **Mastering Angular Signals in 2025 for EASY State Management** 310 | In this video, you'll learn all about Angular Signals, a new and powerful way to manage state and keep your UI in sync with data changes in Angular applications. Discover how Signals simplify the process of state management by automatically updating the UI when their value changes, making it easier and more efficient than ever to create reactive applications. 311 | 312 | We also explore Computed Signals and Effects, which enable you to work with dynamic and real-time data efficiently. Plus, see a practical example with our Contacts Component to see Signals in action! 313 | 314 | 315 | - [Angular 19 Repo](https://github.com/learnsmartcoding/smartcertify-ui-angular19) 316 | - [YT Video](https://youtu.be/Wm2yRolfK8I) 317 | 318 | - ### **.NET Core 9 Web API – Build Full CRUD with PATCH | Clean Architecture & Fluent Validation** 319 | 🚀 Mastering .NET Core 9 Web API – Full CRUD with PATCH, Clean Architecture & Fluent Validation! - Episode 12 320 | 321 | In this episode, we dive deep into .NET Core 9 Web API and implement 3 controllers with complete CRUD operations (GET, POST, PUT, DELETE, PATCH). We follow Clean Architecture principles, ensuring a scalable and maintainable API structure. 322 | 323 | 🔥 Key Topics Covered: 324 | ✅ Implementing GET, POST, PUT, DELETE & PATCH endpoints in .NET Core 9 325 | ✅ Using Fluent Validation for robust input validation 326 | ✅ Applying Clean Architecture to structure the Web API efficiently 327 | ✅ Handling partial updates (PATCH) properly 328 | ✅ Ensuring best practices for API design and maintainability 329 | 330 | 📌 Whether you're a beginner or an experienced developer, this tutorial will help you build a well-structured, professional-grade Web API. 331 | 332 | - [.Net 9 Web API Repo](https://github.com/learnsmartcoding/smartcertify-api-clean-architecture-dotnet9) 333 | - [Angular 19 Repo](https://github.com/learnsmartcoding/smartcertify-ui-angular19) 334 | - [App Demo](https://smartcertify-web.azurewebsites.net/home) 335 | - [YT Video](https://youtu.be/lAVUy2U9QgY) 336 | 337 | - ### **Mastering API Calls In Angular With A Reusable Service** 338 | In this video, we’ll build an Angular service to fetch data from a .NET Web API using HttpClient. We’ll cover best practices, how to handle API calls efficiently, and demonstrate a real-time example of fetching and displaying data in an Angular app. 🚀 339 | 340 | 👉 What You’ll Learn: 341 | ✅ How to create an Angular service using ng generate service 342 | ✅ Use HttpClient to call a .NET Web API 343 | ✅ Implement best practices for API calls in Angular 344 | ✅ Handle error responses and implement loading states 345 | ✅ Display fetched data dynamically in an Angular component 346 | 347 | 💡 This is a must-watch for developers working with Angular & .NET Core APIs! Don’t forget to like, share, and subscribe for more hands-on tutorials. 🔥 348 | 349 | 350 | - [Angular 19 Repo](https://github.com/learnsmartcoding/smartcertify-ui-angular19) 351 | - [YT Video](https://youtu.be/kqHFDvnjbcw) 352 | 353 | - ### **Angular Input() & Output() Explained | Parent-Child Communication with Filters** 354 | In this video, Learn how to use @Input() and @Output() in Angular to enable smooth parent-child component communication. In this video, we demonstrate real-world filtering functionality using a course browsing example. The child component handles the filter selection, and the parent component updates the course list accordingly. Master Angular component interaction with this practical hands-on guide! 🚀 355 | 356 | 🔹 Topics Covered: 357 | ✅ Understanding @Input() for passing data from parent to child 358 | ✅ Using @Output() and EventEmitter for child-to-parent communication 359 | ✅ Implementing a course filtering system using these concepts 360 | ✅ Best practices for Angular component interaction 361 | 362 | 363 | - [Angular 19 Repo](https://github.com/learnsmartcoding/smartcertify-ui-angular19) 364 | - [YT Video](https://youtu.be/ThkG1Pkf_1s) 365 | 366 | - ### **Azure AD B2C Makes Securing Angular And .NET Apps Easy And FAST** 367 | In this video, learn how to configure Azure AD B2C in the Azure portal for Angular and .NET Web API applications. We will walk through the step-by-step setup, integrating Azure AD B2C authentication into both frontend (Angular) and backend (.NET Core Web API) applications. Finally, we will test and demo the authentication flow to see it in action! 368 | 369 | 🔹 What You'll Learn: 370 | ✅ Azure AD B2C setup in the Azure portal 371 | ✅ App registrations for Angular and .NET Web API 372 | ✅ Configuring authentication in Angular using MSAL 373 | ✅ Securing .NET Web API with Azure AD B2C 374 | ✅ Testing login, logout, and API authorization 375 | 376 | 📌 Whether you're a beginner or an experienced developer, this tutorial will help you seamlessly integrate Azure AD B2C authentication in your apps! 377 | - [YT Video](https://youtu.be/V-3WAJd67EM) 378 | 379 | - ### **Deploy Your .NET Core 9 Web API to Azure For FREE | CI/CD with Azure DevOps** 380 | In this step-by-step tutorial, we will deploy a .NET Core 9 Web API to Azure App Service using Azure DevOps CI/CD. We'll cover everything from setting up a free F1 App Service, managing code in Azure DevOps, creating a CI/CD pipeline, using a service principal for secure deployment, and configuring connection strings in environment settings. 381 | 382 | 🔹 What You’ll Learn in This Video: 383 | ✅ Create a .NET Core 9 Web API Azure App Service (Free F1 Plan) 384 | ✅ Move your project from GitHub to Azure DevOps 385 | ✅ Set up CI/CD Pipelines for automated deployment 386 | ✅ Create a Service Principal for secure authentication 387 | ✅ Configure Build & Release Pipelines to deploy code 388 | ✅ Manage environment variables & connection strings in Azure 389 | 390 | By the end of this video, your .NET Core 9 Web API will be live on Azure, fully automated with Azure DevOps CI/CD! 🚀 391 | 392 | - [YT Video](https://youtu.be/8tYvsO4emAc) 393 | 394 | - ### **Angular Input() & Output() Explained | Parent-Child Communication with Filters** 395 | In this step-by-step tutorial, we will deploy an Angular 19 app to Azure App Service using Azure DevOps CI/CD. This video covers the complete process, from creating a free F1 App Service, setting up Azure DevOps repositories, configuring CI/CD pipelines, using a service principal for secure deployment, and handling environment settings in Azure. 396 | 397 | 🔹 What You’ll Learn in This Video: 398 | - ✅ Create an Angular 19 App Service on Azure (Free F1 Plan) 399 | - ✅ Move project from GitHub to Azure DevOps 400 | - ✅ Set up CI/CD Pipelines for automated Angular deployment 401 | - ✅ Use Service Principal for secure authentication 402 | - ✅ Configure Build & Release Pipelines to deploy Angular 403 | - ✅ Manage environment variables & settings in Azure 404 | 405 | By the end of this video, your Angular 19 app will be live on Azure, fully automated with Azure DevOps CI/CD! 🚀 406 | - [YT Video](https://youtu.be/IuELMXSAslo) 407 | 408 | - ### **Azure AD B2C Makes Securing Angular And .NET Apps Easy And FAST** 409 | 410 | In this video, learn how to configure Azure AD B2C in the Azure portal for Angular and .NET Web API applications. We will walk through the step-by-step setup, integrating Azure AD B2C authentication into both frontend (Angular) and backend (.NET Core Web API) applications. Finally, we will test and demo the authentication flow to see it in action! 411 | 412 | 🔹 What You'll Learn: 413 | ✅ Azure AD B2C setup in the Azure portal 414 | ✅ App registrations for Angular and .NET Web API 415 | ✅ Configuring authentication in Angular using MSAL 416 | ✅ Securing .NET Web API with Azure AD B2C 417 | ✅ Testing login, logout, and API authorization 418 | 419 | 📌 Whether you're a beginner or an experienced developer, this tutorial will help you seamlessly integrate Azure AD B2C authentication in your apps! 420 | 421 | - ### **Deploy Your .NET Core 9 Web API to Azure For FREE | CI/CD with Azure DevOps** 422 | In this step-by-step tutorial, we will deploy a .NET Core 9 Web API to Azure App Service using Azure DevOps CI/CD. We'll cover everything from setting up a free F1 App Service, managing code in Azure DevOps, creating a CI/CD pipeline, using a service principal for secure deployment, and configuring connection strings in environment settings. 423 | 424 | 🔹 What You’ll Learn in This Video: 425 | - ✅ Create a .NET Core 9 Web API Azure App Service (Free F1 Plan) 426 | - ✅ Move your project from GitHub to Azure DevOps 427 | - ✅ Set up CI/CD Pipelines for automated deployment 428 | - ✅ Create a Service Principal for secure authentication 429 | - ✅ Configure Build & Release Pipelines to deploy code 430 | - ✅ Manage environment variables & connection strings in Azure 431 | 432 | By the end of this video, your .NET Core 9 Web API will be live on Azure, fully automated with Azure DevOps CI/CD! 🚀 433 | 434 | - ### **Deploy Angular 19 to Azure App Service | CI/CD with Azure DevOps** 435 | In this step-by-step tutorial, we will deploy an Angular 19 app to Azure App Service using Azure DevOps CI/CD. This video covers the complete process, from creating a free F1 App Service, setting up Azure DevOps repositories, configuring CI/CD pipelines, using a service principal for secure deployment, and handling environment settings in Azure. 436 | 437 | 🔹 What You’ll Learn in This Video: 438 | - ✅ Create an Angular 19 App Service on Azure (Free F1 Plan) 439 | - ✅ Move project from GitHub to Azure DevOps 440 | - ✅ Set up CI/CD Pipelines for automated Angular deployment 441 | - ✅ Use Service Principal for secure authentication 442 | - ✅ Configure Build & Release Pipelines to deploy Angular 443 | - ✅ Manage environment variables & settings in Azure 444 | 445 | By the end of this video, your Angular 19 app will be live on Azure, fully automated with Azure DevOps CI/CD! 🚀 446 | 447 | 448 | - ### **Mastering Azure Application Insights For PRO API Monitoring** 449 | In this video, we will integrate Azure Application Insights into a .NET Core 9 Web API to monitor performance, detect failures, and log application telemetry. Additionally, we will cover global exception handling and how to return meaningful error responses, ensuring a better user experience and enabling users to contact support when issues arise. 450 | 451 | 🔹 What You’ll Learn in This Video: 452 | - ✅ Set up Azure Application Insights for real-time monitoring 453 | - ✅ Track API requests, failures, and performance metrics 454 | - ✅ Implement Global Exception Handling in .NET Core 9 455 | - ✅ Return structured error responses with relevant details 456 | - ✅ Include user-friendly support info in API error messages 457 | - ✅ Real-world debugging & troubleshooting best practices 458 | 459 | By the end of this tutorial, your .NET Core 9 Web API will be equipped with robust monitoring and error handling, making it production-ready with detailed insights for debugging! 🚀 460 | 461 | 462 | - ### **Handle 500 Errors Gracefully in Angular 🚀 | Global Exception Handling & Toastr Notifications** 463 | Handling errors properly is crucial for a great user experience! In this video, we will implement global exception handling in Angular using an HTTP Interceptor to catch 500 Internal Server Errors returned from a .NET Core Web API. We will display a persistent Toastr notification with a request ID, allowing users to contact support if needed. 464 | 465 | 🔥 What You’ll Learn: 466 | - ✅ Create an HTTP Interceptor to catch API errors globally. 467 | - ✅ Display Toastr notifications for errors, keeping them visible until closed. 468 | - ✅ Extract and show request IDs from API error responses. 469 | - ✅ Improve user experience by handling errors in a real-world scenario. 470 | 471 | If you're working with Angular and .NET Core APIs, this is a must-watch! 🚀 472 | 473 | - ### **Set Up Azure App Insights Alerts for 500 Errors | Monitor & Detect API Failures** 474 | Proactively monitor your .NET Core Web API errors with Azure Application Insights! In this video, we’ll set up alerts for 500 exceptions in Azure App Insights, ensuring that critical errors are detected and addressed quickly. 475 | 476 | 🔥 What You’ll Learn: 477 | - ✅ Configure Azure App Insights Alerts to detect API failures automatically. 478 | - ✅ Use a custom Kusto Query to fetch and track exceptions. 479 | - ✅ Receive real-time notifications when a 500 Internal Server Error occurs. 480 | - ✅ Improve API reliability and quickly respond to application failures. 481 | 482 | 🔔 This alert setup is possible because we previously configured App Insights for our Web API. If you haven’t done that yet, check out our earlier video! 483 | 484 | - ### **Azure Functions Guide: Create .NET Core 9 Isolated HTTP Trigger for Email Alerts** 485 | Azure Functions provide a serverless computing model, making it easy to run event-driven code without managing infrastructure. In this video, we will: 486 | - ✅ Understand Azure Functions – What they are and when to use them 487 | - ✅ Create an Azure Function in the portal 488 | - ✅ Build an HTTP-triggered function in Visual Studio using .NET Core 9 (STS) isolated model 489 | - ✅ Accept userId and notificationId as request body 490 | - ✅ Fetch notification details from a database and send an email to the user 491 | - ✅ Learn how serverless functions work with databases and external services 492 | 493 | This real-world example demonstrates how to use Azure Functions for automating tasks like sending notifications with minimal infrastructure overhead. 494 | 495 | - ### **CI/CD for Azure Functions: Deploy .NET Core 9 Function App via Azure DevOps** 496 | Automate your Azure Function App deployment with Azure DevOps CI/CD pipeline! In this video, we will: 497 | - ✅ Push the Azure Function App code to Azure DevOps 498 | - ✅ Set up a CI/CD pipeline to automatically build and deploy the function 499 | - ✅ Use the Azure Function App created in the previous video 500 | - ✅ Configure application settings in the Azure portal for smooth execution 501 | - ✅ Ensure secure and efficient deployment with best practices 502 | 503 | This tutorial eliminates manual deployments and ensures continuous integration and delivery for serverless applications. 504 | 505 | - ### **Azure AD B2C API Connector: Enrich JWT Tokens with User Data via Azure Functions** 506 | Enhance Azure AD B2C authentication by integrating an API Connector with Azure Functions! 🚀 In this video, we will: 507 | - ✅ Add two more HTTP trigger functions to our existing Azure Function App 508 | - ✅ Integrate one function with Azure AD B2C API Connector to handle authentication requests 509 | - ✅ Read token information, fetch user details from the database, and update the UserProfile table 510 | - ✅ Enrich Azure AD B2C tokens with additional claims like UserID and Role 511 | - ✅ Demonstrate how this enriched token will be used later in Angular for role-based access control 512 | 513 | This tutorial helps customize authentication flows, ensuring better security and a personalized experience for users. 514 | 515 | - ### **.NET Core Background Service | Automate Database Tasks in Web API** 516 | Learn how to implement a background service in .NET Core Web API that runs automatically and interacts with a database! 🚀 In this video, we will: 517 | 518 | - ✅ Create a Background Service in .NET Core Web API 519 | - ✅ Configure it to run every hour to process new records in the Notification table 520 | - ✅ Retrieve all users from the UserProfile table 521 | - ✅ Insert new notification records for each user into the UserNotification table 522 | - ✅ Understand how a background service interacts with a database behind the scenes 523 | 524 | This tutorial is perfect for automating scheduled tasks like notifications, cleanups, and background data processing in your ASP.NET Core applications! 525 | 526 | - ### **Automate Emails Like a Pro! Azure Function Timer & HTTP Trigger in .NET** 527 | In this video, we will explore Azure Functions with both Timer and HTTP Triggers to automate email notifications! 🚀 Here's what you'll learn: 528 | 529 | - ✅ Create a Timer-Triggered Azure Function to check the UserNotification table for unsent notifications 530 | - ✅ Invoke another HTTP-triggered Azure Function to send emails 531 | - ✅ Pass UserId & NotificationId dynamically to trigger the email-sending function 532 | - ✅ Configure local settings & Azure Function App settings for seamless execution 533 | - ✅ Understand function-to-function communication in Azure 534 | 535 | This tutorial is perfect for implementing scheduled tasks, event-driven notifications, and serverless automation using .NET Core 9 (Isolated Model) with Azure Functions. 536 | 537 | - ### **Effortless & Secure Image Upload to Azure | Angular & .NET Core API Guide** 538 | 🚀 Learn how to securely upload images from an Angular app to Azure Storage using .NET Core API! In this step-by-step guide, we will: 539 | 540 | - ✅ Create API Endpoints to handle image uploads 541 | - ✅ Upload Images from Angular App with a simple UI 542 | - ✅ Securely Store Images in a private Azure Storage container 543 | - ✅ Save Image URLs in the UserProfile table for future retrieval 544 | - ✅ Best Practices for Secure File Upload & Storage 545 | 546 | 📢 By the end of this video, you’ll be able to integrate secure image uploads in your own applications! 547 | 548 | - ### **🔒 Protect Your Azure Storage! Generate SAS Token in .NET Core API** 549 | 🔐 Secure Azure Storage with SAS Tokens! In this video, we’ll show you how to generate SAS tokens in .NET Core API to securely access and manage user files in Azure Blob Storage. 550 | 551 | - ✅ Generate SAS Token in .NET Core API to control access to private Azure Storage containers 552 | - ✅ User-Specific Access – Ensure only logged-in users can access their images 553 | - ✅ Set expiry & permissions for controlled, time-bound access to files 554 | - ✅ Use the SAS URL in Angular to display images securely and privately 555 | - ✅ Best Practices for securing private files in Azure Storage 556 | 557 | 🚀 By the end of this video, you’ll know how to securely manage and control file access in Azure Storage using SAS tokens and .NET Core API. Protect user images and other private content with ease! 558 | 559 | - ### **Securely Fetch Azure AD B2C Data with .NET Core & Microsoft Graph API** 560 | 🔍 Want to unlock user details from Azure AD B2C? In this step-by-step video, we will: 561 | 562 | - ✅ Set up App Registration for seamless access to Microsoft Graph API 563 | - ✅ Configure API Permissions to securely retrieve Azure AD B2C user information 564 | - ✅ Implement Graph API calls within your .NET Core Web API 565 | - ✅ Fetch key user details like name, email, and roles for your app 566 | - ✅ Real-world use case: How to enrich user data in your applications 567 | 568 | 🚀 By the end of this video, you’ll have the skills to securely fetch and utilize Azure AD B2C user data via Microsoft Graph API in your .NET Core applications! 569 | 570 | - ### **Master Full-Stack Development with Angular, .NET Core & Azure | Build Real-World App** 571 | 572 | Full Video https://youtu.be/zlybQJVLYrQ 573 | 574 | 575 | 576 | ### Project Setup 577 | 578 | #### Requirements 579 | - **.NET 9 SDK** 580 | - **Visual Studio 2022 or higher** 581 | - **SQL Server or SQL Server Express** 582 | - **SQL Server Management Studio (SSMS)** 583 | - **Azure Data Studio** 584 | - **Azure DevOps account (for CI/CD)** 585 | 586 | ### Installation 587 | 588 | 1. Clone this repository: 589 | ```bash 590 | git clone https://github.com/learnsmartcoding/smartcertify-api-clean-architecture-dotnet9 591 | ``` 592 | 593 | 2. Navigate to the project folder: 594 | ```bash 595 | cd smartcertify-api-clean-architecture-dotnet9 596 | ``` 597 | 598 | 3. Open the solution in Visual Studio. 599 | 600 | 4. Restore the NuGet packages: 601 | ```bash 602 | dotnet restore 603 | ``` 604 | 605 | 5. Run the project: 606 | ```bash 607 | dotnet run 608 | ``` 609 | 610 | 6. Open the Swagger UI to test the API at: 611 | `http://localhost:5000/swagger` 612 | 613 | --- 614 | 615 | ## Contributing 616 | 617 | Feel free to fork the repository and create pull requests. Contributions are welcome! 618 | 619 | --- 620 | 621 | ## License 622 | 623 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 624 | --------------------------------------------------------------------------------