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)