C# Guide: .NET Apps, Classes, Packages, and Backend Patterns
C# is a statically typed, object-oriented language that Microsoft created for the .NET platform, and it has grown into one of the most widely used languages for server applications, desktop software, games, and cloud services. It pairs a strong type system with modern features like pattern matching, records, async and await, and nullable reference types, so large codebases stay maintainable as they grow.
This page is the home for C# the language. The frameworks that run on it, .NET and ASP.NET Core and Entity Framework Core, have their own home one level down, because a framework is a separate thing from the language it is written in.
Where C# Fits
Reach for C# when you want a fast, typed language with first-class tooling and a single vendor-backed platform behind it. It is the default for the Microsoft ecosystem, a strong choice for back-end web services through ASP.NET Core, and the language most Unity game developers write every day. The runtime is cross-platform now, so a C# service deploys to Linux containers as readily as it runs on Windows.
How C# Runs
C# is compiled, but not straight to machine code, and knowing the middle step explains a lot of what the tooling does. The compiler turns your source into an intermediate language that ships inside the assembly, and the runtime's just-in-time compiler turns that into native code as the program runs. This is why a .NET assembly is portable across operating systems and processor architectures without being rebuilt, and why a decompiler can reconstruct readable C# from a released binary.
The just-in-time step also explains a benchmarking trap. The first call into a method pays for its compilation, so a naive timing loop that runs a method once reports the compile time, not the running time. Warm the method up before measuring, or use a benchmarking library that does it for you.
Ahead-of-time compilation is the alternative where that startup cost matters, such as short-lived command-line tools and serverless functions that pay the warm-up on every invocation. It produces a native binary with no just-in-time step and a much faster start, at the cost of a larger build and restrictions on the reflection-heavy patterns some libraries rely on. The just-in-time path remains the default for long-running server applications, where a process starts once and then benefits from a compiler that can optimise against the code paths actually being taken.
Learning Path
Learn C# in the order that makes the framework code read like ordinary classes:
- Value and reference types, variables, and
const. - Nullable value and reference types, so missing data is explicit.
- Collections:
List,Dictionary, arrays, andforeach. - Classes, properties, constructors, and creating objects.
- Inheritance and interfaces, leaning on interfaces for flexible design.
- Methods, overloading, optional and named arguments.
- LINQ and
async/awaitonce the fundamentals are comfortable. - ASP.NET Core, EF Core, and the wider .NET stack.
Build a small console app once the basics feel normal. A task tracker or a tiny CSV parser will teach you types, collections, and classes without a framework in the way.
A Worked Example
Here is a small program that builds an order line and prints its total, the kind of pattern that shows up in almost every real C# codebase. In a modern top-level Program.cs, your executable statements come first and the classes they use sit below them, so the OrderLine type is defined at the bottom.
var line = new OrderLine
{
ProductName = "Keyboard",
Quantity = 2,
UnitPrice = 49.99m
};
Console.WriteLine($"{line.ProductName}: {line.Total:C}");
public class OrderLine
{
public required string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public string? Note { get; set; }
public decimal Total => Quantity * UnitPrice;
} ProductName, Quantity, and UnitPrice are auto-implemented properties, generated getters and setters without the boilerplate. ProductName also carries required, which makes the compiler insist you assign it in the initializer, so you cannot construct an OrderLine without giving it a value. Note is nullable, marked with a question mark, so the compiler knows this field may stay empty. Total has no backing field, so it recalculates on every read and can never drift out of sync with the values it depends on. The m suffix on 49.99m forces the literal to a decimal, the type you want for money, since binary floating point silently rounds cents away.
Value Types and Reference Types
The split between value types and reference types is the one piece of C# that changes how you read every other line, and it is worth learning before the object-oriented material rather than after it.
A value type holds its data directly. Assign it by value, pass it to an ordinary parameter, or store it in a field, and the whole thing is copied, so the copy and the original are independent from that moment on. The copy is shallow: a reference-type field inside a struct is copied as a reference, so both structs still point at the same object and a mutation through either is visible through the other. The numeric types, bool, char, decimal, every enum, and every struct are value types. A reference type holds a reference to an object elsewhere in memory. Assigning it copies the reference, not the object. Classes, interfaces, arrays, delegates and string are reference types.
That difference is behind several behaviours that otherwise look arbitrary. Pull a struct out of a List into a local variable, mutate the local, and you have changed your copy while the list's element is untouched, and you have to assign the local back. Writing list[i].Field = value directly does not silently mutate a copy; it is a compile error, CS1612, which is the language stopping you from making exactly that mistake. Passing a large struct by value copies it every time, which is why the guidance is to keep structs small. And a method that reassigns a reference parameter changes only its own local reference, while a method that mutates the object behind that reference changes what every other holder sees.
The copying rules apply to by-value operations specifically. The ref, in and out modifiers, along with ref locals and ref returns, all pass a value type by reference instead, which is how the framework avoids copying large structs on hot paths.
Equality follows the same split, with a wrinkle worth knowing before it bites. Plain structs inherit field-by-field Equals, and the built-in value types define their own type-specific equality, though even there double.NaN is famously not equal to itself. The == operator is a separate question from Equals: the built-in types define it, while a struct you write yourself does not, and comparing two of them with == is a compile error until you overload it. Reference types compare by identity by default, so two separately constructed objects with identical fields are not equal, which is the behaviour the gotchas section below describes.
A record is the shortcut when you want a reference type that compares by value: the compiler generates the equality members, == included, and record struct does the same for a value type. That generated equality is memberwise rather than deep. It defers to each member's own equality, so a record holding an array or a List still compares those members by reference and two records with equal-looking contents come out unequal. string is the reference type most worth memorising, because the language gives it value-like equality, but it is not the only exception: delegates compare by invocation list, and any type can overload == for itself.
Common Beginner Gotchas
A few habits catch almost everyone moving into C# from a looser language. == on reference types checks identity by default, so two objects with identical values fail the comparison. Overriding Equals only changes what a.Equals(b) returns; the == operator keeps using identity until you overload it or switch to a record, which generates value-based Equals and == together. Integer division truncates silently, so dividing two int values needs a cast to double or decimal before you get a fractional result. A NullReferenceException on a variable marked non-nullable usually means an earlier warning got ignored, since the nullable check happens only at compile time and a forced ! can still let a null through at runtime.
Why Old C# Advice Is Dangerous
C# has a long history and an unusually large body of tutorials written for .NET Framework, the original Windows-only platform. It is still supported as a Windows component, so that material is not abandoned, but it is no longer where new work starts, and following it can leave you with a habit that does not fit the platform you are actually on.
The platform split is the big one. .NET Framework is Windows-only, and some of its app models, notably Web Forms and the System.Web stack, never moved to modern .NET at all. Code that depends on them does not simply recompile when you retarget; it needs rewriting against a different framework. The reliable tell is those APIs themselves. IIS is not the tell, which is the trap: ASP.NET Core supports IIS hosting as a first-class option, so an article configuring IIS may be perfectly current. Look for System.Web or Web Forms instead. The project file is a useful hint but only a hint. The modern SDK-style format is identified by its SDK declaration, and an SDK-style project can still target .NET Framework.
Disposing HttpClient per request is the most damaging piece of stale advice still in circulation. Wrapping it in a using block looks correct, because that is the right instinct for almost everything else implementing IDisposable. Here it backfires, though not for the reason usually given: disposing the client does close its pooled connections, and the problem is what happens next. A closed TCP connection leaves its local port in a TIME_WAIT state that blocks immediate reuse, so a service creating and disposing a client per request can run out of usable ports under load. Use a long-lived client with a PooledConnectionLifetime set so it still picks up DNS changes, or let IHttpClientFactory pool the handlers for you. A single global client is one valid answer, not the only one.
ConfigureAwait(false) after every await is advice that has outlived most of its context. It existed to avoid deadlocks when async code resumed onto a UI or legacy web synchronization context. ASP.NET Core installs no synchronization context by default, so in ordinary application code on a modern web project it usually changes nothing, but "by default" is doing real work in that sentence, since code can install one, and a non-default task scheduler can still affect where a continuation runs. It remains genuinely useful in library code that might be called from a desktop application, and it is about controlling where continuations resume, not only about deadlocks.
Boilerplate is the mildest case and the most common. Older tutorials open with a namespace, a Program class, and a static void Main; current C# lets an executable project start with statements directly, as the worked example above does. Exactly one file in a project may do this, using directives still come first, and any type declarations in that file follow the statements. The old form remains valid, so nothing breaks.
Nullable reference types are a related but sharper change. Code written before them compiles unchanged while the nullable context stays off. Turning it on changes how the compiler interprets every unannotated reference type and can produce a large number of new warnings at once. Those warnings point at places where null was never ruled out, which is worth knowing, but a warning is not by itself proof of a live bug. The annotations are a compile-time contract and normally add no runtime checks, though frameworks that read metadata, Entity Framework Core among them, may treat them as real constraints.
The frameworks hub covers the modern stack these decisions sit inside, including how the request pipeline, package choices, and deployment fit together.
Frameworks and the Wider Stack
- C# frameworks: .NET, ASP.NET Core, and EF Core is the framework hub for building real applications: the runtime, the web framework, the ORM, NuGet package discipline, testing, and deployment.
- Essential ASP.NET Core NuGet packages is the practical shortlist for a production web app.
Related Languages
C# is the right starting point when a mature, vendor-backed platform and strong tooling fit the team. If you are coming from another typed language, the .NET class library is the part that takes time, more than the syntax.
Reach for Go when you want a lighter backend language with simpler deployment, Rust when memory control matters most, or Swift for Apple platforms. SQL covers the databases an EF Core application talks to.
Related
- C# Frameworks - .NET, ASP.NET Core, and EF Core.
- Backend development - C# alongside the other server-side languages.
- Programming - the broader language and data-structure index.
Frequently Asked Questions
What is the difference between .NET Framework and .NET?
.NET Framework is the original Windows-only runtime, now in maintenance with no new features. .NET, formerly .NET Core, is the current cross-platform runtime that new C# work targets, and it is what the platform means today.
What is a NuGet package?
The package format and package manager for .NET, filling the role npm fills for Node or pip for Python. A project declares the packages it needs and the tooling downloads them and wires up the references.
Are C# and Java the same language?
No. Both are statically typed, garbage-collected and C-family, which makes the syntax look alike, but they run on different runtimes, have different owners, and have diverged considerably in language features since they appeared.
What is the difference between a struct and a class?
A struct is a value type, typically stack-allocated and copied whenever it is assigned or passed. A class is a reference type on the heap, so assigning one variable to another shares the same object rather than copying it.
Is C# compiled or interpreted?
Compiled, though not straight to machine code. The compiler produces intermediate language, and the runtime's just-in-time compiler turns that into native code as the program runs. Ahead-of-time native compilation is available where startup time matters.
Sources
-
[1]
A tour of the C# language(learn.microsoft.com)
-
[2]
C# language reference(learn.microsoft.com)
Read Next
The C# framework stack: .NET, ASP.NET Core, Entity Framework Core, NuGet package discipline, testing, and deployment for the Microsoft web platform.
A starting point for server-side programming on CodeWalkers: the languages that run on the server, the databases behind them, and the APIs that connect everything.
A practical SQL guide for joins, schema design, indexes, transactions, database choices, CSV imports, search, PostgreSQL, MySQL, SQLite, MariaDB, and interview-ready reasoning.