Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<LangVersion>latest</LangVersion>
<RootNamespace>MiniExcelLib.Benchmarks</RootNamespace>
<NoWarn>$(NoWarn);CA2000;CA2007</NoWarn>
<AcceptNPOIOSMFLicense>true</AcceptNPOIOSMFLicense>
</PropertyGroup>

<ItemGroup>
Expand Down
5 changes: 3 additions & 2 deletions src/MiniExcel.Core/Helpers/FileHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

public static class FileHelper
{
public static FileStream OpenSharedRead(string path) => File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
public static FileStream OpenSharedRead(string path)
=> File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
3 changes: 2 additions & 1 deletion src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public static async Task AddPictureAsync(Stream excelStream, CancellationToken c
var excelArchive = await OpenXmlZip.CreateAsync(excelStream, cancellationToken: cancellationToken).ConfigureAwait(false);
await using var disposableExcelArchive = excelArchive.ConfigureAwait(false);

using var reader = await OpenXmlReader.CreateAsync(excelStream, null, false, cancellationToken).ConfigureAwait(false);
var reader = await OpenXmlReader.CreateAsync(excelStream, null, false, cancellationToken).ConfigureAwait(false);
await using var dispoableReader = reader.ConfigureAwait(false);

#if NET10_0_OR_GREATER
var archive = await ZipArchive.CreateAsync(excelStream, ZipArchiveMode.Update, true, null, cancellationToken).ConfigureAwait(false);
Expand Down
107 changes: 84 additions & 23 deletions src/MiniExcel/MiniExcelConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,112 @@

namespace MiniExcelLib;

/// <summary>
/// Provides methods for converting between CSV and OpenXml (XLSX) documents.
/// </summary>
public static partial class MiniExcelConverter
{
/// <remarks>The origin stream is left open.</remarks>
/// <summary>
/// Converts a CSV file to an OpenXml (XLSX) file.
/// </summary>
/// <param name="csvPath">The file path to the CSV file to be converted.</param>
/// <param name="xlsxPath">The file path where the resulting XLSX file will be created.</param>
/// <param name="csvHasHeader">If true, the first row will be treated as headers and printed in the Excel file. Otherwise, all rows are treated as data. Default is false</param>
/// <param name="cancellationToken"> A cancellation token to signal that the operation should be cancelled.</param>
/// <returns>
/// A task that completes when the OpenXml file has been successfully created.
/// </returns>
[CreateSyncVersion]
public static async Task ConvertCsvToXlsxAsync(Stream csv, Stream xlsx, bool csvHasHeader = false, CancellationToken cancellationToken = default)
public static async Task ConvertCsvToXlsxAsync(string csvPath, string xlsxPath, bool csvHasHeader = false, CancellationToken cancellationToken = default)
{
var value = MiniExcel.Importers
.GetCsvImporter()
.QueryAsync(csv, hasHeaderRow: csvHasHeader, leaveOpen: true, cancellationToken: cancellationToken);
#if SYNC_ONLY
using var csvStream = MiniExcelLib.Core.Helpers.FileHelper.OpenSharedRead(csvPath);
using var xlsxStream = new FileStream(xlsxPath, FileMode.CreateNew);
#else
var csvStream = FileHelper.OpenSharedRead(csvPath);
await using var disposableCsvStream = csvStream.ConfigureAwait(false);

await MiniExcel.Exporters
.GetOpenXmlExporter()
.ExportAsync(xlsx, value, printHeader: csvHasHeader, cancellationToken: cancellationToken)
.ConfigureAwait(false);
var xlsxStream = new FileStream(xlsxPath, FileMode.CreateNew);
await using var disposableXlsxStream = xlsxStream.ConfigureAwait(false);
#endif

await ConvertCsvToXlsxAsync(csvStream, xlsxStream, csvHasHeader, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Converts CSV data from a stream to OpenXml (XLSX) format in another stream.
/// </summary>
/// <param name="csvStream">A readable stream containing CSV data.</param>
/// <param name="xlsxStream">A writable stream where the Excel data will be written.</param>
/// <param name="csvHasHeader">If true, the first row will be treated as headers and printed in the Excel file. Otherwise, all rows are treated as data. Default is false.</param>
/// <param name="cancellationToken">A cancellation token to signal that the operation should be cancelled.</param>
/// <returns>
/// A task that completes when the OpenXml data has been successfully written to the output stream.
/// </returns>
/// <remarks>
/// The streams will not be closed by this method; the caller is responsible for disposing them.
/// </remarks>
[CreateSyncVersion]
public static async Task ConvertCsvToXlsxAsync(string csvPath, string xlsx, bool csvHasHeader = false, CancellationToken cancellationToken = default)
public static async Task ConvertCsvToXlsxAsync(Stream csvStream, Stream xlsxStream, bool csvHasHeader = false, CancellationToken cancellationToken = default)
{
using var csvStream = FileHelper.OpenSharedRead(csvPath);
using var xlsxStream = new FileStream(xlsx, FileMode.CreateNew);
var value = MiniExcel.Importers.GetCsvImporter()
.QueryAsync(csvStream, hasHeaderRow: csvHasHeader, leaveOpen: true, cancellationToken: cancellationToken)
.ConfigureAwait(false);

await ConvertCsvToXlsxAsync(csvStream, xlsxStream, csvHasHeader, cancellationToken).ConfigureAwait(false);
await MiniExcel.Exporters.GetOpenXmlExporter()
.ExportAsync(xlsxStream, value, printHeader: csvHasHeader, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Converts an OpenXml (XLSX) file to a CSV file.
/// </summary>
/// <param name="xlsxPath">The file path to the XLSX file to be converted.</param>
/// <param name="csvPath">The file path where the resulting CSV file will be created.</param>
/// <param name="xlsxHasHeader">If true, the first row will be treated as headers and printed in the CSV file. Otherwise, all rows are treated as data. Default is false.</param>
/// <param name="cancellationToken"> A cancellation token to signal that the operation should be cancelled.</param>
/// <returns>
/// A task that completes when the CSV file has been successfully created.
/// </returns>
[CreateSyncVersion]
public static async Task ConvertXlsxToCsvAsync(string xlsx, string csvPath, bool xlsxHasHeader = true, CancellationToken cancellationToken = default)
public static async Task ConvertXlsxToCsvAsync(string xlsxPath, string csvPath, bool xlsxHasHeader = true, CancellationToken cancellationToken = default)
{
using var xlsxStream = FileHelper.OpenSharedRead(xlsx);
#if SYNC_ONLY
using var xlsxStream = MiniExcelLib.Core.Helpers.FileHelper.OpenSharedRead(xlsxPath);
using var csvStream = new FileStream(csvPath, FileMode.CreateNew);
#else
var xlsxStream = FileHelper.OpenSharedRead(xlsxPath);
await using var disposableXlsxStream = xlsxStream.ConfigureAwait(false);

var csvStream = new FileStream(csvPath, FileMode.CreateNew);
await using var disposableCsvStream = csvStream.ConfigureAwait(false);
#endif

await ConvertXlsxToCsvAsync(xlsxStream, csvStream, xlsxHasHeader, cancellationToken).ConfigureAwait(false);
}

/// <remarks>The origin stream is left open.</remarks>
/// <summary>
/// Converts OpenXml (XLSX) data from a stream to CSV format in another stream.
/// </summary>
Comment thread
michelebastione marked this conversation as resolved.
/// <param name="xlsxStream">A readable stream containing the OpenXml data.</param>
/// <param name="csvStream">A writable stream where the CSV data will be written.</param>
/// <param name="xlsxHasHeader">If true, the first row will be treated as headers and printed in the Excel file. Otherwise, all rows are treated as data. Default is false.</param>
/// <param name="cancellationToken">A cancellation token to signal that the operation should be cancelled.</param>
/// <returns>
/// A task that completes when the CSV data has been successfully written to the output stream.
/// </returns>
/// <remarks>
/// The streams will not be closed by this method; the caller is responsible for disposing them.
/// </remarks>
[CreateSyncVersion]
public static async Task ConvertXlsxToCsvAsync(Stream xlsx, Stream csv, bool xlsxHasHeader = true, CancellationToken cancellationToken = default)
public static async Task ConvertXlsxToCsvAsync(Stream xlsxStream, Stream csvStream, bool xlsxHasHeader = true, CancellationToken cancellationToken = default)
{
var value = MiniExcel.Importers
.GetOpenXmlImporter()
.QueryAsync(xlsx, hasHeaderRow: xlsxHasHeader, leaveOpen: true, cancellationToken: cancellationToken)
var value = MiniExcel.Importers.GetOpenXmlImporter()
.QueryAsync(xlsxStream, hasHeaderRow: xlsxHasHeader, leaveOpen: true, cancellationToken: cancellationToken)
.ConfigureAwait(false);

await MiniExcel.Exporters
.GetCsvExporter()
.ExportAsync(csv, value, printHeader: xlsxHasHeader, cancellationToken: cancellationToken).ConfigureAwait(false);
await MiniExcel.Exporters.GetCsvExporter()
.ExportAsync(csvStream, value, printHeader: xlsxHasHeader, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
}
4 changes: 2 additions & 2 deletions tests/MiniExcel.Csv.Tests/Main/MiniExcelCsvTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ public void ExportAndQueryMixedFieldAndPropertyTest()
using var reader = new StreamReader(path);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
var records = csv.GetRecords<dynamic>().ToList();
var first = records[0] as IDictionary<string, object>;
var first = (IDictionary<string, object>)records[0];

Assert.Contains("F1", first.Keys);
Assert.Contains("P1", first.Keys);
Expand All @@ -622,7 +622,7 @@ public void ExportAndQueryFieldsWithoutAttributeTest()
using var reader = new StreamReader(path);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
var records = csv.GetRecords<dynamic>().ToList();
var first = records[0] as IDictionary<string, object>;
var first = (IDictionary<string, object>)records[0];

Assert.Contains("Mapped", first.Keys);
Assert.DoesNotContain("NotMappedField", first.Keys);
Expand Down
10 changes: 5 additions & 5 deletions tests/MiniExcel.Csv.Tests/Main/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal class TestDto
internal class CsvFieldMappingTest
{
[MiniExcelColumnName("Column1")]
public string Test1;
public string? Test1;

[MiniExcelColumnName("Column2")]
public int Test2;
Expand All @@ -21,18 +21,18 @@ internal class CsvFieldMappingTest
internal class MixedFieldPropertyTest
{
[MiniExcelColumnName("F1")]
public string Field1;
public string? Field1;

[MiniExcelColumnName("P1")]
public string Prop1 { get; set; }
public string? Prop1 { get; set; }
}

internal class CsvFieldsWithoutAttributeDemo
{
public string NotMappedField;
public string? NotMappedField;

[MiniExcelColumnName("Mapped")]
public string MappedField;
public string? MappedField;
}

internal class TestWithAlias
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1251,7 +1251,7 @@ public void Multiple_Items_With_Collections_Should_Detect_Pattern()
{
var boundaries = mapping.OptimizedBoundaries;
// Pattern detection for multiple items
Assert.True(boundaries.PatternHeight > 0 || !boundaries.IsMultiItemPattern);
Assert.True(boundaries?.PatternHeight > 0 || boundaries?.IsMultiItemPattern is false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ public void ToValueDictionary_Given_InputIsDictionaryWithDataReader_Then_DataRea

var sut = new OpenXmlValueExtractor();
var extracted = sut.ToValueDictionary(valueDictionary);
var result = (List<IDictionary<string, object>>)extracted["DataReader"];
var result = (List<IDictionary<string, object>>?)extracted["DataReader"];

Assert.Equal(result.Count, expectedOutput.Count);
for (int i = 0; i < result.Count; i++)
Assert.Equal(result?.Count, expectedOutput.Count);
for (int i = 0; i < result?.Count; i++)
{
var row = result[i];
var expected = expectedOutput[i];
Expand Down Expand Up @@ -114,8 +114,8 @@ public void ToValueDictionary_Given_InputIsPocoClass_Then_Output_IsAnEquivalentD
private record PocoRecord(string Name, int Age, IEnumerable<string> Fruits);
private class PocoClass
{
public string Name { get; set; }
public string? Name { get; set; }
public int Age { get; set; }
public IEnumerable<string> Fruits; // Field
};
}
public IEnumerable<string>? Fruits; // Field
}
}
6 changes: 3 additions & 3 deletions tests/MiniExcel.OpenXml.Tests/Templates/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace MiniExcelLib.OpenXml.Tests.Templates;

internal class TestIEnumerableTypePoco
{
public string @string { get; set; }
public string? @string { get; set; }
public int? @int { get; set; }
public decimal? @decimal { get; set; }
public double? @double { get; set; }
Expand All @@ -13,8 +13,8 @@ internal class TestIEnumerableTypePoco

internal class Employee
{
public string name { get; set; }
public string department { get; set; }
public string? name { get; set; }
public string? department { get; set; }
}

internal record struct Identity(int Type, string Id);
Expand Down
Loading