使用System.Text.Json入門 - 1

by vivid 5. 二月 2020 01:03

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N200221601
出刊日期: 2020/2/5

在過去使用ASP.NET MVC與 .NET Core開發的專案之中,經常會使用到「Json.NET程式庫」來處理JSON(JavaScript Object Notation)資料格式的序列化(Serialization)與還原序列化(Deserialization)的動作,以便於在內、外部系統中做資料交換。在.NET Core 3版之後,內建了「System.Text.Json」套件來處理這個問題,讓你可以不必再依賴非官方的「Json.NET」程式庫,而可以根據喜好來選擇這兩種不同的序列化程式庫。

若專案的目標Framework(Target Framework)設定為「.NET Standard」或「.NET Framework 4.6.1」以上版本,則需要在專案之中使用NuGet手動安裝「System.Text.Json」套件,方能夠透過它來進行開發。

至於效能方面,有許多的文章針對「Json.NET」與「System.Text.Json」這兩個套件做比較,就結果而言,大部分時候.NET Core內建的「System.Text.Json」套件的效能是優於「Json.NET」套件,在此篇文章之中,暫不討論效能這個問題,現在你可以開始考慮開始來試試「System.Text.Json」這個套件,了解它的基本功能與用法。

「System.Text.Json」與「System.Text.Json.Serialization」命名空間包含了大部分的類別與API來處理自訂序列化與還原序列化的情境。讓我們先利用一個主控台應用程式來熟悉一下「System.Text.Json」用法。使用Visual Studio 2019開發工具建立專案時,選擇「主控台應用程式 (.NET Core)」,請參考下圖所示:

clip_image002

圖 1:建立「主控台應用程式 (.NET Core)」。

接著設定專案名稱、與程式存放資料夾,按下「建立」按鈕就可以建立專案,請參考下圖所示:

clip_image004

圖 2:設定新專案。

在專案中加入「Employee」類別,假設有一個員工資料如下:

public class Employee {
  public int EmployeeId { get; set; }
  public string EmployeeName { get; set; }
  public DateTime BirthDate { get; set; }
  public bool IsMarried { get; set; }

  public List<string> Interests { get; set; }
  public WorkExperience WorkExperience { get; set; }
}
public class WorkExperience {
  public string CompanyName { get; set; }
  public int Years { get; set; }
}


 

我們可以使用以下程式碼,引用「System.Text.Json」命名空間,將「Employee」物件的屬性值,利用「JsonSerializer」類別的「Serialize」方法序列化成JSON字串,再利用「JsonSerializer」類別的「Deserialize」方法將JSON字串還原回「Employee」物件:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {
  class Program {
    static void Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      Console.WriteLine("Serize : ");
      var jsonString = JsonSerializer.Serialize( emp );
      Console.WriteLine( jsonString );

      Console.WriteLine("===============================");
      Console.WriteLine("Deserialize : ");
      var obj = JsonSerializer.Deserialize<Employee>( jsonString );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join(',',obj.Interests ));
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }
}

 

這個主控台程式執行的結果參考如下:

clip_image006

圖 3:序列化與還原序列化測試結果。

JsonSerializerOptions物件

「JsonSerializerOptions」物件提供許多屬性可以搭配「JsonSerializer」類別來控制序列化的細節。以下介紹一些常用的屬性。

WriteIndented屬性

為了方便閱讀序列化完的結果,我們可以使用「JsonSerializerOptions」物件的「WriteIndented」屬性,適當地將序列化完的結果美化、排版,例如修改上個範例的程式碼如下:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );
      var obj = JsonSerializer.Deserialize<Employee>( jsonString );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

叫用「JsonSerializer」類別的「Serialize」方法時,傳入「JsonSerializerOptions」物件做參數,

則範例執行結果,請參考下圖所示:

clip_image008

圖 4:序列化與還原序列化測試結果。

通常「JsonSerializerOptions」物件的「WriteIndented」屬性設定為「true」只是方便閱讀,一般生產環境中,建議將其設定為「false」,可使序列化完的結果儘可能精簡,這樣有助於提升傳輸的效能。

序列化到檔案

若想要將序列化完成的結果儲存成文字檔案,則可以利用「System.IO」命名空間下的「File」類別提供的「WriteAllText」方法;而讀取文字檔案可以利用「File」類別提供的「ReadAllText」方法,參考以下範例程式碼,直接將序列化完成的結果儲存成文字檔案:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );

      System.IO.File.WriteAllText( "json.txt" , jsonString );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var str = System.IO.File.ReadAllText( "json.txt" );
      var obj = JsonSerializer.Deserialize<Employee>( str );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

使用非同步方式進行序列化

「JsonSerializer」類別也提供了「SerializeAsync」與「DeserializeAsync」方法用於非同步的序列化與還原序列化,參考以下使用非同步方式的程式碼:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

namespace JSONApp1 {

  class Program {
    static async Task Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );

      using ( FileStream fs = File.Create( "json2.txt" ) ) {
        await JsonSerializer.SerializeAsync( fs , emp );
      }

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      using ( FileStream fs = File.OpenRead( "json2.txt" ) ) {
        var obj = await JsonSerializer.DeserializeAsync<Employee>( fs );
        Console.WriteLine( obj.EmployeeId );
        Console.WriteLine( obj.EmployeeName );
        Console.WriteLine( obj.BirthDate );
        Console.WriteLine( obj.IsMarried );
        Console.WriteLine( string.Join( ',' , obj.Interests ) );
        Console.WriteLine( obj.WorkExperience.CompanyName );
        Console.WriteLine( obj.WorkExperience.Years );
      }
    }
  }
  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

特別注意,「await」關鍵字只能夠在非同步方法之中使用,因此「Main」方法要加上「async」關鍵字宣告為非同步方法,並使其方法的回傳值為「Task」物件。

PropertyNamingPolicy屬性

在撰寫JavaScript時,習慣性名稱會使用camelCase命名法,而C#程式語言則習慣使用PascalCase命名法,此時我們便可以利用「JsonSerializerOptions」物件的「PropertyNamingPolicy」屬性來控制序列化、與還原序列化的屬性名稱要改用camelCase命名法,參考下範例程式碼,為了方便閱讀,直接將序列化完的結果輸出到主控台:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase ,
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );


      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( jsonString , options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

 

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

這個範例程式的執行結果參考如下:

clip_image010

圖 5:序列化與還原序列化測試結果。

特別要注意,叫用「JsonSerializer」類別「Deserialize」方法進行還原序列化時,需使用相同的「JsonSerializerOptions」設定,否則可能會得到例外錯誤。

IgnoreNullValues 屬性

「JsonSerializerOptions」物件的「IgnoreNullValues」 屬性預設值為「 false」,當要序列化的物件屬性值為「null」時,依然會將屬性名稱與「null」包含在序列化完的結果,例如以下的程式碼,故意將「Employee」物件的「EmployeeName」屬性設定為「null」:

 

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        //EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        IgnoreNullValues = false ,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase ,
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );


      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( jsonString , options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }


  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

當「IgnoreNullValues」屬性的值為「false」的情況下,範例程式的執行結果參考如下,包含「EmployeeName」項目:

clip_image012

圖 6:序列化與還原序列化測試結果。

當「IgnoreNullValues」屬性的值為「true」的情況下:

var options = new JsonSerializerOptions {
        IgnoreNullValues = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase ,
        WriteIndented = true
      };

 

範例程式的執行結果參考如下,不包含「EmployeeName」項目:

clip_image014

圖 7:序列化與還原序列化測試結果。

IgnoreReadOnlyProperties 屬性

預設類別的公開(public)屬性都將被序列化。若屬性是唯讀的,或是包含public getter / private setter,只要設定「JsonSerializerOptions」類別的「IgnoreReadOnlyProperties」屬性為「true」,就可以排除這些屬性的序列化,例如以下範例程式碼,「Employee」類別包含唯讀的「CompanyName」屬性,利用「IgnoreReadOnlyProperties」忽略掉唯讀屬性不序列化:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        IgnoreReadOnlyProperties = true ,
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );


      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( jsonString , options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( obj.CompanyName );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }


  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }

    public string CompanyName { get; private set; } = "UCOM";
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

這個範例執行結果請參考下圖,「IgnoreReadOnlyProperties」屬性只會影響序列化的過程,不影響還原序列化,還原序列化的過程中會將之忽略:

clip_image016

圖 8:序列化與還原序列化測試結果。

Encoder屬性

預設非ASCII字元都會以「\uxxx」格式呈現,例如上例程式碼,只要輸入中文字元:

Employee emp = new Employee() {
  EmployeeId = 1 ,
  EmployeeName = "瑪麗" ,
  BirthDate = new DateTime( 2000 , 1 , 2 ) ,
  IsMarried = false ,
  Interests = new List<string> { "游泳" , "爬山" , "跑步" } ,
  WorkExperience = new WorkExperience() {
    CompanyName = "UUU" ,
    Years = 3
  }
};

序列化完的結果看起來如下:

Serize :

{

"EmployeeId": 1,

"EmployeeName": "\u746A\u9E97",

"BirthDate": "2000-01-02T00:00:00",

"IsMarried": false,

"Interests": [

"\u6E38\u6CF3",

"\u722C\u5C71",

"\u8DD1\u6B65"

],

"WorkExperience": {

"CompanyName": "UUU",

"Years": 3

}

}

這個範例執行結果請參考下圖:

clip_image018

圖 9:序列化與還原序列化測試結果。

設定「Encoder」屬性,可以處理多個語言的編碼問題,參考以下範例程式碼,設定「Encoder」屬性的值為「UnicodeRanges.All」,可序列化所有語言,不使用「\uxxx」替代:

 

using System;
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "瑪麗" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "游泳" , "爬山" , "跑步" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };


      var options = new JsonSerializerOptions {
        Encoder = JavaScriptEncoder.Create( UnicodeRanges.All ),
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );
      var obj = JsonSerializer.Deserialize<Employee>( jsonString );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

這個範例執行結果請參考下圖:

clip_image020

圖 10:序列化與還原序列化測試結果。

UnsafeRelaxedJsonEscaping屬性

要序列化所有字元還可以使用「JavaScriptEncoder」類別的「UnsafeRelaxedJsonEscaping」屬性,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "瑪麗" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "游泳" , "爬山" , "跑步" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };


      var options = new JsonSerializerOptions {
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping ,
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );
      var obj = JsonSerializer.Deserialize<Employee>( jsonString );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

設定「UnsafeRelaxedJsonEscaping」屬性與預設編碼器相較之下,編碼較為寬鬆,例如在網頁程式常使用到的<、>符號都不會進行編碼。

Tags:

.NET Core | .NET Magazine國際中文電子雜誌 | C# | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List