Skip to content

Commit bc67ab9

Browse files
committed
Fixed by move ticket seeding to DataSeeder, remove FK-violating HasData from migration
1 parent 6e4be73 commit bc67ab9

4 files changed

Lines changed: 100 additions & 133 deletions

File tree

src/SupportOS.Infrastructure/Migrations/20260329184157_InitialCreate.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,6 @@ protected override void Up(MigrationBuilder migrationBuilder)
144144
{ new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), 8, "Software", new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) }
145145
});
146146

147-
migrationBuilder.InsertData(
148-
table: "Tickets",
149-
columns: new[] { "Id", "AssignedAgentId", "CategoryId", "CreatedAt", "CustomerId", "Description", "FirstResponseAt", "Priority", "ResolvedAt", "SLADueAt", "Status", "Title", "UpdatedAt" },
150-
values: new object[,]
151-
{
152-
{ new Guid("cccccccc-cccc-cccc-cccc-cccccccccccc"), null, new Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), new Guid("33333333-3333-3333-3333-333333333333"), "The laptop screen flickers intermittently when running on battery power.", null, 1, null, new DateTime(2025, 1, 2, 0, 0, 0, 0, DateTimeKind.Utc), 0, "Laptop screen flickering", new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) },
153-
{ new Guid("dddddddd-dddd-dddd-dddd-dddddddddddd"), new Guid("22222222-2222-2222-2222-222222222222"), new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), new Guid("33333333-3333-3333-3333-333333333333"), "Corporate VPN disconnects automatically after approximately 60 minutes of use.", new DateTime(2025, 1, 1, 0, 30, 0, 0, DateTimeKind.Utc), 2, null, new DateTime(2025, 1, 1, 8, 0, 0, 0, DateTimeKind.Utc), 1, "VPN connection drops every hour", new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) },
154-
{ new Guid("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"), new Guid("22222222-2222-2222-2222-222222222222"), new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), new Guid("33333333-3333-3333-3333-333333333333"), "Outlook is not syncing new emails. Inbox shows emails from 2 days ago as the latest.", new DateTime(2025, 1, 1, 1, 0, 0, 0, DateTimeKind.Utc), 0, new DateTime(2025, 1, 1, 6, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 1, 3, 0, 0, 0, 0, DateTimeKind.Utc), 3, "Email client not syncing", new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) }
155-
});
156-
157147
migrationBuilder.CreateIndex(
158148
name: "IX_Comments_AuthorId",
159149
table: "Comments",

src/SupportOS.Infrastructure/Migrations/SupportOSDbContextModelSnapshot.cs

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -198,52 +198,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
198198
b.HasIndex("Status");
199199

200200
b.ToTable("Tickets");
201-
202-
b.HasData(
203-
new
204-
{
205-
Id = new Guid("cccccccc-cccc-cccc-cccc-cccccccccccc"),
206-
CategoryId = new Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
207-
CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
208-
CustomerId = new Guid("33333333-3333-3333-3333-333333333333"),
209-
Description = "The laptop screen flickers intermittently when running on battery power.",
210-
Priority = 1,
211-
SLADueAt = new DateTime(2025, 1, 2, 0, 0, 0, 0, DateTimeKind.Utc),
212-
Status = 0,
213-
Title = "Laptop screen flickering",
214-
UpdatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)
215-
},
216-
new
217-
{
218-
Id = new Guid("dddddddd-dddd-dddd-dddd-dddddddddddd"),
219-
AssignedAgentId = new Guid("22222222-2222-2222-2222-222222222222"),
220-
CategoryId = new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
221-
CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
222-
CustomerId = new Guid("33333333-3333-3333-3333-333333333333"),
223-
Description = "Corporate VPN disconnects automatically after approximately 60 minutes of use.",
224-
FirstResponseAt = new DateTime(2025, 1, 1, 0, 30, 0, 0, DateTimeKind.Utc),
225-
Priority = 2,
226-
SLADueAt = new DateTime(2025, 1, 1, 8, 0, 0, 0, DateTimeKind.Utc),
227-
Status = 1,
228-
Title = "VPN connection drops every hour",
229-
UpdatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)
230-
},
231-
new
232-
{
233-
Id = new Guid("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"),
234-
AssignedAgentId = new Guid("22222222-2222-2222-2222-222222222222"),
235-
CategoryId = new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
236-
CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
237-
CustomerId = new Guid("33333333-3333-3333-3333-333333333333"),
238-
Description = "Outlook is not syncing new emails. Inbox shows emails from 2 days ago as the latest.",
239-
FirstResponseAt = new DateTime(2025, 1, 1, 1, 0, 0, 0, DateTimeKind.Utc),
240-
Priority = 0,
241-
ResolvedAt = new DateTime(2025, 1, 1, 6, 0, 0, 0, DateTimeKind.Utc),
242-
SLADueAt = new DateTime(2025, 1, 3, 0, 0, 0, 0, DateTimeKind.Utc),
243-
Status = 3,
244-
Title = "Email client not syncing",
245-
UpdatedAt = new DateTime(2025, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)
246-
});
247201
});
248202

249203
modelBuilder.Entity("SupportOS.Domain.Entities.User", b =>

src/SupportOS.Infrastructure/Persistence/DataSeeder.cs

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
using Microsoft.Extensions.Logging;
33
using SupportOS.Domain.Entities;
44
using SupportOS.Domain.Enums;
5+
using SupportOS.Domain.Services;
56

67
namespace SupportOS.Infrastructure.Persistence;
78

89
/// <summary>
9-
/// Seeds initial users at startup. Runs only when the users table is empty,
10-
/// so it is idempotent and safe to call on every application start.
10+
/// Seeds initial data at startup. Each section is idempotent — it only inserts
11+
/// when the target table is empty, so this is safe to call on every application start.
1112
///
12-
/// Password hashes are computed at runtime (not baked into EF migrations) to
13-
/// avoid the BCrypt random-salt issue where each call to HashPassword produces a
14-
/// different value, which would trigger spurious EF migrations.
13+
/// Users are seeded here (not via HasData) because BCrypt generates a new random
14+
/// salt on every call, which would cause spurious EF migrations.
15+
/// Tickets are seeded here (not via HasData) because they FK-reference users who
16+
/// only exist after this seeder runs — i.e., after the migration completes.
1517
/// </summary>
1618
public sealed class DataSeeder
1719
{
@@ -25,6 +27,12 @@ public DataSeeder(SupportOSDbContext db, ILogger<DataSeeder> logger)
2527
}
2628

2729
public async Task SeedAsync(CancellationToken cancellationToken = default)
30+
{
31+
await SeedUsersAsync(cancellationToken);
32+
await SeedTicketsAsync(cancellationToken);
33+
}
34+
35+
private async Task SeedUsersAsync(CancellationToken cancellationToken)
2836
{
2937
if (await _db.Users.AnyAsync(cancellationToken))
3038
{
@@ -39,36 +47,102 @@ public async Task SeedAsync(CancellationToken cancellationToken = default)
3947
_db.Users.AddRange(
4048
new User
4149
{
42-
Id = new Guid("11111111-1111-1111-1111-111111111111"),
43-
Name = "Admin User",
44-
Email = "admin@supportos.io",
50+
Id = new Guid("11111111-1111-1111-1111-111111111111"),
51+
Name = "Admin User",
52+
Email = "admin@supportos.io",
4553
PasswordHash = BCrypt.Net.BCrypt.HashPassword("Admin@1234", workFactor: 12),
46-
Role = UserRole.Admin,
47-
CreatedAt = seedDate,
48-
UpdatedAt = seedDate
54+
Role = UserRole.Admin,
55+
CreatedAt = seedDate,
56+
UpdatedAt = seedDate
4957
},
5058
new User
5159
{
52-
Id = new Guid("22222222-2222-2222-2222-222222222222"),
53-
Name = "Support Agent",
54-
Email = "agent@supportos.io",
60+
Id = new Guid("22222222-2222-2222-2222-222222222222"),
61+
Name = "Support Agent",
62+
Email = "agent@supportos.io",
5563
PasswordHash = BCrypt.Net.BCrypt.HashPassword("Agent@1234", workFactor: 12),
56-
Role = UserRole.Agent,
57-
CreatedAt = seedDate,
58-
UpdatedAt = seedDate
64+
Role = UserRole.Agent,
65+
CreatedAt = seedDate,
66+
UpdatedAt = seedDate
5967
},
6068
new User
6169
{
62-
Id = new Guid("33333333-3333-3333-3333-333333333333"),
63-
Name = "Demo Customer",
64-
Email = "customer@supportos.io",
70+
Id = new Guid("33333333-3333-3333-3333-333333333333"),
71+
Name = "Demo Customer",
72+
Email = "customer@supportos.io",
6573
PasswordHash = BCrypt.Net.BCrypt.HashPassword("Customer@1234", workFactor: 12),
66-
Role = UserRole.Customer,
67-
CreatedAt = seedDate,
68-
UpdatedAt = seedDate
74+
Role = UserRole.Customer,
75+
CreatedAt = seedDate,
76+
UpdatedAt = seedDate
6977
});
7078

7179
await _db.SaveChangesAsync(cancellationToken);
7280
_logger.LogInformation("DataSeeder: seeded 3 users (admin, agent, customer).");
7381
}
82+
83+
private async Task SeedTicketsAsync(CancellationToken cancellationToken)
84+
{
85+
if (await _db.Tickets.AnyAsync(cancellationToken))
86+
{
87+
_logger.LogDebug("DataSeeder: tickets already present, skipping.");
88+
return;
89+
}
90+
91+
_logger.LogInformation("DataSeeder: seeding demo tickets.");
92+
93+
var hardwareCategoryId = new Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
94+
var softwareCategoryId = new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
95+
var agentId = new Guid("22222222-2222-2222-2222-222222222222");
96+
var customerId = new Guid("33333333-3333-3333-3333-333333333333");
97+
var seedDate = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
98+
99+
_db.Tickets.AddRange(
100+
new Ticket
101+
{
102+
Id = new Guid("cccccccc-cccc-cccc-cccc-cccccccccccc"),
103+
Title = "Laptop screen flickering",
104+
Description = "The laptop screen flickers intermittently when running on battery power.",
105+
Status = TicketStatus.Open,
106+
Priority = Priority.Medium,
107+
CategoryId = hardwareCategoryId,
108+
CustomerId = customerId,
109+
SLADueAt = SLACalculator.CalculateDueDate(Priority.Medium, seedDate),
110+
CreatedAt = seedDate,
111+
UpdatedAt = seedDate
112+
},
113+
new Ticket
114+
{
115+
Id = new Guid("dddddddd-dddd-dddd-dddd-dddddddddddd"),
116+
Title = "VPN connection drops every hour",
117+
Description = "Corporate VPN disconnects automatically after approximately 60 minutes of use.",
118+
Status = TicketStatus.InProgress,
119+
Priority = Priority.High,
120+
CategoryId = softwareCategoryId,
121+
CustomerId = customerId,
122+
AssignedAgentId = agentId,
123+
FirstResponseAt = seedDate.AddMinutes(30),
124+
SLADueAt = SLACalculator.CalculateDueDate(Priority.High, seedDate),
125+
CreatedAt = seedDate,
126+
UpdatedAt = seedDate
127+
},
128+
new Ticket
129+
{
130+
Id = new Guid("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"),
131+
Title = "Email client not syncing",
132+
Description = "Outlook is not syncing new emails. Inbox shows emails from 2 days ago as the latest.",
133+
Status = TicketStatus.Resolved,
134+
Priority = Priority.Low,
135+
CategoryId = softwareCategoryId,
136+
CustomerId = customerId,
137+
AssignedAgentId = agentId,
138+
FirstResponseAt = seedDate.AddHours(1),
139+
ResolvedAt = seedDate.AddHours(6),
140+
SLADueAt = SLACalculator.CalculateDueDate(Priority.Low, seedDate),
141+
CreatedAt = seedDate,
142+
UpdatedAt = seedDate
143+
});
144+
145+
await _db.SaveChangesAsync(cancellationToken);
146+
_logger.LogInformation("DataSeeder: seeded 3 demo tickets.");
147+
}
74148
}

src/SupportOS.Infrastructure/Persistence/SupportOSDbContext.cs

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
6060
.OnDelete(DeleteBehavior.Restrict);
6161
});
6262

63-
// Seed data with fixed GUIDs
64-
// Users are seeded by DataSeeder at startup (not here) to avoid BCrypt random-salt issues.
65-
var customerId = new Guid("33333333-3333-3333-3333-333333333333");
63+
// Category seed data — no FK dependency on Users, safe in migration.
64+
// Users and Tickets are seeded by DataSeeder at startup (after migration)
65+
// because Tickets reference User FKs that only exist post-seeding.
6666
var hardwareCategoryId = new Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
6767
var softwareCategoryId = new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
68-
var ticket1Id = new Guid("cccccccc-cccc-cccc-cccc-cccccccccccc");
69-
var ticket2Id = new Guid("dddddddd-dddd-dddd-dddd-dddddddddddd");
70-
var ticket3Id = new Guid("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee");
71-
var agentId = new Guid("22222222-2222-2222-2222-222222222222");
7268
var seedDate = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
7369

7470
modelBuilder.Entity<Category>().HasData(
@@ -88,52 +84,5 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
8884
CreatedAt = seedDate,
8985
UpdatedAt = seedDate
9086
});
91-
92-
modelBuilder.Entity<Ticket>().HasData(
93-
new Ticket
94-
{
95-
Id = ticket1Id,
96-
Title = "Laptop screen flickering",
97-
Description = "The laptop screen flickers intermittently when running on battery power.",
98-
Status = TicketStatus.Open,
99-
Priority = Priority.Medium,
100-
CategoryId = hardwareCategoryId,
101-
CustomerId = customerId,
102-
AssignedAgentId = null,
103-
SLADueAt = seedDate.AddHours(24),
104-
CreatedAt = seedDate,
105-
UpdatedAt = seedDate
106-
},
107-
new Ticket
108-
{
109-
Id = ticket2Id,
110-
Title = "VPN connection drops every hour",
111-
Description = "Corporate VPN disconnects automatically after approximately 60 minutes of use.",
112-
Status = TicketStatus.InProgress,
113-
Priority = Priority.High,
114-
CategoryId = softwareCategoryId,
115-
CustomerId = customerId,
116-
AssignedAgentId = agentId,
117-
SLADueAt = seedDate.AddHours(8),
118-
FirstResponseAt = seedDate.AddMinutes(30),
119-
CreatedAt = seedDate,
120-
UpdatedAt = seedDate
121-
},
122-
new Ticket
123-
{
124-
Id = ticket3Id,
125-
Title = "Email client not syncing",
126-
Description = "Outlook is not syncing new emails. Inbox shows emails from 2 days ago as the latest.",
127-
Status = TicketStatus.Resolved,
128-
Priority = Priority.Low,
129-
CategoryId = softwareCategoryId,
130-
CustomerId = customerId,
131-
AssignedAgentId = agentId,
132-
SLADueAt = seedDate.AddHours(48),
133-
FirstResponseAt = seedDate.AddHours(1),
134-
ResolvedAt = seedDate.AddHours(6),
135-
CreatedAt = seedDate,
136-
UpdatedAt = seedDate
137-
});
13887
}
13988
}

0 commit comments

Comments
 (0)