Допустим, у меня есть следующий интерфейс:
public interface IStateInformation
{
public int TaskId { get; set; }
public string Name { get; set; }
}
и следующий класс, реализующий этот интерфейс:
public class ExternalStateInformation : IStateInformation
{
public int TaskId { get; set; }
public string Name { get; set; }
public string External { get; set; }
}
Теперь я хочу написать следующий класс в CSV:
public class SwitchingData
{
public string DateTime { get; set; }
public int EpisodeId { get; set; }
public IStateInformation SourceState { get; set; }
public IStateInformation TargetState { get; set; }
}
Как видите, в этот класс входит IStateInformation объектов. Когда я пытаюсь записать запись в файл CSV, учитываются только свойства IStateInformation (TaskId и Name), даже если, например, SourceState имеет тип ExternalStateInformation (поэтому External отсутствует). Мой вопрос: как я могу написать свойства динамического типа вместо статического?
Я пытался использовать ClassMap, но безуспешно.
Подробности
Сценарий написания выглядит следующим образом:
...
using (var stream = File.Open(path, FileMode.Append))
using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(writer, config))
{
if (type != null)
{
csv.Context.RegisterClassMap(type);
}
csv.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
csv.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);
csv.WriteRecords(data);
}
...
где type был ClassMap, с которым я играл.
Спасибо, имеет смысл. В конце концов я изменил способ записи записей в CSV, чтобы решить свою проблему (см. мой ответ).





Это некрасиво, но кажется, что это ClassMap сработает.
public sealed class SwitchingDataClassMap : ClassMap<SwitchingData>
{
public SwitchingDataClassMap()
{
Map(m => m.DateTime).Index(0);
Map(m => m.EpisodeId).Index(1);
Map(m => m.SourceState.TaskId).Name("SourceStateTaskId").Index(2);
Map(m => m.SourceState.Name).Name("SourceStateName").Index(3);
Map(m => ((ExternalStateInformation)m.SourceState).External)
.Convert(args => args.Value.SourceState is ExternalStateInformation ?
((ExternalStateInformation)args.Value.SourceState).External :
null)
.Name("SourceStateExternal").Index(4);
Map(m => m.TargetState.TaskId).Name("TargetStateTaskId").Index(5);
Map(m => m.TargetState.Name).Name("TargetStateName").Index(6);
Map(m => ((ExternalStateInformation)m.TargetState).External)
.Convert(args => args.Value.TargetState is ExternalStateInformation ?
((ExternalStateInformation)args.Value.TargetState).External :
null)
.Name("TargetStateExternal").Index(7);
}
}
Спасибо, но это связано с классом ExternalStateInformation, но мне нужно универсальное решение, которое динамически печатает свойства любого класса, реализующего IStateInformation.
К сожалению, CsvHelper потребуется знать тип класса во время компиляции, чтобы сопоставить свойства. Вы можете выполнить сопоставление самостоятельно во время выполнения, используя отражение, но у меня такое ощущение, что вам придется пострадать от скорости.
В конце концов я написал свои собственные функции, записывающие заголовок и записи. Мой приведенный пример приведет к следующему заголовку:
DateTime,EpisodeId,SourceState_TaskId,SourceState_Name,SourceState_External,TargetState_TaskId,TargetState_Name,TargetState_External
Если бы TargetState реализовал интерфейс IStateInformation с другим дополнительным свойством, скажем Internal, заголовок выглядел бы так:
DateTime,EpisodeId,SourceState_TaskId,SourceState_Name,SourceState_External,TargetState_TaskId,TargetState_Name,TargetState_External,TargetState_Internal
Общая функция записи
private static void WriteToCSV<T>(string path, TypeConverterOptions options, List<T> data, FileMode mode)
{
using (var stream = File.Open(path, mode))
using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
csv.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);
List<Type> structure = GetStructureOfCSV(csv, data);
if (mode == FileMode.Create)
{
WriteHeaderToCSV(csv, data);
csv.NextRecord();
}
foreach (var record in data)
{
WriteRecordToCSV(csv, record, structure);
csv.NextRecord();
}
}
if (mode == FileMode.Create)
{
Debug.Log(String.Format("Write data to new file {0}", path));
}
else if (mode == FileMode.Append)
{
Debug.Log(String.Format("Write data to existing file {0}", path));
}
}
Функция для получения структуры объекта
Это необходимо для записи значений по умолчанию вместо нулевых значений, что приведет к уменьшению количества полей CSV.
private static List<Type> GetStructureOfCSV<T>(CsvWriter csv, List<T> data)
{
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
MemberInfo[] members = data[0].GetType().GetMemberInfos(bindingFlags);
List<Type> structure = new List<Type>
{
data[0].GetType()
};
foreach (var thisVar in members)
{
if (!HasCSVHelperBuiltInConverter(thisVar.GetUnderlyingType()))
{
try
{
List<T> records = data.Where(x => thisVar.GetValue(x) != null).ToList();
List<object> values = records.Select(x => thisVar.GetValue(x)).ToList();
structure = structure.Concat(GetStructureOfCSV(csv, values)).ToList();
}
catch (InvalidOperationException)
{
}
}
}
return structure;
}
Проверьте наличие ReferenceConverter
private static bool HasCSVHelperBuiltInConverter(Type type)
{
return TypeDescriptor.GetConverter(type).GetType() != typeof(ReferenceConverter);
}
Заголовок
private static void WriteHeaderToCSV<T>(CsvWriter csv, List<T> data, string prefix = "")
{
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
MemberInfo[] members = data[0].GetType().GetMemberInfos(bindingFlags);
foreach (var thisVar in members)
{
if (!HasCSVHelperBuiltInConverter(thisVar.GetUnderlyingType()))
{
try
{
List<T> records = data.Where(x => thisVar.GetValue(x) != null).ToList();
List<object> values = records.Select(x => thisVar.GetValue(x)).ToList();
WriteHeaderToCSV(csv, values, thisVar.Name);
}
catch (InvalidOperationException)
{
csv.WriteField(thisVar.Name);
}
}
else
{
if (prefix != "" && prefix != null)
{
csv.WriteField(string.Format("{0}_{1}", prefix, thisVar.Name));
}
else
{
csv.WriteField(thisVar.Name);
}
}
}
}
Рекорды
private static void WriteRecordToCSV(CsvWriter csv, object record, List<Type> structure)
{
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
MemberInfo[] members = record.GetType().GetMemberInfos(bindingFlags);
List<Type> local_structure = new List<Type>(structure);
local_structure.RemoveAt(0);
foreach (var thisVar in members)
{
if (!HasCSVHelperBuiltInConverter(thisVar.GetUnderlyingType()))
{
if (thisVar.GetValue(record) != null)
{
WriteRecordToCSV(csv, thisVar.GetValue(record), local_structure);
}
else
{
object instance = Activator.CreateInstance(local_structure.First());
WriteRecordToCSV(csv, instance, local_structure);
}
}
else
{
csv.WriteField(thisVar.GetValue(record));
}
}
}
Есть ли у вашего сериализатора CSV механизм, который можно использовать для предоставления ему метаданных? Если подумать, сделать что-то подобное было бы очень сложно. Если бы у вас была коллекция, включающая множество различных реализаций
IStateInformation, средству записи CSV нужно было бы получить метаданные из каждой из них, прежде чем записать первую строку вывода (чтобы оно знало, какие столбцы следует записать для каждой записи).