[ASP.NET Core][Entity Framework Core] Try System.Text.Json
Tags: csharp, aspnetcore, entityframeworkcore
Published: 2022-03-08 17:55:05

## Intro
Although "System.Text.Json" has become the default JSON library of ASP.NET Core now.
But I have still used "Newtonsoft.Json".

Because I felt inconvenient using "System.Text.Json" when I used it last time.
But after that, it has been update several times.

So I will try it again.

### Environments
* .NET ver.6.0.200

## Sample projects
#### Book.cs
```cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BookshelfSample.Models;
[Table("book")]
public record class Book
{
    [Key]
    [Column("id")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; init; }
    [Required]
    [Column("name")]
    public string Name { get; init; } = "";
    [Required]
    [Column("author_id")]
    public int AuthorId { get; init; }
    [Required]
    [Column("language_id")]
    public int LanguageId { get; init; }
    [Column("purchase_date", TypeName = "date")]
    public DateOnly? PurchaseDate { get; init; }
    [Column("price", TypeName = "money")]
    public decimal? Price { get; init; }
    [Required]
    [Column("last_update_date", TypeName = "timestamp with time zone")]
    public DateTime LastUpdateDate { get; init; }
    public Author Author { get; init; } = new Author();
}
```
#### Author.cs
```cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BookshelfSample.Models;
[Table("author")]
public record class Author
{
    [Key]
    [Column("id")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; init; }
    [Required]
    [Column("name")]
    public string Name { get; init; } = "";
    public List<Book> Books { get; init; } = new List<Book>();
}
```
#### ISearchBooks.cs
```cs
using BookshelfSample.Books.Dto;
using BookshelfSample.Models;
namespace BookshelfSample.Books;
public interface ISearchBooks
{
    Task<List<Book>> GetAllAsync();
}
```
#### SearchBooks.cs
```cs
using BookshelfSample.Books.Dto;
using BookshelfSample.Models;
using Microsoft.EntityFrameworkCore;
namespace BookshelfSample.Books;
public class SearchBooks: ISearchBooks
{
    private readonly BookshelfContext context;
    public SearchBooks(BookshelfContext context)
    {
        this.context = context;
    }
    public async Task<List<Book>> GetAllAsync()
    {
        return await this.context.Books
            .Include(b => b.Author)
            .ToListAsync();
    }
}
```
#### BookController.cs
```cs
using BookshelfSample.Books;
using BookshelfSample.Books.Dto;
using BookshelfSample.Models;
using Microsoft.AspNetCore.Mvc;

namespace BookshelfSample.Controllers;
public class BookController: Controller
{
    private readonly ILogger<BookController> logger;
    private readonly ISearchBooks books;
    public BookController(ILogger<BookController> logger,
        ISearchBooks books)
    {
        this.logger = logger;
        this.books = books;
    }
    [Route("")]
    public IActionResult Index()
    {
        return View("Views/Index.cshtml");
    }
    [HttpGet]
    [Route("books/messages")]
    public async Task<IActionResult> GetMessage()
    {
        return Json(await this.books.GetAllAsync());
    }
    [HttpPost]
    [Route("books/messages")]
    public IActionResult GenerateMessage([FromBody] Book book)
    {
        logger.LogDebug($"BOOK: {book}");
        return Json(await this.books.GetAllAsync());
    }
}
```
#### main.page.ts
```ts
export async function getMessage(): Promise<void> {
    const response = await fetch("books/messages",
    {
        mode: "cors",
        method: "GET"
    });
    console.log(await response.json());    
}
export async function postMessage(): Promise<void> {
    const response = await fetch("books/messages",
    {
        mode: "cors",
        method: "POST",
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            id: 3,
            name: "Hello",
            authorId: 1,
            languageId: 2,
            price: 3000,
        }),
    });
    console.log(await response.json());  
}
``` 
#### Index.cshtml
```html
<!DOCTYPE html>
<html>
    <head>
        <title>Bookshelf sample</title>
        <meta charset="utf-8">
    </head>
    <body>
        <button onclick="Page.getMessage()">Get</button>
        <button onclick="Page.postMessage()">Post</button>
        <script src="js/main.page.js"></script>
    </body>
</html>
```

## Ignore reference loop
Because "System.Text.Json" is set as default, I can use it directly in ASP.NET Core projects.

One important problem is self reference loop.
By default, when I call "GetMessage" or "GenerateMessage" from client side, I will get exceptions.

Because "Book" has "Author" and "Author" has a list of "Book".

* [[ASP.NET Core][Entity Framework Core] Errors for inserting and reading](https://dev.to/masanori_msl/asp-net-core-entity-framework-core-errors-for-inserting-and-reading-1p91)

As same as "Newtonsoft.Json", I have to add JsonOptions.

#### Program.cs
```cs
using System.Text.Json.Serialization;
using BookshelfSample.Books;
using BookshelfSample.Models;
using Microsoft.EntityFrameworkCore;
using NLog.Web;
...
    var builder = WebApplication.CreateBuilder(args);
...
    builder.Services.AddRazorPages();
    
    builder.Services.AddControllers()
        .AddJsonOptions(options => {
            // Ignore self reference loop
            options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
        });

    builder.Services.AddDbContext<BookshelfContext>(options =>
    {
        options.UseNpgsql(builder.Configuration["DbConnection"]);
    });
    builder.Services.AddScoped<IAuthors, Authors>();
    builder.Services.AddScoped<ISearchBooks, SearchBooks>();
    var app = builder.Build();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
    app.Run();
...
```

## Use Pascal case
By default, the property names are named as lower camel case.
To use Pascal case, I can add JsonOptions.

#### Program.cs
```cs
...
    builder.Services.AddControllers()
        .AddJsonOptions(options => {
            // Ignore self reference loop
            options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
            // set as pascal case
            options.JsonSerializerOptions.PropertyNamingPolicy = null;
        });
...
```

## DateOnly
By default, I can't use "DateOnly" and "TimeOnly".

```
System.NotSupportedException: Serialization and deserialization of 'System.DateOnly' instances are not supported. The unsupported member type is located on type 'System.Nullable`1[System.DateOnly]'. Path: $.purchaseDate | LineNumber: 0 | BytePositionInLine: 78.
       ---> System.NotSupportedException: Serialization and deserialization of 'System.DateOnly' instances are not supported.
         at System.Text.Json.Serialization.Converters.UnsupportedTypeConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
         at System.Text.Json.Serialization.Converters.NullableConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
         at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
         at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
         at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
         at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
         --- End of inner exception stack trace ---
...
```

According to this blog post, I have to add converters.

* [DateOnly and TimeOnly support with System.Text.Json - Marco Minerva's Blog](https://marcominerva.wordpress.com/2021/11/22/dateonly-and-timeonly-support-with-system-text-json/)

In this time, I add a "DateTime" property to treat the "DateOnly" property between the client-side and the server-side.

#### Book.cs
```cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace BookshelfSample.Models;
[Table("book")]
public record class Book
{
...
    private DateOnly? purchaseDate;
    [JsonIgnore]
    [Column("purchase_date", TypeName = "date")]
    public DateOnly? PurchaseDate
    {
        get { return this.purchaseDate; }
        init { this.purchaseDate = value; }
    }
    [NotMapped]
    public DateTime? PurchaseDateTime
    {
        get { return this.purchaseDate?.ToDateTime(new TimeOnly(0)); }
        init {
            this.purchaseDate = (value == null)? null: DateOnly.FromDateTime(value.Value);
        }
    }
...
}
```

## Outro
Except using "DateOnly", I don't have any problems to use "System.Text.Json" in .NET 6.
So I will start to use it in my new projects.

## Resources
* [Compare Newtonsoft.Json to System.Text.Json, and migrate to System.Text.Json - Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-6-0)
* [Configuring JSON options in ASP.NET Core - Meziantou's blog](https://www.meziantou.net/configuring-json-options-in-asp-net-core.htm)
* [System.Text.Json を試してみる on .NET 6 - Zenn](https://zenn.dev/okazuki/articles/system-text-json-on-dotnet6)