Anonymous Types As Object Templates
I've got a bit of an addiction to intellisense. Dynamic types are interesting but I still prefer being able to see a nice neat list of properties on each of my objects.
Since anonymous types are 'real' classes then you can actually create them at runtime. This means you can use them as templates for other objects if you can provide all of the required parameters.
Below is a simple class that allows you to perform queries against a database and use an anonymous type to provide a template for the records to return.
/// <summary> /// Helps working with database connections /// </summary> public class DataConnection { #region Constructors /// <summary> /// Creates a new DataConnection for the connection string /// </summary> public DataConnection(string connection) { this.ConnectionString = connection; } #endregion #region Properties /// <summary> /// The connection string to use with this DataConnection /// </summary> public string ConnectionString { get; private set; } #endregion #region Performing Queries /// <summary> /// Performs a query for the connection without any parameters /// </summary> public IEnumerable<T> Query<T>(string query, T template) where T : class { return this.Query(query, (object)null, template); } /// <summary> /// Performs a query for the connection with the provided paramters /// </summary> public IEnumerable<T> Query<U, T>(string query, U parameters, T template) where T : class where U : class { //prepare the connection and command Type type = template.GetType(); //create the connection - (thanks @johnsheehan about the 'using' tip) using (SqlConnection connection = new SqlConnection(this.ConnectionString)) { using (SqlCommand command = new SqlCommand(query, connection)) { //assign each of the properties if (parameters != null) { this._UsingProperties( parameters, (name, value) => command.Parameters.AddWithValue(name, value)); } //execute the query connection.Open(); SqlDataReader reader = command.ExecuteReader(); //start reading each of the records List<T> results = new List<T>(); Dictionary<string, int> fields = null; while (reader.Read()) { //prepare the fields for the first time if needed fields = fields == null ? this._ExtractFields(reader) : fields; //generate the record T record = this._CreateAnonymousResult(reader, fields, type, template); results.Add(record); } //return the results for the query return results.AsEnumerable(); } } } #endregion #region Helpers //creates a new anonymous type from private T _CreateAnonymousResult<T>(SqlDataReader reader, Dictionary<string, int> fields, Type templateType, T template) where T : class { //create a container to queue up each record List<object> values = new List<object>(); //use the template to find values this._UsingProperties(template, (name, @default) => { //try and find the field name if (fields.ContainsKey(name)) { //check if this is a value int index = fields[name]; object value = reader[index]; Type convert = @default.GetType(); //try and copy the va;ue if (value != null) { values.Add(Convert.ChangeType(value, convert)); } //not the same type, use the default else { value.Equals(@default); } } //no field was found, just use the default else { values.Add(@default); } }); //with each of the values, invoke the anonymous constructor //which accepts each of the values on the template try { return Activator.CreateInstance(templateType, values.ToArray()) as T; } catch (Exception e) { Console.WriteLine(e.Message); return template; } } //reads a set of records to determine the field names and positions private Dictionary<string, int> _ExtractFields(SqlDataReader reader) { Dictionary<string, int> fields = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < reader.FieldCount; i++) { fields.Add(reader.GetName(i), i); } return fields; } //performs an action with each of the properties on an object private void _UsingProperties(object obj, Action<string, object> with) { //if there isn't anything to work with, just cancel if (obj == null) { return; } //get the type and peform an action for each property Type type = obj.GetType(); foreach (PropertyInfo prop in type.GetProperties()) { with(prop.Name, prop.GetValue(obj, null)); } } #endregion }
This class allows us to perform basic queries against a database and then provide a template of the records to return. This means that if the information is found then the database value is used but if it is missing then the template value is used instead.
In order to create an anonymous type we need to know the type and each of the properties in the order they appear. After collecting this information we simply need to use the Activator.CreateInstance
method to instantiate the class and we're set! (method _CreateAnonymousResult)
This means we can write a 'dynamic' query with results that have intellisense!
//set up the connection DataConnection data = new DataConnection(CONNECTION_DATA); //perform the query var results = data.Query( "select * from orders where orderId > @orderId", new { orderId = 11000 }, new { orderId = -1, shipName = "missing", shipRegion = "none" }); //access properties var record = results.First(); int id = record.orderId; //intellisense!
I've actually used this same approach before in CSMongo as a way to provide intellisense without knowing anything about the database in advance.
Of course, this is just some code I hacked out this evening. You aren't going to be able to do a lot with it but it is a good example of how you can reuse anonymous types in your code.
August 30, 2010
Anonymous Types As Object Templates
Post titled "Anonymous Types As Object Templates"