Skip to content

Advanced Object Construction

Ricardo Diaz edited this page Aug 8, 2017 · 1 revision

These are the topics you'll find covered on this page:

Introduction

There are many situations in which you want to create an object instance given a set of input values, but do not know which constructor to call and which members to update.

The classic example of this is when you retrieve some combination of columns from a database and want to use these values to create a single corresponding entity. If the entity class does not provide a default constructor and writable members for all of the retrieved values, you're left with the complex task of figuring out whether a value maps to a constructor parameter or a writable member. It'll be even harder to ensure that the combination with the best performance is chosen (the fastest will be to pass as many values as possible on the constructor).

All of this is further complicated by differences in data types, such as when an integer column is mapped to a .NET enumeration or a Guid is stored as an array of bytes. Let's also not forget that database results usually contain DBNull.Value for NULL columns rather than simply null (a workaround that was necessary because ADO.NET shipped before the introduction of generics and nullable types).

As you have probably guessed, Fasterflect provides an easy-to-use solution to all of the above problems - read on below for the details.

Creating instances without knowing which constructor to call

Fasterflect provides the following extensions for creating object instances where it automatically figures out how best to create the object given a set of input values.

// use the public properties from the sample object to create an instance
object TryCreateInstance( this Type type, object sample );
// use the contents of the supplied dictionary to create an instance
object TryCreateInstance( this Type type, IDictionary<string,object> parameters );
// use the supplied names and associated values to create an instance (types are inferred from the supplied values)
object TryCreateInstance( this Type type, string[] parameterNames, object[] parameterValues );
// use the supplied names and associated types and values to create an instance
object TryCreateInstance( this Type type, string[] parameterNames, Type[] parameterTypes, object[] parameterValues );

Regardless of which overload you use the internal logic is the same. Fasterflect will inspect the supplied parameters and try to match these to constructor parameters and writable members on the type to create. Matching is done by comparing names (ignoring case and any single leading underscore).

It examines all of the available constructors and automatically picks the combination that allows it to use the most values from the input source, while passing as many values as possible as constructor parameters for best performance. Once found the optimal "construction recipe" is cached for subsequent requests that use an identically shaped set of input values (that is, have the same names and types but ignoring the actual values), which ensures the best possible performance not only when creating a single instance but also when creating multiple instances.

// lets assume we want to create an instance of the following class
public class Lion
{
	public int Id { get; private set; }
	public string Name { get; private set; }

	public Lion( int id, string name )
	{
                Id = id;
		Name = name;
	}
}

// creating an instance is a simple one-step process - TryCreateInstance does all the work
Lion animal = typeof(Lion).TryCreateInstance( new { id=42, name="Scar", age=4 } ) as Lion;
Mapping input values named using a reserved (language) keyword

As mentioned above these methods ignore any single leading underscore appearing in either the input or in the name of members or constructor parameters. This detail adds a slight performance cost but allows you to map names that would otherwise be impossible to match. For example, you might have an XML file containing nodes that you wish to map to classes, but your XML file has an attribute called "class". Because this is a reserved word in C# you cannot declare a constructor to take a parameter named class. This feature allows you to declare the parameter as "_class" and thus still be able to make full use of the capabilities provided by TryCreateInstance. Supported type conversions

Another feature inspired by XML is the fact that values retrieved from XML are invariably of type string. Should an input value of type string happen to match a parameter or member with a different data type, TryCreateInstance will try to convert the value. As an example, this feature makes it possible to supply a string input value for a parameter that is of type double, since TryCreateInstance will attempt to convert the string value before using the value. One caveat resulting from this is that the cached "construction recipe" includes information on what type conversions were performed for the first instance created. If you later pass an identical set of input values, but this time with a string that cannot be converted to double, then construction will fail.

The following tables shows the type conversions that TryCreateInstance will attempt to make.

Source type(s) Target type(s)
numeric Any other numeric type, including types that would truncate the original value (such as double to int)
byte[16] Guid
string Guid, enumeration or any numeric primitive type
enum string or any numeric primitive type
XmlNode triggers type conversion of the InnerXml (string) property
XElement and XAttribute triggers type conversion of the Value (string) property

The following example illustrates the some of power of having these type conversions built in.

// lets whip up a quick XElement structure that we use as input 
XElement xml = new XElement( "Books",
	new XElement( "Book",
		new XAttribute( "id", 1 ),
		new XAttribute( "author", "Douglad Adams" ),
		new XAttribute( "title", "The Hitchhikers Guide to the Galaxy" ),
		new XAttribute( "rating", 4.8 )
	)
);

// since we're creating known types we need a corresponding Book class
private class Book
{
	private int _id; // observe that this field is not on the constructor below and contains a leading underscore
	public string Author { get; private set; }
	public string Title { get; private set; }
	public double Rating { get; private set; }

	public Book( string author, string title, double rating )
	{
		Author = author;
		Title = title;
		Rating = rating;
	}
}

// finally the real action begins - first step is to select the data we want to feed to TryCreateInstance
var data = from book in xml.Elements("Book")
                select new { id=book.Attribute("id"), author=book.Attribute("author"), title=book.Attribute("title"), rating=book.Attribute("rating") };
// did you notice that we just grabbed the attributes without selecting the Value property?

// the final step is to create some book instances
IList<Book> books = data.Select( b => typeof(Book).TryCreateInstance( b ) as Book ).ToList();
// we'll verify the results to make sure we got what we expected
Assert.AreEqual( 1, books.Count );
Assert.AreEqual( 1, books[ 0 ].GetFieldValue( "_id" ) );
Assert.AreEqual( "Douglad Adams", books[ 0 ].Author );
Assert.AreEqual( "The Hitchhikers Guide to the Galaxy", books[ 0 ].Title );
Assert.AreEqual( 4.8, books[ 0 ].Rating );

We think this is pretty useful and hope that you will too!

Performance notes

Although the implementation is rather snappy considering all of the magic taking place behind the scenes, this method can never be as fast as doing all of the conversions manually and knowing which constructor to call. TryCreateInstance performs a lot of reflection to locate constructors, parameters, fields and properties, attempts type conversions and builds up internal metadata structures (the "construction recipe"). But because this only happens once (for every unique set of inputs, as explained above), any subsequent calls will be almost as fast as the corresponding calls to CreateInstance.

To quantify the difference, our internal measurements suggest that if you make a lot of calls to TryCreateInstance (with identically shaped inputs so we can reuse the construction recipe) then the overall execution time would be about twice that of calling CreateInstance, the latter of which would require you to manually ensure that parameters are of correct types and in the correct order, and that a corresponding constructor exists.