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.
Top comments (0)