前言
我們在日常開發中對Excel的操作可能會比較頻繁,好多功能都會涉及到Excel的操作。在.Net Core中大家可能使用Npoi比較多,這款軟體功能也十分強大,而且接近原始程式設計。但是直接使用Npoi大部分時候我們可能都會自己封裝一下,畢竟根據二八原則,我們百分之八十的場景可能都是進行簡單的匯入匯出操作,這裡就引出我們的主角Npoi.Mapper了。
簡介
關於Npoi.Mapper看名字我們就知道,它並不是一款創新型的軟體,而是針對Npoi的二次封裝增強了關於Mapper相關的操作。秉承著使用非常簡單的原則,不過這樣能夠滿足我們日常開發工作中很大一部分應用場景。它的GitHub地址為https://github.com/donnytian/Npoi.Mapper,目前Star並不多才240多,但是確實是非常好用,這裡強烈推薦一波。接下來我們就大概演示一下的它的使用。
常規操作
Npoi.Mapper的主題內容包括兩大塊,一個是針對匯入,一個是針對匯出。接下來我們先來簡單演示一下最基礎的匯入匯出。首先我們新建一個Student類作為資料承載的載體,簡單定義大致如下
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public string Sex { get; set; }
public DateTime BirthDay { get; set; }
}
然後引入Npoi.Mapper的nuget包
<PackageReference Include="Npoi.Mapper" Version="3.5.1" />
匯出操作
接下來我們構建一個Student集合,然後初始化一部分簡單的資料,將這些資料匯出到Excel,接下來做一個簡單的演示
static void Main(string[] args)
{
List<Student> students = new List<Student>
{
new Student{ Id = 1,Name="夫子",Sex="男",BirthDay=new DateTime(1999,10,11) },
new Student{ Id = 2,Name="餘簾",Sex="女",BirthDay=new DateTime(1999,12,12) },
new Student{ Id = 3,Name="李慢慢",Sex="男",BirthDay=new DateTime(1999,11,11) },
new Student{ Id = 4,Name="葉紅魚",Sex="女",BirthDay=new DateTime(1999,10,10) }
};
//宣告mapper操作物件
var mapper = new Mapper();
//第一個引數為匯出Excel名稱
//第二個引數為Excel資料來源
//第三個引數為匯出的Sheet名稱
//overwrite引數如果是要覆蓋已存在的Excel或者新建Excel則為true,如果在原有Excel上追加資料則為false
//xlsx引數是用於區分匯出的資料格式為xlsx還是xls
mapper.Save("Students.xlsx", students, "sheet1", overwrite: true, xlsx:true);
Console.WriteLine("執行完成");
}
其中overwrite引數如果是要覆蓋已存在的Excel或者新建Excel則為true,如果在原有Excel上追加資料則為false,說白了就是控制是新建Excel檔案還是在原有基礎上直接追加。xlsx引數是用於區分匯出的Excel格式為xlsx還是xls。通過上述簡單程式碼便可以實現Excel的匯出功能,真的是非常簡單,如果你只是進行簡單的匯出操作,通過Npoi.Mapper操作真的是不二的選擇。這樣匯出的Excel效果如下所示
但是這樣匯出的Excel頭資訊為屬性的名稱,而且我們Student類中包含了一個時間欄位BirthDay為DateTime型別,這個表示格式好像也不太符合我們常規的閱讀習慣,那該怎麼辦呢?Npoi.Mapper為我們提供了兩種處理方式,一種是通過Fluent的方式指定對映關係如下所示
var mapper = new Mapper();
//第一個參數列示匯出的列名,第二個表示對應的屬性欄位
mapper.Map<Student>("姓名", s => s.Name)
.Map<Student>("學號", s => s.Id)
.Map<Student>("性別", s => s.Sex)
.Map<Student>("生日", s => s.BirthDay)
//格式化操作,第一個參數列示格式,第二表示對應欄位
//Format不僅僅只支援時間操作,還可以是數字或金額等
.Format<Student>("yyyy-MM-dd", s => s.BirthDay);
mapper.Save("Students.xlsx", students, "sheet1", overwrite: true, xlsx:true);
經過上面相關操作之後匯出後的效果如下所示還有一種形式是通過ColumnAttribute的形式在匯出的實體類的屬性上進行宣告匯出列相關設定,具體操作如下
public class Student
{
[Column("學號")]
public int Id { get; set; }
[Column("姓名")]
public string Name { get; set; }
[Column("性別")]
public string Sex { get; set; }
[Column("生日",CustomFormat = "yyyy-MM-dd")]
public DateTime BirthDay { get; set; }
}
通過這種方式操作和通過Fluent的效果是完全一樣的,至於使用哪一種完全看個人喜好,不過我個人更喜歡在屬性上直接宣告的方式,這樣看起來顯得一目瞭然。
有時候我們可能需要將不同的資料來源匯入到同一個Excel的不同Sheet中,Npoi.Mapper也提供了這方面的支援,具體操作方式如下所示
static void Main(string[] args)
{
//構建Student集合
List<Student> students = new List<Student>
{
new Student{ Id = 1,Name="夫子",Sex="男",BirthDay=new DateTime(1999,10,11) },
new Student{ Id = 2,Name="餘簾",Sex="女",BirthDay=new DateTime(1999,12,12) }
};
//構建Person集合
List<Person> persons = new List<Person>
{
new Person{ Id = 1,Name="陳某", Tel= 18833445566},
new Person{ Id = 2,Name="柯浩然", Tel = 15588997766}
};
var mapper = new Mapper();
//放入Mapper中
//第一個引數是資料集合,第二個引數是Sheet名稱,第三個參數列示是追加資料還是覆蓋資料
mapper.Put<Student>(students, "student",true);
mapper.Put<Person>(persons, "person",true);
mapper.Save("Human.xlsx");
}
不過很多時候我們是通過Web程式直接將資料轉換為檔案流返回的,並不會生成Excel檔案,Npoi.Mapper很貼心的為我們提供了將資料讀取到Stream的操作,操作方式如下
[HttpGet]
public ActionResult DownLoadFile()
{
List<Student> students = new List<Student>
{
new Student{ Id = 1,Name="夫子",Sex="男",BirthDay=new DateTime(1999,10,11) },
new Student{ Id = 2,Name="餘簾",Sex="女",BirthDay=new DateTime(1999,12,12) },
new Student{ Id = 3,Name="李慢慢",Sex="男",BirthDay=new DateTime(1999,11,11) },
new Student{ Id = 4,Name="葉紅魚",Sex="女",BirthDay=new DateTime(1999,10,10) }
};
var mapper = new Mapper();
MemoryStream stream = new MemoryStream();
//將students集合生成的Excel直接放置到Stream中
mapper.Save(stream, students, "sheet1", overwrite: true, xlsx: true);
return File(stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","Student.xlsx");
}
Save提供了幾個過載方法,其中有一個就是將資料儲存到Stream中,但是這裡也踩到了一個坑,不過這個是Npoi的坑並不是Npoi.Mapper的坑,那就是Workbook.Write(stream)的時候會將stream關閉,如果繼續操作這個Stream會報流已關閉的錯誤,而Npoi.Mapper的Save到Stream的方法恰恰是對這個方法的封裝,這也是為何上面我沒直接在File中直接返回Stream,而是將其轉換為byte陣列再返回的原因。
匯入操作
上面我們演示了使用Npoi.Mapper將資料匯出的場景,接下來我們來演示通過Npoi.Mapper的讀取Excel的相關操作,操作也是非常的簡單,話不多說直接上程式碼,比如我讀取上面匯出的Excel
//Excel檔案的路徑
var mapper = new Mapper("Students.xlsx");
//讀取的sheet資訊
var studentRows = mapper.Take<Student>("sheet1");
foreach (var row in studentRows)
{
//對映的資料保留在value中
Student student = row.Value;
Console.WriteLine($"姓名:[{student.Name}],學號:[{student.Id}],性別:[{student.Sex}],生日:[{student.BirthDay:yyyy-MM-dd}]");
}
通過Take方法直接讀取出來的是RowInfo集合,RowInfo是用來包裝讀取資料的包裝類。通過它可以獲取讀取的行號,或讀取過程中可能會出現異常情況,比如某一列讀取失敗,它會將列資訊和報錯資訊記錄下來,如果你不需要這些資訊或者覺得遍歷的時候比較麻煩想直接拿到需要的集合,可以通過如下方式轉換一下
var studentRows = mapper.Take<Student>("sheet1");
//通過lambda獲取到Student集合
var students = studentRows.Select(i => i.Value);
有的時候你可能不想定義一個POCO去接收返回的結果,而是想直接拿到讀取資訊,轉換成你需要的資料格式。比如你想讀取Excel中的資料,將結果轉換為實體類直接入庫,但是你不想定義一個專門的對映類去接收讀取結果,這時候你需要一個動態型別去接收,而Npoi.Mapper恰恰提供了這樣的功能,可以將Excel中的資料直接讀取到dynamic中去,具體操作和上面類似
var mapper = new Mapper("Students.xlsx");
var studentRows = mapper.Take<dynamic>("sheet1");
foreach (var row in studentRows)
{
var student = row.Value;
Console.WriteLine($"姓名:[{student.姓名}],學號:[{student.學號}],性別:[{student.性別}],生日:[{student.生日:yyyy-MM-dd}]");
}
其中你要操作的欄位名稱和Excel的列名是一致的,比如我的Excel列名叫姓名,那麼我讀取的時候對應的屬性名稱也叫姓名。
同樣的情況也存在於匯入操作,比如許多情況下我們是通過Web介面直接上傳的檔案,這種場景下,我們通常能拿到上傳的流資訊,Npoi.Mapper也支援讀取Excel檔案流的形式獲取Excel資料,如下所示
[HttpPost]
public IEnumerable<Student> UploadFile(IFormFile formFile)
{
//通過上傳檔案流初始化Mapper
var mapper = new Mapper(formFile.OpenReadStream());
//讀取sheet1的資料
return mapper.Take<Student>("sheet1").Select(i=>i.Value);
}
其他功能
除了上面介紹的主要功能之外Npoi.Mapper還提供了一些其他的功能,簡單介紹一下幾個比較實用的點
忽略操作
有時候我們的匯出或匯入資料可能想忽略某些列不匯出,Npoi.Mapper為了我們提供了類似EF的Ignore操作
[Ignore]
public string IgnoredProperty { get; set; }
這樣的話無論是匯入還是匯出都會忽略這個屬性,即匯出不會顯示這個列,匯入不會對映這一列的資料
合併單元格
如果我們匯入的資料有一列資料的值是大家都擁有的,在Excel上可以通過合併單元格的操作來顯示這一列,對於合併單元格的列,對於程式來講就是等價於所有列都是同一個值,Npoi.Mapper為我們做了這種處理
[UseLastNonBlankValue]
public string ClassName { get; set; }
自定義Map規則
雖然預設情況下Npoi.Mapper能幫我們滿足大部分的型別對映關係,但是有時候我們需要根據我們自己的規則處理處理資料對映關係,這時候我們需要用到Map功能,他有許多過載的方法,我們就檢視一個比較常用的方法做引數講解
/// <param name="columnName">對應Excel列的名稱</param>
/// <param name="propertyName">對應實體的屬性名稱</param>
/// <param name="tryTake">該函式用於處理從Excel讀取時針對單元格資料的處理</param>
/// <param name="tryPut">該函式用於處理將資料匯出到Excel是針對源資料的處理</param>
public static Mapper Map<T>(this Mapper mapper, string columnName, string propertyName,
Func<IColumnInfo, object, bool> tryTake = null,
Func<IColumnInfo, object, bool> tryPut = null)
{
}
其中tryTake用於處理從Excel匯出時針對單元格資料的處理,IColumnInfo代表資料的來源,object代表對應將Row匯入到某個實體中。tryPut恰恰相反,用於處理將資料匯出到Excel是針對源資料的處理。其中IColumnInfo代表要匯出到的列資訊,object代表資料的源。簡單演示一下,比如我想將上述示例中,讀取到Excel裡的性別資料對映到實體中的時候做一下中英文的處理,就可以使用以下操作
var mapper = new Mapper("Students.xlsx");
mapper.Map<Student>("性別", "Sex", (c, t) => {
Student student = t as Student;
student.Sex = c.CurrentValue == "男" ? "MAN" : "WOMAN";
return true;
}, null);
因為我是要讀取Excel,所以使用tryTake函式,t代表target表示要對映到的實體,c代表讀取到的單元格資訊,我將讀取到target裡的資料做一下處理,如果在單元格中讀取的是"男"那麼對應到Student轉換為"MAN",反之則為"WOMAN"。總之你想處理一下,自定義對映邏輯都可以使用這個功能。
總結
以上是我們對Npoi.Mapper的大致講解,我個人還是非常推薦的。它的使用足夠簡單而且功能非常完善,因為它既可以處理Excel匯入操作,也可以處理Excel匯出操作。它很強大,因為它可以滿足我們日常開發中,大部分關於匯入匯出Excel的場景。但是它還不夠強大,因為它還存在一定的缺陷,而且許多細節可能還沒考慮到。不過慶幸的是,它的原始碼非常的簡單一共不到20個類,而且邏輯非常清晰。如果有的情況它真的不能滿足,我們完全可以下載它的原始碼自己擴充套件操作。最後再次貼上它的GitHub地址https://github.com/donnytian/Npoi.Mapper如果大家有類似的場景可以嘗試使用一下。