Menu
CoddyTech

Advanced C# Interview Questions

Delegates, generics, variance, dependency injection, memory leaks, LINQ performance and the ASP.NET Core pipeline.

12 questions
  1. What is the difference between a delegate and an event in C#?

    A delegate is a type-safe reference to one or more methods (Func<int, int>, Action<string>). An event wraps a delegate so outside code can only += and -=; only the declaring class can raise it.

    C#
    using System;
    
    class Thermostat
    {
        public event Action<int> TooHot;     // outside code can only += or -=
    
        public void Report(int temp)
        {
            if (temp > 30 && TooHot != null)
                TooHot(temp);                  // only this class can raise it
        }
    }
    
    class Program
    {
        static void Main()
        {
            Func<int, int, int> add = (a, b) => a + b;
            Console.WriteLine(add(2, 3));
    
            var t = new Thermostat();
            t.TooHot += temp => Console.WriteLine("alert: " + temp);
            t.TooHot += temp => Console.WriteLine("logged: " + temp);
            t.Report(25);
            t.Report(35);
            // t.TooHot = null;  does not compile outside Thermostat
        }
    }

    A publisher keeps every subscriber alive until it unsubscribes, a common memory leak. See delegates.

  2. What are generic constraints in C#? Give an example.

    A constraint (where T : ...) limits which types T can be, so generic code can use members those types guarantee: class, struct, new(), a base class or an interface.

    C#
    using System;
    using System.Collections.Generic;
    
    class Program
    {
        static T Max<T>(IEnumerable<T> items) where T : IComparable<T>
        {
            bool first = true;
            T best = default(T);
            foreach (var item in items)
            {
                if (first || item.CompareTo(best) > 0) best = item;
                first = false;
            }
            return best;
        }
    
        static T Create<T>() where T : new() { return new T(); }
    
        static void Main()
        {
            Console.WriteLine(Max(new[] { 3, 9, 4 }));
            Console.WriteLine(Max(new[] { "pear", "apple", "zucchini" }));
            Console.WriteLine(Create<List<int>>().Count);
        }
    }

    .NET generics are reified, unlike Java's: List<int> is a real runtime type with no boxing. See generics.

  3. What are covariance and contravariance in C# generics?

    Covariance (out T) lets an IEnumerable<string> be used as an IEnumerable<object>; contravariance (in T) lets an Action<object> be used as an Action<string>.

    C#
    using System;
    using System.Collections.Generic;
    
    class Program
    {
        static void Main()
        {
            IEnumerable<string> names = new List<string> { "a", "b" };
            IEnumerable<object> objects = names;          // covariance: out T
            foreach (var o in objects) Console.Write(o + " ");
            Console.WriteLine();
    
            Action<object> printAny = x => Console.WriteLine("got " + x);
            Action<string> printString = printAny;        // contravariance: in T
            printString("text");
    
            // List<object> list = new List<string>();    does not compile: List<T> is invariant
        }
    }

    List<T> is invariant because it both reads and writes T. Variance works only on interfaces and delegates, and only for reference types.

  4. What is the difference between throw; and throw ex;?

    throw; rethrows the caught exception with its original stack trace; throw ex; resets the trace to the current line, so logs lose where the error happened.

    C#
    using System;
    using System.Runtime.CompilerServices;
    
    class Program
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        static void Inner() { throw new InvalidOperationException("boom"); }
    
        static void RethrowEx()
        {
            try { Inner(); }
            catch (Exception ex) { throw ex; }
        }
    
        static void Rethrow()
        {
            try { Inner(); }
            catch (Exception) { throw; }
        }
    
        static void Main()
        {
            try { RethrowEx(); }
            catch (Exception ex) { Console.WriteLine("throw ex keeps Inner: " + ex.StackTrace.Contains("Inner")); }
    
            try { Rethrow(); }
            catch (Exception ex) { Console.WriteLine("throw keeps Inner:    " + ex.StackTrace.Contains("Inner")); }
        }
    }

    To add context, wrap it: throw new OrderException("Save failed", ex) keeps the original as InnerException.

  5. What do lambdas capture from a for loop versus a foreach loop?

    C#
    using System;
    using System.Collections.Generic;
    
    class Program
    {
        static void Main()
        {
            var actions = new List<Action>();
            for (int i = 0; i < 3; i++)
                actions.Add(() => Console.Write(i));
            foreach (var j in new[] { 0, 1, 2 })
                actions.Add(() => Console.Write(j));
    
            foreach (var a in actions) a();
            Console.WriteLine();
        }
    }

    A lambda captures the variable, not its value. A for loop has one i, which is 3 when the lambdas run: 333. Since C# 5, foreach declares a fresh variable per iteration: 012. Fix the for loop with a copy in the body: int copy = i;.

  6. What are the dependency injection lifetimes in ASP.NET Core, and what is a captive dependency?

    ASP.NET Core has three lifetimes: Transient (a new instance every resolve), Scoped (one per HTTP request) and Singleton (one for the app).

    builder.Services.AddTransient<IEmailFormatter, EmailFormatter>(); // stateless, cheap
    builder.Services.AddScoped<AppDbContext>();                       // one per request
    builder.Services.AddSingleton<IClock, SystemClock>();             // shared, thread-safe
    
    // Captive dependency: a singleton holding a scoped service
    public class ReportCache(AppDbContext db) { }                     // registered as singleton: bug
    

    A captive dependency is a longer-lived service holding a shorter-lived one, such as a singleton keeping one scoped DbContext forever, across requests and threads. A singleton that needs scoped work creates scopes with IServiceScopeFactory.

  7. How can a .NET application leak memory if it has a garbage collector, and how do you find the leak?

    A .NET leak is an object that stays reachable by mistake: event handlers never unsubscribed from a long-lived publisher, static caches that only grow, timers never disposed. To find one, take two dotnet-gcdump snapshots minutes apart and compare which types grew and which roots keep them alive.

  8. How do you implement a thread-safe singleton in C#?

    The simplest is a static readonly field, because the CLR runs static initialization once even with many threads. For laziness, use Lazy<T>, which is thread-safe by default.

    C#
    using System;
    using System.Threading.Tasks;
    
    sealed class Config
    {
        private static readonly Lazy<Config> instance = new Lazy<Config>(() => new Config());
        public static Config Instance { get { return instance.Value; } }
    
        public static int Created;
        private Config() { Created++; }
    }
    
    class Program
    {
        static void Main()
        {
            Parallel.For(0, 1000, i => { var c = Config.Instance; });
            Console.WriteLine("instances created: " + Config.Created);
            Console.WriteLine(ReferenceEquals(Config.Instance, Config.Instance));
        }
    }

    In ASP.NET Core, register the class with AddSingleton instead, so tests can replace it.

  9. What are common LINQ performance mistakes you look for in code review?

    Enumerating a deferred query more than once (call ToList() once), Count() > 0 instead of Any(), and queries inside loops: Where(...).First() per item (build a Dictionary once) or lazy loading per row in Entity Framework, the N+1 problem (use Include or a projection). With EF, filter before ToList() so SQL does the work.

  10. What are attributes and reflection in C#, and when are they used?

    An attribute is metadata attached to code ([Obsolete], [HttpGet]); reflection reads types and attributes at run time. Frameworks use both to find routes, serializable properties and test methods.

    C#
    using System;
    using System.Reflection;
    
    [AttributeUsage(AttributeTargets.Property)]
    class ColumnAttribute : Attribute
    {
        public string Name { get; private set; }
        public ColumnAttribute(string name) { Name = name; }
    }
    
    class User
    {
        [Column("user_name")] public string Name { get; set; }
        [Column("user_age")] public int Age { get; set; }
    }
    
    class Program
    {
        static void Main()
        {
            foreach (PropertyInfo p in typeof(User).GetProperties())
            {
                var col = (ColumnAttribute)Attribute.GetCustomAttribute(p, typeof(ColumnAttribute));
                Console.WriteLine(p.Name + " -> " + col.Name);
            }
        }
    }

    Reflection is slow and works badly with trimming and Native AOT, so modern libraries use source generators. See reflection and attributes.

  11. How do you design exception handling in a large C# application?

    Catch only where you can do something useful, and let the rest reach one global handler that logs and returns a clean error. Use TryParse-style methods for expected failures, catch specific types, wrap to add context, and filter with catch ... when.

    C#
    using System;
    
    class InsufficientFundsException : Exception
    {
        public decimal Shortfall { get; private set; }
        public InsufficientFundsException(decimal shortfall)
            : base("Short by " + shortfall) { Shortfall = shortfall; }
    }
    
    class Program
    {
        static void Withdraw(decimal balance, decimal amount)
        {
            if (amount > balance) throw new InsufficientFundsException(amount - balance);
        }
    
        static void Main()
        {
            try
            {
                Withdraw(100m, 130m);
            }
            catch (InsufficientFundsException ex) when (ex.Shortfall < 50m)
            {
                Console.WriteLine("Offer overdraft: " + ex.Message);
            }
        }
    }

    In ASP.NET Core, an IExceptionHandler (.NET 8) maps exceptions to ProblemDetails responses.

  12. How does the ASP.NET Core middleware pipeline work?

    Each request passes through an ordered chain of middleware; each component can act on the request, call next(), and act on the response on the way back. Skipping next() short-circuits the pipeline.

    var app = builder.Build();
    
    app.UseExceptionHandler("/error");   // outermost: catches what later steps throw
    
    app.Use(async (context, next) =>     // custom inline middleware
    {
        var sw = Stopwatch.StartNew();
        await next();                    // the rest of the pipeline runs here
        app.Logger.LogInformation("{Path} took {Ms} ms", context.Request.Path, sw.ElapsedMilliseconds);
    });
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();                // may short-circuit for files
    app.UseRouting();
    app.UseAuthentication();             // who are you
    app.UseAuthorization();              // are you allowed
    app.MapControllers();
    

    Order matters: the exception handler goes first, and authentication before authorization.

Coddy programming languages illustration

Learn to code with Coddy

Get started