Skip to content

Commit 6b65f28

Browse files
committed
Refactor ExcelService and optimize PDFService export
Removed IStringLocalizer from ExcelService and added helper methods for cell styling and value conversion. Rewrote template, export, and import methods for clarity, type safety, and improved error handling. In PDFService, optimized column width calculation and row rendering by pre-evaluating mapper values and simplifying loops. Improved code readability and performance.
1 parent 623912d commit 6b65f28

File tree

2 files changed

+206
-148
lines changed

2 files changed

+206
-148
lines changed

src/Infrastructure/Services/ExcelService.cs

Lines changed: 176 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -3,165 +3,226 @@
33

44
using System.Data;
55
using ClosedXML.Excel;
6-
using Microsoft.Extensions.Localization;
76

87
namespace CleanArchitecture.Blazor.Infrastructure.Services;
98

109
public class ExcelService : IExcelService
1110
{
12-
private readonly IStringLocalizer<ExcelService> _localizer;
11+
public ExcelService()
12+
{
13+
}
1314

14-
public ExcelService(IStringLocalizer<ExcelService> localizer)
15+
/// <summary>
16+
/// Applies the header cell style.
17+
/// </summary>
18+
/// <param name="cell">The cell to style.</param>
19+
private void ApplyHeaderStyle(IXLCell cell)
1520
{
16-
_localizer = localizer;
21+
var style = cell.Style;
22+
style.Fill.PatternType = XLFillPatternValues.Solid;
23+
style.Fill.BackgroundColor = XLColor.LightBlue;
24+
style.Border.BottomBorder = XLBorderStyleValues.Thin;
1725
}
1826

19-
public Task<byte[]> CreateTemplateAsync(IEnumerable<string> fields, string sheetName = "Sheet1")
27+
/// <summary>
28+
/// Converts a CLR value to an <see cref="XLCellValue"/> that preserves the
29+
/// native Excel type (Number, Boolean, DateTime, TimeSpan) instead of
30+
/// falling back to a string representation.
31+
/// </summary>
32+
private static XLCellValue ConvertToXLCellValue(object? value)
2033
{
21-
using (var workbook = new XLWorkbook())
34+
return value switch
2235
{
23-
workbook.Properties.Author = "";
24-
var ws = workbook.Worksheets.Add(sheetName);
25-
var colIndex = 1;
26-
var rowIndex = 1;
27-
foreach (var header in fields)
28-
{
29-
var cell = ws.Cell(rowIndex, colIndex);
30-
var style = cell.Style;
31-
style.Fill.PatternType = XLFillPatternValues.Solid;
32-
style.Fill.BackgroundColor = XLColor.LightBlue;
33-
style.Border.BottomBorder = XLBorderStyleValues.Thin;
36+
null => Blank.Value,
37+
bool b => b,
38+
DateTime dt => dt,
39+
DateTimeOffset dto => dto.DateTime,
40+
TimeSpan ts => ts,
41+
byte n => n,
42+
sbyte n => n,
43+
short n => n,
44+
ushort n => n,
45+
int n => n,
46+
uint n => n,
47+
long n => n,
48+
ulong n => (double)n,
49+
float n => n,
50+
double n => n,
51+
decimal n => (double)n,
52+
string s => s,
53+
_ => value.ToString() ?? string.Empty
54+
};
55+
}
3456

35-
cell.Value = header;
57+
/// <summary>
58+
/// Converts an <see cref="IXLCell"/> to its native CLR value based on
59+
/// <see cref="XLDataType"/>, so that <see cref="DataRow"/> consumers
60+
/// receive properly typed values instead of plain strings.
61+
/// Numbers are returned as <see cref="decimal"/> to preserve precision
62+
/// when downstream code converts to <c>int</c>, <c>decimal</c>, etc.
63+
/// via <see cref="Convert.ChangeType(object, Type)"/>.
64+
/// </summary>
65+
private static object ConvertFromXLCell(IXLCell cell)
66+
{
67+
return cell.DataType switch
68+
{
69+
XLDataType.Blank => DBNull.Value,
70+
XLDataType.Boolean => cell.GetBoolean(),
71+
XLDataType.Number => (decimal)cell.GetDouble(),
72+
XLDataType.DateTime => cell.GetDateTime(),
73+
XLDataType.TimeSpan => cell.GetTimeSpan(),
74+
XLDataType.Text => cell.GetString(),
75+
_ => cell.Value.ToString()
76+
};
77+
}
3678

37-
colIndex++;
38-
}
79+
/// <summary>
80+
/// Saves the given workbook to a byte array.
81+
/// </summary>
82+
/// <param name="workbook">The workbook to save.</param>
83+
/// <returns>A byte array representing the workbook.</returns>
84+
private static byte[] SaveWorkbookToByteArray(XLWorkbook workbook)
85+
{
86+
using var stream = new MemoryStream();
87+
workbook.SaveAs(stream);
88+
stream.Seek(0, SeekOrigin.Begin);
89+
return stream.ToArray();
90+
}
3991

40-
using (var stream = new MemoryStream())
41-
{
42-
workbook.SaveAs(stream);
43-
stream.Seek(0, SeekOrigin.Begin);
44-
return Task.FromResult(stream.ToArray());
45-
}
92+
public Task<byte[]> CreateTemplateAsync(IEnumerable<string> fields, string sheetName = "Sheet1")
93+
{
94+
using var workbook = new XLWorkbook();
95+
workbook.Properties.Author = string.Empty;
96+
var ws = workbook.Worksheets.Add(sheetName);
97+
int rowIndex = 1;
98+
int colIndex = 1;
99+
foreach (var header in fields)
100+
{
101+
var cell = ws.Cell(rowIndex, colIndex++);
102+
ApplyHeaderStyle(cell);
103+
cell.Value = header;
46104
}
105+
106+
return Task.FromResult(SaveWorkbookToByteArray(workbook));
47107
}
48108

49109
public Task<byte[]> ExportAsync<TData>(IEnumerable<TData> data, Dictionary<string, Func<TData, object?>> mappers, string sheetName = "Sheet1")
50110
{
51-
using (var workbook = new XLWorkbook())
111+
using var workbook = new XLWorkbook();
112+
workbook.Properties.Author = string.Empty;
113+
var ws = workbook.Worksheets.Add(sheetName);
114+
int rowIndex = 1;
115+
int colIndex = 1;
116+
var headers = mappers.Keys.ToList();
117+
118+
// Write header row
119+
foreach (var header in headers)
52120
{
53-
workbook.Properties.Author = "";
54-
var ws = workbook.Worksheets.Add(sheetName);
55-
var colIndex = 1;
56-
var rowIndex = 1;
57-
var headers = mappers.Keys.ToList();
58-
foreach (var header in headers)
59-
{
60-
var cell = ws.Cell(rowIndex, colIndex);
61-
var style = cell.Style;
62-
style.Fill.PatternType = XLFillPatternValues.Solid;
63-
style.Fill.BackgroundColor = XLColor.LightBlue;
64-
style.Border.BottomBorder = XLBorderStyleValues.Thin;
65-
66-
cell.Value = header;
67-
68-
colIndex++;
69-
}
70-
71-
var dataList = data.ToList();
72-
foreach (var item in dataList)
73-
{
74-
colIndex = 1;
75-
rowIndex++;
76-
77-
var result = headers.Select(header => mappers[header](item));
121+
var cell = ws.Cell(rowIndex, colIndex++);
122+
ApplyHeaderStyle(cell);
123+
cell.Value = header;
124+
}
78125

79-
foreach (var value in result)
80-
{
81-
ws.Cell(rowIndex, colIndex).Value = value == null ? Blank.Value : value.ToString();
82-
colIndex++;
83-
}
84-
}
126+
// Write data rows
127+
var dataList = data.ToList();
128+
foreach (var item in dataList)
129+
{
130+
colIndex = 1;
131+
rowIndex++;
85132

86-
using (var stream = new MemoryStream())
133+
foreach (var header in headers)
87134
{
88-
workbook.SaveAs(stream);
89-
stream.Seek(0, SeekOrigin.Begin);
90-
return Task.FromResult(stream.ToArray());
135+
var value = mappers[header](item);
136+
var cell = ws.Cell(rowIndex, colIndex++);
137+
cell.Value = ConvertToXLCellValue(value);
91138
}
92139
}
140+
141+
return Task.FromResult(SaveWorkbookToByteArray(workbook));
93142
}
94143

95-
public async Task<IResult<IEnumerable<TEntity>>> ImportAsync<TEntity>(byte[] data, Dictionary<string, Func<DataRow, TEntity, object?>> mappers, string sheetName = "Sheet1")
144+
public async Task<IResult<IEnumerable<TEntity>>> ImportAsync<TEntity>(
145+
byte[] data,
146+
Dictionary<string, Func<DataRow, TEntity, object?>> mappers,
147+
string sheetName = "Sheet1")
96148
{
97-
using (var workbook = new XLWorkbook(new MemoryStream(data)))
149+
using var workbook = new XLWorkbook(new MemoryStream(data));
150+
if (!workbook.Worksheets.TryGetWorksheet(sheetName, out var ws))
98151
{
99-
if (!workbook.Worksheets.TryGetWorksheet(sheetName, out var ws))
100-
{
101-
return await Result<IEnumerable<TEntity>>.FailureAsync(string.Format(_localizer["Sheet with name {0} does not exist!"], sheetName));
102-
}
103-
104-
var dt = new DataTable();
105-
var titlesInFirstRow = true;
106-
107-
foreach (var firstRowCell in ws.Range(1, 1, 1, ws.LastCellUsed().Address.ColumnNumber).Cells())
152+
ws = workbook.Worksheets.Count > 0 ? workbook.Worksheets.First() : null;
153+
if (ws is null)
108154
{
109-
dt.Columns.Add(titlesInFirstRow ? firstRowCell.GetString() : $"Column {firstRowCell.Address.ColumnNumber}");
155+
return await Result<IEnumerable<TEntity>>.FailureAsync("Workbook contains no worksheets.");
110156
}
157+
}
111158

112-
var startRow = titlesInFirstRow ? 2 : 1;
113-
var headers = mappers.Keys.ToList();
114-
var errors = new List<string>();
159+
// Check if the worksheet contains any cells.
160+
var lastCellUsed = ws.LastCellUsed()?.Address.ColumnNumber ?? 0;
161+
if (lastCellUsed == 0)
162+
{
163+
var msg = $"Sheet with name {sheetName} is empty!";
164+
return await Result<IEnumerable<TEntity>>.FailureAsync(msg);
165+
}
115166

116-
foreach (var header in headers)
117-
{
118-
if (!dt.Columns.Contains(header))
119-
{
120-
errors.Add(string.Format(_localizer["Header '{0}' does not exist in table!"], header));
121-
}
122-
}
167+
// Create a DataTable from the header row.
168+
var dt = new DataTable();
169+
bool titlesInFirstRow = true;
170+
foreach (var cell in ws.Range(1, 1, 1, lastCellUsed).Cells())
171+
{
172+
string colName = titlesInFirstRow ? cell.GetString() : $"Column {cell.Address.ColumnNumber}";
173+
dt.Columns.Add(colName, typeof(object));
174+
}
175+
int startRow = titlesInFirstRow ? 2 : 1;
123176

124-
if (errors.Any())
177+
// Validate that all expected headers exist.
178+
var headers = mappers.Keys.ToList();
179+
var errors = new List<string>();
180+
foreach (var header in headers)
181+
{
182+
if (!dt.Columns.Contains(header))
125183
{
126-
return await Result<IEnumerable<TEntity>>.FailureAsync(errors.ToArray());
184+
errors.Add($"Header '{header}' does not exist in table!");
127185
}
186+
}
187+
if (errors.Any())
188+
{
189+
return await Result<IEnumerable<TEntity>>.FailureAsync(errors.ToArray());
190+
}
128191

129-
var lastRow = ws.LastRowUsed();
130-
var list = new List<TEntity>();
192+
var lastRowNumber = ws.LastRowUsed()?.RowNumber() ?? 0;
193+
var list = new List<TEntity>();
131194

132-
foreach (var row in ws.Rows(startRow, lastRow.RowNumber()))
195+
// Process each row in the worksheet.
196+
for (int rowIndex = startRow; rowIndex <= lastRowNumber; rowIndex++)
197+
{
198+
var row = ws.Row(rowIndex);
199+
try
133200
{
134-
try
201+
var dataRow = dt.NewRow();
202+
// Populate the DataRow with cell values, preserving native types.
203+
foreach (var cell in row.Cells(1, dt.Columns.Count))
135204
{
136-
var dataRow = dt.Rows.Add();
137-
var item = (TEntity?)Activator.CreateInstance(typeof(TEntity)) ?? throw new NullReferenceException($"{nameof(TEntity)}");
138-
139-
foreach (var cell in row.Cells())
140-
{
141-
if (cell.DataType == XLDataType.DateTime)
142-
{
143-
dataRow[cell.Address.ColumnNumber - 1] = cell.GetDateTime().ToString("yyyy-MM-dd HH:mm:ss");
144-
}
145-
else
146-
{
147-
dataRow[cell.Address.ColumnNumber - 1] = cell.Value.ToString();
148-
}
149-
}
150-
151-
foreach (var header in headers)
152-
{
153-
mappers[header](dataRow, item);
154-
}
155-
156-
list.Add(item);
205+
int colIndex = cell.Address.ColumnNumber - 1;
206+
dataRow[colIndex] = ConvertFromXLCell(cell);
157207
}
158-
catch (Exception e)
208+
dt.Rows.Add(dataRow);
209+
210+
// Create an instance of TEntity and apply the mapping functions.
211+
var item = (TEntity)Activator.CreateInstance(typeof(TEntity))!; // using null-forgiving operator
212+
foreach (var header in headers)
159213
{
160-
return await Result<IEnumerable<TEntity>>.FailureAsync(string.Format(_localizer["Sheet name {0}:{1}"], sheetName, e.Message));
214+
mappers[header](dataRow, item);
161215
}
216+
list.Add(item);
217+
}
218+
catch (Exception e)
219+
{
220+
var errorMsg = $"Error in sheet {sheetName}: {e.Message}";
221+
return await Result<IEnumerable<TEntity>>.FailureAsync(errorMsg);
162222
}
163-
164-
return await Result<IEnumerable<TEntity>>.SuccessAsync(list);
165223
}
224+
225+
return await Result<IEnumerable<TEntity>>.SuccessAsync(list);
166226
}
167227
}
228+

0 commit comments

Comments
 (0)