C# Essentials for .NET Developers: Types, LINQ, and Async/Await
Published:
This post covers the C# features every .NET developer uses daily. You do not need to master every corner of the language on day one, but you do need a solid grip on how types behave, how LINQ transforms data, and how async/await keeps your application responsive. These three areas show up in almost every code review, bug report, and production service.
Why C# fundamentals matter
The .NET ecosystem is wide, but the language patterns repeat everywhere:
- APIs receive DTOs and map them to domain types
- services query collections and databases using LINQ-like syntax
- apps call networks, disks, and queues asynchronously
When these foundations are weak, common problems follow:
- null reference exceptions
- confusing collection code
- slow or blocked threads
- async deadlocks or fire-and-forget bugs
The goal is not just “write code that works”. The goal is write code that is predictable under load and easy for the next developer to read.
Understanding core C# types
C# has two broad categories of types:
- value types
- reference types
Value types store their data directly. Common examples are:
intdecimalboolDateTimestruct
Reference types store a reference to an object. Common examples are:
string- arrays
classrecord class- interfaces
Example:
int quantity = 5;
decimal price = 19.99m;
bool isInStock = true;
DateTime createdOn = DateTime.UtcNow;
string productName = "Mechanical Keyboard";
int[] scores = [10, 20, 30];
Why the difference matters:
- value types are copied by value unless wrapped or boxed
- reference types point to shared objects
- nullability rules are different between them
For example:
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 10
Console.WriteLine(b); // 20
But with a reference type:
var order = new Order { Id = 1 };
var sameOrder = order;
sameOrder.Id = 99;
Console.WriteLine(order.Id); // 99
Both variables refer to the same object instance.
Nullable types and null safety
In modern .NET projects, nullable reference types should stay enabled:
<Nullable>enable</Nullable>
That tells the compiler to warn you when code may dereference null.
Examples:
int? optionalCount = null;
string? middleName = null;
string firstName = "Santosh";
Use nullable types when “no value” is a valid state. Do not use them everywhere by default. A property that must exist should remain non-nullable and be initialized correctly.
Bad habit:
public string Name { get; set; }
Better:
public string Name { get; set; } = string.Empty;
Or require it through the constructor:
public sealed record Customer(string Name, string Email);
var, explicit types, and readability
var does not mean “dynamic”. It means the compiler infers the type.
var total = 42.50m; // decimal
var customers = new List<string>();
Use var when the type is obvious from the right side. Avoid it when the type is unclear and would force the reader to guess.
Good:
var stream = new FileStream(path, FileMode.Open);
Less clear:
var result = service.Execute();
In the second case, an explicit type may communicate intent better.
Collections you should recognize immediately
You will constantly see these collection types:
T[]for arraysList<T>for mutable ordered listsDictionary<TKey, TValue>for key-value lookupIEnumerable<T>for sequences you can iterateIReadOnlyList<T>when callers should not mutate the collection
Example:
var products = new List<Product>
{
new(1, "Keyboard", 129.00m, true),
new(2, "Mouse", 59.00m, true),
new(3, "Cable", 12.00m, false)
};
If a method only needs to read data, returning IReadOnlyList<T> or IEnumerable<T> often communicates intent better than returning a mutable List<T>.
LINQ basics
LINQ stands for Language Integrated Query. It gives you a standard way to filter, project, sort, and aggregate data.
Common operators:
WherefiltersSelectprojectsOrderBysortsAnychecks for existenceFirstOrDefaultreturns one element or a default valueGroupBygroups elementsSum,Count,Max, andMinaggregate values
Given this model:
public sealed record Product(int Id, string Name, decimal Price, bool IsActive);
You can write:
var activeProductNames = products
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.Select(p => p.Name)
.ToList();
This reads as:
- keep only active products
- sort by name
- select only the name
- materialize the results as a list
LINQ query syntax also exists:
var activeProductNames =
(from p in products
where p.IsActive
orderby p.Name
select p.Name).ToList();
Most .NET teams prefer method syntax because it is more common in modern codebases and chains well with extension methods.
Deferred execution vs materialization
This is one of the most important LINQ concepts.
Many LINQ operations are lazily evaluated. That means the query is not executed until you iterate it.
var activeProducts = products.Where(p => p.IsActive);
At this point, no list has been created yet. Execution happens when you loop over activeProducts or call a materializing method such as:
ToList()ToArray()ToDictionary()
Why it matters:
- deferred execution can be efficient
- it can also lead to repeated work if you enumerate the sequence multiple times
- with database providers like Entity Framework Core, the query may not execute until materialization
If you need a stable snapshot, materialize it once:
var activeProducts = products
.Where(p => p.IsActive)
.ToList();
Async and await
Asynchronous programming is essential whenever your app waits on external work such as:
- HTTP calls
- database access
- file I/O
- message brokers
The key rule is simple: do not block a thread while waiting for I/O if an asynchronous API exists.
Example service:
public sealed class ProductApiClient(HttpClient httpClient)
{
public async Task<string> GetProductsJsonAsync(CancellationToken cancellationToken)
{
using var response = await httpClient.GetAsync(
"/api/products",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
}
What is happening here:
- the method returns
Task<string> awaitpauses the method without blocking the thread- execution resumes when the HTTP operation completes
An async call site looks like:
var json = await client.GetProductsJsonAsync(cancellationToken);
Console.WriteLine(json);
Guidelines for async methods
Keep these rules in mind:
- return
TaskorTask<T>from async methods - use
async voidonly for UI event handlers - propagate
CancellationTokenwhen available - prefer
awaitover.Resultor.Wait() - name async methods with the
Asyncsuffix
Bad:
var json = client.GetProductsJsonAsync(CancellationToken.None).Result;
Blocking like this can reduce throughput and, in some application models, cause deadlocks.
Running multiple async operations
If independent operations can run at the same time, use Task.WhenAll:
var inventoryTask = inventoryClient.GetInventoryAsync(cancellationToken);
var pricingTask = pricingClient.GetPricingAsync(cancellationToken);
await Task.WhenAll(inventoryTask, pricingTask);
var inventory = await inventoryTask;
var pricing = await pricingTask;
This is better than awaiting each one sequentially when there is no dependency between them.
A realistic example that combines types, LINQ, and async
The following method fetches products, filters them, and projects a response model:
public sealed record ProductDto(int Id, string Name, decimal Price);
public sealed class ProductService(ProductApiClient client)
{
public async Task<IReadOnlyList<ProductDto>> GetActiveProductsAsync(
CancellationToken cancellationToken)
{
var json = await client.GetProductsJsonAsync(cancellationToken);
var products = JsonSerializer.Deserialize<List<Product>>(
json,
new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? [];
return products
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.ToList();
}
}
That method demonstrates several everyday patterns:
- non-null fallback with
?? [] - LINQ projection to a DTO
Task<IReadOnlyList<ProductDto>>for asynchronous results- explicit cancellation support
Common mistakes to avoid
Watch for these problems in beginner C# code:
- disabling nullable warnings instead of fixing them
- returning mutable collections when callers should not change data
- chaining too much LINQ into unreadable one-liners
- mixing synchronous and asynchronous APIs in the same workflow
- swallowing exceptions inside async methods without logging or handling them meaningfully
The best C# code is not the cleverest code. It is the code a teammate can read quickly and trust.
Next Article: Configuration and Secrets in .NET 8: appsettings.json, Environment Variables, and User Secrets
