Home› C# / .NET› Complete Guide
Complete Guide

Complete C# and .NET Tutorial

C# is Microsoft's flagship language powering everything from Unity games to enterprise ASP.NET applications. This guide covers all 60 C# and .NET tutorials on TheCodeForge.

Learning Roadmap
Beginner → Learn C# syntax, OOP and the .NET runtime
Intermediate → Build web APIs with ASP.NET, work with LINQ and Entity Framework
Advanced → Design enterprise applications, handle async patterns and testing
60
Topics
8
Beginner
30
Intermediate
22
Advanced
Jump to section
C# Basics (11)OOP in C# (10)C# Advanced (15)ASP.NET (14)Testing (5)Exceptions (5)

The most useful thing to understand about modern .NET is that it is not the .NET Framework people remember. The rewrite that became .NET Core and is now simply .NET is cross-platform, open source, runs in containers without ceremony, and is among the faster mainstream web runtimes. C# itself has quietly absorbed most of what people go to other languages for — pattern matching, records, nullable reference types, async streams, source generators.

What has not changed is where C# developers actually lose time, and it is a short list: async misuse that deadlocks or exhausts the thread pool, Entity Framework doing far more queries than the code suggests, and NullReferenceException in code written before nullable reference types existed. Those three account for most production incidents in .NET services, and all three are covered here.

Async: the rules that prevent the two classic failures

async does not make code parallel. It frees the calling thread while an I/O operation is outstanding, which is why it raises throughput on a server without adding threads. Two mistakes break that, and both are common enough to have names.

Blocking on async code — .Result, .Wait(), GetAwaiter().GetResult() — deadlocks in any context with a synchronization context, and starves the thread pool everywhere else, because a pool thread sits blocked waiting for work that needs a pool thread. async void is the other: it cannot be awaited, so exceptions escape to the top level and crash the process rather than surfacing at the call site.

csharp
// Deadlock in any context with a synchronization context;
// thread-pool starvation everywhere else.
var user = GetUserAsync(id).Result;

// Async all the way up - the only correct shape
var user = await GetUserAsync(id);

// async void: unawaitable, and its exceptions crash the process.
// The single legitimate use is a top-level event handler.
async void OnClick(object s, EventArgs e) { ... }     // avoid
async Task HandleClickAsync() { ... }                  // prefer

// Genuine concurrency needs to be asked for explicitly
var a = FetchAAsync();                 // start
var b = FetchBAsync();                 // start
await Task.WhenAll(a, b);              // then await both
In practiceIn library code, add .ConfigureAwait(false) to every await. It tells the runtime the continuation does not need the original context, which removes the deadlock risk for whoever calls your library from a UI or legacy ASP.NET application.

Entity Framework: the query you wrote is not the query that ran

EF Core's value is that LINQ reads like code and executes as SQL. Its cost is that the translation is invisible, so a small change in a query can move work from the database to your process, or turn one query into a thousand.

Two rules prevent most of it. Know exactly where IQueryable becomes IEnumerable — everything after that point runs in memory, over whatever the database already returned. And load what you will need, explicitly, rather than letting lazy loading issue a query per entity inside a loop.

PatternWhat actually happensPrefer
.ToList() then .Where()Whole table pulled into memory, then filteredFilter on the IQueryable first, materialise last
Navigation property inside a loopN+1 — one query per iteration.Include(), or a projection that selects only the columns you need
.Select() of whole entities for a read-only listFull entity materialisation plus change trackingProject to a DTO and add .AsNoTracking()
.Count() on a loaded collectionCounts in memory over already-fetched rows.CountAsync() on the queryable — SELECT COUNT(*)
csharp
// Log the SQL in development and the guesswork stops
optionsBuilder
    .UseSqlServer(conn)
    .LogTo(Console.WriteLine, LogLevel.Information)
    .EnableSensitiveDataLogging();          // development only

// Read-only projection: one query, only the columns needed, no tracking
var rows = await db.Orders
    .AsNoTracking()
    .Where(o => o.CreatedAt >= since)
    .Select(o => new OrderSummary(o.Id, o.Total, o.Customer.Name))
    .ToListAsync();

Nullable reference types turn a runtime crash into a warning

NullReferenceException is the most common exception in .NET applications, and since C# 8 the compiler can find most instances before you ship. Enabling nullable reference types makes string mean 'never null' and string? mean 'may be null', and the compiler warns wherever your code disagrees with its own declarations.

On an existing codebase, enable it per file or per project and work through the warnings gradually. The temptation to silence them with the null-forgiving ! operator is worth resisting — every ! is a promise to the compiler that you have not verified, and it is exactly where the next NullReferenceException will come from.

IDisposable, using, and the file-in-use error

The process cannot access the file because it is being used by another process is nearly always your own process holding a handle it never released. A FileStream or StreamWriter that is not disposed keeps the OS handle open until the finaliser eventually runs, which is not a schedule you can rely on.

The using declaration makes this hard to get wrong, and for anything holding an unmanaged resource — files, sockets, database connections — it should be automatic. The one thing that should not be wrapped this way is HttpClient: disposing it per request exhausts sockets, which is why IHttpClientFactory exists.

csharp
// using declaration - disposed at end of scope, no nesting needed
using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
var text = await reader.ReadToEndAsync();

// HttpClient is the exception: long-lived, injected, never per-request
public class RatesClient(HttpClient http)          // from IHttpClientFactory
{
    public Task<Rate[]> GetAsync(CancellationToken ct) =>
        http.GetFromJsonAsync<Rate[]>("/rates", ct)!;
}

Frequently Asked Questions

.NET Framework or .NET — which am I on, and does it matter?
It matters a great deal. .NET Framework 4.8 is Windows-only and receives security fixes but no new features. Modern .NET is cross-platform, substantially faster, and where all development happens. If a tutorial mentions System.Web or Global.asax it is Framework-era and the advice may not apply.
Why does .Result deadlock in ASP.NET but not in a console app?
Because classic ASP.NET and UI frameworks install a synchronization context that resumes continuations on a specific thread. Blocking that thread while awaiting a continuation that needs it produces a deadlock. A console app has no such context, so it does not deadlock — it just wastes a thread. ASP.NET Core also removed the context, which is why the deadlock is rarer now, but blocking is still the wrong shape.
What is the difference between Task and ValueTask?
Task is a reference type and allocates. ValueTask avoids that allocation when the result is already available — useful on hot paths that usually complete synchronously, like a cache hit. It has real restrictions: it must not be awaited twice or stored. Use Task by default and reach for ValueTask after measuring.
Is Entity Framework too slow for production?
No, but the naive usage is. Most EF performance complaints resolve to N+1 queries, tracking entities that are never modified, or filtering in memory after materialising. Log the generated SQL, fix those three, and EF is fast enough for the large majority of applications. Drop to Dapper or raw SQL for the specific hot queries that still need it.
Should I use records instead of classes?
For immutable data carriers — DTOs, API request and response models, domain value objects — yes. Records give you value equality, a with expression for non-destructive copies, and a readable ToString for nothing. Keep classes for entities with identity and mutable state.
What is the most valuable thing to learn after the language basics?
Dependency injection and the generic host, because everything in modern .NET is built on them — configuration, logging, options, hosted services, HTTP clients. Once the container and the lifetime rules are clear, ASP.NET Core, Worker Services and Minimal APIs all become the same small set of ideas in different clothes.

C# Basics

OOP in C#

C# Advanced

ASP.NET

Testing

Exceptions

Also Explore
Java 257 tutorials → System Design 145 tutorials → Database 144 tutorials → DevOps 310 tutorials → Cloud 13 tutorials → Testing 12 tutorials →
Start from the beginning

Every tutorial starts with a plain-English analogy — then real code, then interview questions.

Browse C# / .NET Tutorials →