DEV Community

Masui Masanori
Masui Masanori

Posted on

3 2

[ASP.NET Core][Entity Framework Core] Try System.Text.Json

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

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();
}
Enter fullscreen mode Exit fullscreen mode

Author.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>();
}
Enter fullscreen mode Exit fullscreen mode

ISearchBooks.cs

using BookshelfSample.Books.Dto;
using BookshelfSample.Models;
namespace BookshelfSample.Books;
public interface ISearchBooks
{
    Task<List<Book>> GetAllAsync();
}
Enter fullscreen mode Exit fullscreen mode

SearchBooks.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();
    }
}
Enter fullscreen mode Exit fullscreen mode

BookController.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());
    }
}
Enter fullscreen mode Exit fullscreen mode

main.page.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());  
}
Enter fullscreen mode Exit fullscreen mode

Index.cshtml

<!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>
Enter fullscreen mode Exit fullscreen mode

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".

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

Program.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();
...
Enter fullscreen mode Exit fullscreen mode

Use Pascal case

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

Program.cs

...
    builder.Services.AddControllers()
        .AddJsonOptions(options => {
            // Ignore self reference loop
            options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
            // set as pascal case
            options.JsonSerializerOptions.PropertyNamingPolicy = null;
        });
...
Enter fullscreen mode Exit fullscreen mode

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 ---
...
Enter fullscreen mode Exit fullscreen mode

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

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

Book.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);
        }
    }
...
}
Enter fullscreen mode Exit fullscreen mode

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

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay