Skip to content

Commit 9e5fea6

Browse files
Discolaiizanhzh
andauthored
Generalize excel writing with a common write adapter and implement writing IAsyncEnumerable (#712)
* Generalize excel writing with a common write adapter * Remove testcase for single row in empty table * Implement sync IExcelWriteAdapter * Remove testcase for single row in empty table * Add back support for writing IMiniExcelDataReader * Add support for writing AsyncEnumerable * Add cancellation support * Add MiniExcelWriteAdapterFactory * Implement write adapters for csv writing * cancel break change * fix build bug * fix get props bug * cancel some adjust * clean code --------- Co-authored-by: izanhzh <hzh990094740@outlook.com>
1 parent 578d98c commit 9e5fea6

18 files changed

+860
-1009
lines changed

src/MiniExcel/Csv/CsvWriter.cs

Lines changed: 102 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
using MiniExcelLibs.Utils;
2+
using MiniExcelLibs.WriteAdapter;
23
using System;
3-
using System.Collections;
44
using System.Collections.Generic;
5-
using System.Data;
65
using System.Globalization;
76
using System.IO;
87
using System.Linq;
8+
using System.Text;
99
using System.Threading;
1010
using System.Threading.Tasks;
1111

@@ -31,205 +31,152 @@ public CsvWriter(Stream stream, object value, IConfiguration configuration, bool
3131

3232
public void SaveAs()
3333
{
34-
var seperator = _configuration.Seperator.ToString();
35-
var newLine = _configuration.NewLine;
34+
if (_value == null)
3635
{
37-
if (_value == null)
38-
{
39-
_writer.Write("");
40-
this._writer.Flush();
41-
return;
42-
}
43-
44-
var type = _value.GetType();
45-
46-
if (_value is IDataReader dataReader)
47-
{
48-
GenerateSheetByIDataReader(dataReader, seperator, newLine, _writer);
49-
}
50-
else if (_value is IEnumerable enumerable)
51-
{
52-
GenerateSheetByIEnumerable(enumerable, seperator, newLine, _writer);
53-
}
54-
else if (_value is DataTable dataTable)
55-
{
56-
GenerateSheetByDataTable(_writer, dataTable, seperator, newLine);
57-
}
58-
else
59-
{
60-
throw new NotImplementedException($"Type {type?.Name} not Implemented. please issue for me.");
61-
}
62-
63-
this._writer.Flush();
36+
_writer.Write("");
37+
_writer.Flush();
38+
return;
6439
}
65-
}
6640

67-
public async Task SaveAsAsync(CancellationToken cancellationToken = default)
68-
{
69-
await Task.Run(() => SaveAs(), cancellationToken).ConfigureAwait(false);
41+
WriteValues(_writer, _value);
42+
_writer.Flush();
7043
}
7144

7245
public void Insert(bool overwriteSheet = false)
7346
{
7447
SaveAs();
7548
}
7649

77-
public async Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default)
50+
private void AppendColumn(StringBuilder rowBuilder, CellWriteInfo column)
7851
{
79-
await Task.Run(() => SaveAs(), cancellationToken).ConfigureAwait(false);
52+
rowBuilder.Append(CsvHelpers.ConvertToCsvValue(ToCsvString(column.Value, column.Prop), _configuration.AlwaysQuote, _configuration.Seperator));
53+
rowBuilder.Append(_configuration.Seperator);
8054
}
8155

82-
private void GenerateSheetByIEnumerable(IEnumerable values, string seperator, string newLine, StreamWriter writer)
56+
private void RemoveTrailingSeparator(StringBuilder rowBuilder)
8357
{
84-
Type genericType = null;
85-
List<ExcelColumnInfo> props = null;
86-
string mode = null;
87-
88-
var enumerator = values.GetEnumerator();
89-
var empty = !enumerator.MoveNext();
90-
if (empty)
91-
{
92-
// only when empty IEnumerable need to check this issue #133 https://github.com/shps951023/MiniExcel/issues/133
93-
genericType = TypeHelper.GetGenericIEnumerables(values).FirstOrDefault();
94-
if (genericType == null || genericType == typeof(object) // sometime generic type will be object, e.g: https://user-images.githubusercontent.com/12729184/132812859-52984314-44d1-4ee8-9487-2d1da159f1f0.png
95-
|| typeof(IDictionary<string, object>).IsAssignableFrom(genericType)
96-
|| typeof(IDictionary).IsAssignableFrom(genericType)
97-
|| typeof(KeyValuePair<string, object>).IsAssignableFrom(genericType))
98-
{
99-
_writer.Write(newLine);
100-
this._writer.Flush();
101-
return;
102-
}
103-
104-
mode = "Properties";
105-
props = CustomPropertyHelper.GetSaveAsProperties(genericType, _configuration);
106-
}
107-
else
58+
if (rowBuilder.Length == 0)
10859
{
109-
var firstItem = enumerator.Current;
110-
if (firstItem is IDictionary<string, object> genericDic)
111-
{
112-
mode = "IDictionary<string, object>";
113-
props = CustomPropertyHelper.GetDictionaryColumnInfo(genericDic, null, _configuration);
114-
}
115-
else if (firstItem is IDictionary dic)
116-
{
117-
mode = "IDictionary";
118-
props = CustomPropertyHelper.GetDictionaryColumnInfo(null, dic, _configuration);
119-
mode = "IDictionary";
120-
}
121-
else
122-
{
123-
mode = "Properties";
124-
genericType = firstItem.GetType();
125-
props = CustomPropertyHelper.GetSaveAsProperties(genericType, _configuration);
126-
}
127-
}
128-
129-
if (this._printHeader)
130-
{
131-
_writer.Write(string.Join(seperator, props.Select(s => CsvHelpers.ConvertToCsvValue(s?.ExcelColumnName, _configuration.AlwaysQuote, _configuration.Seperator))));
132-
_writer.Write(newLine);
133-
}
134-
135-
if (!empty)
136-
{
137-
if (mode == "IDictionary<string, object>") //Dapper Row
138-
GenerateSheetByDapperRow(_writer, enumerator, props.Select(x => x.Key.ToString()).ToList(), seperator, newLine);
139-
else if (mode == "IDictionary") //IDictionary
140-
GenerateSheetByIDictionary(_writer, enumerator, props.Select(x => x.Key).ToList(), seperator, newLine);
141-
else if (mode == "Properties")
142-
GenerateSheetByProperties(_writer, enumerator, props, seperator, newLine);
143-
else
144-
throw new NotImplementedException($"Mode for genericType {genericType?.Name} not Implemented. please issue for me.");
60+
return;
14561
}
62+
rowBuilder.Remove(rowBuilder.Length - 1, 1);
14663
}
14764

148-
private void GenerateSheetByIDataReader(IDataReader reader, string seperator, string newLine, StreamWriter writer)
65+
private string GetHeader(List<ExcelColumnInfo> props) => string.Join(
66+
_configuration.Seperator.ToString(),
67+
props.Select(s => CsvHelpers.ConvertToCsvValue(s?.ExcelColumnName, _configuration.AlwaysQuote, _configuration.Seperator)));
68+
69+
private void WriteValues(StreamWriter writer, object values)
14970
{
150-
int fieldCount = reader.FieldCount;
151-
if (fieldCount == 0)
152-
throw new InvalidDataException("fieldCount is 0");
71+
IMiniExcelWriteAdapter writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration);
15372

154-
if (this._printHeader)
73+
var props = writeAdapter.GetColumns();
74+
if (props == null)
15575
{
156-
for (int i = 0; i < fieldCount; i++)
157-
{
158-
var columnName = reader.GetName(i);
76+
_writer.Write(_configuration.NewLine);
77+
_writer.Flush();
78+
return;
79+
}
15980

160-
if (i != 0)
161-
writer.Write(seperator);
162-
writer.Write(CsvHelpers.ConvertToCsvValue(ToCsvString(columnName, null), _configuration.AlwaysQuote, _configuration.Seperator));
163-
}
164-
writer.Write(newLine);
81+
if (_printHeader)
82+
{
83+
_writer.Write(GetHeader(props));
84+
_writer.Write(_configuration.NewLine);
16585
}
16686

167-
while (reader.Read())
87+
var rowBuilder = new StringBuilder();
88+
if (writeAdapter != null)
16889
{
169-
for (int i = 0; i < fieldCount; i++)
90+
foreach (var row in writeAdapter.GetRows(props))
17091
{
171-
var cellValue = reader.GetValue(i);
172-
if (i != 0)
173-
writer.Write(seperator);
174-
writer.Write(CsvHelpers.ConvertToCsvValue(ToCsvString(cellValue, null), _configuration.AlwaysQuote, _configuration.Seperator));
92+
rowBuilder.Clear();
93+
foreach (var column in row)
94+
{
95+
AppendColumn(rowBuilder, column);
96+
}
97+
RemoveTrailingSeparator(rowBuilder);
98+
_writer.Write(rowBuilder.ToString());
99+
_writer.Write(_configuration.NewLine);
175100
}
176-
writer.Write(newLine);
177101
}
178102
}
179103

180-
private void GenerateSheetByDataTable(StreamWriter writer, DataTable dt, string seperator, string newLine)
104+
private async Task WriteValuesAsync(StreamWriter writer, object values, string seperator, string newLine, CancellationToken cancellationToken)
181105
{
106+
#if NETSTANDARD2_0_OR_GREATER || NET
107+
IMiniExcelWriteAdapter writeAdapter = null;
108+
if (!MiniExcelWriteAdapterFactory.TryGetAsyncWriteAdapter(values, _configuration, out var asyncWriteAdapter))
109+
{
110+
writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration);
111+
}
112+
var props = writeAdapter != null ? writeAdapter.GetColumns() : await asyncWriteAdapter.GetColumnsAsync();
113+
#else
114+
IMiniExcelWriteAdapter writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration);
115+
var props = writeAdapter.GetColumns();
116+
#endif
117+
if (props == null)
118+
{
119+
await _writer.WriteAsync(_configuration.NewLine);
120+
await _writer.FlushAsync();
121+
return;
122+
}
182123
if (_printHeader)
183124
{
184-
writer.Write(string.Join(seperator, dt.Columns.Cast<DataColumn>().Select(s => CsvHelpers.ConvertToCsvValue(s.Caption ?? s.ColumnName, _configuration.AlwaysQuote, _configuration.Seperator))));
185-
writer.Write(newLine);
125+
await _writer.WriteAsync(GetHeader(props));
126+
await _writer.WriteAsync(newLine);
186127
}
187-
for (int i = 0; i < dt.Rows.Count; i++)
128+
var rowBuilder = new StringBuilder();
129+
if (writeAdapter != null)
188130
{
189-
var first = true;
190-
for (int j = 0; j < dt.Columns.Count; j++)
131+
foreach (var row in writeAdapter.GetRows(props, cancellationToken))
191132
{
192-
var cellValue = CsvHelpers.ConvertToCsvValue(ToCsvString(dt.Rows[i][j], null), _configuration.AlwaysQuote, _configuration.Seperator);
193-
if (!first)
194-
writer.Write(seperator);
195-
writer.Write(cellValue);
196-
first = false;
133+
rowBuilder.Clear();
134+
foreach (var column in row)
135+
{
136+
AppendColumn(rowBuilder, column);
137+
}
138+
RemoveTrailingSeparator(rowBuilder);
139+
await _writer.WriteAsync(rowBuilder.ToString());
140+
await _writer.WriteAsync(newLine);
197141
}
198-
writer.Write(newLine);
199142
}
200-
}
201-
202-
private void GenerateSheetByProperties(StreamWriter writer, IEnumerator value, List<ExcelColumnInfo> props, string seperator, string newLine)
203-
{
204-
do
143+
#if NETSTANDARD2_0_OR_GREATER || NET
144+
else
205145
{
206-
var v = value.Current;
207-
var values = props.Select(s => CsvHelpers.ConvertToCsvValue(ToCsvString(s?.Property.GetValue(v), s), _configuration.AlwaysQuote, _configuration.Seperator));
208-
writer.Write(string.Join(seperator, values));
209-
writer.Write(newLine);
210-
} while (value.MoveNext());
146+
await foreach (var row in asyncWriteAdapter.GetRowsAsync(props, cancellationToken))
147+
{
148+
rowBuilder.Clear();
149+
await foreach (var column in row)
150+
{
151+
AppendColumn(rowBuilder, column);
152+
}
153+
RemoveTrailingSeparator(rowBuilder);
154+
await _writer.WriteAsync(rowBuilder.ToString());
155+
await _writer.WriteAsync(newLine);
156+
}
157+
}
158+
#endif
211159
}
212160

213-
private void GenerateSheetByIDictionary(StreamWriter writer, IEnumerator value, List<object> keys, string seperator, string newLine)
161+
public async Task SaveAsAsync(CancellationToken cancellationToken = default)
214162
{
215-
do
163+
var seperator = _configuration.Seperator.ToString();
164+
var newLine = _configuration.NewLine;
165+
166+
if (_value == null)
216167
{
217-
var v = (IDictionary)value.Current;
218-
var values = keys.Select(key => CsvHelpers.ConvertToCsvValue(ToCsvString(v[key], null), _configuration.AlwaysQuote, _configuration.Seperator));
219-
writer.Write(string.Join(seperator, values));
220-
writer.Write(newLine);
221-
} while (value.MoveNext());
168+
await _writer.WriteAsync("");
169+
await _writer.FlushAsync();
170+
return;
171+
}
172+
173+
await WriteValuesAsync(_writer, _value, seperator, newLine, cancellationToken);
174+
await _writer.FlushAsync();
222175
}
223176

224-
private void GenerateSheetByDapperRow(StreamWriter writer, IEnumerator value, List<string> keys, string seperator, string newLine)
177+
public async Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default)
225178
{
226-
do
227-
{
228-
var v = (IDictionary<string, object>)value.Current;
229-
var values = keys.Select(key => CsvHelpers.ConvertToCsvValue(ToCsvString(v[key], null), _configuration.AlwaysQuote, _configuration.Seperator));
230-
writer.Write(string.Join(seperator, values));
231-
writer.Write(newLine);
232-
} while (value.MoveNext());
179+
await SaveAsAsync(cancellationToken);
233180
}
234181

235182
public string ToCsvString(object value, ExcelColumnInfo p)

src/MiniExcel/MiniExcelLibs.csproj

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
<TargetFrameworks>net45;netstandard2.0;net8.0;</TargetFrameworks>
44
<Version>1.36.1</Version>
55
</PropertyGroup>
6+
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
7+
<LangVersion>8</LangVersion>
8+
</PropertyGroup>
69
<PropertyGroup>
710
<AssemblyName>MiniExcel</AssemblyName>
811
<Company>Mini-Software</Company>
@@ -35,9 +38,6 @@ Todo : https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true</De
3538
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
3639
<PackageReadmeFile>README.md</PackageReadmeFile>
3740
</PropertyGroup>
38-
<ItemGroup Condition=" '$(TargetFramework)' == 'net461'">
39-
<Reference Include="System.IO.Compression" />
40-
</ItemGroup>
4141
<ItemGroup Condition=" '$(TargetFramework)' == 'net45'">
4242
<Reference Include="System.IO.Compression" />
4343
</ItemGroup>
@@ -56,4 +56,9 @@ Todo : https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true</De
5656
<ItemGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
5757
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
5858
</ItemGroup>
59+
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
60+
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces">
61+
<Version>9.0.0</Version>
62+
</PackageReference>
63+
</ItemGroup>
5964
</Project>

0 commit comments

Comments
 (0)