From 82fd15644038e3ade061f9be02a6e0066bdd2eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Severino?= Date: Fri, 20 May 2022 20:57:42 +0100 Subject: [PATCH 1/7] Replace constructor with System.Activator.CreateInstance CreateInstance will allow for parameterless constructor or a constructor with parameters to be used --- TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs b/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs index c005870..76abced 100644 --- a/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs +++ b/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs @@ -123,7 +123,10 @@ private void AddPropertyMapping(RangeDefinition range, CsvCollectionP public CsvMappingResult Map(TokenizedRow values) { - TEntity entity = new TEntity(); + List args = new List(); + // TODO build constructor arguments + + TEntity entity = (TEntity)Activator.CreateInstance(typeof(TEntity), args.ToArray()); // Iterate over Index Mappings: for (int pos = 0; pos < csvIndexPropertyMappings.Count; pos++) From 2bc49db283179db5905d781313bf89a37a041669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Severino?= Date: Sat, 21 May 2022 00:04:36 +0100 Subject: [PATCH 2/7] Minor refactor --- TinyCsvParser/TinyCsvParser/TypeConverter/ITypeConverter.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TinyCsvParser/TinyCsvParser/TypeConverter/ITypeConverter.cs b/TinyCsvParser/TinyCsvParser/TypeConverter/ITypeConverter.cs index feb9a75..1aa7f70 100644 --- a/TinyCsvParser/TinyCsvParser/TypeConverter/ITypeConverter.cs +++ b/TinyCsvParser/TinyCsvParser/TypeConverter/ITypeConverter.cs @@ -6,20 +6,16 @@ namespace TinyCsvParser.TypeConverter { public interface ITypeConverter { - + Type TargetType { get; } } public interface ITypeConverter : ITypeConverter { bool TryConvert(string value, out TTargetType result); - - Type TargetType { get; } } public interface IArrayTypeConverter : ITypeConverter { bool TryConvert(string[] value, out TTargetType result); - - Type TargetType { get; } } } From 5bc1d87d22e985e160a812be547f71f2043faad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Severino?= Date: Sat, 21 May 2022 00:30:25 +0100 Subject: [PATCH 3/7] Support for immutable types and map to constructor parameter Immutable types that are filled in from the constructor are now also possible to map from CSV data Syntax similar to existing MapProperty options --- .../Mapping/CsvCollectionPropertyMapping.cs | 2 +- .../TinyCsvParser/Mapping/CsvMapping.cs | 585 +++++++++++------- .../Mapping/CsvPropertyMapping.cs | 2 +- 3 files changed, 370 insertions(+), 219 deletions(-) diff --git a/TinyCsvParser/TinyCsvParser/Mapping/CsvCollectionPropertyMapping.cs b/TinyCsvParser/TinyCsvParser/Mapping/CsvCollectionPropertyMapping.cs index 08ec198..af981c4 100644 --- a/TinyCsvParser/TinyCsvParser/Mapping/CsvCollectionPropertyMapping.cs +++ b/TinyCsvParser/TinyCsvParser/Mapping/CsvCollectionPropertyMapping.cs @@ -6,7 +6,7 @@ namespace TinyCsvParser.Mapping { public class CsvCollectionPropertyMapping : ICsvPropertyMapping - where TEntity : class, new() + where TEntity : class { private readonly string propertyName; private readonly IArrayTypeConverter propertyConverter; diff --git a/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs b/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs index 76abced..e766085 100644 --- a/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs +++ b/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs @@ -10,221 +10,372 @@ namespace TinyCsvParser.Mapping { - public abstract class CsvMapping : ICsvMapping - where TEntity : class, new() - { - private class IndexToPropertyMapping - { - public int ColumnIndex { get; set; } - - public ICsvPropertyMapping PropertyMapping { get; set; } - - public override string ToString() - { - return $"IndexToPropertyMapping (ColumnIndex = {ColumnIndex}, PropertyMapping = {PropertyMapping}"; - } - } - - private class RangeToPropertyMapping - { - public RangeDefinition Range { get; set; } - - public ICsvPropertyMapping PropertyMapping { get; set; } - - public override string ToString() - { - return $"IndexToPropertyMapping (Range = {Range}, PropertyMapping = {PropertyMapping}"; - } - } - - - private readonly ITypeConverterProvider typeConverterProvider; - private readonly List csvIndexPropertyMappings; - private readonly List csvRangePropertyMappings; - private readonly List> csvRowMappings; - - protected CsvMapping() - : this(new TypeConverterProvider()) - { - } - - protected CsvMapping(ITypeConverterProvider typeConverterProvider) - { - this.typeConverterProvider = typeConverterProvider; - this.csvIndexPropertyMappings = new List(); - this.csvRangePropertyMappings = new List(); - this.csvRowMappings = new List>(); - } - - protected CsvRowMapping MapUsing(Func action) - { - var rowMapping = new CsvRowMapping(action); - - csvRowMappings.Add(rowMapping); - - return rowMapping; - } - - protected CsvPropertyMapping MapProperty(int columnIndex, Expression> property) - { - return MapProperty(columnIndex, property, typeConverterProvider.Resolve()); - } - - protected CsvCollectionPropertyMapping MapProperty(RangeDefinition range, Expression> property) - { - return MapProperty(range, property, typeConverterProvider.ResolveCollection()); - } - - protected CsvCollectionPropertyMapping MapProperty(RangeDefinition range, Expression> property, IArrayTypeConverter typeConverter) - { - var propertyMapping = new CsvCollectionPropertyMapping(property, typeConverter); - - AddPropertyMapping(range, propertyMapping); - - return propertyMapping; - } - - protected CsvPropertyMapping MapProperty(int columnIndex, Expression> property, ITypeConverter typeConverter) - { - if (csvIndexPropertyMappings.Any(x => x.ColumnIndex == columnIndex)) - { - throw new InvalidOperationException($"Duplicate mapping for column index {columnIndex}"); - } - - var propertyMapping = new CsvPropertyMapping(property, typeConverter); - - AddPropertyMapping(columnIndex, propertyMapping); - - return propertyMapping; - } - - - private void AddPropertyMapping(int columnIndex, CsvPropertyMapping propertyMapping) - { - var indexToPropertyMapping = new IndexToPropertyMapping - { - ColumnIndex = columnIndex, - PropertyMapping = propertyMapping - }; - - csvIndexPropertyMappings.Add(indexToPropertyMapping); - } - - private void AddPropertyMapping(RangeDefinition range, CsvCollectionPropertyMapping propertyMapping) - { - var rangeToPropertyMapping = new RangeToPropertyMapping - { - Range = range, - PropertyMapping = propertyMapping - }; - - csvRangePropertyMappings.Add(rangeToPropertyMapping); - } - - public CsvMappingResult Map(TokenizedRow values) - { - List args = new List(); - // TODO build constructor arguments - - TEntity entity = (TEntity)Activator.CreateInstance(typeof(TEntity), args.ToArray()); - - // Iterate over Index Mappings: - for (int pos = 0; pos < csvIndexPropertyMappings.Count; pos++) - { - var indexToPropertyMapping = csvIndexPropertyMappings[pos]; - - var columnIndex = indexToPropertyMapping.ColumnIndex; - - if (columnIndex >= values.Tokens.Length) - { - return new CsvMappingResult - { - RowIndex = values.Index, - Error = new CsvMappingError - { - ColumnIndex = columnIndex, - Value = $"Column {columnIndex} is Out Of Range", - UnmappedRow = string.Join("|", values.Tokens) - } - }; - } - - var value = values.Tokens[columnIndex]; - - if (!indexToPropertyMapping.PropertyMapping.TryMapValue(entity, value)) - { - return new CsvMappingResult - { - RowIndex = values.Index, - Error = new CsvMappingError - { - ColumnIndex = columnIndex, - Value = $"Column {columnIndex} with Value '{value}' cannot be converted", - UnmappedRow = string.Join("|", values.Tokens) - } - }; - } - } - - // Iterate over Range Mappings: - for (int pos = 0; pos < csvRangePropertyMappings.Count; pos++) - { - var rangeToPropertyMapping = csvRangePropertyMappings[pos]; - - var range = rangeToPropertyMapping.Range; - - // Copy the Sub Array. This needs optimization, like ReadOnlyMemory! - var slice = values.Tokens.Skip(range.Start).Take(range.Length).ToArray(); - - if (!rangeToPropertyMapping.PropertyMapping.TryMapValue(entity, slice)) - { - var columnIndex = range.Start; - - return new CsvMappingResult - { - RowIndex = values.Index, - Error = new CsvMappingError - { - ColumnIndex = columnIndex, - Value = $"Range with Start Index {range.Start} and End Index {range.End} cannot be converted!", - UnmappedRow = string.Join("|", values.Tokens) - } - }; - } - } - - // Iterate over Row Mappings. At this point previous values for the entity - // should be set: - for(int pos = 0; pos < csvRowMappings.Count; pos++) - { - var csvRowMapping = csvRowMappings[pos]; - - if(!csvRowMapping.TryMapValue(entity, values)) - { - return new CsvMappingResult - { - RowIndex = values.Index, - Error = new CsvMappingError - { - Value = $"Row could not be mapped!", - UnmappedRow = string.Join("|", values.Tokens) - } - }; - } - } - - return new CsvMappingResult - { - RowIndex = values.Index, - Result = entity - }; - } - - public override string ToString() - { - var csvPropertyMappingsString = string.Join(", ", csvIndexPropertyMappings.Select(x => x.ToString())); - - return $"CsvMapping (TypeConverterProvider = {typeConverterProvider}, Mappings = {csvPropertyMappingsString})"; - } - } + public abstract class CsvMapping : ICsvMapping + where TEntity : class + { + private abstract class IndexToConstructorParameterMapping + { + public int ColumnIndex { get; set; } + + public int ConstructorIndex { get; set; } + + public abstract bool TryMapValue(string value, out object result); + + public override string ToString() + { + return $"IndexToConstructorParameterMapping (ColumnIndex = {ColumnIndex}, ConstructorIndex = {ConstructorIndex}"; + } + } + + private class IndexToConstructorParameterMapping : IndexToConstructorParameterMapping + { + public ITypeConverter ValueMapping { get; set; } + + public override bool TryMapValue(string value, out object result) + { + bool success = ValueMapping.TryConvert(value, out TValue tmp); + result = tmp; + return success; + } + } + + private abstract class RangeToConstructorParameterMapping + { + public RangeDefinition Range { get; set; } + + public int ConstructorIndex { get; set; } + + public abstract bool TryMapValue(string[] value, out object result); + + public override string ToString() + { + return $"IndexToPropertyMapping (Range = {Range}, ConstructorIndex = {ConstructorIndex}"; + } + } + + private class RangeToConstructorParameterMapping : RangeToConstructorParameterMapping + { + public IArrayTypeConverter ValueMapping { get; set; } + + public override bool TryMapValue(string[] value, out object result) + { + bool success = ValueMapping.TryConvert(value, out TValue tmp); + result = tmp; + return success; + } + } + + private class IndexToPropertyMapping + { + public int ColumnIndex { get; set; } + + public ICsvPropertyMapping PropertyMapping { get; set; } + + public override string ToString() + { + return $"IndexToPropertyMapping (ColumnIndex = {ColumnIndex}, PropertyMapping = {PropertyMapping}"; + } + } + + private class RangeToPropertyMapping + { + public RangeDefinition Range { get; set; } + + public ICsvPropertyMapping PropertyMapping { get; set; } + + public override string ToString() + { + return $"IndexToPropertyMapping (Range = {Range}, PropertyMapping = {PropertyMapping}"; + } + } + + private readonly Type entityType = typeof(TEntity); + + private readonly ITypeConverterProvider typeConverterProvider; + private readonly List csvIndexConstructorMappings; + private readonly List csvRangeConstructorMappings; + private readonly List csvIndexPropertyMappings; + private readonly List csvRangePropertyMappings; + private readonly List> csvRowMappings; + + protected CsvMapping() + : this(new TypeConverterProvider()) + { + } + + protected CsvMapping(ITypeConverterProvider typeConverterProvider) + { + this.typeConverterProvider = typeConverterProvider; + this.csvIndexConstructorMappings = new List(); + this.csvRangeConstructorMappings = new List(); + this.csvIndexPropertyMappings = new List(); + this.csvRangePropertyMappings = new List(); + this.csvRowMappings = new List>(); + } + + protected CsvRowMapping MapUsing(Func action) + { + var rowMapping = new CsvRowMapping(action); + + csvRowMappings.Add(rowMapping); + + return rowMapping; + } + + protected void MapConstructorParameter(int columnIndex, int constructorIndex) + { + MapConstructorParameter(columnIndex, constructorIndex, typeConverterProvider.Resolve()); + } + + protected void MapConstructorParameter(RangeDefinition range, int constructorIndex) + { + MapConstructorParameter(range, constructorIndex, typeConverterProvider.ResolveCollection()); + } + + protected void MapConstructorParameter(int columnIndex, int constructorIndex, ITypeConverter typeConverter) + { + csvIndexConstructorMappings.Add(new IndexToConstructorParameterMapping + { + ColumnIndex = columnIndex, + ConstructorIndex = constructorIndex, + ValueMapping = typeConverter + }); + } + + protected void MapConstructorParameter(RangeDefinition range, int constructorIndex, IArrayTypeConverter typeConverter) + { + csvRangeConstructorMappings.Add(new RangeToConstructorParameterMapping + { + Range = range, + ConstructorIndex = constructorIndex, + ValueMapping = typeConverter + }); + } + + protected CsvPropertyMapping MapProperty(int columnIndex, Expression> property) + { + return MapProperty(columnIndex, property, typeConverterProvider.Resolve()); + } + + protected CsvCollectionPropertyMapping MapProperty(RangeDefinition range, Expression> property) + { + return MapProperty(range, property, typeConverterProvider.ResolveCollection()); + } + + protected CsvCollectionPropertyMapping MapProperty(RangeDefinition range, Expression> property, IArrayTypeConverter typeConverter) + { + var propertyMapping = new CsvCollectionPropertyMapping(property, typeConverter); + + AddPropertyMapping(range, propertyMapping); + + return propertyMapping; + } + + protected CsvPropertyMapping MapProperty(int columnIndex, Expression> property, ITypeConverter typeConverter) + { + if (csvIndexPropertyMappings.Any(x => x.ColumnIndex == columnIndex)) + { + throw new InvalidOperationException($"Duplicate mapping for column index {columnIndex}"); + } + + var propertyMapping = new CsvPropertyMapping(property, typeConverter); + + AddPropertyMapping(columnIndex, propertyMapping); + + return propertyMapping; + } + + + private void AddPropertyMapping(int columnIndex, CsvPropertyMapping propertyMapping) + { + var indexToPropertyMapping = new IndexToPropertyMapping + { + ColumnIndex = columnIndex, + PropertyMapping = propertyMapping + }; + + csvIndexPropertyMappings.Add(indexToPropertyMapping); + } + + private void AddPropertyMapping(RangeDefinition range, CsvCollectionPropertyMapping propertyMapping) + { + var rangeToPropertyMapping = new RangeToPropertyMapping + { + Range = range, + PropertyMapping = propertyMapping + }; + + csvRangePropertyMappings.Add(rangeToPropertyMapping); + } + + public CsvMappingResult Map(TokenizedRow values) + { + int mappedConstructorParameterCount = csvIndexConstructorMappings.Count + csvRangeConstructorMappings.Count; + object[] args = null; + if (mappedConstructorParameterCount > 0) + { + args = new object[mappedConstructorParameterCount]; + foreach (var indexToConstructorMapping in csvIndexConstructorMappings) + { + var columnIndex = indexToConstructorMapping.ColumnIndex; + + if (columnIndex >= values.Tokens.Length) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = columnIndex, + Value = $"Column {columnIndex} is Out Of Range", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + + var value = values.Tokens[columnIndex]; + + if (!indexToConstructorMapping.TryMapValue(value, out object result)) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = columnIndex, + Value = $"Column {columnIndex} with Value '{value}' cannot be converted", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + + args[indexToConstructorMapping.ConstructorIndex] = result; + } + + foreach (var rangeToConstructorMapping in csvRangeConstructorMappings) + { + var range = rangeToConstructorMapping.Range; + + // Copy the Sub Array. This needs optimization, like ReadOnlyMemory! + var slice = values.Tokens.Skip(range.Start).Take(range.Length).ToArray(); + + if (!rangeToConstructorMapping.TryMapValue(slice, out object result)) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = range.Start, + Value = $"Range with Start Index {range.Start} and End Index {range.End} cannot be converted!", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + + args[rangeToConstructorMapping.ConstructorIndex] = result; + } + } + + TEntity entity = (TEntity)Activator.CreateInstance(entityType, args); + + // Iterate over Index Mappings: + for (int pos = 0; pos < csvIndexPropertyMappings.Count; pos++) + { + var indexToPropertyMapping = csvIndexPropertyMappings[pos]; + + var columnIndex = indexToPropertyMapping.ColumnIndex; + + if (columnIndex >= values.Tokens.Length) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = columnIndex, + Value = $"Column {columnIndex} is Out Of Range", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + + var value = values.Tokens[columnIndex]; + + if (!indexToPropertyMapping.PropertyMapping.TryMapValue(entity, value)) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = columnIndex, + Value = $"Column {columnIndex} with Value '{value}' cannot be converted", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + } + + // Iterate over Range Mappings: + for (int pos = 0; pos < csvRangePropertyMappings.Count; pos++) + { + var rangeToPropertyMapping = csvRangePropertyMappings[pos]; + + var range = rangeToPropertyMapping.Range; + + // Copy the Sub Array. This needs optimization, like ReadOnlyMemory! + var slice = values.Tokens.Skip(range.Start).Take(range.Length).ToArray(); + + if (!rangeToPropertyMapping.PropertyMapping.TryMapValue(entity, slice)) + { + var columnIndex = range.Start; + + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = columnIndex, + Value = $"Range with Start Index {range.Start} and End Index {range.End} cannot be converted!", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + } + + // Iterate over Row Mappings. At this point previous values for the entity + // should be set: + for (int pos = 0; pos < csvRowMappings.Count; pos++) + { + var csvRowMapping = csvRowMappings[pos]; + + if (!csvRowMapping.TryMapValue(entity, values)) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + Value = $"Row could not be mapped!", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + } + + return new CsvMappingResult + { + RowIndex = values.Index, + Result = entity + }; + } + + public override string ToString() + { + var csvPropertyMappingsString = string.Join(", ", csvIndexPropertyMappings.Select(x => x.ToString())); + + return $"CsvMapping (TypeConverterProvider = {typeConverterProvider}, Mappings = {csvPropertyMappingsString})"; + } + } } \ No newline at end of file diff --git a/TinyCsvParser/TinyCsvParser/Mapping/CsvPropertyMapping.cs b/TinyCsvParser/TinyCsvParser/Mapping/CsvPropertyMapping.cs index fb28f1d..23df3af 100644 --- a/TinyCsvParser/TinyCsvParser/Mapping/CsvPropertyMapping.cs +++ b/TinyCsvParser/TinyCsvParser/Mapping/CsvPropertyMapping.cs @@ -8,7 +8,7 @@ namespace TinyCsvParser.Mapping { public class CsvPropertyMapping : ICsvPropertyMapping - where TEntity : class, new() + where TEntity : class { private readonly string propertyName; private readonly ITypeConverter propertyConverter; From d96d345bfe1584867c8d8f4f52e6277ee12a14f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Severino?= Date: Sat, 21 May 2022 11:30:50 +0100 Subject: [PATCH 4/7] Add Visual Studio gitignore --- .gitignore | 350 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f298ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,350 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ \ No newline at end of file From 82af36ce8acb6051fe670a3047d81866c75c73da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Severino?= Date: Sat, 21 May 2022 11:42:52 +0100 Subject: [PATCH 5/7] Support for immutable types with MapUsing style syntax Add also support for MapUsing style syntax for immutable types. Multiple MapUsing can be configured and the first that succeeds will be used, if no MapUsing is configured or none succeeeds then it will default to the Activator implementation. --- .../TinyCsvParser/Mapping/CsvMapping.cs | 162 +++++++++++------- .../Mapping/CsvRowConstructor.cs | 21 +++ .../TinyCsvParser/Ranges/RangeDefinition.cs | 9 + 3 files changed, 129 insertions(+), 63 deletions(-) create mode 100644 TinyCsvParser/TinyCsvParser/Mapping/CsvRowConstructor.cs diff --git a/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs b/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs index e766085..636e388 100644 --- a/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs +++ b/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs @@ -92,6 +92,7 @@ public override string ToString() private readonly Type entityType = typeof(TEntity); private readonly ITypeConverterProvider typeConverterProvider; + private readonly List> csvUsingConstructorMappings; private readonly List csvIndexConstructorMappings; private readonly List csvRangeConstructorMappings; private readonly List csvIndexPropertyMappings; @@ -106,6 +107,7 @@ protected CsvMapping() protected CsvMapping(ITypeConverterProvider typeConverterProvider) { this.typeConverterProvider = typeConverterProvider; + this.csvUsingConstructorMappings = new List>(); this.csvIndexConstructorMappings = new List(); this.csvRangeConstructorMappings = new List(); this.csvIndexPropertyMappings = new List(); @@ -113,6 +115,15 @@ protected CsvMapping(ITypeConverterProvider typeConverterProvider) this.csvRowMappings = new List>(); } + protected CsvRowConstructor MapUsing(Func action) + { + var rowConstructor = new CsvRowConstructor(action); + + csvUsingConstructorMappings.Add(rowConstructor); + + return rowConstructor; + } + protected CsvRowMapping MapUsing(Func action) { var rowMapping = new CsvRowMapping(action); @@ -210,74 +221,25 @@ private void AddPropertyMapping(RangeDefinition range, CsvCollectionP public CsvMappingResult Map(TokenizedRow values) { - int mappedConstructorParameterCount = csvIndexConstructorMappings.Count + csvRangeConstructorMappings.Count; - object[] args = null; - if (mappedConstructorParameterCount > 0) + TEntity entity = null; + foreach (var csvRowConstructor in csvUsingConstructorMappings) { - args = new object[mappedConstructorParameterCount]; - foreach (var indexToConstructorMapping in csvIndexConstructorMappings) + if(csvRowConstructor.TryMapValue(values, out entity)) { - var columnIndex = indexToConstructorMapping.ColumnIndex; - - if (columnIndex >= values.Tokens.Length) - { - return new CsvMappingResult - { - RowIndex = values.Index, - Error = new CsvMappingError - { - ColumnIndex = columnIndex, - Value = $"Column {columnIndex} is Out Of Range", - UnmappedRow = string.Join("|", values.Tokens) - } - }; - } - - var value = values.Tokens[columnIndex]; - - if (!indexToConstructorMapping.TryMapValue(value, out object result)) - { - return new CsvMappingResult - { - RowIndex = values.Index, - Error = new CsvMappingError - { - ColumnIndex = columnIndex, - Value = $"Column {columnIndex} with Value '{value}' cannot be converted", - UnmappedRow = string.Join("|", values.Tokens) - } - }; - } - - args[indexToConstructorMapping.ConstructorIndex] = result; + break; } + } - foreach (var rangeToConstructorMapping in csvRangeConstructorMappings) + if (entity == null) + { + var activatorResult = MapFromConstructorMappings(values); + if (!activatorResult.IsValid) { - var range = rangeToConstructorMapping.Range; - - // Copy the Sub Array. This needs optimization, like ReadOnlyMemory! - var slice = values.Tokens.Skip(range.Start).Take(range.Length).ToArray(); - - if (!rangeToConstructorMapping.TryMapValue(slice, out object result)) - { - return new CsvMappingResult - { - RowIndex = values.Index, - Error = new CsvMappingError - { - ColumnIndex = range.Start, - Value = $"Range with Start Index {range.Start} and End Index {range.End} cannot be converted!", - UnmappedRow = string.Join("|", values.Tokens) - } - }; - } - - args[rangeToConstructorMapping.ConstructorIndex] = result; + return activatorResult; } - } - TEntity entity = (TEntity)Activator.CreateInstance(entityType, args); + entity = activatorResult.Result; + } // Iterate over Index Mappings: for (int pos = 0; pos < csvIndexPropertyMappings.Count; pos++) @@ -324,8 +286,7 @@ public CsvMappingResult Map(TokenizedRow values) var range = rangeToPropertyMapping.Range; - // Copy the Sub Array. This needs optimization, like ReadOnlyMemory! - var slice = values.Tokens.Skip(range.Start).Take(range.Length).ToArray(); + var slice = range.GetSlice(values); if (!rangeToPropertyMapping.PropertyMapping.TryMapValue(entity, slice)) { @@ -371,6 +332,81 @@ public CsvMappingResult Map(TokenizedRow values) }; } + private CsvMappingResult MapFromConstructorMappings(TokenizedRow values) + { + int mappedConstructorParameterCount = csvIndexConstructorMappings.Count + csvRangeConstructorMappings.Count; + object[] args = null; + if (mappedConstructorParameterCount > 0) + { + args = new object[mappedConstructorParameterCount]; + foreach (var indexToConstructorMapping in csvIndexConstructorMappings) + { + var columnIndex = indexToConstructorMapping.ColumnIndex; + + if (columnIndex >= values.Tokens.Length) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = columnIndex, + Value = $"Column {columnIndex} is Out Of Range", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + + var value = values.Tokens[columnIndex]; + + if (!indexToConstructorMapping.TryMapValue(value, out object result)) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = columnIndex, + Value = $"Column {columnIndex} with Value '{value}' cannot be converted", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + + args[indexToConstructorMapping.ConstructorIndex] = result; + } + + foreach (var rangeToConstructorMapping in csvRangeConstructorMappings) + { + var range = rangeToConstructorMapping.Range; + var slice = range.GetSlice(values); + + if (!rangeToConstructorMapping.TryMapValue(slice, out object result)) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + ColumnIndex = range.Start, + Value = $"Range with Start Index {range.Start} and End Index {range.End} cannot be converted!", + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } + + args[rangeToConstructorMapping.ConstructorIndex] = result; + } + } + + TEntity entity = (TEntity)Activator.CreateInstance(entityType, args); + return new CsvMappingResult + { + RowIndex = values.Index, + Result = entity + }; + } + public override string ToString() { var csvPropertyMappingsString = string.Join(", ", csvIndexPropertyMappings.Select(x => x.ToString())); diff --git a/TinyCsvParser/TinyCsvParser/Mapping/CsvRowConstructor.cs b/TinyCsvParser/TinyCsvParser/Mapping/CsvRowConstructor.cs new file mode 100644 index 0000000..410e567 --- /dev/null +++ b/TinyCsvParser/TinyCsvParser/Mapping/CsvRowConstructor.cs @@ -0,0 +1,21 @@ +namespace TinyCsvParser.Mapping +{ + using System; + using TinyCsvParser.Model; + + public class CsvRowConstructor + { + private readonly Func action; + + public CsvRowConstructor(Func action) + { + this.action = action; + } + + public bool TryMapValue(TokenizedRow value, out TEntity entity) + { + entity = action(value); + return entity != null; + } + } +} diff --git a/TinyCsvParser/TinyCsvParser/Ranges/RangeDefinition.cs b/TinyCsvParser/TinyCsvParser/Ranges/RangeDefinition.cs index d07a09c..a37d289 100644 --- a/TinyCsvParser/TinyCsvParser/Ranges/RangeDefinition.cs +++ b/TinyCsvParser/TinyCsvParser/Ranges/RangeDefinition.cs @@ -1,6 +1,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Linq; +using TinyCsvParser.Model; namespace TinyCsvParser.Ranges { @@ -28,5 +30,12 @@ public RangeDefinition(int start, int end) { return Tuple.Create(Start, End - Start); } + + public string[] GetSlice(TokenizedRow row) + { + // Copy the Sub Array. This needs optimization, like ReadOnlyMemory! + var slice = row.Tokens.Skip(Start).Take(Length).ToArray(); + return slice; + } } } From f0a6af114c24f99beaee18741199276a024d2e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Severino?= Date: Sat, 21 May 2022 12:08:16 +0100 Subject: [PATCH 6/7] Add Unit Tests for MapConstructorParameter --- .../CsvParser/CsvParserImmutableTest.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 TinyCsvParser/TinyCsvParser.Test/CsvParser/CsvParserImmutableTest.cs diff --git a/TinyCsvParser/TinyCsvParser.Test/CsvParser/CsvParserImmutableTest.cs b/TinyCsvParser/TinyCsvParser.Test/CsvParser/CsvParserImmutableTest.cs new file mode 100644 index 0000000..d4e6c66 --- /dev/null +++ b/TinyCsvParser/TinyCsvParser.Test/CsvParser/CsvParserImmutableTest.cs @@ -0,0 +1,87 @@ +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NUnit.Framework; +using System; +using System.Linq; +using System.Text; +using TinyCsvParser.Mapping; +using TinyCsvParser.Ranges; + +namespace TinyCsvParser.Test.CsvParser +{ + [TestFixture] + public class CsvParserImmutableTest + { + private class Measurement + { + public string Id { get; set; } + + public float[] Values { get; set; } + + public string ReadOnlyId { get; } + + public int[] ReadOnlyArray { get; } + + public Measurement(string readOnlyId, int[] readOnlyArray) + { + ReadOnlyId = readOnlyId; + ReadOnlyArray = readOnlyArray; + } + } + + private class CsvMeasurementMapping : CsvMapping + { + public CsvMeasurementMapping() + { + MapProperty(0, x => x.Id); + MapProperty(new RangeDefinition(1, 2), x => x.Values); + MapConstructorParameter(3, 0); + MapConstructorParameter(new RangeDefinition(4, 6), 1); + } + } + + [Test] + public void MapConstructorParametersTest() + { + CsvParserOptions csvParserOptions = new CsvParserOptions(false, ';'); + CsvReaderOptions csvReaderOptions = new CsvReaderOptions(new[] { Environment.NewLine }); + CsvMeasurementMapping csvMapper = new CsvMeasurementMapping(); + CsvParser csvParser = new CsvParser(csvParserOptions, csvMapper); + + + var stringBuilder = new StringBuilder() + .AppendLine("Device1;1.0;2.0;DeviceBrand1;0;1;2") + .AppendLine("Device2;3.0;4.0;DeviceBrand2;0;3;4"); + + var result = csvParser + .ReadFromString(csvReaderOptions, stringBuilder.ToString()) + .ToList(); + + Assert.AreEqual(2, result.Count); + + Assert.IsTrue(result.All(x => x.IsValid)); + + Assert.AreEqual("Device1", result[0].Result.Id); + Assert.IsNotNull(result[0].Result.Values); + Assert.AreEqual(2, result[0].Result.Values.Length); + Assert.AreEqual(1.0, result[0].Result.Values[0]); + Assert.AreEqual(2.0, result[0].Result.Values[1]); + Assert.AreEqual("DeviceBrand1", result[0].Result.ReadOnlyId); + Assert.AreEqual(3, result[0].Result.ReadOnlyArray.Length); + Assert.AreEqual(0, result[0].Result.ReadOnlyArray[0]); + Assert.AreEqual(1, result[0].Result.ReadOnlyArray[1]); + Assert.AreEqual(2, result[0].Result.ReadOnlyArray[2]); + + Assert.AreEqual("Device2", result[1].Result.Id); + Assert.IsNotNull(result[1].Result.Values); + Assert.AreEqual(2, result[1].Result.Values.Length); + Assert.AreEqual(3.0, result[1].Result.Values[0]); + Assert.AreEqual(4.0, result[1].Result.Values[1]); + Assert.AreEqual("DeviceBrand2", result[1].Result.ReadOnlyId); + Assert.AreEqual(3, result[1].Result.ReadOnlyArray.Length); + Assert.AreEqual(0, result[1].Result.ReadOnlyArray[0]); + Assert.AreEqual(3, result[1].Result.ReadOnlyArray[1]); + Assert.AreEqual(4, result[1].Result.ReadOnlyArray[2]); + } + } +} From 0db13e5840d3eaf7e55be726e25eac4385e409a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Severino?= Date: Sat, 21 May 2022 12:36:59 +0100 Subject: [PATCH 7/7] Add protection in Activator and Unit Tests for MapUsing --- .../CsvParser/MapConstructorUsingTest.cs | 149 ++++++++++++++++++ .../TinyCsvParser/Mapping/CsvMapping.cs | 25 ++- 2 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 TinyCsvParser/TinyCsvParser.Test/CsvParser/MapConstructorUsingTest.cs diff --git a/TinyCsvParser/TinyCsvParser.Test/CsvParser/MapConstructorUsingTest.cs b/TinyCsvParser/TinyCsvParser.Test/CsvParser/MapConstructorUsingTest.cs new file mode 100644 index 0000000..49568f5 --- /dev/null +++ b/TinyCsvParser/TinyCsvParser.Test/CsvParser/MapConstructorUsingTest.cs @@ -0,0 +1,149 @@ +using NUnit.Framework; +using System; +using System.Linq; +using System.Text; +using TinyCsvParser.Mapping; + +namespace TinyCsvParser.Test.CsvParser +{ + [TestFixture] + public class MapConstructorUsingTest + { + private class MainClass + { + public MainClass(SubClass subClass) + { + SubClass = subClass; + } + + public MainClass(int property2) + { + Property2 = property2; + } + + public string Property1 { get; set; } + + public SubClass SubClass { get; } + + public int Property2 { get; } + } + + private class SubClass + { + public string Property3 { get; set; } + + public string Property4 { get; set; } + } + + private class CsvMainClassMapping : CsvMapping + { + public CsvMainClassMapping() + { + MapProperty(0, x => x.Property1); + MapUsing((values) => + { + // Example of invalidating the row based on its contents + if (values.Tokens.Any(t => t == "Z")) + { + return null; + } + + var subClass = new SubClass(); + + subClass.Property3 = values.Tokens[1]; + subClass.Property4 = values.Tokens[2]; + + var entity = new MainClass(subClass); + + return entity; + }); + } + } + + private class CsvMainClassMappingDefaultToActivator : CsvMapping + { + public CsvMainClassMappingDefaultToActivator() + { + MapProperty(0, x => x.Property1); + MapUsing((values) => + { + // Example of invalidating the row based on its contents + if (values.Tokens.Any(t => t == "Z")) + { + return null; + } + + var subClass = new SubClass(); + + subClass.Property3 = values.Tokens[1]; + subClass.Property4 = values.Tokens[2]; + + var entity = new MainClass(subClass); + + return entity; + }); + + MapConstructorParameter(3, 0); + } + } + + [Test] + public void MapUsingTest() + { + CsvParserOptions csvParserOptions = new CsvParserOptions(false, ';' ); + CsvReaderOptions csvReaderOptions = new CsvReaderOptions(new[] { Environment.NewLine }); + CsvMainClassMapping csvMapper = new CsvMainClassMapping(); + CsvParser csvParser = new CsvParser(csvParserOptions, csvMapper); + + var stringBuilder = new StringBuilder() + .AppendLine("X;Y;Z;4") + .AppendLine("A;B;C;5"); + + var result = csvParser + .ReadFromString(csvReaderOptions, stringBuilder.ToString()) + .ToList(); + + Assert.AreEqual(2, result.Count); + + Assert.IsFalse(result[0].IsValid); + Assert.IsTrue(result[1].IsValid); + + Assert.AreEqual("A", result[1].Result.Property1); + + Assert.IsNotNull(result[1].Result.SubClass); + + Assert.AreEqual("B", result[1].Result.SubClass.Property3); + Assert.AreEqual("C", result[1].Result.SubClass.Property4); + } + + [Test] + public void MapUsingDefaultToActivatorTest() + { + CsvParserOptions csvParserOptions = new CsvParserOptions(false, ';'); + CsvReaderOptions csvReaderOptions = new CsvReaderOptions(new[] { Environment.NewLine }); + CsvMainClassMappingDefaultToActivator csvMapper = new CsvMainClassMappingDefaultToActivator(); + CsvParser csvParser = new CsvParser(csvParserOptions, csvMapper); + + var stringBuilder = new StringBuilder() + .AppendLine("X;Y;Z;4") + .AppendLine("A;B;C;5"); + + var result = csvParser + .ReadFromString(csvReaderOptions, stringBuilder.ToString()) + .ToList(); + + Assert.AreEqual(2, result.Count); + + Assert.IsTrue(result[0].IsValid); + Assert.AreEqual("X", result[0].Result.Property1); + Assert.IsNull(result[0].Result.SubClass); + Assert.AreEqual(4, result[0].Result.Property2); + + Assert.IsTrue(result[1].IsValid); + Assert.AreEqual("A", result[1].Result.Property1); + Assert.IsNotNull(result[1].Result.SubClass); + Assert.AreEqual("B", result[1].Result.SubClass.Property3); + Assert.AreEqual("C", result[1].Result.SubClass.Property4); + } + } +} diff --git a/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs b/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs index 636e388..0632e60 100644 --- a/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs +++ b/TinyCsvParser/TinyCsvParser/Mapping/CsvMapping.cs @@ -399,12 +399,27 @@ private CsvMappingResult MapFromConstructorMappings(TokenizedRow values } } - TEntity entity = (TEntity)Activator.CreateInstance(entityType, args); - return new CsvMappingResult + try { - RowIndex = values.Index, - Result = entity - }; + TEntity entity = (TEntity)Activator.CreateInstance(entityType, args); + return new CsvMappingResult + { + RowIndex = values.Index, + Result = entity + }; + } + catch (Exception e) + { + return new CsvMappingResult + { + RowIndex = values.Index, + Error = new CsvMappingError + { + Value = e.Message, + UnmappedRow = string.Join("|", values.Tokens) + } + }; + } } public override string ToString()