C# is a modern, type-safe language built on .NET — powering web APIs, desktop apps, game engines (Unity), mobile (MAUI), and cloud-native microservices. This roadmap shows you exactly what to learn, in what order, and realistic timelines to go from zero to a job-ready C# developer.
At a glance
| Phase |
Topics |
Time estimate |
| 1 |
C# fundamentals — syntax, types, control flow |
4–6 weeks |
| 2 |
Object-oriented programming (OOP) |
3–4 weeks |
| 3 |
.NET ecosystem — LINQ, generics, async/await |
3–4 weeks |
| 4 |
Web development — ASP.NET Core |
6–8 weeks |
| 5 |
Databases — Entity Framework Core |
3–4 weeks |
| 6 |
Testing — xUnit, Moq, integration tests |
2–3 weeks |
| 7 |
DevOps — Docker, CI/CD, cloud deployment |
3–4 weeks |
| 8 |
Advanced C# — performance, DI, architecture |
3–4 weeks |
| 9 |
Specialisation — Unity / MAUI / Blazor |
4–8 weeks |
| 10 |
System design + portfolio + job search |
4–6 weeks |
| Total to first job |
|
~12–16 months |
Phase 1 — C# fundamentals (Weeks 1–6)
C# has clean, readable syntax with a strong type system. Get comfortable with the basics before touching frameworks.
Core concepts to master
| Concept |
Key details |
| Variables & types |
int, double, bool, string, char; var for type inference |
string |
Immutable; $"Hello {name}" interpolation; StringBuilder for loops |
| Arrays |
Fixed-size int[] arr = new int[5]; prefer List<T> in practice |
| Control flow |
if/else, switch expressions (C# 8+), for, foreach, while |
| Methods |
Return types, parameters, out/ref, optional params with defaults |
static |
Class-level; entry point static void Main(string[] args) or top-level |
const vs readonly |
const = compile-time; readonly = set once in constructor |
| Nullable types |
int?, string?; null-coalescing ??, null-conditional ?. |
// Hello World — top-level statement (C# 10+)
Console.WriteLine("Hello, World!");
// Classic entry point
class Program
{
static void Main(string[] args)
{
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");
// Switch expression (C# 8+)
string category = age switch
{
< 18 => "Minor",
< 65 => "Adult",
_ => "Senior"
};
// Null-coalescing
string? input = null;
string display = input ?? "default";
}
}
C# versions cheat sheet
| Version |
.NET |
Key features |
| C# 8 |
.NET Core 3.x |
Nullable reference types, switch expressions, async streams |
| C# 9 |
.NET 5 |
Records, top-level statements, init-only setters, pattern matching |
| C# 10 |
.NET 6 |
Global usings, file-scoped namespaces, record structs |
| C# 11 |
.NET 7 |
Raw string literals, list patterns, required members |
| C# 12 |
.NET 8 |
Primary constructors, collection expressions, inline arrays |
| C# 13 |
.NET 9 |
params collections, field keyword, Lock type |
Phase 2 — Object-oriented programming (Weeks 7–10)
C# is a deeply OOP language. You need to understand classes, inheritance, interfaces, and polymorphism to work with any real .NET codebase.
Four pillars in C#
| Pillar |
C# mechanism |
| Encapsulation |
private fields + public properties (get; set;) |
| Inheritance |
class Dog : Animal — single inheritance only |
| Polymorphism |
virtual methods + override; abstract classes; interfaces |
| Abstraction |
abstract class, interface — hide implementation details |
// Abstract base class
public abstract class Shape
{
public abstract double Area();
public string Describe() => $"Area = {Area():F2}";
}
// Concrete implementation
public class Circle : Shape
{
public double Radius { get; init; }
public Circle(double radius) => Radius = radius;
public override double Area() => Math.PI * Radius * Radius;
}
// Interface
public interface IResizable
{
void Scale(double factor);
}
// Record (C# 9+) — immutable value object
public record Point(double X, double Y)
{
public double DistanceTo(Point other) =>
Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}
// Usage
Shape shape = new Circle(5);
Console.WriteLine(shape.Describe());
var p1 = new Point(0, 0);
var p2 = new Point(3, 4);
Console.WriteLine(p1.DistanceTo(p2)); // 5
abstract class vs interface
| Feature |
abstract class |
interface |
| Multiple inheritance |
No |
Yes (a class can implement many) |
| State (fields) |
Yes |
No (default interface methods only) |
| Constructor |
Yes |
No |
| Access modifiers |
Yes |
All public by default |
| Use when |
Shared base behaviour + state |
Contract / capability |
Phase 3 — .NET ecosystem: LINQ, generics & async/await (Weeks 11–14)
These three features are everywhere in C# code. You cannot write real .NET applications without them.
LINQ — Language Integrated Query
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Method syntax (preferred)
var evens = numbers
.Where(n => n % 2 == 0)
.Select(n => n * n)
.ToList(); // [4, 16, 36, 64, 100]
// Grouping
var words = new[] { "apple", "avocado", "banana", "blueberry", "cherry" };
var grouped = words
.GroupBy(w => w[0])
.ToDictionary(g => g.Key, g => g.ToList());
// Aggregation
double average = numbers.Average();
int max = numbers.Max();
bool any = numbers.Any(n => n > 5);
bool all = numbers.All(n => n > 0);
Generics
// Generic method
public T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
// Generic class
public class Repository<T> where T : class
{
private readonly List<T> _items = new();
public void Add(T item) => _items.Add(item);
public IEnumerable<T> GetAll() => _items;
}
// Common generic constraints
// where T : class — reference type
// where T : struct — value type
// where T : new() — has parameterless constructor
// where T : BaseClass — inherits from BaseClass
// where T : IInterface — implements interface
Async/await
using System.Net.Http.Json;
// async method must return Task, Task<T>, or void (for event handlers only)
public async Task<string> FetchDataAsync(string url)
{
using var http = new HttpClient();
var response = await http.GetStringAsync(url);
return response;
}
// Run multiple tasks in parallel
public async Task<(string a, string b)> FetchBothAsync()
{
var task1 = FetchDataAsync("https://api.example.com/a");
var task2 = FetchDataAsync("https://api.example.com/b");
await Task.WhenAll(task1, task2);
return (task1.Result, task2.Result);
}
// CancellationToken — always accept it in library code
public async Task LongOperationAsync(CancellationToken ct = default)
{
for (int i = 0; i < 100; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(100, ct);
}
}
Phase 4 — Web development: ASP.NET Core (Weeks 15–22)
ASP.NET Core is the go-to framework for building web APIs, MVC apps, and minimal APIs in .NET.
Minimal API (modern approach)
// Program.cs — the entry point in .NET 6+
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IProductService, ProductService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Route handlers
app.MapGet("/products", async (IProductService svc) =>
await svc.GetAllAsync());
app.MapGet("/products/{id:int}", async (int id, IProductService svc) =>
await svc.GetByIdAsync(id) is Product p
? Results.Ok(p)
: Results.NotFound());
app.MapPost("/products", async (Product product, IProductService svc) =>
{
var created = await svc.CreateAsync(product);
return Results.Created($"/products/{created.Id}", created);
});
app.MapPut("/products/{id:int}", async (int id, Product product, IProductService svc) =>
await svc.UpdateAsync(id, product) ? Results.NoContent() : Results.NotFound());
app.MapDelete("/products/{id:int}", async (int id, IProductService svc) =>
await svc.DeleteAsync(id) ? Results.NoContent() : Results.NotFound());
app.Run();
Controller-based API
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
public ProductsController(IProductService service) => _service = service;
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetAll()
=> Ok(await _service.GetAllAsync());
[HttpGet("{id:int}")]
public async Task<ActionResult<Product>> GetById(int id)
{
var product = await _service.GetByIdAsync(id);
return product is null ? NotFound() : Ok(product);
}
[HttpPost]
public async Task<ActionResult<Product>> Create(Product product)
{
var created = await _service.CreateAsync(product);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
}
ASP.NET Core ecosystem
| Package |
Purpose |
Microsoft.AspNetCore.Authentication.JwtBearer |
JWT auth middleware |
FluentValidation.AspNetCore |
Request validation |
Serilog.AspNetCore |
Structured logging |
MediatR |
CQRS mediator pattern |
AutoMapper |
Object-to-object mapping |
Swashbuckle.AspNetCore |
Swagger/OpenAPI docs |
Carter |
Minimal API modules |
FastEndpoints |
Fast minimal API framework |
Phase 5 — Databases: Entity Framework Core (Weeks 23–26)
Entity Framework Core (EF Core) is the standard ORM for .NET. It supports SQL Server, PostgreSQL, SQLite, MySQL, and more.
// 1. Define your model
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; } = null!;
}
// 2. Define the DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Product>(e =>
{
e.Property(p => p.Name).HasMaxLength(200).IsRequired();
e.Property(p => p.Price).HasPrecision(10, 2);
e.HasOne(p => p.Category).WithMany().HasForeignKey(p => p.CategoryId);
});
}
}
// 3. Register in DI
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
// 4. Typical repository operations
public class ProductRepository(AppDbContext db)
{
public Task<List<Product>> GetAllAsync() =>
db.Products.Include(p => p.Category).AsNoTracking().ToListAsync();
public Task<Product?> GetByIdAsync(int id) =>
db.Products.FindAsync(id).AsTask();
public async Task<Product> CreateAsync(Product product)
{
db.Products.Add(product);
await db.SaveChangesAsync();
return product;
}
public async Task<bool> UpdateAsync(Product product)
{
db.Products.Update(product);
return await db.SaveChangesAsync() > 0;
}
public async Task<bool> DeleteAsync(int id)
{
return await db.Products.Where(p => p.Id == id).ExecuteDeleteAsync() > 0;
}
}
// 5. Migrations
// dotnet ef migrations add InitialCreate
// dotnet ef database update
EF Core best practices
| Tip |
Why |
Use AsNoTracking() for read-only queries |
Avoids EF's change-tracking overhead |
Avoid loading full graph; use Include selectively |
Prevents N+1 and over-fetching |
Use ExecuteDeleteAsync / ExecuteUpdateAsync (EF 7+) |
Skips materialisation for bulk operations |
| Use compiled queries for hot paths |
Eliminates repeated expression-tree compilation |
Add indexes via HasIndex in OnModelCreating |
Faster lookups; not auto-added by EF |
Phase 6 — Testing (Weeks 27–29)
The .NET testing ecosystem is first-class. Aim for 80%+ coverage.
// Unit test with xUnit + Moq
using Xunit;
using Moq;
public class ProductServiceTests
{
private readonly Mock<IProductRepository> _repoMock = new();
private readonly ProductService _sut;
public ProductServiceTests()
{
_sut = new ProductService(_repoMock.Object);
}
[Fact]
public async Task GetById_ExistingId_ReturnsProduct()
{
// Arrange
var expected = new Product { Id = 1, Name = "Widget", Price = 9.99m };
_repoMock.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(expected);
// Act
var result = await _sut.GetByIdAsync(1);
// Assert
Assert.NotNull(result);
Assert.Equal("Widget", result.Name);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public async Task GetById_InvalidId_ThrowsArgumentException(int id)
{
await Assert.ThrowsAsync<ArgumentException>(() => _sut.GetByIdAsync(id));
}
}
// Integration test with WebApplicationFactory
public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductsApiTests(WebApplicationFactory<Program> factory)
=> _client = factory.CreateClient();
[Fact]
public async Task GetProducts_Returns200()
{
var response = await _client.GetAsync("/products");
response.EnsureSuccessStatusCode();
}
}
Testing pyramid for .NET
| Level |
Framework |
Speed |
Quantity |
| Unit |
xUnit + Moq/NSubstitute |
Fast |
Most (~70%) |
| Integration |
xUnit + WebApplicationFactory + Testcontainers |
Medium |
Some (~20%) |
| E2E |
Playwright (.NET bindings) |
Slow |
Few (~10%) |
Phase 7 — DevOps (Weeks 30–33)
# Multi-stage Dockerfile for ASP.NET Core
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
# Run as non-root
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
# GitHub Actions CI/CD
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0'
- run: dotnet restore
- run: dotnet build --no-restore
- run: dotnet test --no-build --verbosity normal
- run: docker build -t myapp .
Cloud deployment options
| Platform |
Best for |
Notes |
| Azure App Service |
ASP.NET Core APIs |
Native .NET support; easy deployment |
| Azure Container Apps |
Containerised .NET microservices |
Serverless containers |
| AWS Elastic Beanstalk |
.NET on AWS |
.NET worker tier support |
| Railway / Render |
Quick deployment |
Docker-based |
| Fly.io |
Global edge deployment |
Dockerfile deploy |
Phase 8 — Advanced C# patterns (Weeks 34–37)
Dependency Injection
.NET has a built-in IoC container. Understanding DI lifetimes is essential.
// Service registration lifetimes
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>(); // new instance every time
builder.Services.AddScoped<IOrderService, OrderService>(); // one per HTTP request
builder.Services.AddSingleton<ICache, RedisCache>(); // one for the app lifetime
// Constructor injection (preferred)
public class OrderService(IOrderRepository repo, IEmailSender email)
{
public async Task PlaceOrderAsync(Order order)
{
await repo.SaveAsync(order);
await email.SendConfirmationAsync(order.CustomerEmail);
}
}
Options pattern
// appsettings.json
// { "Smtp": { "Host": "smtp.example.com", "Port": 587 } }
public class SmtpOptions
{
public string Host { get; set; } = "";
public int Port { get; set; }
}
// Registration
builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"));
// Usage
public class SmtpEmailSender(IOptions<SmtpOptions> options)
{
private readonly SmtpOptions _opts = options.Value;
}
Performance patterns
| Pattern |
When to use |
Span<T> / Memory<T> |
Zero-copy slicing of arrays/strings in hot paths |
ArrayPool<T>.Shared |
Reuse buffers; avoid GC pressure |
ValueTask<T> |
When async path often completes synchronously |
IAsyncEnumerable<T> |
Streaming large result sets from DB |
record struct |
Immutable value type without heap allocation |
FrozenDictionary<K,V> (.NET 8+) |
Read-only lookup table with faster reads |
Technology map
Language
└── C# 12 / 13 → .NET 8 / 9 runtime
Web
├── ASP.NET Core (Minimal API / MVC / Razor Pages)
├── Blazor (WebAssembly + Server)
└── SignalR (real-time WebSockets)
Databases
├── Entity Framework Core (SQL Server / PostgreSQL / SQLite)
├── Dapper (lightweight micro-ORM)
└── MongoDB.Driver (NoSQL)
Cloud / Infrastructure
├── Azure (App Service / Functions / Container Apps / Service Bus)
├── Docker + Kubernetes
└── GitHub Actions / Azure DevOps
Testing
├── xUnit / NUnit / MSTest
├── Moq / NSubstitute
└── Playwright (.NET) / Testcontainers
Specialisations
├── Unity (game development)
├── .NET MAUI (cross-platform mobile + desktop)
├── Blazor (full-stack C# in the browser)
└── Orleans (distributed actor model)
Phase 9 — Specialisation paths
| Track |
Tech |
Why C# |
Entry salary (US) |
| Backend / Web API |
ASP.NET Core + EF Core |
Performance, enterprise adoption |
$90k–$130k |
| Game development |
Unity + C# |
Most used game engine; C# is native |
$75k–$110k |
| Mobile / Desktop |
.NET MAUI |
One codebase → iOS, Android, Windows, macOS |
$85k–$120k |
| Full-stack browser |
Blazor WebAssembly |
C# on both client and server |
$90k–$130k |
| Cloud / Serverless |
Azure Functions + Durable Functions |
First-class .NET support in Azure |
$100k–$145k |
| Enterprise / ERP |
Dynamics 365, SharePoint, Power Platform |
Huge enterprise market |
$90k–$140k |
12-month timeline
| Month |
Focus |
| 1–2 |
C# syntax, types, control flow, OOP fundamentals |
| 3 |
LINQ, generics, async/await, collections |
| 4–5 |
ASP.NET Core — Minimal API, routing, middleware, auth |
| 6 |
Entity Framework Core — models, migrations, queries |
| 7 |
Testing — xUnit, Moq, integration tests |
| 8 |
Docker, CI/CD with GitHub Actions |
| 9 |
Build Portfolio Project 1 (CRUD API with auth + tests) |
| 10 |
Advanced topics: DI, CQRS with MediatR, background services |
| 11 |
Choose specialisation; build Portfolio Project 2 |
| 12 |
Resume, GitHub, LeetCode (Easy/Medium), job applications |
Portfolio project ideas
| Project |
Tech |
Skills demonstrated |
| REST API + Swagger |
ASP.NET Core + EF Core + PostgreSQL |
CRUD, auth, DI, migrations |
| Real-time chat |
ASP.NET Core + SignalR + Redis |
WebSockets, pub/sub, caching |
| Background job processor |
ASP.NET Core + Hangfire / Quartz |
Scheduling, retries, queues |
| Blazor dashboard |
Blazor WebAssembly + minimal API |
Full-stack C#, component design |
| Microservices demo |
3 services + RabbitMQ/Azure Service Bus |
Message-driven architecture |
| CLI tool |
System.CommandLine |
Argument parsing, testability |
C# developer roles & salary
| Role |
US median |
EU median |
Notes |
| Junior .NET Developer |
$70k–$90k |
€35k–$55k |
0–2 years |
| Mid-level C# Developer |
$90k–$120k |
€50k–$75k |
2–5 years |
| Senior .NET Developer |
$120k–$155k |
€70k–$100k |
5+ years |
| .NET Architect / Lead |
$145k–$185k |
€90k–$130k |
8+ years |
| Unity Developer |
$85k–$130k |
€50k–$85k |
Game-focused |
| Azure / Cloud Developer |
$120k–$160k |
€75k–$115k |
Cloud certifications help |
Common mistakes
| Mistake |
Why it's wrong |
Fix |
Using async void outside event handlers |
Exceptions can't be caught; fire-and-forget risks |
Return Task or Task<T> |
await inside a loop one-by-one |
Sequential instead of parallel; slow |
Use Task.WhenAll() |
Not using using for IDisposable |
Resource leaks (DB connections, HTTP clients) |
using var or await using |
Creating new HttpClient() per request |
Socket exhaustion under load |
Inject IHttpClientFactory |
Catching Exception everywhere |
Swallows unexpected errors silently |
Catch specific exceptions; log + rethrow others |
Blocking async code with .Result or .Wait() |
Deadlocks in ASP.NET context |
Always await async methods |
| Ignoring EF Core lazy loading pitfalls |
N+1 queries; slow API |
Use Include() or projection; disable lazy loading |
Skipping CancellationToken |
Requests can't be cancelled; wastes resources |
Accept and propagate CancellationToken |
C# vs other backend languages
| Dimension |
C# / .NET |
Java |
Python |
Go |
Node.js |
| Type safety |
Strong static |
Strong static |
Optional static |
Strong static |
Optional (TypeScript) |
| Performance |
Excellent (AOT .NET 9) |
Excellent (JVM) |
Moderate |
Excellent |
Good |
| Async model |
async/await |
Project Loom (Java 21+) |
asyncio |
Goroutines |
Event loop |
| Web framework |
ASP.NET Core |
Spring Boot |
FastAPI / Django |
Gin / Echo |
Express / Fastify |
| Primary use |
Enterprise, cloud, Unity |
Enterprise, Android |
Data science, web |
Cloud-native, DevOps |
Full-stack web |
| Learning curve |
Moderate |
Moderate–high |
Easy |
Moderate |
Easy–moderate |
| Job market 2025 |
Very strong |
Very strong |
Very strong |
Growing |
Very strong |
| Ecosystem |
NuGet (340k+ packages) |
Maven Central |
PyPI (500k+) |
pkg.go.dev |
npm (2M+) |
FAQ
Do I need to learn .NET Framework or .NET Core?
Start with .NET 8 or 9 (the modern, cross-platform runtime). .NET Framework (4.x) is legacy, Windows-only, and in maintenance mode. All new .NET projects use the modern runtime. You may encounter .NET Framework at legacy employers, but the API surface is similar.
Should I learn C# or Java first?
If you target game development (Unity), learn C# first — there is no Java alternative. For enterprise backend, either is excellent; C# has better tooling (Visual Studio, Rider), while Java has a larger existing talent pool. Both will get you a job.
Is Blazor production-ready?
Yes. Blazor Server is mature and widely used. Blazor WebAssembly is suitable for internal tools and dashboards. For public-facing apps with SEO requirements, pair Blazor with SSR (.NET 8 static SSR) or use a JS framework for the frontend.
What IDE should I use?
Visual Studio 2022 (Windows, free Community edition) is the most feature-rich option. JetBrains Rider (cross-platform, paid) is popular in the industry. VS Code with the C# Dev Kit extension works for smaller projects and is free and cross-platform.
Do I need Azure to be a C# developer?
No, but it helps. C# runs on any cloud — AWS, GCP, Fly.io, Render. However, Azure has first-class .NET support (App Service, Azure Functions, Durable Functions, Azure Service Bus), and many enterprise .NET shops are on Azure. Azure certifications (AZ-900, AZ-204) are a bonus.
How long does it take to get a C# job?
With consistent daily practice (~2–4 hours), most beginners land a junior role in 10–14 months. Coming from another OOP language (Java, TypeScript), you can be job-ready in 3–6 months — the learning curve is primarily the .NET ecosystem, not the language syntax.