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"
hugoware.net is licensed under a