{"id":4992,"date":"2022-08-17T00:14:27","date_gmt":"2022-08-17T07:14:27","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=4992"},"modified":"2022-08-18T21:35:56","modified_gmt":"2022-08-19T04:35:56","slug":"using-the-new-json-writer-in-odata","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/using-the-new-json-writer-in-odata\/","title":{"rendered":"Using the new JSON writer in OData"},"content":{"rendered":"<p><code>Microsoft.OData.Core<\/code> version 7.12.2 has introduced a new JSON writer that\u2019s based on .NET\u2019s <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.text.json.utf8jsonwriter\">Utf8JsonWriter<\/a>. The current JSON writer used in <code>Microsoft.OData.Core<\/code>, internally called <code>JsonWriter<\/code>, is a custom implementation that uses <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.io.textwriter\"><code>TextWriter<\/code><\/a> to write the JSON output to the destination stream. We decided to write a new implementation to take advantage of the performance optimizations in <code>Utf8JsonWriter<\/code>.<\/p>\n<p>In order not to disrupt existing customers, people will have to explicitly \u201copt-in\u201d to using the new writer. Currently, <code>ODataMessageWriter<\/code> allows you to override the default <code>IJsonWriter<\/code> used for writing JSON output by providing a custom <code>IJsonWriterFactory<\/code> to <a href=\"https:\/\/docs.microsoft.com\/en-us\/odata\/odatalib\/di-support\">the dependency-injection container that\u2019s attached to the message instance<\/a> (<code>IODataResponseMessage<\/code> or <code>IODataRequestMessage<\/code>). However, the existing <code>IJsonWriterFactory<\/code> is tightly coupled to <code>TextWriter<\/code>. We introduced a new factory interface that allows writing directly to the output <code>Stream<\/code> instead of <code>TextWriter<\/code>. We call it <code>IStreamBasedJsonWriterFactory<\/code>. The default implementation of this factory is called <code>DefaultStreamBasedJsonWriter.<\/code> It creates an instance of the new JSON Writer, internally called <code>ODataUtf8JsonWriter<\/code>. To opt-in to using the new writer, you need to add this factory as a service to the dependency-injection container. We\u2019ll demonstrate how to do that in OData Web API 7.x and OData Web API 8.x libraries and when using <code>ODataMessageWriter<\/code> directly.<\/p>\n<h2>OData Web API 8.x<\/h2>\n<p>You can use the new writer starting with <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.AspNetCore.OData\/\"><code>Microsoft.AspNetCore.OData<\/code><\/a> 8.0.11. You can configure custom services when registering new OData route components using the <code>AddRouteComponens<\/code> method. Here\u2019s an example:<\/p>\n<p>Add the following using statement to the <code>Startup.cs<\/code> file, or the file where you configure application services:<\/p>\n<pre class=\"prettyprint\">using Microsoft.OData.Json;<\/pre>\n<p>Then register the new factory inside the <code>Startup.ConfigureServices<\/code> method or the method where you\u2019re configuring application services if not <code>Startup.ConfigureServices<\/code>:<\/p>\n<pre class=\"prettyprint\">services.AddControllers()\r\n    .AddOData(options =&gt;\r\n    options.AddRouteCompontents(\u201codata\u201d, model, services =&gt;\r\n    {\r\n        Services.AddSingleton&lt;IStreamBasedJsonWriterFactory&gt;(_ =&gt; DefaultStreamBasedJsonWriterFactory.Default);\r\n    });\r\n<\/pre>\n<h2>OData Web API 7.x<\/h2>\n<p>This feature is available in <code>Microsoft.AspNetCore.OData<\/code> 7.5.17. There are different methods to configure custom OData services depending on whether you\u2019re using endpoint routing or classic MVC routing.<\/p>\n<p><div class=\"alert alert-warning\">This feature was initially announced with Microsoft.AspNetCore.OData version 7.5.16. However, customers reported <a href=\"https:\/\/github.com\/OData\/WebApi\/issues\/2700\">issues<\/a> with that release. As result, 7.5.16 has been unlisted on Nuget and 7.5.17 was released to address the issues.<\/div><\/p>\n<h2>Endpoint routing<\/h2>\n<p>If you\u2019re using Endpoint routing, then you configure the OData route using <code>endpoints.MapODataRoute<\/code> in the action passed to <code>app.UseEndpoints<\/code> in <code>Startup.Configure<\/code>. There\u2019s an overload of <code>MapODataRoute<\/code> that allows you to configure custom OData services. We\u2019ll use this to inject the new factory class into the internal service container. We\u2019ll also need to manually register the default routing conventions since this overload of <code>MapODataRoute<\/code> does not handle that automatically, otherwise routing will not work properly.<\/p>\n<p>Example:<\/p>\n<p>Ensure you have the following using statements to the <code>Startup.cs<\/code> file<\/p>\n<pre class=\"prettyprint\">using System.Collections.Generic;\r\nusing Microsoft.AspNet.OData.Routing.Conventions;\r\nusing Microsoft.OData;\r\nusing Microsoft.OData.Json;\r\n<\/pre>\n<pre class=\"prettyprint\">public void Configure(IApplicationBuilder app, IWebHostEnvironment env)\r\n{\r\n\r\n    IEdmModel model = GetEdmModel();\r\n\r\n    app.UseODataBatching();\r\n\r\n    app.UseRouting();\r\n\r\n    app.UseEndpoints(endpoints =&gt;\r\n    {\r\n        endpoints.MapODataRoute(\r\n            \"odata\", \u201codata\u201d,\r\n            container =&gt;\r\n            {\r\n                container.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp =&gt; model);\r\n                container.AddService&lt;IStreamBasedJsonWriterFactory&gt;(Microsoft.OData.ServiceLifetime.Singleton, sp =&gt; DefaultStreamBasedJsonWriterFactory.Default);\r\n                container.AddService&lt;IEnumerable&lt;IODataRoutingConvention&gt;&gt;(Microsoft.OData.ServiceLifetime.Singleton,\r\n                    sp =&gt; ODataRoutingConventions.CreateDefaultWithAttributeRouting(\"nullPrefix\", endpoints.ServiceProvider));\r\n            });\r\n    });\r\n}\r\n<\/pre>\n<h2>MVC routing<\/h2>\n<p>If you\u2019re using MVC routing, you can configure custom OData services inside the <code>Startup.Configure<\/code> method by passing configuration action to the <code>MapODataServiceRoute<\/code> method. Using this overload of <code>MapODataServiceRoute<\/code> will also require you to manually register the default routing conventions, otherwise routing will not work properly.<\/p>\n<p>Add the following using statements to the <code>Startup.cs<\/code> file.<\/p>\n<pre class=\"prettyprint\">using System.Collections.Generic;\r\nusing Microsoft.AspNet.OData.Routing.Conventions;\r\nusing Microsoft.OData;\r\nusing Microsoft.OData.Json;\r\n<\/pre>\n<pre class=\"prettyprint\">public void Configure(IApplicationBuilder app, IWebHostEnvironment env)\r\n{\r\n    IEdmModel model = GetEdmModel();\r\n\r\n    app.UseMvc(builder =&gt;\r\n    {\r\n        builder.Select().Expand().Filter().OrderBy().MaxTop(100).Count();\r\n\r\n        builder.MapODataServiceRoute(\"odata\", \"odata\", container =&gt;\r\n        {\r\n            container.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp =&gt; model)\r\n                .AddService&lt;IStreamBasedJsonWriterFactory&gt;(Microsoft.OData.ServiceLifetime.Singleton, sp =&gt; DefaultStreamBasedJsonWriterFactory.Default)\r\n                .AddService&lt;IEnumerable&lt;IODataRoutingConvention&gt;&gt;(Microsoft.OData.ServiceLifetime.Singleton, sp =&gt;\r\n                    ODataRoutingConventions.CreateDefaultWithAttributeRouting(\"odata\", builder));\r\n        });\r\n    });\r\n}\r\n<\/pre>\n<p>&nbsp;<\/p>\n<h2>ODataMessageWriter<\/h2>\n<p>If you\u2019re using ODataMessageWriter directly from <code>Microsot.OData.Core<\/code> to write JSON payloads, it takes a few extra steps to configure.<\/p>\n<p>To write a message, you need to provide an implementation of <code>IODataResponseMessage<\/code> when writing a response and <code>IODataRequestMessage<\/code> when writing a request body. Whatever I mention next about <code>IODataResponseMessage<\/code> also applies to <code>IODataRequestMessage<\/code>. To make your custom services accessible to the <code>ODataMessageWriter<\/code>, your implementation of <code>IODataResponseMessage<\/code> must also implement <code>IContainerProvider<\/code>. This is a simple interface that exposes a <code>Container<\/code> property of type <code>IServiceProvider<\/code>. <code>ODataMessageWriter<\/code> will use this property (if it has been defined on the message instance) to request services it needs, like an implementation of the <code>IStreamBasedJsonWriterFactory<\/code>.<\/p>\n<p>For example, an <code>IODataResponseMessage<\/code> implementation could look like<\/p>\n<pre class=\"prettyprint\">class MyResponseMessage : IODataResponseMessage, IContainerProvider \r\n{ \r\n    \/\/\u2026 other methods omitted for brevity\r\n    IServiceProvider Container { get; init; };\r\n}\r\n<\/pre>\n<p>You must also implement <code>IContainerBuilder<\/code>. This is a <code>Microsoft.OData.Core<\/code> interface that provides an abstraction for service containers, without tying itself to any specific dependency injection library. If you don\u2019t already have an existing implementation in your code, you can copy or refer to the <a href=\"https:\/\/github.com\/OData\/AspNetCoreOData\/blob\/69eec03c7003fe12d92cdc619efdc16781683694\/src\/Microsoft.AspNetCore.OData\/Abstracts\/DefaultContainerBuilder.cs\"><code>DefaultContainerBuilder<\/code><\/a> implementation in <code>Microsoft.AspNetCore.OData<\/code>which is based on Microsoft.Extensions.DependencyInjection library. To learn more about dependency injection in the OData core library, <a href=\"https:\/\/docs.microsoft.com\/en-us\/odata\/odatalib\/di-support\">visit this page<\/a>.<\/p>\n<p>Then use the following steps to configure the container builder.<\/p>\n<p>You will need the following using statements:<\/p>\n<pre class=\"prettyprint\">using Microsoft.OData;\r\nusing Microsoft.OData.Json;\r\n<\/pre>\n<pre class=\"prettyprint\">var containerBuilder = new DefaultContainerBuild();\r\ncontainerBuilder.AddDefaultODataServices(); \r\ncontainerBuilder.Services.Add&lt;IStreamBasedJsonWriterFactory&gt;(DefaultStreamBasedJsonWrtiterFactory.Default);\r\n<\/pre>\n<p>Then create the service provider:<\/p>\n<pre class=\"prettyprint\">IServiceProvider services = containerBuilder.BuildContainer();<\/pre>\n<p>And here\u2019s how to set it on the response message when using <code>ODataMessageWriter<\/code>:<\/p>\n<pre class=\"prettyprint\">MyResponseMessage message = new MyResponseMessage(\u2026) { Container = services };\r\nODataMessageWriterSettings settings = \u2026;\r\nIEdmModel model = \u2026;\r\n\r\nODataMessageWriter writer = new ODataMessageWriter(message, settings, model);\r\nawait  writer.WriteStartAsync(new ODataResourceSet());\r\n<\/pre>\n<h2>Performance<\/h2>\n<p>Based on <a href=\"https:\/\/github.com\/OData\/odata.net\/tree\/master\/test\/PerformanceTests\/SerializationComparisonsTests\">some of our benchmarks<\/a>, <code>ODataUtf8JsonWriter<\/code> is about 38% faster than the default <code>JsonWriter<\/code> and uses less memory. It can increase the speed of the overall <code>ODataMessageWriter<\/code> by about 5-7% and reduces its memory usage by 2.5% when using the synchronous API. When using the asynchronous API, it makes <code>ODataMessageWriter<\/code> about 47% faster and reduces memory usage by almost 33%.<\/p>\n<h2>Limitations and other considerations<\/h2>\n<h2>Support for streaming writes<\/h2>\n<p>The default <code>JsonWriter<\/code> implements the <code>IJsonStreamWriter<\/code> interface. This interface allows you to pipe data directly from an input stream to the output destination. This may be useful when writing the contents of large binary or text file into the JSON output without reading everything in memory first. The new <code>ODataUtf8JsonWriter<\/code> does not yet implement this interface, which means input from a stream will be buffered entirely in memory before being written out to the destination.<\/p>\n<h2>Supported .NET versions<\/h2>\n<p>This feature is only available with <code>Microsoft.OData.Core<\/code> on .NET Core 3.1 and above (i.e. .NET 6 etc.). It is not supported on <code>Microsoft.OData.Core<\/code> versions running on .NET Framework.<\/p>\n<h2>Choosing a JavaScriptEncoder<\/h2>\n<p>The <code>DefaultStreamBasedJsonWriterFactory.Default<\/code> uses the default <code>JavaScriptEncoder<\/code> used by <code>Utf8JsonWriter<\/code> to encode and escape output text. The default encoder escapes control characters, non-ASCII characters and HTML-sensitive characters like \u2018&lt;\u2019. This differs from OData\u2019s default behavior which does not escape HTML-sensitive characters.<\/p>\n<p>You can provide a different <code>JavaScriptEncoder<\/code>. Instead of injecting <code>DefaultStreamBasedJsonWriterFactory.Default<\/code>, you can create a new instance of the factory and pass the encoder to the constructor. The following snippet uses the <code>JavaScriptEncoder.UnsafeRelaxedJsonEscaping<\/code> which does not escape HTML-sensitive characters. This is the default encoder used by ASP.NET Core.<\/p>\n<pre class=\"prettyprint\">IStreamBasedJsonWriterFactory factory = new DefaultStreamBasedJsonWriterFactory(JavaScriptEncoder.UnsafeRelaxedJsonEscaping);<\/pre>\n<p>Note that there\u2019s no built-in <code>JavaScriptEncoder<\/code> that exactly matches the escaping implementation of OData\u2019s default <code>JsonWriter<\/code>. The raw output of the two writers will be different. But if the client is compliant, this should not be a problem. Nevertheless, this is something you should take into consideration before adopting the new writer. In general, you should not be highly dependent on a whether certain characters are escaped or not because the internal implementation may change from one version of .NET to another (See relevant discussions <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/70419\">here<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/70419\">here<\/a>).<\/p>\n<h2>Conclusion<\/h2>\n<p>We have introduced the new <code>ODataUtf8JsonWriter<\/code> that ships with <code>Microsoft.OData.Core<\/code> 7.12.2 and showed how to configure OData serializers and writers to use it. This is part of a larger effort to make the core libraries more performant and efficient. We invite you to try out the new writer and share feedback with us. Let us know what you think, whether you observe any performance improvements (or regressions) and what other improvements we can make.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Microsoft.OData.Core version 7.12.2 has introduced a new JSON writer that\u2019s based on .NET\u2019s Utf8JsonWriter. The current JSON writer used in Microsoft.OData.Core, internally called JsonWriter, is a custom implementation that uses TextWriter to write the JSON output to the destination stream. We decided to write a new implementation to take advantage of the performance optimizations in [&hellip;]<\/p>\n","protected":false},"author":20326,"featured_media":3253,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4992","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-odata"],"acf":[],"blog_post_summary":"<p>Microsoft.OData.Core version 7.12.2 has introduced a new JSON writer that\u2019s based on .NET\u2019s Utf8JsonWriter. The current JSON writer used in Microsoft.OData.Core, internally called JsonWriter, is a custom implementation that uses TextWriter to write the JSON output to the destination stream. We decided to write a new implementation to take advantage of the performance optimizations in [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4992","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/users\/20326"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/comments?post=4992"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4992\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media\/3253"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media?parent=4992"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=4992"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=4992"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}