Top 50 C# and .NET Code Hacks That Instantly Make You a Better Developer

50 C#/.NET Code Hacks to Improve Fast

Real C#/.NET tips with short code samples. Cleaner syntax, safer async, faster APIs, fewer bugs. Copy‑paste ready and team‑friendly.

.NET Fundamentals·By amarozka · November 4, 2025

50 C#/.NET Code Hacks to Improve Fast

Think your C# is clean? I thought so too-until a code review took me down in three minutes. Here are the tiny fixes and patterns I now use every day so you don’t learn the hard way.

If you write C# or ship ASP.NET Core services, you can shave minutes off tasks, cut allocations, and avoid classic bugs with a few small habits. Below are 50 practical hacks I’ve collected from real projects, broken into themed packs with short samples you can copy.

1) Language & Syntax Wins

1. Prefer using declarations over blocks
Keeps scopes tight and nesting flat.

using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
var line = await reader.ReadLineAsync();

2. File‑scoped namespaces
Less indent, same meaning (C# 10+).

namespace MyApp.Models;

public sealed record UserId(Guid Value);

3. Target‑typed new
Cut noise when the type is clear.

Dictionary<string, int> scores = new();

4. with on records for safe copies
Immutable updates read better.

public record User(string Name, int Age);
var older = user with { Age = user.Age + 1 };

5. Switch expressions beat long if chains

string ToEmoji(LogLevel level) => level switch
{
    LogLevel.Trace => "·",
    LogLevel.Debug => "•",
    LogLevel.Information => "i",
    LogLevel.Warning => "!",
    LogLevel.Error => "✖",
    _ => "?"
};

6. Pattern matching: is not null

if (obj is not null)
{
    // safe to use
}

7. Primary constructors (C# 12)
Great for tiny types.

public class Clock(ILogger<Clock> log)
{
    public DateTimeOffset Now() => DateTimeOffset.UtcNow;
}

8. Required members (C# 11)

public sealed class AppOptions
{
    public required string ApiBaseUrl { get; init; }
    public int TimeoutSeconds { get; init; } = 30;
}

9. Collection expressions (C# 12)

int[] odds = [1, 3, 5, 7];

10. Use readonly structs for value objects
Avoid surprises from hidden copies.

public readonly record struct Money(decimal Amount, string Currency);

From my project: switching to file‑scoped namespaces and target‑typed new dropped about 200 lines across a small library. Reviews got easier.

2) Guard Clauses & Fail Fast

11. Small guard helpers

public static class Guard
{
    public static T NotNull<T>(T? value, string name) where T : class
        => value ?? throw new ArgumentNullException(name);
}

var client = Guard.NotNull(httpClient, nameof(httpClient));

12. Validate early in constructors

public sealed class EmailService(string apiKey)
{
    private readonly string _apiKey = !string.IsNullOrWhiteSpace(apiKey)
        ? apiKey : throw new ArgumentException("apiKey required");
}

13. Prefer TryParse to exceptions

if (!Guid.TryParse(idText, out var id)) return BadRequest("Invalid id");

14. Use exception filters to log once

try
{
    await work();
}
catch (Exception ex) when (Log(ex))
{
    // filter always returns false, so this never runs
}

static bool Log(Exception ex) { /* log */ return false; }

15. Return ProblemDetails in APIs

app.MapGet("/users/{id}", async (string id, IUserRepo repo) =>
{
    if (!Guid.TryParse(id, out var guid))
        return Results.Problem("Invalid id", statusCode: 400);

    var user = await repo.Find(guid);
    return user is null ? Results.NotFound() : Results.Ok(user);
});

3) Async & Concurrency

16. Never async void in library code
Use Task so callers can await and handle errors.

17. Respect CancellationToken

public async Task<User?> Find(Guid id, CancellationToken ct)
    => await _db.Users.FindAsync([id], ct);

18. ConfigureAwait(false) in libraries

await stream.WriteAsync(buffer, ct).ConfigureAwait(false);

19. Use IAsyncEnumerable<T> for streams

await foreach (var line in ReadLines(path, ct))
{
    // process
}

static async IAsyncEnumerable<string> ReadLines(string p, [EnumeratorCancellation] CancellationToken ct)
{
    using var r = new StreamReader(File.OpenRead(p));
    while (!r.EndOfStream)
        yield return (await r.ReadLineAsync(ct))!;
}

20. Limit parallel work with SemaphoreSlim

var gate = new SemaphoreSlim(4);
await Parallel.ForEachAsync(items, async (x, ct) =>
{
    await gate.WaitAsync(ct);
    try { await Process(x, ct); }
    finally { gate.Release(); }
});

21. Use Channel<T> for producer/consumer

var channel = Channel.CreateUnbounded<string>();
_ = Task.Run(async () =>
{
    await foreach (var m in channel.Reader.ReadAllAsync())
        Console.WriteLine(m);
});
await channel.Writer.WriteAsync("Hello");

22. PeriodicTimer for steady jobs

var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
{
    await DoWork(ct);
}

23. Prefer ValueTask for hot paths
Only when a result is often cached/synchronous.

public ValueTask<User?> GetCached(Guid id)
    => _cache.TryGetValue(id, out var u)
        ? new(u) : new(_repo.Find(id));

24. Task.WhenAll for true parallelism

var tasks = urls.Select(DownloadAsync);
var pages = await Task.WhenAll(tasks);

Lesson learned: my old “fire‑and‑forget” method swallowed exceptions and took down a worker later. Moving to Channel<T> plus one consumer fixed backpressure and made errors visible.

4) LINQ & Collections

25. Avoid double enumeration

var list = source.ToList();
if (list.Count == 0) return;
// use list multiple times safely

26. Use Any() not Count() > 0

if (users.Any()) { /* ... */ }

27. Prefer TryGetValue on dictionaries

if (map.TryGetValue(key, out var value))
{
    // use value
}

28. Chunk for batching (NET 6+)

foreach (var batch in items.Chunk(100))
{
    await SaveBatch(batch);
}

29. DistinctBy and MaxBy (NET 6+)

var latestPerUser = events
    .GroupBy(e => e.UserId)
    .Select(g => g.MaxBy(e => e.Timestamp));

30. Prefer ImmutableArray<T> for read‑only

ImmutableArray<string> roles = users
    .Select(u => u.Role).ToImmutableArray();

31. Use Span<T>/Memory<T> for parsing

ReadOnlySpan<char> s = "2025-11-04";
if (DateOnly.TryParse(s, out var date)) { /* fast */ }

5) I/O, HTTP & JSON

32. Always reuse HttpClient with factory

builder.Services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.UserAgent.ParseAdd("my-app");
});

app.MapGet("/stars", async (IHttpClientFactory f) =>
{
    var client = f.CreateClient("github");
    return await client.GetStringAsync("repos/dotnet/runtime");
});

33. Use JsonSerializerContext for AOT speed

[JsonSerializable(typeof(User))]
public partial class AppJsonContext : JsonSerializerContext { }

var json = JsonSerializer.Serialize(user, AppJsonContext.Default.User);

34. Stream JSON for big payloads

await using var stream = response.Content.ReadAsStream();
var result = await JsonSerializer.DeserializeAsync<Result>(stream, ct: ct);

35. Prefer DateTimeOffset over DateTime for APIs

public record Post(string Title, DateTimeOffset PublishedAt);

36. Use Path.Combine and Path.GetTempPath()

var path = Path.Combine(Path.GetTempPath(), "report.csv");

6) EF Core & Data Access

37. Keep DbContext scoped

builder.Services.AddDbContext<AppDb>(opt =>
    opt.UseNpgsql(connString));

38. Project columns, not entities

var dto = await db.Users
    .Where(u => u.Id == id)
    .Select(u => new UserDto(u.Id, u.Name))
    .SingleOrDefaultAsync(ct);

39. Use AsNoTracking() for read‑only

var posts = await db.Posts.AsNoTracking()
    .Where(p => p.Published)
    .ToListAsync(ct);

40. Cancellation in queries

await db.SaveChangesAsync(ct);

Why it helped: switching a read‑heavy endpoint to AsNoTracking() cut CPU by ~20% and reduced GC pressure.

7) ASP.NET Core, DI & Middleware

41. Minimal APIs for simple endpoints

var app = WebApplication.CreateBuilder(args).Build();
app.MapGet("/health", () => Results.Ok(new { ok = true }));
app.Run();

42. Typed options with validation

builder.Services.AddOptions<AppOptions>()
    .BindConfiguration("App")
    .ValidateDataAnnotations()
    .Validate(o => Uri.IsWellFormedUriString(o.ApiBaseUrl, UriKind.Absolute),
        "ApiBaseUrl must be absolute").ValidateOnStart();

43. Keyed services for multi‑client setups (NET 8+)

builder.Services.AddKeyedSingleton<IStorage>("s3", new S3Storage());

44. Health checks and readiness

builder.Services.AddHealthChecks().AddDbContextCheck<AppDb>();
app.MapHealthChecks("/healthz");

45. Structured logging with scopes

using (_log.BeginScope(new { OrderId = id }))
{
    _log.LogInformation("Processing order");
}

46. Rate limit middleware (NET 7+)

builder.Services.AddRateLimiter(o =>
{
    o.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
        RateLimitPartition.GetFixedWindowLimiter("global", _ =>
            new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromSeconds(1)
            }));
});
app.UseRateLimiter();

8) Testing, Tooling & Quality

47. Enable nullable and analyzers

<!-- Directory.Build.props -->
<Project>
  <PropertyGroup>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <AnalysisLevel>latest</AnalysisLevel>
  </PropertyGroup>
</Project>

48. Golden master for risky refactors

// serialize output before change, compare after change
var before = JsonSerializer.Serialize(await RunOld());
var after  = JsonSerializer.Serialize(await RunNew());
Assert.Equal(before, after);

49. dotnet format in CI

- name: Format
  run: dotnet tool restore && dotnet format --verify-no-changes

50. Benchmark with BenchmarkDotNet

[MemoryDiagnoser]
public class ParseBench
{
    [Benchmark] public int ParseInt() => int.Parse("123");
    [Benchmark] public bool TryParseInt() => int.TryParse("123", out _);
}

Common Pitfalls I Still See

  • Random usage: newing Random() per call causes repeats. Use a single static Random or RandomNumberGenerator for crypto.
  • Local time in logs: use UTC everywhere. Convert only for UI.
  • Timing with DateTime.Now: use Stopwatch for durations.
  • Blind catches: catch (Exception) without logging hides real issues.
  • DTO drift: returning EF entities from APIs couples schema to clients; project to DTOs.

FAQ: Quick Answers Before You Ask

Which of these are safe for older projects?

Most language tips work back to C# 10. For C# 12 features (primary constructors, collection expressions), keep them to new modules or guard with analyzers.

Do I need ConfigureAwait(false) in ASP.NET Core?

In your app code, the sync context isn’t captured, so it’s optional. In libraries used by many hosts, keep it.

Will ValueTask always be faster?

No. Use it only when the result is often synchronous or cached. Otherwise Task is simpler and just fine.

Is HttpClientFactory really needed?

Yes if you call HTTP often. It fixes socket exhaustion and gives handlers/policies per client.

How do I roll this out to a team without chaos?

Add analyzers + Directory.Build.props, write a short style doc, and flip rules one by one. Pull requests should show the change and the reason.

Conclusion: 50 small wins add up fast

You don’t need a rewrite to raise quality. Pick five hacks from above and ship them this week: guard clauses, AsNoTracking, HttpClientFactory, Any() over Count(), and nullable + analyzers. Your diffs will shrink, bugs will drop, and on‑call will be calmer.

What would you add as hack #51? Drop your tip in the comments – I’ll test it on a real service and report back.

Leave a Reply

Your email address will not be published. Required fields are marked *