I need to convert a double field into a custom string output depending on a mapped class' parameter. This is most easily shown with code.
public enum Type {
Mod,
NonMod
}
public class Document {
public double Value { get; set; }
public Type DocType { get; set; }
}
Now attempting to convert the Value field...
public class DocumentMap : ClassMap<Document>
{
public DocumentMap
{
Map(m => m.Value).Index(0).Name("Value").TypeConverter<CustomDoubleConverter>()
Map(m => m.Type).Index(1).Name("Type");
}
private class CustomDoubleConverter : DefaultTypeConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
return text == "ModVal" ? null : double.Parse(text);
}
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
return (double)value == 0 /* && Document.Type == Type.Mod*/ ? "ModVal" : value.ToString();
}
}
}
I would need the Document type within CustomDoubleConverter to write "ModVal" only for Mod type documents. The only issue is converting the value to a string as converting back would mean it was properly delineated initially. I would need to know the document types for each of the documents, so I don't believe a parameter could be passed into the DocumentMap instantiation as it is only instantiated once.
I'm not quite sure I understand all of your logic, but I think Convert could work for you in the ClassMap.
public class DocumentMap : ClassMap<Document>
{
public DocumentMap()
{
Map(m => m.Value).Name("Value")
.Convert(args =>
{
var value = args.Row.GetField<string>(0);
return value == "ModVal" ? 0 : double.Parse(value);
}).Convert(args =>
{
return args.Value.Value == 0 && args.Value.DocType == Type.Mod ? "ModVal" : args.Value.Value.ToString();
});
Map(m => m.DocType).Index(1).Name("Type");
}
}
Related
How can you map Value object that has different data type in constructor than property type.
public class Information : ValueObject
{
private const string Delimiter = ",";
public Information(string value)
{
Value = SetValueAsReadOnlyList(value);
}
public ReadOnlyCollection<int> Value { get; }
private ReadOnlyCollection<int> SetValueAsReadOnlyList(string value)
{
var collection = value
.Split(Delimiter)
.Select(x =>
{
if(int.Parse(x, out var result))
{
return result;
}
throw new ParseStringToIntDomainException(x);
}).ToList();
return collection.AsReadOnly();
}
}
Mongo map will look like this, which is not working because x.Value is of type string and constructor is expecting ReadOnlyCollection
public class InformationMap : IBsonClassMap
{
public void Map()
{
if (BsonClassMap.IsClassMapRegistered(typeof(Information)))
{
return;
}
BsonClassMap.RegisterClassMap<Information>(map =>
{
map.MapCreator(x => new Information(x.Value));
map.MapProperty(x => x.Value);
});
}
}
I don't think it's possible. I can wrap Value to another nested data type with it's own mapping. But I would need to define transformation inside InformationMap class.
I'm creating a generic filtering user control to allow the user to apply various filters on a CollectionView, on an WPF app.
So, I have a filled CollectionView with entities, with properties. So, instead of creating a different user control for each entity, I came up with this:
foreach (PropertyInfo propertyInfo in _typeMessaging.Mensagem.GetProperties())
{
var attrs = propertyInfo.GetCustomAttributes(true);
foreach (object attr in attrs)
{
if (attr is DescriptionAttribute descr)
fields.Add(new FilteringInfo() { Property = propertyInfo, Description = descr.Description }); ;
}
}
foreach (FilteringInfo filteringInfo in fields.OrderBy(x => x.Property.Name))
{
Columns.Add(filteringInfo);
}
So I just bind Columns to a combo box and the user can select which column (i.e. property) they want to filter their view by, all I need is to set the properties I want the user to be able to filter by with a description attribute. If the property type is string, DateTime, int or decimal, the user simply enters the info they want to filter by and it generates a filter to be applied on parent ViewModel's CollectionView. It then returns a FilteringInfo object to the parent ViewModel, which has the chosen PropertyInfo and the value the user wants to filter by preceded by a filtering word as a parameter.
This FilteringInfo is passed to a FiltersCollection which stores all the filters requested by the user and returns a Filter to be added to the CollectionView:
public class FiltersCollection
{
private readonly GroupFilter _filtros = new();
public Predicate<object> AddNewFilter(EntityBase entity)
{
FilteringInfo filteringInfo = entity as FilteringInfo;
switch (filteringInfo.FilterInfo.Split(':')[0])
{
case "wholefield":
_filtros.AddFilter(x => x is EntityBase entityBase && ((string)entityBase.GetPropValue(filteringInfo.Property.Name)).Equals(filteringInfo.FilterInfo.Split(':')[1], StringComparison.OrdinalIgnoreCase));
break;
case "contains":
_filtros.AddFilter(x => x is EntityBase entityBase && ((string)entityBase.GetPropValue(filteringInfo.Property.Name)).Contains(filteringInfo.FilterInfo.Split(':')[1], StringComparison.OrdinalIgnoreCase));
break;
case "startswith":
_filtros.AddFilter(x => x is EntityBase entityBase && ((string)entityBase.GetPropValue(filteringInfo.Property.Name)).StartsWith(filteringInfo.FilterInfo.Split(':')[1], StringComparison.OrdinalIgnoreCase));
break;
case "datebetween":
string[] dates = filteringInfo.FilterInfo.Split(':')[1].Split(';');
DateTime start = DateTime.Parse(dates[0]);
DateTime end = DateTime.Parse(dates[1]).AddDays(1).AddSeconds(-1);
_filtros.AddFilter(x => x is EntityBase entityBase && ((DateTime)entityBase.GetPropValue(filteringInfo.Property.Name)).IsBetween(start, end));
break;
case "valuebetween":
string[] valuesBetween = filteringInfo.FilterInfo.Split(':')[1].Split(';');
decimal startValue = decimal.Parse(valuesBetween[0]);
decimal endValue = decimal.Parse(valuesBetween[1]);
_filtros.AddFilter(x => x is EntityBase entityBase && ((decimal)entityBase.GetPropValue(filteringInfo.Property.Name)).IsBetween(startValue, endValue));
break;
case "enumvalue":
_filtros.AddFilter(x => x is EntityBase entityBase && ((Enum)entityBase.GetPropValue(filteringInfo.Property.Name)).Equals(Enum.Parse(filteringInfo.Property.PropertyType, filteringInfo.FilterInfo.Split(':')[1])));
break;
case "abovevalue":
string[] values = filteringInfo.FilterInfo.Split(':')[1].Split(';');
if (filteringInfo.Property.PropertyType == typeof(int))
{
int headValue = int.Parse(values[0]);
_filtros.AddFilter(x => x is EntityBase entityBase && (int)entityBase.GetPropValue(filteringInfo.Property.Name) >= headValue);
}
if (filteringInfo.Property.PropertyType == typeof(decimal))
{
decimal headValue = decimal.Parse(values[0]);
_filtros.AddFilter(x => x is EntityBase entityBase && (decimal)entityBase.GetPropValue(filteringInfo.Property.Name) >= headValue);
}
break;
case "clearfilters":
_filtros.RemoveAllFilters();
return null;
}
return _filtros.Filter;
}
}
GroupFilter:
public class GroupFilter
{
private List<Predicate<object>> _filters;
public Predicate<object> Filter { get; private set; }
public GroupFilter()
{
_filters = new List<Predicate<object>>();
Filter = InternalFilter;
}
private bool InternalFilter(object o)
{
foreach (var filter in _filters)
{
if (!filter(o))
{
return false;
}
}
return true;
}
public void AddFilter(Predicate<object> filter)
{
_filters.Add(filter);
}
public void RemoveFilter(Predicate<object> filter)
{
if (_filters.Contains(filter))
{
_filters.Remove(filter);
}
}
public void RemoveAllFilters()
{
_filters.Clear();
}
}
The issue is when the property the user wants to filter by is an enum. I can easily use a converter to populate the combo box with the enum's description attributes:
public class EnumDescriptionConverter : IValueConverter
{
private string GetEnumDescription(Enum enumObj)
{
if (enumObj is null) return String.Empty;
if (Enum.IsDefined(enumObj.GetType(), enumObj) is false) return String.Empty;
FieldInfo fieldInfo = enumObj.GetType().GetField(enumObj.ToString());
object[] attribArray = fieldInfo.GetCustomAttributes(false);
if (attribArray.Length == 0)
{
return enumObj.ToString();
}
else
{
DescriptionAttribute attrib = attribArray[0] as DescriptionAttribute;
return attrib.Description;
}
}
object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Enum myEnum = (Enum)value;
string description = GetEnumDescription(myEnum);
return description;
}
}
However, I'm having a hard time getting the Enum from a given description. I found https://stackoverflow.com/a/3422440/ which states I can use LINQ to iterate through Enum.GetValues(myEnum), but it requires passing the enum I want to evaluate, which the binding does not; as far as the converter knows, the target type it's trying to convert back to is just Enum.
I tried passing the list of enums used to populate the available values so ConvertBack could use it, but I was told bound data cannot be used as converter parameters. Is there a way I can do this? If not, are there other ways I could do it?
If the targetType parameter in IValueConverter.Convert is really only giving you Enum, rather than the specific type of enumeration you need to convert to, then I don't think converting from the Description value alone will be possible. Actually, it might not be reliable anyway, because nothing is stopping anyone from creating two different values in the same enumeration and giving them identical descriptions (thus resulting in ambiguity).
Here's my suggestion: Instead of returning a string return a custom struct. Something like this:
public struct EnumValue
{
public EnumValue(Enum value, string description)
{
Value = value;
Description = description;
}
public Enum Value { get; }
public string Description { get; }
public override string ToString()
{
return Description;
}
}
Returning something like the above instead of just the description string value, will allow you to convert back to the enumeration value just by reading the Value property.
(You could also go a step further and put the actual logic for retrieving the description into the EnumValue struct, removing the description parameter from the constructor.)
I'm having a bit of a problem with converting an empty (value is "NULL") Key field to a byte array with CsvHelper, i keep getting this exception:
CsvHelper.ReaderException: An unexpected error occurred.
---> System.FormatException: Could not find any recognizable digits.
at System.ParseNumbers.StringToInt(ReadOnlySpan`1 s, Int32 radix, Int32 flags, Int32& currPos)
at System.Convert.ToByte(String value, Int32 fromBase)
at CsvHelper.TypeConversion.ByteArrayConverter.HexStringToByteArray(String hex)
at CsvHelper.TypeConversion.ByteArrayConverter.ConvertFromString(String text, IReaderRow row, MemberMapData memberMapData)
at lambda_method(Closure )
at CsvHelper.Expressions.RecordCreator.Create[T]()
at CsvHelper.Expressions.RecordManager.Create[T]()
at CsvHelper.CsvReader.GetRecord[T]()
--- End of inner exception stack trace ---
The mapping is set up like this:
public sealed class ZaznamMapping : ClassMap<Zaznam>
{
public ZaznamMapping(FileSettings configuration)
{
var nullValues = new[] { "NULL", "null", string.Empty };
for (int i = 0; i < configuration.Count(); i++)
{
switch (configuration[i])
{
case Col.Ignore: continue;
case Col.Id: Map(m => m.Id).Index(i).TypeConverterOption.NullValues(nullValues); break;
case Col.Idd: Map(m => m.Idd).Index(i).TypeConverterOption.NullValues(nullValues); break;
case Col.Data: Map(m => m.Data).Index(i).TypeConverterOption.NullValues(nullValues); break;
case Col.Key: Map(m => m.Key).Index(i).TypeConverterOption.NullValues(nullValues); break;
default: throw new NotSupportedException($"Mapping() - Unknown column \"{configuration[i].ToString()}\"!");
}
}
}
}
And Zaznam class:
public sealed class Zaznam
{
public int Id { get; set; }
public int Idd { get; set; }
public byte[] Data { get; set; }
public byte[] Key { get; set; }
}
The value in file Key coluimn is literally NULL (as in string containing the letters "NULL"). Should not the ByteArrayconverter respect the TypeConverterOptions?
The questions being:
Am I doing something wrong?
Should I be making my own converter instead?
It looks like the null value logic wasn't added to ByteArrayConverter. If you look at StringConverter, the same null logic is not in the ConvertFromString method for ByteArrayConverter. You should be able to create your own custom TypeConverter to add the logic and then register it for all byte[].
public class NullByteArrayConverter : ByteArrayConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
foreach (var nullValue in memberMapData.TypeConverterOptions.NullValues)
{
if (text == nullValue)
{
return null;
}
}
return base.ConvertFromString(text, row, memberMapData);
}
}
csv.Configuration.TypeConverterCache.AddConverter<byte[]>(new NullByteArrayConverter());
I'm using CsvHelper to serialize a class to csv file - until here everything works well.
Now I'm trying to find a way to convert the class's enum properties to their int value in the csv, so I could use the CSV for bulk insert later.
I found out the EnumConverter class in CsvHelper but I can't figure out how to properly use it, as all my tries are failing.
Here is my mapping class code
public sealed class MyMapping : CsvClassMap<TradingCalendarException>
{
public MyMapping()
{
EnumConverter enumConverter = new EnumConverter(typeof(CalendarExceptionEntityType));
Map(m => m.ExceptionEntityType).Index(0).Name("EXCEPTION_ENTITY_TYPE").TypeConverter(enumConverter);
Map(m => m.ExceptionEntityIdentifier).Index(1).Name("EXCEPTION_ENTITY_IDENTIFIER");
Map(m => m.OptionType).Index(2).Name("OPTION_TYPE");
Map(m => m.StartDatetime).Index(3).Name("EXCEPTION_START_DATETIME");
Map(m => m.EndDatetime).Index(4).Name("EXCEPTION_END_DATETIME");
Map(m => m.DataSourceType).Index(5).Name("DATA_SOURCE_TYPE");
Map(m => m.Description).Index(6).Name("DESCRIPTION");
}
}
and the writing part
using (StreamWriter file = new StreamWriter(filePath, false, Encoding.UTF8))
{
CsvWriter writer = new CsvWriter(file);
MyMapping mapping = new MyMapping();
writer.Configuration.RegisterClassMap(mapping);
writer.WriteRecords(calendarExceptionList);
}
The rest of the mapping (indexing and naming) is working, it's just the EnumConverter that doesn't do any change.
I didn't find any examples online.
Thank you!
This is the solution I made:
public class CalendarExceptionEnumConverter<T> : DefaultTypeConverter where T : struct
{
public override string ConvertToString(TypeConverterOptions options, object value)
{
T result;
if(Enum.TryParse<T>(value.ToString(),out result))
{
return (Convert.ToInt32(result)).ToString();
}
throw new InvalidCastException(String.Format("Invalid value to EnumConverter. Type: {0} Value: {1}",typeof(T),value));
}
}
and used it as the following:
Map(m => m.ExceptionEntityType).TypeConverter<CalendarExceptionEnumConverter<CalendarExceptionEntityType>>();
I used Yarimi's solution, but found it can't read the enum value back from the .csv (can write ok)
my solution is to make the class extend from EnumTypeConverter, not DefaultTypeConverter.
here is the full code
public class OurEnumConverter<T> : CsvHelper.TypeConversion.EnumConverter where T : struct
{
public OurEnumConverter(): base(typeof(T))
{ }
public override string ConvertToString(CsvHelper.TypeConversion.TypeConverterOptions options, object value)
{
T result;
if (Enum.TryParse<T>(value.ToString(), out result))
{
return (Convert.ToInt32(result)).ToString();
}
return base.ConvertToString(options, value);
//throw new InvalidCastException(String.Format("Invalid value to EnumConverter. Type: {0} Value: {1}", typeof (T), value));
}
public override object ConvertFromString(TypeConverterOptions options, string text)
{
int parsedValue;
//System.Diagnostics.Debug.WriteLine($"{typeof(T).Name} = {text}");
if (Int32.TryParse(text, out parsedValue))
{
return (T)(object)parsedValue;
}
return base.ConvertFromString(options, text);
//throw new InvalidCastException(String.Format("Invalid value to EnumConverter. Type: {0} Value: {1}", typeof(T), text));
}
}
and here is how it's used
public class TickTradeClassMap : CsvHelper.Configuration.CsvClassMap<TickData.TickTrade>
{
public TickTradeClassMap()
{
Map(m => m.price);
Map(m => m.size);
Map(m => m.exchange).TypeConverter<OurEnumConverter<ATExchangeEnum>>();
Map(m => m.condition1).TypeConverter<OurEnumConverter<ATTradeConditionEnum>>();
}
}
This is how I did it for the latest version of CSV Helper which is 7.1.1:
public class AggregateEnumConverter<T> : EnumConverter where T : struct
{
public AggregateEnumConverter() : base(typeof(T)) { }
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
if(!Enum.TryParse(text, out AggregateType aggregateType))
{
// This is just to make the user life simpler...
if(text == "24HAVG")
{
return AggregateType._24HAVG;
}
// If an invalid value is found in the CSV for the Aggregate column, throw an exception...
throw new InvalidCastException($"Invalid value to EnumConverter. Type: {typeof(T)} Value: {text}");
}
return aggregateType;
}
}
Note: the code above is making use of C# 7 new inline out variables.More info here: How should I convert a string to an enum in C#?
This is how you make use of the custom EnumConverter:
/// <summary>
/// Maps Tag class properties to the CSV columns' names
/// </summary>
public sealed class TagMap : ClassMap<Tag>
{
public TagMap(ILogger<CsvImporter> logger)
{
Map(tag => tag.Aggregate).Name("aggregate").TypeConverter<AggregateEnumConverter<AggregateType>>();
}
}
Add a int property to your TradingCalendarException class that casts back and forth to your custom enum, CalendarExceptionEntityType, like:
public int ExceptionEntityTypeInt {
get { return (int)ExceptionEntityType; }
set { ExceptionEntityType = (CalendarExceptionEntityType)value; }
}
Use Map(m => m.ExceptionEntityTypeInt).Index(0).Name("EXCEPTION_ENTITY_TYPE_INT") instead of your enum converter Map(m => m.ExceptionEntityType).Index(0).Name("EXCEPTION_ENTITY_TYPE").TypeConverter(new MyMapping())
I've got a generic class, which contains a value. This class stores a default value and I want to check, whether the stored value equals to the default one.
At first, I tried simply:
public bool IsDefault
{
get
{
return value == defaultValue;
}
}
But unfortunately (and surprisingly) that does not compile - compiler complains, that it cannot compare T to T. The second approach is:
public bool IsDefault
{
get
{
if (value == null)
return defaultValue == null;
else
return value.Equals(defaultValue);
}
}
It works, but I have a problem with strings, because a null string in my case equals to empty string, but the previous code does not cover that.
I may specialize the class for strings, but I'd avoid that if it is not necessary. Is there a way to compare two T's in a generic way?
Edit: in response to comments
Let's assume, that the class looks like the following:
public class MyClass<T>
{
private T value;
private T defaultValue;
public MyClass(T newDefault)
{
value = newDefault;
defaultValue = newDefault;
}
public T Value
{
get
{
return value;
}
set
{
this.value = value;
}
}
public bool IsDefault
{
get
{
// ?
}
}
}
The constructor of your class should take an IEqualityComparer<T> as a parameter, and an overload could pass in the EqualityComparer<T>.Default. Store it and use to test for equality. In the case of strings, pass in an IEqualityComparer<string> that considers "" == null.
Like this:
class Example<T>
{
private readonly IEqualityComparer<T> comparer;
private readonly T defaultValue;
private T value;
public Example(T value, T defaultValue, IEqualityComparer<T> comparer)
{
this.value = value;
this.defaultValue = defaultValue;
this.comparer = comparer;
}
public Example(T value, T defaultValue)
: this(value, defaultValue, EqualityComparer<T>.Default)
{
}
public Example(T value)
: this(value, default(T))
{
}
public Example()
: this (default(T))
{
}
public bool IsDefault
{
get
{
if (value == null)
{
return defaultValue == null;
}
else
{
return comparer.Equals(value, defaultValue);
}
}
}
}