- [New] SaveAsByTemplate support input IEnmerable<IDicionary<string,object>> or DapperRows parameters [#201](https://github.com/shps951023/MiniExcel/issues/201)

This commit is contained in:
ITWeiHan 2021-04-18 11:59:34 +08:00
parent 61d816f477
commit fbfca6354e
9 changed files with 348 additions and 129 deletions

View File

@ -8,6 +8,7 @@
### 0.13.1
- [New] SaveAsByTemplate by template bytes, convenient to cache and support multiple users to read the same template at the same time #189
- [New] SaveAsByTemplate support input `IEnmerable<IDicionary<string,object>> or DapperRows` parameters [#201](https://github.com/shps951023/MiniExcel/issues/201)
- [Bug] Fix after stream SaveAs/SaveAsByTemplate, miniexcel will close stream [#200](https://github.com/shps951023/MiniExcel/issues/200)
### 0.13.0

View File

@ -9,6 +9,7 @@
### 0.13.1
- [New] SaveAsByTemplate 支持读取模板 byte[],方便缓存跟支持多用户同时读取同一个模板 #189
- [New] SaveAsByTemplate 支持传入 `IEnmerable<IDicionary<string,object>> 或 DapperRows` 参数 [#201](https://github.com/shps951023/MiniExcel/issues/201)
- [Bug] 修正使用 stream SaveAs/SaveAsByTemplate 系统会自动关闭流 stream [#200](https://github.com/shps951023/MiniExcel/issues/200)
### 0.13.0

View File

@ -9,6 +9,7 @@
### 0.13.1
- [New] SaveAsByTemplate 支持讀取模板 byte[],方便緩存跟支持多用戶同時讀取同一個模板 [#189](https://github.com/shps951023/MiniExcel/issues/189)
- [New] SaveAsByTemplate 支持傳入 `IEnmerable<IDicionary<string,object>> 或 DapperRows` 參數 [#201](https://github.com/shps951023/MiniExcel/issues/201)
- [Bug] 修正使用 stream SaveAs/SaveAsByTemplate 系統會自動關閉流 stream [#200](https://github.com/shps951023/MiniExcel/issues/200)
### 0.13.0

View File

@ -21,12 +21,178 @@ namespace MiniExcelLibs.OpenXml
public string IEnumerablePropName { get; set; }
public XmlElement Row { get; set; }
public Type IEnumerableGenricType { get; set; }
public IDictionary<string, PropertyInfo> PropsMap { get; set; }
public IDictionary<string, PropInfo > PropsMap { get; set; }
public bool IsDictionary { get; set; }
public IEnumerable CellIEnumerableValues { get; set; }
}
internal class PropInfo
{
public PropertyInfo PropertyInfo { get; set; }
public Type UnderlyingTypePropType { get; set; }
}
private List<XRowInfo> XRowInfos { get; set; }
private void GenerateSheetXmlImpl(ZipArchiveEntry sheetZipEntry, Stream stream, Stream sheetStream, Dictionary<string, object> inputMaps, List<string> sharedStrings, XmlWriterSettings xmlWriterSettings = null)
{
var doc = new XmlDocument();
doc.Load(sheetStream);
sheetStream.Dispose();
sheetZipEntry.Delete(); // ZipArchiveEntry can't update directly, so need to delete then create logic
var worksheet = doc.SelectSingleNode("/x:worksheet", _ns);
var sheetData = doc.SelectSingleNode("/x:worksheet/x:sheetData", _ns);
var newSheetData = sheetData.Clone(); //avoid delete lost data
var rows = newSheetData.SelectNodes($"x:row", _ns);
ReplaceSharedStringsToStr(sharedStrings, ref rows);
//Update dimension && Check if the column contains a collection and get type and properties infomations
UpdateDimensionAndGetCollectionPropertiesInfos(inputMaps, ref doc, ref rows);
#region Render cell values
//Q.Why so complex?
//A.Because try to use string stream avoid OOM when rendering rows
sheetData.RemoveAll();
sheetData.InnerText = "{{{{{{split}}}}}}"; //TODO: bad smell
var prefix = string.IsNullOrEmpty(sheetData.Prefix) ? "" : $"{sheetData.Prefix}:";
var endPrefix = string.IsNullOrEmpty(sheetData.Prefix) ? "" : $":{sheetData.Prefix}"; //![image](https://user-images.githubusercontent.com/12729184/115000066-fd02b300-9ed4-11eb-8e65-bf0014015134.png)
var contents = doc.InnerXml.Split(new string[] { $"<{prefix}sheetData>{{{{{{{{{{{{split}}}}}}}}}}}}</{prefix}sheetData>" }, StringSplitOptions.None); ;
using (var writer = new StreamWriter(stream, Encoding.UTF8))
{
writer.Write(contents[0]);
writer.Write($"<{prefix}sheetData>"); // prefix problem
int originRowIndex;
int rowIndexDiff = 0;
foreach (var xInfo in XRowInfos)
{
var row = xInfo.Row;
//TODO: some xlsx without r
originRowIndex = int.Parse(row.GetAttribute("r"));
var newRowIndex = originRowIndex + rowIndexDiff;
if (xInfo.CellIEnumerableValues != null)
{
var first = true;
foreach (var item in xInfo.CellIEnumerableValues)
{
var newRow = row.Clone() as XmlElement;
newRow.SetAttribute("r", newRowIndex.ToString());
newRow.InnerXml = row.InnerXml.Replace($"{{{{$rowindex}}}}", newRowIndex.ToString());
if (xInfo.IsDictionary)
{
var dic = item as IDictionary<string, object>;
foreach (var propInfo in xInfo.PropsMap)
{
var key = $"{{{{{xInfo.IEnumerablePropName}.{propInfo.Key}}}}}";
if (item == null) //![image](https://user-images.githubusercontent.com/12729184/114728510-bc3e5900-9d71-11eb-9721-8a414dca21a0.png)
{
newRow.InnerXml = newRow.InnerXml.Replace(key, "");
continue;
}
var cellValue = dic[propInfo.Key];
if (cellValue == null)
{
newRow.InnerXml = newRow.InnerXml.Replace(key, "");
continue;
}
var cellValueStr = ExcelOpenXmlUtils.EncodeXML(cellValue);
var type = propInfo.Value.UnderlyingTypePropType;
if (type == typeof(bool))
{
cellValueStr = (bool)cellValue ? "1" : "0";
}
else if (type == typeof(DateTime))
{
//c.SetAttribute("t", "d");
cellValueStr = ((DateTime)cellValue).ToString("yyyy-MM-dd HH:mm:ss");
}
//TODO: ![image](https://user-images.githubusercontent.com/12729184/114848248-17735880-9e11-11eb-8258-63266bda0a1a.png)
newRow.InnerXml = newRow.InnerXml.Replace(key, cellValueStr);
}
}
else
{
foreach (var propInfo in xInfo.PropsMap)
{
var prop = propInfo.Value.PropertyInfo;
var key = $"{{{{{xInfo.IEnumerablePropName}.{prop.Name}}}}}";
if (item == null) //![image](https://user-images.githubusercontent.com/12729184/114728510-bc3e5900-9d71-11eb-9721-8a414dca21a0.png)
{
newRow.InnerXml = newRow.InnerXml.Replace(key, "");
continue;
}
var cellValue = prop.GetValue(item);
if (cellValue == null)
{
newRow.InnerXml = newRow.InnerXml.Replace(key, "");
continue;
}
var cellValueStr = ExcelOpenXmlUtils.EncodeXML(cellValue);
var type = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
if (type == typeof(bool))
{
cellValueStr = (bool)cellValue ? "1" : "0";
}
else if (type == typeof(DateTime))
{
//c.SetAttribute("t", "d");
cellValueStr = ((DateTime)cellValue).ToString("yyyy-MM-dd HH:mm:ss");
}
//TODO: ![image](https://user-images.githubusercontent.com/12729184/114848248-17735880-9e11-11eb-8258-63266bda0a1a.png)
newRow.InnerXml = newRow.InnerXml.Replace(key, cellValueStr);
}
}
// note: only first time need add diff ![image](https://user-images.githubusercontent.com/12729184/114494728-6bceda80-9c4f-11eb-9685-8b5ed054eabe.png)
if (!first)
rowIndexDiff++;
first = false;
newRowIndex++;
writer.Write(CleanXml(newRow.OuterXml, endPrefix));
newRow = null;
}
}
else
{
row.SetAttribute("r", newRowIndex.ToString());
row.InnerXml = row.InnerXml.Replace($"{{{{$rowindex}}}}", newRowIndex.ToString());
writer.Write(CleanXml(row.OuterXml, endPrefix));
}
}
writer.Write($"</{prefix}sheetData>");
writer.Write(contents[1]);
}
#endregion
}
private static string CleanXml(string xml, string endPrefix)
{
//TODO: need to optimize
return xml
.Replace("xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\"", "")
.Replace($"xmlns{endPrefix}=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"", "");
}
private void ReplaceSharedStringsToStr(List<string> sharedStrings, ref XmlNodeList rows)
{
foreach (XmlElement row in rows)
@ -109,7 +275,19 @@ namespace MiniExcelLibs.OpenXml
{
xRowInfo.IEnumerablePropName = propNames[0];
xRowInfo.IEnumerableGenricType = element.GetType();
xRowInfo.PropsMap = xRowInfo.IEnumerableGenricType.GetProperties().ToDictionary(s => s.Name, s => s);
if (element is IDictionary<string, object>)
{
xRowInfo.IsDictionary = true;
var dic = element as IDictionary<string, object>;
xRowInfo.PropsMap = dic.Keys.ToDictionary(key => key, key => dic[key] != null
? new PropInfo { UnderlyingTypePropType= Nullable.GetUnderlyingType(dic[key].GetType()) ?? dic[key].GetType() }
: new PropInfo { UnderlyingTypePropType = typeof(object) } ) ;
}
else
{
xRowInfo.PropsMap = xRowInfo.IEnumerableGenricType.GetProperties()
.ToDictionary(s => s.Name, s => new PropInfo { PropertyInfo = s, UnderlyingTypePropType = Nullable.GetUnderlyingType(s.PropertyType) ?? s.PropertyType });
}
}
// ==== get demension max rowindex ====
if (!first) //avoid duplicate add first one, this row not add status ![image](https://user-images.githubusercontent.com/12729184/114851829-d2512580-9e14-11eb-8e7d-520c89a7ebee.png)
@ -124,7 +302,7 @@ namespace MiniExcelLibs.OpenXml
// auto check type https://github.com/shps951023/MiniExcel/issues/177
var prop = xRowInfo.PropsMap[propNames[1]];
var type = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; //avoid nullable
var type = prop.UnderlyingTypePropType; //avoid nullable
//
if (!xRowInfo.PropsMap.ContainsKey(propNames[1]))
throw new InvalidDataException($"{propNames[0]} doesn't have {propNames[1]} property");

View File

@ -6,6 +6,7 @@ namespace MiniExcelLibs.OpenXml
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.IO.Compression;
using System.Linq;
@ -93,121 +94,5 @@ namespace MiniExcelLibs.OpenXml
_archive.ZipFile.Dispose();
}
}
private void GenerateSheetXmlImpl(ZipArchiveEntry sheetZipEntry, Stream stream, Stream sheetStream, Dictionary<string, object> inputMaps, List<string> sharedStrings, XmlWriterSettings xmlWriterSettings = null)
{
var doc = new XmlDocument();
doc.Load(sheetStream);
sheetStream.Dispose();
sheetZipEntry.Delete(); // ZipArchiveEntry can't update directly, so need to delete then create logic
var worksheet = doc.SelectSingleNode("/x:worksheet", _ns);
var sheetData = doc.SelectSingleNode("/x:worksheet/x:sheetData", _ns);
var newSheetData = sheetData.Clone(); //avoid delete lost data
var rows = newSheetData.SelectNodes($"x:row", _ns);
ReplaceSharedStringsToStr(sharedStrings, ref rows);
//Update dimension && Check if the column contains a collection and get type and properties infomations
UpdateDimensionAndGetCollectionPropertiesInfos(inputMaps, ref doc, ref rows);
#region Render cell values
//Q.Why so complex?
//A.Because try to use string stream avoid OOM when rendering rows
sheetData.RemoveAll();
sheetData.InnerText = "{{{{{{split}}}}}}"; //TODO: bad smell
var prefix = string.IsNullOrEmpty(sheetData.Prefix) ? "" : $"{sheetData.Prefix}:";
var endPrefix = string.IsNullOrEmpty(sheetData.Prefix) ? "" : $":{sheetData.Prefix}"; //![image](https://user-images.githubusercontent.com/12729184/115000066-fd02b300-9ed4-11eb-8e65-bf0014015134.png)
var contents = doc.InnerXml.Split(new string[] { $"<{prefix}sheetData>{{{{{{{{{{{{split}}}}}}}}}}}}</{prefix}sheetData>" }, StringSplitOptions.None); ;
using (var writer = new StreamWriter(stream, Encoding.UTF8))
{
writer.Write(contents[0]);
writer.Write($"<{prefix}sheetData>"); // prefix problem
int originRowIndex;
int rowIndexDiff = 0;
foreach (var xInfo in XRowInfos)
{
var row = xInfo.Row;
//TODO: some xlsx without r
originRowIndex = int.Parse(row.GetAttribute("r"));
var newRowIndex = originRowIndex + rowIndexDiff;
if (xInfo.CellIEnumerableValues != null)
{
var first = true;
foreach (var item in xInfo.CellIEnumerableValues)
{
var newRow = row.Clone() as XmlElement;
newRow.SetAttribute("r", newRowIndex.ToString());
newRow.InnerXml = row.InnerXml.Replace($"{{{{$rowindex}}}}", newRowIndex.ToString());
foreach (var propInfo in xInfo.PropsMap)
{
var prop = propInfo.Value;
var key = $"{{{{{xInfo.IEnumerablePropName}.{prop.Name}}}}}";
if (item == null) //![image](https://user-images.githubusercontent.com/12729184/114728510-bc3e5900-9d71-11eb-9721-8a414dca21a0.png)
{
newRow.InnerXml = newRow.InnerXml.Replace(key, "");
continue;
}
var cellValue = prop.GetValue(item);
if (cellValue == null)
{
newRow.InnerXml = newRow.InnerXml.Replace(key, "");
continue;
}
var cellValueStr = ExcelOpenXmlUtils.EncodeXML(cellValue);
var type = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
if (type == typeof(bool))
{
cellValueStr = (bool)cellValue ? "1" : "0";
}
else if (type == typeof(DateTime))
{
//c.SetAttribute("t", "d");
cellValueStr = ((DateTime)cellValue).ToString("yyyy-MM-dd HH:mm:ss");
}
//TODO: ![image](https://user-images.githubusercontent.com/12729184/114848248-17735880-9e11-11eb-8258-63266bda0a1a.png)
newRow.InnerXml = newRow.InnerXml.Replace(key, cellValueStr);
}
// note: only first time need add diff ![image](https://user-images.githubusercontent.com/12729184/114494728-6bceda80-9c4f-11eb-9685-8b5ed054eabe.png)
if (!first)
rowIndexDiff++;
first = false;
newRowIndex++;
writer.Write(CleanXml(newRow.OuterXml, endPrefix));
newRow = null;
}
}
else
{
row.SetAttribute("r", newRowIndex.ToString());
row.InnerXml = row.InnerXml.Replace($"{{{{$rowindex}}}}", newRowIndex.ToString());
writer.Write(CleanXml(row.OuterXml, endPrefix));
}
}
writer.Write($"</{prefix}sheetData>");
writer.Write(contents[1]);
}
#endregion
}
private static string CleanXml(string xml,string endPrefix)
{
//TODO: need to optimize
return xml
.Replace("xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\"", "")
.Replace($"xmlns{endPrefix}=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"", "");
}
}
}

View File

@ -737,7 +737,7 @@ namespace MiniExcelLibs.Tests
{
{
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
using (var connection = GetConnection("Data Source=:memory:"))
using (var connection = Db.GetConnection("Data Source=:memory:"))
{
var rows = connection.Query(@"with cte as (select 1 id,2 val) select * from cte where 1=2");
MiniExcel.SaveAs(path, rows);
@ -816,7 +816,7 @@ namespace MiniExcelLibs.Tests
// Dapper Query
using (var connection = GetConnection("Data Source=:memory:"))
using (var connection = Db.GetConnection("Data Source=:memory:"))
{
var rows = connection.Query(@"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2");
MiniExcel.SaveAs(path, rows);
@ -837,7 +837,7 @@ namespace MiniExcelLibs.Tests
File.Delete(path);
// Empty
using (var connection = GetConnection("Data Source=:memory:"))
using (var connection = Db.GetConnection("Data Source=:memory:"))
{
var rows = connection.Query(@"with cte as (select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2)select * from cte where 1=2").ToList();
MiniExcel.SaveAs(path, rows);
@ -860,7 +860,7 @@ namespace MiniExcelLibs.Tests
// ToList
using (var connection = GetConnection("Data Source=:memory:"))
using (var connection = Db.GetConnection("Data Source=:memory:"))
{
var rows = connection.Query(@"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2").ToList();
MiniExcel.SaveAs(path, rows);
@ -950,11 +950,6 @@ namespace MiniExcelLibs.Tests
File.Delete(path);
}
private static SQLiteConnection GetConnection(string connectionString)
{
return new SQLiteConnection(connectionString);
}
[Fact()]
public void SQLiteInsertTest()
{

View File

@ -1,4 +1,5 @@
using MiniExcelLibs;
using Dapper;
using MiniExcelLibs;
using MiniExcelLibs.Tests.Utils;
using System;
using System.Collections.Generic;
@ -10,6 +11,146 @@ namespace MiniExcelTests
{
public class MiniExcelTemplateTests
{
[Fact]
public void DapperTemplateTest()
{
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString()}.xlsx");
var templatePath = @"..\..\..\..\..\samples\xlsx\TestTemplateComplex.xlsx";
var connection = Db.GetConnection("Data Source=:memory:");
var value = new Dictionary<string, object>()
{
["title"] = "FooCompany",
["managers"] = connection.Query("select 'Jack' name,'HR' department union all select 'Loan','IT'"),
["employees"] = connection.Query(@"select 'Wade' name,'HR' department union all select 'Felix','HR' union all select 'Eric','IT' union all select 'Keaton','IT'")
};
MiniExcel.SaveAsByTemplate(path, templatePath, value);
{
var rows = MiniExcel.Query(path).ToList();
Assert.Equal(9, rows.Count);
Assert.Equal("FooCompany", rows[0].A);
Assert.Equal("Jack", rows[2].B);
Assert.Equal("HR", rows[2].C);
Assert.Equal("Loan", rows[3].B);
Assert.Equal("IT", rows[3].C);
Assert.Equal("Wade", rows[5].B);
Assert.Equal("HR", rows[5].C);
Assert.Equal("Felix", rows[6].B);
Assert.Equal("HR", rows[6].C);
Assert.Equal("Eric", rows[7].B);
Assert.Equal("IT", rows[7].C);
Assert.Equal("Keaton", rows[8].B);
Assert.Equal("IT", rows[8].C);
var demension = Helpers.GetFirstSheetDimensionRefValue(path);
Assert.Equal("A1:C9", demension);
}
{
var rows = MiniExcel.Query(path, sheetName: "Sheet2").ToList();
Assert.Equal(9, rows.Count);
Assert.Equal("FooCompany", rows[0].A);
Assert.Equal("Jack", rows[2].B);
Assert.Equal("HR", rows[2].C);
Assert.Equal("Loan", rows[3].B);
Assert.Equal("IT", rows[3].C);
Assert.Equal("Wade", rows[5].B);
Assert.Equal("HR", rows[5].C);
Assert.Equal("Felix", rows[6].B);
Assert.Equal("HR", rows[6].C);
Assert.Equal("Eric", rows[7].B);
Assert.Equal("IT", rows[7].C);
Assert.Equal("Keaton", rows[8].B);
Assert.Equal("IT", rows[8].C);
var demension = Helpers.GetFirstSheetDimensionRefValue(path);
Assert.Equal("A1:C9", demension);
}
}
[Fact]
public void DictionaryTemplateTest()
{
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString()}.xlsx");
var templatePath = @"..\..\..\..\..\samples\xlsx\TestTemplateComplex.xlsx";
var value = new Dictionary<string, object>()
{
["title"] = "FooCompany",
["managers"] = new[] {
new Dictionary<string, object>{["name"]="Jack",["department"]="HR"},
new Dictionary<string, object>{["name"]="Loan",["department"]="IT"}
},
["employees"] = new[] {
new Dictionary<string, object>{["name"]="Wade",["department"]="HR"},
new Dictionary<string, object>{["name"]="Felix",["department"]="HR"},
new Dictionary<string, object>{["name"]="Eric",["department"]="IT"},
new Dictionary<string, object>{["name"]="Keaton",["department"]="IT"}
}
};
MiniExcel.SaveAsByTemplate(path, templatePath, value);
{
var rows = MiniExcel.Query(path).ToList();
Assert.Equal(9, rows.Count);
Assert.Equal("FooCompany", rows[0].A);
Assert.Equal("Jack", rows[2].B);
Assert.Equal("HR", rows[2].C);
Assert.Equal("Loan", rows[3].B);
Assert.Equal("IT", rows[3].C);
Assert.Equal("Wade", rows[5].B);
Assert.Equal("HR", rows[5].C);
Assert.Equal("Felix", rows[6].B);
Assert.Equal("HR", rows[6].C);
Assert.Equal("Eric", rows[7].B);
Assert.Equal("IT", rows[7].C);
Assert.Equal("Keaton", rows[8].B);
Assert.Equal("IT", rows[8].C);
var demension = Helpers.GetFirstSheetDimensionRefValue(path);
Assert.Equal("A1:C9", demension);
}
{
var rows = MiniExcel.Query(path, sheetName: "Sheet2").ToList();
Assert.Equal(9, rows.Count);
Assert.Equal("FooCompany", rows[0].A);
Assert.Equal("Jack", rows[2].B);
Assert.Equal("HR", rows[2].C);
Assert.Equal("Loan", rows[3].B);
Assert.Equal("IT", rows[3].C);
Assert.Equal("Wade", rows[5].B);
Assert.Equal("HR", rows[5].C);
Assert.Equal("Felix", rows[6].B);
Assert.Equal("HR", rows[6].C);
Assert.Equal("Eric", rows[7].B);
Assert.Equal("IT", rows[7].C);
Assert.Equal("Keaton", rows[8].B);
Assert.Equal("IT", rows[8].C);
var demension = Helpers.GetFirstSheetDimensionRefValue(path);
Assert.Equal("A1:C9", demension);
}
}
[Fact]
public void TestGithubProject()
{

View File

@ -0,0 +1,16 @@
/**
This Class Modified from ExcelDataReader : https://github.com/ExcelDataReader/ExcelDataReader
**/
namespace MiniExcelLibs.Tests.Utils
{
using System.Data.SQLite;
internal static class Db
{
internal static SQLiteConnection GetConnection(string connectionString)
{
return new SQLiteConnection(connectionString);
}
}
}

View File

@ -6,6 +6,7 @@ namespace MiniExcelLibs.Tests.Utils
using MiniExcelLibs.OpenXml;
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Dynamic;
using System.Globalization;
using System.IO;