Get subclass properties from list of superclass - c#

I have a method that will convert a List<T> into a TSV string with a header row and will only have rows which have a value for any item in the list. What I was doing previously was getting the type of T and working off that. My problem now, it that my list will no longer contain an item of type T, but something that derives from the type of the list. So doing typeof(T).GetProperties() will no longer work because that's looking at the parent type and not the child type.
public static string ListToTSVString<T>(List<T> items)
{
StringBuilder builder = new StringBuilder();
PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
// Get only properties that actually have a value in the list.
var propsDictionary =
properties.ToDictionary(p => p, p => items.Select(l => p.GetValue(l)).ToArray())
.Where(pair => pair.Value.Any(o => o != null))
.ToDictionary(pair => pair.Key, pair => pair.Value);
// Header row
builder.AppendLine(string.Join("\t", propsDictionary.Keys.Select(x => x.GetCustomAttribute<JsonPropertyAttribute>().PropertyName)));
// Body of TSV
foreach (T item in items)
{
builder.AppendLine(string.Join("\t", propsDictionary.Keys.Select(x => x.GetValue(item))));
}
// Remove new line character
return builder.ToString().TrimEnd();
}
As an example of something I am passing in is:
public class CustomObject
{
public string GUID { get; set; }
}
public class Referral : CustomObject
{
public string Name { get; set; }
}
I'd then call this with a: List<CustomObject> objects = new List<CustomObject> { new Referral() { Name = "Jimenemex" } }.
I'm trying to get the TSV string to now contain only the declared properties on the Referral type and not the CustomObject type. Before I was only working with an object that doesn't inherit from anything so this was working nicely.
I've tried to use items.GetType().GetGenericArguments()[0], but that would still get the CustomObject type.

Instead of using typeof(T) you can use the type of item stored in items collections. See following code (use ConcurrentDictionary to cache the properties only)
private static ConcurrentDictionary<Type, PropertyInfo[]> typeProperties = new ConcurrentDictionary<Type, PropertyInfo[]>();
private static PropertyInfo[] GetProperties(Type type)
{
if (!typeProperties.ContainsKey(type))
{
typeProperties[type] = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
}
return typeProperties[type];
}
public static string ListToTSVString<T>(List<T> items)
{
StringBuilder builder = new StringBuilder();
PropertyInfo[] properties = GetProperties(items.First().GetType());
// Get only properties that actually have a value in the list.
var propsDictionary =
properties.ToDictionary(p => p, p => items.Select(l => p.GetValue(l)).ToArray())
.Where(pair => pair.Value.Any(o => o != null))
.ToDictionary(pair => pair.Key, pair => pair.Value);
// Header row
builder.AppendLine(string.Join("\t", propsDictionary.Keys.Select(x => x.GetCustomAttribute<JsonPropertyAttribute>().PropertyName)));
// Body of TSV
foreach (T item in items)
{
builder.AppendLine(string.Join("\t", propsDictionary.Keys.Select(x => x.GetValue(item))));
}
// Remove new line character
return builder.ToString().TrimEnd();
}
You can see this fiddle - https://dotnetfiddle.net/HtnFkG created for demo your scenario.

Related

Build Expression tree to Add elements to Collection dynamically c#

I have a class and I need to iterate tru each property reading the attribute name to map to my data Source the value, in the cases where I have a ICollection that property will have multiple attributes to map the correct value.
I'm using Expression trees to set the values efficiently to each property but I'm having issues to set the values to the Collection.
I think this is because I need to create an instance of that Collection but I don't know. I'm a bit lost on that one here's what I got:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class MapToAttribute : Attribute
{
public MapToAttribute(string field)
{
Field = field;
}
public string Field { get; private set; }
}
public class MyDataClass
{
[MapTo("one")]
public int propOne { get; set; }
[MapTo("two")]
public string propTwo { get; set; }
[MapTo("item1")]
[MapTo("item2")]
[MapTo("item3")]
public ICollection<int> collection { get; set; }
}
class Program
{
static void Main(string[] args)
{
var setter = SetValues<MyDataClass>();
}
private static IEnumerable<T> SetValues<T>()
where T : new()
{
var properties = GetClassProperties<T>();
var results = new List<T>();
for (int x = 1; x<=100; x++)
{
var row = new T();
//Simulated Datasource
var dataSource = new Dictionary<string, object>();
dataSource.Add("one", x);
dataSource.Add("two", x.ToString());
dataSource.Add("item1", x);
dataSource.Add("item2", x+x);
dataSource.Add("item3", x*x);
foreach (var property in properties)
{
//this line executes the Action
property.Value(row, dataSource[property.Key]);
}
results.Add(row);
}
return results;
}
private static Dictionary<string, Action<T, object>> GetClassProperties<T>()
{
var setters = new Dictionary<string, Action<T, object>>();
var instance = Expression.Parameter(typeof(T));
var argument = Expression.Parameter(typeof(object));
foreach (var property in typeof(T).GetProperties())
{
var names = property.GetCustomAttributes(typeof(MapToAttribute), true)
.Select(p => ((MapToAttribute)p).Field);
var setter = Expression.Lambda<Action<T, object>>(
Expression.Call(
instance,
property.GetSetMethod(),
Expression.Convert(argument, property.PropertyType)
), instance, argument
).Compile();
// Due to the types I cannot just assign a value to a ICollection,
// that's why I tried to create HERE a different setter
// when the property Type is ICollection, I commented out the code failing.
//var getCollection = Expression.Lambda<Func<T, object>>(
// Expression.Call(
// instance,
// prop.GetGetMethod()
// ), instance
// ).Compile();
//Action<T, object> setter = (classInstance, value) =>
// getCollection(classInstance).Add(value);
foreach (var name in names)
{
setters.Add(name, setter);
}
}
return setters;
}
}
First of all to make life easier you will need to initialize collection:
public ICollection<int> collection { get; set; } = new List<int>();
Second try this:
private static Dictionary<string, Action<T, object>> GetClassProperties<T>()
{
var setters = new Dictionary<string, Action<T, object>>();
var instance = Expression.Parameter(typeof(T));
var argument = Expression.Parameter(typeof(object));
foreach (var property in typeof(T).GetProperties())
{
var names = property.GetCustomAttributes(typeof(MapToAttribute), true)
.Select(p => ((MapToAttribute)p).Field)
.ToList();
if (property.PropertyType.IsGenericType) // start checking that prop implements ICollection
{
// get ICollection generic parameter type
var genericParam = property.PropertyType.GetGenericArguments().First();
// construct concrete ICollection type
var propColType = typeof(ICollection<>).MakeGenericType(genericParam);
if (propColType.IsAssignableFrom(property.PropertyType)) // check if is ICollection of genericParam
{
var getCollection = Expression.Call(instance, property.GetGetMethod());
var addMethod = propColType.GetMethod("Add");
var colAddSetter = Expression.Lambda<Action<T, object>>(
Expression.Call(getCollection, addMethod, Expression.Convert(argument, genericParam)),
instance, argument)
.Compile();
foreach (var name in names)
{
setters.Add(name, colAddSetter);
}
continue; // process next property
}
}
// process "ordinary" property
var setter = Expression.Lambda<Action<T, object>>(
Expression.Call(
instance,
property.GetSetMethod(),
Expression.Convert(argument, property.PropertyType)
), instance, argument
).Compile();
setters.Add(names.Single(), setter); // todo throw normal exception instead of Single
}
return setters;
}

Get members of custom class to read their values

Let say I have class:
public class TestClass
{
public string Prop1 { get; set; }
public int Field1 = 1234567890;
public string Method1() { return "ABCDEFGHIJKLMNOPQRSTUVXYZ"; }
}
... class instance and list:
TestClass TC = new TestClass();
List<object> TCValues = new List<object>();
... and populate the list with values in loop:
foreach (var v in TC.GetType().GetProperties()) // or .GetFields()
{
TCValues.Add(v.GetValue(TC, null));
}
... problem is that in my particular case I need to get list of all class members first, then filter them to properties and fields (ignoring methods of course) and then read their values as I did in first example:
foreach (var v in TC.GetType().GetMembers())
{
if (v.MemberType == System.Reflection.MemberTypes.Property || v.MemberType == System.Reflection.MemberTypes.Field)
{
TCValues.Add(v.?????????); // Can't get values !
}
}
... I understand that GetMembers() returns class MemberInfo which unlike PropertyInfo and FieldInfo doesn't contain method GetValue(). Is there any way to read values from filtered property and field members inside the loop iterating through MemberInfo collection ?
In your foreach-Loop try
foreach (var v in TC.GetType().GetMembers())
{
if (v is PropertyInfo)
{
var value = ((PropertyInfo)v).GetValue(TC, null);
TCValues.Add(value);
}
else if (v is FieldInfo)
{
var value = ((FieldInfo) v).GetValue(TC);
TCValues.Add(value);
}
}
TC.GetType().GetProperty(propName).GetValue(TC);
You have to cast the members to the correct type:
foreach (var v in TC.GetType().GetMembers())
{
if (v.MemberType == System.Reflection.MemberTypes.Property)
{
TCValues.Add(((System.Reflection.PropertyInfo)v).GetValue(TC,null));
}
else if (v.MemberType == System.Reflection.MemberTypes.Field)
{
TCValues.Add(((System.Reflection.FieldInfo)v).GetValue(TC));
}
}

Linq group by except column

I have a class with large amount of properties that I need to group by almost all columns.
class Sample {
public string S1 { get; set; }
public string S2 { get; set; }
public string S3 { get; set; }
public string S4 { get; set; }
// ... all the way to this:
public string S99 { get; set; }
public decimal? N1 { get; set; }
public decimal? N2 { get; set; }
public decimal? N3 { get; set; }
public decimal? N4 { get; set; }
// ... all the way to this:
public decimal? N99 { get; set; }
}
From time to time I need to group by all columns except one or two decimal columns and return some result based on this (namely object with all the fields, but with some decimal value as a sum or max).
Is there are any extension method that would allow me to do something like this:
sampleCollection.GroupByExcept(x => x.N2, x => x.N5).Select(....);
instead of specifying all columns in object?
You won't find anything builtin that handles such a case. You'd have to create one yourself. Depending on how robust you need this to be, you could take a number of approaches.
The main hurdle you'll come across is how you'll generate the key type. In an ideal situation, the new keys that are generated would have their own distinct type. But it would have to be dynamically generated.
Alternatively, you could use another type that could hold multiple distinct values and still could be suitably used as the key. Problem here is that it will still have to be dynamically generated, but you will be using existing types.
A different approach you could take that doesn't involve generating new types, would be to use the existing source type, but reset the excluded properties to their default values (or not set them at all). Then they would have no effect on the grouping. This assumes you can create instances of this type and modify its values.
public static class Extensions
{
public static IQueryable<IGrouping<TSource, TSource>> GroupByExcept<TSource, TXKey>(this IQueryable<TSource> source, Expression<Func<TSource, TXKey>> exceptKeySelector) =>
GroupByExcept(source, exceptKeySelector, s => s);
public static IQueryable<IGrouping<TSource, TElement>> GroupByExcept<TSource, TXKey, TElement>(this IQueryable<TSource> source, Expression<Func<TSource, TXKey>> exceptKeySelector, Expression<Func<TSource, TElement>> elementSelector)
{
return source.GroupBy(BuildKeySelector(), elementSelector);
Expression<Func<TSource, TSource>> BuildKeySelector()
{
var exclude = typeof(TXKey).GetProperties()
.Select(p => (p.PropertyType, p.Name))
.ToHashSet();
var itemExpr = Expression.Parameter(typeof(TSource));
var keyExpr = Expression.MemberInit(
Expression.New(typeof(TSource).GetConstructor(Type.EmptyTypes)),
from p in typeof(TSource).GetProperties()
where !exclude.Contains((p.PropertyType, p.Name))
select Expression.Bind(p, Expression.Property(itemExpr, p))
);
return Expression.Lambda<Func<TSource, TSource>>(keyExpr, itemExpr);
}
}
}
Then to use it you would do this:
sampleCollection.GroupByExcept(x => new { x.N2, x.N5 })...
But alas, this approach won't work under normal circumstances. You won't be able to create new instances of the type within a query (unless you're using Linq to Objects).
If you're using Roslyn, you could generate that type as needed, then use that object as your key. Though that'll mean you'll need to generate the type asynchronously. So you probably will want to separate this from your query all together and just generate the key selector.
public static async Task<Expression<Func<TSource, object>>> BuildExceptKeySelectorAsync<TSource, TXKey>(Expression<Func<TSource, TXKey>> exceptKeySelector)
{
var exclude = typeof(TXKey).GetProperties()
.Select(p => (p.PropertyType, p.Name))
.ToHashSet();
var properties =
(from p in typeof(TSource).GetProperties()
where !exclude.Contains((p.PropertyType, p.Name))
select p).ToList();
var targetType = await CreateTypeWithPropertiesAsync(
properties.Select(p => (p.PropertyType, p.Name))
);
var itemExpr = Expression.Parameter(typeof(TSource));
var keyExpr = Expression.New(
targetType.GetConstructors().Single(),
properties.Select(p => Expression.Property(itemExpr, p)),
targetType.GetProperties()
);
return Expression.Lambda<Func<TSource, object>>(keyExpr, itemExpr);
async Task<Type> CreateTypeWithPropertiesAsync(IEnumerable<(Type type, string name)> properties) =>
(await CSharpScript.EvaluateAsync<object>(
AnonymousObjectCreationExpression(
SeparatedList(
properties.Select(p =>
AnonymousObjectMemberDeclarator(
NameEquals(p.name),
DefaultExpression(ParseTypeName(p.type.FullName))
)
)
)
).ToFullString()
)).GetType();
}
To use this:
sampleCollection.GroupBy(
await BuildExceptKeySelector((CollectionType x) => new { x.N2, x.N5 })
).Select(....);
Borrowing from this answer here:
Create a class EqualityComparer
public class EqualityComparer<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
IDictionary<string, object> xP = x as IDictionary<string, object>;
IDictionary<string, object> yP = y as IDictionary<string, object>;
if (xP.Count != yP.Count)
return false;
if (xP.Keys.Except(yP.Keys).Any())
return false;
if (yP.Keys.Except(xP.Keys).Any())
return false;
foreach (var pair in xP)
if (pair.Value.Equals( yP[pair.Key])==false)
return false;
return true;
}
public int GetHashCode(T obj)
{
return obj.ToString().GetHashCode();
}
}
Then create your GroupContent method:
private void GroupContent<T>(List<T> dataList, string[] columns, string[] columnsToExclude)
{
string[] columnsToGroup = columns.Except(columnsToExclude).ToArray();
EqualityComparer<IDictionary<string, object>> equalityComparer = new EqualityComparer<IDictionary<string, object>>();
var groupedList = dataList.GroupBy(x =>
{
var groupByColumns = new System.Dynamic.ExpandoObject();
((IDictionary<string, object>)groupByColumns).Clear();
foreach (string column in columnsToGroup)
((IDictionary<string, object>)groupByColumns).Add(column, GetPropertyValue(x, column));
return groupByColumns;
}, equalityComparer);
foreach (var item in groupedList)
{
Console.WriteLine("Group : " + string.Join(",", item.Key));
foreach (object obj in item)
Console.WriteLine("Item : " + obj);
Console.WriteLine();
}
}
private static object GetPropertyValue(object obj, string propertyName)
{
return obj.GetType().GetProperty(propertyName).GetValue(obj, null);
}
I extended the code above borrowing another answer.
public static class IEnumerableExt {
public static IEnumerable<T> GroupBye<T, C>(this IEnumerable<T> query, Func<IGrouping<IDictionary<string, object>, T>, C> grouping) where T : class
{
var cProps = typeof(C).GetProperties().Select(prop => prop.Name).ToArray();
var columnsToGroup = typeof(T).GetProperties().Select(prop => prop.Name).Except(cProps).ToArray();
var equalityComparer = new EqualityComparer<IDictionary<string, object>>();
return query
.GroupBy(x => ExpandoGroupBy(x, columnsToGroup), equalityComparer)
.Select(x => MergeIntoNew(x, grouping, cProps));
}
private static IDictionary<string, object> ExpandoGroupBy<T>(T x, string[] columnsToGroup) where T : class
{
var groupByColumns = new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
groupByColumns.Clear();
foreach (string column in columnsToGroup)
groupByColumns.Add(column, typeof(T).GetProperty(column).GetValue(x, null));
return groupByColumns;
}
private static T MergeIntoNew<T, C>(IGrouping<IDictionary<string, object>, T> x, Func<IGrouping<IDictionary<string, object>, T>, C> grouping, string[] cProps) where T : class
{
var tCtor = typeof(T).GetConstructors().Single();
var tCtorParams = tCtor.GetParameters().Select(param => param.Name).ToArray();
//Calling grouping lambda function
var grouped = grouping(x);
var paramsValues = tCtorParams.Select(p => cProps.Contains(p) ? typeof(C).GetProperty(p).GetValue(grouped, null) : x.Key[p]).ToArray();
return (T)tCtor.Invoke(paramsValues);
}
private class EqualityComparer<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
var xDict = x as IDictionary<string, object>;
var yDict = y as IDictionary<string, object>;
if (xDict.Count != yDict.Count)
return false;
if (xDict.Keys.Except(yDict.Keys).Any())
return false;
if (yDict.Keys.Except(xDict.Keys).Any())
return false;
foreach (var pair in xDict)
if (pair.Value == null && yDict[pair.Key] == null)
continue;
else if (pair.Value == null || !pair.Value.Equals(yDict[pair.Key]))
return false;
return true;
}
public int GetHashCode(T obj)
{
return obj.ToString().GetHashCode();
}
}
}
Which can be used in the following way:
var list = enumerable.GroupBye(grp => new
{
Value = grp.Sum(val => val.Value)
});
The result will like grouping all other columns but Value, which will be valued to the sum of grouped elements' value

Linq OrderBy dependencies on other entities

I have collections of entities that I need to sort depending on their dependencies to each other. Here's an example because it's fairly tough to explain:
public class A : I {
private B objB;
public B propB { get{ return objB; } }
// Some other fields and properties.
}
public class B : I { /* Some fields and properties. */ }
public class C : I {
private A objA;
public A propA { get{ return objA; } }
// Some other fields and properties.
}
public interface I {}
The thing is that I need to import data into collections of those types but I need to import in a certain order because if I import A objects first, I will not be able to link the corresponding B since it won't exist yet.
So what I'd like is to sort my collections in a way that all dependencies are imported in the right order (There are no circular dependencies). I can't figure out a linq statement that will do that though.
lists.OrderBy(l => l. ??? );
I was thinking maybe get a list typesList of all the types T of the List<T> in lists and somehow use reflection to count how many fields in T are in typesList but that seems... inefficient ?
EDIT: Realized the wording of my structure is a bit vague.
Basically lists is a List<List<I>>. Here's a results example:
List<List<I>> collections before:
-List<A>
-List<C>
-List<B>
List<List<I>> collections after:
-List<B> // B has 0 dependency to B or C.
-List<A> // A has 1 dependency to B.
-List<C> // C has 1 dependency to A.
The easiest way I know to do it is build up a new collection, as you build it up see if you see any known types in the class. If you do see a known type put it it just before the first sighting or put it at the end if no known types were found. Once you have that collection just enumerate the collection in reverse order and it will the items in reverse dependency order.
The below example is used like
var sortedLists = lists.OrderByTypeDependency();
How to implement the LINQ style extension method:
static class ExtensionMethods
{
public static IEnumerable<T> OrderByTypeDependency<T>(this IEnumerable<T> items)
{
LinkedList<T> knownItems = new LinkedList<T>();
foreach (var item in items)
{
var itemType = item.GetType();
var itemPropertyTypes =
itemType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Select(x => x.PropertyType);
var itemFieldTypes =
itemType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Select(x => x.FieldType);
//Create a set of all types internal to type we are checking on.
HashSet<Type> itemChildTypes = new HashSet<Type>(itemPropertyTypes.Concat(itemFieldTypes));
bool found = false;
for (var knownItemNode = knownItems.First; knownItemNode != null; knownItemNode = knownItemNode.Next)
{
var knownItemType = knownItemNode.Value.GetType();
if (itemType == knownItemType || itemChildTypes.Contains(knownItemType))
{
knownItems.AddBefore(knownItemNode, item);
found = true;
break;
}
}
if (!found)
{
knownItems.AddLast(item);
}
}
//output the result in reverse order.
for (var knownItemNode = knownItems.Last; knownItemNode != null; knownItemNode = knownItemNode.Previous)
{
yield return knownItemNode.Value;
}
}
}
EDIT: it was not clear if you where passing in a list of types or a list of objects. If you are passing in a list of types it is only a few small tweaks to the code, just drop the two .GetType() calls and switch from generics to only accepting IEnumerable<Type>
static class ExtensionMethods
{
public static IEnumerable<Type> OrderByTypeDependency(this IEnumerable<Type> items)
{
LinkedList<Type> knownItems = new LinkedList<Type>();
foreach (var item in items)
{
var itemType = item;
var itemPropertyTypes =
itemType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Select(x => x.PropertyType);
var itemFieldTypes =
itemType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Select(x => x.FieldType);
//Create a set of all types internal to type we are checking on.
HashSet<Type> itemChildTypes = new HashSet<Type>(itemPropertyTypes.Concat(itemFieldTypes));
bool found = false;
for (var knownItemNode = knownItems.First; knownItemNode != null; knownItemNode = knownItemNode.Next)
{
var knownItemType = knownItemNode.Value;
if (itemType == knownItemType || itemChildTypes.Contains(knownItemType))
{
knownItems.AddBefore(knownItemNode, item);
found = true;
break;
}
}
if (!found)
{
knownItems.AddLast(item);
}
}
for (var knownItemNode = knownItems.Last; knownItemNode != null; knownItemNode = knownItemNode.Previous)
{
yield return knownItemNode.Value;
}
}
}
UPDATE:
Per your update to the question, as long as the inner lists hold the same type of object in the list so we can just check the first item in the list to find it's type this modification of the original code will do the sorting you need.
static class ExtensionMethods
{
public static IEnumerable<T> OrderByTypeDependency<T>(this IEnumerable<T> outerList)
where T: IList
{
LinkedList<T> knownItems = new LinkedList<T>();
foreach (var innerList in outerList)
{
if(innerList.Count == 0)
continue;
var itemType = innerList[0].GetType();
var itemPropertyTypes = itemType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Select(x => x.PropertyType);
var itemFieldTypes = itemType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Select(x => x.FieldType);
//Create a set of all types internal to type we are checking on.
HashSet<Type> itemChildTypes = new HashSet<Type>(itemPropertyTypes.Concat(itemFieldTypes));
bool found = false;
for (var knownItemNode = knownItems.First; knownItemNode != null; knownItemNode = knownItemNode.Next)
{
var knownItemType = knownItemNode.Value[0].GetType();
if (itemType == knownItemType || itemChildTypes.Contains(knownItemType))
{
knownItems.AddBefore(knownItemNode, innerList);
found = true;
break;
}
}
if (!found)
{
knownItems.AddLast(innerList);
}
}
for (var knownItemNode = knownItems.Last; knownItemNode != null; knownItemNode = knownItemNode.Previous)
{
yield return knownItemNode.Value;
}
}
}

Generic class to CSV (all properties)

Im looking for a way to create CSV from all class instances.
What i want is that i could export ANY class (all of its instances) to CSV.
Can some1 direct me to possible solution for this (in case already anwsered).
thanx !
Have a look at LINQ to CSV. Although it's a little on the heavy side, which is why I wrote the following code to perform just the small subset of functionality that I needed. It handles both properties and fields, like you asked for, although not much else. One thing it does do is properly escape the output in case it contains commas, quotes, or newline characters.
public static class CsvSerializer {
/// <summary>
/// Serialize objects to Comma Separated Value (CSV) format [1].
///
/// Rather than try to serialize arbitrarily complex types with this
/// function, it is better, given type A, to specify a new type, A'.
/// Have the constructor of A' accept an object of type A, then assign
/// the relevant values to appropriately named fields or properties on
/// the A' object.
///
/// [1] http://tools.ietf.org/html/rfc4180
/// </summary>
public static void Serialize<T>(TextWriter output, IEnumerable<T> objects) {
var fields =
from mi in typeof (T).GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
where new [] { MemberTypes.Field, MemberTypes.Property }.Contains(mi.MemberType)
let orderAttr = (ColumnOrderAttribute) Attribute.GetCustomAttribute(mi, typeof (ColumnOrderAttribute))
orderby orderAttr == null ? int.MaxValue : orderAttr.Order, mi.Name
select mi;
output.WriteLine(QuoteRecord(fields.Select(f => f.Name)));
foreach (var record in objects) {
output.WriteLine(QuoteRecord(FormatObject(fields, record)));
}
}
static IEnumerable<string> FormatObject<T>(IEnumerable<MemberInfo> fields, T record) {
foreach (var field in fields) {
if (field is FieldInfo) {
var fi = (FieldInfo) field;
yield return Convert.ToString(fi.GetValue(record));
} else if (field is PropertyInfo) {
var pi = (PropertyInfo) field;
yield return Convert.ToString(pi.GetValue(record, null));
} else {
throw new Exception("Unhandled case.");
}
}
}
const string CsvSeparator = ",";
static string QuoteRecord(IEnumerable<string> record) {
return String.Join(CsvSeparator, record.Select(field => QuoteField(field)).ToArray());
}
static string QuoteField(string field) {
if (String.IsNullOrEmpty(field)) {
return "\"\"";
} else if (field.Contains(CsvSeparator) || field.Contains("\"") || field.Contains("\r") || field.Contains("\n")) {
return String.Format("\"{0}\"", field.Replace("\"", "\"\""));
} else {
return field;
}
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ColumnOrderAttribute : Attribute {
public int Order { get; private set; }
public ColumnOrderAttribute(int order) { Order = order; }
}
}
Actually, something similar has been addressed here:
Best practices for serializing objects to a custom string format for use in an output file
Is this useful to you?
There is a sample that uses reflection to pull out the field names and values and append them to a string.
You can use reflection to traverse all the class properties/fields and write them to CSV.
A better approach would be to define a custom attribute and decorate the members you want to export and only export those attributes.
I am separating my answer into two sections:
The first one is how to export some generic item list into csv, with encoding, headers - (it will build csv data only for specified headers, and will ignore unneeded properties).
public string ExportCsv<T>(IEnumerable<T> items, Dictionary<string, string> headers)
{
string result;
using (TextWriter textWriter = new StreamWriter(myStream, myEncoding))
{
result = this.WriteDataAsCsvWriter<T>(items, textWriter, headers);
}
return result;
}
private string WriteDataAsCsvWriter<T>(IEnumerable<T> items, TextWriter textWriter, Dictionary<string, string> headers)
{
//Add null validation
////print the columns headers
StringBuilder sb = new StringBuilder();
//Headers
foreach (KeyValuePair<string, string> kvp in headers)
{
sb.Append(ToCsv(kvp.Value));
sb.Append(",");
}
sb.Remove(sb.Length - 1, 1);//the last ','
sb.Append(Environment.NewLine);
//the values
foreach (var item in items)
{
try
{
Dictionary<string, string> values = GetPropertiesValues(item, headers);
foreach (var value in values)
{
sb.Append(ToCsv(value.Value));
sb.Append(",");
}
sb.Remove(sb.Length - 1, 1);//the last ','
sb.Append(Environment.NewLine);
}
catch (Exception e1)
{
//do something
}
}
textWriter.Write(sb.ToString());
return sb.ToString();
}
//Help function that encode text to csv:
public static string ToCsv(string input)
{
if (input != null)
{
input = input.Replace("\r\n", string.Empty)
.Replace("\r", string.Empty)
.Replace("\n", string.Empty);
if (input.Contains("\""))
{
input = input.Replace("\"", "\"\"");
}
input = "\"" + input + "\"";
}
return input;
}
This is the most important function, Its extracting the properties values out of (almost) any generic class.
private Dictionary<string, string> GetPropertiesValues(object item, Dictionary<string, string> headers)
{
Dictionary<string, string> values = new Dictionary<string, string>();
if (item == null)
{
return values;
}
//We need to make sure each value is coordinated with the headers, empty string
foreach (var key in headers.Keys)
{
values[key] = String.Empty;
}
Type t = item.GetType();
PropertyInfo[] propertiesInfo = t.GetProperties();
foreach (PropertyInfo propertiyInfo in propertiesInfo)
{
//it not complex: string, int, bool, Enum
if ((propertiyInfo.PropertyType.Module.ScopeName == "CommonLanguageRuntimeLibrary") || propertiyInfo.PropertyType.IsEnum)
{
if (headers.ContainsKey(propertiyInfo.Name))
{
var value = propertiyInfo.GetValue(item, null);
if (value != null)
{
values[propertiyInfo.Name] = value.ToString();
}
}
}
else//It's complex property
{
if (propertiyInfo.GetIndexParameters().Length == 0)
{
Dictionary<string, string> lst = GetPropertiesValues(propertiyInfo.GetValue(item, null), headers);
foreach (var value in lst)
{
if (!string.IsNullOrEmpty(value.Value))
{
values[value.Key] = value.Value;
}
}
}
}
}
return values;
}
Example for GetPropertiesValues:
public MyClass
{
public string Name {get; set;}
public MyEnum Type {get; set;}
public MyClass2 Child {get; set;}
}
public MyClass2
{
public int Age {get; set;}
public DateTime MyDate {get; set;}
}
MyClass myClass = new MyClass()
{
Name = "Bruce",
Type = MyEnum.Sometype,
Child = new MyClass2()
{
Age = 18,
MyDate = DateTime.Now()
}
};
Dictionary<string, string> headers = new Dictionary<string, string>();
headers.Add("Name", "CustomCaption_Name");
headers.Add("Type", "CustomCaption_Type");
headers.Add("Age", "CustomCaption_Age");
GetPropertiesValues(myClass, headers)); // OUTPUT: {{"Name","Bruce"},{"Type","Sometype"},{"Age","18"}}
My answer is based on Michael Kropat's answer from above.
I added two functions to his answer because it didn't want to write straight to file as I still had some further processing to do. Instead I wanted the header information separate to the values so I could put everything back together later.
public static string ToCsvString<T>(T obj)
{
var fields =
from mi in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
where new[] { MemberTypes.Field, MemberTypes.Property }.Contains(mi.MemberType)
let orderAttr = (ColumnOrderAttribute)Attribute.GetCustomAttribute(mi, typeof(ColumnOrderAttribute))
select mi;
return QuoteRecord(FormatObject(fields, obj));
}
public static string GetCsvHeader<T>(T obj)
{
var fields =
from mi in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
where new[] { MemberTypes.Field, MemberTypes.Property }.Contains(mi.MemberType)
let orderAttr = (ColumnOrderAttribute)Attribute.GetCustomAttribute(mi, typeof(ColumnOrderAttribute))
select mi;
return QuoteRecord(fields.Select(f => f.Name));
}

Categories