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: ,

Comments (3) -

Wow, that was quick! Great work, I can't wait to test it out!

Glad to be of some inspiration.

You know what? We outta build on this, and go ahead and implement some more Ruby functions. A friend of mine implemented ruby's String.squeeze method the other day.

That could be a really fun/useful project for .NET developers by day, Ruby hackers by night!

frank rizzo
United States frank rizzo says:

It's funny that you are calling it Ruby's Send command.  Coming from a VB6 background, I used to call it CallByName, which does exactly the same thing.  In fact, I had a wrapper called that exact thing, very much similar to yours.

I called it "Ruby's send" because is it supposed to answer the question in the blog comment I mention at the beginning. I'd probably call it "Invoke" or so anyway, because it resembles the Invoke methods found elsewhere in the FCL.

Pingbacks and trackbacks (1)+