tags:

views:

274

answers:

5
+2  A: 

OK, the scope of the question shifted but my original observation and objection to some other solutions still stands.

I think you don't/can't want to use 'generics' here. You don't know the type ahead of time, and since you will need to create the type, there is no need to use a generic implementation because MethodBase.Invoke takes an array of Object.

This code assumes you are instantiating the target from database field. If not just adjust accordingly.

Of course this is not all encompassing and has no useful exception handling, but it will allow you to dynamically execute arbitrary methods on an arbitrary type with arbitrary parameters values all coming from string values in a row.

NOTE: there are many many many scenarios in which this simple executor will not work. You will need to ensure that you engineer your dynamic methods to cooperate with whatever strategy you do end up deciding to use.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Globalization;
using System.Reflection;
using NUnit.Framework;

namespace DynamicMethodInvocation
{

    [TestFixture]
    public class Tests
    {
        [Test]
        public void Test()
        {
            // from your database 
            string assemblyQualifiedTypeName = "DynamicMethodInvocation.TestType, DynamicMethodInvocation";
            string methodName = "DoSomething";

            // this is how you would get the strings to put in your database
            string enumString = Executor.ConvertToString(typeof(AttributeTargets), AttributeTargets.Assembly);
            string colorString = Executor.ConvertToString(typeof(Color), Color.Red);
            string stringString = "Hmm... String?";

            object result = Executor.ExecuteMethod(assemblyQualifiedTypeName, methodName,
                                                   new[] { enumString, colorString, stringString });

            Assert.IsInstanceOf<bool>(result);
            Assert.IsTrue((bool)result);
        }
    }


    public class TestType
    {
        public bool DoSomething(AttributeTargets @enum, Color color, string @string)
        {
            return true;
        }
    }

    public class Executor
    {
        public static object ExecuteMethod(string assemblyQualifiedTypeName, string methodName,
                                           string[] parameterValueStrings)
        {
            Type targetType = Type.GetType(assemblyQualifiedTypeName);
            MethodBase method = targetType.GetMethod(methodName);

            ParameterInfo[] pInfo = method.GetParameters();
            var parameterValues = new object[parameterValueStrings.Length];

            for (int i = 0; i < pInfo.Length; i++)
            {
                parameterValues[i] = ConvertFromString(pInfo[i].ParameterType, parameterValueStrings[i]);
            }

            // assumes you are instantiating the target from db and that it has a parameterless constructor
            // otherwise, if the target is already known to you and instantiated, just use it...

            return method.Invoke(Activator.CreateInstance(targetType), parameterValues);
        }


        public static string ConvertToString(Type type, object val)
        {
            if (val is string)
            {
                return (string) val;
            }
            TypeConverter tc = TypeDescriptor.GetConverter(type);
            if (tc == null)
            {
                throw new Exception(type.Name + " is not convertable to string");
            }
            return tc.ConvertToString(null, CultureInfo.InvariantCulture, val);
        }

        public static object ConvertFromString(Type type, string val)
        {
            TypeConverter tc = TypeDescriptor.GetConverter(type);
            if (tc == null)
            {
                throw new Exception(type.Name + " is not convertable.");
            }
            if (!tc.IsValid(val))
            {
                throw new Exception(type.Name + " is not convertable from " + val);
            }

            return tc.ConvertFrom(null, CultureInfo.InvariantCulture, val);
        }
    }

}
Sky Sanders
They might as well use the new dynamic type from C# 4.0 or switch to another language because this is not something I would ever want to maintain.
ChaosPandion
Hey, I am just answering a question in the context in which it was asked. ;-).
Sky Sanders
A: 

If you are using .NET 4 you can do the following.

var result = default(CustomerType);
if (!Enum.TryParse("Master", out result))
{
    // handle error
}
ChaosPandion
@Chaos, he doesn't have the type. He is creating the parameter value from strings in database....
Sky Sanders
@Sky - I reread the question twice and found no mention of not having the type.
ChaosPandion
@Chaos, lol. maybe the third time will do the trick! He is building a methodbase and parameters from strings in a database row. I would assume that not having the type would be inferred...
Sky Sanders
+1  A: 

Below is a useful extension method I use in .NET 3.5.

With this extension method available, your code could look like this:

var valueInDb = GetStringFromDb().Replace("CustomerType.", string.Empty);
var value = valueInDb.ToEnum(CustomerType.Associate);

By supplying the default value in the parameter, the compiler will know which Enum you want your string to be turned into. It will try to find your text in the Enum. If it doesn't it will return the default value.

Here is the extension method: (this version also does partial matches, so even "M" would work nicely!)

public static T ToEnum<T>(this string input, T defaultValue)
    {
      var enumType = typeof (T);
      if (!enumType.IsEnum)
      {
        throw new ArgumentException(enumType + " is not an enumeration.");
      }

      // abort if no value given
      if (string.IsNullOrEmpty(input))
      {
        return defaultValue;
      }

      // see if the text is valid for this enumeration (case sensitive)
      var names = Enum.GetNames(enumType);

      if (Array.IndexOf(names, input) != -1)
      {
        // case insensitive...
        return (T) Enum.Parse(enumType, input, true);
      }

      // do partial matching...
      var match = names.Where(name => name.StartsWith(input, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
      if(match != null)
      {
        return (T) Enum.Parse(enumType, match);
      }

      // didn't find one
      return defaultValue;
    }
Glen Little
@Glen, as I said to Chaos, if you read the requirements outlined in the question, he does not have a Type. In fact, he does not even know if the paramter *is* and enum. He just needs to be able to construct a parameter value from strings and could not figure out how to do so with enums.
Sky Sanders
@Glen, sorry i downvoted you. Even though it does not address the question, this is a nice little class - i would like to un-downvote, which actually results in an up-vote. ;-) please edit your question so that I may do so...
Sky Sanders
@Sky... 'tis done for you. Thanks for following up.
Glen Little
+2 kneejerk downvote tax paid. And now that I think of it, I could have made an invisible edit to allow the upvote. Just don't normally think of editing someone's answer for any reason...
Sky Sanders
+1  A: 

I would think you have 2 major options:

  1. Store the type name along with the parameter value and use that to cast things using Type.GetType(string) to resolve the type in question.
  2. Standardize all the methods to be called this way to accept an array of strings, and expect the methods to do any necessary casting.

I know you've stated that you're not doing option 1, but it would help things from the standpoint of calling the functions.

Option 2 is the far more 'generic' way to handle the situation, assuming all values can be represented by and cast/converted from strings to the appropriate type. Of course, that only helps if you actually have control over the definition of the methods being called.

Sam Erwin
Thanks Sam. Plumped for option 2. Not what I wanted to do.I'll put some more thought into it when I get chance.-- the annoying thing is I have the parameter as a string; I knowm the type of the parameter I want to pass in (either by looking in the DB or by reflection). I just can't convert.Thanks again.
Jim
A: 

I still don't fully understand your question... however, you say "Everything is fine except for the parameters."

I'll assume "CustomerType" the name of a property on your object, and "Master" is the string value you want to put in that property.

Here is (another) extension method that may help.

Once you have your new object and the value and property name from the database field, you could use this:

// string newValue = "Master";
// string propertyName = "CustomerType"; 

myNewObject.SetPropertyValue(propertyName, newValue)

Method:

/// <summary>Set the value of this property, as an object.</summary>
public static void SetPropertyValue(this object obj, 
                                    string propertyName, 
                                    object objValue)
{
  const BindingFlags attr = BindingFlags.Public | BindingFlags.Instance;
  var type = obj.GetType();

  var property = type.GetProperty(propertyName, attr);
  if(property == null) return;

  var propertyType = property.PropertyType;
  if (propertyType.IsValueType && objValue == null)
  {
    // This works for most value types, but not custom ones
    objValue = 0;
  }

  // need to change some types... e.g. value may come in as a string...
  var realValue = Convert.ChangeType(objValue, propertyType);

  property.SetValue(obj, realValue, null);
}
Glen Little