From b6b6f06722351bdab3d986474c0295e47fbb4a93 Mon Sep 17 00:00:00 2001 From: Eric <36512385+PhucNghi176@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:42:07 +0700 Subject: [PATCH] Update code --- .../MBS_COMMAND.Application.csproj | 2 +- .../Slots/CreateSlotCommandHandler.cs | 152 +++++++++++++----- .../GenerateSlotForSemesterCommandHandler.cs | 114 ++++++++++--- MBS_COMMAND.Domain/Entities/Slot.cs | 2 +- MBS_COMMAND.Domain/Entities/User.cs | 2 + MBS_COMMAND.Domain/MBS_COMMAND.Domain.csproj | 4 +- .../ProcessOutboxMessagesJob.cs | 67 +++----- .../APIs/Slots/SlotApi.cs | 2 +- 8 files changed, 231 insertions(+), 114 deletions(-) diff --git a/MBS_COMMAND.Application/MBS_COMMAND.Application.csproj b/MBS_COMMAND.Application/MBS_COMMAND.Application.csproj index acc56a4..f6240ea 100644 --- a/MBS_COMMAND.Application/MBS_COMMAND.Application.csproj +++ b/MBS_COMMAND.Application/MBS_COMMAND.Application.csproj @@ -16,7 +16,7 @@ - + diff --git a/MBS_COMMAND.Application/UserCases/Commands/Slots/CreateSlotCommandHandler.cs b/MBS_COMMAND.Application/UserCases/Commands/Slots/CreateSlotCommandHandler.cs index 45b247d..ab9e290 100644 --- a/MBS_COMMAND.Application/UserCases/Commands/Slots/CreateSlotCommandHandler.cs +++ b/MBS_COMMAND.Application/UserCases/Commands/Slots/CreateSlotCommandHandler.cs @@ -5,82 +5,152 @@ using MBS_COMMAND.Domain.Abstractions.Repositories; using MBS_COMMAND.Domain.Entities; using static System.DateOnly; + namespace MBS_COMMAND.Application.UserCases.Commands.Slots; public sealed class CreateSlotCommandHandler( IRepositoryBase slotRepository, IRepositoryBase userRepository, - IRepositoryBase semesterRepository, + IRepositoryBase semesterRepository, IUnitOfWork unitOfWork) : ICommandHandler { public async Task Handle(Command.CreateSlot request, CancellationToken cancellationToken) { + // Parallel fetch of mentor and current semester var mentor = await userRepository.FindByIdAsync(request.MentorId, cancellationToken); if (mentor == null) return Result.Failure(new Error("404", "User Not Found")); + var currentSemester = await semesterRepository.FindSingleAsync(x => x.IsActive, cancellationToken); - if(request.SlotModels.Any(x=>Parse(x.Date) < currentSemester!.From || Parse(x.Date) > currentSemester.From.AddDays(6))) - return Result.Failure(new Error("400", "Slot date must within the current semester and in the 1st week of the semester")); - var newSlots = request.SlotModels.Select(slotModel => new Slot + if (currentSemester == null) + return Result.Failure(new Error("404", "Active semester not found")); + + var semesterEndDate = currentSemester.From.AddDays(6); + + // Validate all dates in parallel (no DB operations here, so parallel is safe) + var invalidSlots = request.SlotModels + .AsParallel() + .Select(x => + { + var date = Parse(x.Date); + var startTime = TimeOnly.Parse(x.StartTime); + var endTime = TimeOnly.Parse(x.EndTime); + var duration = endTime - startTime; + + return new + { + Date = x.Date, + StartTime = startTime, + EndTime = endTime, + IsDateInvalid = date < currentSemester.From || date > semesterEndDate, + IsDurationInvalid = duration < TimeSpan.FromHours(1.5) + }; + }) + .Where(x => x.IsDateInvalid || x.IsDurationInvalid) + .ToList(); + + if (invalidSlots.Count != 0) { - Id = Guid.NewGuid(), - MentorId = mentor.Id, - StartTime = TimeOnly.Parse(slotModel.StartTime), - EndTime = TimeOnly.Parse(slotModel.EndTime), - Date = Parse(slotModel.Date), - Note = slotModel.Note, - IsOnline = slotModel.IsOnline, - Month = (short?)Parse(slotModel.Date).Month - }).ToList(); - - // Check for overlaps within the request - var overlapResult = CheckForOverlapsInRequest(newSlots); + var dateErrors = invalidSlots + .Where(x => x.IsDateInvalid) + .Select(x => x.Date); + + var durationErrors = invalidSlots + .Where(x => x.IsDurationInvalid) + .Select(x => $"{x.Date} {x.StartTime}-{x.EndTime}"); + + var errorMessages = new List(); + + if (dateErrors.Any()) + errorMessages.Add( + $"Slot dates {string.Join(", ", dateErrors)} must be within the first week of the current semester"); + + if (durationErrors.Any()) + errorMessages.Add( + $"Slots must be at least 1.5 hours long. Invalid slots: {string.Join(", ", durationErrors)}"); + + return Result.Failure(new Error("400", string.Join(". ", errorMessages))); + } + + // Create slots (parallel is safe here as it's just object creation) + var newSlots = request.SlotModels + .AsParallel() + .Select(slotModel => new Slot + { + Id = Guid.NewGuid(), + MentorId = mentor.Id, + StartTime = TimeOnly.Parse(slotModel.StartTime), + EndTime = TimeOnly.Parse(slotModel.EndTime), + Date = Parse(slotModel.Date), + Note = slotModel.Note, + IsOnline = slotModel.IsOnline, + Month = (short)Parse(slotModel.Date).Month + }) + .ToList(); + + // Check for overlaps within the request (no DB operations) + var overlapResult = CheckForOverlapsInRequestOptimized(newSlots); if (overlapResult.IsFailure) return overlapResult; - // Fetch existing slots for the mentor - var existingSlots = slotRepository.FindAll(x => x.MentorId == mentor.Id); + // Fetch existing slots synchronously to avoid context threading issues + var existingSlots = slotRepository + .FindAll(x => x.MentorId == mentor.Id) + .ToList(); + + // Check for overlaps (no DB operations) + var overlappingSlots = FindOverlappingSlots(newSlots, existingSlots); - // Check for overlaps with existing slots - foreach (var newSlot in newSlots.Where(newSlot => HasOverlap(newSlot, existingSlots))) + if (overlappingSlots.Count != 0) { - return Result.Failure(new Error("409", $"Slot overlaps with existing slot: {newSlot.Date} {newSlot.StartTime}-{newSlot.EndTime}")); + var firstOverlap = overlappingSlots.First(); + return Result.Failure(new Error("409", + $"Slot overlaps with existing slot: {firstOverlap.Date} {firstOverlap.StartTime}-{firstOverlap.EndTime}")); } + // Use regular AddRange since AddRangeAsync isn't available slotRepository.AddRange(newSlots); - mentor.CreateSlot(newSlots); await unitOfWork.SaveChangesAsync(cancellationToken); - + return Result.Success(); } - private static Result CheckForOverlapsInRequest(List slots) + private static Result CheckForOverlapsInRequestOptimized(List slots) { - for (var i = 0; i < slots.Count; i++) + var sortedSlots = slots + .OrderBy(s => s.Date) + .ThenBy(s => s.StartTime) + .ToList(); + + for (var i = 0; i < sortedSlots.Count - 1; i++) { - for (var j = i + 1; j < slots.Count; j++) + var current = sortedSlots[i]; + var next = sortedSlots[i + 1]; + + if (current.Date == next.Date && current.EndTime > next.StartTime) { - if (AreOverlapping(slots[i], slots[j])) - { - return Result.Failure(new Error("409", $"Overlapping slots in request: " + - $"{slots[i].Date} {slots[i].StartTime}-{slots[i].EndTime} and " + - $"{slots[j].Date} {slots[j].StartTime}-{slots[j].EndTime}")); - } + return Result.Failure(new Error("409", + $"Overlapping slots in request: {current.Date} {current.StartTime}-{current.EndTime} and " + + $"{next.Date} {next.StartTime}-{next.EndTime}")); } } + return Result.Success(); } - private static bool HasOverlap(Slot newSlot, IEnumerable existingSlots) + private static List FindOverlappingSlots(List newSlots, List existingSlots) { - return existingSlots.Any(existingSlot => AreOverlapping(newSlot, existingSlot)); - } + var existingSlotsByDate = existingSlots + .GroupBy(s => s.Date) + .ToDictionary(g => g.Key, g => g.ToList()); - private static bool AreOverlapping(Slot slot1, Slot slot2) - { - return slot1.Date == slot2.Date && - slot1.StartTime < slot2.EndTime && - slot2.StartTime < slot1.EndTime; + return newSlots + .Where(newSlot => + existingSlotsByDate.TryGetValue(newSlot.Date, out var slotsOnSameDay) && + slotsOnSameDay.Any(existingSlot => + newSlot.StartTime < existingSlot.EndTime && + existingSlot.StartTime < newSlot.EndTime)) + .ToList(); } -} +} \ No newline at end of file diff --git a/MBS_COMMAND.Application/UserCases/Commands/Slots/GenerateSlotForSemesterCommandHandler.cs b/MBS_COMMAND.Application/UserCases/Commands/Slots/GenerateSlotForSemesterCommandHandler.cs index 2baf51a..2265edd 100644 --- a/MBS_COMMAND.Application/UserCases/Commands/Slots/GenerateSlotForSemesterCommandHandler.cs +++ b/MBS_COMMAND.Application/UserCases/Commands/Slots/GenerateSlotForSemesterCommandHandler.cs @@ -1,10 +1,12 @@ using MBS_COMMAND.Contract.Abstractions.Messages; using MBS_COMMAND.Contract.Abstractions.Shared; -using MBS_COMMAND.Contract.Services.Slots; using MBS_COMMAND.Domain.Abstractions; using MBS_COMMAND.Domain.Abstractions.Repositories; using MBS_COMMAND.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using static MBS_COMMAND.Contract.Abstractions.Shared.Result; +using Command = MBS_COMMAND.Contract.Services.Slots.Command; namespace MBS_COMMAND.Application.UserCases.Commands.Slots; public sealed class GenerateSlotForSemesterCommandHandler( @@ -16,42 +18,104 @@ public sealed class GenerateSlotForSemesterCommandHandler( { public async Task Handle(Command.GenerateSlotForSemester request, CancellationToken cancellationToken) { + // Get current semester var currentSemester = await semesterRepository.FindSingleAsync(x => x.IsActive, cancellationToken); if (currentSemester == null) return Failure(new Error("404", "No active semester found")); - var mentors = userRepository.FindAll(x => x.Role == 2); - var newSlots = new List(); + // Get all mentors in parallel with first week slots + var mentors = await userRepository + .FindAll(x => x.Role == 2) + .AsTracking() + .ToListAsync(cancellationToken); - foreach (var mentor in mentors) + if (mentors.Count == 0) + return Success(); // No mentors to process + + // Get first week slots for all mentors in one query + var firstWeekEnd = currentSemester.From.AddDays(6); + var allFirstWeekSlots = await slotRepository + .FindAll(x => + mentors.Select(m => m.Id.ToString()).Contains(x.MentorId.ToString()) && + x.Date >= currentSemester.From && + x.Date <= firstWeekEnd) + .ToListAsync(cancellationToken); + + // Group slots by mentor for efficient processing + var slotsByMentor = allFirstWeekSlots + .GroupBy(x => x.MentorId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // Process all mentors in parallel for slot generation + var newSlotsList = mentors + .AsParallel() + .Select Slots)>(mentor => + { + // Get first week slots for this mentor + if (!slotsByMentor.TryGetValue(mentor.Id, out var mentorFirstWeekSlots) || + mentorFirstWeekSlots.Count == 0) + return (Mentor: mentor, Slots: Array.Empty()); + + // Generate new slots for this mentor + var generatedSlots = GenerateNewSlots(mentorFirstWeekSlots, 10).ToList(); + return (Mentor: mentor, Slots: generatedSlots); + }) + .Where<(User Mentor, IEnumerable Slots)>(result => result.Slots.Any()) + .ToList(); + + // Prepare all new slots and update mentors + var allNewSlots = new List(); + foreach (var (mentor, slots) in newSlotsList) { - var firstWeekSlots = slotRepository.FindAll(x => - x.MentorId == mentor.Id && x.Date >= currentSemester.From && - x.Date <= currentSemester.From.AddDays(6)); - var slot = GenerateNewSlots(firstWeekSlots, 10); - newSlots.AddRange(slot); - + allNewSlots.AddRange(slots); + mentor.CreateSlot(slots); + } + + // Batch insert if the repository supports it + const int batchSize = 1000; + for (var i = 0; i < allNewSlots.Count; i += batchSize) + { + var batch = allNewSlots + .Skip(i) + .Take(batchSize) + .ToList(); + + slotRepository.AddRange(batch); } - new Slot().CreateSlot(newSlots); - slotRepository.AddRange(newSlots); await unitOfWork.SaveChangesAsync(cancellationToken); return Success(); } - private static IEnumerable GenerateNewSlots(IEnumerable firstWeekSlots, int weeks) + private static List GenerateNewSlots(IEnumerable firstWeekSlots, int weeks) { - return Enumerable.Range(1, weeks) - .SelectMany(i => firstWeekSlots.Select(x => new Slot - { - Id = Guid.NewGuid(), - MentorId = x.MentorId, - StartTime = x.StartTime, - EndTime = x.EndTime, - Date = x.Date.AddDays(7 * i), - Note = x.Note, - IsOnline = x.IsOnline, - Month = (short)x.Date.AddDays(7 * i).Month - })); + // Convert to array to avoid multiple enumeration + var slotsArray = firstWeekSlots.ToArray(); + + // Pre-calculate the number of slots we'll generate + var totalSlots = slotsArray.Length * weeks; + var result = new List(totalSlots); + + // Generate slots in a more efficient way + for (var weekOffset = 1; weekOffset <= weeks; weekOffset++) + { + var daysToAdd = weekOffset * 7; + + result.AddRange(from slot in slotsArray + let newDate = slot.Date.AddDays(daysToAdd) + select new Slot + { + Id = Guid.NewGuid(), + MentorId = slot.MentorId, + StartTime = slot.StartTime, + EndTime = slot.EndTime, + Date = newDate, + Note = slot.Note, + IsOnline = slot.IsOnline, + Month = (short)newDate.Month + }); + } + + return result; } } \ No newline at end of file diff --git a/MBS_COMMAND.Domain/Entities/Slot.cs b/MBS_COMMAND.Domain/Entities/Slot.cs index e62dbc2..3687f0b 100644 --- a/MBS_COMMAND.Domain/Entities/Slot.cs +++ b/MBS_COMMAND.Domain/Entities/Slot.cs @@ -32,7 +32,7 @@ public void CreateSlot(IEnumerable slots) Month = x.Month, IsBook = x.IsBook, }).ToList(); - RaiseDomainEvent(new DomainEvent.SlotsCreated(Guid.NewGuid(), slot,new Guid())); + RaiseDomainEvent(new DomainEvent.SlotsCreated(Guid.NewGuid(), slot)); } } diff --git a/MBS_COMMAND.Domain/Entities/User.cs b/MBS_COMMAND.Domain/Entities/User.cs index 4acb59f..5f87e3d 100644 --- a/MBS_COMMAND.Domain/Entities/User.cs +++ b/MBS_COMMAND.Domain/Entities/User.cs @@ -43,5 +43,7 @@ public void CreateSlot(IEnumerable slots) IsBook = x.IsBook, }).ToList(); RaiseDomainEvent(new DomainEvent.MentorSlotCreated(Guid.NewGuid(), slot)); + Console.BackgroundColor = ConsoleColor.DarkGreen; + Console.WriteLine("MentorSlotCreatedDomainevent"); } } \ No newline at end of file diff --git a/MBS_COMMAND.Domain/MBS_COMMAND.Domain.csproj b/MBS_COMMAND.Domain/MBS_COMMAND.Domain.csproj index 620d841..05cd798 100644 --- a/MBS_COMMAND.Domain/MBS_COMMAND.Domain.csproj +++ b/MBS_COMMAND.Domain/MBS_COMMAND.Domain.csproj @@ -7,11 +7,11 @@ - + - + diff --git a/MBS_COMMAND.Infrastucture/BackgroundJobs/ProcessOutboxMessagesJob.cs b/MBS_COMMAND.Infrastucture/BackgroundJobs/ProcessOutboxMessagesJob.cs index b2e5f72..43ee150 100644 --- a/MBS_COMMAND.Infrastucture/BackgroundJobs/ProcessOutboxMessagesJob.cs +++ b/MBS_COMMAND.Infrastucture/BackgroundJobs/ProcessOutboxMessagesJob.cs @@ -10,7 +10,6 @@ using ServicesShared = MBS_CONTRACT.SHARE.Services; namespace MBS_COMMAND.Infrastucture.BackgroundJobs; - [DisallowConcurrentExecution] public class ProcessOutboxMessagesJob : IJob { @@ -40,7 +39,7 @@ public async Task Execute(IJobExecutionContext context) new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All - }); + }); if (domainEvent is null) continue; @@ -50,31 +49,34 @@ public async Task Execute(IJobExecutionContext context) switch (domainEvent.GetType().Name) { case nameof(ServicesShared.MentorSkills.DomainEvent.MentorSkillsCreated): - var mentorSkillsCreated = JsonConvert.DeserializeObject( - outboxMessage.Content, - new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.All - }); + var mentorSkillsCreated = + JsonConvert.DeserializeObject( + outboxMessage.Content, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); await _publishEndpoint.Publish(mentorSkillsCreated, context.CancellationToken); break; - + case nameof(ServicesShared.Mentors.DomainEvent.MentorCreated): - var mentorCreated = JsonConvert.DeserializeObject( - outboxMessage.Content, - new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.All - }); + var mentorCreated = + JsonConvert.DeserializeObject( + outboxMessage.Content, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); await _publishEndpoint.Publish(mentorCreated, context.CancellationToken); break; case nameof(ServicesShared.Users.DomainEvent.MentorSlotCreated): - var MentorSlotCreated = JsonConvert.DeserializeObject( - outboxMessage.Content, - new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.All - }); + var MentorSlotCreated = + JsonConvert.DeserializeObject( + outboxMessage.Content, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); await _publishEndpoint.Publish(MentorSlotCreated, context.CancellationToken); break; case nameof(ServicesShared.Slots.DomainEvent.SlotsCreated): @@ -84,30 +86,9 @@ public async Task Execute(IJobExecutionContext context) { TypeNameHandling = TypeNameHandling.All }); + await _publishEndpoint.Publish(slotsCreated, context.CancellationToken); break; - - // case nameof(DomainEvent.ProductUpdated): - // var productUpdated = JsonConvert.DeserializeObject( - // outboxMessage.Content, - // new JsonSerializerSettings - // { - // TypeNameHandling = TypeNameHandling.All - // }); - // await _publishEndpoint.Publish(productUpdated, context.CancellationToken); - // break; - - // case nameof(DomainEvent.ProductDeleted): - // var productDeleted = JsonConvert.DeserializeObject( - // outboxMessage.Content, - // new JsonSerializerSettings - // { - // TypeNameHandling = TypeNameHandling.All - // }); - // await _publishEndpoint.Publish(productDeleted, context.CancellationToken); - // break; - default: - break; } outboxMessage.ProcessedOnUtc = DateTime.UtcNow; diff --git a/MBS_COMMAND.Presentation/APIs/Slots/SlotApi.cs b/MBS_COMMAND.Presentation/APIs/Slots/SlotApi.cs index 1b1626a..dfa103a 100644 --- a/MBS_COMMAND.Presentation/APIs/Slots/SlotApi.cs +++ b/MBS_COMMAND.Presentation/APIs/Slots/SlotApi.cs @@ -8,7 +8,7 @@ public void AddRoutes(IEndpointRouteBuilder app) { var gr1 = app.NewVersionedApi("Slots").MapGroup(BaseUrl).HasApiVersion(1); gr1.MapPost(string.Empty, CreateSlot).WithSummary("mm/dd/yyyy"); - gr1.MapGet("generate", GenerateSlotForSemester); + gr1.MapPost("generate", GenerateSlotForSemester); } private static async Task CreateSlot(ISender sender,[FromBody] Command.CreateSlot command)