- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Building C# Library Collections
Cedar Hollow Library is retiring its spreadsheet-based tracking in favor of a proper in-memory data layer that will eventually back a lending kiosk. You are the engineer building that layer: a catalog of books, a directory of members with nested loan histories, genre tags that must stay duplicate-free across branches, reporting queries for overdue and grouped loans, a nightly JSON export/import job that must preserve shared author and member identities, and a public read-only view of the catalog protected from accidental mutation, with a fast in-place span-based tool for bulk due-date updates.
Lab Info
Table of Contents
-
Challenge
Step 1: Tour the Cedar Hollow Library catalog system
Welcome to Cedar Hollow Library
Cedar Hollow Library is retiring its spreadsheet-based tracking in favor of a real in-memory data layer that will eventually back a lending kiosk. You've been brought in to build that layer: a catalog of books, a directory of members with nested loan histories, genre tags that must stay duplicate-free across branches, reporting queries for overdue and grouped loans, a nightly JSON export/import job that must preserve shared author and member identities, and a public read-only view of the catalog with a fast span-based tool for bulk due-date updates.
Every collection type you already know — arrays,
List<T>,Dictionary<TKey,TValue>— has a job here, and part of this lab is learning which one fits which job. ## Touring the starter modelOpen
src/Models.cs. Four types anchor everything you'll build:Author— a small record with anIdandName.Book— a record holding anIsbn,Title, itsAuthor, and aGenre. Two books can point at the sameAuthorinstance, which matters later when you serialize the catalog.Member— a record with anId,Name, and a mutableLoanslist that starts empty.Loan— a class (not a record, becauseDueDateandReturnedchange over time) linking aMemberIdandIsbn.
Nothing in this file has a TODO — it's the shared vocabulary every later step builds on. ## Where you're headed
- Step 2 — seed the book catalog as an array, promote it to a
List<Book>, and add search, replace, and insert. - Step 3 — index members in a
Dictionary<int, Member>, nest each member's loan history underneath it, and merge genre tags into a duplicate-freeHashSet<string>. - Step 4 — write LINQ pipelines that filter and order overdue loans, project and materialize summary DTOs, and group loans by member.
- Step 5 — round-trip the catalog through JSON without losing shared references, wrap it in a read-only view, and use
Span<T>to bulk-update due dates in place.
Each step edits a different file under
src/, so you'll always know exactly where the next change belongs.If you get stuck, you can refer to the provided solution code for each task, available in the
solutionfolder.info> This lab experience was developed by the Pluralsight team using an internally developed AI tool. All sections were verified by human experts for accuracy prior to publications. However, content may still contain errors or inaccuracies, and we recommend independent verification.
To report a problem or provide feedback, click here. Feedback may be used to improve accuracy in accordance with our Privacy Policy. -
Challenge
Step 2: Build the book catalog with arrays and lists
Arrays first, then a list
An array is the right shape for data you know the size of up front — a fixed seed set of starter books.
Book[] seed = [ new Book(...), new Book(...) ];gives you that fixed block. But the catalog needs to grow and shrink as titles are added or withdrawn, and arrays can't resize.List<T>can:seed.ToList()copies an array into a resizable list, and from thereAdd,Insert, andRemoveAtare all available.BuildCatalogis where this catalog is born: seed it as an array, then hand back aList<Book>so every later method in this step has something it can grow. ## Searching a listOnce the catalog is a list, finding a specific book means scanning it for a match.
List<T>.Find(predicate)andEnumerable.FirstOrDefault(predicate)both walk the list and hand back the first element where the predicate is true — ornullif nothing matches, sinceBookis a reference type. Thatnullmatters: a catalog search should let the caller check for "not found" rather than crash.FindByIsbnis the catalog's lookup path — every later report and update in this lab that needs one specific book goes through it. ## Replacing versus insertingList<T>.FindIndex(predicate)returns the position of the first match, or-1if there isn't one. That-1is the branch point for an upsert: when the index is real, overwritecatalog[index]; when it's-1, there's nothing to overwrite, soAdda new entry instead. One method, two outcomes, driven entirely by whether the ISBN was already in the list.UpsertEditionis that method — the catalog's single entry point for "this title has a new edition," whether or not the library had a previous one on the shelf. -
Challenge
Step 3: Organize members and loans with dictionaries and sets
Dictionaries for O(1) lookup
Scanning a
List<Member>for one member by ID gets slower as the directory grows.Dictionary<int, Member>trades that scan for a hash lookup:members.ToDictionary(m => m.Id)builds the whole index in one call, keyed by whatever selector you give it. From then on,directory[42]or a safe lookup finds that member in constant time, no matter how many members Cedar Hollow adds.BuildMemberDirectorycreates that index once, and every method in this step reads or writes through it. ## Nesting mutable state safelyEach
Memberalready carries aLoanslist. Recording a loan means finding that member in the directory and appending to their list — but indexing a dictionary directly withdirectory[id]throws aKeyNotFoundExceptionif the ID isn't there.TryGetValue(key, out var value)avoids that: it returnsfalseinstead of throwing, so you can just skip the update when the member doesn't exist.RecordLoanuses that pattern to nest each newLoanunder the right member's history, in the same dictionaryBuildMemberDirectorybuilt. ## Sets for duplicate-free tagsGenre tags arrive from several branches, and the same tag — "Mystery," say — can show up in more than one branch's list. A
List<string>would just accumulate duplicates as you concatenate them.HashSet<string>won't:UnionWithon a set silently drops anything already present, so merging several branches' tags into one set gives you every distinct tag exactly once.MergeGenreTagsis that merge step — the single duplicate-free view of genre coverage across every branch. -
Challenge
Step 4: Shape catalog reports with LINQ
LINQ pipelines are lazy
WhereandOrderBydon't run when you write them — they build a pipeline that only executes when something enumerates it (aforeach,ToList(), and so on). That's why you can chainloans.Where(predicate).OrderBy(keySelector)and hand the result back asIEnumerable<Loan>without doing any work yet: the caller decides when, and whether, to pull results through.GetOverdueLoansis exactly that kind of pipeline — a filter for "still out and past due," ordered so the most overdue loan comes first. ## Projecting into DTOs, then materializingSelectreshapes each element of a sequence into something new — here, a fullBookbecomes a smallerBookSummarycarrying just the fields a report needs. LikeWhere,Selectis lazy, which is fine for a one-shot pipeline but wasteful for a report that gets read more than once: every enumeration would re-run the projection. CallingToList()at the end forces the projection to run exactly once and hands back a concreteList<BookSummary>.GetCatalogSummariesdoes both: project, then materialize. ## Grouping with GroupByloans.GroupBy(l => l.MemberId)doesn't return loans — it returns groups, oneIGrouping<int, Loan>per distinct member ID, each with aKey(the member ID) and the loans in that group. From there,Select(g => new SomeDto(g.Key, g.Count()))turns each group into a single summary row.GetLoanActivityByMemberuses that grouping to turn a flat list of loans into one activity row per member. -
Challenge
Step 5: Serialize, protect, and slice the library dataset
Preserving shared references in JSON
By default,
JsonSerializertreats every object it meets as brand new — if twoBooks share oneAuthorinstance, a default serialize/deserialize round trip gives you back two separateAuthorcopies with the same data.JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve }fixes that: it tags the first occurrence of a shared object with an$idand every later occurrence with an$refto it, on both serialize and deserialize, so identity survives the round trip.RoundTripCatalogis that full trip — catalog out to JSON, and back in with the same sharedAuthorinstances intact. ## Read-only views that stay liveHanding out a
List<Book>directly means any caller canAddorClearit.System.Collections.ObjectModel.ReadOnlyCollection<T>(or theIReadOnlyList<T>interface it implements) blocks mutation through the wrapper itself — but critically, it wraps the same backing list rather than copying it, so changes made to the original list afterward still show up when you read through the wrapper.GetReadOnlyCatalogbuilds exactly that: a protected, always-current view for external callers. ## Spans for in-place slicingSpan<T>gives you a view over a contiguous slice of an array without copying it.array.AsSpan(start, length)produces aSpan<T>over just that range, and writing tospan[i]writes straight into the original array at the corresponding position — no allocation, no separate collection to keep in sync. That makes spans a good fit for bulk in-place edits over part of a larger array, like extending due dates for one branch's loans without touching every other branch's.BulkExtendDueDatestakes aSpan<DateOnly>and shifts every entry in it forward by a number of days, in place. -
Challenge
Step 6: Run the application
Every task is done, and each one was graded the moment you completed it. What you have not done yet is watch the whole application run at once -- which is the only place the pieces you wrote separately become a single working program.
Start the application
Open the Terminal tab and run:
dotnet run --project src/Lab.csprojThe program runs in the terminal and prints its output there. Every behaviour it shows is code you wrote in the previous steps, so read the output against what each task asked for -- that is the whole lab, working as one program. ### Run the checks yourself
The lab ran each task's test for you as you went, but they are ordinary tests and you can run them whenever you like. The whole suite:
dotnet test tests/Lab.Tests.csproj --nologo --verbosity quietOr one task at a time:
bash runTest.sh Task_2_1Each failure names the task it belongs to, so the output tells you which step to go back to rather than which line to stare at. Two things worth trying before you finish: break something on purpose and watch the matching check fail, then put it back. Knowing what a failure looks like is worth as much as knowing what a pass looks like, and it costs you nothing here.
When you are done, everything you wrote is still in the editor tabs -- the lab is yours to keep reading.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.