Real examples, cleaner syntax, and lessons learned from everyday coding mistakes.
Section 1: Syntax Cleanups & Language Tricks (1–20)
Every developer starts with working code. Productive developers write readable code — the kind that explains itself. These twenty language patterns cut the noise and make your intent shine.
1. Primary Constructors (C# 12+)
Old-style constructors add lines for something the compiler already understands. Primary constructors declare parameters directly in the class header and assign them automatically.
Bad Code
public class User {
public string Name { get; }
public int Age { get; }
public User(string name, int age) {
Name = name;
Age = age;
}
}
Good Code
public class User(string name, int age) {
public string Name { get; } = name;
public int Age { get; } = age;
}
Benefit: Removes boilerplate constructors and keeps intent visible.
Lesson: Brevity builds clarity. Less code means fewer bugs to maintain.
2. Static Lambdas
Regular lambdas can capture variables from outer scopes, creating hidden memory allocations. Declaring them static blocks that capture and improves performance.
Bad Code
Func<int,int> square = x => x * x;
Good Code
Func<int,int> square = static x => x * x;
Benefit: Eliminates unintended closures and reduces allocations.
Lesson: Add static whenever a lambda doesn’t touch outer scope.
3. Return Tuples Instead of Extra Classes
Creating small “result” classes clutters projects with one-off types. Tuples return multiple values directly and stay light.
Bad Code
public class Result { public int Sum; public int Count; }
Good Code
public (int Sum,int Count) GetStats()=> (sum,count);
Benefit: Avoids unnecessary class definitions.
Lesson: Classes are heavy for throwaway results.
4. Lazy Initialization
Expensive services shouldn’t load before they’re needed. Lazy defers creation until the first call, improving startup time.
Bad Code
private HeavyService _svc = new HeavyService();
Good Code
private readonly Lazy<HeavyService> _svc = new(() => new HeavyService());
Benefit: Defers costly creation until needed.
Lesson: Start slow things late.
5. Auto-Mapper Instead of Manual Mapping
Manual property assignment duplicates effort and risks typos. A mapper library copies values based on configuration and naming.
Bad Code
dto.Name = user.Name;
dto.Email = user.Email;
Good Code
var dto = _mapper.Map<UserDto>(user);
Benefit: Reduces repetitive mapping code.
Lesson: Let libraries handle boring work.
6. Expression-Bodied Methods
Methods that return a single value don’t need full braces. Expression-bodied members make intent obvious.
Bad Code
public string GetName() { return name; }
Good Code
public string GetName() => name;
Benefit: Compact syntax for simple logic.
Lesson: One-liners deserve one line.
7. Expression-Bodied Properties
Nested getters clutter simple expressions. An arrow property shows direct relationships between fields.
Bad Code
public string FullName {
get { return First + " " + Last; }
}
Good Code
public string FullName => $"{First} {Last}";
Benefit: Cleaner syntax and instant readability.
Lesson: Consistency is documentation.
8. Modern Null Check
Classic != null reads fine but can collide with pattern matching syntax. The new is not null feels natural and avoids confusion.
Bad Code
if (user != null) { … }
Good Code
if (user is not null) { … }
Benefit: Natural language style improves clarity.
Lesson: Express logic like conversation.
9. Object Initializers
Setting each property after creation doubles lines. Initializers set them in place, showing complete object state.
Bad Code
var u = new User();
u.Name = "Yogesh";
u.Age = 30;
Good Code
var u = new User { Name = "Yogesh", Age = 30 };
Benefit: Builds and populates in one step.
Lesson: Group intent; separate steps slow you down.
10. Combine ?. and ?? Operators
Nested conditional checks for null waste space. The combination of conditional access and coalescing expresses it in one clean line.
Bad Code
string name = user != null ? user.Name : "Guest";
Good Code
string name = user?.Name ?? "Guest";
Benefit: Handles nulls gracefully in one expression.
Lesson: Let the language protect you.
11. Null-Safe Access
Chained null checks fill logs with defensive code. The ?. operator safely walks properties without extra ifs.
Bad Code
if (user != null && user.Name != null)
Console.WriteLine(user.Name);
Good Code
Console.WriteLine(user?.Name);
Benefit: Shorter and safer property access.
Lesson: Guard once, not everywhere.
12. Simplify Boolean Returns
Returning literal true/false after a condition adds noise. A direct return states it plainly.
Bad Code
if (status == "Active") return true;
else return false;
Good Code
return status == "Active";
Benefit: Eliminates redundant words.
Lesson: The shortest path is often the clearest.
13. Switch Expressions
Nested if-else blocks become unreadable fast. Switch expressions replace them with concise pattern logic.
Bad Code
string role;
if (id==1) role="Admin";
else if(id==2) role="User";
else role="Guest";
Good Code
string role = id switch {
1 => "Admin",
2 => "User",
_ => "Guest"
};
Benefit: Easier to extend and reason about.
Lesson: Replace chains with patterns.
14. Use var for Clarity
Repeating type names in declarations distracts from meaning. var lets the initializer show the type.
Bad Code
List<string> names = new List<string>();
Good Code
var names = new List<string>();
Benefit: Removes redundancy, keeps focus on purpose.
Lesson: Brevity done right improves readability.
15. LINQ Select Over Manual Loops
Explicit loops for transformations repeat boilerplate. Select communicates intent: transform this sequence.
Bad Code
var upper = new List<string>();
foreach (var n in names)
upper.Add(n.ToUpper());
Good Code
var upper = names.Select(n => n.ToUpper()).ToList();
Benefit: Declarative, concise, and composable.
Lesson: Write what you mean, not how to do it.
16. Any() Instead of Count() > 0
Checking Count() > 0 forces enumeration; Any() stops on the first match.
Bad Code
if (users.Count() > 0) …
Good Code
if (users.Any()) …
Benefit: Faster and clearer intent.
Lesson: Ask “is there?” not “how many?” when counting isn’t needed.
17. Distinct() for Uniqueness
Manual deduplication wastes CPU and logic. Distinct() provides the same result in one call.
Bad Code
var unique = new List<string>();
foreach (var i in items)
if(!unique.Contains(i)) unique.Add(i);
Good Code
var unique = items.Distinct().ToList();
Benefit: Built-in algorithm beats hand-written loops.
Lesson: The framework already solved this problem.
18. Pattern Matching for Casting
Checking type and casting separately repeats work. Pattern matching merges both safely.
Bad Code
if (obj is MyClass) {
var m = (MyClass)obj;
m.Do();
}
Good Code
if (obj is MyClass m)
m.Do();
Benefit: One check, one cast, cleaner scope.
Lesson: Fewer lines, fewer mistakes.
19. Await Properly Instead of Blocking
Calling .Result blocks threads and risks deadlocks. await keeps execution asynchronous.
Bad Code
var r = GetDataAsync().Result;
Good Code
var r = await GetDataAsync();
Benefit: Prevents thread starvation.
Lesson: In async code, blocking defeats the point.
20. Records for DTOs
Mutable DTO classes invite accidental changes. Records store value-based, immutable data safely.
Bad Code
public class UserDto {
public string Name { get; set; }
public int Age { get; set; }
}
Good Code
public record UserDto(string Name,int Age);
Benefit: Immutable and concise; perfect for data transfer.
Lesson: Use classes for behavior, records for data.
Section 2: Async, LINQ & Performance Patterns (21–35)
Performance issues rarely come from missing features — they come from small inefficiencies hiding in plain sight. These next fifteen habits will make your async calls smoother, loops faster, and code more predictable.
21. nameof() for Arguments and Logs
Hardcoding variable names leads to silent bugs when you rename something later. The nameof() operator ensures your logs and exceptions stay correct through refactors.
Bad Code
throw new ArgumentNullException("user");
Good Code
throw new ArgumentNullException(nameof(user));
Benefit: Automatically updates during refactor, no string errors.
Lesson: Let the compiler keep your argument names consistent.
22. Guard Clauses to Avoid Nested Logic
Nested if conditions bury the main idea under layers of checks. Guard clauses stop early and keep happy paths visible.
Bad Code
if(user!=null){
if(user.IsActive){
Save(user);
}
}
Good Code
if (user is null) return;
if (!user.IsActive) return;
Save(user);
Benefit: Improves readability and reduces indentation.
Lesson: Exit early, focus on the main path.
23. File-Scoped Namespaces
Extra braces from block-style namespaces make large files cluttered. File-scoped syntax keeps headers clean and saves space.
Bad Code
namespace Project.Services
{
public class Mailer { }
}
Good Code
namespace Project.Services;
public class Mailer { }
Benefit: Reduces structural noise.
Lesson: Simple files are easier to scan in a review.
24. Global Usings
When every file imports the same namespaces, repetition wastes time. Global using files centralize imports across the project.
Bad Code
using System;
using System.Collections.Generic;
Good Code
// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
Benefit: Cleaner headers and consistent imports.
Lesson: Keep repetitive declarations in one place.
25. Top-Level Statements
Small console apps or scripts shouldn’t need a Program class. Top-level statements make tiny programs instant to write.
Bad Code
class Program {
static void Main() {
Console.WriteLine("Hello");
}
}
Good Code
Console.WriteLine("Hello");
Benefit: Reduces ceremony for small utilities.
Lesson: Simplicity is a productivity multiplier.
26. Deconstruct Tuples for Readable Returns
Accessing tuple items by index hides meaning. Deconstruction names each value clearly at use.
Bad Code
var stats = GetStats();
int sum = stats.Item1;
int count = stats.Item2;
Good Code
var (sum, count) = GetStats();
Benefit: Clarifies intent and avoids magic numbers.
Lesson: Name your data where you use it.
27. Collection Expressions (C# 12+)
List creation syntax used to be wordy. Collection expressions cut it down to the essentials.
Bad Code
var numbers = new List<int> {1,2,3,4};
Good Code
var numbers = [1,2,3,4];
Benefit: Cleaner literal-style syntax for lists.
Lesson: Express what, not how.
28. Range and Index Operators
Manually slicing arrays with math makes code fragile. The ^ and .. operators do it precisely.
Bad Code
var lastThree = array.Skip(array.Length - 3).ToArray();
Good Code
var lastThree = array[^3..];
Benefit: Simpler slicing without helper methods.
Lesson: Let syntax reflect intention, not arithmetic.
29. Init-Only Setters for Safe Immutability
When you expose full set access, objects can mutate anywhere. init locks down properties after construction.
Bad Code
public class User {
public string Name { get; set; }
}
Good Code
public class User {
public string Name { get; init; }
}
Benefit: Reduces side effects and threading risks.
Lesson: Build, then freeze your data.
30. Target-Typed new()
Writing types twice makes declarations verbose. Let the compiler infer them from the left-hand side.
Bad Code
List<string> users = new List<string>();
Good Code
List<string> users = new();
Benefit: Keeps focus on the variable name.
Lesson: Trust inference when it’s unambiguous.
31. Coalescing Assignment (??=)
Conditional assignments spread across lines. The ??= operator assigns defaults compactly when null.
Bad Code
if (user.Name == null)
user.Name = "Guest";
Good Code
user.Name ??= "Guest";
Benefit: Fewer lines, same safety.
Lesson: Write defensive code in one move.
32. Using Declarations for Cleanup
Nested using statements make indentation explode. Inline declarations manage disposal automatically.
Bad Code
using(var conn = new SqlConnection(cs))
{
conn.Open();
}
Good Code
using var conn = new SqlConnection(cs);
conn.Open();
Benefit: Simplifies structure while retaining safety.
Lesson: Disposal doesn’t need drama.
33. Asynchronous Streams for Live Data
Fetching all data before looping delays results. Asynchronous streams let you process items as they arrive.
Bad Code
foreach (var i in await GetAsyncList()) Console.WriteLine(i);
Good Code
await foreach (var i in GetAsyncList())
Console.WriteLine(i);
Benefit: Streams large sets efficiently without blocking.
Lesson: Process data in motion, not at rest.
34. ConfigureAwait(false) in Libraries
Background tasks don’t need UI synchronization. Adding ConfigureAwait(false) prevents deadlocks and boosts throughput.
Bad Code
await Task.Delay(100);
Good Code
await Task.Delay(100).ConfigureAwait(false);
Benefit: Keeps async code free from context capture.
Lesson: Use it in libraries; skip it in UI.
35. Parallel.ForEachAsync (.NET 6+)
Sequential loops waste CPU when each iteration is I/O-bound. Parallel.ForEachAsync distributes the work efficiently.
Bad Code
foreach (var id in ids)
await Process(id);
Good Code
await Parallel.ForEachAsync(ids, async (id, _) => await Process(id));
Benefit: Maximizes concurrency without threads.
Lesson: Modern .NET schedules better than you can.
Section 3: Architecture, APIs & Project Practices (36–50)
Good syntax is half the battle; the other half is structure. These final fifteen habits help your .NET projects stay organized, scalable, and easy to debug.
36. Records with Inheritance
Traditional class inheritance can lead to mutable hierarchies that behave unpredictably. Records preserve immutability even across inheritance.
Bad Code
public class Shape { }
public class Circle : Shape { }
Good Code
public record Shape;
public record Circle(double Radius) : Shape;
Benefit: Safe data inheritance with built-in value equality.
Lesson: Extend models without breaking immutability.
37. Minimal APIs for Microservices
For small services, controller scaffolding adds weight. Minimal APIs let you define endpoints inline with minimal setup.
Bad Code
app.MapControllers();
Good Code
app.MapGet("/ping", () => "pong");
Benefit: Faster to create and deploy lightweight APIs.
Lesson: Keep small things small.
38. Route Groups for Organization
Scattering endpoints makes routing hard to manage. Route groups bundle related routes under one prefix.
Bad Code
app.MapGet("/users", GetAll);
app.MapPost("/users", Add);
Good Code
var users = app.MapGroup("/users");
users.MapGet("/", GetAll);
users.MapPost("/", Add);
Benefit: Keeps endpoints organized by feature.
Lesson: Group your routes like your folders.
39. Dependency Injection (DI) Basics
Instantiating services manually ties your code to concrete types. DI abstracts creation and simplifies testing.
Bad Code
var service = new MailService();
Good Code
builder.Services.AddScoped<IMail, MailService>();
Benefit: Promotes reusability and easier mocking in tests.
Lesson: New() is quick; injected is sustainable.
40. Structured Logging
String-concatenated logs are hard to search and analyze. Structured logging embeds variables as named fields.
Bad Code
_logger.LogInformation("User logged in: " + id);
Good Code
_logger.LogInformation("User {UserId} logged in", id);
Benefit: Produces searchable, indexed logs.
Lesson: Treat logs as data, not decoration.
41. Exception Filters for Centralized Handling
Catching exceptions everywhere scatters responsibility. Filters handle cross-cutting errors once, globally.
Bad Code
try { Save(); }
catch(Exception ex){ Log(ex); }
Good Code
public class ErrorFilter : IExceptionFilter {
public void OnException(ExceptionContext ctx){
// global handler
}
}
Benefit: Consistent error handling and cleaner controllers.
Lesson: Centralize repeating behavior.
42. Middleware for Shared Logic
Placing auth or logging code inside controllers repeats work. Middleware runs once per request and keeps concerns separate.
Bad Code
// Logging inside every controller
Good Code
app.Use(async (ctx, next) =>
{
Console.WriteLine(ctx.Request.Path);
await next();
});
Benefit: Reduces duplication and enforces cross-cutting rules.
Lesson: Move plumbing to the pipeline.
43. Health Checks for Monitoring
Apps without status endpoints force manual checks. Built-in health checks expose real-time service health.
Bad Code
// No standard way to verify running status
Good Code
app.MapHealthChecks("/health");
Benefit: Simplifies observability and uptime alerts.
Lesson: Good software tells you when it’s sick.
44. Configuration Binding to Typed Models
Reading config values by string keys is brittle. Typed binding maps configuration sections directly to objects.
Bad Code
var key = config["Jwt:Key"];
Good Code
builder.Configuration.GetSection("Jwt").Bind(jwtSettings);
Benefit: Strongly typed, compiler-safe configuration.
Lesson: Replace string keys with properties.
45. Options Pattern for Maintainable Settings
Passing IConfiguration around couples every class to appsettings. Options pattern injects configuration safely.
Bad Code
var opts = new MyOpts();
config.Bind("MyOpts", opts);
Good Code
services.Configure<MyOpts>(config.GetSection("MyOpts"));
Benefit: Centralized configuration with DI support.
Lesson: Pass options, not configuration roots.
46. Environment-Based AppSettings
One configuration file for all environments invites mistakes. Environment-specific JSONs keep each build safe.
Bad Code
// single appsettings.json for all
Good Code
appsettings.Development.json
appsettings.Production.json
Benefit: Clear separation between environments.
Lesson: Local and prod should never share secrets.
47. HttpClient Factory
Creating HttpClient manually leaks sockets over time. The factory manages lifetimes and pooling for you.
Bad Code
var client = new HttpClient();
Good Code
builder.Services.AddHttpClient("github", c =>
c.BaseAddress = new("https://api.github.com"));
Benefit: Prevents socket exhaustion and supports reuse.
Lesson: Manage connections through the framework.
48. Source Generators for Boilerplate
Manually generating repetitive mapping or serialization code wastes effort. Source generators handle it at compile-time.
Bad Code
// Hand-written repetitive mapping logic
Good Code
// Generated automatically at compile-time via source generator
Benefit: Zero runtime cost and fewer maintenance files.
Lesson: Automate what doesn’t require creativity.
49. BenchmarkDotNet for Real Performance
Optimizing by intuition leads to false wins. BenchmarkDotNet gives precise runtime measurements.
Bad Code
// “Feels faster” approach
var list = data.OrderBy(x => x);
Good Code
[Benchmark]
public void SortTest() => _ = data.OrderBy(x => x);
Benefit: Data-driven optimization decisions.
Lesson: Measure before you tweak.
50. Readability Over Cleverness
The final habit isn’t about syntax — it’s mindset. Clever tricks impress once; clean code helps forever.
Bad Code
// One-liner with nested ternaries
var result = flag ? x > 0 ? "A" : "B" : "C";
Good Code
if (!flag) return "C";
return x > 0 ? "A" : "B";
Benefit: Clear intent over condensed logic.
Lesson: Write for humans, not for the compiler.
Mastery in .NET isn’t about memorizing features — it’s about writing code that stays readable long after you’re gone.
Each of these habits saves seconds today and hours next month. Practice them until they feel natural, and you’ll see fewer bugs, faster reviews, and more confident teammates.
Keep building, keep simplifying…
Building, breaking, and fixing .NET apps since before async was cool.


















