Practice: LINQ and Files
Student Grade Analyzer
Serialize student data to a JSON file, then deserialize it and use LINQ for multi-dimensional statistical analysis: average score, top three, pass count, and grouping by grade level.
Requirements
- Define a
Studentclass withNameandScoreproperties - Serialize the student list to JSON and save it to a temporary file
- Read from the file and deserialize
- Use LINQ to calculate: average score, top three, pass count (>=60), and grouping by grade level
Example
CSHARP
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Collections.Generic;
public class Student
{
public string Name { get; set; }
public int Score { get; set; }
}
class Program
{
static void Main()
{
var students = new List<Student>
{
new Student { Name = "Zhang San", Score = 92 },
new Student { Name = "Li Si", Score = 45 },
new Student { Name = "Wang Wu", Score = 78 },
new Student { Name = "Zhao Liu", Score = 88 },
new Student { Name = "Qian Qi", Score = 55 },
new Student { Name = "Sun Ba", Score = 95 },
new Student { Name = "Zhou Jiu", Score = 33 },
new Student { Name = "Wu Shi", Score = 71 }
};
string tempDir = Path.GetTempPath();
string filePath = Path.Combine(tempDir, "students.json");
var options = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(students, options);
File.WriteAllText(filePath, json);
Console.WriteLine("Written to: " + filePath);
string readJson = File.ReadAllText(filePath);
var loaded = JsonSerializer.Deserialize<List<Student>>(readJson);
double average = loaded.Average(s => s.Score);
Console.WriteLine($"Average score: {average:F1}");
var top3 = loaded.OrderByDescending(s => s.Score).Take(3);
Console.WriteLine("Top three:");
foreach (var s in top3)
{
Console.WriteLine($" {s.Name} - {s.Score}");
}
int passCount = loaded.Count(s => s.Score >= 60);
Console.WriteLine($"Pass count: {passCount}");
var grouped = loaded.GroupBy(s => s.Score >= 90 ? "Excellent"
: s.Score >= 60 ? "Pass" : "Fail");
Console.WriteLine("Grade groups:");
foreach (var group in grouped)
{
Console.WriteLine($" {group.Key}: {string.Join(", ", group.Select(s => s.Name))}");
}
File.Delete(filePath);
}
}
TEXT
Written to: /tmp/students.json
Average score: 69.6
Top three:
Sun Ba - 95
Zhang San - 92
Zhao Liu - 88
Pass count: 5
Grade groups:
Excellent: Zhang San, Sun Ba
Fail: Li Si, Qian Qi, Zhou Jiu
Pass: Wang Wu, Zhao Liu, Wu Shi
Simple Log Analyzer
Read a text log file, parse each line, and use LINQ to count errors/warnings, find the most frequent error, and filter by date range.
Requirements
- Log format:
[Date] [Level] Message, e.g.[2025-01-15] [ERROR] Disk full - Count entries by level
- Find the most frequently occurring ERROR message
- Filter logs by date range
Example
CSHARP
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main()
{
string tempDir = Path.GetTempPath();
string logPath = Path.Combine(tempDir, "app.log");
var lines = new List<string>
{
"[2025-01-10] [INFO] System started",
"[2025-01-10] [WARN] Memory usage 80%",
"[2025-01-11] [ERROR] Database connection failed",
"[2025-01-11] [ERROR] Disk full",
"[2025-01-12] [INFO] User login",
"[2025-01-12] [ERROR] Database connection failed",
"[2025-01-13] [WARN] CPU usage 90%",
"[2025-01-13] [ERROR] Network timeout",
"[2025-01-14] [INFO] Scheduled task completed",
"[2025-01-14] [ERROR] Database connection failed"
};
File.WriteAllLines(logPath, lines);
var logEntries = File.ReadAllLines(logPath)
.Select(line =>
{
var parts = line.Split(']');
return new
{
Date = parts[0].TrimStart('[').Trim(),
Level = parts[1].TrimStart('[').Trim(),
Message = parts[2].Trim()
};
})
.ToList();
var levelCounts = logEntries
.GroupBy(e => e.Level)
.Select(g => new { Level = g.Key, Count = g.Count() })
.OrderByDescending(x => x.Count);
Console.WriteLine("Level counts:");
foreach (var item in levelCounts)
{
Console.WriteLine($" {item.Level}: {item.Count}");
}
var topError = logEntries
.Where(e => e.Level == "ERROR")
.GroupBy(e => e.Message)
.OrderByDescending(g => g.Count())
.First();
Console.WriteLine($"Most frequent error: {topError.Key} ({topError.Count()} times)");
var filtered = logEntries
.Where(e => string.Compare(e.Date, "2025-01-11") >= 0
&& string.Compare(e.Date, "2025-01-13") <= 0)
.ToList();
Console.WriteLine("Logs from Jan 11-13:");
foreach (var entry in filtered)
{
Console.WriteLine($" [{entry.Date}] [{entry.Level}] {entry.Message}");
}
File.Delete(logPath);
}
}
TEXT
Level counts:
ERROR: 5
INFO: 3
WARN: 2
Most frequent error: Database connection failed (3 times)
Logs from Jan 11-13:
[2025-01-11] [ERROR] Database connection failed
[2025-01-11] [ERROR] Disk full
[2025-01-12] [INFO] User login
[2025-01-12] [ERROR] Database connection failed
[2025-01-13] [WARN] CPU usage 90%
[2025-01-13] [ERROR] Network timeout
Batch File Rename Tool
List files in a directory, filter by extension using LINQ, then batch-add a prefix or suffix.
Requirements
- Create sample files in a temporary directory
- Filter files by extension
- Add a prefix and suffix to file names (preserving the extension)
- Display rename logic (output old vs. new name comparison)
Example
CSHARP
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main()
{
string workDir = Path.Combine(Path.GetTempPath(), "rename_demo");
Directory.CreateDirectory(workDir);
var demoFiles = new[] { "photo1.jpg", "photo2.jpg", "doc.txt", "notes.txt", "data.csv" };
foreach (var f in demoFiles)
{
File.WriteAllText(Path.Combine(workDir, f), "demo");
}
Console.WriteLine("Original files:");
foreach (var f in Directory.GetFiles(workDir))
{
Console.WriteLine(" " + Path.GetFileName(f));
}
string targetExt = ".txt";
string prefix = "backup_";
var filesToRename = Directory.GetFiles(workDir)
.Where(f => Path.GetExtension(f).Equals(targetExt, StringComparison.OrdinalIgnoreCase))
.ToList();
Console.WriteLine($"\nFiltering extension {targetExt}:");
foreach (var f in filesToRename)
{
string dir = Path.GetDirectoryName(f);
string nameNoExt = Path.GetFileNameWithoutExtension(f);
string newName = prefix + nameNoExt + targetExt;
string newPath = Path.Combine(dir, newName);
Console.WriteLine($" {Path.GetFileName(f)} -> {newName}");
File.Move(f, newPath);
}
Console.WriteLine("\nAfter renaming:");
foreach (var f in Directory.GetFiles(workDir))
{
Console.WriteLine(" " + Path.GetFileName(f));
}
Directory.Delete(workDir, true);
}
}
TEXT
Original files:
photo1.jpg
photo2.jpg
doc.txt
notes.txt
data.csv
Filtering extension .txt:
doc.txt -> backup_doc.txt
notes.txt -> backup_notes.txt
After renaming:
photo1.jpg
photo2.jpg
backup_doc.txt
backup_notes.txt
data.csv
❓ FAQ
Q What if property names don't match during JSON deserialization?
A Use the
[JsonPropertyName] attribute to map names, or customize PropertyNamingPolicy in JsonSerializerOptions.Q Is the order of GroupBy results guaranteed?
A No, GroupBy does not guarantee group order. Use OrderBy to sort before iterating.
Q What happens if the target file already exists when using File.Move to rename?
A It throws an IOException. You should check whether the target path exists first, or use File.Replace.
Q Is ReadAllLines appropriate for reading large log files?
A No, ReadAllLines loads all content into memory at once. For large files, use File.ReadLines for lazy, line-by-line reading.
📖 Summary
- Comprehensively apply LINQ and JSON serialization for data persistence and statistical analysis
- Master log parsing, LINQ grouping and aggregation, and date filtering techniques
- Use Directory and Path classes combined with LINQ for file filtering and batch operations
- All file operation examples use Path.GetTempPath() for cross-platform compatibility
- Temporary files and directories are cleaned up after use to avoid leftovers
📝 Exercises
- Extend the student grade analyzer to count students by score range (0-59/60-79/80-89/90-100) and output a bar chart (using
*characters) - Add a feature to the log analyzer: count ERROR entries per day and output the date range with the most consecutive ERROR occurrences
- Refactor the file rename tool to accept command-line arguments for directory path, extension filter, and prefix, and add a
--dry-runmode that only shows a preview without executing



