如何使用EPPlus将excel行解析回类型

EPPlus有一个方便的LoadFromCollection方法,可以将我自己类型的数据导入工作表。

例如,如果我有一个类:

 public class Customer { public int Id { get; set; } public string Firstname { get; set; } public string Surname { get; set; } public DateTime Birthdate { get; set; } } 

然后是以下代码:

 var package = new ExcelPackage(); var sheet = package.Workbook.Worksheets.Add("Customers"); var customers = new List{ new Customer{ Id = 1, Firstname = "John", Surname = "Doe", Birthdate = new DateTime(2000, 1, 1) }, new Customer{ Id = 2, Firstname = "Mary", Surname = "Moe", Birthdate = new DateTime(2001, 2, 2) } }; sheet.Cells[1, 1].LoadFromCollection(customers); package.Save(); 

…将在名为“Customers”的工作表中添加2行。

我的问题是,是否有一个方便的对应物从excel中提取行(例如在进行了一些修改后)回到我的类型中。

就像是:

 var package = new ExcelPackage(inputStream); var customers = sheet.Dimension.SaveToCollection() ?? 

我有

  • 一直在浏览EPPlus代码库
  • 搜索任何保存问题
  • 搜索任何解析问题
  • 看过这个关于读单细胞的问题

…但是没有发现如何简单地将行解析为我的类型。

受上述启发,我采取了一条略有不同的路线。

  1. 我创建了一个属性并将每个属性映射到一列。
  2. 我使用DTO类型来定义我期望每列的内容
  3. 允许不要求列
  4. 使用EPPlus转换类型

通过这样做,它允许我使用传统的模型validation,并包含对列标题的更改

– 用法:

 using(FileStream fileStream = new FileStream(_fileName, FileMode.Open)){ ExcelPackage excel = new ExcelPackage(fileStream); var workSheet = excel.Workbook.Worksheets[RESOURCES_WORKSHEET]; IEnumerable newcollection = workSheet.ConvertSheetToObjects(); newcollection.ToList().ForEach(x => Console.WriteLine(x.Title)); } 

Dto映射到excel

 public class ExcelResourceDto { [Column(1)] [Required] public string Title { get; set; } [Column(2)] [Required] public string SearchTags { get; set; } } 

这是属性定义

 [AttributeUsage(AttributeTargets.All)] public class Column : System.Attribute { public int ColumnIndex { get; set; } public Column(int column) { ColumnIndex = column; } } 

用于处理映射行到DTO的扩展类

 public static class EPPLusExtensions { public static IEnumerable ConvertSheetToObjects(this ExcelWorksheet worksheet) where T : new() { Func columnOnly = y => y.AttributeType == typeof(Column); var columns = typeof(T) .GetProperties() .Where(x => x.CustomAttributes.Any(columnOnly)) .Select(p => new { Property = p, Column = p.GetCustomAttributes().First().ColumnIndex //safe because if where above }).ToList(); var rows= worksheet.Cells .Select(cell => cell.Start.Row) .Distinct() .OrderBy(x=>x); //Create the collection container var collection = rows.Skip(1) .Select(row => { var tnew = new T(); columns.ForEach(col => { //This is the real wrinkle to using reflection - Excel stores all numbers as double including int var val = worksheet.Cells[row, col.Column]; //If it is numeric it is a double since that is how excel stores all numbers if (val.Value == null) { col.Property.SetValue(tnew, null); return; } if (col.Property.PropertyType == typeof(Int32)) { col.Property.SetValue(tnew, val.GetValue()); return; } if (col.Property.PropertyType == typeof(double)) { col.Property.SetValue(tnew, val.GetValue()); return; } if (col.Property.PropertyType == typeof(DateTime)) { col.Property.SetValue(tnew, val.GetValue()); return; } //Its a string col.Property.SetValue(tnew, val.GetValue()); }); return tnew; }); //Send it back return collection; } } 

遗憾的是,EPPlus没有这种原生方法。 它是一个难以破解的坚果,因为你必须使用reflection,如果你真的希望它是通用的。 而且由于Excel将所有数字和日期存储为双倍,您必须处理大量的拆箱和类型检查。

这是我一直在努力的事情。 它是一种扩展方法,可以通过Generics 。 它只能在有限的测试下工作,所以一定要自己检查一下。 我不能保证它是最优化的(但是)但它在他的观点上相当不错。 你会像这样使用它:

 IEnumerable newcollection = worksheet.ConvertSheetToObjects(); 

扩展名:

 public static IEnumerable ConvertSheetToObjects(this ExcelWorksheet worksheet) where T:new() { //DateTime Conversion var convertDateTime = new Func(excelDate => { if (excelDate < 1) throw new ArgumentException("Excel dates cannot be smaller than 0."); var dateOfReference = new DateTime(1900, 1, 1); if (excelDate > 60d) excelDate = excelDate - 2; else excelDate = excelDate - 1; return dateOfReference.AddDays(excelDate); }); //Get the properties of T var tprops = (new T()) .GetType() .GetProperties() .ToList(); //Cells only contains references to cells with actual data var groups = worksheet.Cells .GroupBy(cell => cell.Start.Row) .ToList(); //Assume the second row represents column data types (big assumption!) var types = groups .Skip(1) .First() .Select(rcell => rcell.Value.GetType()) .ToList(); //Assume first row has the column names var colnames = groups .First() .Select((hcell, idx) => new { Name = hcell.Value.ToString(), index = idx }) .Where(o => tprops.Select(p => p.Name).Contains(o.Name)) .ToList(); //Everything after the header is data var rowvalues = groups .Skip(1) //Exclude header .Select(cg => cg.Select(c => c.Value).ToList()); //Create the collection container var collection = rowvalues .Select(row => { var tnew = new T(); colnames.ForEach(colname => { //This is the real wrinkle to using reflection - Excel stores all numbers as double including int var val = row[colname.index]; var type = types[colname.index]; var prop = tprops.First(p => p.Name == colname.Name); //If it is numeric it is a double since that is how excel stores all numbers if (type == typeof (double)) { //Unbox it var unboxedVal = (double) val; //FAR FROM A COMPLETE LIST!!! if (prop.PropertyType == typeof (Int32)) prop.SetValue(tnew, (int) unboxedVal); else if (prop.PropertyType == typeof (double)) prop.SetValue(tnew, unboxedVal); else if (prop.PropertyType == typeof (DateTime)) prop.SetValue(tnew, convertDateTime(unboxedVal)); else throw new NotImplementedException(String.Format("Type '{0}' not implemented yet!", prop.PropertyType.Name)); } else { //Its a string prop.SetValue(tnew, val); } }); return tnew; }); //Send it back return collection; } 

一个完整的例子:

 [TestMethod] public void Read_To_Collection_Test() { //A collection to Test var objectcollection = new List(); for (var i = 0; i < 10; i++) objectcollection.Add(new TestObject {Col1 = i, Col2 = i*10, Col3 = Path.GetRandomFileName(), Col4 = DateTime.Now.AddDays(i)}); //Create a test file to convert back byte[] bytes; using (var pck = new ExcelPackage()) { //Load the random data var workbook = pck.Workbook; var worksheet = workbook.Worksheets.Add("data"); worksheet.Cells.LoadFromCollection(objectcollection, true); bytes = pck.GetAsByteArray(); } //********************************* //Convert from excel to a collection using (var pck = new ExcelPackage(new MemoryStream(bytes))) { var workbook = pck.Workbook; var worksheet = workbook.Worksheets["data"]; var newcollection = worksheet.ConvertSheetToObjects(); newcollection.ToList().ForEach(to => Console.WriteLine("{{ Col1:{0}, Col2: {1}, Col3: \"{2}\", Col4: {3} }}", to.Col1, to.Col2, to.Col3, to.Col4.ToShortDateString())); } } //test object class public class TestObject { public int Col1 { get; set; } public int Col2 { get; set; } public string Col3 { get; set; } public DateTime Col4 { get; set; } } 

控制台输出:

 { Col1:0, Col2: 0, Col3: "wrulvxbx.wdv", Col4: 10/30/2015 } { Col1:1, Col2: 10, Col3: "wflh34yu.0pu", Col4: 10/31/2015 } { Col1:2, Col2: 20, Col3: "ps0f1jg0.121", Col4: 11/1/2015 } { Col1:3, Col2: 30, Col3: "skoc2gx1.2xs", Col4: 11/2/2015 } { Col1:4, Col2: 40, Col3: "urs3jnbb.ob1", Col4: 11/3/2015 } { Col1:5, Col2: 50, Col3: "m4l2fese.4yz", Col4: 11/4/2015 } { Col1:6, Col2: 60, Col3: "v3dselpn.rqq", Col4: 11/5/2015 } { Col1:7, Col2: 70, Col3: "v2ggbaar.r31", Col4: 11/6/2015 } { Col1:8, Col2: 80, Col3: "da4vd35p.msl", Col4: 11/7/2015 } { Col1:9, Col2: 90, Col3: "v5dtpuad.2ao", Col4: 11/8/2015 }