Created
March 13, 2013 18:06
-
-
Save spartanthe/5154626 to your computer and use it in GitHub Desktop.
Converting .NET DataTable to a strongly typed rows collection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Reflection; | |
using System.Data; | |
namespace spartan.COBOL | |
{ | |
/// <summary> | |
/// DataTable wrapper exposing strongly typed rows collection. | |
/// TRow property names are same as data table's column names. | |
/// </summary> | |
/// <typeparam name="TRow">Table row type</typeparam> | |
/// <remarks> | |
/// Intended use case: when it's difficult to find uses of dynamic tables | |
/// passed around everywhere, it should help you to sort things out, | |
/// step by step. | |
/// </remarks> | |
public class CobolTable<TRow> where TRow : new() | |
{ | |
private DataTable table; | |
/// <summary> | |
/// Constructs new empty typed table. | |
/// </summary> | |
public CobolTable() | |
{ | |
table = new DataTable(); | |
foreach (Column col in TableStructure<TRow>.Columns) | |
{ | |
table.Columns.Add(new DataColumn(col.Name, col.Type)); | |
} | |
} | |
/// <summary> | |
/// Constructs a typed table over an existing data table <paramref name="dataTable"/>. | |
/// </summary> | |
/// <param name="dataTable">Data table</param> | |
/// <remarks> | |
/// Data table structure must match TRow (to the extent triggered by code). | |
/// Ideally TRow property names are same as data table's column names. | |
/// </remarks> | |
public CobolTable(DataTable dataTable) | |
: base() | |
{ | |
if (dataTable == null) | |
throw new ArgumentNullException("dataTable"); | |
this.table = dataTable; | |
} | |
/// <summary> | |
/// Returns wrapped data table. | |
/// </summary> | |
/// <remarks> | |
/// Use to pass it to older method accepting dynamic data table in middle of refactoring. | |
/// </remarks> | |
public DataTable DataTable | |
{ | |
get { return this.table; } | |
} | |
public TypedRowCollection Rows | |
{ | |
get { return new TypedRowCollection(table.Rows); } | |
} | |
public void AddRow(TRow newRow) | |
{ | |
DataRow tableRow = table.NewRow(); | |
CopyToDataRow(tableRow, newRow); | |
table.Rows.Add(tableRow); | |
} | |
#region Implementation | |
private static TRow Extract(DataRow tableRow) | |
{ | |
TRow row = new TRow(); | |
CopyFromDataRow(row, tableRow); | |
return row; | |
} | |
/// <summary> | |
/// Copies DataRow row <paramref name="src"/> | |
/// to typed row <paramref name="dest"/>. | |
/// </summary> | |
private static void CopyFromDataRow(TRow dest, DataRow src) | |
{ | |
foreach (Column col in TableStructure<TRow>.Columns) | |
col.CopyFromDataRow(dest, src); | |
} | |
/// <summary> | |
/// Copies typed row <paramref name="src"/> | |
/// to DataTable row <paramref name="dest"/>. | |
/// </summary> | |
private static void CopyToDataRow(DataRow dest, TRow src) | |
{ | |
foreach (Column col in TableStructure<TRow>.Columns) | |
col.CopyToDataRow(dest, src); | |
} | |
#endregion | |
#region struct TypedRowCollection | |
/// <summary> | |
/// Typed rows wrapper over <see cref="System.Data.DataRowCollection"/>. | |
/// </summary> | |
public struct TypedRowCollection | |
{ | |
DataRowCollection rows; | |
public TypedRowCollection(DataRowCollection rows) | |
{ | |
this.rows = rows; | |
} | |
public int Count { get { return this.rows.Count; } } | |
public TRow this[int index] | |
{ | |
get | |
{ | |
DataRow row = this.rows[index]; | |
TRow typedRow = Extract(row); | |
return typedRow; | |
} | |
} | |
public Enumerator GetEnumerator() | |
{ | |
return new Enumerator(rows.GetEnumerator()); | |
} | |
/// <summary> | |
/// Enumerator (foreach support) | |
/// </summary> | |
public class Enumerator | |
{ | |
IEnumerator rowsEnumerator; | |
public Enumerator(IEnumerator rowsEnumerator) | |
{ | |
this.rowsEnumerator = rowsEnumerator; | |
} | |
public TRow Current | |
{ | |
get | |
{ | |
return Extract((DataRow)rowsEnumerator.Current); | |
} | |
} | |
public bool MoveNext() | |
{ | |
return rowsEnumerator.MoveNext(); | |
} | |
} | |
} | |
#endregion struct TypedRowCollection | |
#region class TableStructure | |
/// <summary> | |
/// Shared type-2-table mapping structure. | |
/// </summary> | |
/// <typeparam name="TRow2">Type of "typed row".</typeparam> | |
private class TableStructure<TRow2> where TRow2 : new() | |
{ | |
static List<Column> sColumns; | |
// TODO: R/O coll | |
public static List<Column> Columns | |
{ | |
get | |
{ | |
if (sColumns == null) | |
sColumns = Column.GetRowColumns(); | |
return sColumns; | |
} | |
} | |
} | |
#endregion class TableStructure | |
#region class Column | |
/// <summary> | |
/// Describes public properties of typed row, provides attribute copiers between | |
/// data table columns and TRow instances. | |
/// </summary> | |
private class Column | |
{ | |
private static Dictionary<Type, List<Column>> TypeToColumnsCache = | |
new Dictionary<Type, List<Column>>(); | |
/// <summary> | |
/// Attribute getter. Reads attribute from the instance. | |
/// value = getter(row) ::= value = row.attr. | |
/// </summary> | |
private MethodInfo attributeGetter; | |
/// <summary> | |
/// Attribute setter. Assigns property to the instance. | |
/// setter(row, value) ::= row.attr = value. | |
/// </summary> | |
private MethodInfo attributeSetter; | |
/// <summary> | |
/// Attribute name; same as table column name. | |
/// </summary> | |
public string Name { get; private set; } | |
/// <summary> | |
/// Attribute type; same as table column data type. | |
/// </summary> | |
public Type Type { get; private set; } | |
/// <summary> | |
/// Copies properties from typed instance to data row. | |
/// </summary> | |
/// <param name="dest">To: Table data row</param> | |
/// <param name="src">From: typed instance</param> | |
internal void CopyToDataRow(DataRow dest, TRow src) | |
{ | |
object attributeValue = attributeGetter.Invoke(src, new object[] {}); | |
object dbValue = (attributeValue ?? DBNull.Value); | |
dest[Name] = dbValue; | |
} | |
/// <summary> | |
/// Copies properties from data row to typed instance. | |
/// </summary> | |
/// <param name="src">From: Table data row</param> | |
/// <param name="dest">To: typed instance</param> | |
internal void CopyFromDataRow(TRow dest, DataRow src) | |
{ | |
object dbValue = src[Name]; | |
object attributeValue = (dbValue != DBNull.Value) ? dbValue : null; | |
attributeSetter.Invoke(dest, new object[] { attributeValue }); | |
} | |
// = Static, Cache | |
/// <summary> | |
/// Parses TRow public properties to find out what "data columns" | |
/// are needed for typed table. | |
/// </summary> | |
/// <returns>Data columns needed for typed table.</returns> | |
internal static List<Column> GetRowColumns() | |
{ | |
return Parse(typeof(TRow)); | |
} | |
#region Implementation | |
/// <summary> | |
/// Reflect public get/set properties of <paramref name="rowType"/> | |
/// as column descriptors. | |
/// Tries type cache first. | |
/// </summary> | |
/// <param name="rowType">'Row' type</param> | |
/// <returns>A list if column descriptors</returns> | |
private static List<Column> Parse(Type rowType) | |
{ | |
List<Column> columns = null; | |
if (!TypeToColumnsCache.TryGetValue(rowType, out columns)) | |
{ | |
columns = ParseType(rowType); | |
TypeToColumnsCache[rowType] = columns; | |
} | |
return columns; | |
} | |
/// <summary> | |
/// Same as Parse, but doesn't use type cache. | |
/// </summary> | |
private static List<Column> ParseType(Type rowType) | |
{ | |
var result = new List<Column>(); | |
// Public R/W properties | |
foreach (PropertyInfo pi in rowType.GetProperties()) | |
{ | |
if (pi.CanRead && pi.CanWrite) | |
{ | |
Column col = new Column(); | |
col.Name = pi.Name; | |
col.attributeGetter = pi.GetGetMethod(); | |
col.attributeSetter = pi.GetSetMethod(); | |
col.Type = pi.PropertyType; | |
result.Add(col); | |
} | |
} | |
return result; | |
} | |
#endregion | |
} | |
#endregion class Column | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Text; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace spartan.COBOL | |
{ | |
[TestClass] | |
public class CobolTableTests | |
{ | |
/// <summary>Row class.</summary> | |
private class MyRow | |
{ | |
public int A { get; set; } | |
public int B { get; set; } | |
} | |
[TestMethod] | |
public void AddRow_AddsCorrectNumberOfRows() | |
{ | |
var table = new CobolTable<MyRow>(); | |
var row1 = new MyRow() { A = 10, B = 2 }; | |
table.AddRow(row1); | |
var row2 = new MyRow() { A = 1000, B = 1000 }; | |
table.AddRow(row2); | |
Assert.AreEqual(2, table.Rows.Count, "Incorrect table.Rows.Count"); | |
} | |
[TestMethod] | |
public void TestSumAllTableFields() | |
{ | |
var table = new CobolTable<MyRow>(); | |
var row1 = new MyRow() { A = 10, B = 2 }; | |
table.AddRow(row1); | |
var row2 = new MyRow() { A = 1000, B = 1000 }; | |
table.AddRow(row2); | |
int tableTotal = 0; | |
foreach (MyRow row in table.Rows) | |
tableTotal += row.A + row.B; | |
Assert.AreEqual(2012, tableTotal, "Incorrect tableTotal"); | |
} | |
[TestMethod] | |
public void WrapsExistingTable_WithSameColumnNamesAsPropertyNames() | |
{ | |
var dataTable = new System.Data.DataTable(); | |
dataTable.Columns.Add("A", typeof(int)); | |
dataTable.Columns.Add("B", typeof(int)); | |
System.Data.DataRow newRow = dataTable.NewRow(); | |
newRow["A"] = 1; | |
newRow["B"] = 2; | |
dataTable.Rows.Add(newRow); | |
var typedTable = new CobolTable<MyRow>(dataTable); | |
Assert.AreEqual(1, typedTable.Rows.Count, "Incorrect number of rows"); | |
// Check first row | |
MyRow row = typedTable.Rows[0]; | |
Assert.AreEqual(1, row.A, "Incorrect rows[0].A"); | |
Assert.AreEqual(2, row.B, "Incorrect rows[0].B"); | |
// Assign new value vie DataRow | |
dataTable.Rows[0]["A"] = 10; | |
Assert.AreEqual(10, typedTable.Rows[0].A, "Incorrect rows[0].A after assignment"); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment