[ASP.NET Core][Entity Framework Core] Try System.Text.Json
Masui Masanori
Posted on March 8, 2022
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();
}
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>();
}
ISearchBooks.cs
using BookshelfSample.Books.Dto;
using BookshelfSample.Models;
namespace BookshelfSample.Books;
public interface ISearchBooks
{
Task<List<Book>> GetAllAsync();
}
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();
}
}
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());
}
}
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());
}
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>
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();
...
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;
});
...
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.
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);
}
}
...
}
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
Posted on March 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.