Microsoft.OData.Core
version 7.12.2 has introduced a new JSON writer that’s based on .NET’s 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 Utf8JsonWriter
.
In order not to disrupt existing customers, people will have to explicitly “opt-in” to using the new writer. Currently, ODataMessageWriter
allows you to override the default IJsonWriter
used for writing JSON output by providing a custom IJsonWriterFactory
to the dependency-injection container that’s attached to the message instance (IODataResponseMessage
or IODataRequestMessage
). However, the existing IJsonWriterFactory
is tightly coupled to TextWriter
. We introduced a new factory interface that allows writing directly to the output Stream
instead of TextWriter
. We call it IStreamBasedJsonWriterFactory
. The default implementation of this factory is called DefaultStreamBasedJsonWriter.
It creates an instance of the new JSON Writer, internally called ODataUtf8JsonWriter
. To opt-in to using the new writer, you need to add this factory as a service to the dependency-injection container. We’ll demonstrate how to do that in OData Web API 7.x and OData Web API 8.x libraries and when using ODataMessageWriter
directly.
OData Web API 8.x
You can use the new writer starting with Microsoft.AspNetCore.OData
8.0.11. You can configure custom services when registering new OData route components using the AddRouteComponens
method. Here’s an example:
Add the following using statement to the Startup.cs
file, or the file where you configure application services:
using Microsoft.OData.Json;
Then register the new factory inside the Startup.ConfigureServices
method or the method where you’re configuring application services if not Startup.ConfigureServices
:
services.AddControllers() .AddOData(options => options.AddRouteCompontents(“odata”, model, services => { Services.AddSingleton<IStreamBasedJsonWriterFactory>(_ => DefaultStreamBasedJsonWriterFactory.Default); });
OData Web API 7.x
This feature is available in Microsoft.AspNetCore.OData
7.5.17. There are different methods to configure custom OData services depending on whether you’re using endpoint routing or classic MVC routing.
Endpoint routing
If you’re using Endpoint routing, then you configure the OData route using endpoints.MapODataRoute
in the action passed to app.UseEndpoints
in Startup.Configure
. There’s an overload of MapODataRoute
that allows you to configure custom OData services. We’ll use this to inject the new factory class into the internal service container. We’ll also need to manually register the default routing conventions since this overload of MapODataRoute
does not handle that automatically, otherwise routing will not work properly.
Example:
Ensure you have the following using statements to the Startup.cs
file
using System.Collections.Generic; using Microsoft.AspNet.OData.Routing.Conventions; using Microsoft.OData; using Microsoft.OData.Json;
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { IEdmModel model = GetEdmModel(); app.UseODataBatching(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapODataRoute( "odata", “odata”, container => { container.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => model); container.AddService<IStreamBasedJsonWriterFactory>(Microsoft.OData.ServiceLifetime.Singleton, sp => DefaultStreamBasedJsonWriterFactory.Default); container.AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting("nullPrefix", endpoints.ServiceProvider)); }); }); }
MVC routing
If you’re using MVC routing, you can configure custom OData services inside the Startup.Configure
method by passing configuration action to the MapODataServiceRoute
method. Using this overload of MapODataServiceRoute
will also require you to manually register the default routing conventions, otherwise routing will not work properly.
Add the following using statements to the Startup.cs
file.
using System.Collections.Generic; using Microsoft.AspNet.OData.Routing.Conventions; using Microsoft.OData; using Microsoft.OData.Json;
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { IEdmModel model = GetEdmModel(); app.UseMvc(builder => { builder.Select().Expand().Filter().OrderBy().MaxTop(100).Count(); builder.MapODataServiceRoute("odata", "odata", container => { container.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => model) .AddService<IStreamBasedJsonWriterFactory>(Microsoft.OData.ServiceLifetime.Singleton, sp => DefaultStreamBasedJsonWriterFactory.Default) .AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", builder)); }); }); }
ODataMessageWriter
If you’re using ODataMessageWriter directly from Microsot.OData.Core
to write JSON payloads, it takes a few extra steps to configure.
To write a message, you need to provide an implementation of IODataResponseMessage
when writing a response and IODataRequestMessage
when writing a request body. Whatever I mention next about IODataResponseMessage
also applies to IODataRequestMessage
. To make your custom services accessible to the ODataMessageWriter
, your implementation of IODataResponseMessage
must also implement IContainerProvider
. This is a simple interface that exposes a Container
property of type IServiceProvider
. ODataMessageWriter
will use this property (if it has been defined on the message instance) to request services it needs, like an implementation of the IStreamBasedJsonWriterFactory
.
For example, an IODataResponseMessage
implementation could look like
class MyResponseMessage : IODataResponseMessage, IContainerProvider { //… other methods omitted for brevity IServiceProvider Container { get; init; }; }
You must also implement IContainerBuilder
. This is a Microsoft.OData.Core
interface that provides an abstraction for service containers, without tying itself to any specific dependency injection library. If you don’t already have an existing implementation in your code, you can copy or refer to the DefaultContainerBuilder
implementation in Microsoft.AspNetCore.OData
which is based on Microsoft.Extensions.DependencyInjection library. To learn more about dependency injection in the OData core library, visit this page.
Then use the following steps to configure the container builder.
You will need the following using statements:
using Microsoft.OData; using Microsoft.OData.Json;
var containerBuilder = new DefaultContainerBuild(); containerBuilder.AddDefaultODataServices(); containerBuilder.Services.Add<IStreamBasedJsonWriterFactory>(DefaultStreamBasedJsonWrtiterFactory.Default);
Then create the service provider:
IServiceProvider services = containerBuilder.BuildContainer();
And here’s how to set it on the response message when using ODataMessageWriter
:
MyResponseMessage message = new MyResponseMessage(…) { Container = services }; ODataMessageWriterSettings settings = …; IEdmModel model = …; ODataMessageWriter writer = new ODataMessageWriter(message, settings, model); await writer.WriteStartAsync(new ODataResourceSet());
Performance
Based on some of our benchmarks, ODataUtf8JsonWriter
is about 38% faster than the default JsonWriter
and uses less memory. It can increase the speed of the overall ODataMessageWriter
by about 5-7% and reduces its memory usage by 2.5% when using the synchronous API. When using the asynchronous API, it makes ODataMessageWriter
about 47% faster and reduces memory usage by almost 33%.
Limitations and other considerations
Support for streaming writes
The default JsonWriter
implements the IJsonStreamWriter
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 ODataUtf8JsonWriter
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.
Supported .NET versions
This feature is only available with Microsoft.OData.Core
on .NET Core 3.1 and above (i.e. .NET 6 etc.). It is not supported on Microsoft.OData.Core
versions running on .NET Framework.
Choosing a JavaScriptEncoder
The DefaultStreamBasedJsonWriterFactory.Default
uses the default JavaScriptEncoder
used by Utf8JsonWriter
to encode and escape output text. The default encoder escapes control characters, non-ASCII characters and HTML-sensitive characters like ‘<’. This differs from OData’s default behavior which does not escape HTML-sensitive characters.
You can provide a different JavaScriptEncoder
. Instead of injecting DefaultStreamBasedJsonWriterFactory.Default
, you can create a new instance of the factory and pass the encoder to the constructor. The following snippet uses the JavaScriptEncoder.UnsafeRelaxedJsonEscaping
which does not escape HTML-sensitive characters. This is the default encoder used by ASP.NET Core.
IStreamBasedJsonWriterFactory factory = new DefaultStreamBasedJsonWriterFactory(JavaScriptEncoder.UnsafeRelaxedJsonEscaping);
Note that there’s no built-in JavaScriptEncoder
that exactly matches the escaping implementation of OData’s default JsonWriter
. 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 here and here).
Conclusion
We have introduced the new ODataUtf8JsonWriter
that ships with Microsoft.OData.Core
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.
0 comments