C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
218: Practice Exercise: Building a Simple Caching Decorator
Caching is one of those things that looks trivial on a whiteboard but becomes a nightmare in production if you aren't careful. When we use the Decorator pattern for caching, we're trying to add a layer of performance without polluting the actual business logic of our service. But it's easy to write a decorator that "looks" right but does absolutely nothing.
The "Ghost Cache" Problem
I recently saw a junior dev commit a caching decorator that passed all the unit tests but didn't actually improve performance in the staging environment. The code looked like this:
public class CachingProductService : IProductService
{
private readonly IProductService _innerService;
private readonly Dictionary<int, Product> _cache = new();
public CachingProductService(IProductService innerService)
{
_innerService = innerService;
}
public Product GetProduct(int id)
{
if (_cache.ContainsKey(id))
{
return _cache[id];
}
// Fetch from the "slow" database service
var product = _innerService.GetProduct(id);
return product;
}
}
At first glance, it's clean. It checks the dictionary, and if the item isn't there, it calls the inner service. But there's a glaring hole here: we're fetching the product from the inner service, but we're never actually putting it back into the cache. Every single call to GetProduct results in a cache miss, meaning we're paying the overhead of the dictionary lookup only to perform the expensive database call anyway. It's a "ghost cache"—it exists in the code, but it never actually stores anything.
Closing the Loop with Assignment
To fix this, we need to ensure that once we've done the hard work of fetching the data, we store it for the next caller. I also want to point out that using a standard Dictionary in a service like this is a ticking time bomb if your app is multi-threaded. In a real C# app, you'll almost always want a ConcurrentDictionary to avoid those nasty InvalidOperationException crashes when two threads try to write to the cache at once.
using System.Collections.Concurrent;
public class CachingProductService : IProductService
{
private readonly IProductService _innerService;
// Use ConcurrentDictionary for thread-safety
private readonly ConcurrentDictionary<int, Product> _cache = new();
public CachingProductService(IProductService innerService)
{
_innerService = innerService;
}
public Product GetProduct(int id)
{
// GetOrAdd is a shorthand that checks the key and
// executes the factory function if the key is missing
return _cache.GetOrAdd(id, key => _innerService.GetProduct(key));
}
}
By using GetOrAdd, we've collapsed the "check, fetch, and save" logic into a single, atomic operation. It's cleaner, thread-safe, and—most importantly—it actually caches the data. Now, the first call is slow, and every subsequent call for that ID is nearly instantaneous.
📋 Practical Task
Exercise: Implementing a Time-Expiring User Profile Cache
In a real system, you can't cache data forever because it eventually becomes stale. Your task is to build a caching decorator for a user profile system that implements a basic "expiration" mechanism.
Requirements:
- Create an
IUserProfileServiceinterface with a methodUserProfile GetProfile(int userId). - Create a
UserProfileService(the "slow" version) that simulates a delay usingThread.Sleep(1000). - Create a
CachingUserProfileDecoratorthat wraps the service. - Instead of a simple
ConcurrentDictionary<int, UserProfile>, use a dictionary that stores a tuple or a small helper class containing both theUserProfileand aDateTimetimestamp of when it was cached. - The decorator should only return the cached value if it is less than 10 seconds old. If the data is older than 10 seconds (or doesn't exist), it must fetch fresh data from the inner service and update the cache.
There are no comments for now.