From 99ede3f21d3545710b5945f21e24b34a4d7566f7 Mon Sep 17 00:00:00 2001 From: Fabian918 <70378443+Fabian918@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:13:00 +0100 Subject: [PATCH 1/2] Add Functionallity to specify properties --- CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs | 6 ++++++ CsvPortable/CsvPortable/Configuration/PropertyMode.cs | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs create mode 100644 CsvPortable/CsvPortable/Configuration/PropertyMode.cs diff --git a/CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs b/CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs new file mode 100644 index 0000000..23d9db8 --- /dev/null +++ b/CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs @@ -0,0 +1,6 @@ +namespace CsvPortable.Tests; + +public class PropertySelectionTest +{ + +} \ No newline at end of file diff --git a/CsvPortable/CsvPortable/Configuration/PropertyMode.cs b/CsvPortable/CsvPortable/Configuration/PropertyMode.cs new file mode 100644 index 0000000..d42ce28 --- /dev/null +++ b/CsvPortable/CsvPortable/Configuration/PropertyMode.cs @@ -0,0 +1,6 @@ +namespace CsvPortable.Configuration; + +public class PropertyMode +{ + +} \ No newline at end of file From af9d7bdbc9c07d4110c2108d13b38f7050100bc6 Mon Sep 17 00:00:00 2001 From: Fabian918 <70378443+Fabian918@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:46:42 +0100 Subject: [PATCH 2/2] -implemented explicit property selection --- .../PropertySelectionTest.cs | 126 +++++++++++++- .../CsvPortable/Configuration/CsvParameter.cs | 155 ++++++++---------- .../CsvPortable/Configuration/PropertyMode.cs | 5 +- .../CsvPortable/Interfaces/ICsvPortable.cs | 62 +++++-- .../CreateReportsFromExistingModels.md | 68 ++++++++ README.md | 2 + 6 files changed, 315 insertions(+), 103 deletions(-) create mode 100644 Documentation/CreateReportsFromExistingModels.md diff --git a/CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs b/CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs index 23d9db8..81b4080 100644 --- a/CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs +++ b/CsvPortable/CsvPortable.Tests/PropertySelectionTest.cs @@ -1,6 +1,130 @@ namespace CsvPortable.Tests; +using System.Globalization; +using Attributes; +using Configuration; +using Interfaces; + public class PropertySelectionTest { - + [Theory] + [InlineData(null)] + [InlineData(PropertyMode.Explicit)] + [InlineData(PropertyMode.All)] + public void PropertySelectionModeInParameter(PropertyMode? mode) + { + void AssertParameter(CsvParameter parameter, PropertyMode? expectedMode) + { + Assert.Equal(expectedMode, parameter.PropertyMode); + } + + AssertParameter(new CsvParameter(propertyMode: mode), mode ?? CsvParameter.DefaultPropertyMode); + } + + + [Theory] + [InlineData(PropertyMode.All)] + [InlineData(PropertyMode.Explicit)] + public void ImportExportDataPropertyMode(PropertyMode mode) + { + var objectToExport = new PropertySelectionSpecifiedTestDto() + { + Property1 = 100, + Property2 = "SomethingElse", + Property3 = true, + Property4 = DateTime.Parse("1999.12.12") + }; + + CsvParameter parameter = new CsvParameter(propertyMode: mode); + + + void AssertExportedString(string definition, string expectedString, bool shouldBeContained = true) + { + if (shouldBeContained) + { + Assert.Contains(expectedString, definition); + } + else + { + Assert.DoesNotContain(expectedString, definition); + } + } + + // Assert Definition. + // Property1 and Property3 should be always in the definition. + // Property2 and Property4 should be in the definition only if mode is All. + + AssertExportedString( + ICsvPortable.ExportDefinition(parameter), + nameof(PropertySelectionSpecifiedTestDto.Property1)); + + + AssertExportedString( + ICsvPortable.ExportDefinition(parameter), + nameof(PropertySelectionSpecifiedTestDto.Property2), + mode == PropertyMode.All); + + AssertExportedString( + ICsvPortable.ExportDefinition(parameter), + nameof(PropertySelectionSpecifiedTestDto.Property3)); + + AssertExportedString( + ICsvPortable.ExportDefinition(parameter), + nameof(PropertySelectionSpecifiedTestDto.Property4), + mode == PropertyMode.All); + + // Assert Exported csv string. + // Property1 and Property3 values should be always exported. + // Property2 and Property4 should be exported only if mode is All. + + AssertExportedString( + ICsvPortable.ExportToCsvLine(objectToExport, parameter), + objectToExport.Property1.ToString()); + + + AssertExportedString( + ICsvPortable.ExportToCsvLine(objectToExport, parameter), + objectToExport.Property2, + mode == PropertyMode.All); + + AssertExportedString( + ICsvPortable.ExportToCsvLine(objectToExport, parameter), + objectToExport.Property3.ToString()); + + AssertExportedString( + ICsvPortable.ExportToCsvLine(objectToExport, parameter), + objectToExport.Property4.ToString(CultureInfo.CurrentCulture), + mode == PropertyMode.All); + + // Assert imported values + // Property1 and Property3 values should be always be imported again. + // Property2 and Property4 should be imported only if mode is All, if not the values should be the defaults. + Assert.Equal(objectToExport.Property1, ICsvPortable.FromCsvRow( + ICsvPortable.ExportToCsvLine(objectToExport, parameter), parameter).Property1); + + Assert.Equal(mode == PropertyMode.All ? objectToExport.Property2 : PropertySelectionSpecifiedTestDto.DefaultProperty2, ICsvPortable.FromCsvRow( + ICsvPortable.ExportToCsvLine(objectToExport, parameter), parameter).Property2); + + Assert.Equal(objectToExport.Property3, ICsvPortable.FromCsvRow( + ICsvPortable.ExportToCsvLine(objectToExport, parameter), parameter).Property3); + + Assert.Equal(mode == PropertyMode.All ? objectToExport.Property4 : PropertySelectionSpecifiedTestDto.DefaultProperty4, ICsvPortable.FromCsvRow( + ICsvPortable.ExportToCsvLine(objectToExport, parameter), parameter).Property4); + } + + public record PropertySelectionSpecifiedTestDto + { + public const int DefaultProperty1 = -1; + public const string DefaultProperty2 = "default"; + public const bool DefaultProperty3 = true; + public static readonly DateTime DefaultProperty4 = DateTime.Parse("2022.01.01"); + + [CsvProperty] public int Property1 { get; set; } = DefaultProperty1; + + public string Property2 { get; set; } = DefaultProperty2; + + [CsvProperty] public bool Property3 { get; set; } = DefaultProperty3; + + public DateTime Property4 { get; set; } = DefaultProperty4; + } } \ No newline at end of file diff --git a/CsvPortable/CsvPortable/Configuration/CsvParameter.cs b/CsvPortable/CsvPortable/Configuration/CsvParameter.cs index 4ac830f..86cb579 100644 --- a/CsvPortable/CsvPortable/Configuration/CsvParameter.cs +++ b/CsvPortable/CsvPortable/Configuration/CsvParameter.cs @@ -1,89 +1,76 @@ - using CsvPortable.Interfaces; namespace CsvPortable.Configuration { - public class CsvParameter - { - public static CsvParameter Default {get => (null, ";");} - public CsvParameter(CsvConfiguration? configuration) - { - Configuration = configuration; - } - - public CsvParameter(CsvConfiguration? configuration, CsvDelimiter delimiter) : this(configuration) - { - Delimiter = delimiter ?? throw new ArgumentNullException(nameof(delimiter)); - } - - public CsvParameter(CsvConfiguration? configuration, CsvDelimiter delimiter, bool closeEnd) : this(configuration, delimiter) - { - CloseEnd = closeEnd; - } - - public CsvParameter(CsvConfiguration? configuration, CsvDelimiter delimiter, bool closeEnd, List<(Type, CsvConfiguration)> specifiedConfigurations) : this(configuration, delimiter, closeEnd) - { - SpecifiedConfigurations = specifiedConfigurations ?? throw new ArgumentNullException(nameof(specifiedConfigurations)); - } - - public CsvParameter(CsvConfiguration? configuration, CsvDelimiter delimiter, bool closeEnd, params (Type, CsvConfiguration)[] specifiedConfigurations) : this(configuration, delimiter, closeEnd) - { - SpecifiedConfigurations = specifiedConfigurations.ToList(); - } - - - - /// - /// Gets or Sets Configuration for the Type. - /// - public CsvConfiguration? Configuration { get; set; } = null; - - /// - /// Gets or Sets Delimiter --> default ";" - /// - public CsvDelimiter Delimiter { get; set; } = ";"; - - /// - /// Gets or Sets CloseEnd (The CSVLine gets closed(\r\n)) --> default "true" - /// - public bool CloseEnd { get; set; } = true; - - - public CsvParameter ParameterToUse(Type type, bool closeEnd) - { - bool MatchType((Type Type, CsvConfiguration Configuration) t) - { - return t.Type == type; - } - - var configToUse = this.SpecifiedConfigurations.Exists(MatchType) - ? this.SpecifiedConfigurations.FirstOrDefault(MatchType).Configuration - : this.Configuration; - - return new CsvParameter(configToUse, this.Delimiter, closeEnd, this.SpecifiedConfigurations); - } - /// - /// Gets or Sets Specified Configurations. - /// For Class X use that Specification. - /// - public List<(Type Type, CsvConfiguration Configuration)> SpecifiedConfigurations { get; set; } = new List<(Type Type, CsvConfiguration Configuration)>(); - - public static implicit operator CsvParameter((CsvConfiguration? Configuration, CsvDelimiter Delimiter) tupel) - { - return new CsvParameter(tupel.Configuration, tupel.Delimiter); - } - - public static implicit operator CsvParameter((CsvConfiguration? Configuration, CsvDelimiter Delimiter, bool CloseEnd) tupel) - { - return new CsvParameter(tupel.Configuration, tupel.Delimiter, tupel.CloseEnd); - } - - public static implicit operator CsvParameter((CsvConfiguration Configuration, CsvDelimiter Delimiter, bool CloseEnd, List<(Type, CsvConfiguration)> SpecifiedConfigurations) tupel) - { - return new CsvParameter(tupel.Configuration, tupel.Delimiter, tupel.CloseEnd, tupel.SpecifiedConfigurations); - } - - - - } + public class CsvParameter + { + public static readonly CsvConfiguration? DefaultConfiguration = null; + public static readonly CsvDelimiter DefaultDelimiter = ";"; + public static readonly bool DefaultCloseEnd = true; + public static readonly PropertyMode DefaultPropertyMode = PropertyMode.All; + public static readonly List<(Type, CsvConfiguration)> DefaultSpecifiedConfigurations = new List<(Type, CsvConfiguration)>(); + + public static CsvParameter Default + { + get => (DefaultConfiguration, DefaultDelimiter); + } + + public CsvParameter(CsvConfiguration? configuration = null, CsvDelimiter? delimiter = null, bool? closeEnd = null, PropertyMode? propertyMode = null, List<(Type Type, CsvConfiguration Configuration)>? specifiedConfigurations = null) + { + this.Configuration = configuration ?? DefaultConfiguration; + this.Delimiter = delimiter ?? DefaultDelimiter; + this.CloseEnd = closeEnd ?? DefaultCloseEnd; + this.PropertyMode = propertyMode ?? DefaultPropertyMode; + this.SpecifiedConfigurations = specifiedConfigurations ?? DefaultSpecifiedConfigurations; + } + + + /// + /// Gets or Sets Configuration for the Type. + /// + public CsvConfiguration? Configuration { get; set; } + + /// + /// Gets or Sets Delimiter --> default ";" + /// + public CsvDelimiter Delimiter { get; set; } + + /// + /// Gets or Sets CloseEnd (The CSVLine gets closed(\r\n)) --> default "true" + /// + public bool CloseEnd { get; set; } + + public PropertyMode PropertyMode { get; set; } + + + public CsvParameter ParameterToUse(Type type, bool closeEnd) + { + bool MatchType((Type Type, CsvConfiguration Configuration) t) + { + return t.Type == type; + } + + var configToUse = this.SpecifiedConfigurations.Exists(MatchType) + ? this.SpecifiedConfigurations.FirstOrDefault(MatchType).Configuration + : this.Configuration; + + return new CsvParameter(configToUse, this.Delimiter, closeEnd, this.PropertyMode, this.SpecifiedConfigurations); + } + + /// + /// Gets or Sets Specified Configurations. + /// For Class X use that Specification. + /// + public List<(Type Type, CsvConfiguration Configuration)> SpecifiedConfigurations { get; set; } + + public static implicit operator CsvParameter((CsvConfiguration? Configuration, CsvDelimiter Delimiter) tupel) + { + return new CsvParameter(tupel.Configuration, tupel.Delimiter); + } + + public static implicit operator CsvParameter((CsvConfiguration? Configuration, CsvDelimiter Delimiter, bool CloseEnd) tupel) + { + return new CsvParameter(tupel.Configuration, tupel.Delimiter, tupel.CloseEnd); + } + } } \ No newline at end of file diff --git a/CsvPortable/CsvPortable/Configuration/PropertyMode.cs b/CsvPortable/CsvPortable/Configuration/PropertyMode.cs index d42ce28..0ef05af 100644 --- a/CsvPortable/CsvPortable/Configuration/PropertyMode.cs +++ b/CsvPortable/CsvPortable/Configuration/PropertyMode.cs @@ -1,6 +1,7 @@ namespace CsvPortable.Configuration; -public class PropertyMode +public enum PropertyMode { - + Explicit, + All } \ No newline at end of file diff --git a/CsvPortable/CsvPortable/Interfaces/ICsvPortable.cs b/CsvPortable/CsvPortable/Interfaces/ICsvPortable.cs index dfd5f55..b4e19bb 100644 --- a/CsvPortable/CsvPortable/Interfaces/ICsvPortable.cs +++ b/CsvPortable/CsvPortable/Interfaces/ICsvPortable.cs @@ -22,7 +22,7 @@ public static string ExportDefinition(Type t, CsvParameter? parameter = null) { parameter ??= CsvParameter.Default; string export = ""; - foreach (var prop in GetPropertiesToMap(t, parameter.Configuration)) + foreach (var prop in GetPropertiesToMap(t, parameter)) { TypeConverter converter = TypeDescriptor.GetConverter(prop.PropertyInfo.PropertyType); @@ -53,7 +53,7 @@ public static string ExportToCsvLine(T valObject, CsvParameter? parameter = n string export = ""; parameter ??= CsvParameter.Default; - foreach (var prop in GetPropertiesToMap(valObject!.GetType(), parameter.Configuration)) + foreach (var prop in GetPropertiesToMap(valObject!.GetType(), parameter)) { var propValue = prop.PropertyInfo.GetValue(valObject); TypeConverter converter = TypeDescriptor.GetConverter(prop.PropertyInfo.PropertyType); @@ -133,7 +133,7 @@ private static object FromCsvRow(Type type, List items, CsvParameter par var itemsSave = items.GetRange(0, items.Count); var item = Activator.CreateInstance(type); - foreach (var prop in GetPropertiesToMap(type, parameter.Configuration)) + foreach (var prop in GetPropertiesToMap(type, parameter)) { // TODO: Enable caching here TypeConverter converter = TypeDescriptor.GetConverter(prop.PropertyInfo.PropertyType); @@ -175,37 +175,67 @@ private static object FromCsvRow(Type type, List items, CsvParameter par return item ?? DeserializationException(type, "Could not create instance of type"); } - + private static CsvDeserializationException DeserializationException(Type type, string message, string? csvRow = null) { return new CsvDeserializationException($"Error while creating '{type.Name}' - '{message}'", csvRow); } + private record CacheReflection + { + public CacheReflection(Type type, PropertyMode propertyMode, CsvConfiguration? configuration, List properties) + { + this.Type = type ?? throw new ArgumentNullException(nameof(type)); + this.PropertyMode = propertyMode; + this.Configuration = configuration; + this.Properties = properties ?? throw new ArgumentNullException(nameof(properties)); + } + + public Type Type { get; set; } + public PropertyMode PropertyMode { get; set; } + public CsvConfiguration? Configuration { get; set; } + public List Properties { get; set; } + } - private static readonly List<(Type Type, CsvConfiguration? Configuration, List Properties)> + private static readonly List CacheReflections = - new List<(Type Type, CsvConfiguration? Configuration, List Properties)>(); + new List(); - internal static List GetPropertiesToMap(Type T, CsvConfiguration? configuration) + internal static List GetPropertiesToMap(Type T, CsvParameter parameter) { - if (CacheReflections.Exists(k => k.Type == T && k.Configuration == configuration)) + var cacheReflection = CacheReflections.FirstOrDefault(k => k.Type == T && k.PropertyMode == parameter.PropertyMode && k.Configuration == parameter.Configuration); + if (cacheReflection is not null) + { + return cacheReflection.Properties; + } + + List maps; + if (parameter.PropertyMode == PropertyMode.Explicit) + { + maps = T + .GetProperties() + .ToList() + .Where(k => + k.GetCustomAttributes(false).Any(k => k.GetType() == typeof(CsvPropertyAttribute))) + .Select(k => new CsvProperty(k)).ToList(); + } + else { - return CacheReflections.First(k => k.Type == T && k.Configuration == configuration).Properties; + maps = T + .GetProperties() + .ToList() + .Where(k => + !k.GetCustomAttributes(false).Any(k => k.GetType() == typeof(CsvIgnoreAttribute))) + .Select(k => new CsvProperty(k)).ToList(); } - var maps = T - .GetProperties() - .ToList() - .Where(k => - !k.GetCustomAttributes(false).Any(k => k.GetType() == typeof(CsvIgnoreAttribute))) - .Select(k => new CsvProperty(k)).ToList(); var t = T.GetProperties(); maps = maps.OrderBy(k => k.Index).ToList(); - CacheReflections.Add((T, configuration, maps)); + CacheReflections.Add(new CacheReflection(T, parameter.PropertyMode, parameter.Configuration, maps)); return maps; } diff --git a/Documentation/CreateReportsFromExistingModels.md b/Documentation/CreateReportsFromExistingModels.md new file mode 100644 index 0000000..b271462 --- /dev/null +++ b/Documentation/CreateReportsFromExistingModels.md @@ -0,0 +1,68 @@ +# Export Data from existing Models + +Lets say your application has multiple Data Models. You want to export certain fields from the data in csv reports. +By default, the mapper takes all public properties of the model. + +```csharp + public record PropertySelectionSpecifiedTestDto +{ + public const int DefaultProperty1 = -1; + public const string DefaultProperty2 = "default"; + public const bool DefaultProperty3 = true; + public static readonly DateTime DefaultProperty4 = DateTime.Parse("2022.01.01"); + + public int Property1 { get; set; } = DefaultProperty1; + + public string Property2 { get; set; } = DefaultProperty2; + + public bool Property3 { get; set; } = DefaultProperty3; + + public DateTime Property4 { get; set; } = DefaultProperty4; +} + +// Printing the Header Row +Console.WriteLine(ICsvPortable.ExportDefinition); + +// Would print something like: "Property1";"Property2";"Property3";"Property4" + +// Printing the values +Console.WriteLine(ICsvPortable.ExportToCsvLine(dto)); + +// Would print something like: -1;"default";true;"2022/01/01 00:00:00" +``` + + +## PropertyMode +The mapper can be configured to only take specified properties via the `PropertyMode enum`: + +```csharp +public record PropertySelectionSpecifiedTestDto +{ + public const int DefaultProperty1 = -1; + public const string DefaultProperty2 = "default"; + public const bool DefaultProperty3 = true; + public static readonly DateTime DefaultProperty4 = DateTime.Parse("2022.01.01"); + + [CsvProperty] public int Property1 { get; set; } = DefaultProperty1; + + public string Property2 { get; set; } = DefaultProperty2; + + [CsvProperty] public bool Property3 { get; set; } = DefaultProperty3; + + public DateTime Property4 { get; set; } = DefaultProperty4; +} + +// Printing the Header Row +Console.WriteLine(ICsvPortable.ExportDefinition(new CsvParameter(propertyMode: PropertyMode.Explicit))); +// Would print something like: "Property1";"Property3" + +// Printing the values +PropertySelectionSpecifiedTestDto dto = new PropertySelectionSpecifiedTestDto(); +Console.WriteLine(ICsvPortable.ExportToCsvLine(dto), new CsvParameter(propertyMode: PropertyMode.Explicit)); + +// Would print something like: -1;true +``` + +## Custom Configuration + +In future development, multiple configurations on each property will be possible. So the caller can specify wich kind of properties should be mapped. \ No newline at end of file diff --git a/README.md b/README.md index 7b57bac..1dd58c2 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,5 @@ List
newAddresses = new List
() await ICsvPortable.ToStream(newAddresses, writeStream); ``` +## Tutorials +[Export existing models](./Documentation/CreateReportsFromExistingModels.md) \ No newline at end of file