How would you describe C# and what environments does it target?
Answer
C# is a multi-paradigm language running on the .NET runtime, targeting Windows desktop (WinForms, WPF), web (ASP.NET Core), mobile (Xamarin), and cloud applications.
These are the C# questions you'll encounter in technical interviews, each with a detailed written answer. Master core OOP concepts, then progress through async/await, LINQ, and the .NET ecosystem.
C# is a modern, object-oriented language developed by Microsoft. It runs on the .NET Framework and is used for web development, game development (Unity), desktop applications, and backend systems.
C# interviews focus on object-oriented design, async/await, LINQ, delegates, and the .NET ecosystem. You'll discuss memory management, garbage collection, and how to write clean, maintainable code.
C# proficiency is essential for .NET developers and critical for Unity game development. Master the language and you'll be competitive for a wide range of roles.
Reading answers is not the same as giving them. Sit a full mock interview and get scored on how you actually explain them.
Try a mock interviewAnswer
C# is a multi-paradigm language running on the .NET runtime, targeting Windows desktop (WinForms, WPF), web (ASP.NET Core), mobile (Xamarin), and cloud applications.
Answer
The CLR compiles C# to Intermediate Language (IL), then JIT compiles to native code at runtime, enabling cross-platform execution and language interoperability.
Answer
.NET (and .NET Core/.NET 5+) provides the runtime, comprehensive standard library, and development tools for diverse application types.
Answer
LINQ (Language Integrated Query) offers query methods (Where, Select, GroupBy) and SQL-like syntax that work with any data source via deferred execution.
Answer
Properties use get and set accessors (can be private) to control field access, enabling validation, computed values, and notifications on changes.
Answer
Async/await is syntactic sugar over Tasks that makes asynchronous code read like synchronous code while maintaining non-blocking execution.
Answer
Delegates define the signature of a method. Events build on delegates to enable the observer pattern, allowing multiple subscribers to handle notifications.
Answer
Lambda expressions (x => x * 2) create inline functions, essential for LINQ queries and functional patterns. They capture local variables in closures.
Answer
DI inverts control: classes depend on abstractions, not concrete implementations. DI containers manage object creation, enabling easy testing and configuration.
Answer
GC automatically frees unreachable objects but doesn't manage unmanaged resources (file handles, database connections). IDisposable and using statements handle those.
Answer
C# was designed as a modern, simple, type-safe language for the .NET platform. It combines the power of C++ with the simplicity of Java, borrowing best practices from both. C# runs on the CLR, which provides memory management, garbage collection, and runtime type safety. The language supports multiple paradigms: object-oriented (classes, inheritance, polymorphism), functional (lambdas, LINQ), and procedural. C# evolves with each .NET release, adding features like nullable reference types, records, and pattern matching. The .NET ecosystem provides comprehensive libraries for web, desktop, mobile, and cloud development.
Answer
The CLR is a virtual machine that compiles C# to Intermediate Language (IL), a platform-independent bytecode. At runtime, JIT (Just-In-Time) compilation converts IL to native machine code. Multiple languages (C#, VB.NET, F#) compile to IL, enabling interoperability: a C# class can inherit from a VB.NET class. CLR manages execution: memory allocation on the heap, garbage collection to reclaim unused objects, exception handling, and type safety through verification. This abstraction enables the same IL to run on Windows, Linux, and macOS without recompilation.
Answer
.NET provides CLR, Base Class Library (BCL) with collections, I/O, and reflection, and language compilers. The .NET Framework (Windows-only) includes additional libraries for WinForms, WPF, and ASP.NET. .NET Core (now .NET 5+) is cross-platform (Windows, Linux, macOS), open-source, and modular, allowing inclusion only of needed libraries. .NET Core enables containerization and cloud deployment. Modern development uses .NET 5+ for new projects; .NET Framework is in maintenance mode. ASP.NET Core (web framework) and Entity Framework Core (ORM) run on both platforms.
Answer
LINQ uses deferred execution: queries don't run immediately when constructed, only when enumerated (in a foreach or ToList()). This enables composing queries: Where() filters, Select() transforms, both returning IEnumerable. The query is executed once during enumeration. Different providers handle different sources: LINQ-to-Objects for collections, LINQ-to-SQL for databases, LINQ-to-XML for XML. Each provider translates LINQ methods to the appropriate query format (SQL for databases). Deferred execution allows combining filters efficiently; executing too early causes unnecessary iteration.
Answer
Properties use get/set accessors (can be separate access levels: public get, private set). Behind the scenes, they typically wrap a private field. This enables validation in the setter, notifications via PropertyChanged event, and computed values in the getter. Example: public string Email { get; set; } is automatic, storing in a compiler-generated field. The syntax feels like field access but provides method-like control. Properties can be virtual for overriding or abstract in interfaces. This design provides data hiding while maintaining clean syntax.
Answer
Async methods return Task or Task<T>. Await pauses the method, returning control to the caller. When the awaited operation completes, execution resumes. This avoids blocking threads: a web server can handle many requests without creating threads for each. Async methods are compiled to state machines by the compiler. Tasks can wrap I/O operations (file, network) that don't require CPU. Combining async I/O with thread pools enables high concurrency without thread explosion. Exception handling works as expected: try-catch around awaits catches asynchronous exceptions.
Answer
A delegate is a type-safe function pointer: public delegate void OnDataReceived(string data); declares a delegate type. A class can define an event: public event OnDataReceived DataReceived; Subscribers register: obj.DataReceived += handler; and unregister: obj.DataReceived -= handler; The owner publishes: DataReceived?.Invoke(data); This decouples publishers from subscribers. Multiple handlers can subscribe without knowing about each other. Events prevent subscribers from clearing others' handlers or invoking the event (public field would allow this). This pattern is fundamental to UI frameworks and observer implementations.
Answer
Lambda syntax is (parameters) => expression or (parameters) => { statements }. Examples: x => x * 2 or (x, y) => x + y. Lambdas capture local variables, creating closures. They integrate seamlessly with LINQ: list.Where(x => x > 10).Select(x => x.ToString()). Lambdas reduce boilerplate compared to anonymous methods or named functions. They enable functional patterns: Function<int, int> square = x => x * x. The compiler infers types from context. Lambdas can have side effects, though pure functions (no side effects) are preferred in LINQ for predictable execution.
Answer
DI containers (e.g., Microsoft.Extensions.DependencyInjection) maintain a registry: IServiceCollection.AddScoped<IRepository, Repository>(); Scoped means one instance per request; Singleton means one globally; Transient means new each time. At runtime, requesting IRepository returns a Repository instance. The container recursively resolves dependencies: if Repository needs IDbContext, the container creates that first. Configuration in Startup or Program: services.AddScoped<IUserService>(sp => new UserService(sp.GetRequiredService<IRepository>())). Constructor injection receives dependencies. This decouples classes from instantiation, enabling testing (inject mocks) and runtime substitution without code changes.
Answer
GC organizes heap into generations: Gen0 (new objects), Gen1 (survived one GC), Gen2 (old). GC collects Gen0 frequently, older generations rarely. This optimizes for objects dying young. Finalizers (~ClassName()) run before GC reclaims objects but add overhead. The IDisposable pattern is preferred: public void Dispose() { /* clean up */ } Using statements: using (var file = new StreamReader(path)) { } call Dispose automatically. For unmanaged resources (file handles, network sockets), implement IDisposable and use using statements. Modern using statements (C# 8+) simplify this. Finalizers are the safety net if Dispose wasn't called.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I created controllers inheriting from ControllerBase, each with actions returning IActionResult. I used dependency injection to inject services: private readonly IUserService _service; in the constructor. Services used interfaces for decoupling. I configured middleware: app.UseRouting() and app.UseEndpoints(). I implemented proper error handling, returning appropriate HTTP status codes. I used async actions: public async Task<IActionResult> GetUser(int id). Validation occurred in services, not controllers. I structured as Presentation → Service → Repository → Database, with interfaces between layers.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I had an API that made sequential calls to multiple external services, blocking the UI thread. Converting to async with Task.WhenAll() ran them in parallel. Each service method was async Task<T>. The controller action was async. This freed the UI thread, allowing other requests to be processed. Response time improved significantly without adding threads. I handled timeouts with CancellationToken and proper exception handling.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I created a delegate: public delegate void OnOrderStatusChanged(Order order); and an event in the Order class. UI and notification services subscribed: order.StatusChanged += (o) => UpdateUI(o); When order status changed, I invoked the event: StatusChanged?.Invoke(this); Multiple handlers executed independently. Unsubscribing prevented memory leaks. This pattern decoupled order processing from its side effects.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I was executing a LINQ query in a loop, running the query thousands of times. Calling ToList() once before the loop cached results, reducing database hits from thousands to one. I also avoided Select(x => x.Property) in the query, letting the database handle filtering before retrieving data. Understanding that LINQ-to-SQL translates to SQL taught me to defer execution and push filters to the database.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I structured the application: Domain (business logic), Application (use cases), Infrastructure (database, external services), Presentation (UI). Interfaces defined contracts between layers. Dependency injection injected implementations. The IoC container managed lifetimes. Adding new implementations didn't require changing existing code. Testing was easy: inject mocks of dependencies. This architecture scaled as the application grew.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I wrapped await calls in try-catch blocks. For multiple concurrent operations, I used Task.WhenAll() inside try-catch to catch any task's exception. I used TaskCompletionSource to convert callbacks to Tasks for consistent error handling. I ensured operations could be cancelled with CancellationToken. I logged exceptions before rethrowing or returning error responses.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I created an IQueryable provider implementing IQueryable<T> and IQueryProvider. The visitor pattern walked the expression tree, translating it to my data source's query format. I implemented methods like Where, Select, and GroupBy as extension methods. This allowed LINQ syntax to work with custom data sources without requiring the LINQ provider library.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I defined IRepository<T> with methods like GetAsync, AddAsync, UpdateAsync, SaveAsync. Repository<T> implemented this, using Entity Framework Core internally. Controllers depended on IRepository, not the concrete class. Adding new entity types required minimal code: inherit Repository<Entity> or configure mapping. This pattern made testing easy: I created a mock repository returning test data.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
Legacy code had public int Age; { get; set; }. I refactored to private int _age; public int Age { get { return _age; } set { _age = value; } }. This allowed adding validation later without changing callers. I gradually moved more logic into properties as needed. Properties are syntactic sugar for methods, maintaining backward compatibility while enabling encapsulation.
This browser cannot record audio. Answer the question out loud anyway, then compare yourself against the model answer below.
Answer
I used reflection: Assembly.GetExecutingAssembly().GetTypes() found all types. I filtered for implementations of IPlugin. For each, I registered with DI: services.AddScoped(pluginType). At runtime, requesting IEnumerable<IPlugin> gave all plugins. Each plugin was discovered and loaded without hardcoding. This enabled adding plugins by dropping assemblies in a folder.
public async Task<CombinedData> FetchDataAsync(string id) {
// Fetch from multiple APIs concurrently
}Answer
public async Task<CombinedData> FetchDataAsync(string id) {
var task1 = FetchFromApiOneAsync(id);
var task2 = FetchFromApiTwoAsync(id);
var task3 = FetchFromApiThreeAsync(id);
await Task.WhenAll(task1, task2, task3);
return new CombinedData { Data1 = await task1, Data2 = await task2, Data3 = await task3 };
}public List<ProductSummary> GetDiscountedProducts(List<Product> products, decimal minDiscount) {
// Use LINQ to filter and transform
}Answer
public List<ProductSummary> GetDiscountedProducts(List<Product> products, decimal minDiscount) {
return products
.Where(p => p.Discount >= minDiscount)
.Where(p => p.Stock > 0)
.OrderByDescending(p => p.Discount)
.Select(p => new ProductSummary { Name = p.Name, DiscountedPrice = p.Price * (1 - p.Discount) })
.ToList();
}public class Repository<T> : IRepository<T> where T : class {
private readonly DbContext _context;
// Implement CRUD operations
}Answer
public class Repository<T> : IRepository<T> where T : class {
private readonly DbContext _context;
public Repository(DbContext context) { _context = context; }
public async Task<T> GetByIdAsync(int id) { return await _context.Set<T>().FindAsync(id); }
public async Task AddAsync(T entity) { await _context.Set<T>().AddAsync(entity); }
public void Remove(T entity) { _context.Set<T>().Remove(entity); }
public async Task SaveAsync() { await _context.SaveChangesAsync(); }
}public delegate void OnStateChanged(StateChangedEventArgs args);
public class StateManager {
public event OnStateChanged StateChanged;
// Implement state change notification
}Answer
public delegate void OnStateChanged(StateChangedEventArgs args);
public class StateManager {
public event OnStateChanged StateChanged;
private string _state;
public string State {
get { return _state; }
set {
if (_state != value) {
_state = value;
StateChanged?.Invoke(new StateChangedEventArgs { NewState = value });
}
}
}
}public class User {
private string _email;
public string Email {
get { return _email; }
set {
// Add validation
}
}
}Answer
public class User {
private string _email;
public string Email {
get { return _email; }
set {
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Email cannot be empty");
if (!value.Contains("@")) throw new ArgumentException("Invalid email format");
_email = value;
}
}
}public List<Employee> SortEmployees(List<Employee> employees) {
// Use OrderBy with lambda expressions
}Answer
public List<Employee> SortEmployees(List<Employee> employees) {
return employees
.OrderBy(e => e.Department)
.ThenByDescending(e => e.Salary)
.ThenBy(e => e.Name)
.ToList();
}public List<string> ProcessOrders(List<Order> orders) {
// Use LINQ methods: Where, Select, GroupBy, OrderBy
}Answer
public List<string> ProcessOrders(List<Order> orders) {
return orders
.Where(o => o.Total > 100)
.GroupBy(o => o.Customer)
.Select(g => $"{g.Key}: {g.Count()} orders, Total: ${g.Sum(o => o.Total)}")
.OrderByDescending(s => s)
.ToList();
}public async Task<T[]> ExecuteTasksInParallel<T>(params Task<T>[] tasks) {
// Execute tasks in parallel
}Answer
public async Task<T[]> ExecuteTasksInParallel<T>(params Task<T>[] tasks) {
return await Task.WhenAll(tasks);
}public class DatabaseConnection : IDisposable {
private IntPtr _handle;
// Implement Dispose pattern
}Answer
public class DatabaseConnection : IDisposable {
private IntPtr _handle;
private bool _disposed = false;
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (!_disposed) {
if (disposing) { /* managed cleanup */ }
if (_handle != IntPtr.Zero) { CloseHandle(_handle); }
_disposed = true;
}
}
~DatabaseConnection() { Dispose(false); }
}public class DataCache {
private Dictionary<string, CachedData> _cache;
// Implement caching with dictionary
}Answer
public class DataCache {
private Dictionary<string, CachedData> _cache = new();
public CachedData GetOrCompute(string key, Func<CachedData> compute) {
if (!_cache.TryGetValue(key, out var cached)) {
cached = compute();
_cache[key] = cached;
}
return cached;
}
}