Upgrade to .NET 8.0 and refactor input value extraction (#681)

* Upgrade to .NET 8.0

* Add tests
This commit is contained in:
Rami O. Hassan 2024-11-02 12:12:59 +01:00 committed by GitHub
parent f0fe803d6a
commit a0797a53f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 296 additions and 164 deletions

View File

@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
env:

1
.gitignore vendored
View File

@ -395,6 +395,7 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
.idea/
*.sln.iml
/BenchmarkDotNet.Artifacts

View File

@ -2,6 +2,7 @@
{
using MiniExcelLibs.Csv;
using MiniExcelLibs.OpenXml;
using MiniExcelLibs.OpenXml.SaveByTemplate;
using System;
using System.IO;
@ -49,7 +50,8 @@
switch (excelType)
{
case ExcelType.XLSX:
return new ExcelOpenXmlTemplate(stream, configuration);
var valueExtractor = new InputValueExtractor();
return new ExcelOpenXmlTemplate(stream, configuration, valueExtractor);
default:
throw new NotSupportedException($"Please Issue for me");
}

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net45;netstandard2.0;net6.0;</TargetFrameworks>
<TargetFrameworks>net45;netstandard2.0;net8.0;</TargetFrameworks>
<Version>1.34.2</Version>
</PropertyGroup>
<PropertyGroup>

View File

@ -1,154 +0,0 @@

namespace MiniExcelLibs.OpenXml
{
using MiniExcelLibs.Utils;
using MiniExcelLibs.Zip;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
internal partial class ExcelOpenXmlTemplate : IExcelTemplate, IExcelTemplateAsync
{
private static readonly XmlNamespaceManager _ns;
private static readonly Regex _isExpressionRegex;
static ExcelOpenXmlTemplate()
{
_isExpressionRegex = new Regex("(?<={{).*?(?=}})");
_ns = new XmlNamespaceManager(new NameTable());
_ns.AddNamespace("x", Config.SpreadsheetmlXmlns);
_ns.AddNamespace( "x14ac", Config.SpreadsheetmlXml_x14ac );
}
private readonly Stream _stream;
private readonly OpenXmlConfiguration _configuration;
private readonly StringBuilder _calcChainContent = new StringBuilder();
public ExcelOpenXmlTemplate(Stream stream, IConfiguration configuration)
{
_stream = stream;
_configuration = (OpenXmlConfiguration)configuration ?? OpenXmlConfiguration.DefaultConfig;
}
public void SaveAsByTemplate(string templatePath, object value)
{
using (var stream = FileHelper.OpenSharedRead(templatePath))
SaveAsByTemplateImpl(stream, value);
}
public void SaveAsByTemplate(byte[] templateBtyes, object value)
{
using (Stream stream = new MemoryStream(templateBtyes))
SaveAsByTemplateImpl(stream, value);
}
public void SaveAsByTemplateImpl(Stream templateStream, object value)
{
//only support xlsx
Dictionary<string, object> values = null;
if (value is Dictionary<string, object>)
{
values = value as Dictionary<string, object>;
foreach (var key in values.Keys)
{
var v = values[key];
if (v is IDataReader)
{
values[key] = TypeHelper.ConvertToEnumerableDictionary(v as IDataReader).ToList();
}
}
}
else
{
var type = value.GetType();
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
values = new Dictionary<string, object>();
foreach (var p in props)
{
values.Add(p.Name, p.GetValue(value));
}
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (var f in fields)
{
if (!values.ContainsKey(f.Name))
{ values.Add(f.Name, f.GetValue(value)); }
}
}
{
templateStream.CopyTo(_stream);
var reader = new ExcelOpenXmlSheetReader(_stream, null);
var _archive = new ExcelOpenXmlZip(_stream, mode: ZipArchiveMode.Update, true, Encoding.UTF8);
{
//read sharedString
var sharedStrings = reader._sharedStrings;
StringBuilder calcSheetContent = new StringBuilder();
//read all xlsx sheets
var sheets = _archive.zipFile.Entries.Where(w => w.FullName.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)
|| w.FullName.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)
).ToList();
int sheetIdx = 0;
foreach (var sheet in sheets)
{
this.XRowInfos = new List<XRowInfo>(); //every time need to use new XRowInfos or it'll cause duplicate problem: https://user-images.githubusercontent.com/12729184/115003101-0fcab700-9ed8-11eb-9151-ca4d7b86d59e.png
this.XMergeCellInfos = new Dictionary<string, XMergeCell>();
this.NewXMergeCellInfos = new List<XMergeCell>();
var sheetStream = sheet.Open();
var fullName = sheet.FullName;
ZipArchiveEntry entry = _archive.zipFile.CreateEntry(fullName);
using (var zipStream = entry.Open())
{
GenerateSheetXmlImpl(sheet, zipStream, sheetStream, values, sharedStrings, false);
//doc.Save(zipStream); //don't do it because : ![image](https://user-images.githubusercontent.com/12729184/114361127-61a5d100-9ba8-11eb-9bb9-34f076ee28a2.png)
}
// disposing writer disposes streams as well. reopen the entry to read and parse calc functions
using (var filledStream = entry.Open())
{
sheetIdx++;
_calcChainContent.Append( CalcChainHelper.GetCalcChainContent( CalcChainCellRefs, sheetIdx ) );
}
}
var calcChain = _archive.zipFile.Entries.FirstOrDefault(e => e.FullName.Contains("xl/calcChain.xml"));
if (calcChain != null)
{
string calcChainPathname = calcChain.FullName;
calcChain.Delete();
var calcChainEntry = _archive.zipFile.CreateEntry(calcChainPathname);
using (var calcChainStream = calcChainEntry.Open())
{
CalcChainHelper.GenerateCalcChainSheet(calcChainStream, _calcChainContent.ToString());
}
}
}
_archive.zipFile.Dispose();
}
}
public Task SaveAsByTemplateAsync(string templatePath, object value, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.Run(() => SaveAsByTemplate(templatePath, value), cancellationToken);
}
public Task SaveAsByTemplateAsync(byte[] templateBtyes, object value, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.Run(() => SaveAsByTemplate(templateBtyes, value), cancellationToken);
}
}
}

View File

@ -12,7 +12,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace MiniExcelLibs.OpenXml
namespace MiniExcelLibs.OpenXml.SaveByTemplate
{
internal partial class ExcelOpenXmlTemplate
{
@ -113,7 +113,7 @@ namespace MiniExcelLibs.OpenXml
public List<XMergeCell> NewXMergeCellInfos { get; private set; }
private void GenerateSheetXmlImpl(ZipArchiveEntry sheetZipEntry, Stream stream, Stream sheetStream,
Dictionary<string, object> inputMaps, IDictionary<int, string> sharedStrings,
IDictionary<string, object> inputMaps, IDictionary<int, string> sharedStrings,
bool mergeCells = false)
{
var doc = new XmlDocument();
@ -911,7 +911,7 @@ namespace MiniExcelLibs.OpenXml
}
}
private void UpdateDimensionAndGetRowsInfo(Dictionary<string, object> inputMaps, ref XmlDocument doc, ref XmlNodeList rows, bool changeRowIndex = true)
private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object> inputMaps, ref XmlDocument doc, ref XmlNodeList rows, bool changeRowIndex = true)
{
// note : dimension need to put on the top ![image](https://user-images.githubusercontent.com/12729184/114507911-5dd88400-9c66-11eb-94c6-82ed7bdb5aab.png)

View File

@ -9,7 +9,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MiniExcelLibs.OpenXml
namespace MiniExcelLibs.OpenXml.SaveByTemplate
{
internal partial class ExcelOpenXmlTemplate
{

View File

@ -0,0 +1,126 @@
namespace MiniExcelLibs.OpenXml.SaveByTemplate
{
using MiniExcelLibs.Utils;
using MiniExcelLibs.Zip;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
internal partial class ExcelOpenXmlTemplate : IExcelTemplate, IExcelTemplateAsync
{
private static readonly XmlNamespaceManager _ns;
private static readonly Regex _isExpressionRegex;
static ExcelOpenXmlTemplate()
{
_isExpressionRegex = new Regex("(?<={{).*?(?=}})");
_ns = new XmlNamespaceManager(new NameTable());
_ns.AddNamespace("x", Config.SpreadsheetmlXmlns);
_ns.AddNamespace("x14ac", Config.SpreadsheetmlXml_x14ac);
}
private readonly Stream _stream;
private readonly OpenXmlConfiguration _configuration;
private readonly IInputValueExtractor _inputValueExtractor;
private readonly StringBuilder _calcChainContent = new StringBuilder();
public ExcelOpenXmlTemplate(Stream stream, IConfiguration configuration, InputValueExtractor inputValueExtractor)
{
_stream = stream;
_configuration = (OpenXmlConfiguration)configuration ?? OpenXmlConfiguration.DefaultConfig;
_inputValueExtractor = inputValueExtractor;
}
public void SaveAsByTemplate(string templatePath, object value)
{
using (var stream = FileHelper.OpenSharedRead(templatePath))
SaveAsByTemplateImpl(stream, value);
}
public void SaveAsByTemplate(byte[] templateBtyes, object value)
{
using (Stream stream = new MemoryStream(templateBtyes))
SaveAsByTemplateImpl(stream, value);
}
public void SaveAsByTemplateImpl(Stream templateStream, object value)
{
//only support xlsx
templateStream.CopyTo(_stream);
var reader = new ExcelOpenXmlSheetReader(_stream, null);
var _archive = new ExcelOpenXmlZip(_stream, mode: ZipArchiveMode.Update, true, Encoding.UTF8);
{
//read sharedString
var sharedStrings = reader._sharedStrings;
StringBuilder calcSheetContent = new StringBuilder();
//read all xlsx sheets
var sheets = _archive.zipFile.Entries.Where(w =>
w.FullName.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)
|| w.FullName.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)
).ToList();
int sheetIdx = 0;
foreach (var sheet in sheets)
{
this.XRowInfos =
new List<XRowInfo>(); //every time need to use new XRowInfos or it'll cause duplicate problem: https://user-images.githubusercontent.com/12729184/115003101-0fcab700-9ed8-11eb-9151-ca4d7b86d59e.png
this.XMergeCellInfos = new Dictionary<string, XMergeCell>();
this.NewXMergeCellInfos = new List<XMergeCell>();
var sheetStream = sheet.Open();
var fullName = sheet.FullName;
var inputValues = _inputValueExtractor.ToValueDictionary(value);
ZipArchiveEntry entry = _archive.zipFile.CreateEntry(fullName);
using (var zipStream = entry.Open())
{
GenerateSheetXmlImpl(sheet, zipStream, sheetStream, inputValues, sharedStrings, false);
//doc.Save(zipStream); //don't do it because : ![image](https://user-images.githubusercontent.com/12729184/114361127-61a5d100-9ba8-11eb-9bb9-34f076ee28a2.png)
}
// disposing writer disposes streams as well. reopen the entry to read and parse calc functions
using (var filledStream = entry.Open())
{
sheetIdx++;
_calcChainContent.Append(CalcChainHelper.GetCalcChainContent(CalcChainCellRefs, sheetIdx));
}
}
var calcChain = _archive.zipFile.Entries.FirstOrDefault(e => e.FullName.Contains("xl/calcChain.xml"));
if (calcChain != null)
{
string calcChainPathname = calcChain.FullName;
calcChain.Delete();
var calcChainEntry = _archive.zipFile.CreateEntry(calcChainPathname);
using (var calcChainStream = calcChainEntry.Open())
{
CalcChainHelper.GenerateCalcChainSheet(calcChainStream, _calcChainContent.ToString());
}
}
}
_archive.zipFile.Dispose();
}
public Task SaveAsByTemplateAsync(string templatePath, object value,
CancellationToken cancellationToken = default(CancellationToken))
{
return Task.Run(() => SaveAsByTemplate(templatePath, value), cancellationToken);
}
public Task SaveAsByTemplateAsync(byte[] templateBtyes, object value,
CancellationToken cancellationToken = default(CancellationToken))
{
return Task.Run(() => SaveAsByTemplate(templateBtyes, value), cancellationToken);
}
}
}

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace MiniExcelLibs.OpenXml.SaveByTemplate
{
public interface IInputValueExtractor
{
IDictionary<string, object> ToValueDictionary(object valueObject);
}
}

View File

@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using MiniExcelLibs.Utils;
namespace MiniExcelLibs.OpenXml.SaveByTemplate
{
public class InputValueExtractor : IInputValueExtractor
{
public IDictionary<string, object> ToValueDictionary(object valueObject)
=> valueObject is Dictionary<string, object> valueDictionary
? GetValuesFromDictionary(valueDictionary)
: GetValuesFromObject(valueObject);
private static IDictionary<string, object> GetValuesFromDictionary(Dictionary<string, object> valueDictionary)
{
return valueDictionary
.ToDictionary(
x => x.Key,
x => x.Value is IDataReader dataReader
? TypeHelper.ConvertToEnumerableDictionary(dataReader).ToList()
: x.Value);
}
private static IDictionary<string, object> GetValuesFromObject(object valueObject)
{
var type = valueObject.GetType();
var propertyValues = type
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(property => new { property.Name, Value = property.GetValue(valueObject) });
var fieldValues = type
.GetFields(BindingFlags.Public | BindingFlags.Instance)
.Select(field => new { field.Name, Value = field.GetValue(valueObject) });
return propertyValues
.Concat(fieldValues)
.ToDictionary(x => x.Name, x => x.Value);
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFrameworks>net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<AssemblyOriginatorKeyFile>miniexcel.snk</AssemblyOriginatorKeyFile>
@ -22,6 +22,7 @@
<PackageReference Include="EPPlus" Version="4.5.3.3" />
<PackageReference Include="ExcelDataReader" Version="3.6.0" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.6.0" />
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NPOI" Version="2.7.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />

View File

@ -0,0 +1,104 @@
using System.Collections.Generic;
using System.Data;
using FluentAssertions;
using MiniExcelLibs.OpenXml.SaveByTemplate;
using Xunit;
namespace MiniExcelTests.SaveByTemplate;
public class InputValueExtractorTests
{
[Fact]
public void ToValueDictionary_Given_InputIsDictionaryWithoutDataReader_Then_Output_IsAnEquivalentDictionary()
{
var valueDictionary = new Dictionary<string, object>
{
["Name"] = "John",
["Age"] = 18,
["Fruits"] = new List<string> { "Apples, Oranges" },
};
var sut = new InputValueExtractor();
var result = sut.ToValueDictionary(valueDictionary);
result.Should().BeEquivalentTo(valueDictionary);
}
[Fact]
public void ToValueDictionary_Given_InputIsDictionaryWithDataReader_Then_DataReaderIsConvertedToListOfDictionaries()
{
var dataTable = new DataTable();
dataTable.Columns.Add("id", typeof(int));
dataTable.Columns.Add("name", typeof(string));
dataTable.Rows.Add(1, "Jack");
dataTable.Rows.Add(2, "Mike");
var expectedOutput = new List<Dictionary<string, object>>
{
new() { ["id"] = 1, ["name"] = "Jack" },
new() { ["id"] = 2, ["name"] = "Mike" }
};
var valueDictionary = new Dictionary<string, object>
{
["DataReader"] = dataTable.CreateDataReader()
};
var sut = new InputValueExtractor();
var result = sut.ToValueDictionary(valueDictionary);
result["DataReader"].Should().BeEquivalentTo(expectedOutput);
}
[Fact]
public void ToValueDictionary_Given_InputIsPocoRecord_Then_Output_IsAnEquivalentDictionary()
{
var valueObject = new PocoRecord("John", 18, new List<string> { "Apples, Oranges" });
var expectedOutput = new Dictionary<string, object>()
{
["Name"] = "John",
["Age"] = 18,
["Fruits"] = new List<string> { "Apples, Oranges" }
};
var sut = new InputValueExtractor();
var result = sut.ToValueDictionary(valueObject);
result.Should().BeEquivalentTo(expectedOutput);
}
[Fact]
public void ToValueDictionary_Given_InputIsPocoClass_Then_Output_IsAnEquivalentDictionary()
{
var valueObject = new PocoClass
{
Name = "John",
Age = 18,
Fruits = new List<string> { "Apples, Oranges" }
};
var expectedOutput = new Dictionary<string, object>()
{
["Name"] = "John",
["Age"] = 18,
["Fruits"] = new List<string> { "Apples, Oranges" }
};
var sut = new InputValueExtractor();
var result = sut.ToValueDictionary(valueObject);
result.Should().BeEquivalentTo(expectedOutput);
}
private record PocoRecord(string Name, int Age, IEnumerable<string> Fruits);
private class PocoClass
{
public string Name { get; set; }
public int Age { get; set; }
public IEnumerable<string> Fruits; // Field
};
}

View File

@ -10,7 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace MiniExcelTests
namespace MiniExcelTests.SaveByTemplate
{
public class MiniExcelTemplateAsyncTests
{

View File

@ -9,7 +9,7 @@ using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace MiniExcelTests
namespace MiniExcelTests.SaveByTemplate
{
public class MiniExcelTemplateTests
{