Using Reflection to create a DataTable from a Class? - c#

I've just learned about Generics and I'm wondering whether I can use it to dynamically build datatables from my classes.
Or I might be missing the point here.
Here is my code, what I'm trying to do is create a datatable from my existing class and populate it. However I'm getting stuck in my thought process.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Data;
namespace Generics
{
public class Dog
{
public string Breed { get; set; }
public string Name { get; set; }
public int legs { get; set; }
public bool tail { get; set; }
}
class Program
{
public static DataTable CreateDataTable(Type animaltype)
{
DataTable return_Datatable = new DataTable();
foreach (PropertyInfo info in animaltype.GetProperties())
{
return_Datatable.Columns.Add(new DataColumn(info.Name, info.PropertyType));
}
return return_Datatable;
}
static void Main(string[] args)
{
Dog Killer = new Dog();
Killer.Breed = "Maltese Poodle";
Killer.legs = 3;
Killer.tail = false;
Killer.Name = "Killer";
DataTable dogTable = new DataTable();
dogTable = CreateDataTable(Dog);
//How do I continue from here?
}
}
}
Now At the DataTable point it errors.
Also, being new to reflection and Generics, how will I actually populate the data with the Killer class?

Building up on all the previous answers, here is a version that creates a DataTable from any collection:
public static DataTable CreateDataTable<T>(IEnumerable<T> list)
{
Type type = typeof(T);
var properties = type.GetProperties();
DataTable dataTable = new DataTable();
dataTable.TableName = typeof(T).FullName;
foreach (PropertyInfo info in properties)
{
dataTable.Columns.Add(new DataColumn(info.Name, Nullable.GetUnderlyingType(info.PropertyType) ?? info.PropertyType));
}
foreach (T entity in list)
{
object[] values = new object[properties.Length];
for (int i = 0; i < properties.Length; i++)
{
values[i] = properties[i].GetValue(entity);
}
dataTable.Rows.Add(values);
}
return dataTable;
}

Here is a more compact version of David's answer that is also an extension function. I've posted the code in a C# project on Github.
public static class Extensions
{
public static DataTable ToDataTable<T>(this IEnumerable<T> self)
{
var properties = typeof(T).GetProperties();
var dataTable = new DataTable();
foreach (var info in properties)
dataTable.Columns.Add(info.Name, Nullable.GetUnderlyingType(info.PropertyType)
?? info.PropertyType);
foreach (var entity in self)
dataTable.Rows.Add(properties.Select(p => p.GetValue(entity)).ToArray());
return dataTable;
}
}
I have found that this works very well in conjunction with code to write a DataTable to CSV.

my favorite homemade function. it create and populate all at same time. throw any object.
public static DataTable ObjectToData(object o)
{
DataTable dt = new DataTable("OutputData");
DataRow dr = dt.NewRow();
dt.Rows.Add(dr);
o.GetType().GetProperties().ToList().ForEach(f =>
{
try
{
f.GetValue(o, null);
dt.Columns.Add(f.Name, f.PropertyType);
dt.Rows[0][f.Name] = f.GetValue(o, null);
}
catch { }
});
return dt;
}

The error can be resolved by changing this:
dogTable = CreateDataTable(Dog);
to this:
dogTable = CreateDataTable(typeof(Dog));
But there are some caveats with what you're trying to do. First, a DataTable can't store complex types, so if Dog has an instance of Cat on it, you won't be able to add that as a column. It's up to you what you want to do in that case, but keep it in mind.
Second, I would recommend that the only time you use a DataTable is when you're building code that knows nothing about the data its consuming. There are valid use cases for this (e.g. a user-driven data mining tool). If you already have the data in the Dog instance, just use it.
Another little tidbit, this:
DataTable dogTable = new DataTable();
dogTable = CreateDataTable(Dog);
can be condensed to this:
DataTable dogTable = CreateDataTable(Dog);

Here is a little bit modified code, which fixed time zone issue for datatime fields:
public static DataTable ToDataTable<T>(this IList<T> data)
{
PropertyDescriptorCollection props =
TypeDescriptor.GetProperties(typeof(T));
DataTable table = new DataTable();
for (int i = 0; i < props.Count; i++)
{
PropertyDescriptor prop = props[i];
table.Columns.Add(prop.Name, prop.PropertyType);
}
object[] values = new object[props.Count];
foreach (T item in data)
{
for (int i = 0; i < values.Length; i++)
{
if (props[i].PropertyType == typeof(DateTime))
{
DateTime currDT = (DateTime)props[i].GetValue(item);
values[i] = currDT.ToUniversalTime();
}
else
{
values[i] = props[i].GetValue(item);
}
}
table.Rows.Add(values);
}
return table;
}

Here's a VB.Net version that creates a data table from a generic list passed to the function as an object. There is also a helper function (ObjectToDataTable) that creates a data table from an object.
Imports System.Reflection
Public Shared Function ListToDataTable(ByVal _List As Object) As DataTable
Dim dt As New DataTable
If _List.Count = 0 Then
MsgBox("The list cannot be empty. This is a requirement of the ListToDataTable function.")
Return dt
End If
Dim obj As Object = _List(0)
dt = ObjectToDataTable(obj)
Dim dr As DataRow = dt.NewRow
For Each obj In _List
dr = dt.NewRow
For Each p as PropertyInfo In obj.GetType.GetProperties
dr.Item(p.Name) = p.GetValue(obj, p.GetIndexParameters)
Next
dt.Rows.Add(dr)
Next
Return dt
End Function
Public Shared Function ObjectToDataTable(ByVal o As Object) As DataTable
Dim dt As New DataTable
Dim properties As List(Of PropertyInfo) = o.GetType.GetProperties.ToList()
For Each prop As PropertyInfo In properties
dt.Columns.Add(prop.Name, prop.PropertyType)
Next
dt.TableName = o.GetType.Name
Return dt
End Function

Using the answer provided by #neoistheone I've changed the following sections. Works fine now.
DataTable dogTable = new DataTable();
dogTable = CreateDataTable(typeof(Dog));
dogTable.Rows.Add(Killer.Breed, Killer.Name,Killer.legs,Killer.tail);
foreach (DataRow row in dogTable.Rows)
{
Console.WriteLine(row.Field<string>("Name") + " " + row.Field<string>("Breed"));
Console.ReadLine();
}

you can convert the object to xml then load the xml document to a dataset, then extract the first table out of the data set. However i dont see how this be practical as it infers creating streams, datasets & datatables and using converstions to create the xml document.
I guess for proof of concept i can understand why. Here is an example, but somewhat hesitant to use it.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Data;
using System.Xml.Serialization;
namespace Generics
{
public class Dog
{
public string Breed { get; set; }
public string Name { get; set; }
public int legs { get; set; }
public bool tail { get; set; }
}
class Program
{
public static DataTable CreateDataTable(Object[] arr)
{
XmlSerializer serializer = new XmlSerializer(arr.GetType());
System.IO.StringWriter sw = new System.IO.StringWriter();
serializer.Serialize(sw, arr);
System.Data.DataSet ds = new System.Data.DataSet();
System.Data.DataTable dt = new System.Data.DataTable();
System.IO.StringReader reader = new System.IO.StringReader(sw.ToString());
ds.ReadXml(reader);
return ds.Tables[0];
}
static void Main(string[] args)
{
Dog Killer = new Dog();
Killer.Breed = "Maltese Poodle";
Killer.legs = 3;
Killer.tail = false;
Killer.Name = "Killer";
Dog [] array_dog = new Dog[5];
Dog [0] = killer;
Dog [1] = killer;
Dog [2] = killer;
Dog [3] = killer;
Dog [4] = killer;
DataTable dogTable = new DataTable();
dogTable = CreateDataTable(array_dog);
// continue here
}
}
}
look the following example here

If you want to set columns order/ Include only some columns/ exclude some columns try this:
private static DataTable ConvertToDataTable<T>(IList<T> data, string[] fieldsToInclude = null,
string[] fieldsToExclude = null)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
DataTable table = new DataTable();
foreach (PropertyDescriptor prop in properties)
{
if ((fieldsToInclude != null && !fieldsToInclude.Contains(prop.Name)) ||
(fieldsToExclude != null && fieldsToExclude.Contains(prop.Name)))
continue;
table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
}
foreach (T item in data)
{
var atLeastOnePropertyExists = false;
DataRow row = table.NewRow();
foreach (PropertyDescriptor prop in properties)
{
if ((fieldsToInclude != null && !fieldsToInclude.Contains(prop.Name)) ||
(fieldsToExclude != null && fieldsToExclude.Contains(prop.Name)))
continue;
row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
atLeastOnePropertyExists = true;
}
if(atLeastOnePropertyExists) table.Rows.Add(row);
}
if (fieldsToInclude != null)
SetColumnsOrder(table, fieldsToInclude);
return table;
}
private static void SetColumnsOrder(DataTable table, params String[] columnNames)
{
int columnIndex = 0;
foreach (var columnName in columnNames)
{
table.Columns[columnName].SetOrdinal(columnIndex);
columnIndex++;
}
}

Related

How to convert string separated by new line and comma to DataTable in C#

I have a string like this:
"Product,Price,Condition
Cd,13,New
Book,9,Used
"
Which is being passed like this:
"Product,Price,Condition\r\Cd,13,New\r\nBook,9,Used"
How could I convert it to DataTable?
Trying to do it with this helper function:
DataTable dataTable = new DataTable();
bool columnsAdded = false;
foreach (string row in data.Split(new string[] { "\r\n" }, StringSplitOptions.None))
{
DataRow dataRow = dataTable.NewRow();
foreach (string cell in row.Split(','))
{
string[] keyValue = cell.Split('~');
if (!columnsAdded)
{
DataColumn dataColumn = new DataColumn(keyValue[0]);
dataTable.Columns.Add(dataColumn);
}
dataRow[keyValue[0]] = keyValue[1];
}
columnsAdded = true;
dataTable.Rows.Add(dataRow);
}
return dataTable;
However I don't get that "connecting cells with appropriate columns" part - my cells don't have ~ in string[] keyValue = cell.Split('~'); and I obviously get an IndexOutOfRange at DataColumn dataColumn = new DataColumn(keyValue[0]);
Based on your implementation, I have written the code for you, I have not tested it. But you can use the concept.
DataRow dataRow = dataTable.NewRow();
int i = 0;
foreach (string cell in row.Split(','))
{
if (!columnsAdded)
{
DataColumn dataColumn = new DataColumn(cell);
dataTable.Columns.Add(dataColumn);
}
else
{
dataRow[i] = cell;
}
i++;
}
if(columnsAdded)
{
dataTable.Rows.Add(dataRow);
}
columnsAdded = true;
You can do that simply with Linq (and actually there is LinqToCSV on Nuget, maybe you would prefer that):
void Main()
{
string data = #"Product,Price,Condition
Cd,13,New
Book,9,Used
";
var table = ToTable(data);
Form f = new Form();
var dgv = new DataGridView { Dock = DockStyle.Fill, DataSource = table };
f.Controls.Add(dgv);
f.Show();
}
private DataTable ToTable(string CSV)
{
DataTable dataTable = new DataTable();
var lines = CSV.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var colname in lines[0].Split(','))
{
dataTable.Columns.Add(new DataColumn(colname));
}
foreach (var row in lines.Where((r, i) => i > 0))
{
dataTable.Rows.Add(row.Split(','));
}
return dataTable;
}
You can split given string into flattened string array in one call. Then you can iterate through the array and populate list of objects.
That part is optional, since you can immediately populate DataTable but I think it's way easier (more maintainable) to work with strongly-typed objects when dealing with DataTable.
string input = "Product,Price,Condition\r\nCd,13,New\r\nBook,9,Used";
string[] deconstructedInput = input.Split(new string[] { "\r\n", "," }, StringSplitOptions.None);
List<Product> products = new List<Product>();
for (int i = 3; i < deconstructedInput.Length; i += 3)
{
products.Add(new Product
{
Name = deconstructedInput[i],
Price = Decimal.Parse(deconstructedInput[i + 1]),
Condition = deconstructedInput[i + 2]
});
}
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Condition { get; set; }
}
So, products collection holds 2 objects which you can easily iterate over and populate your DataTable.
Note: This requires further checks to avoid possible runtime exceptions, also it is not dynamic. That means, if you have differently structured input it won't work.
DataTable dataTable = new DataTable();
dataTable.Columns.Add(new DataColumn(nameof(Product.Name)));
dataTable.Columns.Add(new DataColumn(nameof(Product.Price)));
dataTable.Columns.Add(new DataColumn(nameof(Product.Condition)));
foreach (var product in products)
{
var row = dataTable.NewRow();
row[nameof(Product.Name)] = product.Name;
row[nameof(Product.Price)] = product.Price;
row[nameof(Product.Condition)] = product.Condition;
dataTable.Rows.Add(row);
}

C# Reading CSV file with SQL conditions

I am using CsvHelper lib to read CSV file and I can successfully read the file with the lib. However I cannot use SQL condition to filter values. How can I do that without using SQL Server. I am really stuck on it.
It was very easy with Pandas and Pandasql libs in Python but it is being too hard in C#..
My Code:
public static void Main(string[] args)
{
var fileInfo = new FileInfo(#"filePath");
using (TextReader reader = fileInfo.OpenText())
using (var csvReader = new CsvReader(reader))
{
csvReader.Configuration.Delimiter = ",";
csvReader.Configuration.HasHeaderRecord = false;
csvReader.Configuration.IgnoreQuotes = true;
csvReader.Configuration.TrimFields = true;
csvReader.Configuration.WillThrowOnMissingField = false;
while (csvReader.Read())
{
var myStrinVar = csvReader.GetField<string>(0);
Console.Write(myStrinVar); //SELECT * FROM table...
}
}
}
I would suggest using LINQ to filter your results.
https://msdn.microsoft.com/en-us/library/bb397906.aspx
Say you have some class MyClass that you can serialize the lines in your file into.
For example:
public class MyClass
{
public int ID { get; set; }
}
var records = csv.GetRecords<MyClass>().ToList();
var filtered = records.Where(r => r.ID >= 10);
That example is a bit contrived but you can use any boolean expression you like in the where clause.
I know this is too late for OP, but the issue with the accepted answer is that you have to read in the entire result set to memory which may not be tenable for large files. Also, if you can extend this code below to get the top N rows without having to read the entire CSV if you find matches early in the file.
public static void Main(string[] args)
{
var fileInfo = new FileInfo(#"filePath");
var where = ""; //Code to set up where clause part of query goes here
using (TextReader reader = fileInfo.OpenText())
using (var csvReader = new CsvReader(reader))
{
csvReader.Configuration.Delimiter = ",";
csvReader.Configuration.HasHeaderRecord = false;
csvReader.Configuration.IgnoreQuotes = true;
csvReader.Configuration.TrimFields = true;
csvReader.Configuration.WillThrowOnMissingField = false;
DataTable dt = null;
while (csvReader.Read())
{
//Use the first row to initialize the columns.
if (dt == null)
{
dt = new DataTable();
for (var i = 0; i < csvReader.FieldCount; i++)
{
var fieldType = csvReader.GetFieldType(i);
DataColumn dc;
if (fieldType.IsNullableType())
{
dc = new DataColumn(csvReader.GetName(i), Nullable.GetUnderlyingType(fieldType));
dc.AllowDBNull = true;
}
else
dc = new DataColumn(csvReader.GetName(i), data.GetFieldType(i));
dt.Columns.Add(dc);
}
}
//Map DataReader to DataRow
var newRow = dt.Rows.Add();
foreach(DataColumn col in dt.Columns)
{
newRow[col.ColumnName] = csvReader[col.ColumnName];
}
//Create a temporary DataView and filter it with the where clause.
DataView dv = new DataView(dt);
dv.RowFilter = where;
var data = dv.Count > 0 ? dv[0] : null;
if(data != null)
{
//Row in here matches your where clause.
//Code to read this row or do something with it.
}
//Empty the temporary data table.
dt.Rows.Clear();
}
}
}

08P01: insufficient data left in message for Nullable DateTime

I have database table having one column defined as timestamp without time zone. Now from my c# application, when I try to insert null value in that column using NpgSql BeginBinaryImport it gives error message as mentioned below:
08P01: insufficient data left in message
Below is the code which I am trying to execute:
static void Main(string[] args)
{
BulkInsert();
}
private static void BulkInsert()
{
DataTable table = new DataTable();
table.Columns.Add("firstname", typeof(String));
table.Columns.Add("lastname", typeof(String));
table.Columns.Add("logdatetime", typeof(DateTime));
table.Columns.Add("status", typeof(int));
table.Columns.Add("id", typeof(long));
var dataRow = table.NewRow();
dataRow["firstname"] = "MyFirstName";
dataRow["lastname"] = "MyLastName";
dataRow["logdatetime"] = DBNull.Value;
dataRow["status"] = 1;
dataRow["id"] = 10;
table.Rows.Add(dataRow);
var data = new DataAccess();
using (var npgsqlConn = new NpgsqlConnection([ConnectionString]))
{
npgsqlConn.Open();
var commandFormat = string.Format(CultureInfo.InvariantCulture, "COPY {0} {1} FROM STDIN BINARY", "logging.testtable", "(firstName,LastName,logdatetime,status,id)");
using (var writer = npgsqlConn.BeginBinaryImport(commandFormat))
{
foreach (DataRow item in dataTable.Rows)
{
writer.StartRow();
foreach (var item1 in item.ItemArray)
{
writer.Write(item1);
}
}
}
npgsqlConn.Close();
}
The issue is the DBNull.Value you're trying to write - Npgsql doesn't support writing nulls this way. To write a null you need to use the WriteNull() method instead.
I can make Npgsql accept DBNull.Value, but only for the overload of Write() which also accepts an NpgsqlDbType (because Npgsql has to write the data type, and with DBNull.Value we have no idea what that is).
EDIT: Have done this, see https://github.com/npgsql/npgsql/issues/1122.
I faced same issue while bulk copying data in table. To solve this I have created an extension method so you dont have to null check on all fields
public static void WriteWithNullCheck(this NpgsqlBinaryImporter writer, string value)
{
if (string.IsNullOrEmpty(value))
{
writer.WriteNull();
}
else
{
writer.Write(value);
}
}
this can be made generic by
public static void WriteWithNullCheck<T>(this NpgsqlBinaryImporter writer, T value,NpgsqlDbType type)
{
if (value == null)
{
writer.WriteNull();
}
else
{
writer.Write(value, type);
}
}

Convert IEnumerable string array to datatable

I have a csv file delimited with pipe(|). I am reading it using the following line of code:
IEnumerable<string[]> lineFields = File.ReadAllLines(FilePath).Select(line => line.Split('|'));
Now, I need to bind this to a GridView. So I am creating a dynamic DataTable as follows:
DataTable dt = new DataTable();
int i = 0;
foreach (string[] order in lineFields)
{
if (i == 0)
{
foreach (string column in order)
{
DataColumn _Column = new DataColumn();
_Column.ColumnName = column;
dt.Columns.Add(_Column);
i++;
//Response.Write(column);
//Response.Write("\t");
}
}
else
{
int j = 0;
DataRow row = dt.NewRow();
foreach (string value in order)
{
row[j] = value;
j++;
//Response.Write(column);
//Response.Write("\t");
}
dt.Rows.Add(row);
}
//Response.Write("\n");
}
This works fine. But I want to know if there is a better way to convert IEnumerable<string[]> to a DataTable. I need to read many CSVs like this, so I think the above code might have performance issues.
Starting from .Net 4:
use ReadLines.
DataTable FileToDataTable(string FilePath)
{
var dt = new DataTable();
IEnumerable<string[]> lineFields = File.ReadLines(FilePath).Select(line => line.Split('|'));
dt.Columns.AddRange(lineFields.First().Select(i => new DataColumn(i)).ToArray());
foreach (var order in lineFields.Skip(1))
dt.Rows.Add(order);
return dt;
}
(edit: instead this code, use the code of #Jodrell answer, This prevents double charging of the Enumerator).
Before .Net 4:
use streaming:
DataTable FileToDataTable1(string FilePath)
{
var dt = new DataTable();
using (var st = new StreamReader(FilePath))
{
// first line procces
if (st.Peek() >= 0)
{
var order = st.ReadLine().Split('|');
dt.Columns.AddRange(order.Select(i => new DataColumn(i)).ToArray());
}
while (st.Peek() >= 0)
dt.Rows.Add(st.ReadLine().Split('|'));
}
return dt;
}
since, in your linked example, the file has a header row.
const char Delimiter = '|';
var dt = new DataTable;
using (var m = File.ReadLines(filePath).GetEnumerator())
{
m.MoveNext();
foreach (var name in m.Current.Split(Delimiter))
{
dt.Columns.Add(name);
}
while (m.MoveNext())
{
dt.Rows.Add(m.Current.Split(Delimiter));
}
}
This reads the file in one pass.

Dynamic linq to Dataset datarow based on collection property name

I'm working on a project that connects to Oracle. It Brings back data through a dataset. I use Linq to bind it to a collection and throw it back to be read by json. It works great but I can't help but think - there's got to be a better way to do this. Here's an example of what I do. I hope it helps others. Dsp is Dataset.
List<Information> lstSearch = null;
lstSearch = (from l in dsp.Tables[0].AsEnumerable()
select new Information
{
application_id = l["APPLICATION_ID"].ToString(),
hospital_name_1 = l["HOSPITAL_NAME_"].ToString(),
physical_address = l["PHYSICAL_ADDRESS"].ToString(),
// may have to add more here...
}).ToList<Information>();
// serialize and send back as a json string
System.Web.Script.Serialization.JavaScriptSerializer oSerializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
string sJSON = oSerializer.Serialize(lstSearch.First());
Theoretically, yes works. The "information" collection matches the html "name" tag of each control on the page which provides a nice robust binding. My concern lies with having
to go through each field name in order to populate the List<> object.
Isn't there a specific where clause in where the collection (get/set) property matches the dataset column name thus populating the collection only if the column name (not the value) matches the data row column?
I'd recommend AutoMapper ... see this question.
Here's a very simple example. Make sure that your .NET type exactly mirrors the structure of your DB object or AutoMapper will not work as advertised:
namespace EnumerableDT
{
class Program
{
public class Information
{
public int APPLICATION_ID { get; set; }
public string HOSPITAL_NAME { get; set; }
public string PHYSICAL_ADDRESS { get; set; }
public string SOME_OTHER_FIELD { get; set; }
}
static DataSet dsp = new DataSet();
static void Main(string[] args)
{
dsp.Tables.Add(BuildDataTableStructure());
dsp.Tables[0].Rows.Add(BuildRow());
AutoMapper.Mapper.Reset();
AutoMapper.Mapper.CreateMap<IDataReader, Information>();
var il = AutoMapper.Mapper.Map<IDataReader, IList<Information>>(dsp.Tables[0].CreateDataReader());
Console.ReadLine();
}
static DataTable BuildDataTableStructure()
{
var dt = new DataTable();
var dc = new DataColumn("APPLICATION_ID", typeof(int));
dt.Columns.Add(dc);
dc = new DataColumn("HOSPITAL_NAME", typeof(string));
dt.Columns.Add(dc);
dc = new DataColumn("PHYSICAL_ADDRESS", typeof(string));
dt.Columns.Add(dc);
dc = new DataColumn("SOME_OTHER_FIELD", typeof(string));
dt.Columns.Add(dc);
return dt;
}
static DataRow BuildRow()
{
DataRow dr = dsp.Tables[0].NewRow();
dr["APPLICATION_ID"] = 1;
dr["HOSPITAL_NAME"] = "The Hospital";
dr["PHYSICAL_ADDRESS"] = "123 Main St.";
dr["SOME_OTHER_FIELD"] = "Some Other Data";
return dr;
}
}
}

Categories