Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Beginner
Last updated
Oct 07, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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 model

    Open src/Models.cs. Four types anchor everything you'll build:

    • Author — a small record with an Id and Name.
    • Book — a record holding an Isbn, Title, its Author, and a Genre. Two books can point at the same Author instance, which matters later when you serialize the catalog.
    • Member — a record with an Id, Name, and a mutable Loans list that starts empty.
    • Loan — a class (not a record, because DueDate and Returned change over time) linking a MemberId and Isbn.

    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-free HashSet<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 solution folder.

    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.

  2. 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 there Add, Insert, and RemoveAt are all available.

    BuildCatalog is where this catalog is born: seed it as an array, then hand back a List<Book> so every later method in this step has something it can grow. ## Searching a list

    Once the catalog is a list, finding a specific book means scanning it for a match. List<T>.Find(predicate) and Enumerable.FirstOrDefault(predicate) both walk the list and hand back the first element where the predicate is true — or null if nothing matches, since Book is a reference type. That null matters: a catalog search should let the caller check for "not found" rather than crash.

    FindByIsbn is the catalog's lookup path — every later report and update in this lab that needs one specific book goes through it. ## Replacing versus inserting

    List<T>.FindIndex(predicate) returns the position of the first match, or -1 if there isn't one. That -1 is the branch point for an upsert: when the index is real, overwrite catalog[index]; when it's -1, there's nothing to overwrite, so Add a new entry instead. One method, two outcomes, driven entirely by whether the ISBN was already in the list.

    UpsertEdition is 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.

  3. 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.

    BuildMemberDirectory creates that index once, and every method in this step reads or writes through it. ## Nesting mutable state safely

    Each Member already carries a Loans list. Recording a loan means finding that member in the directory and appending to their list — but indexing a dictionary directly with directory[id] throws a KeyNotFoundException if the ID isn't there. TryGetValue(key, out var value) avoids that: it returns false instead of throwing, so you can just skip the update when the member doesn't exist.

    RecordLoan uses that pattern to nest each new Loan under the right member's history, in the same dictionary BuildMemberDirectory built. ## Sets for duplicate-free tags

    Genre 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: UnionWith on a set silently drops anything already present, so merging several branches' tags into one set gives you every distinct tag exactly once.

    MergeGenreTags is that merge step — the single duplicate-free view of genre coverage across every branch.

  4. Challenge

    Step 4: Shape catalog reports with LINQ

    LINQ pipelines are lazy

    Where and OrderBy don't run when you write them — they build a pipeline that only executes when something enumerates it (a foreach, ToList(), and so on). That's why you can chain loans.Where(predicate).OrderBy(keySelector) and hand the result back as IEnumerable<Loan> without doing any work yet: the caller decides when, and whether, to pull results through.

    GetOverdueLoans is 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 materializing

    Select reshapes each element of a sequence into something new — here, a full Book becomes a smaller BookSummary carrying just the fields a report needs. Like Where, Select is 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. Calling ToList() at the end forces the projection to run exactly once and hands back a concrete List<BookSummary>.

    GetCatalogSummaries does both: project, then materialize. ## Grouping with GroupBy

    loans.GroupBy(l => l.MemberId) doesn't return loans — it returns groups, one IGrouping<int, Loan> per distinct member ID, each with a Key (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.

    GetLoanActivityByMember uses that grouping to turn a flat list of loans into one activity row per member.

  5. Challenge

    Step 5: Serialize, protect, and slice the library dataset

    Preserving shared references in JSON

    By default, JsonSerializer treats every object it meets as brand new — if two Books share one Author instance, a default serialize/deserialize round trip gives you back two separate Author copies with the same data. JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve } fixes that: it tags the first occurrence of a shared object with an $id and every later occurrence with an $ref to it, on both serialize and deserialize, so identity survives the round trip.

    RoundTripCatalog is that full trip — catalog out to JSON, and back in with the same shared Author instances intact. ## Read-only views that stay live

    Handing out a List<Book> directly means any caller can Add or Clear it. System.Collections.ObjectModel.ReadOnlyCollection<T> (or the IReadOnlyList<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.

    GetReadOnlyCatalog builds exactly that: a protected, always-current view for external callers. ## Spans for in-place slicing

    Span<T> gives you a view over a contiguous slice of an array without copying it. array.AsSpan(start, length) produces a Span<T> over just that range, and writing to span[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.

    BulkExtendDueDates takes a Span<DateOnly> and shifts every entry in it forward by a number of days, in place.

  6. 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.csproj
    

    The 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 quiet
    

    Or one task at a time:

    bash runTest.sh Task_2_1
    

    Each 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

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

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.

Get started with Pluralsight