diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 68fa7de..c643e47 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -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: diff --git a/.gitignore b/.gitignore index ba3deb0..dc80225 100644 --- a/.gitignore +++ b/.gitignore @@ -395,6 +395,7 @@ FodyWeavers.xsd *.msp # JetBrains Rider +.idea/ *.sln.iml /BenchmarkDotNet.Artifacts diff --git a/src/MiniExcel/ExcelFactory.cs b/src/MiniExcel/ExcelFactory.cs index 3dfa468..d63998b 100644 --- a/src/MiniExcel/ExcelFactory.cs +++ b/src/MiniExcel/ExcelFactory.cs @@ -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"); } diff --git a/src/MiniExcel/MiniExcelLibs.csproj b/src/MiniExcel/MiniExcelLibs.csproj index efadbac..db9dff8 100644 --- a/src/MiniExcel/MiniExcelLibs.csproj +++ b/src/MiniExcel/MiniExcelLibs.csproj @@ -1,6 +1,6 @@  - net45;netstandard2.0;net6.0; + net45;netstandard2.0;net8.0; 1.34.2 diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.cs deleted file mode 100644 index d290695..0000000 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.cs +++ /dev/null @@ -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 values = null; - if (value is Dictionary) - { - values = value as Dictionary; - 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(); - 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(); //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(); - this.NewXMergeCellInfos = new List(); - - 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); - } - } -} diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs similarity index 97% rename from src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs rename to src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs index 40cb8bc..cf716bc 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs @@ -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 NewXMergeCellInfos { get; private set; } private void GenerateSheetXmlImpl(ZipArchiveEntry sheetZipEntry, Stream stream, Stream sheetStream, - Dictionary inputMaps, IDictionary sharedStrings, + IDictionary inputMaps, IDictionary sharedStrings, bool mergeCells = false) { var doc = new XmlDocument(); @@ -911,7 +911,7 @@ namespace MiniExcelLibs.OpenXml } } - private void UpdateDimensionAndGetRowsInfo(Dictionary inputMaps, ref XmlDocument doc, ref XmlNodeList rows, bool changeRowIndex = true) + private void UpdateDimensionAndGetRowsInfo(IDictionary 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) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.MergeCells.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.MergeCells.cs similarity index 96% rename from src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.MergeCells.cs rename to src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.MergeCells.cs index 900086a..304879d 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.MergeCells.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.MergeCells.cs @@ -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 { diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs new file mode 100644 index 0000000..8578023 --- /dev/null +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs @@ -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(); //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(); + this.NewXMergeCellInfos = new List(); + + 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); + } + } +} \ No newline at end of file diff --git a/src/MiniExcel/SaveByTemplate/IInputValueExtractor.cs b/src/MiniExcel/SaveByTemplate/IInputValueExtractor.cs new file mode 100644 index 0000000..ae131da --- /dev/null +++ b/src/MiniExcel/SaveByTemplate/IInputValueExtractor.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MiniExcelLibs.OpenXml.SaveByTemplate +{ + public interface IInputValueExtractor + { + IDictionary ToValueDictionary(object valueObject); + } +} \ No newline at end of file diff --git a/src/MiniExcel/SaveByTemplate/InputValueExtractor.cs b/src/MiniExcel/SaveByTemplate/InputValueExtractor.cs new file mode 100644 index 0000000..3178bad --- /dev/null +++ b/src/MiniExcel/SaveByTemplate/InputValueExtractor.cs @@ -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 ToValueDictionary(object valueObject) + => valueObject is Dictionary valueDictionary + ? GetValuesFromDictionary(valueDictionary) + : GetValuesFromObject(valueObject); + + private static IDictionary GetValuesFromDictionary(Dictionary valueDictionary) + { + return valueDictionary + .ToDictionary( + x => x.Key, + x => x.Value is IDataReader dataReader + ? TypeHelper.ConvertToEnumerableDictionary(dataReader).ToList() + : x.Value); + } + + private static IDictionary 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); + } + } +} \ No newline at end of file diff --git a/tests/MiniExcelTests/MiniExcelTests.csproj b/tests/MiniExcelTests/MiniExcelTests.csproj index c47a8db..28c5d2d 100644 --- a/tests/MiniExcelTests/MiniExcelTests.csproj +++ b/tests/MiniExcelTests/MiniExcelTests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false miniexcel.snk @@ -22,6 +22,7 @@ + diff --git a/tests/MiniExcelTests/SaveByTemplate/InputValueExtractorTests.cs b/tests/MiniExcelTests/SaveByTemplate/InputValueExtractorTests.cs new file mode 100644 index 0000000..5b819e3 --- /dev/null +++ b/tests/MiniExcelTests/SaveByTemplate/InputValueExtractorTests.cs @@ -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 + { + ["Name"] = "John", + ["Age"] = 18, + ["Fruits"] = new List { "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> + { + new() { ["id"] = 1, ["name"] = "Jack" }, + new() { ["id"] = 2, ["name"] = "Mike" } + }; + + var valueDictionary = new Dictionary + { + ["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 { "Apples, Oranges" }); + + var expectedOutput = new Dictionary() + { + ["Name"] = "John", + ["Age"] = 18, + ["Fruits"] = new List { "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 { "Apples, Oranges" } + }; + + var expectedOutput = new Dictionary() + { + ["Name"] = "John", + ["Age"] = 18, + ["Fruits"] = new List { "Apples, Oranges" } + }; + + var sut = new InputValueExtractor(); + var result = sut.ToValueDictionary(valueObject); + + result.Should().BeEquivalentTo(expectedOutput); + } + + + private record PocoRecord(string Name, int Age, IEnumerable Fruits); + + private class PocoClass + { + public string Name { get; set; } + public int Age { get; set; } + public IEnumerable Fruits; // Field + }; +} \ No newline at end of file diff --git a/tests/MiniExcelTests/MiniExcelTemplateAsyncTests.cs b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateAsyncTests.cs similarity index 97% rename from tests/MiniExcelTests/MiniExcelTemplateAsyncTests.cs rename to tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateAsyncTests.cs index 01f93aa..0a3174c 100644 --- a/tests/MiniExcelTests/MiniExcelTemplateAsyncTests.cs +++ b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateAsyncTests.cs @@ -10,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -namespace MiniExcelTests +namespace MiniExcelTests.SaveByTemplate { public class MiniExcelTemplateAsyncTests { diff --git a/tests/MiniExcelTests/MiniExcelTemplateTests.cs b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateTests.cs similarity index 97% rename from tests/MiniExcelTests/MiniExcelTemplateTests.cs rename to tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateTests.cs index fd50fbc..661b5b3 100644 --- a/tests/MiniExcelTests/MiniExcelTemplateTests.cs +++ b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateTests.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -namespace MiniExcelTests +namespace MiniExcelTests.SaveByTemplate { public class MiniExcelTemplateTests {