Skip to main content

Optimize your API performance with the System.Text.Json source generator

.NET 6 ships with a System.Text.Json source generator as a way to improve your API performance. By default the System.Text.Json serializer is using a lot of reflection behind the scenes. Of course this has a negative impact on startup performance, memory usage and is a problem for assembly trimming.

With the introduction of the System.Text.Json source generator you get a compile-time alternative that can give your API performance a boost. It introduces the following benefits:

  • Increased serialization throughput
  • Reduced start-up time
  • Reduced private memory usage
  • Removed runtime use of System.Reflection and System.Reflection.Emit
  • Trim-compatible serialization which reduces application size

Let me walk you through the steps to configure this for your application.

Configure the System.Text.Json source generator

Source generators are a little bit magical. So we have to take some steps to get it working.

Remark: The source generator is part of the 6.0 release of the System.Text.Json NuGet package.

First we need to create an (internal) partial class which derives from JsonSerializerContext.:

internal partial class JsonContext : JsonSerializerContext
{
}
view raw JsonContext.cs hosted with ā¤ by GitHub

For every type we want to serialize through the source generator, we need to add a JsonSerializableAttribute on top of this class:

[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(ProductDto))]
internal partial class JsonContext : JsonSerializerContext
{
}
view raw JsonContext.cs hosted with ā¤ by GitHub

We can further control the serialization process through the JsonSerializerOptionsAttribute:

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(ProductDto))]
internal partial class JsonContext : JsonSerializerContext
{
}
view raw JsonContext.cs hosted with ā¤ by GitHub

Let’s have a look what is generated by the source generator. You can use a decompiler for that or the MSBuild trick mentioned here.

Here are our data contracts:

public record OrderDto(int Id, DateTime OrderDate, DateTime? ShippedDate);
view raw OrderDto.cs hosted with ā¤ by GitHub
public record ProductDto
{
public ProductDto()
{
}
public int ProductId { get; init; }
public string ProductName { get; init; }
}
view raw ProductDto.cs hosted with ā¤ by GitHub

And here is a part of the generated code:

internal partial class JsonContext
{
private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::eShopExample.Application.Products.Dto.ProductDto>? _ProductDto;
public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::eShopExample.Application.Products.Dto.ProductDto> ProductDto
{
get
{
if (_ProductDto == null)
{
global::System.Text.Json.Serialization.JsonConverter? customConverter;
if (Options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(global::eShopExample.Application.Products.Dto.ProductDto))) != null)
{
_ProductDto = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo<global::eShopExample.Application.Products.Dto.ProductDto>(Options, customConverter);
}
else
{
global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::eShopExample.Application.Products.Dto.ProductDto> objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::eShopExample.Application.Products.Dto.ProductDto>()
{
ObjectCreator = static () => new global::eShopExample.Application.Products.Dto.ProductDto(),
ObjectWithParameterizedConstructorCreator = null,
PropertyMetadataInitializer = ProductDtoPropInit,
ConstructorParameterMetadataInitializer = null,
NumberHandling = default,
SerializeHandler = ProductDtoSerializeHandler
};
_ProductDto = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::eShopExample.Application.Products.Dto.ProductDto>(Options, objectInfo);
}
}
return _ProductDto;
}
}
private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] ProductDtoPropInit(global::System.Text.Json.Serialization.JsonSerializerContext context)
{
global::eShopExample.Web.JsonContext jsonContext = (global::VeShopExample.Web.JsonContext)context;
global::System.Text.Json.JsonSerializerOptions options = context.Options;
global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] properties = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[3];
global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Type> info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Type>()
{
IsProperty = true,
IsPublic = false,
IsVirtual = true,
DeclaringType = typeof(global::eShopExample.Application.Products.Dto.ProductDto),
PropertyTypeInfo = jsonContext.Type,
Converter = null,
Getter = null,
Setter = null,
IgnoreCondition = null,
HasJsonInclude = false,
IsExtensionData = false,
NumberHandling = default,
PropertyName = "EqualityContract",
JsonPropertyName = null
};
properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Type>(options, info0);
global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Int32> info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Int32>()
{
IsProperty = true,
IsPublic = true,
IsVirtual = false,
DeclaringType = typeof(global::eShopExample.Application.Products.Dto.ProductDto),
PropertyTypeInfo = jsonContext.Int32,
Converter = null,
Getter = static (obj) => ((global::eShopExample.Application.Products.Dto.ProductDto)obj).ProductId,
Setter = static (obj, value) => throw new global::System.InvalidOperationException("Deserialization of init-only properties is currently not supported in source generation mode."),
IgnoreCondition = null,
HasJsonInclude = false,
IsExtensionData = false,
NumberHandling = default,
PropertyName = "ProductId",
JsonPropertyName = null
};
properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Int32>(options, info1);
global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.String> info2 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.String>()
{
IsProperty = true,
IsPublic = true,
IsVirtual = false,
DeclaringType = typeof(global::eShopExample.Application.Products.Dto.ProductDto),
PropertyTypeInfo = jsonContext.String,
Converter = null,
Getter = static (obj) => ((global::eShopExample.Application.Products.Dto.ProductDto)obj).ProductName!,
Setter = static (obj, value) => throw new global::System.InvalidOperationException("Deserialization of init-only properties is currently not supported in source generation mode."),
IgnoreCondition = null,
HasJsonInclude = false,
IsExtensionData = false,
NumberHandling = default,
PropertyName = "ProductName",
JsonPropertyName = null
};
properties[2] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.String>(options, info2);
return properties;
}
private static void ProductDtoSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::eShopExample.Application.Products.Dto.ProductDto? value)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
writer.WriteStartObject();
writer.WriteNumber(PropName_productId, ((global::eShopExample.Application.Products.Dto.ProductDto)value).ProductId);
writer.WriteString(PropName_productName, ((global::eShopExample.Application.Products.Dto.ProductDto)value).ProductName);
writer.WriteEndObject();
}
}
view raw JsonContext.g.cs hosted with ā¤ by GitHub

Whow! That is a lot of code...

Use the generated code with the JsonSerializer

The JsonSerializer introduces some new overloads that allow you to use the generated code:

OrderDto order = new("1", DateTime.Now,DateTime.Now);
byte[] utf8Json = JsonSerializer.SerializeToUtf8Bytes(order, MyJsonContext.Default.OrderDto);
order = JsonSerializer.Deserialize(utf8Json, MyJsonContext.Default.OrderDto):
view raw JsonSerializer.cs hosted with ā¤ by GitHub

Or:

string json=JsonSerializer.Serialize(order, MyJsonContext.Default.OrderDto);
view raw JsonSerializer.cs hosted with ā¤ by GitHub

Integrate it in your ASP.NET Core application

Of course in your ASP.NET Core application you typically don’t invoke the JsonSerializer directly and typically just return a model:

namespace eShopExample.Web.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
// GET api/orders
[HttpGet]
public async Task<ActionResult<IEnumerable<OrderDto>>> Get()
{
var orders = await _orderService.GetOrders();
return Ok(orders);
}
}
}
view raw OrdersController.cs hosted with ā¤ by GitHub

In that case how can we use this source generator? We need to call the AddJsonOptions() on the IServiceCollection and specify our JsonContext class on the JsonSerializerOptions:

public void ConfigureServices(IServiceCollection services)
{
services
.AddControllersWithViews()
.AddJsonOptions(options => options.JsonSerializerOptions.AddContext<JsonContext>())
.AddControllersAsServices();
}
view raw Startup.cs hosted with ā¤ by GitHub

So that should be enough to get started with the System.Text.Json source generator.

If you want to learn more, have a look here: https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/

Popular posts from this blog

Kubernetes–Limit your environmental impact

Reducing the carbon footprint and CO2 emission of our (cloud) workloads, is a responsibility of all of us. If you are running a Kubernetes cluster, have a look at Kube-Green . kube-green is a simple Kubernetes operator that automatically shuts down (some of) your pods when you don't need them. A single pod produces about 11 Kg CO2eq per year( here the calculation). Reason enough to give it a try! Installing kube-green in your cluster The easiest way to install the operator in your cluster is through kubectl. We first need to install a cert-manager: kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.5/cert-manager.yaml Remark: Wait a minute before you continue as it can take some time before the cert-manager is up & running inside your cluster. Now we can install the kube-green operator: kubectl apply -f https://github.com/kube-green/kube-green/releases/latest/download/kube-green.yaml Now in the namespace where we want t...

Azure DevOps/ GitHub emoji

I’m really bad at remembering emoji’s. So here is cheat sheet with all emoji’s that can be used in tools that support the github emoji markdown markup: All credits go to rcaviers who created this list.

.NET 9 - Goodbye sln!

Although the csproj file evolved and simplified a lot over time, the Visual Studio solution file (.sln) remained an ugly file format full of magic GUIDs. With the latest .NET 9 SDK(9.0.200), we finally got an alternative; a new XML-based solution file(.slnx) got introduced in preview. So say goodbye to this ugly sln file: And meet his better looking slnx brother instead: To use this feature we first have to enable it: Go to Tools -> Options -> Environment -> Preview Features Check the checkbox next to Use Solution File Persistence Model Now we can migrate an existing sln file to slnx using the following command: dotnet sln migrate AICalculator.sln .slnx file D:\Projects\Test\AICalculator\AICalculator.slnx generated. Or create a new Visual Studio solution using the slnx format: dotnet new sln --format slnx The template "Solution File" was created successfully. The new format is not yet recognized by VSCode but it does work in Jetbr...