TripRepository.cs
5,829 bytes
| 1 | using App.Domain; |
|---|---|
| 2 | using App.Domain.Contracts; |
| 3 | using Microsoft.EntityFrameworkCore; |
| 4 | |
| 5 | namespace App.DAL.EF.Repositories; |
| 6 | |
| 7 | public class TripRepository : BaseRepository<Trip>, ITripRepository |
| 8 | { |
| 9 | public TripRepository(AppDbContext dbContext) : base(dbContext) |
| 10 | { |
| 11 | } |
| 12 | |
| 13 | public override async Task<IEnumerable<Trip>> GetAllAsync() |
| 14 | { |
| 15 | return await DbContext.Trips |
| 16 | .Include(t => t.DefaultCurrency) |
| 17 | .Include(t => t.CreatedBy) |
| 18 | .ToListAsync(); |
| 19 | } |
| 20 | |
| 21 | public override async Task<Trip?> GetByIdAsync(Guid id) |
| 22 | { |
| 23 | return await DbContext.Trips |
| 24 | .Include(t => t.DefaultCurrency) |
| 25 | .Include(t => t.CreatedBy) |
| 26 | .FirstOrDefaultAsync(t => t.Id == id); |
| 27 | } |
| 28 | |
| 29 | public async Task<IEnumerable<Trip>> GetUserTripsAsync(Guid userId) |
| 30 | { |
| 31 | return await DbContext.Trips |
| 32 | .Include(t => t.DefaultCurrency) |
| 33 | .Include(t => t.Participants!) |
| 34 | .ThenInclude(p => p.User) |
| 35 | .Where(t => t.Participants!.Any(p => p.UserId == userId && p.IsActive)) |
| 36 | .ToListAsync(); |
| 37 | } |
| 38 | |
| 39 | public async Task<Trip?> GetByIdWithDetailsAsync(Guid id) |
| 40 | { |
| 41 | return await DbContext.Trips |
| 42 | .Include(t => t.DefaultCurrency) |
| 43 | .Include(t => t.Participants!) |
| 44 | .ThenInclude(p => p.User) |
| 45 | .Include(t => t.Expenses) |
| 46 | .FirstOrDefaultAsync(t => t.Id == id); |
| 47 | } |
| 48 | |
| 49 | // Override because AppDbContext sets all FKs to DeleteBehavior.Restrict, |
| 50 | // which blocks Trip deletion as soon as any child row exists (and a Trip |
| 51 | // always has at least the organizer TripParticipant). |
| 52 | // |
| 53 | // Restrict also prevents EF's in-memory cascade: calling Remove(trip) |
| 54 | // alone makes the tracker try to null the children's FKs (which are |
| 55 | // non-nullable) and throws. We must therefore explicitly mark every |
| 56 | // descendant as Deleted before removing the Trip itself. Everything |
| 57 | // happens in a single SaveChanges, so it's still one DB transaction. |
| 58 | public override async Task<Trip?> RemoveAsync(Guid id) |
| 59 | { |
| 60 | var trip = await DbContext.Trips |
| 61 | .Include(t => t.Participants) |
| 62 | .Include(t => t.Invitations) |
| 63 | .Include(t => t.BudgetCategories) |
| 64 | .Include(t => t.WishlistItems!).ThenInclude(w => w.Votes) |
| 65 | .Include(t => t.Polls!).ThenInclude(p => p.Options!).ThenInclude(o => o.Votes) |
| 66 | .Include(t => t.Expenses!).ThenInclude(e => e.Splits) |
| 67 | .Include(t => t.SettlementPlans!).ThenInclude(sp => sp.Payments) |
| 68 | .FirstOrDefaultAsync(t => t.Id == id); |
| 69 | |
| 70 | if (trip == null) return null; |
| 71 | |
| 72 | // SplitPreset has TripId FK but no navigation collection on Trip, |
| 73 | // so load it separately. |
| 74 | var splitPresets = await DbContext.SplitPresets |
| 75 | .Include(sp => sp.Members) |
| 76 | .Where(sp => sp.TripId == id) |
| 77 | .ToListAsync(); |
| 78 | |
| 79 | // Delete grandchildren first, then children, then Trip. |
| 80 | // Order matters: a parent row cannot be deleted while its dependents |
| 81 | // still reference it (Restrict). |
| 82 | |
| 83 | // Expenses -> ExpenseSplits |
| 84 | if (trip.Expenses != null) |
| 85 | { |
| 86 | foreach (var expense in trip.Expenses) |
| 87 | { |
| 88 | if (expense.Splits != null && expense.Splits.Count > 0) |
| 89 | DbContext.ExpenseSplits.RemoveRange(expense.Splits); |
| 90 | } |
| 91 | DbContext.Expenses.RemoveRange(trip.Expenses); |
| 92 | } |
| 93 | |
| 94 | // SettlementPlans -> SettlementPayments |
| 95 | if (trip.SettlementPlans != null) |
| 96 | { |
| 97 | foreach (var plan in trip.SettlementPlans) |
| 98 | { |
| 99 | if (plan.Payments != null && plan.Payments.Count > 0) |
| 100 | DbContext.SettlementPayments.RemoveRange(plan.Payments); |
| 101 | } |
| 102 | DbContext.SettlementPlans.RemoveRange(trip.SettlementPlans); |
| 103 | } |
| 104 | |
| 105 | // Polls -> PollOptions -> PollVotes |
| 106 | if (trip.Polls != null) |
| 107 | { |
| 108 | foreach (var poll in trip.Polls) |
| 109 | { |
| 110 | if (poll.Options != null) |
| 111 | { |
| 112 | foreach (var option in poll.Options) |
| 113 | { |
| 114 | if (option.Votes != null && option.Votes.Count > 0) |
| 115 | DbContext.TripPollVotes.RemoveRange(option.Votes); |
| 116 | } |
| 117 | DbContext.TripPollOptions.RemoveRange(poll.Options); |
| 118 | } |
| 119 | } |
| 120 | DbContext.TripPolls.RemoveRange(trip.Polls); |
| 121 | } |
| 122 | |
| 123 | // WishlistItems -> WishlistVotes |
| 124 | if (trip.WishlistItems != null) |
| 125 | { |
| 126 | foreach (var item in trip.WishlistItems) |
| 127 | { |
| 128 | if (item.Votes != null && item.Votes.Count > 0) |
| 129 | DbContext.TripWishlistVotes.RemoveRange(item.Votes); |
| 130 | } |
| 131 | DbContext.TripWishlistItems.RemoveRange(trip.WishlistItems); |
| 132 | } |
| 133 | |
| 134 | // SplitPresets -> SplitPresetMembers |
| 135 | foreach (var preset in splitPresets) |
| 136 | { |
| 137 | if (preset.Members != null && preset.Members.Count > 0) |
| 138 | DbContext.SplitPresetMembers.RemoveRange(preset.Members); |
| 139 | } |
| 140 | DbContext.SplitPresets.RemoveRange(splitPresets); |
| 141 | |
| 142 | // Direct children of Trip with no further descendants referenced here |
| 143 | if (trip.BudgetCategories != null && trip.BudgetCategories.Count > 0) |
| 144 | DbContext.BudgetCategories.RemoveRange(trip.BudgetCategories); |
| 145 | if (trip.Invitations != null && trip.Invitations.Count > 0) |
| 146 | DbContext.TripInvitations.RemoveRange(trip.Invitations); |
| 147 | if (trip.Participants != null && trip.Participants.Count > 0) |
| 148 | DbContext.TripParticipants.RemoveRange(trip.Participants); |
| 149 | |
| 150 | DbContext.Trips.Remove(trip); |
| 151 | return trip; |
| 152 | } |
| 153 | } |
| 154 | |