Skip to content

Commit

Permalink
Update code
Browse files Browse the repository at this point in the history
  • Loading branch information
PhucNghi176 committed Oct 23, 2024
1 parent a9c1020 commit b6b6f06
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 114 deletions.
2 changes: 1 addition & 1 deletion MBS_COMMAND.Application/MBS_COMMAND.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="FluentValidation" Version="11.10.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
<PackageReference Include="MBS_CONTRACT.SHARE" Version="1.1.11" />
<PackageReference Include="MBS_CONTRACT.SHARE" Version="1.1.12" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Slot, Guid> slotRepository,
IRepositoryBase<User, Guid> userRepository,
IRepositoryBase<Semester,Guid> semesterRepository,
IRepositoryBase<Semester, Guid> semesterRepository,
IUnitOfWork unitOfWork)
: ICommandHandler<Command.CreateSlot>
{
public async Task<Result> 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<string>();

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<Slot> slots)
private static Result CheckForOverlapsInRequestOptimized(List<Slot> 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<Slot> existingSlots)
private static List<Slot> FindOverlappingSlots(List<Slot> newSlots, List<Slot> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -16,42 +18,104 @@ public sealed class GenerateSlotForSemesterCommandHandler(
{
public async Task<Result> 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<Slot>();
// 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<User, (User Mentor, IEnumerable<Slot> 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<Slot>());

// Generate new slots for this mentor
var generatedSlots = GenerateNewSlots(mentorFirstWeekSlots, 10).ToList();
return (Mentor: mentor, Slots: generatedSlots);
})
.Where<(User Mentor, IEnumerable<Slot> Slots)>(result => result.Slots.Any())
.ToList();

// Prepare all new slots and update mentors
var allNewSlots = new List<Slot>();
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<Slot> GenerateNewSlots(IEnumerable<Slot> firstWeekSlots, int weeks)
private static List<Slot> GenerateNewSlots(IEnumerable<Slot> 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<Slot>(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;
}
}
2 changes: 1 addition & 1 deletion MBS_COMMAND.Domain/Entities/Slot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void CreateSlot(IEnumerable<Slot> slots)
Month = x.Month,
IsBook = x.IsBook,
}).ToList();
RaiseDomainEvent(new DomainEvent.SlotsCreated(Guid.NewGuid(), slot,new Guid()));
RaiseDomainEvent(new DomainEvent.SlotsCreated(Guid.NewGuid(), slot));
}

}
2 changes: 2 additions & 0 deletions MBS_COMMAND.Domain/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@ public void CreateSlot(IEnumerable<Slot> slots)
IsBook = x.IsBook,
}).ToList();
RaiseDomainEvent(new DomainEvent.MentorSlotCreated(Guid.NewGuid(), slot));
Console.BackgroundColor = ConsoleColor.DarkGreen;
Console.WriteLine("MentorSlotCreatedDomainevent");
}
}
4 changes: 2 additions & 2 deletions MBS_COMMAND.Domain/MBS_COMMAND.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MBS_CONTRACT.SHARE" Version="1.1.11" />
<ProjectReference Include="..\MBS_COMMAND.Contract\MBS_COMMAND.Contract.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\MBS_COMMAND.Contract\MBS_COMMAND.Contract.csproj" />
<PackageReference Include="MBS_CONTRACT.SHARE" Version="1.1.12" />
</ItemGroup>

</Project>
Loading

0 comments on commit b6b6f06

Please sign in to comment.