Skip to content

Commit f920031

Browse files
committed
feat: introduce database seeder for sample data and rename book search query parameter from 'q' to 'search'.
1 parent f3795fe commit f920031

4 files changed

Lines changed: 241 additions & 4 deletions

File tree

src/ApiService/BookStore.ApiService/Endpoints/BookEndpoints.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ public static RouteGroupBuilder MapBookEndpoints(this RouteGroupBuilder group)
3030
static async Task<Ok<PagedListDto<BookSearchProjection>>> SearchBooks(
3131
[FromServices] IQuerySession session,
3232
[AsParameters] PagedRequest request,
33-
[FromQuery] string? q = null)
33+
[FromQuery] string? search = null)
3434
{
3535
var paging = request.Normalize();
3636

37-
if (string.IsNullOrWhiteSpace(q))
37+
if (string.IsNullOrWhiteSpace(search))
3838
{
3939
// Return all books if no search query - use Marten's native pagination
4040
var pagedList = await session.Query<BookSearchProjection>()
@@ -46,7 +46,7 @@ static async Task<Ok<PagedListDto<BookSearchProjection>>> SearchBooks(
4646

4747
// Use NGram search for fuzzy, accent-insensitive matching
4848
// This leverages the pg_trgm indexes we configured
49-
var searchQuery = q.Trim();
49+
var searchQuery = search.Trim();
5050

5151
var query = session.Query<BookSearchProjection>()
5252
.Where(b =>
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
using BookStore.ApiService.Aggregates;
2+
using BookStore.ApiService.Events;
3+
using BookStore.ApiService.Projections;
4+
using Marten;
5+
6+
namespace BookStore.ApiService.Infrastructure;
7+
8+
/// <summary>
9+
/// Seeds the database with sample data using event sourcing
10+
/// </summary>
11+
public class DatabaseSeeder(IDocumentStore store)
12+
{
13+
public async Task SeedAsync()
14+
{
15+
await using var session = store.LightweightSession();
16+
17+
// Check if already seeded
18+
var existingBooks = await session.Query<BookSearchProjection>().AnyAsync();
19+
if (existingBooks)
20+
{
21+
return; // Already seeded
22+
}
23+
24+
// Seed in dependency order: Publishers → Authors → Categories → Books
25+
var publisherIds = SeedPublishers(session);
26+
var authorIds = SeedAuthors(session);
27+
var categoryIds = SeedCategories(session);
28+
SeedBooks(session, publisherIds, authorIds, categoryIds);
29+
30+
await session.SaveChangesAsync();
31+
}
32+
33+
Dictionary<string, Guid> SeedPublishers(IDocumentSession session)
34+
{
35+
var publishers = new Dictionary<string, (Guid Id, string Name, string? Website)>
36+
{
37+
["Penguin"] = (Guid.CreateVersion7(), "Penguin Random House", "https://www.penguinrandomhouse.com"),
38+
["HarperCollins"] = (Guid.CreateVersion7(), "HarperCollins Publishers", "https://www.harpercollins.com"),
39+
["Simon"] = (Guid.CreateVersion7(), "Simon & Schuster", "https://www.simonandschuster.com"),
40+
["Hachette"] = (Guid.CreateVersion7(), "Hachette Book Group", "https://www.hachettebookgroup.com"),
41+
["Macmillan"] = (Guid.CreateVersion7(), "Macmillan Publishers", "https://us.macmillan.com")
42+
};
43+
44+
var result = new Dictionary<string, Guid>();
45+
46+
foreach (var (key, (id, name, website)) in publishers)
47+
{
48+
var @event = PublisherAggregate.Create(id, name);
49+
session.Events.StartStream<PublisherAggregate>(id, @event);
50+
result[key] = id;
51+
}
52+
53+
return result;
54+
}
55+
56+
Dictionary<string, Guid> SeedAuthors(IDocumentSession session)
57+
{
58+
var authors = new Dictionary<string, (Guid Id, string Name, string? Bio)>
59+
{
60+
["Fitzgerald"] = (Guid.CreateVersion7(), "F. Scott Fitzgerald", "American novelist and short story writer"),
61+
["Lee"] = (Guid.CreateVersion7(), "Harper Lee", "American novelist known for To Kill a Mockingbird"),
62+
["Orwell"] = (Guid.CreateVersion7(), "George Orwell", "English novelist, essayist, and critic"),
63+
["Austen"] = (Guid.CreateVersion7(), "Jane Austen", "English novelist known for her romantic fiction"),
64+
["Rowling"] = (Guid.CreateVersion7(), "J.K. Rowling", "British author, creator of Harry Potter"),
65+
["Tolkien"] = (Guid.CreateVersion7(), "J.R.R. Tolkien", "English writer and philologist, author of The Lord of the Rings"),
66+
["Hemingway"] = (Guid.CreateVersion7(), "Ernest Hemingway", "American novelist and short story writer"),
67+
["Christie"] = (Guid.CreateVersion7(), "Agatha Christie", "English writer known for detective novels")
68+
};
69+
70+
var result = new Dictionary<string, Guid>();
71+
72+
foreach (var (key, (id, name, bio)) in authors)
73+
{
74+
var @event = AuthorAggregate.Create(id, name, bio);
75+
session.Events.StartStream<AuthorAggregate>(id, @event);
76+
result[key] = id;
77+
}
78+
79+
return result;
80+
}
81+
82+
Dictionary<string, Guid> SeedCategories(IDocumentSession session)
83+
{
84+
var categories = new Dictionary<string, (Guid Id, Dictionary<string, string> Names)>
85+
{
86+
["Fiction"] = (Guid.CreateVersion7(), new() { ["en"] = "Fiction", ["pt"] = "Ficção", ["es"] = "Ficción" }),
87+
["Classic"] = (Guid.CreateVersion7(), new() { ["en"] = "Classic Literature", ["pt"] = "Literatura Clássica", ["es"] = "Literatura Clásica" }),
88+
["Fantasy"] = (Guid.CreateVersion7(), new() { ["en"] = "Fantasy", ["pt"] = "Fantasia", ["es"] = "Fantasía" }),
89+
["Mystery"] = (Guid.CreateVersion7(), new() { ["en"] = "Mystery", ["pt"] = "Mistério", ["es"] = "Misterio" }),
90+
["SciFi"] = (Guid.CreateVersion7(), new() { ["en"] = "Science Fiction", ["pt"] = "Ficção Científica", ["es"] = "Ciencia Ficción" }),
91+
["Romance"] = (Guid.CreateVersion7(), new() { ["en"] = "Romance", ["pt"] = "Romance", ["es"] = "Romance" })
92+
};
93+
94+
var result = new Dictionary<string, Guid>();
95+
96+
foreach (var (key, (id, names)) in categories)
97+
{
98+
// Use English name as primary, create translations for other languages
99+
var translations = names
100+
.Where(kvp => kvp.Key != "en")
101+
.ToDictionary(
102+
kvp => kvp.Key,
103+
kvp => new CategoryTranslation(kvp.Value, null));
104+
105+
var @event = CategoryAggregate.Create(id, names["en"], null, translations);
106+
session.Events.StartStream<CategoryAggregate>(id, @event);
107+
result[key] = id;
108+
}
109+
110+
return result;
111+
}
112+
113+
void SeedBooks(
114+
IDocumentSession session,
115+
Dictionary<string, Guid> publisherIds,
116+
Dictionary<string, Guid> authorIds,
117+
Dictionary<string, Guid> categoryIds)
118+
{
119+
var books = new[]
120+
{
121+
new
122+
{
123+
Title = "The Great Gatsby",
124+
Isbn = "978-0-7432-7356-5",
125+
Description = "A novel set in the Jazz Age that explores themes of decadence, idealism, resistance to change, social upheaval, and excess.",
126+
PublicationDate = new DateOnly(1925, 4, 10),
127+
Publisher = "Penguin",
128+
Authors = new[] { "Fitzgerald" },
129+
Categories = new[] { "Fiction", "Classic" }
130+
},
131+
new
132+
{
133+
Title = "To Kill a Mockingbird",
134+
Isbn = "978-0-06-112008-4",
135+
Description = "A gripping, heart-wrenching, and wholly remarkable tale of coming-of-age in a South poisoned by virulent prejudice.",
136+
PublicationDate = new DateOnly(1960, 7, 11),
137+
Publisher = "HarperCollins",
138+
Authors = new[] { "Lee" },
139+
Categories = new[] { "Fiction", "Classic" }
140+
},
141+
new
142+
{
143+
Title = "1984",
144+
Isbn = "978-0-452-28423-4",
145+
Description = "A dystopian social science fiction novel and cautionary tale about the dangers of totalitarianism.",
146+
PublicationDate = new DateOnly(1949, 6, 8),
147+
Publisher = "Penguin",
148+
Authors = new[] { "Orwell" },
149+
Categories = new[] { "Fiction", "SciFi", "Classic" }
150+
},
151+
new
152+
{
153+
Title = "Pride and Prejudice",
154+
Isbn = "978-0-14-143951-8",
155+
Description = "A romantic novel of manners that follows the character development of Elizabeth Bennet.",
156+
PublicationDate = new DateOnly(1813, 1, 28),
157+
Publisher = "Penguin",
158+
Authors = new[] { "Austen" },
159+
Categories = new[] { "Fiction", "Classic", "Romance" }
160+
},
161+
new
162+
{
163+
Title = "Harry Potter and the Philosopher's Stone",
164+
Isbn = "978-0-7475-3269-9",
165+
Description = "The first novel in the Harry Potter series, following a young wizard's journey at Hogwarts School of Witchcraft and Wizardry.",
166+
PublicationDate = new DateOnly(1997, 6, 26),
167+
Publisher = "Penguin",
168+
Authors = new[] { "Rowling" },
169+
Categories = new[] { "Fantasy" }
170+
},
171+
new
172+
{
173+
Title = "The Lord of the Rings",
174+
Isbn = "978-0-618-00222-1",
175+
Description = "An epic high-fantasy novel following the quest to destroy the One Ring.",
176+
PublicationDate = new DateOnly(1954, 7, 29),
177+
Publisher = "HarperCollins",
178+
Authors = new[] { "Tolkien" },
179+
Categories = new[] { "Fantasy", "Classic" }
180+
},
181+
new
182+
{
183+
Title = "The Old Man and the Sea",
184+
Isbn = "978-0-684-80122-3",
185+
Description = "The story of an aging Cuban fisherman who struggles with a giant marlin far out in the Gulf Stream.",
186+
PublicationDate = new DateOnly(1952, 9, 1),
187+
Publisher = "Simon",
188+
Authors = new[] { "Hemingway" },
189+
Categories = new[] { "Fiction", "Classic" }
190+
},
191+
new
192+
{
193+
Title = "Murder on the Orient Express",
194+
Isbn = "978-0-06-207348-8",
195+
Description = "A detective novel featuring Hercule Poirot investigating a murder on the famous train.",
196+
PublicationDate = new DateOnly(1934, 1, 1),
197+
Publisher = "HarperCollins",
198+
Authors = new[] { "Christie" },
199+
Categories = new[] { "Mystery", "Fiction", "Classic" }
200+
}
201+
};
202+
203+
foreach (var book in books)
204+
{
205+
var bookId = Guid.CreateVersion7();
206+
var @event = BookAggregate.Create(
207+
bookId,
208+
book.Title,
209+
book.Isbn,
210+
book.Description,
211+
book.PublicationDate,
212+
publisherIds[book.Publisher],
213+
book.Authors.Select(a => authorIds[a]).ToList(),
214+
book.Categories.Select(c => categoryIds[c]).ToList()
215+
);
216+
217+
session.Events.StartStream<BookAggregate>(bookId, @event);
218+
}
219+
}
220+
}

src/ApiService/BookStore.ApiService/Program.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,23 @@
196196

197197
var app = builder.Build();
198198

199+
// Seed database in development
200+
if (app.Environment.IsDevelopment())
201+
{
202+
using var scope = app.Services.CreateScope();
203+
var store = scope.ServiceProvider.GetRequiredService<IDocumentStore>();
204+
205+
// Apply schema to create PostgreSQL extensions (pg_trgm, unaccent)
206+
await store.Storage.ApplyAllConfiguredChangesToDatabaseAsync();
207+
208+
var seeder = new DatabaseSeeder(store);
209+
await seeder.SeedAsync();
210+
211+
// Give async projections time to process the seeded events
212+
// In production, projections run continuously in the background
213+
await Task.Delay(TimeSpan.FromSeconds(2));
214+
}
215+
199216
// Configure the HTTP request pipeline.
200217
app.UseExceptionHandler();
201218

src/Web/BookStore.Web/Services/IBookStoreApi.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public interface IBookStoreApi
1313
/// </summary>
1414
[Get("/api/books")]
1515
Task<PagedListDto<BookDto>> GetBooksAsync(
16-
[Query] string? q = null,
16+
[Query] string? search = null,
1717
[Query] int page = 1,
1818
[Query] int pageSize = 20,
1919
CancellationToken cancellationToken = default);

0 commit comments

Comments
 (0)