环境
.Net core 3.1
c# 9.0
目的
实现NPOI IROW和Object的自动转换,无需手写转换代码
原理
- 原始对象转换最高效,但是对每个要映射的对象都手写代码,不仅不利于阅读,同时造成大量冗余代码
- 通过第一次对构造转换函数并缓存,实现逼近原始手写代码的转换效率,同时又可以根据runtime不同的对象进行转换
- 表达式树相对于旧版本使用的EMIT直接编写IL来说,更加易于阅读
转换对象
public class Test
{
public Guid Id { get; set; }
public DateTime Time { get; set; }
public DateTime Time2 { get; set; }
public DateTime Time3 { get; set; }
public DateTime Time4 { get; set; }
public List<string> Description { get; set; }
}
三种转换代码对比
原始转换代码:
public static void SaveTest(List<Test> tests)
{
var book = new NPOI.XSSF.UserModel.XSSFWorkbook();
ISheet sheet = book.CreateSheet("Sheet");
IRow row = sheet.CreateRow(0);
var type = typeof(Test);
var props = type.GetProperties();
var index = 0;
foreach (var prop in props)
row.CreateCell(index++).SetCellValue(prop.Name);
var rowIndex = 1;
foreach (var test in tests)
{
row = sheet.CreateRow(rowIndex++);
row.CreateCell(0).SetCellValue(test.Id.ToString());
row.CreateCell(1).SetCellValue(test.Time.ToString());
row.CreateCell(2).SetCellValue(test.Time2.ToString());
row.CreateCell(3).SetCellValue(test.Time3.ToString());
row.CreateCell(4).SetCellValue(test.Time4.ToString());
}
using var file = new FileStream(Path.Join(AppContext.BaseDirectory, $"./testorigin.xlsx"), FileMode.OpenOrCreate);
book.Write(file);
}
反射代码:
public static IRow Parse<T>(this IRow row, T data)
{
var type = typeof(T);
var props = type.GetProperties();
for (var index = 0; index < props.Length; ++index)
{
var prop = props[index];
var value = prop.GetValue(data);
if (value != null)
row.CreateCell(index).SetCellValue(value.ToString());
}
return row;
}
public static void SaveRef<T>(List<T> tests)
{
var book = new NPOI.XSSF.UserModel.XSSFWorkbook();
ISheet sheet = book.CreateSheet("Sheet");
IRow row = sheet.CreateRow(0);
var type = typeof(T);
var props = type.GetProperties();
var index = 0;
foreach (var prop in props)
row.CreateCell(index++).SetCellValue(prop.Name);
var rowIndex = 1;
foreach (var test in tests)
{
row = sheet.CreateRow(rowIndex++);
row.Parse(test);
}
using var file = new FileStream(Path.Join(AppContext.BaseDirectory, $"./test3.xlsx"), FileMode.OpenOrCreate);
book.Write(file);
}
表达式树代码
public static Action<IRow, T> CreateFunc<T>() where T : new()
{
var typeRow = typeof(IRow);
var typeData = typeof(T);
var body = new List<Expression>();
var para1 = Expression.Parameter(typeRow, "row");
var para2 = Expression.Parameter(typeData, "object");
var props = typeData.GetProperties();
var createCell = typeRow.GetMethod("CreateCell", new[] { typeof(int) });
var setCellValue = typeof(ICell).GetMethod("SetCellValue", new[] { typeof(string) });
var index = 0;
foreach (var prop in props)
{
var toStringMethod = prop.PropertyType.GetMethod("ToString", new Type[] { });
if (toStringMethod != null)
{
var propEx = Expression.Property(para2, prop.Name);
var stringValue = Expression.Call(propEx, toStringMethod);
var cell = Expression.Call(para1, createCell, Expression.Constant(index++));
var setCell = Expression.Call(cell, setCellValue, stringValue);
var ifthen = Expression.IfThen(Expression.NotEqual(propEx, Expression.Default(prop.PropertyType)), setCell);
body.Add(ifthen);
}
}
var lambda = Expression.Lambda<Action<IRow, T>>(Expression.Block(body.ToArray()), para1, para2);
return lambda.Compile();
}
public static void Save<T>(List<T> tests) where T : new()
{
var book = new NPOI.XSSF.UserModel.XSSFWorkbook();
ISheet sheet = book.CreateSheet("Sheet");
IRow row = sheet.CreateRow(0);
var type = typeof(T);
var props = type.GetProperties();
var index = 0;
foreach (var prop in props)
row.CreateCell(index++).SetCellValue(prop.Name);
var rowIndex = 1;
Action<IRow, T> convert = CreateFunc<T>();
foreach (var test in tests)
{
row = sheet.CreateRow(rowIndex++);
convert(row, test);
}
using var file = new FileStream(Path.Join(AppContext.BaseDirectory, $"./test2.xlsx"), FileMode.OpenOrCreate);
book.Write(file);
}
通过BenchmarkDotnet进行测试
测试代码
public class NpoiTest
{
private readonly List<Test> _tests;
public NpoiTest()
{
var cnt = 100000;
_tests = new List<Test>();
for (var i = 0; i < cnt; ++i)
{
_tests.Add(
new Test { Id = Guid.NewGuid(), Time = DateTime.Now, Time2 = DateTime.Now, Time3 = DateTime.Now, Time4 = DateTime.Now }
);
}
}
[Benchmark]
public void OriginParse() => NpoiParse.Npoiparse.SaveTest(_tests);
[Benchmark]
public void ExpressionParse() => NpoiParse.Npoiparse.Save(_tests);
[Benchmark]
public void RefectParse() => NpoiParse.Npoiparse.SaveRef(_tests);
}
测试结果
100000数量级
这里反射的代码其实很少,与原始mapper效率并没有区别很大