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

Put a C# Service Through the Performance Lab

FinPulse ingests a bank's daily transaction export, converts amounts to a home currency, aggregates totals per account, and emails a report to finance. The batch used to finish in minutes; now it crawls, occasionally logs a database connection string in its error output, and finance keeps asking why the report email arrives so late. You are the engineer assigned to make it fast, lean, and safe again, one measured change at a time.

Lab platform
Lab Info
Level
Advanced
Last updated
Oct 02, 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: Orientation: inside FinPulse's stalled nightly batch

    FinPulse ingests a bank's daily transaction export and turns it into a per-account report for finance. The pipeline looks simple on paper: a CSV parser reads the export line by line, a currency-conversion lookup turns each transaction's amount into the bank's home currency, a per-account aggregator totals those converted amounts, an EF Core-backed ledger persists the results, and a final report step emails finance a summary. Each stage lives in its own class under src/, which is exactly what will let you improve one stage at a time without breaking the others as you work through this lab. The batch used to finish in minutes. Lately it crawls, and finance keeps asking why the report email arrives so late even on nights when the numbers look fine. Digging into the logs turns up a second problem: when the currency-rate lookup occasionally fails, the error handler logs the raw exception message, connection string and all, straight into an output that other people can read. Slow, late, and leaky is not a state you can ship, and "it feels slower" is not something you can act on -- you need numbers before you start changing code. Here's how the fixes map to what you just saw. Step 2 builds a small benchmarking harness so you can tell, with numbers, whether the batch is actually CPU-bound or I/O-bound before you touch anything. Step 3 caches the currency lookup and moves report generation onto an asynchronous queue, which is where the late-email complaint gets resolved. Step 4 rewrites the line parser to cut allocations, closes the connection-string leak, and speeds up the aggregation loop with vectorization and parallelism. Step 5 hunts down a hidden N+1 query and a memory leak, then wires every fix into one pipeline run that finishes by proving the hot path is boxing-free. Every task in the steps ahead is graded by a test that runs your code and checks its actual output -- a returned value, a queued item, a redacted string -- so you'll always know whether a change works, not just whether it compiles. Each task also tells you exactly which file and method to edit; the surrounding class is already there, with a // TODO marking the one piece you need to fill in. That's the same discipline you'll be applying to FinPulse itself: measure, change one thing, confirm it worked.

    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 a repeatable benchmarking harness for CPU and I/O work

    Before you optimize anything, you need a way to measure it that isn't "I ran it once and it felt faster." A single stopwatch call around cold code mixes in JIT compilation, cache warm-up, and one-off GC pauses -- noise that has nothing to do with the workload's steady-state cost. The methodology BenchmarkDotNet popularized handles this by running a workload several times to warm it up, discarding those runs, then timing a further batch of runs and reporting the spread, not just an average. You're going to build a small version of that discipline yourself: a Measure method that takes any Action, warms it up, times it, and hands back a BenchmarkResult with mean, min, and max. With a working harness, the next question is what to point it at. FinPulse has two very different kinds of slow: a CPU-bound checksum-style loop that just does a lot of arithmetic, and an I/O-bound file read that spends most of its time waiting on the disk. Which one dominates changes your strategy completely -- a CPU-bound hot loop is a candidate for vectorization and parallelism, while an I/O-bound step is a candidate for caching or asynchronous offloading. Guessing which one FinPulse has gets you nowhere; measuring both and comparing the means tells you immediately.

  3. Challenge

    Step 3: Cut latency with HybridCache and a RabbitMQ-style deferred queue

    The currency-rate lookup gets called for every single transaction, but exchange rates don't change transaction-to-transaction -- they change on their own refresh schedule. That's a textbook case for caching, and HybridCache is built for exactly this: a GetOrCreateAsync call that returns a cached value on a hit, falls through to your factory on a miss, and protects you from a stampede of concurrent misses all recomputing the same key at once. You control freshness with expiration options, so a rate lookup can be cheap for repeated calls within a window and still refresh itself once that window passes. Caching fixes the repeated-read cost, but report generation is a different problem: it's a one-off, relatively slow step that currently runs inline before the batch can return. Finance doesn't need the report the instant the batch finishes counting money -- they need it to arrive reliably, a little later, without holding up everything else. That calls for the classic publish/consume split you'd get from a message broker like RabbitMQ: a producer drops a job onto a queue and moves on immediately, and a separate consumer picks jobs up and does the slow work on its own schedule. You'll model that queue with a Channel<ReportJob>, standing in for the broker. A queue that nothing reads from just grows forever, so the other half of this pattern is a consumer that keeps draining it. Consumers built on System.Threading.Channels typically loop with await foreach over ChannelReader.ReadAllAsync, which naturally waits for new work when the queue is empty and exits cleanly once the channel is completed or cancellation is requested. That's the shape you need for a report consumer that turns queued jobs into finished report lines without busy-waiting or leaking a background loop that never stops.

  4. Challenge

    Step 4: Tune file processing, cut GC pressure, and vectorize/parallelize hot loops

    Every night, FinPulse's parser splits thousands of CSV lines into fields, and the naive way to do that -- line.Split(',') followed by .ToString() calls -- allocates a new string for every field of every line. Span<char> (and ReadOnlySpan<char>) let you slice a view into the original text without copying it, and decimal.Parse has an overload that reads straight from a span, so you can isolate and parse a field without ever materializing an intermediate substring. Cutting those per-line allocations is exactly the kind of change that shows up as lower GC pressure across a whole batch, not just a faster single call. That same parsing path is where the leaked connection string came from: when a malformed line or a failed lookup throws, the exception's message sometimes contains a Password=... fragment straight from the connection string, and today that message goes to the logs unchanged. Redaction belongs right before anything gets written out, and a compiled regular expression -- here generated at build time with [GeneratedRegex] -- is a fast, precise way to find and replace a known secret pattern without touching the rest of the message. Once parsing is lean, the next cost is the arithmetic itself: summing a large array of converted amounts one double at a time. Modern CPUs can add multiple doubles in a single instruction using SIMD registers, and Vector<double> exposes that hardware capability portably -- Vector<double>.Count tells you how many values fit in one lane, and arithmetic operators on Vector<double> operate on the whole lane at once. The catch is that array lengths rarely divide evenly by the lane width, so a correct vectorized sum always finishes with a short scalar loop over whatever didn't fit in a full lane. SIMD speeds up work within a single core; the per-account aggregation step can also be split across cores entirely, since totaling one account's transactions doesn't depend on totaling another's. Parallel.For hands out ranges of an index space to multiple threads automatically, but that convenience comes with a responsibility: any shared state those threads write to has to be updated safely, or updates can be lost when two threads read-modify-write the same slot at once. ConcurrentDictionary.AddOrUpdate gives you an atomic accumulate-or-insert operation that's safe under exactly that kind of concurrent access.

  5. Challenge

    Step 5: Trace memory growth and hidden query costs, then assemble the pipeline

    EF Core's navigation properties make it easy to write code that looks fine but issues one query per row: load a list of accounts, then touch account.Transactions inside a loop, and lazy loading fires a fresh query for every single account. That pattern -- one query to get a list, then N more to get related data -- is the N+1 problem, and it's invisible in the code's shape but very visible in a query count or a slow ledger read. The fix is to tell EF Core up front what related data you need with Include, so it folds everything into one query instead of many. Not every performance problem is about speed -- some are about memory that never gets reclaimed. A static collection that a method appends to on every call, without ever clearing it, keeps every past batch's data reachable forever, which shows up as allocation and GC pressure that climbs night after night even though each individual batch is the same size. Spotting this kind of leak means asking what state outlives a single call and whether it needs to. The fix is almost always to shrink the state's scope to the call that needs it, or to explicitly reset shared state before reusing it. There's one more subtle allocation source worth knowing before you finish: boxing. Any time a value type -- an int, a double, a decimal -- gets assigned to a variable typed object or to certain non-generic interfaces, the runtime allocates a heap box to hold a copy of it, and the IL shows this explicitly as a box opcode. It's easy to trigger by accident: storing a running total in a loosely-typed accumulator, or passing a value type through an API that expects object. The final wiring of FinPulse's pipeline needs to move rate lookups, vectorized totals, and queued report jobs through one method without ever letting a running total take that detour through the heap.

  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