HowTo: “Converting” SPListItem to a C# object

Seriously, how many times have you written code that looks something like this:

SPList list = web.Lists["Orders"];
var items = list.Items;

var orders = from SPListItem order in items
select new
{
Id = int.Parse(order["OrderId"].ToString()),
Title = order[SPBuiltInFieldId.Title].ToString(),
Created = DateTime.Parse(order[SPBuiltInFieldId.Created_x0020_Date].ToString()),
Valid = bool.Parse(order["IsValid"].ToString())

};

foreach (var item in orders)
{

……

What I mean is, iterate through a collection of SPListItems and parse each of them to an entity in your domain model (in the example I´m just using a anonymous type to do that). There´s a lot of int.Parse and bool.Parse and so going on, really tiresome code. What I also should be doing is to check for null and if the field actually exists on the SPListItem.
There´s gotto be something better we can do. So, here goes:

My inspiration comes from the DataRowExtensions class and the Field<T> method. With that extension you can write code that looks like this:
int i = r.Field<int>(“MyColumn”);

So, my mission will be to write something like this for SPListItems and make an extension method that looks just like that. So the final code will look like this (same as first example apart from the new extension method calls in the select statement):

Id = order.Field<int>("OrderId"),
Title = order.Field<string>("Title"),
Created = order.Field<DateTime?>("Created_x0020_Date"),
Valid = order.Field<bool>("IsValid")

This is possible by first creating a helper class for converting objects to reference types, value types or nullable types.

The DataConverter

public static class GenericDataConverter<T>
{
/// <summary>
/// Converts the object to typeof T.
/// </summary>
/// <exception cref="System.InvalidCastException" />
public static readonly Converter<object, T> Convert;


static GenericDataConverter()
{
GenericDataConverter<T>.Convert = GenericDataConverter<T>.Create(typeof(T));
}

private static Converter<object, T> Create(Type type)
{
if (type.IsClass == true)
{
// reference type converter delegate
return new Converter<object, T>(GenericDataConverter<T>.ReferenceType);
}

// nullable type converter delegate
if ((type.IsGenericType && type.IsGenericTypeDefinition == false)
&& (typeof(Nullable<>) == type.GetGenericTypeDefinition()))
{
return (Converter<object, T>)Delegate.CreateDelegate(
typeof(Converter<object, T>),
typeof(GenericDataConverter<T>).GetMethod(
"NullableField", BindingFlags.NonPublic
| BindingFlags.Static).MakeGenericMethod(
new Type[] { type.GetGenericArguments()[0] }));
}

// value type converter delegate
return new Converter<object, T>(GenericDataConverter<T>.ValueType);
}

This class, that does all the magic, has a static constructor that gets called every time someone asks for the public Converter delegate Convert.
The constructor will then call on the private Create method that determines what implementation of the delegate to pass back.
There´s one for each case, so three implementations that look like this:

Reference Types

private static T ReferenceType(object value)
{
if (value == null)
{
return default(T);
}
else if(typeof(T).IsAssignableFrom(value.GetType()) == true)
{
return (T)value;
}


return default(T);
}

Value Types

private static T ValueType(object value)
{
var convertible = value as IConvertible;
if (convertible != null)
{
return (T)System.Convert.ChangeType(convertible, typeof(T));
}


var converter = TypeDescriptor.GetConverter(value);

if (converter.CanConvertTo(typeof(T)))
{
return (T)converter.ConvertTo(value, typeof(T));
}

if (value == null)
{
throw new InvalidCastException(string.Format(CultureInfo.InvariantCulture,
"Unable to cast type '{0}'.", typeof(T).Name));
}

return default(T);

}

Nullable Types

private static U? NullableField<U>(object value) where U : struct
{
if (value == null)
{
return null;
}


var convertible = value as IConvertible;
if (convertible != null)
{
return (U)System.Convert.ChangeType(convertible, typeof(U));
}

var converter = TypeDescriptor.GetConverter(value);

if (converter.CanConvertTo(typeof(T)))
{
return (T)converter.ConvertTo(value, typeof(T));
}

return new U?((U)value);
}

The Extension Method

So, once we have the type conversion in place all we have to do is write an extension method for the SPListItem class and call our helper class, looks like this:

public static T Field<T>(this SPListItem item, string fieldName)
{
if (item.Fields.ContainsField(fieldName) == true)
{
T obj = default(T);


try
{
obj = GenericDataConverter<T>.Convert(item[fieldName]);
return obj;
}
catch
{
throw;
}
}

throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture,
"The name '{0}' was not found on SPListItem.", fieldName));

}

Note: I´m using the name of the field to get somewhat better error messages, ie a guid won´t tell us much.

Note:
I have used the same technique for extracting typed values from XElements in an xml file parser also.

UPDATE 2010-02-20: The code for the data converter and the extension method can now be found at codeplex.

  1. Leave a comment

Leave a comment