Friday, October 24, 2014

Using dynamic and DynamicObject in C#

Up until I came across Simple.Data, I had only used the dynamic type when unit testing controllers that returned JsonResult objects. After playing with Simple.Data and taking a look at the source code, it became clear that there was a lot more to the dynamic type than I had realized; it hadn’t occurred to me that you could use the member’s name essentially as an additional argument (thanks Jon for making me see this!). To better understand all of this, I decided to create my own limited implementation that would allow me to execute stored procedures in the same fashion. VerySimpleData is heavily based on Simple.Data and I am not trying to take any credit for the creativity of this approach. I have tried to keep my implementation as lean as possible to highlight the use and function of the dynamic type.

VerySimpleData

First of all, you can find the repository for VerySimpleData here.

I’m sure you’re familiar with how the compiler will bark at you if you make a typo and end up calling a method or setting a property that doesn’t exist. That’s static typing at work making sure that everything your code is talking about actually does exist. The dynamic type allows you to sidestep this and by-pass what would normally be resolved at compile-time and instead have it resolved dynamically at run-time. This means that you now can call methods or set property that do not exist, as long as you’re using a dynamic type. Let's start with an example of how to use VerySimpleData to highlight this.

1
2
var database = Database.WithNamedConnectionString("connectionString");
var results = database.MyStoredProcedure(Param: "something", AnotherParam: "something-else");

In line one, the connection string is supplied and a dynamic object is returned which will act as the database context. In line two, I am calling a stored procedure called "MyStoredProcedure" and passing it two parameters called "Param" and "AnotherParam"; if I wanted to execute a stored procedure with a different name and/or parameters, I would only need to change the method's name and the names of the parameters that are being supplied:

var results = database.AnotherStoredProcedure(DifferentParam: "something-different");

Looking Under the Hood:

The entry point to VerySimpleData is through one of the static methods on the Database class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class Database
{
    public static dynamic WithNamedConnectionString(string name)
    {
        var connectionStringSettings = ConfigurationManager.ConnectionStrings[name];

        if (connectionStringSettings == null)
        {
            throw new ArgumentException("No connection string found for: " + name);
        }

        return WithConnectionString(connectionStringSettings.ConnectionString);
    }

    public static dynamic WithConnectionString(string connectionString)
    {
        IDatabase database = new SqlServerDatabase(connectionString);
        var dynamicContext = new DatabaseContext(database);

        return dynamicContext;
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DatabaseContext : DynamicObject
{
    private readonly IDatabase Database;

    public DatabaseContext(IDatabase database)
    {
        Database = database;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        if (binder.CallInfo.ArgumentCount != args.Length)
        {
            result = false;

            return false;
        }

        var procedureName = binder.Name;
        var parameters = binder.CallInfo.ArgumentNames.Zip(args, (s, i) => new { s, i })
            .ToDictionary(item => item.s, item => item.i);

        result = Database.ExecuteStoredProcedure(procedureName, parameters);

        return true;
    }
}

An instance of DatabaseContext is returned from the Database static class as a dynamic type when either WithNamedConnectionString() or WithConnectionString() is called. The key to this class is that it inherits from DynamicObject. DynamicObject provides the ability to interact with any of the members that are executed/set on the dynamic type by overriding the base implementation:


TryInvokeMember is invoked whenever a method is called on the dynamic object. By providing your own override of this, you can control what will happen. The example in DatabaseContext.cs is very simple. First there is a check that the argument count matches the number of arguments that were supplied. From a quick inspection, the InvokeMemberBinder appears to contains meta data/semantics about the invoked member (e.g. the names of the arguments, the name of the member being invoked) whereas the values themselves are contained on the args array. It is important to note here that the “out” result parameter holds the object that will be returned when this member is invoked; the returned Boolean for TryInvokeMember indicates if the operation was successful or not. Returning false causes a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException to be thrown. This pattern is the same for all the Try[…] overrides exposed from inheriting from DynamicObject.

My implementation assumes that the name of the invoked member will match the stored procedure’s name (line 19) and that the names of the arguments will match the procedure’s parameters (lines 20-21). Calling the ExecuteStoredProcudure method (line 23) fires off the ADO.NET code that talks to the database and it should be no surprise that this returns a dynamic object as the result could be a single value, a single record, a set of records or nothing at all.

If there’s only one row and multiple columns, this will be returned as a VerySimpleDataRecord:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class VerySimpleDataRecord : DynamicObject
{
    private readonly Dictionary<string, object> _data;

    public VerySimpleDataRecord()
        :this(new Dictionary<string, object>())
    { }

    public VerySimpleDataRecord(Dictionary<string, object> data)
    {
        _data = data;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (_data.ContainsKey(binder.Name))
        {
            result = _data[binder.Name];

            return true;
        }
        
        return base.TryGetMember(binder, out result);
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        _data[binder.Name] = value;

        return true;
    }

    public int ColumnCount
    {
        get { return _data.Count; }
    }
}

This is constructed using a dictionary collection where the key-value pairs comprise of the column name and the data that was held in that column. The TryGetMember override is invoked when performing a property get operation on the dynamic object; if there is a key in the dictionary collection that matches the property’s name, it is returned.

The final scenario is where there are multiple rows with one or more columns. In this case, a collection of data will be returned that is packaged in a way that make it enumerable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class VerySimpleDataRecordSet : DynamicObject, IEnumerable
{
    private readonly IList<VerySimpleDataRecord> _data;

    public VerySimpleDataRecordSet()
        : this(new List<VerySimpleDataRecord>())
    { }

    public VerySimpleDataRecordSet(IList<VerySimpleDataRecord> data)
    {
        _data = data;
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
    {
        if (indexes.Length == 1 && indexes[0].GetType() == typeof(int))
        {
            result = _data[(int)indexes[0]];

            return true;
        }

        result = false;

        return false;
    }

    public int Count
    {
        get { return _data.Count; }
    }

    public IEnumerator GetEnumerator()
    {
        return _data.GetEnumerator();
    }
}

A VerySimpleDataRecordSet is initialized by supplying a collection of VerySimpleDataRecords. Like the dictionary collection in the VerySimpleDataRecord, this is stored internally. VerySimpleDataRecordSet also inherits from DynamicObject that provides the TryGetIndex override. When this is invoked, the supplied index value is first validated and if it passes, the appropriate VerySimpleDataRecord is retrieved from the internal collection and returned. The ability to enumerate the VerySimpleDataRecordSet is achieved by implementing IEnumerable: the GetEnumerator method exposes the internal collection’s IEnumerator.

I have only touched a few of the method overrides that are available through DynamicObject mainly to do with getting and returning data. There are other operations to do with setting values and performing conversions that would be worth exploring.

No comments:

Post a Comment