Sunday, December 9, 2007

Resolving extension method conflicts using a Proxy pattern.

If you happen to get this error: "The call is ambiguous between the following methods or properties: ...", here is how you would fix it:

Problem Setup


Lets say you are using the following library:

namespace PRI.Interfaces {
public interface IEntity {
string Name { get; set; }
}
}

namespace PRI {
public class DataEntity:Interfaces.IEntity {
public string Name { get; set; }
}
}

And the library authors decide to release an extension method (along with a couple of others you wish to use):

 
namespace PRI.Extensions {
    using PRI.Interfaces;
 
    public static class Entity {
        public static string CapitalizeName(this IEntity entity) {
            entity.Name = entity.Name.ToUpper();
            return entity.Name;
        }
    }
}

Meanwhile you are using a third party extension method library which already contains this method:

 
namespace Contoso.Extensions {
    using PRI.Interfaces;
    using System.Text;
 
    public static class Entity {
        public static string CapitalizeName(this IEntity entity) {
            StringBuilder sb = new StringBuilder(entity.Name.Length);
            string[] words = entity.Name.Split(new char[] { ' ' });
            foreach(string word in words) {
                sb.Append(char.ToUpper(word[0]));
                sb.Append(word.Substring(1).ToLower());
                sb.Append(" ");
            }
            entity.Name = sb.ToString().Trim();
            return entity.Name;
        }
    }
}


The Problem


This is a problem because you can no longer call the CapitalizeName method as an extension method if you happen to have both namespaces in your using constructs. As long as you are using functionality from both classes where the names of the methods are different you will not have any conflicts and there will not be any problems using your code. Unfortunately, the moment you add a call to the CapitalizeName method you will get a compile time error.


namespace Program {
    using System;
    using PRI;
    using PRI.Extensions;
    using Contoso.Extensions;
 
    class Program {
        static void Main(string[] args) {
            DataEntity dataEntity = new DataEntity() { Name = "frank smith" };
            dataEntity.CapitalizeName();
            Console.WriteLine(dataEntity.Name);
        }
    }
}

This will not compile because CapitalizeName cannot be resolved.

Enter the Proxy Pattern


Because extension methods are static methods and can be called just as any other static method gets called, you can build a wrapper class around these third party extension methods in order to resolve the conflicts that the method names impose.

namespace Program.ExtensionResolvers {
    using PRI.Interfaces;
 
    internal static class Resolver {
        public static string CapitalizeName(this IEntity entity) {
            return PRI.Extensions.Entity.CapitalizeName(entity);
        }
        public static string ConvertNameToTitleCase(this IEntity entity) {
            return Contoso.Extensions.Entity.CapitalizeName(entity);
        }
    }
}

Using the Solution


The extension method proxy class can now be used in order to resolve the naming conflict.

namespace Program {
    using System;
    using PRI;
    using ExtensionResolvers;
 
    class Program {
        static void Main(string[] args) {
            DataEntity dataEntity = new DataEntity() { Name = "frank smith" };
            dataEntity.ConvertNameToTitleCase();
            Console.WriteLine(dataEntity.Name);
        }
    }
}

Recommendation


If you are going to use more than 1 extension method library ever in the course of working on a project, I believe it is awfully important to encapsulate the extension methods you will be using into a proxy class (or several such classes) as I have shown here. It is probably best to keep this class internal as you wouldn't want to continue to pollute the function name domain to other third party libraries which are using your code.

I will argue that all calls to extension methods from third party libraries be internalized in this way. Doing so adds value (you can add xml documentation to these methods for your coworkers and the R# tools will be able to find usages and such) and it better decouples you from minor API changes in the extension libraries.

--
Thanks to Peter Ritchie on the altnetconf mailing list for most of the above code in the first section of this post.

No comments: