Monday, May 12, 2025

C# Polymorphism, Extension method

 Okay, here's a sample daily learning log based on our conversation. Imagine you went through these discoveries over a few days:


My Learning Log: C# Method Resolution & Dapper

Date: May 10, 2025 (Simulated Day 1)

  • Topic/Problem: My custom LoggingDbConnection.QueryAsync() method (which adds SQL logging) isn't being called. Instead, a different QueryAsync seems to be executing when I use my IDbConnection variable.
  • What I Learned/Discovered:
    • The _dbConnection variable in my service was declared as IDbConnection.
    • Dapper provides QueryAsync as an extension method on IDbConnection.
    • My LoggingDbConnection class is being instantiated and used, but its specific QueryAsync method is being bypassed.
  • Challenges/Confusions: Why is Dapper's version called when my object is clearly a LoggingDbConnection which has its own QueryAsync? I thought the object's actual type would determine the method.
  • Solutions/Clarifications:
    • The issue might be related to how C# resolves method calls when interfaces and extension methods are involved.
    • Potential Fix 1: Changing the declared type of _dbConnection in my service from IDbConnection to the concrete LoggingDbConnection.
    • Potential Fix 2: Explicitly casting _dbConnection to (LoggingDbConnection) before calling .QueryAsync().
    • Outcome: Trying these fixes showed that my LoggingDbConnection.QueryAsync was then called! So the compiler needed to know it was dealing with a LoggingDbConnection at the call site.
  • Key Rule/Guideline Remembered (Initial thought): The compiler needs a specific "hint" (cast or concrete type) to choose my special version of a method if the variable is just an interface type.

Date: May 11, 2025 (Simulated Day 2)

  • Topic/Problem: Deep dive into why the compiler prefers Dapper's extension method over my instance method when the variable is typed as IDbConnection.
  • What I Learned/Discovered:
    • C# method resolution is primarily based on the compile-time (declared) type of the variable.
    • The Compiler's "Method Hunt" Process:
      1. It first checks if the declared type itself (e.g., IDbConnection) has an instance method with the called signature.
      2. If not found on the declared type, then it looks for applicable extension methods for that declared type.
    • IDbConnection itself does not define QueryAsync. So, check #1 fails for IDbConnection.
    • Dapper provides QueryAsync as an extension method for IDbConnection. So, check #2 finds Dapper's method.
  • Challenges/Confusions: The "priority" of extension methods vs. polymorphism. I thought polymorphism (runtime object type determining the method) would always take precedence.
  • Solutions/Clarifications:
    • Polymorphism applies to methods that are part of the declared type's contract (e.g., an interface method, or a virtual method being overridden).
    • Since QueryAsync isn't part of the IDbConnection contract, polymorphism doesn't directly apply to make it choose LoggingDbConnection.QueryAsync when the variable is just IDbConnection.
    • It's not that extension methods have "higher priority" than polymorphism; it's that the conditions for invoking polymorphism (for that specific method call on an IDbConnection variable) weren't met because IDbConnection doesn't define QueryAsync.
  • Key Rule/Guideline Remembered: Compiler checks Instance methods on Declared Type FIRST. If none, THEN it looks for Extension methods for Declared Type. Polymorphism applies to methods defined by the declared type's contract.

Date: May 12, 2025 (Simulated Day 3)

  • Topic/Problem: How to make my database access (with logging) more flexible and adhere to the Open/Closed Principle (OCP).
  • What I Learned/Discovered:
    • OCP: Open for extension, closed for modification.
    • My quick fixes (casting or using concrete LoggingDbConnection type in the service) make the service class itself less OCP-compliant because it now knows about specific implementations.
    • A better OCP approach: Define a new interface (e.g., IDataQueryExecutor) that explicitly includes the methods my service needs (like QueryAsync, QuerySingleOrDefaultAsync).
    • My LoggingDbConnection (or a new LoggingQueryExecutor) would implement this new interface.
    • The service would then depend on IDataQueryExecutor, not IDbConnection or LoggingDbConnection.
  • Challenges/Confusions: This OCP solution seemed very complex with new interfaces and changing dependencies. Felt overwhelming.
  • Solutions/Clarifications:
    • The OCP approach is an ideal for maintainability, especially in larger systems.
    • The core idea is to make the service depend on a "contract" (the new interface) rather than a concrete implementation, allowing different implementations (logging, caching, none) to be swapped without changing the service.
    • It's okay if the immediate fix was simpler; OCP is a goal one can refactor towards.
  • Key Rule/Guideline Remembered (for OCP context): Depend on abstractions (interfaces defining needed operations), not concretions, to allow easier extension without modifying existing client code.

This log captures the essence of the learning journey, from encountering a problem to understanding the underlying C# mechanisms and finally considering advanced design principles.









Polymorphism only applies to methods declared in the base class or interface.
The compiler only uses methods it sees in the declared type — and will never "guess" based on the actual runtime type unless you're using virtual or an interface method.

Let's tackle these excellent questions one by one!

Question 1: "so polymorphism will only apply for Virtual?"

You're very close! virtual methods are a cornerstone of polymorphism in C# (specifically, overriding in derived classes). But it's a bit broader:

Polymorphism (the ability for an object to take on many forms, and for a call to a method to execute different code depending on the runtime type of the object) primarily applies in these C# scenarios:

  1. virtual methods: When a base class declares a method as virtual, derived classes can override it. When you call that method on a base class variable that holds a derived class object, the derived class's overridden version is executed. (This is what you mentioned).
  2. abstract methods: An abstract method in an abstract class must be implemented by any concrete derived class. This is a form of polymorphism because the abstract class variable can hold different concrete derived types, each with its own implementation of that abstract method.
  3. Interface Method Implementations: This is very relevant to our IDbConnection discussion! When a class implements an interface, it provides concrete implementations for the methods defined in that interface. If you have a variable of the interface type (e.g., IDbConnection myConnection), and it holds an object of a class that implements that interface (e.g., LoggingDbConnection), calling an interface method (like myConnection.Open()) will execute the specific implementation provided by LoggingDbConnection.

The common thread: For polymorphism to work as described (where the runtime type's method is chosen), the method being called must be part of the contract defined by the declared type of the variable (the interface contract, or the base class's virtual/abstract method signature).


Question 2: "if an extension method of QueryAsync didn't exist would it then had picked up LoggingDbConnection.QueryAsync() or not?" (Assuming _dbConnection is still declared as IDbConnection).

That's another fantastic question to really solidify the method resolution rules! Let's trace the compiler's "Method Hunt" for this exact scenario:

  • Your variable: IDbConnection _dbConnection; (which at runtime holds a LoggingDbConnection instance).
  • Your call: _dbConnection.QueryAsync<T>(...)
  • Your hypothetical: Dapper's QueryAsync extension method for IDbConnection does not exist.

Compiler's Process:

  1. Step 1: Instance Method on Declared Type (IDbConnection)?

    • The compiler looks at IDbConnection.
    • Does the IDbConnection interface itself define a method QueryAsync<T> with a matching signature?
    • Answer: No. QueryAsync is not part of the standard IDbConnection interface contract.
    • Result of Step 1: No direct instance method found on the declared type.
  2. Step 2: Extension Method for Declared Type (IDbConnection)?

    • The compiler looks for accessible extension methods that target IDbConnection and are named QueryAsync<T> with a matching signature.
    • In your hypothetical scenario, you said "if an extension method of QueryAsync didn't exist."
    • Answer: No. (Because we're pretending Dapper's or any other relevant extension method isn't there).
    • Result of Step 2: No extension method found.

Conclusion for Question 2:

Since both Step 1 (instance method on IDbConnection) and Step 2 (extension method for IDbConnection) failed to find a suitable QueryAsync method, the compiler would NOT have picked up LoggingDbConnection.QueryAsync().

Instead, you would get a compile-time error. The error message would be something like: 'System.Data.IDbConnection' does not contain a definition for 'QueryAsync' and no accessible extension method 'QueryAsync' accepting a first argument of type 'System.Data.IDbConnection' could be found (are you missing a using directive or an assembly reference?) 1  

Why? The compiler doesn't automatically "peek" inside the runtime object (LoggingDbConnection) for methods that aren't part of the declared type's (IDbConnection) contract or provided as an extension for that declared type. The method resolution is based on what's visible and contracted through the lens of the declared type at compile time.

Your LoggingDbConnection.QueryAsync() instance method is only "visible" to the compiler without a cast if the variable _dbConnection is itself declared as type LoggingDbConnection.

No comments:

Post a Comment