Extension methods: If you have a shiny new hammer

March 20, 2009 at 10:40 AMAndre Loker

If you have a shiny new hammer, every problem looks like a nail they say. Although C# 3.0 extension methods are not that new anymore, this saying still applies. I ran across the announcement of the Generic Extension Methods Project. While I think a repository of useful extension methods sounds like a cool idea, one piece of code in that said announcement made me smile slightly:

   1: // IsNotNull is an extension method
   2: if(row.IsNotNull())
   3: {
   4:     row["First Column"] = "some value";
   5: }

Call me old fashioned, but I personally prefer the good old null-check a lot:

   1: // Pure and simple
   2: if(row != null)
   3: {
   4:     row["First Column"] = "some value";
   5: }

something != null  is an expression every developer understands. If I don’t know about extension methods or don’t know in particular that IsNotNull() is an extension method, I’d ask myself: “What does a method IsNotNull() do? Is nullness meant to be something different here? It has to, because calling a method on a null-variable would cause a NullReferenceException, wouldn’t it?”.

One inherent semantic of C# is that if you call a method on a null reference, it will crash. IsNotNull() is therefore completely counterintuitive. Either you force the reader to consider the possibility that the method might be an extension method and therefore you could call that method on null. Or you help those readers by preceding all usages of IsNotNull() with comments like “Note, this is an extension method”. Yuck!

But maybe this IsNotNull() implementation does a bit more for DataTableRows than just checking them to be not null. If it does, it’s name is misleading.

So you see: using IsNotNull() adds nothing but confusion to the code.

My personal recommendations regarding extension methods:

  • Don’t use them just because you can.
    • The ultimate goal should be to write simple, readable, comprehensible code. If an extension method helps – use it. If it doesn’t or is even counterproductive – don’t!
  • If there is a simpler concept built into the language, prefer that over extension methods. Some (partly far-fetched) examples:
    • prefer something != null over something.IsNotNull()
    • prefer if(something)… over if(something.IsTrue())…
    • prefer x = a + 1 over x = a.PlusOne()

A brief review

That being said, here’s a brief review of the extension methods that are found in the project as of now:

CollectionExtensions

FirstItem returns the first item of a list or array or null if there is no such item. Isn’t that what FirstOrDefault already does?

ReflectionExtensions

CreateInstance – which extends Type - comes in two flavours:

The first one tries to create an instance using the default constructor:

   1: public static T CreateInstance<T>(this System.Type type) where T : new()

Although the documentation says that you could use it like “typeof(MyObject).CreateInstance()” this is not true – you need to provide something for T. Not only do you need to provide the type twice (as the extended object and as T), you also end up with possible constructs like:

   1: typeof(string).CreateInstance<Version>();

This happily creates an instance of Version. What’s the use of extending Type then? Just use Activator – it’s simple and works:

   1: Activator.CreateInstance<Version>()

The other overload of CreateInstance additionally allows for constructor arguments being passed. What’s been said above still counts, typeof(string).CreateInstance<Version>(1,2,3,4) is awkward.

Just as a side note: what’s the use of a generic object creation method anyway? The documentation of Activator.CreateInstance<T> puts it nicely:

In general, there is no use for the CreateInstance in application code, because the type must be known at compile time. If the type is known at compile time, normal instantiation syntax can be used.

That is: if somewhere in the code I am able to say Activator.CreateInstance<Foo>() – why don’t I just say new Foo() in the first place? What I’d find more useful is something like this:

   1: public static T InstantiateAs<T>(this Type type) {
   2:   return (T) type.Instantiate();
   3: }
   4:  
   5: public static object Instantiate(this Type type) {
   6:   return Activator.CreateInstance(type);
   7: }

So you could say:

   1: Type type = ReadSomeTypeFromConfig(); // returns e.g. typeof(ServiceImpl)
   2: IService myService = type.InstantiateAs<IService>();

And likewise add overloads that accept constructor arguments.

ValidationExtensions

This class contains extension methods such as IsNull or IsNotNull – which I don’t like at all as explained above.

Additionally, you’ll find assertion methods like

   1: public static void AssertParameterNotNull(this object value, 
   2:                                           string message, 
   3:                                           string name)

It’s probably a matter of taste whether you like it or not. Again, I prefer a more explicit way. I prefer not to allow method calls on possible null-values and go with:

   1: Assert.ArgumentIsNotNull(theArgument, "theArgument", "Boy, that arg can't be null!")

Then we have AssertEquals which is supposed to be used like this:

   1: someValue.AssertEquals<MyException>(someOtherValue, "Some message");

That is, MyException is thrown if someValue does not equal someOtherValue. I can’t help it, it feels weird.

Next one:

   1: public static bool IsEmpty(this string value)
   2: public static bool IsNotEmpty(this string value)

Looks reasonable, doesn’t it? The problem with these methods is that their names are very misleading:

   1: /// <summary>
   2: /// Tests if the string is empty.
   3: /// </summary>
   4: /// <param name="value">The string to test.</param>
   5: /// <returns>True if the string is empty.</returns>
   6: public static bool IsEmpty(this string value)
   7: {
   8:     return value.Trim().Length == 0;
   9: }
  10:  
  11: /// <summary>
  12: /// Tests if the string is not empty.
  13: /// </summary>
  14: /// <param name="value">The string to test.</param>
  15: /// <returns>True if the string is not empty.</returns>
  16: public static bool IsNotEmpty(this string value)
  17: {
  18:     return value.Trim().Length > 0;
  19: }

According to those methods, “    “.IsEmpty() == true. Which is confusing because string.IsNullOrEmpty(“    “) returns false. I’d heavily suggest to rename those methods and be clear about the behaviour in the documentation.

Finally, there are a bunch of methods called IsEmpty and IsNotEmpty that work on different collection types (why aren’t they placed in the CollectionExtensions class?). While I find those methods useful there is an extreme amount of duplication. All those methods can be covered by these to little guys:

   1: public static bool IsEmpty<T>(this T collection) where T : ICollection {
   2:   return collection.Count == 0;
   3: }
   4:  
   5: public static bool IsNotEmpty<T>(this T collection) where T : ICollection {
   6:   return !collection.IsEmpty();
   7: }

Because all of the types handled in the library (ICollection, ICollection<T>, IList, IList<T>, IDictionary, IDictionary<K, T>, Array) inherit ICollection. Why did I make IsEmpty generic instead of just passing an ICollection? It’s just a little trick: if for whatever reason you have a value type that implements ICollection invoking IsEmpty(ICollection) on it would cause it to be boxed to treat it as an ICollection. By making IsEmpty generic with a constrained this boxing will not occur. OK, it’s not likely to happen, but I prefer this style. It explicitly says: “It operates on everything that can behave like an ICollection” rather than “It operates on an ICollection”. Sometimes it’s in the details.

Posted in: C# | Patterns

Tags: ,

Ruby’s send in C#

March 18, 2009 at 1:42 PMAndre Loker

In the comment’s of a blog post some developer coming from Ruby wishes C# to have something similar to Ruby’s send functionality, i.e. being able to dynamically invoke a method on an object like this:

   1: theObject.Send("Foo", 1, 2, 3);

This would call the method Foo on theObject with the arguments 1, 2 and 3.

Of course we can use reflection to invoke a method, but it takes same amount of code to do that. The proposed way (just calling a Send method on an arbitrary) looks very compact. How can we achieve that in C#? Of course, through extension methods! So I took the time to implement the Send functionality in C#:

Source code:

   1: #region Copyright (c) 2009, Andre Loker <mail@andreloker.de>
   2: // Permission to use, copy, modify, and/or distribute this software for any
   3: // purpose with or without fee is hereby granted, provided that the above
   4: // copyright notice and this permission notice appear in all copies.
   5: //
   6: // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   7: // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
   8: // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
   9: // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  10: // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  11: // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  12: // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  13: #endregion
  14:  
  15: using System;
  16: using System.Collections.Generic;
  17: using System.Linq;
  18: using System.Reflection;
  19:  
  20: public static class ObjectSendExtensions {
  21:   /// <summary>
  22:   /// Invokes a method on the <paramref name="target"/>
  23:   /// </summary>
  24:   /// <param name="target">The target, must not be <c>null</c></param>
  25:   /// <param name="methodName">Name of the method, must not be <c>null</c></param>
  26:   /// <param name="args">The arguments passed to the method.</param>
  27:   /// <remarks>
  28:   /// If the target type contains multiple overload of the given <paramref name="methodName"/>
  29:   /// <see cref="Send"/> tries to find the best match.
  30:   /// </remarks>
  31:   /// <exception cref="ArgumentException">
  32:   /// No method with the given <paramref name="methodName"/> was found or the invocation
  33:   /// is ambiguous, ie. multiple methods match.
  34:   /// </exception>
  35:   public static object Send(this object target, string methodName, params object[] args) {
  36:     return Send<object>(target, methodName, args);
  37:   }
  38:  
  39:   /// <summary>
  40:   /// Invokes a method on the <paramref name="target"/>
  41:   /// </summary>
  42:   /// <param name="target">The target, must not be <c>null</c></param>
  43:   /// <param name="methodName">Name of the method, must not be <c>null</c></param>
  44:   /// <param name="args">The arguments passed to the method.</param>
  45:   /// <remarks>
  46:   /// If the target type contains multiple overload of the given <paramref name="methodName"/>
  47:   /// <see cref="Send"/> tries to find the best match.
  48:   /// </remarks>
  49:   /// <exception cref="ArgumentException">
  50:   /// No method with the given <paramref name="methodName"/> was found or the invocation
  51:   /// is ambiguous, ie. multiple methods match.
  52:   /// </exception>
  53:   /// <returns>The value returned from the invoked method cast to a 
  54:   /// <typeparamref name="T"/>
  55:   /// </returns>
  56:   public static T Send<T>(this object target, string methodName, params object[] args) {
  57:     if(target == null) {
  58:       throw new ArgumentNullException("target");
  59:     }
  60:  
  61:     if(methodName == null) {
  62:       throw new ArgumentNullException("methodName");
  63:     }
  64:  
  65:     var type = target.GetType();
  66:     var methods = GetMethodCandidates(methodName, type);
  67:     var methodToInvoke = FindBestFittingMethod(methods, args);
  68:     return InvokeFunction<T>(target, methodToInvoke, args);
  69:   }
  70:  
  71:   static IEnumerable<MethodInfo> GetMethodCandidates(string methodName, Type type) {
  72:     return from method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  73:            where method.Name == methodName
  74:            select method;
  75:   }
  76:  
  77:   static MethodInfo FindBestFittingMethod(IEnumerable<MethodInfo> methods, object[] args) {
  78:     var highestScore = -1;
  79:     var matchingMethodCount = 0;
  80:     MethodInfo selectedMethod = null;
  81:  
  82:     foreach(var method in methods) {
  83:       var methodScore = RateMethodMatch(method.GetParameters(), args);
  84:       if(methodScore > highestScore) {
  85:         matchingMethodCount = 1;
  86:         highestScore = methodScore;
  87:         selectedMethod = method;
  88:       } else if(methodScore == highestScore) {
  89:         // count the number of matches, match count > 1 => ambiguous call
  90:         matchingMethodCount++;
  91:       }
  92:     }
  93:  
  94:     if(matchingMethodCount > 1) {
  95:       throw new ArgumentException("Ambiguous method invocation");
  96:     }
  97:     return selectedMethod;
  98:   }
  99:  
 100:  
 101:   /// <returns>0 if the arguments don't match the parameters; a score &gt; 0 otherwise.</returns>
 102:   static int RateMethodMatch(ParameterInfo[] parameters, object[] args) {
 103:     var argsLength = args != null ? args.Length : 0;
 104:     if(parameters.Length == argsLength) {
 105:       return argsLength == 0 ? 1 : RateParameterMatches(parameters, args);
 106:     }
 107:     return 0;
 108:   }
 109:  
 110:   static int RateParameterMatches(ParameterInfo[] parameters, object[] args) {
 111:     var score = 0;
 112:     for(var i = 0; i < args.Length; ++i) {
 113:       var typeMatchScore = RateParameterMatch(parameters[i], args[i]);
 114:       if(typeMatchScore == 0) {
 115:         return 0;
 116:       }
 117:       score += typeMatchScore;
 118:     }
 119:     return score;
 120:   }
 121:  
 122:  
 123:   static int RateParameterMatch(ParameterInfo parameter, object arg) {
 124:     var parameterType = parameter.ParameterType;
 125:     return arg == null ? RateNullArgument(parameterType) : RateNonNullArgument(arg, parameterType);
 126:   }
 127:  
 128:   static int RateNullArgument(Type parameterType) {
 129:     return CanBeNull(parameterType) ? 1 : 0;
 130:   }
 131:  
 132:   static bool CanBeNull(Type type) {
 133:     return !type.IsValueType || IsNullableType(type);
 134:   }
 135:  
 136:   static bool IsNullableType(Type type) {
 137:     return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
 138:   }
 139:  
 140:   static int RateNonNullArgument(object arg, Type parameterType) {
 141:     var argType = arg.GetType();
 142:     if(argType == parameterType) {
 143:       // perfect match!
 144:       return 2;
 145:     }
 146:     if(parameterType.IsAssignableFrom(argType)) {
 147:       // at least convertible to parameter type
 148:       return 1;
 149:     }
 150:     return 0;
 151:   }
 152:  
 153:   static T InvokeFunction<T>(object target, MethodInfo method, object[] args) {
 154:     if(method == null) {
 155:       throw new ArgumentException("Method not found");
 156:     }
 157:     return (T) method.Invoke(target, args);
 158:   }
 159: }

Tests (MbUnit 3):

   1: #region Copyright (c) 2009, Andre Loker <mail@andreloker.de>
   2: // Permission to use, copy, modify, and/or distribute this software for any
   3: // purpose with or without fee is hereby granted, provided that the above
   4: // copyright notice and this permission notice appear in all copies.
   5: //
   6: // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   7: // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
   8: // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
   9: // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  10: // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  11: // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  12: // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  13: #endregion
  14:  
  15: using System;
  16: using MbUnit.Framework;
  17: using Rhino.Mocks;
  18:  
  19: [TestFixture]
  20: public class ObjectSendExtensionTests {
  21:   [Test]
  22:   public void FailsIfTargetIsNull() {
  23:     const string x = null;
  24:     Assert.Throws<ArgumentNullException>(() => x.Send("ToString"));
  25:   }
  26:  
  27:   [Test]
  28:   public void FailsIfMethodNameIsNull() {
  29:     Assert.Throws<ArgumentNullException>(() => 123.Send(null));
  30:   }
  31:  
  32:   [Test]
  33:   public void CanInvokeUniqueMethodWithoutArgs() {
  34:     var mock = MockRepository.GenerateMock<IUniqueMethodWithoutArgs>();
  35:     mock.Send("Foo");
  36:     mock.AssertWasCalled(x => x.Foo());
  37:   }
  38:  
  39:   [Test]
  40:   public void CanDistinguishMethodsByParameterCount_FirstMethod() {
  41:     var mock = MockRepository.GenerateMock<ISimpleOverload>();
  42:     mock.Send("Foo");
  43:     mock.AssertWasCalled(x => x.Foo());
  44:     mock.AssertWasNotCalled(x => x.Foo(Arg<int>.Is.Anything));
  45:   }
  46:  
  47:   [Test]
  48:   public void CanDistinguishMethodsByParameterCount_SecondMethod() {
  49:     var mock = MockRepository.GenerateMock<ISimpleOverload>();
  50:     mock.Send("Foo", 42);
  51:     mock.AssertWasCalled(x => x.Foo(Arg.Is(42)));
  52:     mock.AssertWasNotCalled(x => x.Foo());
  53:   }
  54:  
  55:   [Test]
  56:   public void CanPassArgumentsToMethod() {
  57:     var mock = MockRepository.GenerateMock<ISimpleArguments>();
  58:     mock.Send("Foo", 4, "bar");
  59:     mock.AssertWasCalled(x => x.Foo(Arg.Is(4), Arg.Is("bar")));
  60:   }
  61:  
  62:   [Test]
  63:   public void CanDoSimpleParameterResolution_StringOverload() {
  64:     var mock = MockRepository.GenerateMock<IParameterResolution>();
  65:     var arg = "bar";
  66:     mock.Send("Foo", arg);
  67:     mock.AssertWasCalled(x => x.Foo(Arg.Is(arg)));
  68:     mock.AssertWasNotCalled(x => x.Foo(Arg<Version>.Is.Anything));
  69:     mock.AssertWasNotCalled(x => x.Foo(Arg<int>.Is.Anything));
  70:   }
  71:  
  72:   [Test]
  73:   public void CanDoSimpleParameterResolution_VersionOverload() {
  74:     var mock = MockRepository.GenerateMock<IParameterResolution>();
  75:     var arg = new Version(1, 2, 3);
  76:     mock.Send("Foo", arg);
  77:     mock.AssertWasCalled(x => x.Foo(Arg.Is(arg)));
  78:     mock.AssertWasNotCalled(x => x.Foo(Arg<string>.Is.Anything));
  79:     mock.AssertWasNotCalled(x => x.Foo(Arg<int>.Is.Anything));
  80:   }
  81:  
  82:   [Test]
  83:   public void CanDoSimpleParameterResolution_IntOverload() {
  84:     var mock = MockRepository.GenerateMock<IParameterResolution>();
  85:     var arg = 42;
  86:     mock.Send("Foo", arg);
  87:     mock.AssertWasCalled(x => x.Foo(Arg.Is(arg)));
  88:     mock.AssertWasNotCalled(x => x.Foo(Arg<string>.Is.Anything));
  89:     mock.AssertWasNotCalled(x => x.Foo(Arg<Version>.Is.Anything));
  90:   }
  91:  
  92:   [Test]
  93:   public void CanHandleNullableArguments() {
  94:     var mock = MockRepository.GenerateMock<INullableParameters>();
  95:     int? arg = 42;
  96:     mock.Send("Foo", arg);
  97:     mock.AssertWasCalled(x => x.Foo(Arg.Is(arg)));
  98:     mock.AssertWasNotCalled(x => x.Foo(Arg<float>.Is.Anything));
  99:   }
 100:  
 101:   [Test]
 102:   public void CanHandleNullableArgumentsWithNullValue() {
 103:     var mock = MockRepository.GenerateMock<INullableParameters>();
 104:     int? arg = null;
 105:     mock.Send("Foo", arg);
 106:     mock.AssertWasCalled(x => x.Foo(Arg.Is(arg)));
 107:     mock.AssertWasNotCalled(x => x.Foo(Arg<float>.Is.Anything));
 108:   }
 109:  
 110:   [Test, Description("Although not desired this behaviour is expected")]
 111:   public void SuffersFromNullableBoxingBehaviour() {
 112:     var mock = MockRepository.GenerateMock<INullableParametersBoxingIssue>();
 113:     int? arg = 42;
 114:     mock.Send("Foo", arg);
 115:     mock.AssertWasCalled(x => x.Foo(Arg<int>.Is.Equal(42)));
 116:     mock.AssertWasNotCalled(x => x.Foo(Arg<int?>.Is.Anything));
 117:   }
 118:  
 119:   [Test]
 120:   public void TriesToMatchTypesAsGoodAsPossible() {
 121:     var mock = MockRepository.GenerateMock<ISelectPolymorphic>();
 122:     var item = new DerivedClass();
 123:     mock.Send("Foo", item);
 124:     mock.AssertWasCalled(x => x.Foo(Arg<DerivedClass>.Is.Same(item)));
 125:     mock.AssertWasNotCalled(x => x.Foo(Arg<BaseClass>.Is.Anything));
 126:   }
 127:  
 128:   [Test]
 129:   public void TriesToMatchTypesAsGoodAsPossible2() {
 130:     var mock = MockRepository.GenerateMock<ISelectPolymorphic2>();
 131:     var item = new DerivedClass();
 132:     mock.Send("Foo", null, item);
 133:     mock.AssertWasCalled(x => x.Foo(Arg<BaseClass>.Is.Null, Arg<DerivedClass>.Is.Same(item)));
 134:     mock.AssertWasNotCalled(x => x.Foo(Arg<BaseClass>.Is.Anything, Arg<BaseClass>.Is.Anything));
 135:   }
 136:  
 137:   [Test]
 138:   public void CanCauseAmbiguousInvocation() {
 139:     var mock = MockRepository.GenerateMock<ISelectPolymorphic>();
 140:  
 141:     var exception = Assert.Throws<ArgumentException>(() => mock.Send("Foo", new object[] { null }));
 142:     Assert.AreEqual("Ambiguous method invocation", exception.Message);
 143:   }
 144:  
 145:   [Test]
 146:   public void CanHandleNullArrayAsArguments() {
 147:     var mock = MockRepository.GenerateMock<IUniqueMethodWithoutArgs>();
 148:     mock.Send("Foo", default(object[]));
 149:     mock.AssertWasCalled(x => x.Foo());
 150:   }
 151:  
 152:   [Test]
 153:   public void ReturnsReturnValue() {
 154:     var stub = MockRepository.GenerateStub<IReturnValue>();
 155:     stub.Stub(x => x.IntFoo()).Return(123);
 156:     stub.Stub(x => x.StringFoo()).Return("bar");
 157:  
 158:     var intResult = stub.Send("IntFoo");
 159:     var stringResult = stub.Send("StringFoo");
 160:  
 161:     Assert.AreEqual(123, intResult);
 162:     Assert.AreEqual("bar", stringResult);
 163:   }
 164:  
 165:   [Test]
 166:   public void ReturnsCastReturnValue() {
 167:     var stub = MockRepository.GenerateStub<IReturnValue>();
 168:     stub.Stub(x => x.IntFoo()).Return(123);
 169:     stub.Stub(x => x.StringFoo()).Return("bar");
 170:  
 171:     int intResult = stub.Send<int>("IntFoo");
 172:     string stringResult = stub.Send<string>("StringFoo");
 173:  
 174:     Assert.AreEqual(123, intResult);
 175:     Assert.AreEqual("bar", stringResult);
 176:   }
 177:  
 178:   public interface IUniqueMethodWithoutArgs {
 179:     void Foo();
 180:   }
 181:  
 182:   public interface ISimpleOverload {
 183:     void Foo();
 184:     void Foo(int x);
 185:   }
 186:  
 187:   public interface ISimpleArguments {
 188:     void Foo(int x, string y);
 189:   }
 190:  
 191:   public interface IParameterResolution {
 192:     void Foo(string x);
 193:     void Foo(Version x);
 194:     void Foo(int x);
 195:   }
 196:  
 197:   public interface INullableParameters {
 198:     void Foo(float a);
 199:     void Foo(int? a);
 200:   }
 201:  
 202:   public interface INullableParametersBoxingIssue {
 203:     void Foo(int a);
 204:     void Foo(int? a);
 205:   }
 206:  
 207:   public interface ISelectPolymorphic {
 208:     void Foo(BaseClass arg);
 209:     void Foo(DerivedClass arg);
 210:   }
 211:  
 212:   public interface ISelectPolymorphic2 {
 213:     void Foo(BaseClass arg, BaseClass arg2);
 214:     void Foo(BaseClass arg, DerivedClass arg2);
 215:   }
 216:  
 217:   public interface IReturnValue {
 218:     int IntFoo();
 219:     string StringFoo();
 220:   }
 221:  
 222:   public class BaseClass {
 223:   }
 224:  
 225:   public class DerivedClass : BaseClass {
 226:   }
 227: }

Usage:

   1: public static void Main(string[] args) {
   2:   object o = new Random();
   3:   UseObject(o);
   4: }
   5:  
   6: static void UseObject(object o) {
   7:   Console.WriteLine("Next: {0}",          o.Send("Next"));
   8:   Console.WriteLine("Ranged next: {0}",   o.Send("Next", 45));
   9:   Console.WriteLine("Ranged next 2: {0}", o.Send("Next", 10, 20));
  10: }

Some facts about the implementation:

  • It intentionally only supports public instance methods
  • It supports overloaded functions and tries to match the method to invoke depending on the arguments being passed
  • It won’t invoke Foo(SomeValueType?) if Foo(SomeValueType) is present due to the way nullable values are boxed

If you find this useful, feel free to use it in your projects. The code is ISC licensed.

ObjectSendExtensions.cs (5.87 kb)

ObjectSendExtensionTests.cs (7.24 kb)

Posted in: C#

Tags: ,