profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
AppDbContext.cs 5,595 bytes
1 using System.Text.Json;
2 using App.Domain;
3 using App.Domain.Identity;
4 using Base.Domain;
5 using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
6 using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
7 using Microsoft.EntityFrameworkCore;
8 using Microsoft.EntityFrameworkCore.ChangeTracking;
9
10 namespace App.DAL.EF;
11
12 public class AppDbContext : IdentityDbContext<AppUser, AppRole, Guid>, IDataProtectionKeyContext
13 {
14 public DbSet<Trip> Trips { get; set; } = default!;
15 public DbSet<TripParticipant> TripParticipants { get; set; } = default!;
16 public DbSet<TripInvitation> TripInvitations { get; set; } = default!;
17 public DbSet<Expense> Expenses { get; set; } = default!;
18 public DbSet<ExpenseSplit> ExpenseSplits { get; set; } = default!;
19 public DbSet<BudgetCategory> BudgetCategories { get; set; } = default!;
20 public DbSet<SplitPreset> SplitPresets { get; set; } = default!;
21 public DbSet<SplitPresetMember> SplitPresetMembers { get; set; } = default!;
22 public DbSet<SettlementPlan> SettlementPlans { get; set; } = default!;
23 public DbSet<SettlementPayment> SettlementPayments { get; set; } = default!;
24 public DbSet<Currency> Currencies { get; set; } = default!;
25 public DbSet<TripWishlistItem> TripWishlistItems { get; set; } = default!;
26 public DbSet<TripWishlistVote> TripWishlistVotes { get; set; } = default!;
27 public DbSet<TripPoll> TripPolls { get; set; } = default!;
28 public DbSet<TripPollOption> TripPollOptions { get; set; } = default!;
29 public DbSet<TripPollVote> TripPollVotes { get; set; } = default!;
30 public DbSet<AppRefreshToken> RefreshTokens { get; set; } = default!;
31 public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } = default!;
32
33 public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
34 {
35 }
36
37 public override int SaveChanges()
38 {
39 UpdateTimestamps();
40 return base.SaveChanges();
41 }
42
43 public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
44 {
45 UpdateTimestamps();
46 return base.SaveChangesAsync(cancellationToken);
47 }
48
49 private void UpdateTimestamps()
50 {
51 var entries = ChangeTracker.Entries<BaseEntity>();
52 foreach (var entry in entries)
53 {
54 if (entry.State == EntityState.Modified)
55 {
56 entry.Entity.UpdatedAt = DateTime.UtcNow;
57 }
58 }
59 }
60
61 protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
62 {
63 base.ConfigureConventions(configurationBuilder);
64
65 // force all DateTime properties to be stored as UTC
66 configurationBuilder.Properties<DateTime>()
67 .HaveConversion<UtcDateTimeConverter>();
68 }
69
70 protected override void OnModelCreating(ModelBuilder builder)
71 {
72 base.OnModelCreating(builder);
73
74 // disable cascade delete for all relationships
75 foreach (var relationship in builder.Model
76 .GetEntityTypes()
77 .SelectMany(e => e.GetForeignKeys()))
78 {
79 relationship.DeleteBehavior = DeleteBehavior.Restrict;
80 }
81
82 // unique index on invitation token
83 builder.Entity<TripInvitation>()
84 .HasIndex(i => i.Token)
85 .IsUnique();
86
87 // composite unique on TripParticipant (TripId, UserId)
88 builder.Entity<TripParticipant>()
89 .HasIndex(tp => new { tp.TripId, tp.UserId })
90 .IsUnique();
91
92 // composite unique on TripWishlistVote (WishlistItemId, UserId)
93 builder.Entity<TripWishlistVote>()
94 .HasIndex(v => new { v.WishlistItemId, v.UserId })
95 .IsUnique();
96
97 // composite unique on TripPollVote (PollOptionId, UserId)
98 builder.Entity<TripPollVote>()
99 .HasIndex(v => new { v.PollOptionId, v.UserId })
100 .IsUnique();
101
102 // LangStr JSON value converter for Currency.Name
103 builder.Entity<Currency>()
104 .Property(c => c.Name)
105 .HasConversion(
106 v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
107 v => JsonSerializer.Deserialize<LangStr>(v, (JsonSerializerOptions?)null) ?? new LangStr())
108 .HasMaxLength(1024)
109 .Metadata.SetValueComparer(new ValueComparer<LangStr>(
110 (a, b) => JsonSerializer.Serialize(a, (JsonSerializerOptions?)null) == JsonSerializer.Serialize(b, (JsonSerializerOptions?)null),
111 v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null).GetHashCode(),
112 v => JsonSerializer.Deserialize<LangStr>(JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), (JsonSerializerOptions?)null)!));
113
114 // LangStr JSON value converter for BudgetCategory.Name
115 builder.Entity<BudgetCategory>()
116 .Property(c => c.Name)
117 .HasConversion(
118 v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
119 v => JsonSerializer.Deserialize<LangStr>(v, (JsonSerializerOptions?)null) ?? new LangStr())
120 .HasMaxLength(1024)
121 .Metadata.SetValueComparer(new ValueComparer<LangStr>(
122 (a, b) => JsonSerializer.Serialize(a, (JsonSerializerOptions?)null) == JsonSerializer.Serialize(b, (JsonSerializerOptions?)null),
123 v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null).GetHashCode(),
124 v => JsonSerializer.Deserialize<LangStr>(JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), (JsonSerializerOptions?)null)!));
125
126 }
127 }
128