.Net Core Excel導入導出神器Npoi.Mapper

前言

    我們在日常開發中對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如果大家有類似的場景可以嘗試使用一下。

👇歡迎掃碼關注我的公衆號👇
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章