📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How are extension methods resolved and when should you use them? Explain how the compiler binds them, why instance methods always win, null receivers, ref extension methods on structs, the C# 14 extension block syntax for extension properties and static members, and the design mistakes extension methods invite.

Extension methods beyond the basics: binding and precedence rules, namespace scoping, null receivers, ref this for structs, C# 14 extension members, and good versus bad uses.

Mid-Level Theory Intermediate api-design csharp csharp-14 extension-members extension-methods

🎯 Key points

  • An extension method is a static method in a static class with `this` on the first parameter; calls compile to a static call
  • Binding happens at compile time: instance members first, then extensions found via `using` directives, closest namespace first
  • An instance method with a matching signature always wins, so adding one to a type silently changes which method runs
  • The receiver can be null; the method decides what to do (no automatic NullReferenceException)
  • `this ref` / `this in` extensions on structs avoid copies and allow mutation of the caller's value
  • C# 14 extension blocks (`extension(Order o) { ... }`) add extension properties, static extension members and operators
  • Extensions cannot access private state and do not participate in virtual dispatch or interfaces
  • Good uses: fluent APIs, LINQ-style helpers, DI registration (`AddMyService`), adapting types you do not own

⏱ 30-second answer

Extension methods are static methods with this on the first parameter. The compiler binds them at compile time, only after looking for instance members, and it searches the namespaces you've imported, closest first. An instance method with the same signature always wins, so adding one to the type can quietly change what your call does. The receiver can be null, and with this ref you can extend structs without copying. C# 14 adds extension blocks, so you can declare extension properties and static members too. I use them for fluent APIs and DI registration, not to hide business logic.

🗣 Full answer (senior)

How I would answer this in an interview

Extension methods are static methods that the compiler lets you call as if they were instance methods on another type. They're how LINQ adds operators to IEnumerable<T>, how ASP.NET Core builds fluent configuration (services.AddX(), app.UseY()), and how you add convenience to types you don't own. I'd explain binding rules, why instance methods win, null receivers, ref extensions on structs, the C# 14 extension block syntax, and the design mistakes they invite.

1. Declaring and binding

namespace Ordering.Extensions;

public static class MoneyExtensions
{
    public static decimal WithGst(this decimal amount, decimal rate = 0.10m) => decimal.Round(amount * (1 + rate), 2);
}

using Ordering.Extensions;
var gross = 100m.WithGst();      // compiled as MoneyExtensions.WithGst(100m)

Rules: a static method in a non-generic, non-nested static class, with this on the first parameter.

How the compiler binds receiver.Method(args):

  1. Look for instance members named Method on the receiver's type (and base types/interfaces). If any applicable instance method exists, use it — extension methods aren't considered.
  2. Otherwise, search for extension methods in enclosing namespaces, from the innermost outwards, then using directives at each level. At the first scope containing applicable candidates, run overload resolution among them; stop there.
  3. Ambiguity between equally good extensions in the same scope → CS0121.

Binding is compile-time and based on the receiver's static type — no virtual dispatch.

2. Why instance methods always win

It protects types from having their behaviour changed by someone else's extension: if List<T> has Contains, a Contains extension can never hijack calls on a List<T>. The flip side: if a library later adds an instance method with the same name and compatible signature, your extension silently stops being called on the next compile — behaviour can change without any code change on your side.

3. Null receivers

Because an extension call is just a static call, the receiver can be null:

public static bool IsBlank(this string? s) => string.IsNullOrWhiteSpace(s);
string? name = null;
name.IsBlank();   // true, no NullReferenceException

Useful for null-tolerant helpers, but it can mislead readers (it looks like an instance call that should throw). Validate (ArgumentNullException.ThrowIfNull) unless null-tolerance is the explicit purpose, and annotate nullability.

4. ref extension methods on structs (C# 7.2)

public static void Advance(ref this Cursor cursor, int count) => cursor.Position += count;   // mutates caller's struct
public static int Remaining(in this Cursor cursor) => cursor.Length - cursor.Position;     // no copy (readonly struct)
  • ref this lets an extension mutate the caller's struct in place (receiver must be a variable, not an rvalue).
  • in this avoids copying large readonly structs.
  • Only allowed for value types (ref this requires struct constraint for generic receivers).

5. C# 14 extension blocks

C# 14 introduces a new syntax that groups extension members by receiver and supports extension properties, static extension members and operators:

public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T> source)
    {
        public bool IsEmpty => !source.Any();                                    // extension property
        public IEnumerable<T> WhereNotNull() => source.Where(x => x is not null);  // extension method
    }

    extension<T>(IEnumerable<T>)
    {
        public static IEnumerable<T> Empty => [];                                // static extension member
    }
}

if (orders.IsEmpty) return;
var none = IEnumerable<Order>.Empty;

The old this-parameter syntax still works and is binary compatible; the new syntax compiles to similar static methods plus metadata. Extension properties are still computed — they can't add state to the type.

6. Design mistakes extension methods invite

  • Business logic in extensions (order.CalculateDiscount() in a utilities namespace) — hard to find, can't access private state, multiple competing versions.
  • Hidden I/O (customer.LoadOrders()) disguised as a cheap member call.
  • Polluting IntelliSense: extensions on object, string or unconstrained T in common namespaces appear everywhere.
  • Duplicate/ambiguous extensions across libraries (ToJson, ToSlug), with behaviour depending on which using is in scope.
  • Fragility when the extended type later adds an instance member.
  • Testing/mocking: static, so callers can't substitute them — fine for pure helpers, bad for anything with dependencies.

Good uses: fluent configuration (IServiceCollection, IApplicationBuilder), LINQ-style operators, adapters/convenience over types you don't own, and domain-specific helpers on value objects in a clearly named namespace.

7. A production example

Two shared libraries both defined string ToSlug(this string) with different rules; which one ran depended on using order per file, so product URLs differed between the website and the sitemap generator, breaking SEO links. We consolidated into one implementation in a dedicated namespace, banned the duplicate via BannedSymbols.txt, and added tests comparing slugs across services.

🔍 What the interviewer is checking

Checks deeper understanding than "static method with this".

Green flags: binding order and instance precedence; null receivers; ref extensions; C# 14 extension blocks correctly described; design judgement on when not to use them.

Red flags: thinks extensions can override instance methods or access private members; unaware of namespace scoping.

📚 Learn in depth

1. Compilation and metadata

The compiler marks extension methods (and their containing class and assembly) with [Extension] (System.Runtime.CompilerServices.ExtensionAttribute). Callers compile x.M(a) to call ExtClass::M(x, a). Reflection sees an ordinary static method. Other .NET languages (VB.NET) honour the attribute.

2. Lookup details

  • Extension methods are only found if their namespace is imported (using, global using) or encloses the call site.
  • C# 6 allowed using static to import extension methods from a specific static class.
  • Generic extensions participate in type inference using the receiver as the first argument.
  • Extensions apply to interfaces (LINQ on IEnumerable<T>), so all implementers get them.
  • Since C# 9, GetEnumerator extension methods make types usable in foreach; GetAwaiter extensions make types awaitable; Deconstruct extensions enable deconstruction.

3. C# 14 extension members under the hood

The compiler emits:

  • static methods in the containing class for each member (e.g. get_IsEmpty(IEnumerable<T> source) for an extension property getter),
  • an "extension marker" type/metadata grouping members by receiver so the compiler can bind orders.IsEmpty and IEnumerable<T>.Empty.

Disambiguation: you can call the static form directly via the containing class. Existing this-style methods and new-style members can coexist.

4. Performance

Extension calls are ordinary static calls — inlinable, no overhead beyond the method itself. in/ref extensions on large structs avoid copies.

5. Designing extension APIs

  • Put them in a namespace matching their purpose (Microsoft.Extensions.DependencyInjection for AddX methods so they appear where services are configured).
  • Keep them small and pure, or clearly named when they do work.
  • Constrain generic receivers to avoid IntelliSense pollution.
  • Document null behaviour.
  • Prefer real members on types you own.

Try it yourself

  1. Write an extension Contains for List<int> and observe that the instance method is always called.
  2. Call an extension on a null receiver.
  3. Write ref this and in this extensions on a struct and inspect copies with a mutable field.
  4. Convert an extension method set to a C# 14 extension block with a property.

💻 Code

namespace Advanced.Extensions;

public sealed record Order(int Id, decimal Total, bool IsCancelled);

public struct Counter { public int Value; }

public static class StringExtensions
{
    // Classic extension method; works with a null receiver
    public static bool IsBlank(this string? value) => string.IsNullOrWhiteSpace(value);
}

public static class CounterExtensions
{
    // ref extension: mutates the caller's struct, no copy
    public static void Increment(this ref Counter counter) => counter.Value++;
}

public static class OrderExtensions
{
    // C# 14 extension block: properties and static members
    extension(Order order)
    {
        public bool IsLarge => order.Total > 1_000m;
        public Order Cancel() => order with { IsCancelled = true };
    }

    extension(Order)
    {
        public static Order Empty => new(0, 0m, false);
    }

    // Sequence helper in LINQ style
    public static IEnumerable<Order> ActiveOnly(this IEnumerable<Order> orders) => orders.Where(o => !o.IsCancelled);
}

public static class Program
{
    public static void Main()
    {
        string? missing = null;
        Console.WriteLine(missing.IsBlank());          // True, no NullReferenceException

        var counter = new Counter();
        counter.Increment();
        counter.Increment();
        Console.WriteLine(counter.Value);              // 2

        var big = new Order(1, 2_500m, false);
        Console.WriteLine(big.IsLarge);                // extension property
        Console.WriteLine(big.Cancel().IsCancelled);   // True
        Console.WriteLine(Order.Empty.Id);             // static extension member

        List<Order> orders = [big, new(2, 50m, true)];
        Console.WriteLine(orders.ActiveOnly().Count());
    }
}

⚠ Common mistakes

1. Business rules as extensions

What happens: core domain logic implemented as extensions in utility namespaces. Why it is wrong: scattered, duplicated and unable to protect invariants. Do instead: put behaviour on the domain type or a domain service.

2. Hiding I/O behind extensions

What happens: user.GetPermissions() performs database calls. Why it is wrong: looks cheap; used in loops; untestable. Do instead: use injected services with explicit async methods.

3. Extending object or unconstrained T broadly

What happens: public static string ToJson<T>(this T value) in a common namespace. Why it is wrong: IntelliSense pollution and accidental use everywhere. Do instead: constrain, or place in a dedicated namespace.

4. Duplicate extensions across libraries

What happens: two ToSlug methods in different namespaces. Why it is wrong: behaviour depends on imports; ambiguities. Do instead: single owned implementation; banned APIs for duplicates.

5. Ignoring the instance-method-wins rule

What happens: relying on an extension that a later library version shadows with an instance method. Why it is wrong: behaviour changes silently on upgrade. Do instead: test behaviour, prefer distinctive names, review upgrade notes.

6. Surprising null handling

What happens: extensions that accept null receivers without documenting it. Why it is wrong: readers assume non-null receivers. Do instead: validate or clearly name null-tolerant helpers.

7. Expecting extension properties to store data

What happens: trying to add state with C# 14 extension properties. Why it is wrong: they're computed; no storage is added. Do instead: use ConditionalWeakTable for attached data, or a wrapper type.

8. Using extensions to avoid proper dependencies

What happens: static extension methods reading configuration or services via static locators. Why it is wrong: hidden dependencies and poor testability. Do instead: inject services.

🏛 Senior insight: trade-offs

Extension methods are a compile-time convenience with real design consequences: binding by namespace, losing to instance members, and invisibility to mocking. Seniors use them for fluent and adapter code and keep domain behaviour on the types themselves. C# 14 extension members are a genuine improvement for library authors (extension properties and static factories) and I would adopt them gradually, since they are source- and binary-compatible with existing extension methods.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions static classes and the this modifier. Why: Checks the basic method and extension method rules behind the advanced answer.
    How to answer

    A method is a named block of code with a signature: its name plus the number, types, order and ref/out/in kinds of its parameters (the return type is not part of it, so you cannot overload by return type alone). Instance methods run on an object and can use this; static methods belong to the type and cannot. Overloading means several methods with the same name and different signatures; the compiler picks the best match at compile time, preferring exact types over conversions and normal forms over params. Expression-bodied methods (=> expr) are short forms for one expression. Extension methods are static methods with a this parameter that look like instance methods; an instance method always wins if one fits. C# 14 adds extension blocks, which also allow extension properties and static extension members.

  2. When: Candidate mentions extension properties or extension blocks. Why: Probes C# 14 extension blocks and static class design.
    How to answer

    A static class cannot be instantiated or inherited and contains only static members; in IL it is abstract sealed. It is the right home for stateless helpers (Math, Path), constants and extension methods, which must be declared in a non-generic, non-nested static class. C# 14 adds extension blocks (extension(Order o) { ... }) inside static classes, so you can add extension properties and static extension members too. Static classes cannot implement interfaces or be injected, so static state is global, shared across threads and hard to replace in tests. Prefer a DI-registered singleton when something has state, configuration or I/O.

  3. When: Candidate says extension methods can add methods to types they do not own. Why: Deconstruct can be an extension method, a common real use.
    How to answer

    Deconstruction unpacks a value into separate variables, like var (id, name) = customer;. Tuples support it built in, positional records generate a Deconstruct method, and any other type works if it has a Deconstruct(out ..., out ...) method, which can be an extension, so you can add it to types you don't own. You can skip parts with _, deconstruct dictionary entries in foreach, and since C# 10 mix new and existing variables. Positional patterns like p is (0, var y) use the same Deconstruct method.

🛠 Real-world problems: how you handle them

Two ToDto extensions produced different API responses
Situation
A .NET 10 API had OrderMappingExtensions.ToDto in both Api.Mapping and Api.Legacy.Mapping namespaces after a merge.
Signals
One endpoint returned prices including GST and another excluding it for the same order; no compiler errors.
Investigation
Used Go To Definition from each call site and found they bound to different ToDto methods because of different using directives.
Resolution
Deleted the legacy mapping, moved the remaining mapper into an explicit OrderMapper class used by both endpoints, and added contract tests on the response shape.
Prevention
Added an architecture test (NetArchTest) forbidding duplicate extension method names for the same receiver type across namespaces.

How to tell it (STAR)

**S:** Two endpoints showed different prices for the same order. **T:** Find the cause before customers noticed. **A:** I traced it to two extension methods with the same name in different namespaces, bound by each file's usings. I consolidated mapping into one explicit class and added contract and architecture tests. **R:** Prices were consistent and the duplicate-mapping class of bug could not come back unnoticed.

More in Advanced Language Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me