다음을 통해 공유


Thinking about new C# method prototypes: object as dictionary

I recently had to write a small Flickr API. I know many .Net API for Flickr already exist but I needed one for a Silverlight application. Whatever, it's only about building some querystrings so I did it by myself. It's been an opportunity to think again about a classical question: how to pass parameters to a method ?

Imagine you have a generic method to call some Flickr functions.

 

 public void CallFlickrMethod(string methodName, ? parameters,
  DownloadStringCompletedEventHandler asyncResult)

The goal here is to finally build a querystring like: https://api.flickr.com/services/rest/?method=mymethod&param1=value1&param2=value2...

 

In that case, I would like to pass a collection of parameters, each parameter being a key+value structure.

The .Net framework (Silverlight too) offers a KeyValuePair<TKey, TValue> structure that we could use here.

We could imagine a method like:

 

 public void CallFlickrMethod(string methodName, KeyValuePair<string, string>[] parameters,
  DownloadStringCompletedEventHandler asyncResult)

Call:

CallFlickrMethod("mymethod", 
  new KeyValuePair[] {
    new KeyValuePair<string, string>("param1", "value1"),
    new KeyValuePair<string, string>("param2", "value2")
  },
  completedHandler);

You may also know the 'params' keyword that allows you to pass parameters separated by commas instead of having to build an array:

 

 

 public void CallFlickrMethod(string methodName, DownloadStringCompletedEventHandler asyncResult,
params KeyValuePair<string, string>[] parameters)

Call:

CallFlickrMethod("mymethod", completedHandler,
  new KeyValuePair<string, string>("param1", "value1"),
  new KeyValuePair<string, string>("param2", "value2"));

You can notice that the params parameter must be the last one of the method prototype. This syntax is shorter but still a little bit heavy to write because we have to create many KeyValuePair structures.

 

In that case, parameters names are unique so we are very close to have a dictionary.
We could imagine:

 

 public void CallFlickrMethod(string methodName, DownloadStringCompletedEventHandler asyncResult,
Dictionary<string, string> parameters)

Call:

var parameters = new Dictionary<string, string>();
parameters.Add("param1", "value1");
parameters.Add("param2", "value2");

CallFlickrMethod("mymethod", completedHandler, parameters);

The method is simpler but the call is still heavy because of the dictionary creation.

 

Then I thought about using anonymous methods. This is only working because keys are strings. In a class, properties names are unique. Properties definitions are stored in the type definition itself and the properties values are stored in the instance of the class. So we could imagine using an object as a kind of readonly dictionary (keys are fixed).

Imagine we change our method to just:

 

 public void CallFlickrMethod(string methodName, DownloadStringCompletedEventHandler asyncResult,
object parameters)

Then we could use new C#3 anonymous types to write:

 

 

 CallFlickrMethod("mymethod", completedHandler, new { param1 = "value1", param2 = "value2"});

This syntax is of course very short and also very easy to use.
The following code is analyzing the 'parameters' object using reflection to retrieve the equivalent of a collection of KeyValuePairs.

 

 

 public void CallFlickrMethod(string methodName, object parameters,
  DownloadStringCompletedEventHandler asyncResult)
{
    string url = 
      string.Format("https://api.flickr.com/services/rest/?method={0}&api_key={1}",
        methodName, apiKey);

    var q =
        from prop in parameters.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
        select string.Format("&{0}={1}", prop.Name, prop.GetValue(parameters, null).ToString());

    url += string.Join("", q.ToArray());

    // Or for linq addicts :p
    // url += q.Aggregate(new StringBuilder(), (sb, value) => sb.Append(value)).ToString();


    WebClient webClient = new WebClient();

    webClient.DownloadStringCompleted += asyncResult;
    webClient.DownloadStringAsync(new Uri(url));
}

Let's say this is the end of part I.

 

We could now think about generalizing the use of an object as a readonly dictionary.

What I propose is to offer a way to wrap an object in a generic class implementing IDictionary<TKey, TValue>. IDictionary is quite long to implement but here is an abstract of the important methods.

 

 

 public class ObjectDictionary<T> : IDictionary<string, object>
{
    public ObjectDictionary(T instance)
    {
        this.instance = instance;
    }

    private T instance;

    #region IDictionary<string,object> Members

    public bool ContainsKey(string key)
    {
        return typeof(T).GetProperty(key) != null;
    }

    public ICollection<string> Keys
    {
        get 
        {
          return typeof(T).GetProperties()
            .Select(p => p.Name).ToArray(); 
        }
    }

    public bool TryGetValue(string key, out object value)
    {
        var p = typeof(T).GetProperty(key);
        if (p == null)
        {
            value = null;
            return false;
        }
        else
        {
            value = p.GetValue(instance, null);
            return true;
        }
    }

    public ICollection<object> Values
    {
        get
        {
          return typeof(T).GetProperties()
            .Select(p => p.GetValue(instance, null)).ToArray();
        }
    }

    public object this[string key]
    {
        get
        {
            object result = null;
            if (TryGetValue(key, out result))
                return result;
            else
                throw new Exception("Key not found");
        }
        set
        {
            object result = null;
            if (TryGetValue(key, out result))
                result = value;
            else
                throw new Exception("Key not found");
        }
    }

    ...

    #endregion

    #region ICollection<KeyValuePair<string,object>> Members

    public int Count
    {
        get { return typeof(T).GetProperties().Length; }
    }

    public bool IsReadOnly
    {
        get { return true; }
    }

    ...

    #endregion

    #region IEnumerable<KeyValuePair<string,object>> Members

    public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
    {
        var q =
            from p in typeof(T).GetProperties()
            select new KeyValuePair<string, object>(p.Name, p.GetValue(instance, null));
        return q.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion
}

Then you can write things like:

 

 

 ObjectDictionary<Customer> c =
  new ObjectDictionary<Customer>(
    new Customer { ID = "1", CompanyName = "Microsoft", ContactName = "Mitsu" });
Console.WriteLine(c["CompanyName"]);

With an extension method ?

 public static class ObjectDictionaryExtensions
{
    public static ObjectDictionary<T> AsDictionary<T>(this T instance)
    {
        return new ObjectDictionary<T>(instance);
    }
}

Use:

var cust = new Customer { ID = "1", CompanyName = "Microsoft", ContactName = "Mitsu" };
var c = cust.AsDictionary();
Console.WriteLine(c["CompanyName"]);

With an implicit conversion ?

 

 

 public class ObjectDictionary<T> : IDictionary<string, object>
{
    public ObjectDictionary(T instance)
    {
        this.instance = instance;
    }

    public static implicit operator T (ObjectDictionary<T> source) 
    {
        return source.instance;
    }

    public static implicit operator ObjectDictionary<T> (T source)
    {
        return source.AsDictionary();
    }

    ...
}

Use:

ObjectDictionary<Customer> c =
  new Customer { ID = "1", CompanyName = "Microsoft", ContactName = "Mitsu"};

Of course all these features are more funny to use with anonymous types:

 

 var cust =  new { ID="1", CompanyName="Microsoft"};
var c = c.AsDictionary();
var id = c["ID"];

Let's see one last point. In the case of an anonymous type, we can't use the following syntax:

 var c =
  new ObjectDictionary<?>(new { ID = "1", CompanyName = "Microsoft", ContactName = "Mitsu"});

This is a classical problem where a regular constructor can not infer T from the parameters. If you want to do it, you have to create a static generic method to create your instance. Then the inference will work fine and make the use of an anonymous type possible.

 public class ObjectDictionary
{
    public static ObjectDictionary<T> Create<T>(T instance)
    {
        return new ObjectDictionary<T>(instance);
    }
}

Use:

var c = ObjectDictionary.Create(new { ID = "1", CompanyName = "Microsoft", ContactName = "Mitsu" });

foreach (var prop in c.Keys)
    Console.WriteLine(c[prop]); You can find the source code for VS2008 attached to this post.

ObjectAsDictionary.zip

Comments

  • Anonymous
    June 18, 2008
    PingBack from http://blog.a-foton.ru/2008/06/18/thinking-about-new-c-method-prototypes-object-as-dictionary/

  • Anonymous
    June 18, 2008
    Mitsu, This post reminds me of a recent post by Paul Stovell: http://www.paulstovell.com/blog/snippet-ruby-like-hashes-in-c ... in which he discusses using lambda expressions as hashes similar to what you've done here with anonymous classes. Matt

  • Anonymous
    June 19, 2008
    Thanks for the link Matt, When I have time I should read more about Ruby...

  • Anonymous
    August 18, 2008
    What about performance ? Because this solution use reflection, isn't it ? So, call "GetProperties" or "GetProperty" methods has a cost, right ? or i'm completely wrong? This is just a question because i'm curious and i want to learn more about c# 3.0 :)

  • Anonymous
    August 19, 2008
    You are right. That's why it's not for general use but to respond to some scenario where you generaly use reflection. For example, an anonymous type can't be used outside of the method that is declaring it. But you can still choose to return your anonymous type as an object and then use reflection for the callers. It can also be used directly with some methods that are waiting dictionaries.(ex: build a Json object).

  • Anonymous
    November 18, 2008
    The comment has been removed