Error exporting to CSV when there are reference maps - c#

I have s Student class where each student record has a list of Results.
I need to export there results to CSV and I'm using CsvHelper.
public class Student
{
public string Id { get; set; }
public string Name { get; set; }
public Result[] Grades { get; set; }
}
public class Result
{
public string Subject { get; set; }
public decimal? Marks { get; set; }
}
I'm using Reference Maps to map the list of Results, but when exporting to CSV it throws and error.
Mapping Code
public sealed class StudentResultExportMap : ClassMap<Student>
{
public StudentResultExportMap ()
{
AutoMap();
References<GradesMap>(m => m.Grades);
}
}
public sealed class GradesMap: ClassMap<Result>
{
public GradesMap()
{
Map(m => m.Subject);
Map(m => m.Marks);
}
}
Error
Property 'System.String Subject' is not defined for type
'{namespace}.GetStudentResults+Result[]' Parameter name: property

Unfortunately References<GradesMap>(m => m.Grades); doesn't work for an array of Result. It would work for an individual result. I have one solution, which overrides the ToString() method of Result to flatten the grades. It might work for you, depending on what you need.
public class Result
{
public string Subject { get; set; }
public decimal? Marks { get; set; }
public override string ToString()
{
return $"{Subject} = {Marks}";
}
}
Make a slight change to your StudentResultExportMap. You can set the 2nd number on .Index(2, 7) to handle the max number of grades you think a student might have.
public sealed class StudentResultExportMap : ClassMap<Student>
{
public StudentResultExportMap()
{
AutoMap();
Map(m => m.Grades).Name("Grade").Index(2, 7);
}
}
You will then get Id, Name, Grade1, Grade2, Grade3, Grade4, Grade5, Grade6 with the toString() value of Result for each grade.
var records = new List<Student>
{
new Student{ Id = "1", Name = "First", Grades = new [] {
new Result { Subject = "Subject1", Marks = (decimal)2.5 } ,
new Result { Subject = "Subject2", Marks = (decimal)3.5 } }},
new Student{ Id = "2", Name = "Second", Grades = new [] {
new Result { Subject = "Subject1", Marks = (decimal)3.5 } ,
new Result { Subject = "Subject2", Marks = (decimal)4.0 } }}
};
using (var writer = new StreamWriter("path\\to\\StudentResults.csv"))
using (var csv = new CsvWriter(writer))
{
csv.Configuration.RegisterClassMap<StudentResultExportMap>();
csv.WriteRecords(records);
}

Related

Fit multiple Objects in one Row with CSVHelper in C#

i am trying to write two different Objects in one row with the C# library CSVHelper.
It should look something like this:
obj1 obj2
-----------|------------
record1 record1
record2 record2
When register the class maps for these two objects and then call WriteRecords(List) and WriteRecords(List) these objects are written but they are not in the same row. Instead the records of obj2 are written in the rows following the records of obj1.
It looks like this:
obj1
----------
record1
record2
obj2
----------
record1
record2
Program.cs:
string fileReadDirectory =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "Stuecklisten");
string fileWriteDirectory =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "Stueckliste.csv");
List<string> files = Directory.GetFiles(fileReadDirectory).ToList();
List<Part> parts = new List<Part>();
List<PartsPerList> partsPerLists = new List<PartsPerList>();
foreach (string file in files)
{
//Reads records from Excel File
CsvReader reader = new CsvReader(new ExcelParser(file));
reader.Context.RegisterClassMap<ExcelSheetMap>();
IEnumerable<Part>? excelRecords = reader.GetRecords<Part>();
foreach (var record in excelRecords)
{
PartsPerList partsPerList = new PartsPerList();
partsPerList.Listname = file;
if (parts.Any(p => p.ManufacturerNr == record.ManufacturerNr))
{
Part part = parts.SingleOrDefault(p => p.ManufacturerNr == record.ManufacturerNr) ?? new Part();
part.TotalQuantity += record.TotalQuantity;
}
else
{
parts.Add(record);
}
partsPerLists.Add(partsPerList);
}
}
using (var stream = File.Open(fileWriteDirectory, FileMode.Create))
using (var streamWriter = new StreamWriter(stream))
using (var writer = new CsvWriter(streamWriter,CultureInfo.InvariantCulture))
{
writer.Context.RegisterClassMap<ExcelSheetMap>();
writer.Context.RegisterClassMap<ManufacturerPartsMap>();
writer.WriteHeader(typeof(Part));
writer.WriteRecords(parts);
writer.WriteHeader(typeof(PartsPerList));
writer.WriteRecords(partsPerLists);
}
Part.cs:
public class Part
{
// public int Quantity { get; set; }
public int TotalQuantity { get; set; }
public string Description { get; set; } = string.Empty;
public string Designator { get; set; } = string.Empty;
public string Case { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string Tolerance { get; set; } = string.Empty;
public string Remark { get; set; } = string.Empty;
public string PartNumber { get; set; } = string.Empty;
public string Manufacturer { get; set; } = string.Empty;
public string ManufacturerNr { get; set; } = string.Empty;
public string RoHS { get; set; } = string.Empty;
public string Nachweis { get; set; } = string.Empty;
}
Part Classmap:
public sealed class ExcelSheetMap : ClassMap<Part>
{
public ExcelSheetMap()
{
// Map(m => m.Quantity).Name("Qty per pcs");
Map(m => m.TotalQuantity).Index(0);
Map(m => m.Description).Name("description");
Map(m => m.Designator).Name("designator");
Map(m => m.Case).Name("case");
Map(m => m.Value).Name("value");
Map(m => m.Tolerance).Name("tolerance");
Map(m => m.Remark).Name("remark");
Map(m => m.PartNumber).Name("partnumber");
Map(m => m.Manufacturer).Name("manufacturer");
Map(m => m.ManufacturerNr).Name("Manufactorer number");
Map(m => m.RoHS).Name("RoHS");
Map(m => m.Nachweis).Name("Nachweis");
}
}
PartsPerList.cs:
public class PartsPerList
{
public string Listname { get; set; } = string.Empty;
}
ManufacturersPartsMap.cs:
public class ManufacturerPartsMap : ClassMap<PartsPerList>
{
public ManufacturerPartsMap()
{
Map(m => m.Listname).Name("test").Optional();
}
}
To write two different objects in one row with CSVHelper, you can loop through the records and write them line by line.
void Main()
{
var fooRecords = new List<Foo>
{
new Foo { Id = 1, Name = "one" },
new Foo { Id = 2, Name = "two" },
};
var barRecords = new List<Bar>
{
new Bar { Id = 3, Description = "The first one" },
new Bar { Id = 4, Description = "The secord one" },
};
//using (var writer = new StreamWriter("path\\to\\file.csv"))
using (var csv = new CsvWriter(Console.Out, CultureInfo.InvariantCulture))
{
csv.WriteHeader<Foo>();
csv.WriteHeader<Bar>();
csv.NextRecord();
for (int i = 0; i < fooRecords.Count; i++)
{
csv.WriteRecord(fooRecords[i]);
csv.WriteRecord(barRecords[i]);
csv.NextRecord();
}
}
}
public class Foo
{
[Name("FooId")]
public int Id { get; set; }
public string Name { get; set; }
}
public class Bar
{
[Name("BarId")]
public int Id { get; set; }
public string Description { get; set; }
}

Create query by dynamically pass the GroupBy() and create new class in Select() using Expression tree

I`m having simple method which builds IQueryable and returns it.
public IQueryable<ClassDTO> ReportByNestedProperty()
{
IQueryable<Class> query = this.dbSet;
IQueryable<ClassDTO> groupedQuery =
from opportunity in query
group new
{
ItemGroup = opportunity.OpportunityStage.Name,
EstimatedRevenue = opportunity.EstimatedRevenue,
CostOfLead = opportunity.CostOfLead
}
by new
{
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
}
into item
select new ClassDTO()
{
ItemGroup = string.IsNullOrEmpty(item.Key.Name) ? "[Not Assigned]" : item.Key.Name,
Count = item.Select(z => z.ItemGroup.Name).Count(), // int
Commission = item.Sum(z => z.EstimatedRevenue), // decimal
Cost = item.Sum(z => z.CostOfLead), // decimal?
};
return groupedQuery;
}
This is fine. The thing i need is to create method with same return type, but groupby by different prperties dynamically. So from the above code I want to have 3 dynamic parts which will be passed as params:
ItemGroup = opportunity.OpportunityStage.Name
and
by new
{
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
}
So the new method should be like this
public IQueryable<ClassDTO> ReportByNestedProperty(string firstNestedGroupByProperty, string secondNestedGroupByProperty)
{
// TODO: ExpressionTree
}
And call it like this:
ReportByNestedProperty("OpportunityStage.Name","OpportunityStage.Id")
ReportByNestedProperty("OtherNestedProperty.Name","OtherNestedProperty.Id")
ReportByNestedProperty("OpportunityStage.Name","OpportunityStage.Price")
So the main thing is to create expressions with these two selects:
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
I have tried toe create the select expressions, groupby, the creation of Anonomoys classes and the DTO Class but I just cant get it right.
EDIT:
Here are the classes involved:
public class ClassDTO
{
public ClassDTO() { }
[Key]
public string ItemGroup { get; set; }
public decimal Commission { get; set; }
public decimal? Cost { get; set; }
public int Count { get; set; }
}
Class obj is a pretty big one so i`m posting just part of it
public partial class Class
{
public Class() { }
[Key]
public Guid Id { get; set; }
public Guid? OpportunityStageId { get; set; }
[ForeignKey(nameof(OpportunityStageId))]
[InverseProperty(nameof(Entities.OpportunityStage.Class))]
public virtual OpportunityStage OpportunityStage { get; set; }
}
public partial class OpportunityStage
{
public OpportunityStage()
{
this.Classes = new HashSet<Class>();
}
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
[InverseProperty(nameof(Class.OpportunityStage))]
public virtual ICollection<TruckingCompanyOpportunity> Classes{ get; set; }
}
I have simplified your Grouping query and introduced private class IdName which should replace anonymous class usage:
class IdName
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
static Expression MakePropPath(Expression objExpression, string path)
{
return path.Split('.').Aggregate(objExpression, Expression.PropertyOrField);
}
IQueryable<ClassDTO> ReportByNestedProperty(IQueryable<Class> query, string nameProperty, string idProperty)
{
// Let compiler to do half of the work
Expression<Func<Class, string, int, IdName>> keySelectorTemplate = (opportunity, name, id) =>
new IdName { Name = name, Id = id };
var param = keySelectorTemplate.Parameters[0];
// generating expressions from prop path
var nameExpr = MakePropPath(param, nameProperty);
var idExpr = MakePropPath(param, idProperty);
var body = keySelectorTemplate.Body;
// substitute parameters
body = ReplacingExpressionVisitor.Replace(keySelectorTemplate.Parameters[1], nameExpr, body);
body = ReplacingExpressionVisitor.Replace(keySelectorTemplate.Parameters[2], idExpr, body);
var keySelectorLambda = Expression.Lambda<Func<Class, IdName>>(body, param);
// finalize query
IQueryable<ClassDTO> groupedQuery = query
.GroupBy(keySelectorLambda)
.Select(item => new ClassDTO()
{
ItemGroup = string.IsNullOrEmpty(item.Key.Name) ? "[Not Assigned]" : item.Key.Name,
Count = item.Count(x => x.Name), // int
Commission = item.Sum(x => x.EstimatedRevenue), // decimal
Cost = item.Sum(x => x.CostOfLead), // decimal?
});
return groupedQuery;
}

Read CSV row and map to class with collection of subclass

I am reading in a CSV file.
There are no headers.
I need to map it to a class which has a collection of sub objects.
I know the amount of objects in the collection.
Public Class Foo{
public int id {get; set;}
public Bar[] bars {get; set;}
public class Bar{
public int id {get; set;}
public string str {get; set;}
}
}
I am trying to accomplish this using CSVHelper
I have tried creating a mapper like below.
However I just get the following error:
CsvHelper.TypeConversion.TypeConverterException: 'The conversion cannot be performed.
public sealed class Mapper : ClassMap<Foo>
{
public Mapper()
{
Map(m => m.id).Index(0);
Map(m => m.bars).Index(1, 2);
}
}
It seems the Index overload with 2 parameters is expecting to just convert collections of values as opposed to objects constructed from multiple columns.
My actual code has a collection size of 80, with objects with 5 fields on them so bringing them out onto the base Foo object is not ideal.
I know I can pull out the CSV as a string and string split by lines and commas and iterate through them manually but using a proper CSV library seemed cleaner and less prone to oversights.
I see there is also the option to add a References with a map to it
References<BarMap>(m => m.Bars);
public sealed class BarMap : ClassMap<Bar>
{
public BarMap()
{
Map(m => m.id).Index(0);
Map(m => m.str).Index(1);
}
}
But I cannot see how I can appropriately set the Indexes for it.
The reference does not allow specifying an index.
You should be able to use Convert in your mapping to get the bars. I assumed two Bar records, but you can change the for loop to account for different numbers of Bar.
void Main()
{
var input = "1,1,Dolor,2,Lorem\n2,3,Sit,4,Ipsum";
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = false,
};
using (var reader = new StringReader(input))
using (var csv = new CsvReader(reader, config))
{
csv.Context.RegisterClassMap<Mapper>();
var records = csv.GetRecords<Foo>().ToList();
}
}
public sealed class Mapper : ClassMap<Foo>
{
public Mapper()
{
Map(m => m.id).Index(0);
Map(m => m.bars).Convert(args =>
{
var bars = new List<Bar>();
for (int i = 1; i < 4; i += 2)
{
var bar = new Bar
{
id = args.Row.GetField<int>(i),
str = args.Row.GetField<string>(i + 1)
};
bars.Add(bar);
}
return bars.ToArray();
});
}
}
public class Foo
{
public int id { get; set; }
public Bar[] bars { get; set; }
}
public class Bar
{
public int id { get; set; }
public string str { get; set; }
}
I don't think it is possible to automatically map the file to your class, but I've achieved the required result using a DTO class.
Considering the data is:
0,0,0,Lorem
1,0,1,Ipsum
2,1,0,Dolor
3,1,1,Sit
4,1,2,Amet
Running the following code
public static void Main(string[] args)
{
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = false,
};
IEnumerable<FoobarDto> records = null;
using (var reader = new StreamReader("file.csv"))
using (var csv = new CsvReader(reader, config))
{
csv.Context.RegisterClassMap<FoobarDtoMap>();
records = csv.GetRecords<FoobarDto>().ToList();
}
var finalRecords = records.GroupBy(x => x.Id).Select(x => new Foo { Id = x.Key, Bars = x.Select(f => f.Bar).ToArray() });
}
public class FoobarDto
{
public int Id { get; set; }
public Foo.Bar Bar { get; set; }
}
public class Foo
{
public int Id { get; set; }
public Bar[] Bars { get; set; }
public class Bar
{
public int Id { get; set; }
public string Str { get; set; }
}
}
public sealed class FoobarDtoMap : ClassMap<FoobarDto>
{
public FoobarDtoMap()
{
Map(m => m.Id).Index(1);
Map(m => m.Bar.Id).Index(2);
Map(m => m.Bar.Str).Index(3);
}
}
It gives you the proper result.
Please note that there should be a unique column at index 0 for CsvHelper to correctly parse every line of the csv file.

Get a particular instance of a class

I have created a model for UnitOfMeasure (UOM) and a model for ingredient where I would like to use UOM to enter a default UOM for the ingredient.
public class IngredientModel
{
public int Id { get; set; }
public string Name { get; set; }
public UnitOfMeasureModel DefaultUOM { get; set; }
}
public class UnitOfMeasureModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
}
I would like to use the Name property in the IngredientModel.
In configure.cs I have put this code to create some default data for the database:
protected override void Seed(RecipeApplication.Models.RecipeApplicationDb context)
{
if (!context.UnitOfMeasures.Any())
{
context.UnitOfMeasures.AddOrUpdate(
u => u.Id,
new UnitOfMeasureModel { Name = "Gram", Abbreviation = "g" },
new UnitOfMeasureModel { Name = "Kilogram", Abbreviation = "kg"},
new UnitOfMeasureModel { Name = "Milligram", Abbreviation = "mg" }
);
}
if (!context.Ingredients.Any())
{
context.Ingredients.AddOrUpdate(
i => i.Id,
new IngredientModel { Name = "Italiaanse Ham", DefaultUOM =
);
}
}
I did not enter anything yet at default UOM because that is where I got stuck.
Could someone help me with this issue?
I'm assuming you just want to be able to access one of the UnitOfMeasureModel classes in both the UnitOfMeasures.AddOrUpdate and the UnitOfMeasures.AddOrUpdate methods. To do this create the instance before the calls and use that same instance in each AddOrUpdate method like so.....
protected override void Seed(RecipeApplication.Models.RecipeApplicationDb context)
{
var defaultUOM = new UnitOfMeasureModel { Name = "Gram", Abbreviation = "g" };
if (!context.UnitOfMeasures.Any())
{
context.UnitOfMeasures.AddOrUpdate(
u => u.Id,
defaultUOM,
new UnitOfMeasureModel { Name = "Kilogram", Abbreviation = "kg"},
new UnitOfMeasureModel { Name = "Milligram", Abbreviation = "mg" }
);
}
if (!context.Ingredients.Any())
{
context.Ingredients.AddOrUpdate(
i => i.Id,
new IngredientModel { Name = "Italiaanse Ham", DefaultUOM = defaultUOM
);
}
}
obviously you can change if gram is not the correct default

RavenDb how do I reduce group values into collection in reduce final result?

I hope it's more clear what I want to do from the code than the title. Basically I am grouping by 2 fields and want to reduce the results into a collection all the ProductKey's constructed in the Map phase.
public class BlockResult
{
public Client.Names ClientName;
public string Block;
public IEnumerable<ProductKey> ProductKeys;
}
public Block()
{
Map = products =>
from product in products
where product.Details.Block != null
select new
{
product.ClientName,
product.Details.Block,
ProductKeys = new List<ProductKey>(new ProductKey[]{
new ProductKey{
Id = product.Id,
Url = product.Url
}
})
};
Reduce = results =>
from result in results
group result by new {result.ClientName, result.Block} into g
select new BlockResult
{
ClientName = g.Key.ClientName,
Block = g.Key.Block,
ProductKeys = g.SelectMany(x=> x.ProductKeys)
};
}
I get some weird System.InvalidOperationException and a source code dump where basically it is trying to initialize the list with an int (?).
If I try replacing the ProductKey with just IEnumerable ProductIds (and make appropriate changes in the code). Then the code runs but I don't get any results in the reduce.
You probably don't want to do this. Are you really going to need to query in this manner? If you know the context, then you should probably just do this:
var q = session.Query<Product>()
.Where(x => x.ClientName == "Joe" && x.Details.Block == "A");
But, to answer your original question, the following index will work:
public class Products_GroupedByClientNameAndBlock : AbstractIndexCreationTask<Product, Products_GroupedByClientNameAndBlock.Result>
{
public class Result
{
public string ClientName { get; set; }
public string Block { get; set; }
public IList<ProductKey> ProductKeys { get; set; }
}
public class ProductKey
{
public string Id { get; set; }
public string Url { get; set; }
}
public Products_GroupedByClientNameAndBlock()
{
Map = products =>
from product in products
where product.Details.Block != null
select new {
product.ClientName,
product.Details.Block,
ProductKeys = new[] { new { product.Id, product.Url } }
};
Reduce = results =>
from result in results
group result by new { result.ClientName, result.Block }
into g
select new {
g.Key.ClientName,
g.Key.Block,
ProductKeys = g.SelectMany(x => x.ProductKeys)
};
}
}
When replicating I get the same InvalidOperationException, stating that it doesn't understand the index definition (stack trace omitted for brevity).
Url: "/indexes/Keys/ByNameAndBlock"
System.InvalidOperationException: Could not understand query:
I'm still not entirely sure what you're attempting here, so this may not be quite what you're after, but I managed to get the following working. In short, Map/Reduce deals in anonymous objects, so strongly typing to your custom types makes no sense to Raven.
public class Keys_ByNameAndBlock : AbstractIndexCreationTask<Product, BlockResult>
{
public Keys_ByNameAndBlock()
{
Map = products =>
from product in products
where product.Block != null
select new
{
product.Name,
product.Block,
ProductIds = product.ProductKeys.Select(x => x.Id)
};
Reduce = results =>
from result in results
group result by new {result.Name, result.Block}
into g
select new
{
g.Key.Name,
g.Key.Block,
ProductIds = g.SelectMany(x => x.ProductIds)
};
}
}
public class Product
{
public Product()
{
ProductKeys = new List<ProductKey>();
}
public int ProductId { get; set; }
public string Url { get; set; }
public string Name { get; set; }
public string Block { get; set; }
public IEnumerable<ProductKey> ProductKeys { get; set; }
}
public class ProductKey
{
public int Id { get; set; }
public string Url { get; set; }
}
public class BlockResult
{
public string Name { get; set; }
public string Block { get; set; }
public int[] ProductIds { get; set; }
}

Categories

Resources