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… 

CodeWithYog

Building, breaking, and fixing .NET apps since before async was cool.