Have put together the code below to read a particular set of CSV files. It works but is very much a work in progress. There is one section of the code (populating datatable row - see snip below) that is taking as long to run as the SqlBulkCopy operation. Asking for advice/recommendations on how to improve performance.
In the code (below) processing a ~15M row file in 50K batches took just under 11.5 minutes. Breaking down the sections. SqlBulkCopy took ~236Kms (4 min) the reader only needed 105Kms (~1.5min), and the section populating the datatable took ~200Kms (3.33 min).
csvTableTimer.Start();
// Process row and populate datatable
DataRow dr = dt.NewRow();
foreach (DataColumn dc in dt.Columns)
{
if (row.GetType().GetProperty(dc.ToString()).GetValue(row) != null)
{
dr[dc.ToString()] = row.GetType().GetProperty(dc.ToString()).GetValue(row);
}
}
dt.Rows.Add(dr);
csvTableTimer.Stop();
The CSV files are very large (10+GB) and do not have headers. I'm using the Class to build the datatable structure and like to continue with that approach when populating the datatable rows as I'll need to expand this to work with multiple CSV types.
The datatable reflects the column names from the class which line up with the SQL DB table. Had wanted to use GetField (converted, not raw) walking each column in the datatable row[column.ColumnName] = csv.GetField( column.DataType, column.ColumnName ); but kept getting an error about there not being headers. Found an open issue relating to HasHeaderRecord = false that matches up with what I was trying to do so that added to my desire to seek advice from those who are more skilled at this. Appreciate the help!
Expanding on the code block;
var rconfig = new CsvHelper.Configuration.CsvConfiguration(CultureInfo.InvariantCulture)
{
BufferSize = 1024,
Delimiter = ",",
AllowComments = true,
HasHeaderRecord = false,
HeaderValidated = null,
IgnoreBlankLines = true,
MissingFieldFound = null,
Comment = '#',
Escape = '"',
TrimOptions = TrimOptions.Trim,
BadDataFound = x =>
{
isBadRecord = true;
ErrRecords.Add(x.RawRecord);
++badCount;
}
};
var loadFType = #"B";
// Create datatable using class as definition.
PropertyDescriptorCollection props1 = TypeDescriptor.GetProperties(loaderFileType);
DataTable dt = new DataTable();
dt = UtilExtensions.CreateDataTable(props1);
using (var reader = new StreamReader(rFile))
{
reader.ReadLine();
using (var csv = new CsvReader(reader, rconfig))
{
switch (loadFType)
{
case "ALL":
csv.Context.RegisterClassMap<CSVLoader.AMap>();
var allRecords = new List<CSVLoader.A>();
break;
case "BAL":
csv.Context.RegisterClassMap<CSVLoader.BMap>();
var balRecords = new List<CSVLoader.B>();
break;
case "CIF":
csv.Context.RegisterClassMap<CSVLoader.CMap>();
var cifRecords = new List<CSVLoader.C>();
break;
}
dt.BeginLoadData();
while (csv.Read())
{
csvReadTimer.Start();
var row = csv.GetRecord(loaderFileType);
csvReadTimer.Stop();
runningCount++;
if (!isBadRecord)
{
csvTableTimer.Start();
// Process row and populate datatable
DataRow dr = dt.NewRow();
foreach (DataColumn dc in dt.Columns)
{
if (row.GetType().GetProperty(dc.ToString()).GetValue(row) != null)
{
dr[dc.ToString()] = row.GetType().GetProperty(dc.ToString()).GetValue(row);
}
}
dt.Rows.Add(dr);
csvTableTimer.Stop();
++goodCount;
if (batchCount >= dtbatchSize || runningCount >= fileRecCount)
{
try
{
// Write from the source to the destination.
bcpLoadTimer.Start();
bulkCopy.WriteToServer(dt);
bcpLoadTimer.Stop();
bcpLoadBatchCount++;
}
catch (Exception ex)
{
}
dt.Clear();
batchCount = 0;
}
batchCount++;
}
isBadRecord = false;
}
dt.EndLoadData();
reader.Close();
dt.Clear();
transaction.Commit();
// B
public class B
{
[Index(0)]
public string A { get; set; }
[Index(1)]
public string BString { get; set; }
[Index(2)]
public int? C { get; set; }
[Index(3)]
public string D { get; set; }
[Index(4)]
public string E { get; set; }
[Index(5)]
public DateTime? F { get; set; }
[Index(6)]
public decimal? G { get; set; }
[Index(7)]
public decimal? H { get; set; }
[Index(8)]
public decimal? I { get; set; }
[Index(9)]
public decimal? J { get; set; }
[Index(10)]
public int? K { get; set; }
[Index(11)]
public string L { get; set; }
[Index(12)]
public DateTime? M { get; set; }
}
// B
public sealed class BMap : ClassMap<B>
{
public BMap()
{
// AutoMap(CultureInfo.InvariantCulture);
Map(m => m.A).Index(0);
Map(m => m.BString).Index(1);
Map(m => m.C).Index(2);
Map(m => m.D).Index(3);
Map(m => m.E).Index(4);
Map(m => m.F).Index(5).TypeConverterOption.Format("yyyyMMdd");
Map(m => m.G).Index(6);
Map(m => m.H).Index(7);
Map(m => m.I).Index(8);
Map(m => m.J).Index(9);
Map(m => m.K).Index(10);
Map(m => m.L).Index(11);
Map(m => m.M).Index(12).TypeConverterOption.Format("yyyy-MM-dd-hh.mm.ss.ffffff");
}
}
Your question doesn't really include a minimal reproducible example, so I simplified your code to create the following FileLoader class that times how long it takes to populate the DataTable from instances of some class TClass (here B) that had been read from a CSV row using CsvReader:
public class FileLoader
{
public System.Diagnostics.Stopwatch csvTableTimer { get; } = new();
public long Load<TClass, TClassMap>(string rFile, int dtbatchSize) where TClassMap : ClassMap<TClass>, new()
{
bool isBadRecord = false;
long badCount = 0;
long runningCount = 0;
long goodCount = 0;
long batchCount = 0;
var rconfig = CreateCsvConfiguration(
x =>
{
isBadRecord = true;
//ErrRecords.Add(x.RawRecord);
++badCount;
});
// Create datatable using class as definition.
var dt = UtilExtensions.CreateDataTable(typeof(TClass));
using (var reader = new StreamReader(rFile))
{
//reader.ReadLine(); FIXED - THIS SKIPPED THE FIRST LINE AND CAUSED A RECORD TO BE OMITTED.
using (var csv = new CsvReader(reader, rconfig))
{
csv.Context.RegisterClassMap<TClassMap>();
dt.BeginLoadData();
while (csv.Read())
{
isBadRecord = false;
//csvReadTimer.Start();
var record = csv.GetRecord<TClass>();
//csvReadTimer.Stop();
runningCount++;
if (!isBadRecord)
{
csvTableTimer.Start();
// Process row and populate datatable
DataRow dr = dt.NewRow();
foreach (DataColumn dc in dt.Columns)
{
if (record.GetType().GetProperty(dc.ToString()).GetValue(record) != null)
{
dr[dc.ToString()] = record.GetType().GetProperty(dc.ToString()).GetValue(record);
}
}
dt.Rows.Add(dr);
csvTableTimer.Stop();
goodCount++;
if (++batchCount >= dtbatchSize)
{
// Flush the data table
FlushTable(dt);
batchCount = 0;
}
}
}
dt.EndLoadData();
FlushTable(dt);
Commit();
}
}
return goodCount;
}
protected virtual void FlushTable(DataTable dt) => dt.Clear(); // Replace with SqlBulkCopy
protected virtual void Commit() {} // Replace with transaction.Commit();
public static CsvConfiguration CreateCsvConfiguration(BadDataFound badDataFound) =>
new CsvHelper.Configuration.CsvConfiguration(CultureInfo.InvariantCulture)
{
BufferSize = 1024,
Delimiter = ",",
AllowComments = true,
HasHeaderRecord = false,
HeaderValidated = null,
IgnoreBlankLines = true,
MissingFieldFound = null,
Comment = '#',
Escape = '"',
TrimOptions = TrimOptions.Trim,
BadDataFound = badDataFound,
};
}
public static partial class UtilExtensions
{
static IEnumerable<PropertyInfo> GetSerializableProperties(this Type type) =>
type.GetProperties().Where(p => p.GetIndexParameters().Length == 0 && p.CanRead && p.CanWrite && p.GetGetMethod() != null && p.GetSetMethod() != null);
public static DataTable CreateDataTable(Type type)
{
var dt = new DataTable();
foreach (var p in type.GetSerializableProperties())
dt.Columns.Add(p.Name, Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType);
return dt;
}
}
Then, if I use the file loader and call loader.Load<B, BMap>(rFile, 1000) to read a CSV file with 5555 rows 20 times, it takes roughly 1049 ms on dotnetfiddle. See demo #1 here.
One problem you are encountering is that reflection in c# can be very slow. You are calling record.GetType().GetProperty(dc.ToString()).GetValue(record) twice, and if I simply reduce the number of calls by 1, the time is reduced to around 706 ms:
foreach (DataColumn dc in dt.Columns)
{
var value = record.GetType().GetProperty(dc.ToString()).GetValue(record);
if (value != null)
{
dr[dc.ToString()] = value;
}
}
Demo #2 here.
However, we can do better by manufacturing a delegate in runtime. First, add the following utility methods that make use of the System.Linq.Expressions namespace:
public static partial class UtilExtensions
{
public static Func<TSource, object> CreatePropertyGetter<TSource>(PropertyInfo propertyInfo)
{
var parameter = Expression.Parameter(typeof(TSource), "obj");
var property = Expression.Property(parameter, propertyInfo);
var convert = Expression.Convert(property, typeof(object));
var lambda = Expression.Lambda(typeof(Func<TSource, object>), convert, parameter);
return (Func<TSource, object>)lambda.Compile();
}
public static ReadOnlyDictionary<string, Func<TSource, object>> PropertyGetters<TSource>() => PropertyExpressionsCache<TSource>.PropertyGetters;
static ReadOnlyDictionary<string, Func<TSource, object>> CreatePropertyGetters<TSource>() =>
typeof(TSource)
.GetSerializableProperties()
.ToDictionary(p => p.Name,
p => CreatePropertyGetter<TSource>(p))
.ToReadOnly();
static class PropertyExpressionsCache<TSource>
{
public static ReadOnlyDictionary<string, Func<TSource, object>> PropertyGetters { get; } = UtilExtensions.CreatePropertyGetters<TSource>();
}
public static ReadOnlyDictionary<TKey, TValue> ToReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) =>
new ReadOnlyDictionary<TKey, TValue>(dictionary ?? throw new ArgumentNullException());
}
And modify Load<TClass, TClassMap>() as follows:
public long Load<TClass, TClassMap>(string rFile, int dtbatchSize) where TClassMap : ClassMap<TClass>, new()
{
bool isBadRecord = false;
long badCount = 0;
long runningCount = 0;
long goodCount = 0;
long batchCount = 0;
var rconfig = CreateCsvConfiguration(
x =>
{
isBadRecord = true;
//ErrRecords.Add(x.RawRecord);
++badCount;
});
var loaderFileType = typeof(TClass);
// Create datatable using class as definition.
var dt = UtilExtensions.CreateDataTable(loaderFileType);
var properties = UtilExtensions.PropertyGetters<TClass>();
using (var reader = new StreamReader(rFile))
{
//reader.ReadLine(); FIXED - THIS SKIPPED THE FIRST LINE AND CAUSED A RECORD TO BE OMITTED.
using (var csv = new CsvReader(reader, rconfig))
{
csv.Context.RegisterClassMap<TClassMap>();
dt.BeginLoadData();
while (csv.Read())
{
isBadRecord = false;
//csvReadTimer.Start();
var record = csv.GetRecord<TClass>();
//csvReadTimer.Stop();
runningCount++;
if (!isBadRecord)
{
csvTableTimer.Start();
// Process row and populate datatable
DataRow dr = dt.NewRow();
foreach (var p in properties)
{
var value = p.Value(record);
if (value != null)
dr[p.Key] = value;
}
dt.Rows.Add(dr);
csvTableTimer.Stop();
goodCount++;
if (++batchCount >= dtbatchSize)
{
// Flush the data table
FlushTable(dt);
batchCount = 0;
}
}
}
dt.EndLoadData();
FlushTable(dt);
}
}
return goodCount;
}
The time will be further reduced, to roughly 404 ms. Demo fiddle #3 here.
I also tried using Delegate.CreateDelegate() instead of Expression:
public static partial class UtilExtensions
{
static Func<TSource, object> CreateTypedPropertyGetter<TSource, TValue>(PropertyInfo propertyInfo)
{
var typedFunc = (Func<TSource, TValue>)Delegate.CreateDelegate(typeof(Func<TSource, TValue>), propertyInfo.GetGetMethod());
return i => (object)typedFunc(i);
}
public static Func<TSource, object> CreatePropertyGetter<TSource>(PropertyInfo propertyInfo)
{
var typedCreator = typeof(UtilExtensions).GetMethod(nameof(CreateTypedPropertyGetter), BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
var concreteTypedCreator = typedCreator = typedCreator.MakeGenericMethod(typeof(TSource), propertyInfo.PropertyType);
return (Func<TSource, object>)concreteTypedCreator.Invoke(null, new object [] { propertyInfo });
}
public static ReadOnlyDictionary<string, Func<TSource, object>> PropertyGetters<TSource>() => PropertyExpressionsCache<TSource>.PropertyGetters;
static ReadOnlyDictionary<string, Func<TSource, object>> CreatePropertyGetters<TSource>() =>
typeof(TSource)
.GetSerializableProperties()
.ToDictionary(p => p.Name,
p => CreatePropertyGetter<TSource>(p))
.ToReadOnly();
static class PropertyExpressionsCache<TSource>
{
public static ReadOnlyDictionary<string, Func<TSource, object>> PropertyGetters { get; } = UtilExtensions.CreatePropertyGetters<TSource>();
}
public static ReadOnlyDictionary<TKey, TValue> ToReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) =>
new ReadOnlyDictionary<TKey, TValue>(dictionary ?? throw new ArgumentNullException());
}
And got roughly the same time, of 410 ms. Demo fiddle #4 here.
Notes:
The code in your question skips the first line of the CSV file, by calling reader.ReadLine();.
In my test harness this caused an incorrect number of records to be read, so I removed this line.
Rather than having a non-generic method that has a switch on the record type, I extracted a generic method that takes the record type and class map type as generic parameters. This makes delegate creation a little easier as it is no longer necessary to do runtime casting to the record type.
// i have this class
public class SCHOOL
{
public int ID { get; set; }
public string Name { get; set; }
public string Country{ get; set; }
public decimal Total{ get; set; }
}
// and another class with different type
public class CLASS_2
{
public string Student { get; set; }
public DateTime Graduate { get; set; }
}
// and may bee i add class 3 and 4 with different type
// i fill the lists with my data
static void Main()
{
List < SCHOOL> FirstClass = new List < SCHOOL>();
FirstClass.Add( new SCHOOL{ID=1,Name="aaa",County="USA",Total=10});
FirstClass.Add( new SCHOOL{ID=1,Name="bbb",County="JAP",Total=7});
FirstClass.Add( new SCHOOL{ID=1,Name="ccc",County="GBR",Total=5});
List < CLASS_2 > SecondClass = new List < CLASS_2 >();
SecondClass.Add( new CLASS_2 {Student =1, Graduate ="2/6/2015"});
SecondClass.Add( new CLASS_2 {Student =1, Graduate ="2/4/2015"});
SecondClass.Add( new CLASS_2 {Student =1, Graduate ="2/8/2015"});
}
// i want to pass the first List and loop through my data
GetmyData ( firstClass);
// and also pass another List with different Class type to the same method and also loop through my data
GetmyData ( SenecdClass );
// i want one method to get the list and loop throught the data like array
private void GetmyData <T> ( List<T> newlist )
{
for (int y=0; y < newList.Count; y++)
{
for ( int x=0 ; x < newLsit[y].Colms; x++ )
{
Console.WriteLine ( newList[y][x].value );
}
}
}
I would say that firstly you need to use a generic method:
private void GetMyData(List<T> the List)
{
foreach (T entry in theList)
{
//deal with the entry on the list
}
}
and for the printing every property of the class, the best way in my eyes would be to override ToString
but if you need to access each property for something other than just displaying it will require reflection:
private void GetMyData(List<T> the List)
{
foreach (T entry in theList)
{
foreach (var property in typeof(T).GetProperties())
{
var propertyName = property.Name;
var propertyValue = property.GetValue(entry);
Console.WriteLine("Value of " + propertyName + " is " + propertyValue);
}
}
}
but bear in mind that not all properties can be read.
static void GetMyData(List theList)
{
int count = typeof(T).GetProperties().Count();
for ( int y = 0 ; y < theList.Count ; y++ )
{
for ( int x = 0; x < count; x++ )
{
var propertyName = typeof(T).GetProperties()[x].Name;
var propertyValue = typeof(T).GetProperties()[x].GetValue( theList[y] , null );
var propertyType = typeof(T).GetProperties()[x].PropertyType;
Console.WriteLine(propertyType + " " + propertyName + " " + propertyValue );
}
}
}
In the first inner loop I was able to pass the correct target object when getting the value from PropertyInfo[], however in the second inner loop it gives an exception that the target object is not correct.
So what I want is to get all of the values of a property with inner properties of this listProperties[j], how do I pass correctly the target object to get all of those values?
Model Data:
public class Foo
{
public string Name { get; set; }
public Bar BarProp { get; set; }
}
public class Bar
{
public string Name { get; set; }
public decimal Value { get; set; }
}
Method:
private void CreateDataRow(ISheet sheet, IList iList)
{
for (int i = 0; i < iList.Count; i++)
{
var listType = iList[i].GetType();
var listProperties = listType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
for (int j = 0; j < listProperties.Count(); j++)
{
int columnCount = j;
if (IsPrimitiveType(listProperties[j].PropertyType))
{
var columnData = listProperties[j].GetValue(iList[i], null) ?? "-";
var dataTypeValue = Convert.ChangeType(columnData, listProperties[j].PropertyType);
//omitted codes
continue;
}
var innerProperties = listProperties[j].PropertyType.GetProperties().ToArray();
for (int k = 0; k < innerProperties.Count(); k++)
{
//this throws an exception
var columnData = innerProperties[k].GetValue(listProperties[j], null) ?? "-";
//omitted codes
}
}
}
}
CreateDataRow is being called here:
private XSSFWorkbook CreateWorkbook(T model)
{
var workbook = new XSSFWorkbook();
var type = model.GetType();
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in properties)
{
//check if the property is a list
if (property.PropertyType.IsGenericType &&
typeof(List<>).IsAssignableFrom(property.PropertyType.GetGenericTypeDefinition()))
{
var propertyValueList = (IList)property.GetValue(model, null);
if (propertyValueList != null && propertyValueList.Count > 0)
{
//create data row
CreateDataRow(sheet, propertyValueList);
}
}
}
return workbook;
}
The T model uses this Model:
public class ExportModel
{
public List<Foo> FooList { get; set; }
}
in this line of code:
var innerProperties = listProperties[j].PropertyType.GetProperties().ToArray();
You are getting properties of listProperties[j].PropertyType, but in GetValue:
var columnData = innerProperties[k].GetValue(listProperties[j], null) ?? "-";
You are sending listProperties[j] as instance argument. Correct one of the following lines:
var innerProperties = listProperties[j].GetProperties().ToArray();
//or
var columnData = innerProperties[k].GetValue(listProperties[j].PropertyType, null) ?? "-";
Different is between object instance and its type. The PropertyType represents types of retrieved Property and acts like:
propertyInfo.GetValue(x,y).GetType();
You should send exact instance of the target object not its type and not type of retrieved property. and if you want to get value of one of properties of property write:
var columnData = innerProperties[k].GetValue(listProperties[j].GetValue(iList[i],null) ,null) ?? "-";
How can i do to get all attributes of this class
public class Data
{
public Person;
public List<Nationality> nationality;
public List<Passport> passport;
public List<Family> family;
public List<WK_Permit> Permit;
public List<Role> role;
}
the expected output is for example
i wan to get something like that "DATA.PERSON.NAME" or "DATA.PASSPORT.NUMBER" or "DATA.ROLES.HOST"...etc so i have to get the class name and the attibuts name
I did this but it doesn't work for class's list
Type parent = typeof(VSM_Data);
FieldInfo[] children = parent.GetFields();
for (int i = 0; i < children.Length; i++)
{
Type child = children[i].FieldType;
var columnnamesChild = from t in child.GetProperties() select t.Name;
foreach (var item in columnnamesChild)
{
DragAndDrop FundDragAndDrop = new DragAndDrop();
FundDragAndDrop.TITLE = item;
FundDragAndDrop.adresse = "{{PERSON." + children[i].Name.ToUpper() + "." + item.ToUpper() + "}}";
FundList.Add(FundDragAndDrop);
}
you can do it using Reflection:
Reflection; for an instance:
obj.GetType().GetProperties();
obj.GetType().GetMembers();
for a type:
typeof(Foo).GetProperties();
typeof(Foo).GetMembers();
for example:
class Foo {
public int A {get;set;}
public string B {get;set;}
public string Member1;
public int Member2;
}
...
For Fields:
foreach(var prop in foo.GetType().GetFields()) {
Console.WriteLine("{0}={1}", prop.Name);
}
For Properties:
Foo foo = new Foo {A = 1, B = "abc"};
foreach(var prop in foo.GetType().GetProperties()) {
Console.WriteLine("{0}={1}", prop.Name, prop.GetValue(foo, null));
}
for Members:
foreach(var prop in foo.GetType().GetMembers()) {
Console.WriteLine("{0}", prop.Name);
}
This code will give you all public property name
Type type = typeof(Data);
foreach (var property in type.GetProperties())
string name = property.Name;
static void Main(string[] args)
{
Data data = new Data();
//you can use this for your case
var fields = data.GetType().GetFields().Select(f => f.Name);
foreach (var f in fields)
{
Console.WriteLine(f);
}
Console.WriteLine("===");
//but if you want a more generic one try this
var members = data.GetType().GetMembers().Select(m=>m.Name);
foreach (var member in members)
{
Console.WriteLine(member);
}
}
It ok i find the solution sometime the best way to find the solution is to ask the question and think by your self ;-)
Type type = typeof(VSM_Data);
foreach (var property in type.GetFields())
{
FieldInfo[] children = type.GetFields();
for (int i = 0; i < children.Length; i++)
{
Type child = property.FieldType;
var columnnamesChild = from t in child.GetFields() select t.Name;
foreach (var item in columnnamesChild)
{
DragAndDrop FundDragAndDrop = new DragAndDrop();
FundDragAndDrop.TITLE = item;
FundDragAndDrop.adresse = "{{PERSON." + children[i].Name.ToUpper() + "." + item.ToUpper() + "}}";
FundList.Add(FundDragAndDrop);
}
}
}
The problem is to generate combinations of search parameters to be used as test case inputs in automation tests.
public class CombinationInput<T>
{
public string Name { get; set; }
public List<T> PossibleValues { get; set; }
public bool ReadOnly { get; set; }
}
GetCombinations is a method in the Combinationsgenerator Class:
private IEnumerable<object[]> _GetCombinations(ArrayList inputs)
{
var returnobjects = new object[inputs.Count];
var element = inputs[0];
var type = element.GetType();
var generictype = type.GenericTypeArguments[0];
var elementvalues = type.GetProperty("PossibleValues").GetValue(element, null) as IEnumerable;
foreach (var val in elementvalues)
{
returnobjects[0] = val;
if (inputs.Count > 1)
{
var objs = _GetCombinations(inputs.GetRange(1, inputs.Count - 1));
foreach (var item in objs)
{
for (int i = 0; i < item.Length; i++)
{
returnobjects[i + 1] = item[i];
}
yield return returnobjects;
}
}
else
{
yield return returnobjects;
}
}
}
Inside TestMethod:
[TestMethod]
public void GetCombinationTest()
{
var x = new CombinationInput<Enums.AmountType>();
var y = new CombinationInput<Enums.AreaMeasureType>();
var z = new CombinationInput<Enums.DisplayItemType>();
ArrayList list = new ArrayList();
list.Add(x);
list.Add(y);
list.Add(z);
var combgen = new CombinationGenerator(list);
var combs = combgen.GetCombinations();
}
Get Combinations is always returning the same combination when I run the code but when I debug through the code each time it hits the yield return statement its having the right combination.
You always return the same object as you are editing the content itself. What you are yielding is the reference to an array, and it's always the same reference. Instead of reusing the same array, you should move the declaration and creation of returnobjects within the foreach loop.