These 27 C# and .NET interview questions are built for hiring engineers who will own ASP.NET Core services in production, not just write controllers. They cover the language through C# 14, EF Core, dependency injection, async behavior, memory and the runtime, and the migration and resilience work that fills a senior .NET engineer's week. When Ryz vets C# developers, recruiters first check what candidates built and ran, then candidates complete structured NTRVSTA AI interviews on topics like these, and recruiters review them before and after. The AI scores are advisory. People decide who moves forward.
Many .NET candidates come from long careers on .NET Framework, and some have never shipped on modern .NET. Use the fundamentals to confirm they write current C#, then spend senior interviews on the async and runtime section, where production incidents actually come from.
The query was materialized or cast to IEnumerable<T> before the Where, so the filter ran in memory as LINQ to Objects instead of being translated to SQL. Common culprits are a ToList() too early, a repository method returning IEnumerable, or a filter calling a C# method EF cannot translate. Keep the query as IQueryable<T> until the final projection.
What a strong answer shows: They know the difference between expression trees and delegates, and check the generated SQL.
LINQ is lazily evaluated. Each enumeration, such as calling Count() and then iterating with foreach, runs the query again. Materialize once with ToListAsync() when you need the results more than once.
What a strong answer shows: A clear model of deferred execution and its cost against a real database.
Use a struct for small, immutable values with no identity, such as a coordinate or a money amount, where avoiding heap allocation matters. Large structs are copied on every assignment, and structs get boxed when cast to an interface or object, which can erase the benefit. readonly struct and record struct make intent explicit.
What a strong answer shows: Awareness of copying and boxing costs rather than "structs are faster".
They are compile-time analysis only. With <Nullable>enable</Nullable>, the compiler warns when you dereference something that might be null, but nothing changes at runtime. Deserialized JSON, reflection, the ! operator and unannotated libraries can all put nulls where the types say none exist, so validate at the boundaries.
What a strong answer shows: They treat warnings as errors in new code and still validate external input.
with expressions?Records provide value-based equality, a readable ToString, deconstruction and non-destructive mutation through with. The catch is that with makes a shallow copy: a record containing a List<T> shares that list with its copy.
public record Order(Guid Id, IReadOnlyList<Line> Lines, OrderStatus Status);
var shipped = order with { Status = OrderStatus.Shipped };
// shipped.Lines and order.Lines are the same list instance
What a strong answer shows: They know value equality compares references for collection members, and use immutable collections where it matters.
This is a captive dependency. DbContext is registered as scoped and is not thread-safe, but a singleton that receives it keeps one instance for the life of the app and shares it across concurrent requests. Fix it by making the consumer scoped, or by injecting IDbContextFactory<T> or IServiceScopeFactory and creating a short-lived context per operation. Scope validation in Development catches this at startup.
What a strong answer shows: Fluency with DI lifetimes and the real failure mode of mixing them.
IDisposable, IAsyncDisposable and a finalizer?Dispose releases resources deterministically, usually through a using declaration. DisposeAsync does the same for resources that need async cleanup, such as flushing a stream, with await using. A finalizer is a last-chance cleanup the GC runs at an unknown time; it delays collection and is rarely needed now that SafeHandle wraps native handles.
What a strong answer shows: They know finalizers are a cost, not a pattern to copy.
Lazy loading or a loop over navigation properties causes N+1 queries. Use Include and ThenInclude, or better, project straight to a DTO with Select so EF fetches only the needed columns. For read-only paths add AsNoTracking(). If several collection includes create a large cartesian product, AsSplitQuery() trades one big join for a few smaller queries.
What a strong answer shows: Projection as the default for reads, and the cartesian explosion trade-off.
new HttpClient() per request runs out of sockets. What is the right pattern?Disposing HttpClient leaves sockets in TIME_WAIT, so a client per request exhausts ports. A single static client fixes that but ignores DNS changes. Use IHttpClientFactory with typed clients, or a long-lived client whose SocketsHttpHandler sets PooledConnectionLifetime.
What a strong answer shows: They know both failure modes, sockets and stale DNS.
[Authorize] endpoint succeed without a token. What do you check in the middleware pipeline?Middleware runs in registration order. UseAuthentication must come before UseAuthorization, and both after UseRouting when routing is explicit. Also check for a fallback policy, an [AllowAnonymous] on the controller, and whether the endpoint is a minimal API that needs RequireAuthorization().
What a strong answer shows: They understand the pipeline as ordered code, not configuration.
IOptions, IOptionsSnapshot and IOptionsMonitor?IOptions<T> is a singleton read once. IOptionsSnapshot<T> is scoped and re-reads config per request, so it cannot be injected into singletons. IOptionsMonitor<T> is a singleton that sees changes and raises OnChange. Add ValidateDataAnnotations() and ValidateOnStart() so bad config fails the deploy, not the first request.
What a strong answer shows: Lifetimes again, and a habit of failing fast on configuration.
Switch expressions with property, relational and list patterns replace nested if/else chains, and the compiler warns when a case is missing.
decimal Shipping(Order o) => o switch
{
{ Total: >= 100m } => 0m,
{ Address.Country: "US", Lines.Count: <= 2 } => 5m,
{ Address.Country: "US" } => 8m,
_ => 25m
};
What a strong answer shows: They use patterns for clarity, order cases from most to least specific and know arms are evaluated top to bottom.
Minimal APIs have less ceremony, start faster, work well with Native AOT and suit small, focused services. Route groups and endpoint filters cover most cross-cutting needs. Controllers still make sense for large APIs that rely on filters, conventions and model binding features the team already knows. Consistency within a codebase matters more than the choice.
What a strong answer shows: A pragmatic call grounded in team and deployment needs.
async void dangerous?The caller cannot await it, so it cannot know when the work finishes, and an exception thrown inside it is raised on the synchronization context or thread pool, which can crash the process. It exists for event handlers. Everywhere else return Task.
What a strong answer shows: They know the crash behavior, not just the style rule.
GetDataAsync().Result. Why, and does the same happen in ASP.NET Core?In classic ASP.NET or a UI app, the awaited continuation tries to resume on the captured synchronization context, which is blocked by .Result, so both wait forever. ConfigureAwait(false) in library code avoids capturing the context. ASP.NET Core has no synchronization context, so it does not deadlock this way, but blocking on async still wastes thread pool threads and leads to starvation under load.
What a strong answer shows: They know why the deadlock happens and why sync-over-async is still wrong on modern .NET.
Thread pool starvation. Blocking calls such as .Result, Thread.Sleep or synchronous I/O hold threads, and the pool only adds threads gradually, so queued work waits. Confirm with dotnet-counters, watching thread pool queue length and thread count, and use dotnet-stack or a dump to see where threads are blocked. Fix the blocking calls rather than raising the minimum thread count.
What a strong answer shows: The low-CPU, high-latency signature and the right diagnostic tools.
Accept a CancellationToken in the endpoint, which ASP.NET Core binds to HttpContext.RequestAborted, and pass it to every async call. Link it with a timeout when calling slow dependencies.
app.MapGet("/report/{id}", async (int id, ReportService svc, CancellationToken ct) =>
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(10));
return await svc.BuildAsync(id, cts.Token);
});
What a strong answer shows: They thread tokens all the way down and avoid wasting work for clients that already left.
ValueTask beat Task?When a method usually completes synchronously, such as a cache hit, ValueTask avoids allocating a Task. It has rules: await it once, do not await it concurrently, and do not call .Result before it completes. For most application code Task is the right default.
What a strong answer shows: They know the constraints and do not apply it everywhere.
Profile first with dotnet-counters and a memory profiler. Then avoid Substring and Split by slicing ReadOnlySpan<char>, rent buffers from ArrayPool<T>, and use stackalloc for small fixed buffers.
static int SumCsv(ReadOnlySpan<char> line)
{
int total = 0;
foreach (Range r in line.Split(','))
total += int.Parse(line[r]);
return total;
}
Spans cannot live on the heap or cross an await; use Memory<T> there.
What a strong answer shows: Measured optimization and the span restrictions that come with it.
The GC is generational, and objects of 85,000 bytes or more go to the large object heap, which is collected with gen 2. Server GC uses a heap per core for throughput; Workstation GC uses less memory. In containers, memory limits shape heap sizing, and since .NET 9 Server GC adapts its heap count to load by default (DATAS), which lowers memory use. Watch gen 2 collection rate, pause time and LOH allocations.
What a strong answer shows: They tie GC modes to deployment shape and know which metrics to watch.
lock cannot contain an await, so use SemaphoreSlim(5) with WaitAsync and release in a finally, or Parallel.ForEachAsync with MaxDegreeOfParallelism = 5. For producer and consumer pipelines, a bounded Channel<T> gives backpressure. For plain synchronous locking, .NET 9 added System.Threading.Lock.
What a strong answer shows: The right primitive for async code and a reason for each choice.
Inventory blockers first: System.Web, WCF services, Web Forms, AppDomains and Windows-only libraries. Move class libraries to .NET Standard 2.0 or multi-target them, then migrate incrementally using a strangler approach with YARP in front, routing one area at a time to a new ASP.NET Core app. WCF servers can move to CoreWCF or gRPC. The .NET Upgrade Assistant helps with project files, but the hard parts are behavioral.
What a strong answer shows: An incremental plan that keeps shipping, not a big-bang rewrite.
Native AOT gives fast startup and small memory use, which suits serverless functions, CLIs and dense container deployments. The cost is no runtime code generation: reflection-heavy libraries, dynamic proxies and some serializers break or need source generators, such as System.Text.Json's JsonSerializerContext. Treat trimming warnings as errors and test the published binary, not just the debug build.
What a strong answer shows: They know what breaks and how to verify compatibility.
Use Microsoft.Extensions.Http.Resilience, built on Polly v8, and add AddStandardResilienceHandler() to the typed client for timeouts, retries with jitter and a circuit breaker. Retry only idempotent operations, set a total timeout, and make sure upstream callers do not also retry, or one slow dependency turns into a retry storm.
What a strong answer shows: System-level thinking about retries, not just adding a library.
Write the invoice and an outbox row in one transaction, then have a BackgroundService or a dedicated worker poll the outbox and publish to a queue such as Azure Service Bus or SQS. Make handlers idempotent and honor the stopping token for graceful shutdown. Hangfire or Quartz.NET fit when you need scheduling and a dashboard.
What a strong answer shows: Durability across crashes and deploys, plus idempotency.
Options range from a shared schema with a TenantId column, to a schema per tenant, to a database per tenant. With a shared schema, global query filters (HasQueryFilter) apply the tenant automatically, but code using IgnoreQueryFilters or raw SQL bypasses them, so add tests and consider database row-level security as a backstop.
What a strong answer shows: Trade-offs between cost and isolation, and defense in depth.
Structured logging through ILogger with message templates, plus the [LoggerMessage] source generator on hot paths. OpenTelemetry for traces and metrics, with ASP.NET Core, HttpClient and EF Core instrumentation and exporters to the team's backend. Health checks for readiness and liveness. .NET Aspire helps wire this up locally with a dashboard.
What a strong answer shows: Correlated logs, traces and metrics, and attention to logging cost.
.Result or .Wait() casually and does not see a problem in ASP.NET Core.DbContext into a singleton without noticing.HttpClient instances per call.dotnet-counters, dotnet-trace or any profiler.Share an ASP.NET Core service on .NET 10 with EF Core and SQL Server or Postgres in a container. It has an endpoint that returns customer statements and calls a flaky mock pricing API. Plant problems: an N+1 query, a scoped service captured in a singleton, a .Result call, and no cancellation or timeout on the outbound call. In a 90-minute pairing session, ask the candidate to make the endpoint fast and stable under a short load test with a tool such as k6 or bombardier.
WebApplicationFactory and Testcontainers?Ryz can introduce senior C# and .NET developers who have already been assessed on this material. They are the top 1% of the candidates we interview, they work on your team, in your repos and standups, and they keep hours within ±1h of US time zones. See the steps of our vetting process, or use our C# / .NET developer job description as a starting point.
Only if you maintain Framework apps. In that case, ask about migration rather than Web Forms trivia: what blocks a move to modern .NET and how they would ship incrementally. For new services, test modern .NET and ASP.NET Core.
Share a short snippet with a hidden deadlock, a missing await or an async void and ask the candidate to review it aloud. Code review shows more about async understanding than asking for definitions.
Live pairing on an existing codebase is usually better for senior roles because most of the job is changing code someone else wrote. A short take-home works for candidates who prefer it, as long as you review it with them afterward.
Questions we didn't answer? Email info@ryzlabs.com.