Skip to content

Commit 2a2c32a

Browse files
Enhanced OpenXmlImporter with new functionality for retrieving comments and notes (mini-software#941)
* Updating xml schemas * Added implementation for extracting comments * Added public api to OpenXmlImporter for retrieving comments * Fixed some minor details in the implementation for retrieving comments * Added missing ConfiguredAsyncDisposable calls to various streams * Added ConfiguredAsyncDisposable calls to OpenXmlImporter, OpenXmlExporter and OpenXmlTemplater api methods * Added an Excel sample and tests for the comment retrieval functionality * Updated documentation
1 parent 06a66f0 commit 2a2c32a

18 files changed

Lines changed: 720 additions & 62 deletions

README-V2.md

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -342,17 +342,47 @@ var excelImporter = MiniExcel.Importers.GetOpenXmlImporter();
342342
var sheetNames = excelImporter.GetSheetNames(path);
343343
```
344344

345-
#### 6. Get the columns' names from an Excel sheet
345+
#### 6. Get the columns' names from an Excel worksheet
346346

347347
```csharp
348348
var excelImporter = MiniExcel.Importers.GetOpenXmlImporter();
349349
var columns = excelImporter.GetColumnNames(path);
350350

351351
// columns = [ColumnName1, ColumnName2, ...] when there is a header row
352352
// columns = ["A","B",...] otherwise
353+
354+
```
355+
#### 7. Retrieve Comments from an Excel worksheet
356+
357+
You can extract threaded comments and their replies from a worksheet using the `RetrieveComments` method:
358+
359+
```csharp
360+
var excelImporter = MiniExcel.Importers.GetOpenXmlImporter();
361+
var comments = excelImporter.RetrieveComments(path, sheetName: "Sheet1").Comments;
362+
363+
foreach (var comment in comments)
364+
{
365+
Console.WriteLine($"Cell: {comment.CellAddress}");
366+
Console.WriteLine($"{comment.CreatedAt:yy-MM-dd HH:mm}, {comment.Author.DisplayName}: {comment.Text}");
367+
368+
foreach (var reply in comment.Replies)
369+
{
370+
Console.WriteLine($"{reply.CreatedAt:yy-MM-dd HH:mm}, {reply.Author.DisplayName}: {reply.Text}");
371+
}
372+
}
373+
```
374+
375+
You can similarly retrieve notes as well:
376+
```csharp
377+
var notes = excelImporter.RetrieveComments(path, sheetName: "Sheet1").Notes;
378+
foreach (var note in notes)
379+
{
380+
Console.WriteLine($"Cell: {note.CellAddress}");
381+
Console.WriteLine($"{note.Author.DisplayName}: {note.Text}");
382+
}
353383
```
354384

355-
#### 7. Casting dynamic rows to IDictionary
385+
#### 8. Casting dynamic rows to IDictionary
356386

357387
Under the hood the dynamic objects returned in a query are implemented using `ExpandoObject`,
358388
making it possible to cast them to `IDictionary<string,object>`:
@@ -370,7 +400,7 @@ foreach(IDictionary<string,object> row in excelImporter.Query(path))
370400
}
371401
```
372402

373-
#### 8. Query Excel sheet as a DataTable
403+
#### 9. Query Excel worksheet as a DataTable
374404

375405
This is not recommended, as `DataTable` will forcibly load all data into memory, effectively losing the advantages MiniExcel offers.
376406

@@ -379,15 +409,15 @@ var excelImporter = MiniExcel.Importers.GetOpenXmlImporter();
379409
var table = excelImporter.QueryAsDataTable(path);
380410
```
381411

382-
#### 9. Specify what cell to start reading data from
412+
#### 10. Specify what cell to start reading data from
383413

384414
```csharp
385415
var excelImporter = MiniExcel.Importers.GetOpenXmlImporter();
386416
excelImporter.Query(path, startCell: "B3")
387417
```
388418
![image](https://user-images.githubusercontent.com/12729184/117260316-8593c400-ae81-11eb-9877-c087b7ac2b01.png)
389419

390-
#### 10. Fill Merged Cells
420+
#### 11. Fill Merged Cells
391421

392422
If the Excel sheet being queried contains merged cells it is possble to enable the option to fill every row with the merged value.
393423

@@ -410,7 +440,7 @@ Filling of cells with variable width and height is also supported
410440
>Note: The performance will take a hit when enabling the feature.
411441
>This happens because in the OpenXml standard the `mergeCells` are indicated at the bottom of the file, which leads to the need of reading the whole sheet twice.
412442
413-
#### 11. Big files and disk-based cache
443+
#### 12. Big files and disk-based cache
414444

415445
If the SharedStrings file size exceeds 5 MB, MiniExcel will default to use a local disk cache.
416446
E.g: on the file [10x100000.xlsx](https://github.com/MiniExcel/MiniExcel/files/8403819/NotDuplicateSharedStrings_10x100000.xlsx) (one million rows of data), when disabling the disk cache the maximum memory usage is 195 MB, but with disk cache enabled only 65 MB of memory are used.

src/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<ItemGroup>
3-
<PackageReference Include="Zomp.SyncMethodGenerator" Version="1.6.13" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
3+
<PackageReference Include="Zomp.SyncMethodGenerator" Version="[1.6.13]" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
44
</ItemGroup>
55
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
66
<PackageReference Include="Microsoft.Bcl.Memory" Version="10.0.5" />

src/MiniExcel.Core/WriteAdapters/DataReaderWriteAdapter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public List<MiniExcelColumnMapping> GetColumns()
1818
{
1919
var columnName = _reader.GetName(i);
2020
if (!_configuration.DynamicColumnFirst ||
21-
_configuration.DynamicColumns.Any(d => string.Equals(d.Key, columnName, StringComparison.OrdinalIgnoreCase)))
21+
_configuration.DynamicColumns?.Any(d => string.Equals(d.Key, columnName, StringComparison.OrdinalIgnoreCase)) is true)
2222
{
2323
var map = ColumnMappingsProvider.GetColumnMappingFromDynamicConfiguration(columnName, _configuration);
2424
mappings.Add(map);

src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ public async Task<int> InsertSheetAsync(string path, object value, string? sheet
1818
return rowsWritten.FirstOrDefault();
1919
}
2020

21+
#if NET8_0_OR_GREATER
22+
var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.SequentialScan);
23+
await using var disposableStream = stream.ConfigureAwait(false);
24+
#else
2125
using var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.SequentialScan);
26+
#endif
2227
return await InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration, progress, cancellationToken).ConfigureAwait(false);
2328
}
2429

@@ -46,8 +51,13 @@ public async Task<int[]> ExportAsync(string path, object value, bool printHeader
4651
throw new NotSupportedException("MiniExcel's Export does not support the .xlsm format");
4752

4853
var filePath = path.EndsWith(".xlsx", StringComparison.InvariantCultureIgnoreCase) ? path : $"{path}.xlsx" ;
49-
54+
55+
#if NET8_0_OR_GREATER
56+
var stream = overwriteFile ? File.Create(filePath) : new FileStream(filePath, FileMode.CreateNew);
57+
await using var disposableStream = stream.ConfigureAwait(false);
58+
#else
5059
using var stream = overwriteFile ? File.Create(filePath) : new FileStream(filePath, FileMode.CreateNew);
60+
#endif
5161
return await ExportAsync(stream, value, printHeader, sheetName, configuration, progress, cancellationToken).ConfigureAwait(false);
5262
}
5363

src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Dynamic;
21
using MiniExcelLib.OpenXml;
32
using OpenXmlReader = MiniExcelLib.OpenXml.OpenXmlReader;
43

@@ -16,11 +15,14 @@ public async IAsyncEnumerable<T> QueryAsync<T>(string path, string? sheetName =
1615
string startCell = "A1", bool treatHeaderAsData = false, OpenXmlConfiguration? configuration = null,
1716
[EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class, new()
1817
{
18+
#if NET8_0_OR_GREATER
19+
var stream = FileHelper.OpenSharedRead(path);
20+
await using var disposableStream = stream.ConfigureAwait(false);
21+
#else
1922
using var stream = FileHelper.OpenSharedRead(path);
20-
23+
#endif
2124
var query = QueryAsync<T>(stream, sheetName, startCell, treatHeaderAsData, configuration, cancellationToken);
2225

23-
//Foreach yield return twice reason : https://stackoverflow.com/questions/66791982/ienumerable-extract-code-lazy-loading-show-stream-was-not-readable
2426
await foreach (var item in query.ConfigureAwait(false))
2527
yield return item;
2628
}
@@ -40,7 +42,13 @@ public async IAsyncEnumerable<dynamic> QueryAsync(string path, bool useHeaderRow
4042
string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null,
4143
[EnumeratorCancellation] CancellationToken cancellationToken = default)
4244
{
45+
#if NET8_0_OR_GREATER
46+
var stream = FileHelper.OpenSharedRead(path);
47+
await using var disposableStream = stream.ConfigureAwait(false);
48+
#else
4349
using var stream = FileHelper.OpenSharedRead(path);
50+
#endif
51+
4452
await foreach (var item in QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false))
4553
yield return item;
4654
}
@@ -72,7 +80,12 @@ public async IAsyncEnumerable<dynamic> QueryRangeAsync(string path, bool useHead
7280
string? sheetName = null, string startCell = "A1", string endCell = "", OpenXmlConfiguration? configuration = null,
7381
[EnumeratorCancellation] CancellationToken cancellationToken = default)
7482
{
83+
#if NET8_0_OR_GREATER
84+
var stream = FileHelper.OpenSharedRead(path);
85+
await using var disposableStream = stream.ConfigureAwait(false);
86+
#else
7587
using var stream = FileHelper.OpenSharedRead(path);
88+
#endif
7689
await foreach (var item in QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration, cancellationToken).ConfigureAwait(false))
7790
yield return item;
7891
}
@@ -93,7 +106,12 @@ public async IAsyncEnumerable<dynamic> QueryRangeAsync(string path, bool useHead
93106
int? endColumnIndex = null, OpenXmlConfiguration? configuration = null,
94107
[EnumeratorCancellation] CancellationToken cancellationToken = default)
95108
{
109+
#if NET8_0_OR_GREATER
110+
var stream = FileHelper.OpenSharedRead(path);
111+
await using var disposableStream = stream.ConfigureAwait(false);
112+
#else
96113
using var stream = FileHelper.OpenSharedRead(path);
114+
#endif
97115
await foreach (var item in QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration, cancellationToken).ConfigureAwait(false))
98116
yield return item;
99117
}
@@ -121,7 +139,12 @@ public async Task<DataTable> QueryAsDataTableAsync(string path, bool useHeaderRo
121139
string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null,
122140
CancellationToken cancellationToken = default)
123141
{
142+
#if NET8_0_OR_GREATER
143+
var stream = FileHelper.OpenSharedRead(path);
144+
await using var disposableStream = stream.ConfigureAwait(false);
145+
#else
124146
using var stream = FileHelper.OpenSharedRead(path);
147+
#endif
125148
return await QueryAsDataTableAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false);
126149
}
127150

@@ -133,7 +156,6 @@ public async Task<DataTable> QueryAsDataTableAsync(Stream stream, bool useHeader
133156
string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null,
134157
CancellationToken cancellationToken = default)
135158
{
136-
/*Issue #279*/
137159
sheetName ??= (await GetSheetNamesAsync(stream, configuration, cancellationToken).ConfigureAwait(false)).First();
138160

139161
var dt = new DataTable(sheetName);
@@ -187,7 +209,12 @@ public async Task<DataTable> QueryAsDataTableAsync(Stream stream, bool useHeader
187209
[CreateSyncVersion]
188210
public async Task<List<string>> GetSheetNamesAsync(string path, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default)
189211
{
212+
#if NET8_0_OR_GREATER
213+
var stream = FileHelper.OpenSharedRead(path);
214+
await using var disposableStream = stream.ConfigureAwait(false);
215+
#else
190216
using var stream = FileHelper.OpenSharedRead(path);
217+
#endif
191218
return await GetSheetNamesAsync(stream, config, cancellationToken).ConfigureAwait(false);
192219
}
193220

@@ -207,7 +234,12 @@ public async Task<List<string>> GetSheetNamesAsync(Stream stream, OpenXmlConfigu
207234
[CreateSyncVersion]
208235
public async Task<List<SheetInfo>> GetSheetInformationsAsync(string path, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default)
209236
{
237+
#if NET8_0_OR_GREATER
238+
var stream = FileHelper.OpenSharedRead(path);
239+
await using var disposableStream = stream.ConfigureAwait(false);
240+
#else
210241
using var stream = FileHelper.OpenSharedRead(path);
242+
#endif
211243
return await GetSheetInformationsAsync(stream, config, cancellationToken).ConfigureAwait(false);
212244
}
213245

@@ -226,7 +258,12 @@ public async Task<List<SheetInfo>> GetSheetInformationsAsync(Stream stream, Open
226258
[CreateSyncVersion]
227259
public async Task<IList<ExcelRange>> GetSheetDimensionsAsync(string path, CancellationToken cancellationToken = default)
228260
{
261+
#if NET8_0_OR_GREATER
262+
var stream = FileHelper.OpenSharedRead(path);
263+
await using var disposableStream = stream.ConfigureAwait(false);
264+
#else
229265
using var stream = FileHelper.OpenSharedRead(path);
266+
#endif
230267
return await GetSheetDimensionsAsync(stream, cancellationToken).ConfigureAwait(false);
231268
}
232269

@@ -242,7 +279,12 @@ public async Task<ICollection<string>> GetColumnNamesAsync(string path, bool use
242279
string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null,
243280
CancellationToken cancellationToken = default)
244281
{
282+
#if NET8_0_OR_GREATER
283+
var stream = FileHelper.OpenSharedRead(path);
284+
await using var disposableStream = stream.ConfigureAwait(false);
285+
#else
245286
using var stream = FileHelper.OpenSharedRead(path);
287+
#endif
246288
return await GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false);
247289
}
248290

@@ -260,6 +302,25 @@ public async Task<ICollection<string>> GetColumnNamesAsync(Stream stream, bool u
260302
return [];
261303
}
262304

305+
[CreateSyncVersion]
306+
public async Task<CommentResultSet> RetrieveCommentsAsync(string path, string? sheetName, CancellationToken cancellationToken = default)
307+
{
308+
#if NET8_0_OR_GREATER
309+
var stream = FileHelper.OpenSharedRead(path);
310+
await using var disposableStream = stream.ConfigureAwait(false);
311+
#else
312+
using var stream = FileHelper.OpenSharedRead(path);
313+
#endif
314+
return await RetrieveCommentsAsync(stream, sheetName, cancellationToken).ConfigureAwait(false);
315+
}
316+
317+
[CreateSyncVersion]
318+
public async Task<CommentResultSet> RetrieveCommentsAsync(Stream stream, string? sheetName, CancellationToken cancellationToken = default)
319+
{
320+
using var reader = await OpenXmlReader.CreateAsync(stream, null, cancellationToken).ConfigureAwait(false);
321+
return await reader.ReadCommentsAsync(sheetName, cancellationToken).ConfigureAwait(false);
322+
}
323+
263324
#endregion
264325

265326
#region DataReader

src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ internal OpenXmlTemplater() { }
1313
[CreateSyncVersion]
1414
public async Task AddPictureAsync(string path, CancellationToken cancellationToken = default, params MiniExcelPicture[] images)
1515
{
16+
#if NET8_0_OR_GREATER
17+
var stream = File.Open(path, FileMode.OpenOrCreate);
18+
await using var disposableStream = stream.ConfigureAwait(false);
19+
#else
1620
using var stream = File.Open(path, FileMode.OpenOrCreate);
21+
#endif
1722
await MiniExcelPictureImplement.AddPictureAsync(stream, cancellationToken, images).ConfigureAwait(false);
1823
}
1924

@@ -27,15 +32,26 @@ public async Task AddPictureAsync(Stream excelStream, CancellationToken cancella
2732
public async Task FillTemplateAsync(string path, string templatePath, object value, bool overwriteFile = false,
2833
OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default)
2934
{
35+
#if NET8_0_OR_GREATER
36+
var stream = overwriteFile ? File.Create(path) : File.Open(path, FileMode.CreateNew);
37+
await using var disposableStream = stream.ConfigureAwait(false);
38+
#else
3039
using var stream = overwriteFile ? File.Create(path) : File.Open(path, FileMode.CreateNew);
40+
#endif
3141
await FillTemplateAsync(stream, templatePath, value, configuration, cancellationToken).ConfigureAwait(false);
3242
}
3343

3444
[CreateSyncVersion]
3545
public async Task FillTemplateAsync(string path, Stream templateStream, object value, bool overwriteFile = false,
3646
OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default)
3747
{
48+
#if NET8_0_OR_GREATER
49+
var stream = overwriteFile ? File.Create(path) : File.Open(path, FileMode.CreateNew);
50+
await using var disposableStream = stream.ConfigureAwait(false);
51+
#else
3852
using var stream = overwriteFile ? File.Create(path) : File.Open(path, FileMode.CreateNew);
53+
#endif
54+
3955
var template = GetOpenXmlTemplate(stream, configuration);
4056
await template.SaveAsByTemplateAsync(templateStream, value, cancellationToken).ConfigureAwait(false);
4157
}
@@ -60,7 +76,12 @@ public async Task FillTemplateAsync(Stream stream, Stream templateStream, object
6076
public async Task FillTemplateAsync(string path, byte[] templateBytes, object value, bool overwriteFile = false,
6177
OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default)
6278
{
79+
#if NET8_0_OR_GREATER
80+
var stream = overwriteFile ? File.Create(path) : File.Open(path, FileMode.CreateNew);
81+
await using var disposableStream = stream.ConfigureAwait(false);
82+
#else
6383
using var stream = overwriteFile ? File.Create(path) : File.Open(path, FileMode.CreateNew);
84+
#endif
6485
await FillTemplateAsync(stream, templateBytes, value, configuration, cancellationToken).ConfigureAwait(false);
6586
}
6687

@@ -78,7 +99,12 @@ public async Task FillTemplateAsync(Stream stream, byte[] templateBytes, object
7899
public async Task MergeSameCellsAsync(string mergedFilePath, string path,
79100
OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default)
80101
{
102+
#if NET8_0_OR_GREATER
103+
var stream = File.Create(mergedFilePath);
104+
await using var disposableStream = stream.ConfigureAwait(false);
105+
#else
81106
using var stream = File.Create(mergedFilePath);
107+
#endif
82108
await MergeSameCellsAsync(stream, path, configuration, cancellationToken).ConfigureAwait(false);
83109
}
84110

src/MiniExcel.OpenXml/Constants/Schemas.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
internal static class Schemas
44
{
5-
public const string SpreadsheetmlXmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
6-
public const string SpreadsheetmlXmlStrictns = "http://purl.oclc.org/ooxml/spreadsheetml/main";
7-
public const string SpreadsheetmlXmlRelationshipns = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
8-
public const string SpreadsheetmlXmlStrictRelationshipns = "http://purl.oclc.org/ooxml/officeDocument/relationships";
5+
public const string SpreadsheetmlXmlNs = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
6+
public const string SpreadsheetmlXmlStrictNs = "http://purl.oclc.org/ooxml/spreadsheetml/main";
7+
8+
public const string OpenXmlPackageRelationships = "http://schemas.openxmlformats.org/package/2006/relationships";
9+
public const string SpreadsheetmlXmlRelationships = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
10+
public const string SpreadsheetmlXmlStrictRelationships = "http://purl.oclc.org/ooxml/officeDocument/relationships";
11+
public const string SpreadsheetmlXmlComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
12+
public const string SpreadsheetmlXmlThreadedComment = "http://schemas.microsoft.com/office/2017/10/relationships/threadedComment";
13+
914
public const string SpreadsheetmlXmlX14Ac = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac";
10-
}
15+
public const string SpreadsheetmlXmlX18Tc = "http://schemas.microsoft.com/office/spreadsheetml/2018/threadedcomments";
16+
public const string SpreadsheetmlXmlX14R = "http://schemas.microsoft.com/office/spreadsheetml/2014/revision";
17+
}

0 commit comments

Comments
 (0)