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
131: Building a File-Based Data Processor
When you're building tools for real-world environments, you'll often find that the data isn't sitting in a tidy database. It's in a text file, a CSV, or a massive log dump. Today, we're going to build a processor that parses a server log file and generates a summary of how many errors occurred for each specific service. It's a classic pattern: Read → Parse → Transform → Write.
Defining our Log Structure
First, I don't want to pass around raw strings throughout the app. That's a recipe for bugs. I'll start by defining a simple record to represent a log entry. I'm using a record here because this data is immutable; once a log is written, it doesn't change.
public record LogEntry(DateTime Timestamp, string Level, string Service, string Message);
Now, I need a way to turn a line of text—something like "2023-10-01 12:00:01|ERROR|PaymentGateway|Timeout occurred"—into that record. I'll write a small helper method to handle the splitting logic.
The Naive Loading Mistake
Here is where I usually start, and where I often make a mistake in the early stages of a project. I'll write a method to read the file and parse the lines. At first glance, this looks clean:
public List<LogEntry> LoadLogs(string filePath)
{
var lines = File.ReadAllLines(filePath); // The mistake is here
var entries = new List<LogEntry>();
foreach (var line in lines)
{
var parts = line.Split('|');
entries.Add(new LogEntry(DateTime.Parse(parts[0]), parts[1], parts[2], parts[3]));
}
return entries;
}
I've done this a dozen times, and it works fine for a 10KB file. But imagine this is a production server log that's 4GB. File.ReadAllLines loads the entire file into a string array in RAM. Your app will throw an OutOfMemoryException before it even starts parsing. It's a classic "it worked on my machine" bug.
Streaming the Data for Efficiency
To fix this, I need to switch to a streaming approach. Instead of loading everything, I'll use File.ReadLines. Unlike ReadAllLines, this returns an IEnumerable<string>, meaning it reads one line at a time as the loop requests it. This keeps the memory footprint tiny regardless of file size.
public IEnumerable<LogEntry> StreamLogs(string filePath)
{
foreach (var line in File.ReadLines(filePath))
{
var parts = line.Split('|');
if (parts.Length == 4)
{
yield return new LogEntry(DateTime.Parse(parts[0]), parts[1], parts[2], parts[3]);
}
}
}
I added a quick length check too. Real logs are messy; you'll inevitably hit a blank line or a corrupted entry that would otherwise crash your Split logic.
Aggregating and Writing the Report
Now that we have a memory-efficient stream of data, we can use LINQ to do the heavy lifting. I want to filter for only "ERROR" levels and then group them by the service name.
public void ProcessErrorReport(string inputPath, string outputPath)
{
var errorCounts = StreamLogs(inputPath)
.Where(e => e.Level == "ERROR")
.GroupBy(e => e.Service)
.Select(g => new { Service = g.Key, Count = g.Count() })
.OrderByDescending(x => x.Count);
using var writer = new StreamWriter(outputPath);
writer.WriteLine("Service Error Report");
writer.WriteLine("-------------------");
foreach (var item in errorCounts)
{
writer.WriteLine($"{item.Service}: {item.Count} errors");
}
}
By combining IEnumerable and StreamWriter, the data flows from the disk, through our filter, and back to the disk without ever loading the whole dataset into memory. This is how you build tools that can handle "enterprise-scale" data without needing an enterprise-scale server.
📋 Practical Task
Exercise: Building a Time-Filtered Log Analyzer
Your task is to extend the data processor we just built. Instead of just counting all errors, you need to modify the ProcessErrorReport logic to only include errors that occurred within a specific time window.
Requirements:
- Modify the method signature to accept two additional parameters:
DateTime startDateandDateTime endDate. - Add a
.Where()clause to the LINQ chain that filters theLogEntrytimestamps to fall between these two dates (inclusive). - Update the output report header to include the date range being analyzed (e.g., "Service Error Report from 2023-01-01 to 2023-01-07").
- Ensure the program still uses the streaming
IEnumerableapproach to maintain memory efficiency.
There are no comments for now.