Transform CSV input (1 field) to destination class (2 fields) - c#

I'm writing my ClassMap which works for my first basic fields (including those with column name not matching class member).
But I have 2 fields which need a particular work
1) I have a color stored as string. I need some code which convert the input to 2 values and store each one in a specific member.
2) I have an ID which match CSV item ID (that's the father or mother ID). But I need to convert it to the ID in my database (so I have to write some code to match CSV_ID to DB_ID).
Is it possible to add this custom logic with CSVHelper ?
Thanks for help.
Vincent

As it seems, CSVHelper also supports the same type converter injection during class mapping.
https://joshclose.github.io/CsvHelper/examples/configuration/class-maps/type-conversion
By combining this with the support for mapping by alternate names,
https://joshclose.github.io/CsvHelper/examples/configuration/class-maps/mapping-by-alternate-names
Having a csv file like this:
Id,Name,Color
1,OGUZ OZGUL,#f0f0f0
2,VINCENT,#80A0C0
3,OZGUL OGUZ,#00A000
it is possible to achieve what's needed as follows:
using System;
using System.Globalization;
using System.IO;
using CsvHelper;
using CsvHelper.Configuration;
using CsvHelper.TypeConversion;
using System.Linq;
namespace console
{
public class Program
{
public class Foo
{
// Represents the database Id
public int Id { get; set; }
public string Name { get; set; }
// Represents a three character color code, like #FFF
public string Color3 { get; set; }
// Represents a six character color code like #FFFFFF
public string Color6 { get; set; }
}
// OK, we are not converting between types here, but who cares?
// CSVHelper certainly doesn't.
public class IdConverter : DefaultTypeConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
string csvId = text;
int databaseId = Convert.ToInt32(text) + 10000;
return databaseId;
}
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
return ((int)value - 10000).ToString();
}
}
// Again, we are changing the value as we wish, not the type.
public class Color3Converter : DefaultTypeConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
// format: #ffffff
return "#" + text[1] + text[3] + text[5];
}
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
// format: #fff
return "#" + ((string)value)[1] + "0" + ((string)value)[2] + "0" + ((string)value)[3] + "0";
}
}
// By combining a type converter and alternative name
// we achieve one CSV field value to be mapped to two properties
// of our class Foo
public sealed class FooMap : ClassMap<Foo>
{
public FooMap()
{
Map(m => m.Id).TypeConverter<IdConverter>();
Map(m => m.Name);
Map(m => m.Color3).TypeConverter<Color3Converter>().Name("Color");
Map(m => m.Color6).Name("Color");
}
}
static void Main(string[] args)
{
using (var reader = new StreamReader("data.csv"))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Configuration.RegisterClassMap<FooMap>();
Foo[] records = csv.GetRecords<Foo>().ToArray();
foreach(Foo record in records)
{
Console.WriteLine
(
"Foo, Id: {0}, Name: {1}, Color3: {2}, Color6: {3}",
record.Id,
record.Name,
record.Color3,
record.Color6
);
}
}
}
}
}
}
The output of the program is:
Foo, Id: 10001, Name: OGUZ OZGUL, Color3: #fff, Color6: #f0f0f0
Foo, Id: 10002, Name: VINCENT, Color3: #8AC, Color6: #80A0C0
Foo, Id: 10003, Name: OZGUL OGUZ, Color3: #0A0, Color6: #00A000

Related

How to read values from array of objects in appsettings.json file

My appsettings json file
{
"StudentBirthdays": [
{ "Anne": "01/11/2000"},
{ "Peter": "29/07/2001"},
{ "Jane": "15/10/2001"},
{ "John": "Not Mentioned"}
]
}
I have a seperate config class.
public string GetConfigValue(string key)
{
var value = _configuration["AppSettings:" + key];
return !string.IsNullOrEmpty(value) ? Convert.ToString(value) : string.Empty;
}
What I have tried is,
list= _configHelper.GetConfigValue("StudentBirthdays");
For the above I dont get the values.
How can I read the values(I want to read the name of the student and his birthday seperatly).
Any help is apreciated
You can obtain the birthdays using the following code:
// get the section that holds the birthdays
var studentBirthdaysSection = _configuration.GetSection("StudentBirthdays");
// iterate through each child object of StudentBirthdays
foreach (var studentBirthdayObject in studentBirthdaysSection.GetChildren())
{
// your format is a bit weird here where each birthday is a key:value pair,
// rather than something like { "name": "Anne", "birthday": "01/11/2000" }
// so we need to get the children and take the first one
var kv = studentBirthdayObject.GetChildren().First();
string studentName = kv.Key;
string studentBirthday = kv.Value;
Console.WriteLine("{0} - {1}", studentName, studentBirthday);
}
Try it online
try this
using System.Linq;
public List<Student> GetStudentsFromConfig()
{
return _configuration
.GetSection("StudentBirthdays")
.Get<Dictionary<string, string>[]>()
.SelectMany(i => i)
.Select(ie => new Student {Name=ie.Key, DOB=ie.Value})
.ToList();
}
test
items= _configHelper.GetStudentsFromConfig();
foreach (var item in items) Console.WriteLine($"Name: {item.Name} , DOB: {item.DOB} ");
result
Name: Anne , DOB: 01/11/2000
Name: Peter , DOB: 29/07/2001
Name: Jane , DOB: 15/10/2001
Name: John , DOB: Not Mentioned
class
public class Student
{
public string Name { get; set; }
public string DOB { get; set; }
}
Try this:
Create Model/Class like below:
public class StudentBirthday
{
String Name,
String Birthday
}
Then access values like this :
List<StudentBirthday StudentBirthdays =
_config.GetSection("Main:StudentBirthdays").Get<List<StudentBirthday();

Apply CsvHelper custom converter to specific class map field(s)

Running CSVHelper 7.0.0 and trying to add a custom string convertor that can be applied to specific class map fields (do not want to applied globally to all fields of type string). Below are snippets on how I currently have my class map, custom convertor, and csv writter calls setup.
Class Map code snippet with custom convertor on NextReviewDate map field:
public sealed class MyCustomClassMap : ClassMap<MyCustomClass>
{
public MyCustomClassMap()
{
Map(m => m.ContentId).Index(0);
Map(m => m.Name).Index(1);
Map(m => m.ContentOwner).Index(2);
Map(m => m.ContentOwnerName).Index(3);
Map(m => m.CopyrightOwner).Index(4);
Map(m => m.CopyrightOwnerName).Index(5);
Map(m => m.NextReviewDate).Index(6).TypeConverter<DateTimeStringConverter>();
Map(m => m.ContentStatus).Index(7);
Map(m => m.UsageRights).Index(8);
Map(m => m.SchemaName).Index(9);
}
}
Custom string converter code snippet:
public class DateTimeStringConverter : StringConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
string formattedDateString = string.Empty;
if (DateTime.TryParse(text, out DateTime dateobj))
{
formattedDateString = dateobj.ToString("MM-dd-yyyy");
}
//throw new Exception("DateTimeStringConverter value: " + formattedDateString);
return formattedDateString;
}
}
Snippet of code of how I am registering my class map and write records:
csv.Configuration.RegisterClassMap<MyCustomClassMap>();
csv.WriteRecords(results);
To troubleshoot I added a throw exception in DateTimeStringConverter and appears it never gets called. Am I missing a piece? Right now the CSV is generating and includes the original NextReviewDate map field value without ever calling the custom convertor.
EDIT: based on #Self feedback changing custom string converter to the following resolved issue:
public class DateTimeStringConverter : DefaultTypeConverter
{
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
string strVal = (string)value;
if (DateTime.TryParse(strVal, out DateTime dateobj))
{
strVal = dateobj.ToString("MM-dd-yyyy");
}
return strVal;
}
}
CSV Helper 26.1.0
First StringConverter offers only one method to overwrite object ConvertFromString(..).
The converstion to string is handled by nothing because it's suppose to be a string.
Here I supposse that your Type is DateTime and you got it in multiple Exotique format. If you have only one format you can change the default format for that type.
A simple demo class and it's mapping:
public class Test
{
public int Id { get; set; }
public DateTime DateTime { get; set; }
public DateTime Date { get; set; }
public DateTime Time { get; set; }
}
public sealed class TestMap : ClassMap<Test>
{
public TestMap()
{
AutoMap(CultureInfo.InvariantCulture);
Map(x => x.Date).TypeConverter(new DateStringConverter("MM - dd - yyyy"));
Map(x => x.Time).TypeConverter(new DateStringConverter("mm # hh # ss"));
}
}
I used a converter that inherit from ITypeConverter in order to have both ConvertFromString and ConvertToString.
With Customisable Format, culture, and style.
public class DateStringConverter : ITypeConverter
{
private readonly string _dateFormat;
private readonly CultureInfo _CultureInfo;
private readonly DateTimeStyles _DateTimeStyles;
public DateStringConverter(string dateFormat) :
this(dateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None)
{ }
public DateStringConverter(string dateFormat, CultureInfo cultureInfo, DateTimeStyles dateTimeStyles)
{
_dateFormat = dateFormat;
_CultureInfo = cultureInfo;
_DateTimeStyles = dateTimeStyles;
}
public object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
string formattedDateString = string.Empty;
if (DateTime.TryParseExact(text, _dateFormat, _CultureInfo, _DateTimeStyles, out DateTime dateObj))
{
return dateObj;
}
return null;
}
public string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
if (value == null) return string.Empty;
if (DateTime.TryParse(value.ToString(), out DateTime dt))
return dt.ToString(_dateFormat);
else
return string.Empty;
}
}
Writing a CSV:
using (var writer = new StringWriter())
using (var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture, true))
{
csvWriter.Context.RegisterClassMap<TestMap>();
csvWriter.WriteRecords(datas);
csvWriter.Flush();
csvTextOuput = writer.ToString();
}
Result:
Id,DateTime,Date,Time
1,04/14/2021 09:18:02,04 - 14 - 2021,18 # 09 # 02
2,04/15/2021 09:18:02,04 - 15 - 2021,18 # 09 # 02
3,04/16/2021 12:18:02,04 - 16 - 2021,18 # 12 # 02
Reading a CSV:
using (var reader = new StringReader(csvTextOuput))
using (var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture, true))
{
csvReader.Context.RegisterClassMap<TestMap>();
ObjectFromCSV = csvReader.GetRecords<Test>().ToArray();
}
Result:
[
{
Date : 04/14/2021
DateTime : 04/14/2021
Id : 1
Time : 04/14/2021
},
{
Date : 04/15/2021
DateTime : 04/15/2021
Id : 2
Time : 04/14/2021
},
{
Date : 04/16/2021
DateTime : 04/16/2021
Id : 3
Time : 04/14/2021
}
]
Live demo https://dotnetfiddle.net/EMdhtn
CSV Helper 7
https://dotnetfiddle.net/5DgwxY
The only modification should be the absence of culture in the reader/writer ctor. And RegisterClassMap that moved from Configuration to Context
~new CsvReader(reader, CultureInfo.InvariantCulture, true))~ => new CsvReader(reader))
~csvWriter.Context.RegisterClassMap()~ => csvWriter.Configuration.RegisterClassMap();
Homogenous datetime format accros all property.
In case youhave the same format everywhere you those proposed solution:
CsvHelper changing how dates and times output
N.B:TypeConverterFactory or TypeConverterCache on older version.

How to query multiple json nodes?

I have the following json:
{
"key445" : {
"text" : "cat",
"id" : 445
},
"key457" : {
"text" : "mouse",
"id" : 457
},
"key458" : {
"text" : "rodent",
"id" : 458
}
}
I am trying extract text and id into a List<TextIdentifier> (a class with string and an int), or even separately into List<string> and List<int>.
I read the text into JObject using JObject.Parse method. However, I can't figure out how to get to the text and id nodes. I've tried the following but it returns nothing.
var textTokens = o.SelectTokens("/*/text");
var idTokens = o.SelectTokens("/*/id");
How can I get to the tokens I want?
I would just convert the whole JSON into a Dictionary<string, TextIdentifier> instead of trying to use JsonPath at all:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
public class TextIdentifier
{
public string Text { get; set; }
public int Id { get; set; }
public override string ToString() => $"Id: {Id}; Text: {Text}";
}
public class Test
{
static void Main()
{
var json = File.ReadAllText("test.json");
var dictionary = JsonConvert.DeserializeObject<Dictionary<string, TextIdentifier>>(json);
foreach (var pair in dictionary)
{
Console.WriteLine($"{pair.Key} => {pair.Value}");
}
}
}
Output:
key445 => Id: 445; Text: cat
key457 => Id: 457; Text: mouse
key458 => Id: 458; Text: rodent
If you don't care about the keys, you can use dictionary.Values.ToList() to get a List<TextIdentifier> although you shouldn't rely on the order of them.

List outputs the namespace's name, instead of its values

So, first of all, I am a beginner and this problem must be super simple, but I just don't find it. So the problem is, I have a small txt, containing fruits and its amounts:
Apple 26
Banana 55
Pear 12
Orange 32
Watermelon 81
Grapefruit 30
And I add these values in a list, but it outputs (in my opinion) the namespace's name. I guess this is some "property access" issue, but I don't know how to solve it.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
namespace practice
{
class Fruits
{
string fruit { get; set; }
int amount { get; set; }
public Fruits(string a, int b)
{
fruit = a;
amount = b;
}
}
class Program
{
static void Main(string[] args)
{
List<Fruits> data = new List<Fruits>();
StreamReader str_R = new StreamReader("list.txt");
while (!str_R.EndOfStream)
{
String[] line = str_R.ReadLine().Split(' ');
data.Add(new Fruits(line[0], Convert.ToInt32(line[1])));
}
foreach (var x in data)
{
Console.WriteLine(x);
}
Console.ReadKey();
}
}
}
Output:
practice.Fruits
practice.Fruits
practice.Fruits
practice.Fruits
practice.Fruits
practice.Fruits
I want the actual values of the list as output.
The base class of Fruits class is Object class, when you call an instance of a class by default it will return the ToString() method. the ToString() method of object returns the type of this object (the namespace). the ToString() method is Virtual (can be overridden by any class that inherits it) so you can override it:
class Fruits
{
string fruit { get; set; }
int amount { get; set; }
public Fruits(string a, int b)
{
fruit = a;
amount = b;
}
public override string ToString()
{
return string.Format("Fruit: {0} Ammount: {1}", fruit, amount);
}
}
Also, you can always do that:
foreach (var x in data)
{
Console.WriteLine(string.Format("Fruit: {0} Ammount: {1}", x.fruit, x.amount));
}
This is normal behavior. If given anything but a String, Console.WriteLine() will call ToString() on it.
Object.ToString() will simply output the class name, unless it is overridden.
You either need to override ToString() for Fruits:
public override string ToString(){
return fruit;
}
Or outpoint x.fruit and x.amount explicitly.
foreach (var x in data)
{
Console.WriteLine(x.fruit);
}

How do you support selecting multiple enum values for an EPiServer property?

I have a property which allows a user to select multiple enum values, and at the moment this works fine for saving the information into the database and for using it. However it doesn't seem to correctly read the values out of the property back to the edit UI.
I presume there is some sort of type issue with the enum which causes the SelectMany values not to be set as you'd expect.
I have the following enum:
public enum Skills
{
People,
IT,
Management,
Sales,
}
And the following ISelectionFactory:
using System;
using System.Collections.Generic;
using System.Linq;
namespace TestSite.Business.EditorDescriptors
{
using EPiServer.Shell.ObjectEditing;
public class EnumSelectionFactory<TEnum> : ISelectionFactory
{
public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
{
var values = Enum.GetValues(typeof(TEnum));
return (from object value in values select new SelectItem { Text = this.GetValueName(value), Value = value }).OrderBy(x => x.Text);
}
private string GetValueName(object value)
{
return value.ToString();
}
}
}
Then I have the property which I've added to the ContactPage model in the Alloy Demo.
[SelectMany(SelectionFactoryType = typeof(EnumSelectionFactory<Skills>))]
public virtual string EmployeeLevels { get; set; }
Does anyone know how to solve this?
Seems to be a bug. Please report to EPiServer.
Set the underlying value type ...
namespace TestSite.Business.EditorDescriptors
{
using EPiServer.Shell.ObjectEditing;
public class EnumSelectionFactory<TEnum, TUnderlying> : ISelectionFactory
{
public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
{
var values = Enum.GetValues(typeof(TEnum));
return (from TEnum value in values select new SelectItem { Text = this.GetValueName(value), Value = Convert.ChangeType(value, typeof(TUnderlying)) }).OrderBy(x => x.Text);
}
private string GetValueName(object value)
{
return Enum.GetName(typeof(TEnum), value);
}
}
}
...implemented by your model with a string type ...
[SelectMany(SelectionFactoryType = typeof(EnumSelectionFactory<Skills,string>))]
public virtual string EmployeeLevels { get; set; }

Categories

Resources