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())
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 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");
}
}
I'm using CsvHelper to import csv files, and in order to do so I use a mapping class as follows:
private class MyClassMap : ClassMap<MyClass>
{
public MyClassMap ()
{
Map(m => m.Number).Name("Number");
Map(m => m.Name).Name("Name");
}
}
Most classes contain many more properties. So what I first did is create an Attribute class and added the attribute to all public properties. So I can change the mapping code:
private class MyClassMap : ClassMap<MyClass>
{
public MyClassMap ()
{
var properties = typeof(MyClass).GetProperties();
foreach (var property in properties)
{
var attr = property.GetCustomAttributes(typeof(HeaderAttribute), false).FirstOrDefault();
if (attr != null)
{
//Here what?
}
}
}
}
Also, I will make the above ctor code an extension method.
How would I use the Map() method in this case?
Assuming your HeaderAttribute accepts a Header as a parameter and exposes it over Header property :
foreach (var property in properties)
{
var attr = property.GetCustomAttributes(typeof(HeaderAttribute), false).FirstOrDefault() as HeaderAttribute;
if (attr != null)
{
//Here we use the Map method overload that takes a Type and a MemberInfo
this.Map(typeof(MyClass), property).Name(attr.Header);
}
}
Is there a way to force the compiler to restrict the usage of a custom attribute to be used only on specific property types like int, short, string (all the primitive types)?
similar to the AttributeUsageAttribute's ValidOn-AttributeTargets enumeration.
No, you can't, basically. You can limit it to struct vs class vs interface, that is about it. Plus: you can't add attributes to types outside your code anyway (except for via TypeDescriptor, which isn't the same).
You can run this unit test to check it.
First, declare validation attribute PropertyType:
[AttributeUsage(AttributeTargets.Class)]
// [JetBrains.Annotations.BaseTypeRequired(typeof(Attribute))] uncomment if you use JetBrains.Annotations
public class PropertyTypeAttribute : Attribute
{
public Type[] Types { get; private set; }
public PropertyTypeAttribute(params Type[] types)
{
Types = types;
}
}
Create unit test:
[TestClass]
public class TestPropertyType
{
public static Type GetNullableUnderlying(Type nullableType)
{
return Nullable.GetUnderlyingType(nullableType) ?? nullableType;
}
[TestMethod]
public void Test_PropertyType()
{
var allTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes());
var allPropertyInfos = allTypes.SelectMany(a => a.GetProperties()).ToArray();
foreach (var propertyInfo in allPropertyInfos)
{
var propertyType = GetNullableUnderlying(propertyInfo.PropertyType);
foreach (var attribute in propertyInfo.GetCustomAttributes(true))
{
var attributes = attribute.GetType().GetCustomAttributes(true).OfType<PropertyTypeAttribute>();
foreach (var propertyTypeAttr in attributes)
if (!propertyTypeAttr.Types.Contains(propertyType))
throw new Exception(string.Format(
"Property '{0}.{1}' has invalid type: '{2}'. Allowed types for attribute '{3}': {4}",
propertyInfo.DeclaringType,
propertyInfo.Name,
propertyInfo.PropertyType,
attribute.GetType(),
string.Join(",", propertyTypeAttr.Types.Select(x => "'" + x.ToString() + "'"))));
}
}
}
}
Your attribute, for example allow only decimal property types:
[AttributeUsage(AttributeTargets.Property)]
[PropertyType(typeof(decimal))]
public class PriceAttribute : Attribute
{
}
Example model:
public class TestModel
{
[Price]
public decimal Price1 { get; set; } // ok
[Price]
public double Price2 { get; set; } // error
}
You could write code yourself to enforce correct use of your attribute class, but that's as much as you can do.
The code below will return an error if the attribute was placed on a property/field that is not List of string.
The line if (!(value is List<string> list)) may be a C#6 or 7 feature.
[AttributeUsage(AttributeTargets.Property |
AttributeTargets.Field, AllowMultiple = false)]
public sealed class RequiredStringListAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (!(value is List<string> list))
return new ValidationResult($"The required attrribute must be of type List<string>");
bool valid = false;
foreach (var item in list)
{
if (!string.IsNullOrWhiteSpace(item))
valid = true;
}
return valid
? ValidationResult.Success
: new ValidationResult($"This field is required"); ;
}
}
The way I am doing this is following:
[AttributeUsage(AttributeTargets.Property)]
public class SomeValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is not string stringToValidate)
{
throw new AttributeValueIsNotStringException(validationContext.DisplayName, validationContext.ObjectType.Name);
}
// validationContext.DisplayName is name of property, where validation attribut was used.
// validationContext.ObjectType.Name is name of class, in which the property is placed to instantly identify, where is the error.
//Some validation here.
return ValidationResult.Success;
}
}
And exception look like this:
public class AttributeValueIsNotStringException : Exception
{
public AttributeValueIsNotStringException(string propertyName, string className) : base(CreateMessage(propertyName, className))
{
}
private static string CreateMessage(string propertyName, string className)
{
return $"Validation attribute cannot be used for property: \"{propertyName}\" in class: \"{className}\" because it's type is not string. Use it only for string properties.";
}
}
I have the following code to validate an entity:
public class AffiliateValidator : AbstractValidator<Affiliate>
{
public AffiliateValidator ()
{
RuleFor(x => x.IBAN).SetValidator(new ValidIBAN()).Unless( x => String.IsNullOrWhiteSpace(x.IBAN));
}
}
And ValidIBAN() Code:
public class ValidIBAN : PropertyValidator
{
public ValidIBAN()
:base("IBAN \"{PropertyValue}\" not valid.")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var iban = context.PropertyValue as string;
IBAN.IBANResult result = IBAN.CheckIban(iban, false);
return result.Validation == (IBAN.ValidationResult.IsValid);
}
}
}
So,CheckIBAN method of IBAN class does the dirty job.
Now, what I need to to is apply the following rule for another property:
If DirectDebit (bool) is true, then IBAN can't be empty and also it must be valid.
I can do this:
RuleFor(x => x.DirectDebit).Equal(false).When(a => string.IsNullOrEmpty(a.IBAN)).WithMessage("TheMessage.");
But how can I Invoke another rule, IBAN's rule in this case, in order to check if is or not valid?
Often the problems are simpler than they seem. This is the solution I adopt to aply the rule for DirectDebit field.
RuleFor(x => x.DirectDebit).Must(HaveValidAccounts).When(x => x.DirectDebit)
.WithMessage("TheMessage");
and change the rule for IBAN also:
RuleFor(x => x.IBAN).Must(IsValidIBAN)
.Unless(x => String.IsNullOrWhiteSpace(x.IBAN))
.WithMessage("The IBAN \"{PropertyValue}\" is not valid.");
...and then:
private bool HaveValidAccounts(ViewModel instance, bool DirectDebit)
{
if (!DirectDebit)
{ return true; }
bool CCCResult = IsValidCCC(instance.CCC);
bool IBANResult = IsValidIBAN(instance.IBAN);
return CCCResult || IBANResult;
}
private bool IsValidIBAN(string iban)
{
return CommonInfrastructure.Finantial.IBAN.CheckIban(iban, false).Validation == IBAN.ValidationResult.IsValid;
}
the trick is use instance parameter of Must() to do whetever I want.