Skip to content
21 changes: 19 additions & 2 deletions README_V2.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ The exporters also fully support asynchronous operations:
await exporter.ExportAsync(outputPath, values);
```


### Release Notes

If you're migrating from a `1.x` version, please check the [upgrade notes](V2-Upgrade-Notes.md).
Expand Down Expand Up @@ -201,6 +202,7 @@ You can find the benchmarks' results for the latest release [here](benchmarks/re
- [Query/Import](#docs-import)
- [Create/Export](#docs-export)
- [Excel Template](#docs-template)
- [Excel Editor](#docs-editing)
- [Attributes and configuration](#docs-attributes)
- [CSV specifics](#docs-csv)
- [Other functionalities](#docs-other)
Expand Down Expand Up @@ -1177,6 +1179,21 @@ Result:
<img width="890" height="999" alt="image" src="https://github.com/user-attachments/assets/fae209ec-b3e2-4f2e-94e4-3b52a37dc364" />


### Editing existing workbooks <a name="docs-editing" />

> Warning: this feature is a work in progress and currently very limited!

Cell style updates are queued and applied in worksheet and cell order when `Save` is called. If the same cell is updated more than once, the last update wins.

```csharp
var editor = MiniExcelV2.Editors.GetOpenXmlEditor();
editor.StartEditingPipeline(path)
.UpdateCellStyle("A1", style => style.FontColor = Color.Red)
.UpdateCellStyle("X100", style => style.FontColor = Color.Blue)
.SaveChanges();
```


### Attributes and configuration <a name="docs-attributes" />

#### 1. Specify the column name, column index, or ignore the column entirely.
Expand Down Expand Up @@ -1611,12 +1628,12 @@ exporter.Export(path, value, configuration: config);
#### Read empty string as null

By default, empty values are mapped to `string.Empty`.
You can modify this behavior and map them to `null` using the `CsvConfiguration.ReadEmptyStringAsNull` property:
You can modify this behavior and map them to `null` using the `CsvConfiguration.ReadEmptyFieldsAsDefault` property:
Comment thread
michelebastione marked this conversation as resolved.

```csharp
var config = new CsvConfiguration
{
ReadEmptyStringAsNull = true
ReadEmptyFieldsAsDefault = true
};
```

Expand Down
Empty file added src/MiniExcel.Core/MiniExcel.cs
Empty file.
5 changes: 5 additions & 0 deletions src/MiniExcel.Core/MiniExcelProviders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ public sealed class MiniExcelTemplaterProvider
{
internal MiniExcelTemplaterProvider() { }
}

public sealed class MiniExcelEditorProvider
{
internal MiniExcelEditorProvider() { }
}
2 changes: 2 additions & 0 deletions src/MiniExcel.Core/MiniExcelV2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public static class MiniExcelV2
public static readonly MiniExcelExporterProvider Exporters = new();
public static readonly MiniExcelImporterProvider Importers = new();
public static readonly MiniExcelTemplaterProvider Templaters = new();
public static readonly MiniExcelEditorProvider Editors = new();
}

[Obsolete("This class will be removed in the full release, use MiniExcelV2 instead.", true)]
Expand All @@ -16,4 +17,5 @@ public static class MiniExcel
public static readonly MiniExcelExporterProvider Exporters = new();
public static readonly MiniExcelImporterProvider Importers = new();
public static readonly MiniExcelTemplaterProvider Templaters = new();
public static readonly MiniExcelEditorProvider Editors = new();
}
87 changes: 87 additions & 0 deletions src/MiniExcel.OpenXml/Api/OpenXmlEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using MiniExcelLib.OpenXml.Editor;

// ReSharper disable once CheckNamespace
namespace MiniExcelLib.OpenXml;

public sealed partial class OpenXmlEditor
{
internal OpenXmlEditor() { }


/// <summary>
/// Creates a new editing pipeline for the provided Excel document.
/// </summary>
/// <param name="path">The file path to the Excel document to edit.</param>
/// <returns>
/// An <see cref="OpenXmlEditingPipeline"/> instance that can be used to apply modifications to the document.
/// </returns>
/// <remarks>
/// This method opens the file for exclusive read-write access. The file is locked until
/// SaveChanges or SaveChangesAsync is called.
/// </remarks>
public OpenXmlEditingPipeline StartEditingPipeline(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("The path cannot be null or whitespace.", nameof(path));

var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
return new OpenXmlEditingPipeline(stream, leaveOpen: false);
}

/// <summary>
/// Creates a new editing pipeline for the provided Excel document.
/// </summary>
/// <param name="stream">The stream containing the Excel file data.</param>
/// <param name="leaveOpen">
/// If true the stream remains open after changes are saved and must be disposed by the caller,
/// if false it is automatically closed when changes are saved. Default is false.
/// </param>
/// <returns>
/// An <see cref="OpenXmlEditingPipeline"/> instance that can be used to apply modifications to the file.
/// </returns>
/// <remarks>
/// Even with parameter <c>leaveOpen: false</c>, the underlying stream will not be disposed until
/// SaveChanges or SaveChangesAsync are called.
/// </remarks>
public OpenXmlEditingPipeline StartEditingPipeline(Stream stream, bool leaveOpen = false)
{
if (stream is null)
throw new ArgumentNullException(nameof(stream));

return new OpenXmlEditingPipeline(stream, leaveOpen);
}

/// <summary>
/// Modify the properties of a worksheet in the specified document.
/// </summary>
/// <param name="path">The path to the OpenXml document.</param>
/// <param name="sheetName">The name of the worksheet to modify.</param>
/// <param name="newSheetName">The new name to assign to the worksheet, or <c>null</c> to leave as is.</param>
/// <param name="newSheetIndex">The position in the workbook to assign to the worksheet, or <c>null</c> to leave as is.</param>
/// <param name="newSheetState">The visibility state to assign to the worksheet, or <c>null</c> to leave as is.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests</param>
[CreateSyncVersion]
public async Task AlterSheetInfoAsync(string path, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default)
{
var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
await using var disposableStream = stream.ConfigureAwait(false);

await AlterSheetInfoAsync(stream, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Modify the properties of a worksheet in the specified document.
/// </summary>
/// <param name="stream">The stream to the OpenXml document.</param>
/// <param name="sheetName">The name of the worksheet to modify.</param>
/// <param name="newSheetName">The new name to assign to the worksheet, or <c>null</c> to leave as is.</param>
/// <param name="newSheetIndex">The position in the workbook to assign to the worksheet, or <c>null</c> to leave as is.</param>
/// <param name="newSheetState">The visibility state to assign to the worksheet, or <c>null</c> to leave as is.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests</param>
[CreateSyncVersion]
public async Task AlterSheetInfoAsync(Stream stream, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default)
{
var internals = new OpenXmlEditorInternals(stream, true);
await internals.AlterWorksheetAsync(sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
}
}
13 changes: 4 additions & 9 deletions src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,10 @@ public async Task<int[]> ExportAsync(Stream stream, object value, bool printHead
/// <param name="newSheetState">The visibility state to assign to the worksheet, or <c>null</c> to leave as is.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests</param>
[CreateSyncVersion]
[Obsolete("This method will be removed in the full release, please use MiniExcelV2.Editors.GetOpenXmlEditor().AlterSheetInfo instead.")]
public async Task AlterSheetAsync(string path, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default)
{
var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
await using var disposableStream = stream.ConfigureAwait(false);

await AlterSheetAsync(stream, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
await new OpenXmlEditor().AlterSheetInfoAsync(path, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -218,12 +216,9 @@ public async Task AlterSheetAsync(string path, string sheetName, string? newShee
/// <param name="newSheetState">The visibility state to assign to the worksheet, or <c>null</c> to leave as is.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests</param>
[CreateSyncVersion]
[Obsolete("This method will be removed in the full release, please use MiniExcelV2.Editors.GetOpenXmlEditor().AlterSheetInfo instead.")]
public async Task AlterSheetAsync(Stream stream, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default)
{
var writer = await OpenXmlWriter
.CreateAsync(stream, null, sheetName, false, new OpenXmlConfiguration { FastMode = true }, cancellationToken)
.ConfigureAwait(false);

await writer.AlterWorksheetAsync(sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
await new OpenXmlEditor().AlterSheetInfoAsync(stream, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
}
}
3 changes: 2 additions & 1 deletion src/MiniExcel.OpenXml/Api/ProviderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public static class ProviderExtensions
public static OpenXmlExporter GetOpenXmlExporter(this MiniExcelExporterProvider exporterProvider) => new();
public static OpenXmlImporter GetOpenXmlImporter(this MiniExcelImporterProvider importerProvider) => new();
public static OpenXmlTemplater GetOpenXmlTemplater(this MiniExcelTemplaterProvider templaterProvider) => new();
}
public static OpenXmlEditor GetOpenXmlEditor(this MiniExcelEditorProvider editorProvider) => new();
}
105 changes: 105 additions & 0 deletions src/MiniExcel.OpenXml/Editor/OpenXmlEditingPipeline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using MiniExcelLib.OpenXml.Styles;

namespace MiniExcelLib.OpenXml.Editor;

/// <summary>
/// Represents a pipeline for editing Excel files with a fluent API.
/// </summary>
public sealed partial class OpenXmlEditingPipeline
{
private readonly OpenXmlEditorInternals _internals;

internal OpenXmlEditingPipeline(Stream stream, bool leaveOpen)
{
_internals = new OpenXmlEditorInternals(stream, leaveOpen);
}

/// <summary>
/// Updates the style of a cell using a callback function that modifies the provided <see cref="OpenXmlCellStyle"/> object.
/// </summary>
/// <param name="cellReference">The cell reference in standard Excel format (e.g., "A1", "B5").</param>
/// <param name="updateCellCallback">A callback function that receives an <see cref="OpenXmlCellStyle"/> object and applies the desired style changes.</param>
/// <param name="sheetName">The name of the worksheet to update. If null or not specified, the first sheet in the workbook is used.</param>
/// <returns>
/// Returns this <see cref="OpenXmlEditingPipeline"/> instance to enable method chaining.
/// </returns>
/// <remarks>
/// Modifications are queued in the pipeline and not written to the file until SaveChanges or SaveChangesAsync is called.
/// </remarks>
public OpenXmlEditingPipeline UpdateCellStyle(string cellReference, Action<OpenXmlCellStyle> updateCellCallback, string? sheetName = null)
{
if (updateCellCallback is null)
throw new ArgumentNullException(nameof(updateCellCallback));

var style = new OpenXmlCellStyle();
updateCellCallback(style);

return UpdateCellStyle(cellReference, style, sheetName);
}

/// <summary>
/// Updates the style of a cell using a pre-configured <see cref="OpenXmlCellStyle"/> object.
/// </summary>
/// <param name="cellReference">The cell reference in standard Excel format (e.g., "A1", "B5").</param>
/// <param name="cellStyle">The <see cref="OpenXmlCellStyle"/> object containing the style properties to apply to the cell.</param>
/// <param name="sheetName"> The name of the worksheet to update. If null or not specified, the first sheet in the workbook is used.</param>
/// <returns>
/// Returns this <see cref="OpenXmlEditingPipeline"/> instance to enable method chaining.
/// </returns>
/// <remarks>
/// Modifications are queued in the pipeline and not written to the file until SaveChanges or SaveChangesAsync is called.
/// </remarks>
public OpenXmlEditingPipeline UpdateCellStyle(string cellReference, OpenXmlCellStyle cellStyle, string? sheetName = null)
{
_internals.UpdateCellStyle(cellReference, cellStyle, sheetName);
return this;
}

/// <summary>
/// Applies all queued modifications to the Excel document and saves it to the original stream or file.
/// The pipeline cannot be reused afterwards.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <remarks>
/// If the pipeline was created from a stream with <c>leaveOpen: true</c>, the caller is responsible for its disposal.
/// </remarks>
[CreateSyncVersion]
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
await _internals.SaveAsync(cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Applies all queued modifications to the Excel document and saves it to the provided path.
/// The pipeline cannot be reused afterwards.
/// </summary>
/// <param name="outputPath">The path to save the modified Excel document to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <remarks>
/// If the pipeline was created from a stream with <c>leaveOpen: true</c>, the caller is responsible for its disposal.
/// </remarks>
[CreateSyncVersion]
public async Task SaveChangesAsync(string outputPath, CancellationToken cancellationToken = default)
{
var stream = File.OpenWrite(outputPath);
await using var disposableStream = stream.ConfigureAwait(false);

await SaveChangesAsync(stream, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Applies all queued modifications to the Excel document and saves it to the provided stream.
/// The pipeline cannot be reused afterwards.
/// </summary>
/// <param name="outputStream">The stream to save the modified Excel document to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <remarks>
/// If the pipeline was created from a stream with <c>leaveOpen: true</c>, the caller is responsible for its disposal.
/// The caller is always responsible for disposing the output stream.
/// </remarks>
[CreateSyncVersion]
public async Task SaveChangesAsync(Stream outputStream, CancellationToken cancellationToken = default)
{
await _internals.SaveAsync(outputStream, cancellationToken).ConfigureAwait(false);
}
}
Loading
Loading