.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
andSystem.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 | |
{ | |
} |
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 | |
{ | |
} |
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 | |
{ | |
} |
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); |
public record ProductDto | |
{ | |
public ProductDto() | |
{ | |
} | |
public int ProductId { get; init; } | |
public string ProductName { get; init; } | |
} |
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(); | |
} | |
} |
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): |
Or:
string json=JsonSerializer.Serialize(order, MyJsonContext.Default.OrderDto); |
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); | |
} | |
} | |
} |
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(); | |
} |
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/