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
21 changes: 18 additions & 3 deletions README-V2.md
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,7 @@ templater.ApplyTemplate(path, templatePath, value, config)

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

#### 1. Specify the column name, column index, or ignore the column entirely
#### 1. Specify the column name, column index, or ignore the column entirely.

![image](https://user-images.githubusercontent.com/12729184/114230869-3e163700-99ac-11eb-9a90-2039d4b4b313.png)

Expand Down Expand Up @@ -1300,7 +1300,22 @@ public class Dto
}
```

#### 8. DynamicColumnAttribute
#### 8. Resource based localization

Support for localizable resources is available for both `MiniExcelColumnAttribute` and `MiniExcelColumnNameAttribute`:

```csharp
public class Dto
{
[MiniExcelColumn(Name = "Column1", ResourceType = typeof(MyResources))]
public string Test1 { get; set; }

[MiniExcelColumnName("Column2", ResourceType = typeof(MyResources))]
public string Test2 { get; set; }
}
```

#### 9. DynamicColumnAttribute

Attributes can also be set on columns dynamically:
```csharp
Expand All @@ -1321,7 +1336,7 @@ var exporter = MiniExcel.Exporters.GetOpenXmlExporter();
exporter.Export(path, value, configuration: config);
```

#### 9. MiniExcelSheetAttribute
#### 10. MiniExcelSheetAttribute

It is possible to define the name and visibility of a sheet through the `MiniExcelSheetAttribute`:

Expand Down
57 changes: 46 additions & 11 deletions src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System.Resources;

namespace MiniExcelLib.Core.Attributes;

public class MiniExcelColumnAttribute : MiniExcelAttributeBase
{
private int _index = -1;
private string? _xName;
private ResourceManager? _resourceManager;

public string? Name { get; set; }
public string[]? Aliases { get; set; } = [];
Expand All @@ -15,39 +16,73 @@ public class MiniExcelColumnAttribute : MiniExcelAttributeBase
public double Width { get; set; } = 8.42857143;
public ColumnType Type { get; set; } = ColumnType.Value;

private int _index = -1;
public int Index
{
get => _index;
set => Init(value);
}

private string? _indexName;
public string? IndexName
{
get => _xName;
get => _indexName;
set => Init(CellReferenceConverter.GetNumericalIndex(value), value);
}

private Type? _resourceType;
public Type? ResourceType
{
get => _resourceType;
set
{
if (_resourceType == value)
return;

_resourceType = value;
if (value is null)
return;

const BindingFlags bindingFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
if (value.GetProperty(nameof(ResourceManager), bindingFlags) is { } property &&
property.GetValue(null) is ResourceManager resourceManager)
{
_resourceManager = resourceManager;
}
else
{
_resourceManager = new ResourceManager(value);
}
}
}
Comment thread
michelebastione marked this conversation as resolved.

internal string? GetColumnName(string? resourceKey = null)
{
if (Name is not null)
return _resourceManager?.GetString(Name) ?? Name;

if (resourceKey is not null)
return _resourceManager?.GetString(resourceKey) ?? resourceKey;

return null;
}

private void Init(int index, string? columnName = null)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index), index, $"Column index {index} must be greater or equal to zero.");

_index = index;
_xName ??= columnName ?? CellReferenceConverter.GetAlphabeticalIndex(index);
_indexName ??= columnName ?? CellReferenceConverter.GetAlphabeticalIndex(index);
Comment thread
michelebastione marked this conversation as resolved.
}

public void SetFormatId(int formatId) => FormatId = formatId;
}

public class DynamicExcelColumn : MiniExcelColumnAttribute
public class DynamicExcelColumn(string key) : MiniExcelColumnAttribute
{
public string Key { get; set; }
public string Key { get; set; } = key;
public Func<object?, object?>? CustomFormatter { get; set; }

public DynamicExcelColumn(string key)
{
Key = key;
}
}

public enum ColumnType { Value, Formula }
38 changes: 37 additions & 1 deletion src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,43 @@
using System.Resources;

namespace MiniExcelLib.Core.Attributes;

public class MiniExcelColumnNameAttribute(string columnName, string[]? aliases = null) : MiniExcelAttributeBase
{
private ResourceManager? _resourceManager;

[Obsolete("Please use the \"Name\" property instead")]
public string ExcelColumnName { get; set; } = columnName;
public string Name { get; set; } = columnName;
Comment thread
michelebastione marked this conversation as resolved.
public string[] Aliases { get; set; } = aliases ?? [];
}

private Type? _resourceType;
public Type? ResourceType
{
get => _resourceType;
set
{
if (_resourceType == value)
return;

_resourceType = value;
if (value is null)
return;

const BindingFlags bindingFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
if (value.GetProperty(nameof(ResourceManager), bindingFlags) is { } property &&
property.GetValue(null) is ResourceManager resourceManager)
{
_resourceManager = resourceManager;
}
else
{
_resourceManager = new ResourceManager(value);
}
}
}

internal string? GetColumnName() => !string.IsNullOrEmpty(Name)
? _resourceManager?.GetString(Name) ?? Name
: null;
}
11 changes: 8 additions & 3 deletions src/MiniExcel.Core/Reflection/ColumnMappingsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ internal static List<MiniExcelColumnMapping> GetMappingsForImport(Type type, str
ExcludeNullableType = accessor.Type,
Nullable = accessor.IsNullable,
ExcelColumnAliases = excelColumnName?.Aliases ?? excelColumn?.Aliases ?? [],
ExcelColumnName = excelColumnName?.ExcelColumnName ?? m.GetAttribute<DisplayNameAttribute>()?.DisplayName ?? excelColumn?.Name ?? m.Name,

ExcelColumnName = excelColumnName?.GetColumnName()
?? excelColumn?.GetColumnName(m.Name)
?? m.GetAttribute<DisplayNameAttribute>()?.DisplayName
?? m.Name,

ExcelColumnIndex = m.GetAttribute<MiniExcelColumnIndexAttribute>()?.ExcelColumnIndex ?? excelColumnIndex,
ExcelIndexName = m.GetAttribute<MiniExcelColumnIndexAttribute>()?.ExcelXName ?? excelColumn?.IndexName,
ExcelColumnWidth = m.GetAttribute<MiniExcelColumnWidthAttribute>()?.ExcelColumnWidth ?? excelColumn?.Width,
Expand Down Expand Up @@ -199,7 +204,7 @@ private static void SetDictionaryColumnInfo(List<MiniExcelColumnMapping?> mappin
if (dynamicColumn.IndexName is { } idxName)
map.ExcelIndexName = idxName;

if (dynamicColumn.Name is { } colName)
if (dynamicColumn.GetColumnName(dynamicColumn.Key) is { } colName)
map.ExcelColumnName = colName;

map.ExcelColumnIndex = dynamicColumn.Index;
Expand Down Expand Up @@ -285,7 +290,7 @@ internal static MiniExcelColumnMapping GetColumnMappingFromDynamicConfiguration(
if (dynamicColumn.IndexName is { } idxName)
member.ExcelIndexName = idxName;

if (dynamicColumn.Name is { } colName)
if (dynamicColumn.GetColumnName(columnName) is { } colName)
member.ExcelColumnName = colName;

return member;
Expand Down
117 changes: 106 additions & 11 deletions tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using ExcelDataReader;
using MiniExcelLib.Core.Exceptions;
using MiniExcelLib.OpenXml.Tests.Utils;
using MiniExcelLib.Tests.Common;
using MiniExcelLib.Tests.Common.Utils;

namespace MiniExcelLib.OpenXml.Tests;
Expand Down Expand Up @@ -1217,22 +1218,15 @@ await Assert.ThrowsAsync<OperationCanceledException>(async () =>
}

[Fact]
public async Task ReadBigExcel_Prcoessing_TakeCancel_Throws_TaskCanceledException()
public async Task ReadBigExcel_Processing_TakeCancel_Throws_TaskCanceledException()
{
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
{
var path = PathHelper.GetFile("xlsx/bigExcel.xlsx");
var cts = new CancellationTokenSource();

_ = Task.Run(async () =>
{
await Task.Delay(500);
await cts.CancelAsync();
cts.Token.ThrowIfCancellationRequested();
});

await using var stream = FileHelper.OpenRead(path);
_ = await _excelImporter.QueryAsync(stream, cancellationToken: cts.Token).ToListAsync(cts.Token);
var exportTask = _excelImporter.QueryAsync(PathHelper.GetFile("xlsx/bigExcel.xlsx"), cancellationToken: cts.Token).ToListAsync(cts.Token);
await cts.CancelAsync();
await exportTask;
});
}

Expand Down Expand Up @@ -1924,4 +1918,105 @@ public async Task InvalidSheetNameCharactersShouldThrow()
ms1.Seek(0, SeekOrigin.Begin);
await Assert.ThrowsAsync<ArgumentException>(() => _excelExporter.AlterSheetAsync(ms3, "Sheet1", "Sheet*"));
}

class LocalizationSupportDto(string firstName, string lastName, string address, int age)
{
[MiniExcelColumn(Name = nameof(FirstName), ResourceType = typeof(Localization), Width = 15)]
public string? FirstName { get; set; } = firstName;

[MiniExcelColumn(Name = nameof(LastName), ResourceType = typeof(Localization), Width = 15)]
public string? LastName { get; set; } = lastName;

[MiniExcelColumnName("Address", ResourceType = typeof(Localization))]
public string? Residency { get; set; } = address;

[MiniExcelColumn(Name = nameof(Age), ResourceType = typeof(Localization), Width = 20)]
public int Age { get; set; } = age;
}

[Theory]
[InlineData("")]
[InlineData("it")]
[InlineData("zh")]
public async Task LocalizationTest(string cultureId)
{
var ogCulture = CultureInfo.CurrentUICulture;

try
{
CultureInfo.CurrentUICulture = new CultureInfo(cultureId);

await using var ms = new MemoryStream();
await _excelExporter.ExportAsync(ms, Array.Empty<LocalizationSupportDto>());
ms.Seek(0, SeekOrigin.Begin);

using var package = new ExcelPackage(ms);
var cells = package.Workbook.Worksheets[0].Cells;

var (firstName, lastName, address, age) = cultureId switch
{
"" => ("Name", "Surname", "Address", "Age"),
"it" => ("Nome", "Cognome", "Indirizzo", "Età"),
"zh" => ("名", "姓", "地址", "年龄"),
_ => throw new UnreachableException()
};

Assert.Equal(firstName, cells["A1"].Value);
Assert.Equal(lastName, cells["B1"].Value);
Assert.Equal(address, cells["C1"].Value);
Assert.Equal(age, cells["D1"].Value);
}
finally
{
CultureInfo.CurrentUICulture = ogCulture;
}
}

[Theory]
[InlineData("")]
[InlineData("it")]
[InlineData("zh")]
public async Task LocalizationTestDynamicColumns(string cultureId)
{
var ogCulture = CultureInfo.CurrentUICulture;

try
{
CultureInfo.CurrentUICulture = new CultureInfo(cultureId);

DynamicExcelColumn[] cols = [
new("FirstName") { ResourceType = typeof(Localization) },
new("LastName") { ResourceType = typeof(Localization) },
new("Address") { ResourceType = typeof(Localization) },
new("Age") { ResourceType = typeof(Localization) }
];

await using var stream = new MemoryStream();
await _excelExporter.ExportAsync(
stream,
new[] { new { FirstName = "", LastName = "", Address = "", Age = 0 } },
configuration: new OpenXmlConfiguration { DynamicColumns = cols });

stream.Seek(0, SeekOrigin.Begin);
using var package = new ExcelPackage(stream);
var cells = package.Workbook.Worksheets[0].Cells;

var (firstName, lastName, address, age) = cultureId switch
{
"" => ("Name", "Surname", "Address", "Age"),
"it" => ("Nome", "Cognome", "Indirizzo", "Età"),
"zh" => ("名", "姓", "地址", "年龄"),
_ => throw new UnreachableException()
};

Assert.Equal(firstName, cells["A1"].Value);
Assert.Equal(lastName, cells["B1"].Value);
Assert.Equal(address, cells["C1"].Value);
Assert.Equal(age, cells["D1"].Value);
}
finally
{
CultureInfo.CurrentUICulture = ogCulture;
}
}
}
Loading
Loading