C# and .NET Interview Questions and Answers: 60 Questions from Easy to Hard
This guide is for C# and .NET developers in India preparing for interviews, from freshers applying for a first role at a Pune IT services firm or a Bengaluru product company to engineers with several years of experience moving into senior positions. It has 60 questions in three levels: language basics such as types, OOP and collections; then everyday topics like LINQ, async and await, ASP.NET Core middleware, dependency injection and Entity Framework Core; and finally senior topics such as the thread pool, garbage collection, performance and design. Each answer is written the way a strong candidate would say it, with C# code where it helps. The answers are based on C# 12 and .NET 8, and most of them apply to later versions too. Try answering each question aloud before you read the answer, then take the mock test.
- 60 questions, 3 levels
- Updated
- CoreHRx Jobs
- 844 open C# and .NET jobs
What C# and .NET interviews cover
- C# fundamentals: value and reference types, boxing, strings, nullable types, records, and OOP with classes, interfaces, virtual and override.
- Collections, generics and LINQ: choosing between List, Dictionary and HashSet, writing queries, and explaining deferred execution.
- Async and await, Tasks and the thread pool, exceptions, delegates and events, and how the garbage collector and IDisposable work.
- ASP.NET Core: Program.cs, middleware order, routing and model binding, dependency injection lifetimes, configuration and error handling.
- Entity Framework Core and testing, plus, for senior roles, performance, memory, concurrency and API design trade-offs.
How to prepare (freshers and experienced)
- Freshers: build one small ASP.NET Core Web API with EF Core, such as a leave tracker or a job board, with CRUD, validation and a few unit tests, and push it to a public Git repository.
- Freshers: practise explaining value versus reference types, interfaces versus abstract classes, and async and await in your own words, with a short example for each.
- Experienced: prepare two or three stories from your work, such as a slow query you fixed, a production bug you traced or a design choice you made, with numbers where you have them.
- Experienced: revise the internals that senior rounds ask about: the async state machine, thread pool starvation, GC generations, DI scopes and EF Core change tracking.
- Everyone: write some code in a plain editor without IntelliSense, because many interviews use a shared screen or a whiteboard.
Common mistakes and interview-day tips
- Mixing up .NET Framework and modern .NET: say which version you used, such as .NET 8, and know that templates since .NET 6 put everything in Program.cs instead of a Startup class.
- Blocking on async code with .Result or .Wait() is a favourite topic; the expected answer is to use await all the way through.
- Giving only definitions: follow each one with a real example and a trade-off, such as when you would choose a struct over a class.
- Guessing the output of a tricky snippet: trace it line by line aloud, and if you are unsure, explain your reasoning instead of inventing an answer.
- Not knowing your own resume: be ready to explain every library you list, such as Serilog, AutoMapper or MediatR, and why your project used it.
C# and .NET interview questions: Easy questions (freshers)
20 questions1. What is the difference between C#, .NET and the CLR?
C# is the programming language. The C# compiler turns your code into Intermediate Language (IL), stored in assemblies, usually .dll files. The CLR (Common Language Runtime) loads that IL, and its JIT compiler turns it into native machine code while the program runs; the CLR also provides garbage collection, type safety and exception handling. .NET is the whole platform: the runtime, the base class libraries and the SDK tools. Modern .NET (5 and later, such as .NET 8) is the cross-platform successor of .NET Core, while .NET Framework 4.8 is the older Windows-only version.
2. What is the difference between value types and reference types in C#?
A value type variable holds the data itself: int, double, bool, DateTime, enums and every struct. Assigning it copies the whole value, so the two copies are independent. A reference type variable holds a reference to an object on the managed heap: classes, string, arrays and delegates. Assigning it copies only the reference, so both variables see the same object. In the code, changing s2 leaves s1 alone, but changing c2 also changes c1. A common mistake is saying value types always live on the stack: a struct field inside a class object lives on the heap with that object.
var s1 = new PointS { X = 1 };
var s2 = s1; // copies the whole value
s2.X = 99;
var c1 = new PointC { X = 1 };
var c2 = c1; // copies the reference
c2.X = 99;
Console.WriteLine($"{s1.X} {c1.X}"); // 1 99
struct PointS { public int X; }
class PointC { public int X; }3. What are boxing and unboxing, and why should you avoid them in hot code?
Boxing happens when a value type is converted to object or to an interface it implements: the runtime allocates an object on the heap and copies the value into it. Unboxing is the explicit cast that copies the value back out. Each box is an allocation the garbage collector must clean up later, so boxing inside a loop over thousands of records adds real GC pressure. The old non-generic ArrayList boxed every int; a generic List of int does not. Unboxing must use the exact type: an int boxed as object cannot be cast straight to long, which throws an InvalidCastException.
int count = 42;
object boxed = count; // boxing: heap allocation + copy
int back = (int)boxed; // unboxing: explicit cast
// long wrong = (long)boxed; // InvalidCastException at runtime
long right = (int)boxed; // unbox as int, then widen
var list = new List<int> { count }; // generic: no boxing4. Why are strings immutable in C#, and when should you use StringBuilder?
Once a string object is created it never changes; methods such as Replace and ToUpper, and the + operator, return a new string. That makes strings safe to share between threads and reliable as dictionary keys. The cost shows up in loops: adding to a string with += inside a loop creates a new string every time and copies all the earlier characters, so the work grows roughly with the square of the number of pieces. StringBuilder keeps one growing buffer and gives you the string once, at the end. For a few joins, interpolation or string.Join is clearer.
5. How do == and Equals() behave for strings in C#?
Unlike Java, C# overloads the == operator for string, so a == b compares the characters, and so does a.Equals(b). Both comparisons are ordinal and case-sensitive. The trap is a variable typed as object: operators are chosen at compile time from the declared type, so (object)a == (object)b compares references and can be false for two strings with the same text. For case-insensitive checks, use string.Equals with StringComparison.OrdinalIgnoreCase instead of calling ToLower on both sides, which creates new strings. For your own classes, == means reference equality unless you overload it.
string a = "hello";
string b = new string(new[] { 'h', 'e', 'l', 'l', 'o' });
Console.WriteLine(a == b); // True: compares text
Console.WriteLine(a.Equals(b)); // True
Console.WriteLine((object)a == (object)b); // False: references
Console.WriteLine(string.Equals("PUNE", "pune",
StringComparison.OrdinalIgnoreCase)); // True6. When would you use a class, a struct or a record?
Use a class for objects with identity and behaviour, such as a LeaveService or an Employee entity; two class instances with the same data are still different objects. Use a struct for small values that are copied and compared by value, like a coordinate or a date range. Use a record when the type is mainly data: the compiler generates value-based equality, a readable ToString and with-expressions that copy the object with some properties changed. A plain record is a class (C# 9), and a record struct (C# 10) gives the same features to a struct. Records suit DTOs and API models.
var a = new Job("Tester", "Pune");
var b = new Job("Tester", "Pune");
Console.WriteLine(a == b); // True: compared by value
var c = a with { City = "Chennai" }; // copy with one change
Console.WriteLine(c); // Job { Title = Tester, City = Chennai }
record Job(string Title, string City);7. Explain the four pillars of OOP with C# examples.
Encapsulation: keep fields private and expose them through properties or methods that protect the rules, such as a Balance that changes only through Deposit and Withdraw. Inheritance: a class derives from one base class and reuses its code, and can implement many interfaces. Polymorphism: a base-class reference calls the override that matches the real object at runtime, through virtual and override; method overloading is the compile-time form. Abstraction: an interface such as IPaymentGateway hides whether a payment goes through UPI or a card. Back each with an example from your project.
8. What is the difference between an abstract class and an interface?
An abstract class can have fields, constructors, and both finished and abstract members, but a class can inherit from only one. An interface is a contract, and a class or struct can implement several interfaces at once. Since C# 8 interfaces can contain default method implementations and static members, so the gap is smaller, but they still cannot hold instance fields or constructors. Use an interface for a capability that unrelated types share, such as IEmailSender, and for dependency injection and mocking in tests. Use an abstract class when subclasses share real code and state.
9. What is the difference between virtual/override and the new keyword on a method?
virtual on a base method lets derived classes replace it with override, and the call is resolved at runtime from the actual object: a Base variable that holds a Derived object runs Derived.Show. The new keyword only hides the base method; the call is chosen at compile time from the declared type of the variable, so the same Base variable still runs Base.Print. That difference is a favourite interview trap. Use override for real polymorphism. new is rare: mainly to silence the hiding warning when a base class you don't own later adds a method with the same name.
Base b = new Derived();
b.Show(); // Derived.Show: override, chosen at runtime
b.Print(); // Base.Print: new only hides, chosen by declared type
class Base
{
public virtual void Show() => Console.WriteLine("Base.Show");
public void Print() => Console.WriteLine("Base.Print");
}
class Derived : Base
{
public override void Show() => Console.WriteLine("Derived.Show");
public new void Print() => Console.WriteLine("Derived.Print");
}10. What access modifiers does C# have, and what are the defaults?
public is visible everywhere; private only inside the same type; protected inside the type and its derived types; internal anywhere in the same assembly, which usually means the same project. protected internal is the union: the same assembly or any derived type. private protected (C# 7.2) is the intersection: derived types inside the same assembly. Class and struct members are private by default, and top-level types are internal. Pick the narrowest level that works. A test project can see internal members through the InternalsVisibleTo attribute.
11. What is the difference between const, readonly and static readonly?
A const is a compile-time constant: it must be a number, bool, char, string or enum value (or null), it is implicitly static, and the compiler copies its value into every assembly that uses it. So if a library changes a public const, callers keep the old value until they are rebuilt. A readonly field can be of any type and is set in its declaration or in a constructor, possibly from runtime data such as configuration. static readonly is the same idea with one value per type. readonly fixes the reference, not the object: you can still add items to a readonly List.
12. What do the ref, out and in keywords do?
By default arguments are passed by value: the method gets a copy of the value, or a copy of the reference for a class, so reassigning the parameter doesn't affect the caller. ref passes the caller's variable itself; it must be assigned before the call, and the method may read and change it. out also passes by reference, but the method must assign it before returning and the caller need not initialise it, which is the TryParse pattern. in passes a read-only reference, mainly to avoid copying large structs. You don't need ref just to change an object's properties.
if (int.TryParse("250", out int qty))
Console.WriteLine(qty * 2); // 500
int bonus = 100;
AddBonus(ref bonus);
Console.WriteLine(bonus); // 150
static void AddBonus(ref int amount) => amount += 50;13. How do nullable value types and nullable reference types differ?
int? is shorthand for the Nullable struct: a value type that can also be empty, with HasValue and Value, useful for an optional column such as a relieving date. Nullable reference types (C# 8) are a compile-time feature: with the Nullable setting enabled, as in .NET 6 and later project templates, string means it should not be null and string? means it may be null, and the compiler warns when you dereference a maybe-null value. They don't change runtime behaviour. Use ?. to call through a possible null, ?? for a fallback, and the ! operator only when you truly know better than the compiler.
int? noticeDays = null;
int days = noticeDays ?? 30; // 30 when there is no value
string? city = FindCity(101); // may be null
int len = city?.Length ?? 0; // 0, no NullReferenceException
static string? FindCity(int empId) => null;14. How do you choose between an array, a List and a Dictionary?
An array has a fixed length set when it is created, with fast index access. A List wraps an array that grows automatically, doubling its capacity when full: indexing is O(1), Add is O(1) on average, but Insert, Remove and Contains are O(n). A Dictionary is a hash table: lookups, adds and removes by key are O(1) on average, keys must be unique, and the key type needs sensible Equals and GetHashCode. A HashSet holds unique values with a fast Contains. To look up employees by ID among 50,000 records, use a Dictionary, not a List with Find, which scans every item.
15. What are generics, and what do constraints add?
Generics let you write a type or method once with a type parameter T and use it safely with many types, because the compiler checks each use. A generic List of int stores ints directly, rejects strings at compile time and needs no casts, while the old ArrayList stored everything as object and boxed each int. Constraints tell the compiler what T can do: where T : class, struct, new(), a base class, or an interface such as IComparable, which then lets you call CompareTo inside the method. Without a constraint you can use only the members of object.
16. What is LINQ, and how would you use it to filter and sort a list?
LINQ (Language Integrated Query) is a set of query methods such as Where, Select, OrderBy, GroupBy, Any and Sum that work on any collection; through Entity Framework Core the same query can be translated into SQL. You can write method syntax or query syntax, as in the code; both compile to the same method calls. Most operators are deferred: nothing runs until you loop over the result or call ToList, Count or First. Know the small traps too: First throws on an empty sequence while FirstOrDefault returns the default value, and Single throws if there is more than one match.
var shortlisted = candidates
.Where(c => c.Experience >= 2 && c.City == "Hyderabad")
.OrderByDescending(c => c.Score)
.Select(c => c.Name)
.ToList();
// The same query in query syntax
var names = (from c in candidates
where c.Experience >= 2 && c.City == "Hyderabad"
orderby c.Score descending
select c.Name).ToList();17. How does exception handling work in C# with try, catch and finally?
Code that may fail goes in try. The catch blocks are checked in order and the first matching one runs, so put specific types such as FormatException before general ones; the compiler rejects a general catch placed above a more specific one. finally runs whether the try succeeded, threw or returned, so it is the place for cleanup. Catch only what you can handle and let the rest reach a global handler that logs it. Don't use exceptions for normal flow: int.TryParse is better than catching FormatException for user input. And never leave an empty catch block that hides errors.
18. What is IDisposable, and what does the using statement do?
The GC frees managed memory when it chooses and knows nothing about files, sockets or database connections. A class that holds such resources implements IDisposable, and its Dispose method releases them straight away. A using statement calls Dispose when the block ends, even if an exception is thrown, because it compiles to try/finally. Since C# 8 a using declaration disposes at the end of the enclosing scope. Typical cases are streams and database connections, where Dispose returns the connection to the pool. Don't dispose objects you don't own, such as a DbContext injected by DI.
using (var reader = new StreamReader("attendance.csv"))
{
string? header = reader.ReadLine();
// read the rest of the file
} // reader.Dispose() runs here, even after an exception
// C# 8 and later: a using declaration, disposed at the end of the scope
using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();19. What do async and await do, and why do web APIs use them?
They let a method wait for I/O, such as a database query or an HTTP call, without blocking a thread. An async method returns a Task, or a Task of T when it has a result. At an await on work that hasn't finished, the method returns to its caller, and the rest of it runs later when the result arrives. In an ASP.NET Core API this frees the thread to serve other requests while the database works, so the server handles more requests with fewer threads. It doesn't make a single call faster. Use async all the way down, end method names with Async, and don't block with .Result.
20. What does Program.cs contain in an ASP.NET Core application?
Since .NET 6, templates use the minimal hosting model in a single Program.cs. WebApplication.CreateBuilder sets up configuration from appsettings.json and environment variables, logging and the dependency injection container. You register services on builder.Services, such as controllers, your own services and the DbContext. builder.Build() creates the app. Then you add middleware in the order requests should pass through, map endpoints with MapControllers or minimal API calls like MapGet, and call Run to start the Kestrel web server. Remember the order: services first, then the pipeline.
var builder = WebApplication.CreateBuilder(args);
// 1. Register services
builder.Services.AddControllers();
builder.Services.AddScoped<IJobService, JobService>();
var app = builder.Build();
// 2. Build the request pipeline, in order
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapGet("/health", () => "OK");
app.Run();Ready to check yourself? Take the free C# and .NET mock test
C# and .NET interview questions: Medium questions (1-3 years)
20 questions21. What is deferred execution in LINQ, and what problems can it cause?
Most LINQ operators, such as Where, Select and OrderBy, don't run when you write them. They build a query that runs each time you enumerate it, so it sees the data as it is at that moment: in the code, the first Count includes the 4 added later. Enumerating twice runs the work twice, and with EF Core that means two database round trips. ToList, ToArray, Count, First and Sum run the query immediately. Call ToList when you need a snapshot or will loop over the result more than once, but not too early on a database query, or you load every row before filtering.
var nums = new List<int> { 1, 2, 3 };
var big = nums.Where(n => n > 1); // nothing runs yet
nums.Add(4);
Console.WriteLine(big.Count()); // 3: the query sees the 4
var snapshot = nums.Where(n => n > 1).ToList(); // runs now
nums.Add(5);
Console.WriteLine(snapshot.Count); // 3: copied before the Add
Console.WriteLine(big.Count()); // 4: runs again, sees the 522. What is the difference between IEnumerable and IQueryable?
IEnumerable runs LINQ in memory: operators run compiled delegates over objects already loaded. IQueryable holds an expression tree that a provider such as EF Core translates into SQL, so Where, OrderBy and Take run in the database and only the matching rows come back. The trap is switching to IEnumerable too early, through AsEnumerable, ToList or a variable typed as IEnumerable: every operator after that runs in memory, on rows the database should have filtered. Also, since EF Core 3.0, a Where condition that cannot be translated throws an exception instead of silently running on the client.
// Translated to SQL: WHERE, ORDER BY and TOP/LIMIT run in the database
var page = await db.Employees
.Where(e => e.DeptId == 4)
.OrderBy(e => e.Name)
.Take(20)
.ToListAsync();
// Typed as IEnumerable: loads every employee, then filters in memory
IEnumerable<Employee> all = db.Employees;
var slow = all.Where(e => e.DeptId == 4).ToList();23. Why should you avoid async void methods?
An async void method can't be awaited, so the caller can't know when it finishes, and an exception inside it is not stored in a Task. It is raised on the synchronization context, or on the thread pool when there is none, as in ASP.NET Core and console apps, and that crashes the process. async void exists for event handlers, such as a button click, where the signature is fixed. Everywhere else return Task. A common hidden case is passing an async lambda to a method that expects an Action, such as List.ForEach: the lambda becomes async void, so nothing waits for it and errors are lost.
24. What happens if you call .Result or .Wait() on a Task, and when do you need ConfigureAwait(false)?
Both block the current thread until the task completes. In a WPF, WinForms or classic ASP.NET app, which has a synchronization context, this can deadlock: the awaited code wants to resume on the same thread that is now blocked waiting for it. ASP.NET Core has no synchronization context, so it usually doesn't deadlock, but every blocked request holds a thread-pool thread, and under load that causes thread pool starvation. The fix is async all the way. ConfigureAwait(false) tells an await not to resume on the captured context; use it in library code, but ASP.NET Core app code doesn't need it.
// Blocks a thread; can deadlock in WPF or WinForms
var user = GetUserAsync(id).Result;
// Frees the thread while waiting
var user2 = await GetUserAsync(id);
// Library code: no need to resume on the caller's context
string json = await http.GetStringAsync(url).ConfigureAwait(false);25. How do you run several independent async calls at the same time?
Start all the tasks first and then await Task.WhenAll, instead of awaiting each call in turn. If the profile, leave and attendance calls take 300, 200 and 400 ms, sequential awaits take about 900 ms in total and WhenAll about 400 ms. If tasks fail, awaiting WhenAll throws the first exception, and the returned task's Exception property holds all of them. Two cautions: an EF Core DbContext is not thread-safe, so never run parallel queries on the same instance; and for hundreds of items, limit concurrency with SemaphoreSlim or Parallel.ForEachAsync so you don't flood the other service.
// Sequential: about 300 + 200 + 400 = 900 ms
var profile = await GetProfileAsync(empId);
var leaves = await GetLeavesAsync(empId);
var attendance = await GetAttendanceAsync(empId);
// Concurrent: about 400 ms, the time of the slowest call
var pTask = GetProfileAsync(empId);
var lTask = GetLeavesAsync(empId);
var aTask = GetAttendanceAsync(empId);
await Task.WhenAll(pTask, lTask, aTask);
var profile2 = await pTask; // already complete, no extra wait26. How and why do you pass a CancellationToken through an ASP.NET Core API?
A CancellationToken tells running work to stop because nobody needs the result: the user closed the page, a timeout passed or the app is shutting down. Add a CancellationToken parameter to a controller action or a minimal API handler and ASP.NET Core binds it to HttpContext.RequestAborted. Pass it to every async call that accepts one, such as EF Core ToListAsync and HttpClient methods, which then throw OperationCanceledException. Cancellation is cooperative, so long CPU loops must call ThrowIfCancellationRequested. Use CancelAfter for timeouts, and don't log a cancelled request as an error.
27. What is the difference between throw and throw ex, and what are exception filters?
Inside a catch block, throw; rethrows the same exception and keeps its original stack trace. throw ex; resets the stack trace to that line, so the log points at the catch block instead of the line that really failed, which makes bugs much harder to find. To add context, wrap it in a new exception and pass the original as the inner exception. An exception filter, a when clause after the catch, decides whether to catch before the stack unwinds, so you can catch only HTTP 503 errors for a retry, as in the code, and let everything else pass through untouched.
try
{
await gateway.RefundAsync(orderId);
}
catch (HttpRequestException ex)
when (ex.StatusCode == HttpStatusCode.ServiceUnavailable)
{
await retryQueue.AddAsync(orderId); // only for HTTP 503
}
catch (Exception ex)
{
logger.LogError(ex, "Refund {OrderId} failed", orderId);
throw; // not "throw ex;", which loses the stack trace
}28. How do you handle unhandled exceptions globally in ASP.NET Core 8?
Rather than a try/catch in every action, handle unexpected exceptions in one place. .NET 8 added the IExceptionHandler interface: you implement TryHandleAsync, register the class with AddExceptionHandler, and call UseExceptionHandler near the start of the pipeline so it wraps everything after it. Add AddProblemDetails so errors go out in the standard ProblemDetails JSON format. Log the full exception on the server, but send the client a generic message and never a stack trace; the developer exception page is for Development only. Expected cases such as not found should be normal results.
public class GlobalHandler(ILogger<GlobalHandler> log)
: IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext ctx,
Exception ex, CancellationToken ct)
{
log.LogError(ex, "Unhandled error on {Path}", ctx.Request.Path);
ctx.Response.StatusCode = StatusCodes.Status500InternalServerError;
await ctx.Response.WriteAsJsonAsync(
new ProblemDetails { Title = "Something went wrong" }, ct);
return true; // handled: stop here
}
}
// Program.cs
builder.Services.AddExceptionHandler<GlobalHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();29. How does the ASP.NET Core middleware pipeline work, and why does order matter?
Each middleware receives the HttpContext, can do some work, calls next to pass the request on, and can do more work as the response comes back; if it doesn't call next, it short-circuits the pipeline. Middleware runs in the order it is added, and the response travels back in reverse. So the exception handler goes first to catch errors from everything after it, CORS goes before authorization, and UseAuthentication must come before UseAuthorization, or no user is known when access is checked. app.Use adds middleware that can call next; app.Run adds a terminal one.
app.UseExceptionHandler("/error"); // first: wraps everything below
app.UseStaticFiles();
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew();
await next(context); // the rest of the pipeline runs here
app.Logger.LogInformation("{Path} took {Ms} ms",
context.Request.Path, sw.ElapsedMilliseconds);
});
app.UseAuthentication(); // who are you?
app.UseAuthorization(); // are you allowed?
app.MapControllers();30. Explain the Transient, Scoped and Singleton lifetimes in ASP.NET Core dependency injection.
Transient creates a new instance every time the service is resolved, for light, stateless classes. Scoped creates one instance per scope, and in ASP.NET Core each HTTP request is a scope, so everything in one request shares it; AddDbContext registers the DbContext as scoped. Singleton creates one instance for the whole app, such as a cache, so it must be thread-safe. The classic bug is a captive dependency: a singleton that takes a scoped DbContext keeps one context forever, shared across threads. In the Development environment the host validates scopes and throws when it finds this.
builder.Services.AddTransient<IOtpGenerator, OtpGenerator>();
builder.Services.AddScoped<ILeaveService, LeaveService>();
builder.Services.AddSingleton<IHolidayCache, HolidayCache>();
builder.Services.AddDbContext<HrDb>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Hr")));
// Bug: a singleton that captures a scoped DbContext
public class HolidayCache(HrDb db) : IHolidayCache { /* ... */ }31. How does configuration work in ASP.NET Core, and what is the options pattern?
The default builder loads configuration in layers: appsettings.json, then appsettings.{Environment}.json, user secrets in Development, environment variables and finally command-line arguments, and later sources win. Nested keys use a colon, such as Smtp:Host, or a double underscore in environment variables (Smtp__Host). The options pattern binds a section to a class: IOptions gives one value for the app's lifetime, IOptionsSnapshot re-reads it per request, and IOptionsMonitor notifies you of changes. Add ValidateOnStart to fail fast on bad settings, and keep secrets out of git.
32. How do routing and model binding work in an ASP.NET Core Web API?
Routing matches the URL and HTTP method to an endpoint. Web API controllers usually use attribute routing, such as [Route("api/jobs")] on the class and [HttpGet("{id:int}")] on an action; minimal APIs use app.MapGet and similar calls. The int constraint means /api/jobs/abc doesn't match at all, but constraints are for matching, not validation. Model binding then fills action parameters from the route, query string, headers, form or body. With [ApiController], complex types come from the JSON body, simple types from the route or query, and an invalid model returns a 400 response automatically.
33. What is change tracking in EF Core, and when should you use AsNoTracking?
By default, entities returned by a query are tracked: the DbContext keeps a snapshot of each one, and SaveChanges compares the current values with the snapshot and sends an UPDATE for only the changed columns. That is what you want to load, change and save. For read-only work such as lists, reports and GET endpoints, AsNoTracking skips the snapshots and uses less memory and CPU. A DbContext is a short-lived unit of work: one per request, not thread-safe and never shared between threads. A common mistake is calling Update on a detached entity, which marks every column as modified.
34. What is the N+1 query problem in EF Core, and how do you fix it?
N+1 means one query loads a list and then one more query runs for each row when you touch a related entity, so 200 orders become 201 queries. EF Core doesn't lazy-load by default; it happens if you enable lazy-loading proxies, and then N+1 hides silently inside a loop. Fixes: eager loading with Include and ThenInclude, which uses a join; or, usually best for APIs, projecting with Select into a DTO so only the needed columns come back; or explicit loading for occasional cases. Find the problem by logging the generated SQL, or call ToQueryString during development.
// Eager loading: one query with a JOIN
var orders = await db.Orders
.Include(o => o.Customer)
.Where(o => o.OrderDate >= from)
.ToListAsync();
// Projection: only the columns the API returns
var rows = await db.Orders
.Where(o => o.OrderDate >= from)
.Select(o => new { o.Id, o.Total, CustomerName = o.Customer.Name })
.ToListAsync();35. How do you manage database schema changes with EF Core migrations?
After changing the entity classes, run dotnet ef migrations add with a clear name. EF Core compares the model with its snapshot and generates a migration class with Up and Down methods. Review it before committing: a renamed property is often generated as dropping one column and adding another, which would lose data, so you change it into a rename. Apply it locally with dotnet ef database update. For production, generate an idempotent SQL script or a migration bundle and run it as a deployment step, rather than calling Database.Migrate() at startup when several instances may start together.
36. What is the difference between a delegate and an event?
A delegate is a type-safe reference to one or more methods, such as Func or Action, and a lambda converts to one. An event wraps a delegate so that code outside the class can only subscribe with += and unsubscribe with -=; only the declaring class can raise it. With a plain public delegate field, any caller could invoke it or wipe out all subscribers by assigning with =. The usual convention is EventHandler with an event-args type, raised with ?.Invoke as in the code. A long-lived publisher keeps every subscriber alive, so forgetting to unsubscribe short-lived objects causes memory leaks.
public class LeaveService
{
public event EventHandler<LeaveApprovedArgs>? Approved;
public void Approve(int leaveId)
{
// ... save the approval
Approved?.Invoke(this, new LeaveApprovedArgs(leaveId));
}
}
public record LeaveApprovedArgs(int LeaveId);
// Subscriber
service.Approved += (sender, e) => SendSms(e.LeaveId);
// service.Approved = null; // compile error outside LeaveService37. What does this code print, and why do lambdas capture loop variables this way?
The for loop prints 333 and the foreach loop prints 012. A lambda captures the variable itself, not its value at that moment. A for loop has one variable i for the whole loop, so all three lambdas share it, and when they run after the loop it holds 3, the value that ended the loop. Since C# 5, foreach declares a fresh variable on each pass, so each lambda keeps its own n. To fix the for loop, copy i into a local variable inside the body and capture the copy. The same trap appears when you start tasks or attach event handlers inside a loop.
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
actions.Add(() => Console.Write(i));
foreach (var a in actions) a(); // 333
actions.Clear();
foreach (var n in new[] { 0, 1, 2 })
actions.Add(() => Console.Write(n));
foreach (var a in actions) a(); // 01238. Why must you override GetHashCode when you override Equals?
Dictionary and HashSet use GetHashCode to choose a bucket and then Equals to confirm a match. The rule is that two equal objects must return the same hash code. If you override Equals but not GetHashCode, which the compiler warns about, equal keys usually land in different buckets and lookups fail. A key's hash must also not change while it is in the collection: if you modify a property used in the hash after adding the key, the entry can't be found any more. Records generate both methods correctly, and HashCode.Combine is the easy way to write your own.
39. How do you write unit tests for a .NET service?
A unit test checks one class on its own, quickly, without a database or network. xUnit, NUnit and MSTest are the common frameworks; in xUnit a [Fact] is a single test and a [Theory] with [InlineData] runs the same test with several inputs, as in the code. Follow Arrange, Act, Assert. Inject dependencies through interfaces so a test can pass a fake, or a mock made with Moq or NSubstitute. Test behaviour rather than private details, and name each test after the rule it checks. For APIs, add integration tests with WebApplicationFactory, which runs the real pipeline in memory.
public class LeaveCalculatorTests
{
[Theory]
[InlineData(1, 18)] // joined in January: the full year
[InlineData(7, 9)] // joined in July: half the year
public void Leave_is_prorated_by_joining_month(int month, int expected)
{
// Arrange
var calc = new LeaveCalculator(annualDays: 18);
// Act
int days = calc.ForJoiningMonth(month);
// Assert
Assert.Equal(expected, days);
}
}40. Why should you not create a new HttpClient for every request?
HttpClient is meant to be reused. Creating and disposing one per call leaves closed connections in the TIME_WAIT state for a while, and under load the server can run out of sockets. One static HttpClient fixes that, but it never notices DNS changes. IHttpClientFactory solves both: AddHttpClient registers named or typed clients, and the factory pools and recycles the underlying handlers, every two minutes by default. It also keeps the base address, timeout and headers in one place, and in .NET 8 the Microsoft.Extensions.Http.Resilience package adds retries and circuit breakers on top.
builder.Services.AddHttpClient<SmsClient>(c =>
{
c.BaseAddress = new Uri("https://sms.example.in/");
c.Timeout = TimeSpan.FromSeconds(10);
});
public class SmsClient(HttpClient http)
{
public Task<HttpResponseMessage> SendAsync(string phone, string text)
=> http.PostAsJsonAsync("v1/send", new { phone, text });
}Ready to check yourself? Take the free C# and .NET mock test
C# and .NET interview questions: Hard questions (experienced)
20 questions41. How does async/await work under the hood?
The compiler rewrites an async method into a state machine: locals become fields and each await a numbered state. At an await it gets the awaiter; if the task is already complete, it simply carries on. If not, it saves its state, registers itself as the continuation and returns an incomplete Task to the caller. When the I/O finishes, the method resumes on the captured synchronization context if there was one, otherwise on a thread-pool thread. No thread waits during true async I/O. In Release builds the state machine is a struct, boxed to the heap only on the first real suspension.
42. What is thread pool starvation, and how do you diagnose and fix it?
It happens when thread-pool threads are blocked, so queued work, including the continuations that would unblock them, waits for a free thread. Symptoms: latency and timeouts climb while CPU stays low, and the thread count grows slowly because the pool adds threads gradually. Typical causes are sync-over-async (.Result, .Wait()), Thread.Sleep and synchronous I/O. Diagnose with dotnet-counters, where the thread-pool queue length and thread count keep rising, then dotnet-stack or a dump showing many threads parked in Wait. Fix the blocking code; raising SetMinThreads only buys time.
43. When should you return ValueTask instead of Task?
ValueTask is a struct that holds either a result or a Task (or a pooled source). It pays off in hot methods that usually complete synchronously, such as a cache hit or a buffered read, because returning a ready result needs no Task allocation. Its rules are stricter: await it only once, never from two places at the same time, and don't read .Result before it has completed; if you need any of that, call AsTask(). Task is simpler and should stay the default for most APIs. Switch to ValueTask only where measurements show the allocations matter.
44. How does the .NET garbage collector work, and what are generations and the Large Object Heap?
The .NET GC is generational, tracing and compacting. New objects start in generation 0; objects that survive a collection are promoted to gen 1 and then gen 2. Most objects die young, so gen 0 collections are frequent and cheap, while a full gen 2 collection is expensive. Objects of 85,000 bytes or more go on the Large Object Heap, which is collected with gen 2 and not compacted by default, so many big temporary arrays cause fragmentation and full GCs. ASP.NET Core uses Server GC by default, with a heap per core. Reduce allocations in hot paths, pool large buffers, and don't call GC.Collect.
45. When do you need a finalizer, and how do you implement IDisposable correctly?
Almost never. A finalizer is a safety net the GC runs at an unknown time if Dispose was not called, and it is costly: finalizable objects survive an extra collection, get promoted, and are finalized on one thread. Write one only when your class directly holds an unmanaged handle, and even then prefer wrapping the handle in a SafeHandle, which has its own. If your class only owns managed disposables, such as a FileStream, make Dispose call their Dispose, make it safe to call twice, and skip the finalizer. Implement IAsyncDisposable when cleanup is async, so callers can use await using.
46. How do Span and Memory help reduce allocations?
Span is a ref struct that points into a block of memory, such as part of an array or a string, a stackalloc buffer or native memory, without copying it. Slicing is free, so parsing a CSV line with spans avoids the small strings that Split and Substring create. Because it is a ref struct it can live only on the stack: you can't store it in a class field or box it, and in C# 12 you can't declare one in an async method at all. Memory is the heap-friendly version for async code and fields. Pair spans with ArrayPool to rent buffers, and use them only in measured hot paths.
using System.Buffers;
ReadOnlySpan<char> line = "Asha,Pune,42";
int first = line.IndexOf(',');
int last = line.LastIndexOf(',');
ReadOnlySpan<char> name = line[..first]; // no new string
ReadOnlySpan<char> city = line[(first + 1)..last];
int age = int.Parse(line[(last + 1)..]); // parses a slice
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
try { /* fill and use the buffer */ }
finally { ArrayPool<byte>.Shared.Return(buffer); }47. What are the pitfalls of structs, and how do you design them well?
Use a struct for small, immutable values such as a GeoPoint or a date range; Microsoft's guidelines suggest under 16 bytes. Large structs are copied on every assignment, argument and return. Mutable structs surprise people: a List indexer returns a copy, so you must change the copy and write it back, as the code shows. Casting to object or to an interface boxes the struct. Calling a method on a readonly field of a non-readonly struct makes a hidden defensive copy. So declare readonly structs, pass big ones with in, and implement IEquatable to avoid the slow default Equals.
var points = new List<Point> { new Point { X = 1 } };
// points[0].X = 5; // error CS1612: the indexer returns a copy
var p = points[0]; // a copy
p.X = 5; // changes only the copy
Console.WriteLine(points[0].X); // 1
points[0] = p; // write the copy back
Console.WriteLine(points[0].X); // 5
public struct Point { public int X; }
// Better: small and immutable
public readonly record struct GeoPoint(double Lat, double Lon);48. How are .NET generics implemented, and how do they differ from Java generics?
.NET generics are reified: a List of int and a List of string are real, distinct types at runtime, so typeof(T), is checks and new T() (with a new() constraint) work, unlike Java, which erases type arguments. The JIT compiles separate native code for each value-type argument, which is why a List of int stores ints unboxed, while all reference-type arguments share one copy of the code. Static fields exist once per closed type, so Counter of int and Counter of string each have their own Count. Since C# 11, static abstract interface members enable generic math, such as INumber in the code.
using System.Numerics;
Console.WriteLine(Sum(new[] { 1, 2, 3 })); // 6
Console.WriteLine(Sum(new[] { 10L, 20L })); // 30
static T Sum<T>(IEnumerable<T> items) where T : INumber<T>
{
T total = T.Zero;
foreach (var x in items)
total += x;
return total;
}
class Counter<T> { public static int Count; }
// Counter<int>.Count and Counter<string>.Count are separate fields49. What are covariance and contravariance in C# generics?
Covariance (out T) lets you use a more derived type argument where a less derived one is expected: an IEnumerable of Manager can be used as an IEnumerable of Employee, safe because T only comes out. Contravariance (in T) goes the other way: an Action of Employee works where an Action of Manager is needed, since code that handles any employee handles a manager. Variance applies only to interfaces and delegates, and only with reference types. List is invariant because it both accepts and returns T. Arrays are covariant unsafely: a wrong store throws ArrayTypeMismatchException.
IEnumerable<Manager> managers = new List<Manager>();
IEnumerable<Employee> staff = managers; // OK: IEnumerable<out T>
Action<Employee> notify = e => Console.WriteLine(e.Name);
Action<Manager> notifyManager = notify; // OK: Action<in T>
// List<Employee> list = new List<Manager>(); // error: invariant
Employee[] team = new Manager[1];
team[0] = new Intern(); // ArrayTypeMismatchException at runtime
class Employee { public string Name = ""; }
class Manager : Employee { }
class Intern : Employee { }50. How do you protect shared state across threads and async code?
For a single counter, Interlocked.Increment is atomic and lock-free. When several fields must change together, use lock on a private object, keep the block short, and never lock on this or on a string. You can't await inside a lock, because the compiler rejects it, so async code uses SemaphoreSlim with WaitAsync and calls Release in finally. ConcurrentDictionary makes each single call thread-safe, but the GetOrAdd factory can run more than once in a race, so wrap expensive work in Lazy. A TryGetValue followed by a separate write is still a race; use AddOrUpdate instead.
// One counter: atomic, no lock
Interlocked.Increment(ref _requestCount);
// Async code: SemaphoreSlim instead of lock
private readonly SemaphoreSlim _gate = new(1, 1);
public async Task RefreshTokenAsync()
{
await _gate.WaitAsync();
try { _token = await _auth.GetTokenAsync(); }
finally { _gate.Release(); }
}
// Expensive value built at most once per key
var report = _cache.GetOrAdd(key,
k => new Lazy<Report>(() => BuildReport(k))).Value;51. How do you use a scoped service such as DbContext from a singleton or a BackgroundService?
Don't inject it into the constructor: the singleton would capture one DbContext for the app's lifetime and share it across threads. Inject IServiceScopeFactory instead, create a scope for each unit of work, resolve the DbContext from it, and dispose the scope when that work ends. Avoid IServiceProvider as a service locator in ordinary classes; it hides dependencies. .NET 8 also added keyed services, such as AddKeyedScoped with a key, to choose between implementations. With C# 12 primary constructors, note that the parameters are mutable captures, not readonly fields.
public class PayrollReminder(IServiceScopeFactory scopes)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync(ct))
{
using var scope = scopes.CreateScope(); // one unit of work
var db = scope.ServiceProvider.GetRequiredService<HrDb>();
// query pending approvals and send reminders
}
}
}52. When would you use middleware and when an MVC filter?
Middleware runs for every request at the pipeline level and sees only the HttpContext, so it suits cross-cutting work: request logging, correlation IDs, compression and rate limiting. Filters run inside MVC after an action has been chosen and know about it: authorization, resource, action, exception and result filters. An action filter sees the bound arguments and ModelState, so an audit log of which action ran with which input belongs in a filter. Minimal APIs have endpoint filters for the same job. Note that an exception filter catches only exceptions from MVC, not from middleware.
53. How would you make an ASP.NET Core API handle much more traffic?
Start by measuring: a load test with a target such as p95 latency under 200 ms at the expected requests per second, plus production metrics. Then the usual wins: async I/O everywhere so threads never block; lean data access with AsNoTracking, projections, indexes and paging; caching with IMemoryCache or Redis, and output caching for responses many users share; DbContext pooling; HttpClient through IHttpClientFactory; and the rate limiter to protect the service. Output caching and rate limiting are built in since .NET 7. Keep the API stateless so you can add instances behind a load balancer.
builder.Services.AddOutputCache();
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("api",
w => { w.PermitLimit = 100; w.Window = TimeSpan.FromSeconds(1); }));
builder.Services.AddDbContextPool<JobsDb>(o => o.UseNpgsql(connString));
var app = builder.Build();
app.UseRateLimiter();
app.UseOutputCache();
app.MapGet("/jobs/cities", GetCities)
.CacheOutput(p => p.Expire(TimeSpan.FromMinutes(5)))
.RequireRateLimiting("api");54. How do you make EF Core queries and updates fast?
First read the SQL and its plan. For reads, use AsNoTracking, project with Select to only the needed columns, and page with an indexed ORDER BY. Including two or more collections in one query multiplies the rows returned (a cartesian explosion); AsSplitQuery runs one query per collection instead. Since EF Core 7, ExecuteUpdateAsync and ExecuteDeleteAsync change many rows in one SQL statement without loading them. AddDbContextPool reuses context instances, and compiled queries cut the per-call overhead of hot queries. Avoid SaveChanges inside a loop; one call batches the changes.
// One UPDATE statement, no entities loaded (EF Core 7 and later)
await db.Jobs
.Where(j => j.IsOpen && j.LastDate < today)
.ExecuteUpdateAsync(s => s.SetProperty(j => j.IsOpen, false));
// Two collections: split queries avoid a cartesian product
var depts = await db.Departments
.Include(d => d.Employees)
.Include(d => d.Openings)
.AsSplitQuery()
.AsNoTracking()
.ToListAsync();55. How do you handle concurrent updates to the same row in EF Core?
Without a concurrency token, the last save silently wins. With SQL Server, a byte array property marked [Timestamp] maps to rowversion, which the database changes on every update. EF Core then adds the original RowVersion value to the WHERE clause of each UPDATE and DELETE, and if no row matches it throws DbUpdateConcurrencyException. The API can then return 409 Conflict, as in the code, or reload and retry. For a hot value like a wallet balance, a single conditional UPDATE through ExecuteUpdate, with Balance >= amount in the WHERE clause, avoids the read-then-write race altogether.
public class Wallet
{
public int Id { get; set; }
public decimal Balance { get; set; }
[Timestamp] public byte[] RowVersion { get; set; } = [];
}
try
{
wallet.Balance -= amount;
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
return Results.Conflict("Balance changed, please try again");
}56. How do authentication and authorization work in ASP.NET Core APIs?
Authentication decides who the caller is; authorization decides what they may do. For APIs, AddJwtBearer validates the token's signature, issuer, audience and expiry and builds the user from its claims. If you add the middleware yourself, UseAuthentication goes before UseAuthorization. Protect endpoints with [Authorize] or RequireAuthorization; roles suit coarse rules, policies built from claims scale better. For rules about one record, such as whether this manager may approve this leave, call IAuthorizationService.AuthorizeAsync with that record, since an attribute can't see it.
57. How would you run background work, such as sending thousands of offer emails, in an ASP.NET Core app?
Not inside the HTTP request. Within one service, a common design is a bounded Channel: the endpoint writes the job ID and returns 202 Accepted, and a BackgroundService reads from the channel and processes each one. A bounded channel gives back-pressure, since WriteAsync waits while it is full, so memory can't grow unbounded. Catch and log errors per item, because since .NET 6 an unhandled exception in a BackgroundService stops the host, and honour the stopping token for a clean shutdown. An in-memory channel loses queued work on a restart, so use a durable queue when work must not be lost.
builder.Services.AddSingleton(Channel.CreateBounded<int>(1000));
builder.Services.AddHostedService<OfferMailWorker>();
public class OfferMailWorker(Channel<int> queue,
ILogger<OfferMailWorker> log) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
await foreach (var offerId in queue.Reader.ReadAllAsync(ct))
{
try { await SendOfferMailAsync(offerId, ct); }
catch (Exception ex)
{
log.LogError(ex, "Offer mail {OfferId} failed", offerId);
}
}
}
}58. How do you test an ASP.NET Core API and its database reliably?
Keep many fast unit tests for business rules and fewer integration tests that run the real pipeline. WebApplicationFactory boots the app in memory, and ConfigureTestServices swaps in fakes. For the database, the EF Core InMemory provider is a poor stand-in: it isn't relational, doesn't enforce foreign keys and doesn't support transactions, so tests can pass while production fails. Prefer the real database engine in a container through Testcontainers, or at least SQLite in-memory. Flaky tests usually come from time, randomness or shared data; .NET 8's TimeProvider lets you fake the clock.
59. How do you measure and profile performance in .NET before optimising?
Measure first, in Release builds, with realistic data, and change one thing at a time. For a single method, BenchmarkDotNet handles warm-up, runs many iterations and reports the mean and error; with [MemoryDiagnoser] it also shows the bytes allocated per call, which is often the real cost. A quick Stopwatch loop misleads because of JIT tiering, GC pauses and code the compiler removes. For a running service, dotnet-counters shows CPU, GC and thread-pool trends, dotnet-trace records CPU samples you can open in PerfView or Visual Studio, and dotnet-gcdump captures the heap for memory questions.
60. How do you find a memory leak in a .NET service running in production?
In .NET a leak means objects stay reachable after they should be garbage, so the GC can't free them. Usual causes: a static or singleton cache with no size limit or expiry, event handlers on a long-lived publisher that were never removed, a singleton holding scoped objects, undisposed timers, and closures that capture large objects. Confirm the trend with dotnet-counters: the GC heap size keeps rising even after gen 2 collections. Then take two dotnet-gcdump snapshots minutes apart, compare which types grew, and follow their GC roots to the owner. Fix the ownership, and give caches limits.
Practice questions for November 2026
New every month3 questions from the C# and .NET mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.
-
After context.Products.Add(item); what writes the new row to the database?
- Calling context.Products.ToList()
- Add writes it immediately
- Calling context.SaveChanges() or SaveChangesAsync()
- Disposing the context without saving
Show the answer
Answer: Calling context.SaveChanges() or SaveChangesAsync(). Add only marks the entity as Added in the change tracker. SaveChanges sends the INSERT, along with any other pending changes, to the database. Disposing the context without saving discards those changes.
-
In .NET 8, how can a constructor ask for the "upi" implementation when several IPaymentGateway services are registered?
- Name the constructor parameter upiGateway, and the container matches the implementation by parameter name
- Register it with AddKeyedScoped<IPaymentGateway, UpiGateway>("upi") and use [FromKeyedServices("upi")]
- Put [Inject("upi")] on a private field
- It is not possible without a third-party container
Show the answer
Answer: Register it with AddKeyedScoped<IPaymentGateway, UpiGateway>("upi") and use [FromKeyedServices("upi")]. Keyed services, new in .NET 8, register an implementation under a key. Consumers choose one with [FromKeyedServices("upi")] or GetRequiredKeyedService. Parameter names are never used to match services.
-
In C# 10 and later on .NET 6+, what does the compiler do for string s = $"Order {id} for {amount}"; where id is an int and amount a decimal?
- It builds the string at compile time
- It concatenates ToString() results with + in a loop
- It uses DefaultInterpolatedStringHandler, appending the values without boxing them
- It calls string.Format("Order {0} for {1}", id, amount), boxing both the int and the decimal
Show the answer
Answer: It uses DefaultInterpolatedStringHandler, appending the values without boxing them. Interpolated string handlers (C# 10) lower $"..." to AppendLiteral and generic AppendFormatted<T> calls on a pooled buffer, so value types are not boxed and fewer temporary strings are made than with string.Format.
Ready to apply?
There are 844 open jobs asking for C# and .NET on CoreHRx right now. Create a free profile once and apply with it.