ASP.NET Core Roadmap
ASP.NET Core is Microsoft's cross-platform framework for web applications and APIs, built on modern .NET and written in C#. It powers banking, insurance, logistics and enterprise software, and many companies that run on Microsoft tools hire for it steadily.
This roadmap takes you from C# fundamentals to running a service in production. Each stage lists topics in learning order, explains each one in plain terms and gives you one exercise. After the stages come projects, interview questions with model answers, common mistakes and a readiness checklist.
By the end you should be able to build and document an API, explain the request pipeline and dependency injection, use Entity Framework Core without performance traps, add caching and messaging, test your work with real dependencies and ship it in a container.
- Level
- Beginner to advanced
- Time
- 14-18 weeks at about 8 hours a week
- Scope
- 59 topics in 10 stages
- Before you start
- Programming basics in any language: variables, loops, functions and classes; Comfort with a terminal and a code editor such as Visual Studio, Rider or VS Code; Basic understanding of HTTP and of what a database is; Optional: Git and GitHub Roadmap for the projects
What you will be able to do
- Write idiomatic C# using LINQ, records, async and await, and nullable reference types
- Explain the middleware pipeline, configuration layers and dependency injection lifetimes, and avoid captive dependencies
- Build, validate, document and secure an HTTP API with minimal APIs or controllers
- Model data with Entity Framework Core, manage migrations and avoid N+1 queries
- Add caching, background jobs and messaging, and explain the trade-offs of microservices
- Test code with xUnit, mocks and real databases in containers, and run the tests in CI
- Package a service in a container with health checks, structured logs and metrics
The roadmap at a glance
- Stage 1C# and .NET FoundationsBeginner · 2-3 weeks · 6 topics
- Stage 2ASP.NET Core FundamentalsBeginner · 2-3 weeks · 7 topics
- Stage 3Building APIsIntermediate · 3 weeks · 7 topics
- Stage 4Web UI and Real-Time CommunicationIntermediate · 3 weeks · 5 topics
- Stage 5Relational Databases and SQLIntermediate · 2-3 weeks · 7 topics
- Stage 6Entity Framework Core in DepthIntermediate · 2-3 weeks · 6 topics
- Stage 7NoSQL, Search, Caching and Query APIsIntermediate · 3 weeks · 6 topics
- Stage 8Background Work, Messaging and Distributed SystemsAdvanced · 3-4 weeks · 6 topics
- Stage 9Testing and Code QualityIntermediate · 2-3 weeks · 5 topics
- Stage 10Delivery, Containers and OperationsAdvanced · 2 weeks · 4 topics
Stage 1 C# and .NET Foundations
Goal. Write basic C# programs, use the .NET command line and explain how a web request works.
C# and C# Basics Core
C# is the main language of .NET: compiled, strongly typed and class based. C# Basics to learn first: variables and built-in types, string handling, operators, if and switch, loops, methods, classes and records, properties, interfaces, and exceptions with try/catch.
public record Book(string Title, decimal Price);
var books = new List<Book> { new("Dune", 9.99m), new("Emma", 4.50m) };
var cheap = books.Where(b => b.Price < 5).Select(b => b.Title);
Console.WriteLine(string.Join(", ", cheap));Modern C# adds top-level statements, nullable reference types (turn them on and treat warnings seriously), pattern matching, async/await, LINQ for querying collections, and record types for immutable data. Practise value versus reference types, async and LINQ early.
Practice. Write a console program that reads a list of numbers, filters and sorts them with LINQ and prints the result.
Data Structures Core
Know the standard collections and when each fits. List<T> is a resizable array, fast for index access and appends. Dictionary<TKey,TValue> is a hash table with near constant-time lookup. HashSet<T> tests membership and removes duplicates. Queue<T> and Stack<T> give first-in-first-out and last-in-first-out order. LinkedList<T>, SortedDictionary and ImmutableArray suit narrower cases.
Behind those sits the idea of Big O: a list search is linear, a dictionary lookup is constant on average. Choosing the wrong collection is a common cause of slow endpoints, such as scanning a list inside a loop when a dictionary would do.
IEnumerable<T> is the interface most methods accept, and LINQ queries on it are lazy until you enumerate. See Data Structures and Algorithms Roadmap for the wider topic.
Practice. Count word frequencies in a text file with a dictionary and print the ten most common.
.NET Framework and modern .NET Core
The .NET Framework is the original Windows-only platform, released in 2002. It still runs many older applications and receives only maintenance fixes. The modern platform, called .NET (starting with version 5, after .NET Core 1 to 3.1), is open source, cross-platform and where all new work happens. ASP.NET Core runs on it, on Windows, Linux and macOS.
A new release arrives every November. Even-numbered ones are long-term support (LTS) with three years of support; odd-numbered ones are shorter. For new projects pick the latest LTS. Related names you will hear: .NET MAUI builds mobile and desktop apps from one C# codebase, and Blazor builds web interfaces in C#. Legacy System.Web ASP.NET (Web Forms, old MVC) is not the same product as ASP.NET Core.
Practice. List three differences between ASP.NET on the .NET Framework and ASP.NET Core.
.NET CLI Core
The dotnet command line tool creates, builds, runs and publishes projects without an IDE.
dotnet new webapi -n Shop.Api
cd Shop.Api
dotnet add package Serilog.AspNetCore
dotnet run
dotnet test
dotnet publish -c Release -o outOther useful commands: dotnet new list to see templates, dotnet restore to fetch packages, dotnet watch to rebuild on change, dotnet ef for database migrations once the tool is installed, and dotnet user-secrets for local secrets. A solution (.sln) groups projects, and each project has a .csproj file that lists the target framework and packages. Knowing the CLI makes CI scripts and containers much easier to write.
Practice. Create a solution with an API project and a test project from the command line only, and run both.
HTTP / HTTPS Protocol Core
A web API is conversation over HTTP. A request has a method, a URL, headers and an optional body; the response has a status code, headers and a body. Methods describe intent: GET reads, POST creates, PUT replaces, PATCH changes part, DELETE removes. Safe methods (GET) do not change data, and idempotent ones (PUT, DELETE) give the same result when repeated.
Status codes tell clients what happened: 2xx success, 3xx redirects, 4xx client mistakes (400, 401, 403, 404, 409, 422), 5xx server faults. HTTPS wraps HTTP in TLS to encrypt traffic and confirm the server's identity. ASP.NET Core redirects HTTP to HTTPS with UseHttpsRedirection and sends HSTS headers in production. Cookies, Authorization headers and content negotiation (the Accept header) appear in every real project.
Practice. Call a public API with curl, and write down the method, status and three headers of each request and response.
Git, repository hosting and General Development Skills Core
General Development Skills that every employer expects: version control, reading documentation and error messages, debugging with breakpoints, writing readable code and small commits, and code review. Git records history locally, with branches for separate lines of work. Learn clone, add, commit, branch, merge, rebase, stash and how to resolve conflicts. See Git and GitHub Roadmap for detail.
Repo Hosting Services such as GitHub, GitLab and Azure DevOps store shared repositories, and add pull requests, issues and automated pipelines. Use short-lived branches and pull requests so that every change is reviewed and tested before merging.
Practice. Put a project on a hosting service, make a branch, open a pull request and merge it.
Stage 2 ASP.NET Core Fundamentals
Goal. Explain the request pipeline, configuration and dependency injection, and use them to structure an application.
Basics of ASP.NET Core Core
ASP.NET Core is Microsoft's framework for web applications and APIs. A project is a console program that builds a web host in Program.cs: it creates a builder, registers services, builds the app, adds middleware and maps endpoints.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapGet("/ping", () => "pong");
app.Run();The server is Kestrel, a fast cross-platform web server; in production it normally sits behind a reverse proxy such as IIS, Nginx or a cloud load balancer. The framework offers several styles of application: Minimal APIs, controllers with MVC, Razor Pages and Blazor, and all share the same pipeline, configuration, dependency injection and logging.
Practice. Create a web project, add two endpoints and call them from a browser and curl.
Middlewares Core
Middlewares are small components, arranged in a pipeline, that each handle a request and either pass it on or end it. Typical ones are exception handling, HTTPS redirection, static files, routing, CORS, authentication, authorisation and response compression. Order matters: authentication must run before authorisation, and exception handling should come first so it catches errors from everything after it.
app.Use(async (context, next) =>
{
var start = DateTime.UtcNow;
await next();
Console.WriteLine($"{context.Request.Path} took {(DateTime.UtcNow - start).TotalMilliseconds} ms");
});Use calls the next component, Run ends the pipeline, and Map branches it. For reusable middleware write a class with an InvokeAsync method. Anything you can do in a filter or middleware, prefer middleware when it applies to every request.
Practice. Write a middleware that adds a correlation id header and test its position relative to the exception handler.
App Settings and Configurations Core
App Settings and Configurations come from layered sources, read in order with later ones winning: appsettings.json, appsettings.{Environment}.json, user secrets in development, environment variables and command line arguments. The environment is set by ASPNETCORE_ENVIRONMENT, with Development, Staging and Production as common values.
Bind sections to typed classes with the options pattern:
builder.Services.Configure<MailOptions>(builder.Configuration.GetSection("Mail"));
// inject IOptions<MailOptions> where neededIOptionsSnapshot re-reads per request and IOptionsMonitor reacts to changes. Validate options at start-up with ValidateDataAnnotations().ValidateOnStart() so a missing value fails fast. Do not keep secrets in appsettings.json: use user secrets locally and environment variables or a vault such as Azure Key Vault in production.
Practice. Bind a settings section to a class, validate it on start-up and override one value with an environment variable.
Dependency Injection Core
Dependency Injection means a class receives the objects it needs instead of creating them. ASP.NET Core has a built-in container: you register services at start-up and the framework supplies them to constructors.
builder.Services.AddScoped<IOrderService, OrderService>();
public class OrdersController(IOrderService orders) : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id) =>
await orders.FindAsync(id) is { } o ? Ok(o) : NotFound();
}Programming to an interface makes classes easy to test, because a test can hand in a fake. It also keeps construction logic in one place. Avoid the service locator pattern, calling GetService all over the code, and avoid constructors that take a dozen dependencies, which usually signals that a class has too many jobs.
Practice. Define an interface and two implementations, register one, then swap to the other without changing the consumer.
Dependency Injection Life Cycles Core
Each registration has a lifetime that decides how long an instance lives.
- Transient: a new instance every time it is requested. Good for light, stateless services.
- Scoped Service Lifetime: one instance per HTTP request.
DbContextis registered this way. - Singleton: one instance for the application, shared by all threads, so it must be thread-safe.
The classic bug is a captive dependency: a singleton that takes a scoped service keeps the first request's instance for ever. The container's scope validation, on by default in Development, throws to warn you. Use IServiceScopeFactory when a singleton or background service needs scoped work. Never store per-user data in a singleton field.
Practice. Register one service with each lifetime, print its instance id in two requests and explain the output.
DI Containers and extensions Recommended
The built-in container, Microsoft.Extensions.DependencyInjection, covers most needs. Third-party DI Containers add features: AutoFac supports modules, decorators and property injection, and integrates through UseServiceProviderFactory. Scrutor adds assembly scanning and decoration to the built-in container, so you can register all classes ending in Service in one line, or wrap a service with a caching decorator.
builder.Services.Scan(s => s.FromAssemblyOf<Program>()
.AddClasses(c => c.Where(t => t.Name.EndsWith("Service")))
.AsImplementedInterfaces().WithScopedLifetime());Start with the built-in container, and move to a replacement only for a feature you actually need. Fewer moving parts make start-up and debugging simpler.
Practice. Use Scrutor to register every service class in an assembly and wrap one with a logging decorator.
Filters and Attributes Recommended
Filters run code at fixed points around an MVC action or a minimal API endpoint. Types in order: authorisation filters, resource filters, action filters, exception filters and result filters. Attributes mark classes and methods, either to configure routing and validation ([HttpGet], [Required], [Authorize]) or to attach a filter.
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext ctx)
{
if (!ctx.ModelState.IsValid) ctx.Result = new BadRequestObjectResult(ctx.ModelState);
}
}Filters can use dependency injection with [ServiceFilter] or [TypeFilter]. Use middleware for concerns across every request, and filters when you need access to action arguments, results or model state. Minimal APIs have endpoint filters for similar jobs.
Practice. Write an action filter that logs the arguments and timing of each action, and apply it globally.
Stage 3 Building APIs
Goal. Build, document and consume a well-designed HTTP API using minimal APIs or controllers.
Minimal APIs Core
Minimal APIs define endpoints with a few lines in Program.cs, with no controller classes. Parameters bind from route, query string, body or services automatically.
var items = app.MapGroup("/items").RequireAuthorization();
items.MapGet("/{id:int}", async (int id, AppDb db) =>
await db.Items.FindAsync(id) is { } item ? Results.Ok(item) : Results.NotFound());
items.MapPost("/", async (ItemDto dto, AppDb db) =>
{
var item = new Item { Name = dto.Name };
db.Items.Add(item);
await db.SaveChangesAsync();
return Results.Created($"/items/{item.Id}", item);
});Route groups share a prefix and policies, endpoint filters add validation and logging, and TypedResults gives strongly typed responses that feed the OpenAPI document. They are quick for small services and are as fast as controllers.
Practice. Build a CRUD API for one entity with a route group, typed results and an endpoint filter that validates input.
MVC and controllers Core
MVC splits an application into models (data and rules), views (output) and controllers (handle requests). In an API there are no views: controllers inherit from ControllerBase, are marked [ApiController], and return data that is serialised to JSON.
[ApiController]
[Route("api/[controller]")]
public class BooksController(IBookService books) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<ActionResult<BookDto>> Get(int id) =>
await books.FindAsync(id) ?? (ActionResult<BookDto>)NotFound();
}[ApiController] adds automatic 400 responses on invalid models and binding rules. Keep controllers thin: map the request, call a service, return a result. Controllers fit teams who like structure, filters, conventions and attribute routing, and they remain the most common style in existing code bases. Know both styles, because interviews ask about them.
Practice. Create a controller with GET, POST and DELETE actions that return correct status codes.
REST Core
REST is a style for designing HTTP APIs around resources. Use nouns in URLs (/orders/42/items), HTTP methods for actions, and status codes for outcomes. Return 201 Created with a Location header for new resources, 204 for success without a body, 404 for a missing resource, 409 for conflicts and 422 or 400 for validation errors, preferably in the Problem Details format that ASP.NET Core supports through AddProblemDetails.
Plan for growth: paginate collection endpoints, allow filtering and sorting through query parameters, version the API from the start (URL segment, header or the Asp.Versioning package), and make unsafe operations idempotent where clients may retry. Never expose database entities directly; return DTOs so that internal changes do not break clients. See API Design Roadmap for the full discussion.
Practice. Design the URLs, methods and status codes for a library API with books, members and loans before coding it.
Scalar and API documentation Recommended
ASP.NET Core generates an OpenAPI document that describes your endpoints through the built-in AddOpenApi() and MapOpenApi(). Earlier templates used Swashbuckle and Swagger UI; the newer templates dropped it, and projects often use Scalar to render the document as a friendly, interactive reference with a built-in request tester.
builder.Services.AddOpenApi();
app.MapOpenApi();
app.MapScalarApiReference(); // package: Scalar.AspNetCoreGood documentation needs help from you: ProducesResponseType or typed results, summaries, example values and described security schemes.
Practice. Add OpenAPI and Scalar to an API, describe two endpoints fully and generate a client from the document.
Object Mapping Recommended
Object Mapping copies data between shapes, usually entities to DTOs and back. Three approaches exist.
- Manual Mapping: write a method or extension,
book.ToDto(). It is explicit, debuggable, fast and has no dependency. For many projects it is the best choice. - AutoMapper uses configuration profiles and reflection to map by convention. It saves typing but hides mistakes until runtime, and it became a commercial product, so check licensing.
- Mapperly generates the mapping code at compile time using source generators, giving speed and compile-time errors with little code.
Whichever you choose, never map inside a loop that triggers database calls, and use LINQ Select projections so that EF Core fetches only the columns the DTO needs. Mapping should not hold business logic.
Practice. Implement the same mapping manually and with Mapperly, then compare the generated code and the speed.
FluentValidation Recommended
FluentValidation moves input rules into small validator classes with a readable fluent syntax, rather than scattering data annotations across models.
public class CreateBookValidator : AbstractValidator<CreateBook>
{
public CreateBookValidator()
{
RuleFor(x => x.Title).NotEmpty().MaximumLength(200);
RuleFor(x => x.Price).GreaterThan(0);
RuleFor(x => x.Isbn).Matches(@"^\d{13}$").When(x => x.Isbn != null);
}
}Rules can be conditional, reuse child validators, run asynchronous checks against a database and carry custom messages. Register validators with DI and call them in a filter or endpoint filter, returning a problem details response. Validate at the API boundary and again at domain level when invariants matter; client-side checks are never enough.
Practice. Write a validator for a registration request with an asynchronous uniqueness rule and return its errors as problem details.
API Clients and Communication Core
Services call other services over HTTP. Use HttpClient created through IHttpClientFactory, never new HttpClient() in a loop, because that exhausts sockets and ignores DNS changes. Typed clients wrap one remote API in a class with a clear interface.
builder.Services.AddHttpClient<WeatherClient>(c =>
c.BaseAddress = new Uri("https://weather.example.test/"))
.AddStandardResilienceHandler();Polly, now integrated through Microsoft.Extensions.Http.Resilience, adds retries with backoff, circuit breakers, timeouts and rate limiting. Retry only idempotent requests. Always set a timeout and pass a CancellationToken. Other communication styles such as gRPC and messaging are covered later in this roadmap; Refit can generate typed clients from an interface, and Kiota can generate them from OpenAPI documents.
Practice. Create a typed client for a public API with a standard resilience handler and test a failing response.
Stage 4 Web UI and Real-Time Communication
Goal. Build server-rendered and interactive pages and choose the right real-time protocol.
Razor Pages and Razor Recommended
Razor is a template syntax that mixes HTML with C# using the @ symbol, in .cshtml files. Razor Pages organise a site by page: each page has a view and a PageModel class with handlers named OnGet and OnPost, which suits form-based sites more simply than MVC.
@page
@model ContactModel
<form method="post">
<input asp-for="Input.Email" />
<span asp-validation-for="Input.Email"></span>
<button>Send</button>
</form>Tag helpers such as asp-for and asp-action connect markup to models and routes, and anti-forgery tokens are added to forms automatically. Razor output is HTML-encoded by default, which prevents most cross-site scripting; avoid Html.Raw with user data.
Practice. Build a contact form with Razor Pages, server-side validation and a thank-you page.
Blazor and Razor Components Recommended
Blazor builds interactive web interfaces in C# instead of JavaScript. A Razor Component is a .razor file that combines markup and code and can be nested and given parameters.
<button @onclick="Increment">Clicked @count times</button>
@code {
private int count;
private void Increment() => count++;
}Hosting models: Blazor Server runs components on the server and updates the browser over a connection; WebAssembly runs them in the browser; and since .NET 8 a single app can mix static server rendering, interactive server and interactive WebAssembly per component. Streaming rendering and enhanced navigation reduce page flicker. Blazor also powers hybrid desktop and mobile apps through .NET MAUI. Choose it when the team is strong in C# and wants to share models and validation with the server.
Practice. Build a small interactive page with a component that has a parameter, an event and a call to your API.
Template Engines Optional
Razor is the default view engine, but other Template Engines can render text for emails, reports and code generation, often with user-editable templates where Razor is unsuitable because it executes arbitrary C#.
- Fluid implements the Liquid template language, which is safe for end-user templates, as it limits what a template can do.
- Scriban is a fast scripting language and template engine with its own syntax and a strict sandboxing model.
var template = new FluidParser().Parse("Hello {{ user.name }}, you owe {{ total | round: 2 }}");
var html = await template.RenderAsync(new TemplateContext(model));Use a safe engine whenever template authors are not developers. Escape output for the target format, pre-parse templates once and reuse them, and test that malicious or broken templates cannot crash the application.
Practice. Render an invoice email from a Liquid template using Fluid, with a currency filter.
Real-Time Communication, SignalR Core and WebSockets Recommended
Real-time features push data to clients without polling. WebSockets give one persistent, two-way connection between browser and server. SignalR Core is the higher-level library on top: it chooses the best transport (WebSockets, then server-sent events or long polling), manages connections, groups and users, and calls methods on clients and servers.
public class ChatHub : Hub
{
public Task Send(string room, string text) =>
Clients.Group(room).SendAsync("message", Context.User?.Identity?.Name, text);
}To scale to several servers, SignalR needs a backplane such as Redis, or a managed service such as Azure SignalR, so messages reach clients connected to other instances. Authenticate connections, limit message size and handle reconnection on the client. Use WebSockets directly only when you need a custom protocol.
Practice. Build a chat room with groups using SignalR and test it with two browser tabs.
gRPC Optional
gRPC is a binary remote procedure call framework using HTTP/2 and Protocol Buffers. You describe services and messages in a .proto file, and tooling generates strongly typed C# client and server code.
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }Benefits: compact messages, speed, streaming in both directions and contracts shared between languages. Costs: browsers cannot call it directly without gRPC-Web, and debugging is harder than with JSON. Use it for internal service-to-service calls, particularly where latency or streaming matters, and REST for public APIs. Add deadlines to every call and use interceptors for logging and authentication.
Practice. Define a proto contract for a simple service and call it from a console client with a deadline.
Stage 5 Relational Databases and SQL
Goal. Design relational tables, write SQL and pick a database engine for a project.
Database Fundamentals and Databases Core
Database Fundamentals start with why a database exists: your application restarts and scales, but data must last and many users must change it at once without corrupting it. Databases give durable storage, indexes for fast lookup, concurrent access and transactions.
The key properties are ACID. Atomicity means a transaction applies entirely or not at all. Consistency keeps data valid against rules. Isolation stops concurrent transactions from seeing each other's half-finished work. Durability means committed data survives a crash.
Two big families exist. Relational Databases keep data in tables with a schema and use SQL. NoSQL Databases trade some structure or consistency for flexibility and scale in a particular shape of data. Most business applications should start relational, a position that SQL Roadmap explains in depth.
Practice. Write a paragraph explaining ACID using a bank transfer as the example.
SQL Basics Core
SQL Basics you need daily: SELECT with WHERE, ORDER BY and LIMIT or TOP; INSERT, UPDATE, DELETE; joins (INNER, LEFT) to combine tables; GROUP BY with COUNT, SUM and AVG, filtered with HAVING; subqueries and common table expressions.
SELECT c.Name, COUNT(o.Id) AS Orders, SUM(o.Total) AS Revenue
FROM Customers c
LEFT JOIN Orders o ON o.CustomerId = c.Id
GROUP BY c.Name
HAVING SUM(o.Total) > 1000
ORDER BY Revenue DESC;Understand what an index does and why it speeds WHERE and JOIN columns but slows writes. Read an execution plan to see whether a query scans a table or uses an index. Even with an ORM, you must be able to read the SQL it produces. Practise on the SQL practice workspace.
Practice. Write queries for top customers, products never ordered and monthly revenue against a sample shop schema.
Relational engines: SQL Server, PostgreSQL, MySQL and MariaDB Core
SQL Server is Microsoft's database and the traditional partner of .NET, with excellent tooling, T-SQL, and strong integration with Azure. It runs on Linux and in containers.
PostgreSQL is a free, standards-focused engine with rich types such as JSON and arrays, strong extensions and a very good reputation; the Npgsql provider makes it a first-class choice for EF Core. MySQL and MariaDB (a community fork of MySQL) are widely used in web hosting; providers such as Pomelo and the official Oracle one connect them.
Choosing: take the one your team can operate, back up and monitor well. SQL Server suits organisations on Microsoft licensing and tools; PostgreSQL suits cost and portability. Dialects differ in paging, upper and lower case sensitivity and date types, so test against the real engine, ideally in a container, not a lightweight substitute.
Practice. Run SQL Server and PostgreSQL in containers and run the same EF Core migration against both.
Database design basics and Constraints Core
Good design starts by finding entities and relationships, then putting each fact in one place. Normalisation removes duplication: each non-key column depends on the key, the whole key and nothing but the key. Denormalise deliberately, for reads, never by accident.
Constraints make the database enforce rules even when code has a bug. Use primary keys, foreign keys (with a decision on delete behaviour), UNIQUE, NOT NULL, CHECK and defaults. Choose key types carefully: sequential integers are compact, GUIDs are safe to generate anywhere, and random GUIDs as clustered keys fragment indexes in SQL Server, where sequential GUIDs or version 7 UUIDs help.
Practice. Design a schema for an online shop with five tables and list every constraint it needs.
Stored Procedures and Triggers Optional
A stored procedure is a named block of SQL kept in the database and called by name. Benefits: logic close to the data, fewer round trips, permission control by granting access to the procedure only. Costs: harder version control, testing and portability, and logic spread across two places. Many teams use them for reporting and bulk operations, and keep ordinary business rules in C#.
A trigger runs automatically on insert, update or delete. Uses include audit trails and keeping derived data in step. Triggers are invisible to application code, which makes behaviour surprising, so document them and keep them small. EF Core can call procedures with FromSql and ExecuteSql, and includes migrations for them through raw SQL in a migration. Always pass parameters, never string concatenation.
Practice. Write a stored procedure and an audit trigger, then call the procedure from EF Core with parameters.
ORM Core
An object-relational mapper translates between tables and C# objects so that you write LINQ and the library writes SQL. It tracks changes, handles relationships, and creates migrations. Benefits: less repetitive code, strong typing, easy refactoring. Risks: hidden queries, inefficient SQL when used carelessly, and leaky abstractions for complex reports.
The ORM options in .NET are Entity Framework Core (the default, full-featured), NHibernate (older, very configurable, popular in legacy systems), and micro-ORMs such as Dapper and RepoDB, which map query results to objects but leave the SQL to you. Choose EF Core by default, use Dapper for hot spots and reports, and mix them in one project through the same connection. See the data stage next for how EF Core works in detail.
Practice. Write the same query with EF Core and Dapper, and compare the code and generated SQL.
Entity Framework Core and Framework Basics Core
Entity Framework Core Framework Basics: a DbContext represents a session with the database, DbSet<T> properties represent tables, and entity classes are plain C# objects.
public class AppDb(DbContextOptions<AppDb> options) : DbContext(options)
{
public DbSet<Book> Books => Set<Book>();
protected override void OnModelCreating(ModelBuilder b) =>
b.Entity<Book>().Property(x => x.Title).HasMaxLength(200).IsRequired();
}Register it with AddDbContext, which makes it scoped per request. Query with LINQ (db.Books.Where(...).ToListAsync()), add entities with Add, and persist with SaveChangesAsync, which writes all pending changes in one transaction. Configure the model with the fluent API or attributes; the fluent API is more powerful. Use AsNoTracking() for read-only queries to save memory and time.
Practice. Create a DbContext with two related entities, save data and query it back with LINQ.
Stage 6 Entity Framework Core in Depth
Goal. Evolve schemas with migrations, control how related data loads and use alternatives when EF Core is not the right tool.
Code First and Migrations in Entity Framework Core Core
In the Code First approach you write entity classes, and migrations create and evolve the database to match. dotnet ef migrations add AddBookIsbn compares the model with the last snapshot and creates a file with Up and Down methods; dotnet ef database update applies it, and dotnet ef migrations script creates SQL for a deployment pipeline.
Rules of thumb: commit migrations with the code, never edit an applied migration, review the generated SQL for data loss (renaming a column can appear as drop and add), and apply migrations as a separate release step rather than at application start-up in multi-instance deployments. Seed reference data with HasData or a migration. A Database First alternative, dotnet ef dbcontext scaffold, generates classes from an existing database.
Practice. Add a column with a default through a migration, review the SQL and generate a script for production.
Change Tracker API Recommended
The change tracker is the part of the DbContext that remembers loaded entities and their original values. When you call SaveChanges, it compares current values with the snapshot and writes only the differences. Each entity has a state: Detached, Unchanged, Added, Modified or Deleted.
You can inspect it with db.ChangeTracker.Entries() and db.Entry(entity).State, or use it to create an audit log by overriding SaveChangesAsync and recording each modified entity. Attach and setting state helps with disconnected scenarios, such as updating from a DTO. Tracking has a cost: for read-only queries use AsNoTracking, and keep contexts short-lived, since a long-lived one accumulates entities and memory. Never share a DbContext between threads.
Practice. Override SaveChangesAsync to write created and modified timestamps automatically for every entity.
Lazy, Eager, and Explicit Loading Core
Related data can load in three ways. Eager loading fetches it with the main query through Include and ThenInclude. Explicit loading fetches it later on request with db.Entry(book).Collection(b => b.Reviews).Load(). Lazy loading fetches automatically when a navigation property is touched; it needs proxies, and UseLazyLoadingProxies.
Lazy loading is convenient and dangerous: a loop that reads order.Customer.Name for a thousand orders creates a thousand queries, the N+1 problem. Prefer eager loading for data you know you need, or a Select projection to a DTO that fetches only specific columns. Many Include calls over collections cause a large cartesian result; AsSplitQuery() can run them as separate queries. Turn on SQL logging in development to see what runs.
Practice. Fix an N+1 problem in an orders list by using Include, then by using a Select projection, and compare the SQL.
Dapper, RepoDB and NHibernate Optional
Dapper is a tiny micro-ORM: you write SQL and it maps rows to objects, quickly and with no change tracking.
var orders = await conn.QueryAsync<OrderRow>(
"SELECT Id, Total FROM Orders WHERE CustomerId = @id", new { id });RepoDB is a hybrid ORM that sits between Dapper and EF: it offers a repository-style API, bulk operations and caching, but still lets you write SQL. NHibernate is the .NET port of Hibernate, mature and highly configurable, with a second-level cache and rich mapping options, and is common in older enterprise systems.
Use Dapper for reporting, bulk reads or performance hot spots next to EF Core, always with parameters. Choose one main data access approach per application so that developers do not have to learn three.
Practice. Write a reporting query in Dapper that joins three tables and returns a DTO list.
Entity Framework 2nd Level Cache and query performance Optional
EF Core has a first-level cache, the change tracker identity map, but no built-in Entity Framework 2nd Level Cache that is shared across contexts. Third-party packages add a second-level cache by intercepting queries and storing results in memory or Redis, with invalidation when tables change.
Often better performance comes from basics: select only needed columns, use AsNoTracking, add indexes, page results with keyset pagination, avoid loading whole tables, use compiled queries on hot paths, and batch writes with ExecuteUpdate and ExecuteDelete.
Measure with logging and a profiler before changing anything. If caching is still needed, cache at application level with IMemoryCache or IDistributedCache, where you control expiry.
Practice. Compare the time of a read-heavy query before and after adding AsNoTracking and an index.
Cloud Databases in ASP.NET Core Recommended
Cloud Databases are managed services where the provider handles patching, backups, replication and scaling. Examples include Azure SQL Database, Amazon RDS and Aurora, Google Cloud SQL, and managed PostgreSQL. You pay for convenience and lose some low-level control.
Practical points for ASP.NET Core: keep the connection string in configuration from a secret store, prefer managed identity over passwords where the platform supports it, enable retries for transient faults with EnableRetryOnFailure(), keep the connection pool sizes sensible, and put the app and database in the same region to reduce latency.
Practice. Create a managed database on a free tier, connect your API with a secret from configuration and enable retry on failure.
Stage 7 NoSQL, Search, Caching and Query APIs
Goal. Choose a non-relational store where it fits, add search and caching, and offer flexible query APIs.
NoSQL Databases Recommended
NoSQL Databases cover several models, each built for a shape of data or access pattern.
- Document stores keep JSON-like documents: MongoDB, Cosmos DB, CouchDB. LiteDB is an embedded document database for a single file inside the application, useful for desktop and small tools. Marten turns PostgreSQL into a document database and event store for .NET.
- Key-value and wide-column stores scale writes across many machines: DynamoDB on AWS, Cassandra.
- Caches such as Redis hold simple values in memory.
Typical reasons to choose them are flexible schemas, huge write volumes, or global distribution. Typical costs are weaker joins and transactions, and designing data around queries. See MongoDB Roadmap and Redis Roadmap for more. Start relational unless a clear need exists.
Practice. Store the same product catalogue in a relational table and in a document database and list the trade-offs you noticed.
MongoDB, Cosmos DB, DynamoDB, Cassandra and CouchDB in .NET Optional
Each has an official or common .NET integration.
- MongoDB: the
MongoDB.Driverpackage with LINQ support; design documents so data read together is stored together. - Cosmos DB: Azure's globally distributed database with several APIs; you choose a partition key carefully and pay in request units, so cheap queries stay inside one partition.
- DynamoDB: AWS key-value and document store, accessed through the AWS SDK; access patterns must be known up front, as design is built around partition and sort keys.
- Cassandra: a wide-column database for very high write volumes, reached through the DataStax driver, with queries modelled by table.
- CouchDB: a document database with an HTTP API and replication, often used where offline sync matters.
Practice. Insert and query documents using the MongoDB driver in a small API, and add an index for the main query.
Search Engines Recommended
A database finds rows by exact values; a search engine finds documents by relevance, handling words, typos, synonyms, filters and facets. Search Engines build an inverted index, which maps each word to the documents containing it.
Elasticsearch is the most used, with the NEST history replaced by the official Elastic.Clients.Elasticsearch client; Solr, built on the same Lucene library, is older with a strong enterprise base; Sphinx is a lightweight engine often paired with MySQL. Azure AI Search and others are managed options.
Treat the search index as a copy of data: the database stays the source of truth, and updates reach the index through events or a scheduled job. Plan for the delay, reindexing and mapping changes. Do not use a search engine as the primary store.
Practice. Index a few hundred products into Elasticsearch and implement search with a filter and highlighting.
Caching Core
Caching stores computed results so that the next request is quick. In ASP.NET Core there are several layers. Response caching and output caching store full HTTP responses. Memory Cache (IMemoryCache) holds objects inside one process, which is fast but not shared. Distributed Cache (IDistributedCache) shares data across instances through a store: Redis Distributed Cache is the common choice, with SQL Server as another, and Memcached as a simple in-memory alternative.
var value = await cache.GetOrCreateAsync(key, e =>
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return LoadFromDatabaseAsync();
});HybridCache in recent versions combines both levels and prevents many callers from rebuilding the same value at once. Decide expiry and invalidation first, never cache per-user data under a shared key, and watch hit rates.
Practice. Cache an expensive query for five minutes in memory, then move it to Redis and run two API instances.
GraphQL, GraphQL .NET and HotChocolate Optional
GraphQL is a query language where the client asks for exactly the fields it needs from a single endpoint, with a typed schema. It helps when many clients, such as web and mobile, need different shapes of data and avoids over-fetching and several round trips.
In .NET there are two main servers. GraphQL .NET is a lower-level library where you define types and resolvers. HotChocolate is a full-featured, code-first server with filtering, sorting, projections on EF Core, subscriptions and good tooling.
Hazards include the N+1 problem in resolvers, which DataLoader batching solves, expensive queries that need depth and cost limits, and harder HTTP caching. REST is simpler for most services. See GraphQL Roadmap for the language itself.
Practice. Expose products and categories through HotChocolate with filtering and compare a nested query with the REST equivalent.
OData and Gridify Optional
OData is a standard that adds query options to REST endpoints: ?$filter=price lt 20&$orderby=name&$top=10&$select=name,price&$expand=category. In ASP.NET Core the Microsoft.AspNetCore.OData package translates these into LINQ so that clients can filter and shape results without new endpoints.
The cost is a very broad query surface: clients can ask for heavy operations, so restrict which properties may be filtered, set maximum page sizes and test the SQL it generates.
Gridify is a lighter alternative that parses simple filtering, sorting and paging strings, such as name=*phone, price<20, into LINQ expressions with far less setup. Either choice needs a limit on page size, an allow-list of fields and an index for each commonly filtered column.
Practice. Add OData-style filtering and paging to a products endpoint and verify the SQL that is generated.
Stage 8 Background Work, Messaging and Distributed Systems
Goal. Run work outside requests, connect services with messages and understand the options for building distributed applications.
Task Scheduling and background jobs Core
Some work must not run inside a request: sending email, generating reports, nightly clean-up. The Native Background Service is the built-in answer: derive from BackgroundService, override ExecuteAsync, register with AddHostedService, and use a PeriodicTimer for a loop. It lives and dies with the process and is not persisted.
For Task Scheduling with persistence, retries and a dashboard, use a library. Hangfire stores jobs in a database and offers fire-and-forget, delayed and recurring jobs with a web dashboard. Quartz is a mature scheduler with cron triggers and clustering. Coravel is a small, simple library for scheduling and queuing in smaller applications.
Jobs must be idempotent, since they can run twice, and must use scoped services through a new DI scope. Run only one scheduler per job in a multi-instance setup.
Practice. Create a recurring Hangfire job that runs every minute, and make it safe to run twice.
Message brokers Recommended
A message broker sits between services and holds messages until a consumer processes them. It decouples senders from receivers in time and availability, smooths spikes and allows retries.
- RabbitMQ is a flexible broker with exchanges, queues and routing; ActiveMQ is a long-standing Java-based broker with similar uses.
- Kafka is a distributed log designed for very high throughput and replay of events, with ordering per partition.
- Azure Service Bus is Microsoft's managed broker with queues, topics, sessions and dead-letter queues.
- NetMQ is a .NET implementation of ZeroMQ, for brokerless messaging patterns inside a system.
Choose a queue (each message handled once) for commands, and topics or logs for events many services observe.
Practice. Send a message from an API to a worker through RabbitMQ and make the consumer ignore duplicates.
Messaging libraries: MassTransit, NServiceBus, EasyNetQ and MediatR Recommended
Libraries hide broker details and give you consumers, retries, sagas and observability.
- MassTransit supports RabbitMQ, Azure Service Bus and others behind one API, with retries, outbox, sagas and test harness.
- NServiceBus is a commercial framework with similar abilities, strong tooling and long-running process support.
- EasyNetQ is a simple client library for RabbitMQ.
- MediatR is not a broker: it dispatches requests and notifications between handlers inside one process, keeping controllers thin. It is useful but optional; a direct service call is often simpler, and the library is now commercially licensed.
The outbox pattern is the key reliability idea: write the message to the same database transaction as the data change, and publish it afterwards, so you never save data without sending the message or the reverse.
Practice. Publish an event with MassTransit using the in-memory transport and consume it in a second service.
Microservices Recommended
Microservices split a system into small services, each owning its data and deployed on its own. Benefits: independent deployment and scaling, team autonomy, technology choice. Costs: network failures, distributed transactions, harder debugging, many deployments to operate. A well-structured monolith is the better start for most teams, and a modular monolith keeps options open.
Core practices: draw boundaries around business capabilities, never share a database between services, communicate through APIs and events, add timeouts and retries, trace requests with OpenTelemetry and correlation ids, and keep health checks and logs consistent. Use sagas, not distributed transactions, for processes that cross services. See System Design Roadmap and Software Design and Architecture Roadmap for fuller coverage.
Practice. Split a small shop into an orders service and an inventory service, connected by an event.
Gateways and the .NET service platform Optional
An API gateway is a single entry point that routes, authenticates and rate-limits traffic to many services. YARP, Microsoft's reverse proxy library, is configured from JSON or code and fits naturally into an ASP.NET Core application. Ocelot is an older, popular gateway library with a configuration file for routes and aggregation.
Further tools for building distributed systems: .NET Aspire is an opinionated stack for local development and observability, which describes services, databases and containers in code, and wires up telemetry and service discovery. Dapr is a portable runtime that offers building blocks, such as state, pub/sub and secrets, through sidecars. Orleans is a virtual actor framework for stateful, scalable services. Steeltoe brings cloud-native patterns from Spring to .NET.
Practice. Put YARP in front of two APIs, route by path and add rate limiting on one route.
Log frameworks Core
Logging in ASP.NET Core goes through ILogger<T>, which has levels (Trace to Critical), categories and message templates. Log Frameworks plug in underneath to control where output goes.
- Serilog is the common choice, with structured logging and many sinks (console, files, Seq, Elasticsearch, cloud services).
- NLog is a flexible alternative with configuration files and targets.
builder.Host.UseSerilog((ctx, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration).Enrich.FromLogContext());
logger.LogInformation("Order {OrderId} paid by {UserId}", order.Id, userId);Use message templates with named placeholders, not string interpolation, so that values stay searchable fields. Add a correlation id, never log secrets or personal data, and set levels per environment.
Practice. Configure Serilog with a console sink and a file sink, and log an order event with structured properties.
Stage 9 Testing and Code Quality
Goal. Test code at several levels with realistic dependencies and keep quality visible.
Testing Core
Testing lets you change code safely. Unit tests check one class with its dependencies replaced; they are quick and numerous. Integration tests run parts together, such as an endpoint with a real database. End-to-end tests drive the full application through a browser and are few and slow. Aim for a pyramid, with many unit tests at the base.
Use the Arrange, Act, Assert shape with one behaviour per test. Tests must be independent and deterministic: inject a TimeProvider rather than calling DateTime.Now, and avoid ordering dependencies.
Practice. Write down the unit, integration and end-to-end tests you would write for a checkout endpoint.
Test frameworks: xUnit, NUnit and MSTest Core
Three frameworks run .NET tests. xUnit is the modern default, used by the .NET team itself: [Fact] for one case, [Theory] with [InlineData] for many, a fresh class instance per test, and constructor injection for set-up. NUnit is long-established with a rich set of attributes and constraints, like [TestCase]. MSTest is Microsoft's framework, integrated with Visual Studio and improved a good deal in recent years.
public class PriceTests
{
[Theory]
[InlineData(100, 10, 90)]
[InlineData(50, 0, 50)]
public void Discount_IsApplied(decimal price, decimal pct, decimal expected) =>
Assert.Equal(expected, Pricing.Apply(price, pct));
}All three work with dotnet test. Pick one per solution, and learn its parallelism rules, because shared static state breaks parallel tests.
Practice. Write a theory with five data rows for a pricing function and run it with dotnet test.
Mocking and assertion libraries Recommended
Mocks replace collaborators so a unit test stays isolated.
- Moq is the best known, with
SetupandVerify. - NSubstitute has a more natural syntax:
repo.Find(1).Returns(book). - FakeItEasy offers readable
A.CallTo(() => ...)calls.
Prefer fakes or in-memory implementations to heavy mocking when behaviour matters, and do not verify every call, since tests tied to the implementation break during refactoring. Shouldly gives readable assertions and clear failure messages: result.ShouldBe(5). FluentAssertions is a similar popular library with a changed licence in recent versions.
Mock what you own, such as interfaces for your services; do not mock HttpClient internals, use a fake handler instead.
Practice. Test a service with a mocked repository using NSubstitute and assertions written with Shouldly.
Integration testing: WebApplicationFactory, Test Containers and Respawn Core
WebApplicationFactory<Program> starts your application in memory and gives you an HttpClient that talks to it without a network, so you test routing, middleware, serialisation and DI together.
public class OrdersApiTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Get_Unknown_Returns404() =>
Assert.Equal(HttpStatusCode.NotFound, (await factory.CreateClient().GetAsync("/orders/999")).StatusCode);
}Replace services in the test with WithWebHostBuilder. For the database, Test Containers starts a real SQL Server or PostgreSQL in Docker for the run, which finds problems an in-memory fake would hide. Respawn resets data between tests quickly by deleting rows in the right order. Real dependencies make tests a little slower and a lot more trustworthy.
Practice. Test an endpoint end to end using WebApplicationFactory and a PostgreSQL container reset with Respawn.
End-to-end and behaviour tests Optional
End-to-end tests run a real browser against a deployed or locally started app. Playwright for .NET drives Chromium, Firefox and WebKit from C#, with auto-waiting and tracing. Cypress, written in JavaScript, has an interactive runner and is popular among front-end teams. Puppeteer controls Chromium and also suits scraping and PDF generation, through the PuppeteerSharp port in .NET.
Behaviour-driven tools describe scenarios in business words. LightBDD writes them in C# with a fluent style and reports, and RecNRoll records and replays interactions against web services for regression tests. Keep the number of end-to-end tests low, cover the key journeys such as sign-in and checkout, and make them independent of one another, since they are the slowest and most fragile layer.
Practice. Write one Playwright test that signs in to a demo app and checks a protected page.
Stage 10 Delivery, Containers and Operations
Goal. Automate builds and releases, package a service in a container and run it on an orchestrator.
CI/CD Core
Continuous integration builds and tests every change automatically; continuous delivery keeps the main branch always releasable, and deployment pipelines push it to environments. A basic pipeline: restore, build, test, analyse, publish, build an image, push to a registry, deploy to staging, run smoke tests, then promote.
Tools differ in syntax but not in ideas: GitHub Actions uses YAML workflows in the repository, GitLab CI/CD uses .gitlab-ci.yml, Azure Pipelines is the natural fit for Azure DevOps, and CircleCI is a hosted service. Nuke lets you write the build itself in C# so it runs the same locally and in CI.
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '8.0.x' }
- run: dotnet test --configuration Release
Practice. Create a GitHub Actions workflow that builds, tests and publishes an artifact on every pull request.
Docker and Kubernetes Recommended
A container packages the application with its runtime so that it runs the same everywhere. Microsoft publishes official base images: use the smaller runtime image for the final stage and the SDK image only to build, in a multi-stage Dockerfile.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:8.0
COPY --from=build /app /app
ENTRYPOINT ["dotnet", "/app/Shop.Api.dll"]Run as a non-root user, listen on a configurable port, and read configuration from environment variables. Kubernetes runs many containers: Deployments keep replicas alive, Services provide stable addresses, probes use your health check endpoints, and ConfigMaps and Secrets provide settings. Details are in Docker Roadmap and Kubernetes Roadmap.
Practice. Containerise an API with a multi-stage Dockerfile and run it with a PostgreSQL container using Compose.
Health checks and observability Recommended
In production you must be able to see what the system is doing. ASP.NET Core has health checks: register checks for the database and dependencies with AddHealthChecks(), expose /health/live and /health/ready, and point the orchestrator at them. Liveness says the process is running; readiness says it can take traffic.
Observability rests on three signals: logs, metrics and traces. OpenTelemetry is the vendor-neutral standard, with ASP.NET Core, HttpClient and EF Core instrumentation packages, exporting to systems such as Prometheus, Grafana, Jaeger or a cloud service. Track request rate, error rate and latency percentiles, and alert on symptoms users feel. Add a correlation id to every log line and propagate it across service calls.
Practice. Add liveness and readiness endpoints and export request metrics and traces through OpenTelemetry.
Securing an ASP.NET Core application Core
Authentication proves identity; authorisation decides access. ASP.NET Core supports cookie authentication for web apps, JWT bearer tokens for APIs, and OpenID Connect for sign-in through an identity provider. ASP.NET Core Identity manages users, passwords and roles in a database. Prefer a dedicated identity provider such as Microsoft Entra ID or a similar service over writing token issuing yourself.
Use policy-based authorisation ([Authorize(Policy = "CanEdit")]) and resource-based checks for ownership. Further protections: HTTPS and HSTS, anti-forgery tokens on forms, CORS limited to named origins, input validation, parameterised queries, rate limiting through the built-in middleware, and secrets from a vault.
Practice. Protect an API with JWT bearer authentication and a policy that only lets owners edit their records.
Projects to build
Build these in order. Each one should end up in a public repository with a short README that explains your decisions.
- Console expense trackerBeginner
Build a console program that stores expenses in a JSON file and reports totals by category using LINQ. Use records, collections and a few unit tests.
C# basics · LINQ · Collections · xUnit
- Library catalogue APIBeginner
Create a CRUD API for books and authors with minimal APIs, EF Core and SQLite. Return correct status codes, validate input and publish an OpenAPI document with Scalar.
Minimal APIs · EF Core · Validation · OpenAPI
- Blog with accountsIntermediate
Build a Razor Pages or Blazor site with registration, login, posts and comments. Use ASP.NET Core Identity, authorisation policies and a PostgreSQL database.
Razor Pages · Identity · Policies · Migrations
- Order management serviceIntermediate
Create an orders API with FluentValidation, a typed HttpClient to a payment stub, caching of product data in Redis and integration tests that use WebApplicationFactory and a database container.
Dependency injection · Caching · Integration tests · Resilience
- Event-driven shopAdvanced
Split orders and inventory into two services that exchange events through RabbitMQ with MassTransit and an outbox. Add a YARP gateway, Serilog logging and OpenTelemetry traces.
Messaging · Outbox · YARP · Observability
- Real-time dashboard in the cloudAdvanced
Build a dashboard that receives live updates through SignalR with a Redis backplane, runs scheduled jobs with Hangfire and deploys through a GitHub Actions pipeline to containers.
SignalR · Hangfire · CI/CD · Docker
Interview questions
Try to answer each question out loud before you open the answer.
How does the ASP.NET Core request pipeline work?Junior
A request arrives at Kestrel, which passes it through a chain of middleware. Each component can do work before calling the next one and after it returns, or end the request itself. Common components are exception handling, HTTPS redirection, static files, routing, CORS, authentication, authorisation and then the endpoint, which is a minimal API handler, controller action or Razor page. The response travels back through the same components in reverse order. Registration order defines the order of execution, so mistakes there cause subtle bugs.
What are the dependency injection lifetimes and when do you use each?Junior
Transient creates a new instance on every request for the service, suited to light stateless services. Scoped creates one per HTTP request, used for DbContext and per-request state. Singleton creates one for the life of the application, for expensive shared objects such as configuration or caches, and it must be thread-safe. A singleton must not depend on a scoped service, because it would hold on to the first request's instance. I use IServiceScopeFactory when a background service needs scoped work.
What is the difference between minimal APIs and controllers?Junior
Both sit on the same routing, binding, filters and DI. Minimal APIs declare endpoints as lambdas or methods in a few lines, with route groups and endpoint filters, and have lower ceremony. Controllers are classes with attribute routing, model binding conventions and the full filter pipeline, which suits large teams and existing codebases. Performance is similar. I use minimal APIs for small services and organise handlers into groups; for large APIs I choose whichever convention the team already follows.
Explain async and await in C#.Mid
An async method returns a Task and can pause at an await without blocking the thread: the thread returns to the pool, and the method resumes when the awaited operation completes. This lets a server handle many requests with few threads, as long as the I/O is truly asynchronous. I avoid .Result and .Wait(), which can deadlock or waste threads, I pass CancellationToken through the call chain, and I use Task.WhenAll for independent operations.
How does Entity Framework Core track changes, and when would you turn tracking off?Mid
The DbContext keeps a snapshot of each loaded entity and its state. On SaveChanges it compares current values with the snapshot, and generates INSERT, UPDATE or DELETE statements for the differences in one transaction. Tracking costs memory and time. For read-only queries, such as listing pages and reports, I use AsNoTracking, or project to DTOs with Select. I also keep DbContext instances short-lived, one per request, and never share one across threads.
What is the N+1 problem in EF Core and how do you fix it?Mid
It happens when code loads a list with one query and then triggers one more query for each item's related data, often through lazy loading or accessing a navigation property inside a loop. For 1,000 orders that is 1,001 queries. Fixes: Include for the related data you need, a Select projection to a DTO that fetches just the required columns, or AsSplitQuery for collections. I turn on SQL logging in development and add tests that assert the number of queries on critical paths.
How do you handle configuration and secrets?Mid
Configuration comes from layered providers: appsettings.json, environment-specific files, user secrets in development, environment variables and command line. Later sources override earlier ones. I bind sections to typed options classes and validate them on start-up. Secrets never go in the repository: user secrets locally, and a vault such as Key Vault or the platform's secret store in production, reached with a managed identity if possible. The same container image runs in every environment with different configuration.
How would you design validation in an API?Mid
I validate at the boundary: data annotations for simple rules, or FluentValidation for richer, testable ones, run in a filter or endpoint filter so every endpoint behaves the same. Failures return 400 or 422 with a Problem Details body listing each field. Business invariants are also enforced in the domain layer, and database constraints, such as unique and foreign keys, are a last line of defence. I never rely on client-side checks, and I return DTOs rather than entities.
When would you use Dapper instead of EF Core?Mid
For read-heavy reports, complex hand-tuned SQL, bulk reads, or hot paths where the overhead of change tracking and expression translation matters. Dapper maps query results to objects and leaves SQL to me, so there is little hidden behaviour. I still use EF Core for most writes and for migrations, and the two share the same connection. Parameters are always used to avoid injection. I would not mix several data access styles without a clear reason.
How do you make calls to other services reliable?Senior
I use IHttpClientFactory with typed clients and the standard resilience handler, built on Polly: a timeout per attempt, retries with exponential backoff and jitter for idempotent requests only, a circuit breaker and a total timeout. CancellationTokens flow through. I log failures with correlation ids, never retry blindly on non-idempotent POSTs without an idempotency key, and apply bulkheads or rate limits so one slow dependency cannot exhaust the thread pool or connections.
What is the outbox pattern and why is it needed?Senior
A service often needs to change its database and publish a message. These are two systems, so doing both can fail halfway, leaving data saved without an event, or the reverse. With the outbox pattern the service writes the message to an outbox table in the same transaction as the data change. A separate publisher reads the table and sends messages to the broker, retrying until success. Delivery is at least once, so consumers must be idempotent. MassTransit and NServiceBus provide it.
Monolith or microservices: how would you decide?Senior
I begin with a modular monolith: one deployable with clear module boundaries and separate data per module. It is simpler to build, test, deploy and debug. I move a module out into a service when there is a concrete reason, such as independent scaling, a different release cadence, team ownership or fault isolation. Microservices bring network failure, eventual consistency, and operational cost, so I only pay that when the benefit is clear.
How do you test an ASP.NET Core API?Mid
Unit tests for domain logic and services, with mocks or fakes at the boundaries, using xUnit. Integration tests with WebApplicationFactory that exercise routing, serialisation, authentication and the database; I run a real database in a container through Testcontainers and reset data with Respawn. A small number of end-to-end tests cover critical journeys. Everything runs in CI, and I assert on behaviour such as status codes and stored data, not implementation details.
How do you implement caching and avoid stale data?Mid
I choose the layer: response or output caching for HTTP, IMemoryCache within one instance, and a distributed cache such as Redis when several instances must share data. HybridCache combines both and protects against stampedes. Each entry gets an expiry suited to how stale the data may be, and I invalidate explicitly when the source changes if staleness is costly. I never cache user-specific data under shared keys, and I monitor hit rates.
How do authentication and authorisation work in ASP.NET Core?Mid
Authentication middleware reads credentials, a cookie or bearer token, and creates a ClaimsPrincipal on HttpContext.User. Authorisation then checks it against requirements: simple [Authorize], roles, or policies that combine claims and custom handlers. For ownership rules I use resource-based authorisation, calling IAuthorizationService with the loaded resource. I rely on an identity provider for tokens, validate issuer, audience and expiry, and use HTTPS everywhere.
How do you run background work safely in a scaled-out deployment?Senior
A hosted BackgroundService runs in every instance, so a periodic job would run several times. For single execution I use a scheduler with persistent storage and locking, such as Hangfire or Quartz in clustered mode, or a distributed lock, or move the work to a queue with competing consumers. Jobs are idempotent and use their own DI scope. I add retries, timeouts and monitoring of failures, and handle shutdown through the cancellation token.
Common mistakes
- Creating a new HttpClient for every call; use IHttpClientFactory and typed clients.
- Blocking on async code with .Result or .Wait(); await it all the way up and pass a CancellationToken.
- Registering a DbContext or other scoped service as a singleton; check the lifetimes and let scope validation catch it.
- Returning EF entities from the API; map to DTOs so that schema changes do not break clients and private fields do not leak.
- Lazy loading in loops, which causes N+1 queries; use Include or projections and read the generated SQL.
- Putting secrets in appsettings.json; use user secrets locally and a vault or environment variables in production.
- Adding middleware in the wrong order, such as authorisation before authentication; follow the documented order.
- Running migrations at start-up on every instance; apply them as a separate release step.
- Mocking everything in tests and trusting them; add integration tests with a real database in a container.
- Starting with microservices before the team or the product needs them; begin with a modular monolith.
Ready-for-interview checklist
- I can write C# with LINQ, records, async and await, and explain value and reference types.
- I can describe the middleware pipeline and write custom middleware in the right order.
- I can explain the DI lifetimes and spot a captive dependency.
- I can build an API with minimal APIs or controllers, validation and Problem Details errors.
- I can bind configuration to options, validate it and keep secrets out of source control.
- I can model entities in EF Core, create migrations and review the generated SQL.
- I can find and fix an N+1 query and choose between tracking and no tracking.
- I can write SQL joins, aggregates and explain an index and a constraint.
- I can add distributed caching with Redis and explain expiry and invalidation.
- I can run a background job safely and explain what an outbox is.
- I can write xUnit tests with mocks, and integration tests using WebApplicationFactory and a container.
- I can secure an API with JWT bearer authentication and a policy.
- I can containerise a service, add health checks and set up a build and test pipeline.
Related roadmaps
- Data Structures and Algorithms RoadmapFrom arrays to graphs and DP, organised by interview pattern.
- SQL RoadmapQueries, joins, window functions, indexes and transactions.
- API Design RoadmapREST, gRPC, GraphQL, versioning, auth, pagination and rate limits.
- Docker RoadmapImages, containers, networking, volumes, Compose and security.
- System Design RoadmapScalability, caching, queues, consistency and the classic designs.
- Redis RoadmapData types, caching patterns, persistence, clustering and streams.
FAQ
How long does it take to learn ASP.NET Core?
If you already program, expect two to three months to build solid web applications, and fourteen to eighteen weeks at about eight hours a week to reach the level expected in a mid-level interview, including EF Core, testing and deployment. Starting without C# adds a few weeks. Projects teach faster than reading.
Do I need to learn C# first?
Yes, at least the basics: types, classes, interfaces, collections, LINQ and async. You can learn the rest of C# alongside the framework. The first stage of this roadmap covers what to learn before you touch web code.
Is ASP.NET Core the same as ASP.NET and .NET Framework?
No. The older ASP.NET runs only on Windows and the .NET Framework, and receives maintenance only. ASP.NET Core is the modern, open source, cross-platform rewrite that runs on current .NET. New projects should use ASP.NET Core, though knowing the older terms helps when you maintain legacy systems.
Should I use controllers, minimal APIs, Razor Pages or Blazor?
Use minimal APIs or controllers for APIs, Razor Pages for simple server-rendered sites, and Blazor when you want rich interactive interfaces written in C#. They share the same pipeline, so skills carry over. Learn one API style well and be able to read the other.
Is ASP.NET Core still worth learning?
Yes. It is fast, actively developed with a yearly release, and widely used in enterprises, so jobs are plentiful, especially in finance and large organisations. It also runs on Linux and in containers, so it fits modern deployment.