diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5b316bc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [ master, main, dev ] + pull_request: + +jobs: + build-dotnet8: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Build modern SDK-style projects (v2) + run: | + dotnet build src/FlatFile.Core.Modern/FlatFile.Core.Modern.csproj -c Release + dotnet build src/FlatFile.Core.Attributes.Modern/FlatFile.Core.Attributes.Modern.csproj -c Release + dotnet build src/FlatFile.Delimited.Modern/FlatFile.Delimited.Modern.csproj -c Release + dotnet build src/FlatFile.FixedLength.Modern/FlatFile.FixedLength.Modern.csproj -c Release + dotnet build src/FlatFile.Delimited.Attributes.Modern/FlatFile.Delimited.Attributes.Modern.csproj -c Release + dotnet build src/FlatFile.FixedLength.Attributes.Modern/FlatFile.FixedLength.Attributes.Modern.csproj -c Release + dotnet build tests/FlatFile.Modern.Tests/FlatFile.Modern.Tests.csproj -c Release + + - name: Run modern test suite + run: dotnet test tests/FlatFile.Modern.Tests/FlatFile.Modern.Tests.csproj -c Release --no-build diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml new file mode 100644 index 0000000..cb83561 --- /dev/null +++ b/.github/workflows/publish-nuget.yml @@ -0,0 +1,48 @@ +name: Publish NuGet (v2) + +on: + push: + branches: [ master ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + PACKAGE_VERSION: 2.0.${{ github.run_number }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Validate NuGet API key is configured + run: | + if [ -z "$NUGET_API_KEY" ]; then + echo "NUGET_API_KEY secret is not configured." >&2 + exit 1 + fi + + - name: Pack v2 packages + run: | + mkdir -p artifacts/packages + dotnet pack src/FlatFile.Core.Modern/FlatFile.Core.Modern.csproj -c Release -o artifacts/packages /p:Version=$PACKAGE_VERSION + dotnet pack src/FlatFile.Core.Attributes.Modern/FlatFile.Core.Attributes.Modern.csproj -c Release -o artifacts/packages /p:Version=$PACKAGE_VERSION + dotnet pack src/FlatFile.Delimited.Modern/FlatFile.Delimited.Modern.csproj -c Release -o artifacts/packages /p:Version=$PACKAGE_VERSION + dotnet pack src/FlatFile.FixedLength.Modern/FlatFile.FixedLength.Modern.csproj -c Release -o artifacts/packages /p:Version=$PACKAGE_VERSION + dotnet pack src/FlatFile.Delimited.Attributes.Modern/FlatFile.Delimited.Attributes.Modern.csproj -c Release -o artifacts/packages /p:Version=$PACKAGE_VERSION + dotnet pack src/FlatFile.FixedLength.Attributes.Modern/FlatFile.FixedLength.Attributes.Modern.csproj -c Release -o artifacts/packages /p:Version=$PACKAGE_VERSION + + - name: Publish packages to NuGet + run: | + dotnet nuget push "artifacts/packages/*.nupkg" \ + --api-key "$NUGET_API_KEY" \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate diff --git a/README.md b/README.md index 5c30b8a..be3136e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,44 @@ FlatFile FlatFile is a library to work with flat files (work up-to 100 times faster then [FileHelpers](https://www.nuget.org/packages/FileHelpers/2.0.0)) + +## Modernization status + +- 🚨 **v2 breaking change**: dropped legacy .NET Framework targets (`net35`-`net48`) and old build pipeline. +- ✅ Modernized runtime support to **.NET 8** only via SDK-style projects. +- ✅ CI now builds modern projects with `dotnet build` on GitHub Actions. + +### Modern .NET support + +Active projects: + +- `src/FlatFile.Core.Modern` +- `src/FlatFile.Core.Attributes.Modern` +- `src/FlatFile.Delimited.Modern` +- `src/FlatFile.FixedLength.Modern` +- `src/FlatFile.Delimited.Attributes.Modern` +- `src/FlatFile.FixedLength.Attributes.Modern` + +All of them target `net8.0` and carry package/assembly version `2.0.0`. + +### Runtime enhancements (v2) + +Recent runtime-focused updates for modern .NET: + +- Faster reflection activation path via compiled constructor delegates and concurrent caches. +- Improved string-to-type conversion using cached `TypeConverter` instances and invariant-culture conversion semantics. +- Reduced allocations in parsing paths (for example, char-based trim overloads and span-based quote detection in delimited parser). + +### NuGet publishing from GitHub + +When changes are merged to `master`, GitHub Actions can publish v2 packages automatically using `.github/workflows/publish-nuget.yml`. + +Required repository secret: + +- `NUGET_API_KEY`: NuGet.org API key with push permission for FlatFile packages. + +The publish workflow packs all `*.Modern` projects and pushes resulting `.nupkg` files to NuGet (`--skip-duplicate`). + ### Installing FlatFile #### Installing all packages diff --git a/assets/psake-common.ps1 b/assets/psake-common.ps1 index 293bd40..dacecac 100644 --- a/assets/psake-common.ps1 +++ b/assets/psake-common.ps1 @@ -20,8 +20,15 @@ Properties { ### Project information $solution_path = "$src_dir\$solution" $sharedAssemblyInfo = "$src_dir\SharedAssemblyInfo.cs" - $config = "Release" - $frameworks = @("NET35", "NET40", "NET45") + $config = "Release" + $modern_projects = @( + "$src_dir\FlatFile.Core.Modern\FlatFile.Core.Modern.csproj", + "$src_dir\FlatFile.Core.Attributes.Modern\FlatFile.Core.Attributes.Modern.csproj", + "$src_dir\FlatFile.Delimited.Modern\FlatFile.Delimited.Modern.csproj", + "$src_dir\FlatFile.FixedLength.Modern\FlatFile.FixedLength.Modern.csproj", + "$src_dir\FlatFile.Delimited.Attributes.Modern\FlatFile.Delimited.Attributes.Modern.csproj", + "$src_dir\FlatFile.FixedLength.Attributes.Modern\FlatFile.FixedLength.Attributes.Modern.csproj" + ) ### Files $releaseNotes = "$base_dir\ChangeLog.md" @@ -29,34 +36,27 @@ Properties { ## Tasks -Task Restore -Description "Restore NuGet packages for solution." { - "Restoring NuGet packages for '$solution_path'..." - Exec { .$nuget restore $solution_path } +Task Restore -Description "Restore .NET packages for modern projects." { + foreach ($project in $modern_projects) { + "Restoring '$project'..." + Exec { dotnet restore $project } + } } Task Clean -Description "Clean up build and project folders." { Clean-Directory $build_dir - if ($solution) { - "Cleaning up '$solution'..." - - foreach ($framework in $frameworks) { - Exec { msbuild $solution_path /target:Clean /nologo /verbosity:minimal /p:Framework=$framework} - } + foreach ($project in $modern_projects) { + "Cleaning '$project'..." + Exec { dotnet clean $project -c $config } } } -Task Compile -Depends Clean, Restore -Description "Compile all the projects in a solution." { - "Compiling '$solution'..." - - $extra = $null - if ($appVeyor) { - $extra = "/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" +Task Compile -Depends Clean, Restore -Description "Compile all modern SDK-style projects." { + foreach ($project in $modern_projects) { + "Compiling '$project'..." + Exec { dotnet build $project -c $config --no-restore } } - - foreach ($framework in $frameworks) { - Exec { msbuild $solution_path /p:"Configuration=$config;Framework=$framework" /nologo /verbosity:minimal $extra } - } } ### Pack functions diff --git a/docs/modernization-plan.md b/docs/modernization-plan.md new file mode 100644 index 0000000..200b23c --- /dev/null +++ b/docs/modernization-plan.md @@ -0,0 +1,48 @@ +# FlatFile modernization plan + +## Current direction (v2) + +This repository now follows a **modern-only** strategy: + +1. Legacy .NET Framework build matrix was removed from CI. +2. SDK-style projects under `*.Modern` are the active build path. +3. Active target is `net8.0` with version `2.0.0` (breaking major release). + +## Why + +The previous mixed strategy (legacy + modern) produced unstable CI and unnecessary maintenance overhead. +A major-version reset enables simpler tooling, faster builds, and a clear support policy. + +## Next steps + +- Publish v2 packages from modern projects (`dotnet pack`). +- Add analyzers and nullable annotations incrementally. +- Add dedicated test projects targeting `net8.0`. + + +## CI/CD publishing + +- `.github/workflows/publish-nuget.yml` publishes NuGet packages on pushes to `master`. +- Configure repository secret `NUGET_API_KEY` before enabling release merges. +- Package versions are generated as `2.0.` in CI. + + +## Implemented performance updates + +- Replaced reflection activation lock+`DynamicInvoke` path with concurrent cached compiled factories. +- Updated conversion pipeline to use converter caching and invariant-culture conversion semantics. +- Applied small parser allocation improvements (`TrimStart/TrimEnd(char)`, span-based quote prefix checks). + + +## Modern tests + +- Added `tests/FlatFile.Modern.Tests` (xUnit, net8.0). +- CI now runs `dotnet test` for the modern test suite. + + +## Span/Memory guidelines used + +- Use `ReadOnlySpan` for scanning/tokenization (delimiter/quote detection) where data remains in-memory and does not need ownership transfer. +- Use `Memory` only when data must survive async boundaries; prefer `Span`/`ReadOnlySpan` in synchronous hot paths. +- Avoid premature `Substring`/`string.Format` allocations in line build/parse loops. +- Keep API compatibility: introduce span optimizations internally first, then expose span APIs in a dedicated v2+ surface when needed. diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..f416ac7 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,17 @@ + + + true + MIT + forcewake + Pavel Nasovich + FlatFile library for high-performance fixed-length and delimited file processing. + https://github.com/forcewake/FlatFile + https://github.com/forcewake/FlatFile + git + README.md + + + + + + diff --git a/src/FlatFile.Benchmark/FlatFile.Benchmark.csproj b/src/FlatFile.Benchmark/FlatFile.Benchmark.csproj index caa3588..09b1eed 100644 --- a/src/FlatFile.Benchmark/FlatFile.Benchmark.csproj +++ b/src/FlatFile.Benchmark/FlatFile.Benchmark.csproj @@ -31,6 +31,63 @@ prompt 4 + + + + v4.5.1 + AnyCPU + + + v4.5.2 + AnyCPU + + + v4.6 + AnyCPU + + + v4.6.1 + AnyCPU + + + v4.6.2 + AnyCPU + + + v4.7 + AnyCPU + + + v4.7.1 + AnyCPU + + + v4.7.2 + AnyCPU + + + v4.8 + AnyCPU + true + full + false + bin\$(Configuration)\$(Framework)\ + DEBUG;TRACE + prompt + 4 + + + v4.8 + AnyCPU + pdbonly + true + bin\$(Configuration)\$(Framework)\ + TRACE + prompt + 4 + bin\$(Configuration)\$(Framework)\FlatFile.Benchmark.XML + + ..\packages\Benchmark.It.1.2.0\lib\BenchmarkIt.dll diff --git a/src/FlatFile.Core.Attributes.Modern/FlatFile.Core.Attributes.Modern.csproj b/src/FlatFile.Core.Attributes.Modern/FlatFile.Core.Attributes.Modern.csproj new file mode 100644 index 0000000..95ba650 --- /dev/null +++ b/src/FlatFile.Core.Attributes.Modern/FlatFile.Core.Attributes.Modern.csproj @@ -0,0 +1,22 @@ + + + net8.0 + FlatFile.Core.Attributes + FlatFile.Core.Attributes + false + latest + disable + true + 2.0.0 + 2.0.0.0 + 2.0.0.0 + + + + + + + + + + diff --git a/src/FlatFile.Core.Attributes/FlatFile.Core.Attributes.csproj b/src/FlatFile.Core.Attributes/FlatFile.Core.Attributes.csproj index 87f339c..5689453 100644 --- a/src/FlatFile.Core.Attributes/FlatFile.Core.Attributes.csproj +++ b/src/FlatFile.Core.Attributes/FlatFile.Core.Attributes.csproj @@ -86,6 +86,63 @@ 4 bin\$(Configuration)\$(Framework)\$(AssemblyName).XML + + + + v4.5.1 + AnyCPU + + + v4.5.2 + AnyCPU + + + v4.6 + AnyCPU + + + v4.6.1 + AnyCPU + + + v4.6.2 + AnyCPU + + + v4.7 + AnyCPU + + + v4.7.1 + AnyCPU + + + v4.7.2 + AnyCPU + + + v4.8 + AnyCPU + true + full + false + bin\$(Configuration)\$(Framework)\ + DEBUG;TRACE + prompt + 4 + + + v4.8 + AnyCPU + pdbonly + true + bin\$(Configuration)\$(Framework)\ + TRACE + prompt + 4 + bin\$(Configuration)\$(Framework)\FlatFile.Core.Attributes.XML + + diff --git a/src/FlatFile.Core.Modern/FlatFile.Core.Modern.csproj b/src/FlatFile.Core.Modern/FlatFile.Core.Modern.csproj new file mode 100644 index 0000000..ce7d9bb --- /dev/null +++ b/src/FlatFile.Core.Modern/FlatFile.Core.Modern.csproj @@ -0,0 +1,18 @@ + + + net8.0 + FlatFile.Core + FlatFile.Core + false + latest + disable + true + 2.0.0 + 2.0.0.0 + 2.0.0.0 + + + + + + diff --git a/src/FlatFile.Core/Base/FlatFileEngine.cs b/src/FlatFile.Core/Base/FlatFileEngine.cs index 22d18a3..03ea1e5 100644 --- a/src/FlatFile.Core/Base/FlatFileEngine.cs +++ b/src/FlatFile.Core/Base/FlatFileEngine.cs @@ -56,7 +56,8 @@ protected FlatFileEngine(Func handleEntryReadError = nu /// Impossible to parse line public virtual IEnumerable Read(Stream stream) where TEntity : class, new() { - var reader = new StreamReader(stream); + using (var reader = new StreamReader(stream)) + { string line; int lineNumber = 0; @@ -65,37 +66,38 @@ protected FlatFileEngine(Func handleEntryReadError = nu ProcessHeader(reader); } - while ((line = reader.ReadLine()) != null) - { - if (string.IsNullOrEmpty(line) || string.IsNullOrEmpty(line.Trim())) continue; - - bool ignoreEntry = false; - var entry = new TEntity(); - try + while ((line = reader.ReadLine()) != null) { - if (!TryParseLine(line, lineNumber++, ref entry)) + if (string.IsNullOrWhiteSpace(line)) continue; + + bool ignoreEntry = false; + var entry = new TEntity(); + try { - throw new ParseLineException("Impossible to parse line", line, lineNumber); + if (!TryParseLine(line, lineNumber++, ref entry)) + { + throw new ParseLineException("Impossible to parse line", line, lineNumber); + } } - } - catch (Exception ex) - { - if (_handleEntryReadError == null) + catch (Exception ex) { - throw; + if (_handleEntryReadError == null) + { + throw; + } + + if (!_handleEntryReadError(line, ex)) + { + throw; + } + + ignoreEntry = true; } - if (!_handleEntryReadError(line, ex)) + if (!ignoreEntry) { - throw; + yield return entry; } - - ignoreEntry = true; - } - - if (!ignoreEntry) - { - yield return entry; } } } @@ -146,22 +148,23 @@ protected virtual void WriteEntry(TextWriter writer, int lineNumber, TE /// The entries. public virtual void Write(Stream stream, IEnumerable entries) where TEntity : class, new() { - TextWriter writer = new StreamWriter(stream); + using (TextWriter writer = new StreamWriter(stream)) + { + this.WriteHeader(writer); - this.WriteHeader(writer); + int lineNumber = 0; - int lineNumber = 0; - - foreach (var entry in entries) - { - this.WriteEntry(writer, lineNumber, entry); + foreach (var entry in entries) + { + this.WriteEntry(writer, lineNumber, entry); - lineNumber += 1; - } + lineNumber += 1; + } - this.WriteFooter(writer); + this.WriteFooter(writer); - writer.Flush(); + writer.Flush(); + } } /// diff --git a/src/FlatFile.Core/Exceptions/ParseLineException.cs b/src/FlatFile.Core/Exceptions/ParseLineException.cs index 707a336..679fa17 100644 --- a/src/FlatFile.Core/Exceptions/ParseLineException.cs +++ b/src/FlatFile.Core/Exceptions/ParseLineException.cs @@ -29,11 +29,13 @@ public ParseLineException(string line, int lineNumber) public int LineNumber { get; private set; } public string Line { get; private set; } +#pragma warning disable SYSLIB0051 protected ParseLineException(SerializationInfo info, StreamingContext context, string line, int lineNumber) : base(info, context) { Line = line; LineNumber = lineNumber; } +#pragma warning restore SYSLIB0051 } } diff --git a/src/FlatFile.Core/Extensions/PropertyAccessorCache.cs b/src/FlatFile.Core/Extensions/PropertyAccessorCache.cs new file mode 100644 index 0000000..036a3b9 --- /dev/null +++ b/src/FlatFile.Core/Extensions/PropertyAccessorCache.cs @@ -0,0 +1,53 @@ +namespace FlatFile.Core.Extensions +{ + using System; + using System.Collections.Concurrent; + using System.Linq.Expressions; + using System.Reflection; + + internal static class PropertyAccessorCache + { + private static readonly ConcurrentDictionary> GetterCache = + new ConcurrentDictionary>(); + + private static readonly ConcurrentDictionary> SetterCache = + new ConcurrentDictionary>(); + + public static object GetValue(PropertyInfo propertyInfo, object target) + { + var getter = GetterCache.GetOrAdd(propertyInfo, BuildGetter); + return getter(target); + } + + public static void SetValue(PropertyInfo propertyInfo, object target, object value) + { + var setter = SetterCache.GetOrAdd(propertyInfo, BuildSetter); + setter(target, value); + } + + private static Func BuildGetter(PropertyInfo propertyInfo) + { + var target = Expression.Parameter(typeof(object), "target"); + var castTarget = Expression.Convert(target, propertyInfo.DeclaringType); + var property = Expression.Property(castTarget, propertyInfo); + var castResult = Expression.Convert(property, typeof(object)); + return Expression.Lambda>(castResult, target).Compile(); + } + + private static Action BuildSetter(PropertyInfo propertyInfo) + { + if (!propertyInfo.CanWrite) + { + return (target, value) => { }; + } + + var target = Expression.Parameter(typeof(object), "target"); + var value = Expression.Parameter(typeof(object), "value"); + var castTarget = Expression.Convert(target, propertyInfo.DeclaringType); + var castValue = Expression.Convert(value, propertyInfo.PropertyType); + var property = Expression.Property(castTarget, propertyInfo); + var assign = Expression.Assign(property, castValue); + return Expression.Lambda>(assign, target, value).Compile(); + } + } +} diff --git a/src/FlatFile.Core/Extensions/ReflectionHelper.cs b/src/FlatFile.Core/Extensions/ReflectionHelper.cs index fcc5375..16cfc49 100644 --- a/src/FlatFile.Core/Extensions/ReflectionHelper.cs +++ b/src/FlatFile.Core/Extensions/ReflectionHelper.cs @@ -1,87 +1,92 @@ -using System.Linq; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace FlatFile.Core.Extensions { - using System; - using System.Collections.Generic; - using System.Linq.Expressions; - /// /// Class ReflectionHelper. /// public static class ReflectionHelper { - static readonly object CacheLock = new object(); - static readonly Dictionary Cache = new Dictionary(); + private static readonly ConcurrentDictionary> NoArgCache = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> ArgCache = new ConcurrentDictionary>(); /// /// Creates an instance of type using the default constructor. /// - /// - /// if set to true [cached]. - /// T. - public static T CreateInstance(bool cached = false) { return (T) CreateInstance(typeof (T), cached); } + public static T CreateInstance(bool cached = false) + { + return (T)CreateInstance(typeof(T), cached); + } /// /// Creates an instance of type using the default constructor. /// - /// Type of the target. - /// if set to true [cached]. - /// System.Object. public static object CreateInstance(Type targetType, bool cached = false) { if (targetType == null) return null; var ctorInfo = targetType.GetConstructor(Type.EmptyTypes); - return CreateInstance(ctorInfo, cached); } /// /// Creates an instance of type using the specified constructor parameters. /// - /// Type of the target. - /// if set to true [cached]. - /// The constructor arguments. - /// System.Object. public static object CreateInstance(Type targetType, bool cached = false, params object[] parameters) { if (targetType == null) return null; - if (parameters == null || !parameters.Any()) return CreateInstance(targetType, cached); + if (parameters == null || parameters.Length == 0) return CreateInstance(targetType, cached); var ctorInfo = targetType.GetConstructor(parameters.Select(a => a.GetType()).ToArray()); - return CreateInstance(ctorInfo, cached, parameters); } - static object CreateInstance(ConstructorInfo ctorInfo, bool cached, object[] parameters = null) + private static object CreateInstance(ConstructorInfo ctorInfo, bool cached, object[] parameters = null) { if (ctorInfo == null) return null; - var hasArguments = parameters != null && parameters.Any(); - Delegate ctor; - lock (CacheLock) + var hasArguments = parameters != null && parameters.Length > 0; + if (!cached) { - if (!Cache.TryGetValue(ctorInfo, out ctor) || !cached) - { - if (hasArguments) - { - var ctorArgs = ctorInfo.GetParameters().Select((param, index) => Expression.Parameter(param.ParameterType, String.Format("Param{0}", index))).ToArray(); - // ReSharper disable once CoVariantArrayConversion - ctor = Expression.Lambda(Expression.New(ctorInfo, ctorArgs), ctorArgs).Compile(); - } - else - { - ctor = Expression.Lambda(Expression.New(ctorInfo)).Compile(); - } - } + return ctorInfo.Invoke(parameters); + } - if (cached) CacheCtor(ctorInfo, ctor); + if (!hasArguments) + { + var factory = NoArgCache.GetOrAdd(ctorInfo, BuildNoArgFactory); + return factory(); } - return hasArguments ? ctor.DynamicInvoke(parameters) : ctor.DynamicInvoke(); + + var argFactory = ArgCache.GetOrAdd(ctorInfo, BuildArgFactory); + return argFactory(parameters); } - static void CacheCtor(ConstructorInfo key, Delegate ctor) { if (!Cache.ContainsKey(key)) Cache.Add(key, ctor); } + private static Func BuildNoArgFactory(ConstructorInfo ctorInfo) + { + var ctorCall = Expression.New(ctorInfo); + var cast = Expression.Convert(ctorCall, typeof(object)); + return Expression.Lambda>(cast).Compile(); + } + + private static Func BuildArgFactory(ConstructorInfo ctorInfo) + { + var argsParameter = Expression.Parameter(typeof(object[]), "args"); + var ctorParameters = ctorInfo.GetParameters(); + + var arguments = ctorParameters + .Select((parameter, index) => + Expression.Convert( + Expression.ArrayIndex(argsParameter, Expression.Constant(index)), + parameter.ParameterType)) + .ToArray(); + + var ctorCall = Expression.New(ctorInfo, arguments); + var cast = Expression.Convert(ctorCall, typeof(object)); + return Expression.Lambda>(cast, argsParameter).Compile(); + } } -} \ No newline at end of file +} diff --git a/src/FlatFile.Core/Extensions/TypeChangeExtensions.cs b/src/FlatFile.Core/Extensions/TypeChangeExtensions.cs index 03d7fe5..e1d2fe8 100644 --- a/src/FlatFile.Core/Extensions/TypeChangeExtensions.cs +++ b/src/FlatFile.Core/Extensions/TypeChangeExtensions.cs @@ -1,23 +1,104 @@ namespace FlatFile.Core.Extensions { using System; + using System.Collections.Concurrent; using System.ComponentModel; + using System.Globalization; public static class TypeChangeExtensions { + private static readonly ConcurrentDictionary ConverterCache = new ConcurrentDictionary(); + public static object Convert(this string input, Type targetType) { - var converter = TypeDescriptor.GetConverter(targetType); + if (targetType == null) + { + throw new ArgumentNullException("targetType"); + } + + var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; + if (string.IsNullOrWhiteSpace(input)) + { + return Nullable.GetUnderlyingType(targetType) != null + ? null + : targetType.GetDefaultValue(); + } + + if (underlyingType.IsEnum) + { + return Enum.Parse(underlyingType, input, true); + } + + if (underlyingType == typeof(string)) + { + return input; + } + + if (underlyingType == typeof(int)) + { + return int.Parse(input, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + + if (underlyingType == typeof(long)) + { + return long.Parse(input, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + + if (underlyingType == typeof(short)) + { + return short.Parse(input, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + + if (underlyingType == typeof(decimal)) + { + return decimal.Parse(input, NumberStyles.Number, CultureInfo.InvariantCulture); + } + + if (underlyingType == typeof(double)) + { + return double.Parse(input, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture); + } + + if (underlyingType == typeof(float)) + { + return float.Parse(input, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture); + } + + if (underlyingType == typeof(bool)) + { + return bool.Parse(input); + } + + if (underlyingType == typeof(Guid)) + { + return Guid.Parse(input); + } + + if (underlyingType == typeof(DateTime)) + { + return DateTime.Parse(input, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + } + + var converter = ConverterCache.GetOrAdd(underlyingType, TypeDescriptor.GetConverter); if (converter != null) { - return converter.ConvertFromString(input); + if (converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFromInvariantString(input); + } + + if (converter.CanConvertFrom(typeof(object))) + { + return converter.ConvertFrom(null, CultureInfo.InvariantCulture, input); + } } - return targetType.GetDefaultValue(); + + return System.Convert.ChangeType(input, underlyingType, CultureInfo.InvariantCulture); } public static T Convert(this string input) { - return (T) Convert(input, typeof (T)); + return (T)Convert(input, typeof(T)); } } -} \ No newline at end of file +} diff --git a/src/FlatFile.Core/FlatFile.Core.csproj b/src/FlatFile.Core/FlatFile.Core.csproj index 08ab631..4cb20c8 100644 --- a/src/FlatFile.Core/FlatFile.Core.csproj +++ b/src/FlatFile.Core/FlatFile.Core.csproj @@ -86,6 +86,63 @@ 4 bin\$(Configuration)\$(Framework)\FlatFile.Core.XML + + + + v4.5.1 + AnyCPU + + + v4.5.2 + AnyCPU + + + v4.6 + AnyCPU + + + v4.6.1 + AnyCPU + + + v4.6.2 + AnyCPU + + + v4.7 + AnyCPU + + + v4.7.1 + AnyCPU + + + v4.7.2 + AnyCPU + + + v4.8 + AnyCPU + true + full + false + bin\$(Configuration)\$(Framework)\ + DEBUG;TRACE + prompt + 4 + + + v4.8 + AnyCPU + pdbonly + true + bin\$(Configuration)\$(Framework)\ + TRACE + prompt + 4 + bin\$(Configuration)\$(Framework)\FlatFile.Core.XML + + diff --git a/src/FlatFile.Delimited.Attributes.Modern/FlatFile.Delimited.Attributes.Modern.csproj b/src/FlatFile.Delimited.Attributes.Modern/FlatFile.Delimited.Attributes.Modern.csproj new file mode 100644 index 0000000..e115b0e --- /dev/null +++ b/src/FlatFile.Delimited.Attributes.Modern/FlatFile.Delimited.Attributes.Modern.csproj @@ -0,0 +1,24 @@ + + + net8.0 + FlatFile.Delimited.Attributes + FlatFile.Delimited.Attributes + false + latest + disable + true + 2.0.0 + 2.0.0.0 + 2.0.0.0 + + + + + + + + + + + + diff --git a/src/FlatFile.Delimited.Attributes/FlatFile.Delimited.Attributes.csproj b/src/FlatFile.Delimited.Attributes/FlatFile.Delimited.Attributes.csproj index 8c89756..3c6c5fa 100644 --- a/src/FlatFile.Delimited.Attributes/FlatFile.Delimited.Attributes.csproj +++ b/src/FlatFile.Delimited.Attributes/FlatFile.Delimited.Attributes.csproj @@ -84,6 +84,63 @@ 4 bin\$(Configuration)\$(Framework)\$(AssemblyName).XML + + + + v4.5.1 + AnyCPU + + + v4.5.2 + AnyCPU + + + v4.6 + AnyCPU + + + v4.6.1 + AnyCPU + + + v4.6.2 + AnyCPU + + + v4.7 + AnyCPU + + + v4.7.1 + AnyCPU + + + v4.7.2 + AnyCPU + + + v4.8 + AnyCPU + true + full + false + bin\$(Configuration)\$(Framework)\ + DEBUG;TRACE + prompt + 4 + + + v4.8 + AnyCPU + pdbonly + true + bin\$(Configuration)\$(Framework)\ + TRACE + prompt + 4 + bin\$(Configuration)\$(Framework)\FlatFile.Delimited.Attributes.XML + + diff --git a/src/FlatFile.Delimited.Modern/FlatFile.Delimited.Modern.csproj b/src/FlatFile.Delimited.Modern/FlatFile.Delimited.Modern.csproj new file mode 100644 index 0000000..0c46795 --- /dev/null +++ b/src/FlatFile.Delimited.Modern/FlatFile.Delimited.Modern.csproj @@ -0,0 +1,22 @@ + + + net8.0 + FlatFile.Delimited + FlatFile.Delimited + false + latest + disable + true + 2.0.0 + 2.0.0.0 + 2.0.0.0 + + + + + + + + + + diff --git a/src/FlatFile.Delimited/FlatFile.Delimited.csproj b/src/FlatFile.Delimited/FlatFile.Delimited.csproj index 1523482..32a8a8f 100644 --- a/src/FlatFile.Delimited/FlatFile.Delimited.csproj +++ b/src/FlatFile.Delimited/FlatFile.Delimited.csproj @@ -84,6 +84,63 @@ 4 bin\$(Configuration)\$(Framework)\FlatFile.Delimited.XML + + + + v4.5.1 + AnyCPU + + + v4.5.2 + AnyCPU + + + v4.6 + AnyCPU + + + v4.6.1 + AnyCPU + + + v4.6.2 + AnyCPU + + + v4.7 + AnyCPU + + + v4.7.1 + AnyCPU + + + v4.7.2 + AnyCPU + + + v4.8 + AnyCPU + true + full + false + bin\$(Configuration)\$(Framework)\ + DEBUG;TRACE + prompt + 4 + + + v4.8 + AnyCPU + pdbonly + true + bin\$(Configuration)\$(Framework)\ + TRACE + prompt + 4 + bin\$(Configuration)\$(Framework)\FlatFile.Delimited.XML + + diff --git a/src/FlatFile.Delimited/Implementation/DelimitedLineBuilder.cs b/src/FlatFile.Delimited/Implementation/DelimitedLineBuilder.cs index c7aaf02..29f5c11 100644 --- a/src/FlatFile.Delimited/Implementation/DelimitedLineBuilder.cs +++ b/src/FlatFile.Delimited/Implementation/DelimitedLineBuilder.cs @@ -1,10 +1,11 @@ -namespace FlatFile.Delimited.Implementation +namespace FlatFile.Delimited.Implementation { - using System.Linq; + using System.Text; using FlatFile.Core.Base; + using FlatFile.Core.Extensions; public class DelimitedLineBuilder : - LineBulderBase, + LineBulderBase, IDelimitedLineBuilder { public DelimitedLineBuilder(IDelimitedLayoutDescriptor descriptor) @@ -14,11 +15,22 @@ public DelimitedLineBuilder(IDelimitedLayoutDescriptor descriptor) public override string BuildLine(T entry) { - string line = Descriptor.Fields.Aggregate(string.Empty, - (current, field) => - current + (current.Length > 0 ? Descriptor.Delimiter : "") + - GetStringValueFromField(field, field.PropertyInfo.GetValue(entry, null))); - return line; + var delimiter = Descriptor.Delimiter; + var lineBuilder = new StringBuilder(); + bool isFirst = true; + + foreach (var field in Descriptor.Fields) + { + if (!isFirst) + { + lineBuilder.Append(delimiter); + } + + lineBuilder.Append(GetStringValueFromField(field, PropertyAccessorCache.GetValue(field.PropertyInfo, entry))); + isFirst = false; + } + + return lineBuilder.ToString(); } protected override string TransformFieldValue(IDelimitedFieldSettingsContainer field, string lineValue) @@ -26,9 +38,10 @@ protected override string TransformFieldValue(IDelimitedFieldSettingsContainer f var quotes = Descriptor.Quotes; if (!string.IsNullOrEmpty(quotes)) { - lineValue = string.Format("{0}{1}{0}", quotes, lineValue); + return quotes + lineValue + quotes; } + return lineValue; } } -} \ No newline at end of file +} diff --git a/src/FlatFile.Delimited/Implementation/DelimitedLineParser.cs b/src/FlatFile.Delimited/Implementation/DelimitedLineParser.cs index e5db25c..5f3ae56 100644 --- a/src/FlatFile.Delimited/Implementation/DelimitedLineParser.cs +++ b/src/FlatFile.Delimited/Implementation/DelimitedLineParser.cs @@ -3,6 +3,7 @@ namespace FlatFile.Delimited.Implementation using System; using FlatFile.Core; using FlatFile.Core.Base; + using FlatFile.Core.Extensions; public class DelimitedLineParser : LineParserBase, @@ -25,20 +26,21 @@ public override TEntity ParseLine(string line, TEntity entity) int nextDelimiterIndex = -1; if (line.Length > linePosition + delimiterSize) { - if (!String.IsNullOrEmpty(Layout.Quotes)) { - if (Layout.Quotes.Equals(line.Substring(linePosition, Layout.Quotes.Length))) + if (!String.IsNullOrEmpty(Layout.Quotes)) + { + if (line.AsSpan(linePosition).StartsWith(Layout.Quotes.AsSpan(), StringComparison.Ordinal)) { - nextDelimiterIndex = line.IndexOf(Layout.Quotes, linePosition + 1, StringComparison.InvariantCultureIgnoreCase); - if (line.Length > nextDelimiterIndex) + nextDelimiterIndex = line.IndexOf(Layout.Quotes, linePosition + 1, StringComparison.Ordinal); + if (nextDelimiterIndex > -1 && line.Length > nextDelimiterIndex) { - nextDelimiterIndex = line.IndexOf(Layout.Delimiter, nextDelimiterIndex, StringComparison.InvariantCultureIgnoreCase); + nextDelimiterIndex = line.IndexOf(Layout.Delimiter, nextDelimiterIndex, StringComparison.Ordinal); } } } if (nextDelimiterIndex == -1) { - nextDelimiterIndex = line.IndexOf(Layout.Delimiter, linePosition, StringComparison.InvariantCultureIgnoreCase); + nextDelimiterIndex = line.IndexOf(Layout.Delimiter, linePosition, StringComparison.Ordinal); } } int fieldLength; @@ -52,7 +54,7 @@ public override TEntity ParseLine(string line, TEntity entity) } string fieldValueFromLine = line.Substring(linePosition, fieldLength); var convertedFieldValue = GetFieldValueFromString(field, fieldValueFromLine); - field.PropertyInfo.SetValue(entity, convertedFieldValue, null); + PropertyAccessorCache.SetValue(field.PropertyInfo, entity, convertedFieldValue); linePosition += fieldLength + (nextDelimiterIndex > -1 ? delimiterSize : 0); } return entity; @@ -65,8 +67,12 @@ protected override string TransformStringValue(IDelimitedFieldSettingsContainer return memberValue; } - var value = memberValue.Replace(Layout.Quotes, String.Empty); - return value; + if (memberValue.IndexOf(Layout.Quotes, StringComparison.Ordinal) < 0) + { + return memberValue; + } + + return memberValue.Replace(Layout.Quotes, string.Empty); } } } \ No newline at end of file diff --git a/src/FlatFile.FixedLength.Attributes.Modern/FlatFile.FixedLength.Attributes.Modern.csproj b/src/FlatFile.FixedLength.Attributes.Modern/FlatFile.FixedLength.Attributes.Modern.csproj new file mode 100644 index 0000000..0b8b8d6 --- /dev/null +++ b/src/FlatFile.FixedLength.Attributes.Modern/FlatFile.FixedLength.Attributes.Modern.csproj @@ -0,0 +1,24 @@ + + + net8.0 + FlatFile.FixedLength.Attributes + FlatFile.FixedLength.Attributes + false + latest + disable + true + 2.0.0 + 2.0.0.0 + 2.0.0.0 + + + + + + + + + + + + diff --git a/src/FlatFile.FixedLength.Attributes/FlatFile.FixedLength.Attributes.csproj b/src/FlatFile.FixedLength.Attributes/FlatFile.FixedLength.Attributes.csproj index 5289115..32c95c4 100644 --- a/src/FlatFile.FixedLength.Attributes/FlatFile.FixedLength.Attributes.csproj +++ b/src/FlatFile.FixedLength.Attributes/FlatFile.FixedLength.Attributes.csproj @@ -84,6 +84,63 @@ 4 bin\$(Configuration)\$(Framework)\$(AssemblyName).XML + + + + v4.5.1 + AnyCPU + + + v4.5.2 + AnyCPU + + + v4.6 + AnyCPU + + + v4.6.1 + AnyCPU + + + v4.6.2 + AnyCPU + + + v4.7 + AnyCPU + + + v4.7.1 + AnyCPU + + + v4.7.2 + AnyCPU + + + v4.8 + AnyCPU + true + full + false + bin\$(Configuration)\$(Framework)\ + DEBUG;TRACE + prompt + 4 + + + v4.8 + AnyCPU + pdbonly + true + bin\$(Configuration)\$(Framework)\ + TRACE + prompt + 4 + bin\$(Configuration)\$(Framework)\FlatFile.FixedLength.Attributes.XML + + diff --git a/src/FlatFile.FixedLength.Modern/FlatFile.FixedLength.Modern.csproj b/src/FlatFile.FixedLength.Modern/FlatFile.FixedLength.Modern.csproj new file mode 100644 index 0000000..f029abe --- /dev/null +++ b/src/FlatFile.FixedLength.Modern/FlatFile.FixedLength.Modern.csproj @@ -0,0 +1,22 @@ + + + net8.0 + FlatFile.FixedLength + FlatFile.FixedLength + false + latest + disable + true + 2.0.0 + 2.0.0.0 + 2.0.0.0 + + + + + + + + + + diff --git a/src/FlatFile.FixedLength/FlatFile.FixedLength.csproj b/src/FlatFile.FixedLength/FlatFile.FixedLength.csproj index f0090b9..b1b1510 100644 --- a/src/FlatFile.FixedLength/FlatFile.FixedLength.csproj +++ b/src/FlatFile.FixedLength/FlatFile.FixedLength.csproj @@ -84,6 +84,63 @@ 4 bin\$(Configuration)\$(Framework)\FlatFile.FixedLength.XML + + + + v4.5.1 + AnyCPU + + + v4.5.2 + AnyCPU + + + v4.6 + AnyCPU + + + v4.6.1 + AnyCPU + + + v4.6.2 + AnyCPU + + + v4.7 + AnyCPU + + + v4.7.1 + AnyCPU + + + v4.7.2 + AnyCPU + + + v4.8 + AnyCPU + true + full + false + bin\$(Configuration)\$(Framework)\ + DEBUG;TRACE + prompt + 4 + + + v4.8 + AnyCPU + pdbonly + true + bin\$(Configuration)\$(Framework)\ + TRACE + prompt + 4 + bin\$(Configuration)\$(Framework)\FlatFile.FixedLength.XML + + diff --git a/src/FlatFile.FixedLength/Implementation/FixedLengthLineBuilder.cs b/src/FlatFile.FixedLength/Implementation/FixedLengthLineBuilder.cs index 088ccc6..bc0c4ef 100644 --- a/src/FlatFile.FixedLength/Implementation/FixedLengthLineBuilder.cs +++ b/src/FlatFile.FixedLength/Implementation/FixedLengthLineBuilder.cs @@ -1,8 +1,9 @@ namespace FlatFile.FixedLength.Implementation { - using System.Linq; + using System.Text; using FlatFile.Core; using FlatFile.Core.Base; + using FlatFile.Core.Extensions; public class FixedLengthLineBuilder : LineBulderBase, IFixedFieldSettingsContainer>, @@ -15,9 +16,14 @@ public FixedLengthLineBuilder(ILayoutDescriptor de public override string BuildLine(T entry) { - string line = Descriptor.Fields.Aggregate(string.Empty, - (current, field) => current + GetStringValueFromField(field, field.PropertyInfo.GetValue(entry, null))); - return line; + var lineBuilder = new StringBuilder(); + + foreach (var field in Descriptor.Fields) + { + lineBuilder.Append(GetStringValueFromField(field, PropertyAccessorCache.GetValue(field.PropertyInfo, entry))); + } + + return lineBuilder.ToString(); } protected override string TransformFieldValue(IFixedFieldSettingsContainer field, string lineValue) @@ -39,4 +45,4 @@ protected override string TransformFieldValue(IFixedFieldSettingsContainer field return lineValue; } } -} \ No newline at end of file +} diff --git a/src/FlatFile.FixedLength/Implementation/FixedLengthLineParser.cs b/src/FlatFile.FixedLength/Implementation/FixedLengthLineParser.cs index ce09de0..77495fd 100644 --- a/src/FlatFile.FixedLength/Implementation/FixedLengthLineParser.cs +++ b/src/FlatFile.FixedLength/Implementation/FixedLengthLineParser.cs @@ -4,6 +4,7 @@ namespace FlatFile.FixedLength.Implementation { using FlatFile.Core; using FlatFile.Core.Base; + using FlatFile.Core.Extensions; public class FixedLengthLineParser : LineParserBase, IFixedFieldSettingsContainer>, @@ -21,7 +22,7 @@ public override TEntity ParseLine(string line, TEntity entity) { string fieldValueFromLine = GetValueFromLine(line, linePosition, field); object convertedFieldValue = GetFieldValueFromString(field, fieldValueFromLine); - field.PropertyInfo.SetValue(entity, convertedFieldValue, null); + PropertyAccessorCache.SetValue(field.PropertyInfo, entity, convertedFieldValue); linePosition += field.Length; } return entity; @@ -52,8 +53,8 @@ private static string GetValueFromLine(string line, int linePosition, IFixedFiel protected override string TransformStringValue(IFixedFieldSettingsContainer fieldSettingsBuilder, string memberValue) { memberValue = fieldSettingsBuilder.PadLeft - ? memberValue.TrimStart(new[] {fieldSettingsBuilder.PaddingChar}) - : memberValue.TrimEnd(new[] {fieldSettingsBuilder.PaddingChar}); + ? memberValue.TrimStart(fieldSettingsBuilder.PaddingChar) + : memberValue.TrimEnd(fieldSettingsBuilder.PaddingChar); return memberValue; } diff --git a/src/FlatFile.Tests/FlatFile.Tests.csproj b/src/FlatFile.Tests/FlatFile.Tests.csproj index 57f98f9..9d1d0e0 100644 --- a/src/FlatFile.Tests/FlatFile.Tests.csproj +++ b/src/FlatFile.Tests/FlatFile.Tests.csproj @@ -31,6 +31,63 @@ prompt 4 + + + + v4.5.1 + AnyCPU + + + v4.5.2 + AnyCPU + + + v4.6 + AnyCPU + + + v4.6.1 + AnyCPU + + + v4.6.2 + AnyCPU + + + v4.7 + AnyCPU + + + v4.7.1 + AnyCPU + + + v4.7.2 + AnyCPU + + + v4.8 + AnyCPU + true + full + false + bin\$(Configuration)\$(Framework)\ + DEBUG;TRACE + prompt + 4 + + + v4.8 + AnyCPU + pdbonly + true + bin\$(Configuration)\$(Framework)\ + TRACE + prompt + 4 + bin\$(Configuration)\$(Framework)\FlatFile.Tests.XML + + ..\packages\FakeItEasy.2.2.0\lib\net40\FakeItEasy.dll diff --git a/tests/FlatFile.Modern.Tests/DelimitedParserTests.cs b/tests/FlatFile.Modern.Tests/DelimitedParserTests.cs new file mode 100644 index 0000000..2eca225 --- /dev/null +++ b/tests/FlatFile.Modern.Tests/DelimitedParserTests.cs @@ -0,0 +1,45 @@ +using FlatFile.Delimited.Implementation; +using Xunit; + +namespace FlatFile.Modern.Tests; + +public class DelimitedParserTests +{ + private sealed class Row + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } + + [Fact] + public void ParseLine_ParsesQuotedDelimitedRow() + { + var layout = new DelimitedLayout() + .WithDelimiter(";") + .WithQuote("\"") + .WithMember(x => x.Id) + .WithMember(x => x.Name); + + var parser = new DelimitedLineParser(layout); + var row = parser.ParseLine("\"1\";\"Alice\"", new Row()); + + Assert.Equal(1, row.Id); + Assert.Equal("Alice", row.Name); + } + + [Fact] + public void ParseLine_UnclosedQuote_DoesNotThrowAndParsesAvailableData() + { + var layout = new DelimitedLayout() + .WithDelimiter(";") + .WithQuote("\"") + .WithMember(x => x.Id) + .WithMember(x => x.Name); + + var parser = new DelimitedLineParser(layout); + var row = parser.ParseLine("\"2;\"Bob\"", new Row()); + + Assert.Equal(2, row.Id); + Assert.Equal("Bob", row.Name); + } +} diff --git a/tests/FlatFile.Modern.Tests/FlatFile.Modern.Tests.csproj b/tests/FlatFile.Modern.Tests/FlatFile.Modern.Tests.csproj new file mode 100644 index 0000000..dad0411 --- /dev/null +++ b/tests/FlatFile.Modern.Tests/FlatFile.Modern.Tests.csproj @@ -0,0 +1,27 @@ + + + net8.0 + enable + enable + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/tests/FlatFile.Modern.Tests/LineBuilderTests.cs b/tests/FlatFile.Modern.Tests/LineBuilderTests.cs new file mode 100644 index 0000000..68e9523 --- /dev/null +++ b/tests/FlatFile.Modern.Tests/LineBuilderTests.cs @@ -0,0 +1,42 @@ +using FlatFile.Delimited.Implementation; +using FlatFile.FixedLength.Implementation; +using Xunit; + +namespace FlatFile.Modern.Tests; + +public class LineBuilderTests +{ + private sealed class Row + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } + + [Fact] + public void DelimitedLineBuilder_BuildsExpectedLine() + { + var layout = new DelimitedLayout() + .WithDelimiter(";") + .WithQuote("\"") + .WithMember(x => x.Id) + .WithMember(x => x.Name); + + var builder = new DelimitedLineBuilder(layout); + var line = builder.BuildLine(new Row { Id = 10, Name = "Alice" }); + + Assert.Equal("\"10\";\"Alice\"", line); + } + + [Fact] + public void FixedLengthLineBuilder_BuildsExpectedLine() + { + var layout = new FixedLayout() + .WithMember(x => x.Id, c => c.WithLength(3).WithLeftPadding('0')) + .WithMember(x => x.Name, c => c.WithLength(5).WithRightPadding(' ')); + + var builder = new FixedLengthLineBuilder(layout); + var line = builder.BuildLine(new Row { Id = 7, Name = "AB" }); + + Assert.Equal("007AB ", line); + } +} diff --git a/tests/FlatFile.Modern.Tests/ReflectionHelperTests.cs b/tests/FlatFile.Modern.Tests/ReflectionHelperTests.cs new file mode 100644 index 0000000..bfdff5e --- /dev/null +++ b/tests/FlatFile.Modern.Tests/ReflectionHelperTests.cs @@ -0,0 +1,28 @@ +using FlatFile.Core.Extensions; +using Xunit; + +namespace FlatFile.Modern.Tests; + +public class ReflectionHelperTests +{ + private sealed class Sample + { + public int Value { get; } + public Sample() { Value = 42; } + public Sample(int value) { Value = value; } + } + + [Fact] + public void CreateInstance_Cached_DefaultConstructor_Works() + { + var instance = (Sample)ReflectionHelper.CreateInstance(typeof(Sample), cached: true)!; + Assert.Equal(42, instance.Value); + } + + [Fact] + public void CreateInstance_Cached_WithArguments_Works() + { + var instance = (Sample)ReflectionHelper.CreateInstance(typeof(Sample), cached: true, 7)!; + Assert.Equal(7, instance.Value); + } +} diff --git a/tests/FlatFile.Modern.Tests/TypeChangeExtensionsTests.cs b/tests/FlatFile.Modern.Tests/TypeChangeExtensionsTests.cs new file mode 100644 index 0000000..984db3e --- /dev/null +++ b/tests/FlatFile.Modern.Tests/TypeChangeExtensionsTests.cs @@ -0,0 +1,25 @@ +using System; +using FlatFile.Core.Extensions; +using Xunit; + +namespace FlatFile.Modern.Tests; + +public class TypeChangeExtensionsTests +{ + [Fact] + public void Convert_UsesInvariantCulture_ForDecimal() + { + var value = (decimal)"1234.56".Convert(typeof(decimal)); + Assert.Equal(1234.56m, value); + } + + [Fact] + public void Convert_HandlesNullableAndEnum() + { + var nullValue = (int?)"".Convert(typeof(int?)); + var day = (DayOfWeek)"friday".Convert(typeof(DayOfWeek)); + + Assert.Null(nullValue); + Assert.Equal(DayOfWeek.Friday, day); + } +}