Skip to content

Commit

Permalink
OData perf guidelines (#303)
Browse files Browse the repository at this point in the history
  • Loading branch information
habbes authored Mar 14, 2024
1 parent c5dbd26 commit 712d53d
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 6 deletions.
9 changes: 9 additions & 0 deletions .openpublishing.redirection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"redirections": [
{
"source_path": "Odata-docs/odatalib//v7/using-ut8jsonwriter-for-better-performance.md",
"redirect_url": "/odata/odatalib/using-utf8jsonwriter-for-better-performance",
"redirect_document_id": true
}
]
}
12 changes: 12 additions & 0 deletions Odata-docs/TOC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@
href: /odata/webapi-8/tutorials/basic-crud-in-fsharp
- name: Implementing a custom skip token handler
href: /odata/webapi-8/tutorials/custom-skiptokenhandler
- name: Using Utf8JsonWriter to improve serialization performance
href: /odata/webapi-8/tutorials/using-utf8jsonwriter-to-improve-serialization-performance
- name: ASP.NET WebAPI
items:
- name: Getting started
Expand Down Expand Up @@ -398,6 +400,16 @@
href: /odata/webapiauth/getting-started
- name: How permissions are applied
href: /odata/webapiauth/how-permissions-are-applied
- name: Performance tips and guidelines
items:
- name: Overview
href: /odata/performance/overview
- name: Marking the Edm model as immutable
href: /odata/performance/mark-edm-model-immutable
- name: Using Utf8JsonWriter
href: /odata/performance/using-utf8jsonwriter
- name: Disable writer validations
href: /odata/performance/disable-writer-validations
- name: OData Connected Service
items:
- name: Getting Started
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ ms.topic: article

The `ODataMessageWriter` internally uses an implementation of [`IJsonWriter`](/dotnet/api/microsoft.odata.json.ijsonwriter) (and [`IJsonWriterAsync`](/dotnet/api/microsoft.odata.json.ijsonwriterasync)) to write JSON output to the destination stream. This is a low-level JSON writer that exposes methods for writing individual properties and values. Before v7.12.2, `Microsoft.OData.Core` shipped a single default implementation (internally called `JsonWriter`) that can be constructed using the [`DefaultJsonWriterFactory`](/dotnet/api/microsoft.odata.json.defaultjsonwriterfactory).

It's possible to replace the writer with a custom implementation by injecting your own implementation of [`IJsonWriterFactory`](/dotnet/api/microsoft.odata.json.ijsonwriterfactory) into the dependency-injection container. For more information about configuring dependency injection in ODate core library, [see this article](/odata/odatalib/di-support).
It's possible to replace the writer with a custom implementation by injecting your own implementation of [`IJsonWriterFactory`](/dotnet/api/microsoft.odata.json.ijsonwriterfactory) into the dependency-injection container. For more information about configuring dependency injection in OData core library, [see this article](/odata/odatalib/di-support).

Microsoft.OData.Core 7.12.2 introduced a new implementation of `IJsonWriter` and `IJsonWriterAsync` called `ODataUtf8JsonWriter`. This is based on .NET's built-in [Utf8JsonWriter](/dotnet/api/system.text.json.utf8jsonwriter). This writer is faster and uses less memory than the default `JsonWriter`.
Microsoft.OData.Core 7.12.2 introduced a [`Utf8JsonWriter`](/dotnet/api/system.text.json.utf8jsonwriter) wrapper that implements `IJsonWriter` and `IJsonWriterAsync`. This writer is faster and uses less memory than the default `JsonWriter`.

Since the existing `IJsonWriterFactory` is tightly coupled to `TextWriter`, we introduced a new interface `IStreamBasedJsonWriterFactory` which accepts the destination `Stream` as input instead of `TextWriter`. The default implementation of this interface is `DefaultStreamBasedJsonWriterFactory`, which creates instances of `ODataUtf8JsonWriter`.
Since the existing `IJsonWriterFactory` is tightly coupled to `TextWriter`, we introduced a new interface `IStreamBasedJsonWriterFactory` which accepts the destination `Stream` as input instead of `TextWriter`. The default implementation of this interface is `DefaultStreamBasedJsonWriterFactory`, which creates instances of the `Utf8JsonWriter`-based writer.

Therefore, in order to use `ODataUtf8JsonWriter`, you have to inject an instance of `DefaultStreamBasedJsonWriterFactory` to the dependency-injection container.
Therefore, in order to use `Utf8JsonWriter`, you have to inject an instance of `DefaultStreamBasedJsonWriterFactory` to the dependency-injection container.

Here's an example:

Expand All @@ -39,11 +39,11 @@ builder.AddService<IStreamBasedJsonWriterFactory>(sp => DefaultStreamBasedJsonWr

### No support for streaming writes

The default `JsonWriter` implements the `IJsonStreamWriter` and `IJsonStreamWriterAsync` interfaces. 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 file into base-64 output without buffering 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. If your use case requires writing from a stream input, consider using the default `JsonWriter`.
The default `JsonWriter` implements the `IJsonStreamWriter` and `IJsonStreamWriterAsync` interfaces. These interfaces allow you to pipe data directly from an input stream to the output destination. This may be useful when writing the contents of large binary file into base-64 output without buffering everything in memory first. The `Utf8JsonWriter`-based writer does not implement these interfaces yet, which means input from a stream will be buffered entirely in memory before being written out to the destination. If your use case requires writing from a stream input, consider using the default `JsonWriter`. Follow [this issue](https://github.com/OData/odata.net/issues/2422) to find out when this will be resolved.

### 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.
This feature is only available with Microsoft.OData.Core on .NET Core 3.1 and .NET 5 and above. It is not supported on Microsoft.OData.Core versions running on .NET Framework.

### Choosing a JavaScriptEncoder

Expand Down
137 changes: 137 additions & 0 deletions Odata-docs/performance/disable-writer-validations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
title: "Disable writer validations"
description: A guide on how to improve performance by disabling writer validations.
date: 2024-01-23
ms.date: 1/23/2023
author: habbes
ms.author: clhabins
---

# Disable writer validations

**Applies To**:[!INCLUDE[appliesto-webapi](../includes/appliesto-webapi-v8.md)][!INCLUDE[appliesto-webapi](../includes/appliesto-webapi-v7.md)][!INCLUDE[appliesto-odatalib](../includes/appliesto-odatalib-v7.md)]

During serialization, the OData writer performs various validations to ensure that the payload is valid (e.g. no duplicate properties) and conforms to the schema (e.g. property type matches the entity type's definition). These validations introduce some overhead to the serialization process. If you know ahead of time that the payload you're writing is correct, you can disable one or more validations to improve performance.

Use the [`ODataMessageWriterSettings.Validations`](/dotnet/api/microsoft.odata.odatamessagewritersettings.validations) property to configure which validations you want to enable or disable.

You can disable all validations using [`ValidationKinds.None`](/dotnet/api/microsoft.odata.validationkinds):

```csharp
messageWriterSettings.Validations = ValidationKinds.None;
```

You can selectively enable validations using the `&` operator:

```csharp
messageWriterSettings.Validations = ValidationKinds.ThrowOnDuplicatePropertyNames & ValidationKinds.ThrowIfTypeConflictsWithMetadata
```

You can enable all validations using [`ValidationKinds.All`](/dotnet/api/microsoft.odata.validationkinds):

```csharp
messageWriterSettings.Validations = ValidationKinds.All;
```

To learn more about different validation options, consult the [`ValidationKinds` enum documentation](/dotnet/api/microsoft.odata.validationkinds).

## Disabling writer validations when using `Microsoft.OData.Core` library directly

When using the core library directly, you pass the [`ODataMessageWriterSettings`](/dotnet/api/microsoft.odata.odatamessagewritersettings) to the [`ODataMessageWriter`](/dotnet/api/microsoft.odata.odatamessagewriter) constructor.

```csharp
ODataMessageWriterSettings settings = new ODataMessageWriterSettings
{
ODataUri = new ODataUri() { /* ... */ },
Validations = ValidationKinds.None
};

ODataMessageWriter writer = new ODataMessageWriter(odataMessage, settings, edmModel);

// ...
```

## Disabling writer validations when using `Microsoft.AspNetCore.OData` 8 library

In `Microsoft.AspNetCore.OData` 8, you can pass a service configuration action as the third argument of `AddRouteComponents`. Use
this to inject a request-scoped instance of `ODataMessageWriterSettings` - with the desired options - to the service container:

```csharp
services
.AddControllers()
.AddOData(options =>
options.AddRouteComponents(
routePrefix: "routePrefix",
model: edmModel,
configurateServices: container =>
{
container.AddScoped<ODataMessageWriterSettings>(_ => new ODataMessageWriterSettings
{
Validations = ValidationKinds.None
})
}));
```

## Disabling writer validations when using `Microsoft.AspNetCore.OData` 7 library

In `Microsoft.AspNetCore.OData` 7, you can pass a service configuration action to the `MapODataRoute` and `MapODataServiceRoute` methods.
Use this to inject a request-scoped instance of `ODataMessageWriterSettings` to the service container.

### Using endpoint routing

```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

// ...
app.UseEndpoints(endpoints =>
{
endpoints.MapODataRoute(
routeName: "odata",
routePrefix: “odata”,
configureAction: container =>
{
container.AddService<ODataMessageWriterSettings>(Microsoft.OData.ServiceLifetime.Scoped, _ => new ODataMessageWriterSettings
{
Validations = ValidationKinds.None
});

container.AddService(Microsoft.OData.ServiceLifetime.Singleton, _ => model);
container.AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton,
sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", sp));
});
});

// ...
}
```

### Using MVC routing

```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
IEdmModel model = GetEdmModel();

app.UseMvc(builder =>
{
builder.Select().Expand().Filter().OrderBy().MaxTop(100).Count();

builder.MapODataServiceRoute(
routeName: "odata",
routePrefix: "odata",
configureAction: container =>
{
container.AddService<ODataMessageWriterSettings>(Microsoft.OData.ServiceLifetime.Scope, _ => new ODataMessageWriterSettings
{
Validations = ValidationKinds.None
});

container.AddService(Microsoft.OData.ServiceLifetime.Singleton, _ => model)
.AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton, _ =>
ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", builder));
});
});
}
```
52 changes: 52 additions & 0 deletions Odata-docs/performance/mark-edm-model-immutable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: "Mark the EDM model as immutable"
description: A guide on how to improve performance by marking the EDM model as immutable.
date: 2023-08-11
ms.date: 8/11/2023
author: habbes
ms.author: clhabins
---

# Mark the EDM model as immutable

**Applies To**:[!INCLUDE[appliesto-webapi](../includes/appliesto-webapi-v8.md)][!INCLUDE[appliesto-webapi](../includes/appliesto-webapi-v7.md)][!INCLUDE[appliesto-odatalib](../includes/appliesto-odatalib-v7.md)]

OData libraries at times perform multiple lookups in the EDM model when processing a request. Some of these lookups may be expensive, especially for large models with a lot of schema elements.
We have some optimizations in place to reduce the performance overhead of these internal lookups, but some of these optimizations are only applicable if the library knows that the EDM model will not change. You can enable these optimizations by explicitly marking your EDM as immutable by calling the `MarkAsImmutable` method, which is an extension method in the `Microsoft.OData.Edm` namespace.

```csharp
using Microsoft.OData.Edm;

IEdmModel GetEdmModel()
{
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Person>("People");
IEdmModel model = modelBuilder.GetEdmModel();
model.MarkAsImmutable();
return model;
}
```

It is recommended to mark the EDM model as immutable since most applications do not modify the model after it has been constructed.

It is however important to note that the library makes no effort to ensure or verify that the model doesn't change after calling this method. It is the responsibility of the developer to ensure that the model is not modified after the `MarkAsImmutable` method is called. Modifying the model after `MarkAsImmutable` method has been called may lead to unexpected behavior.

The following code snippet shows an example of incorrect usage of this method:

```csharp
using Microsoft.OData.Edm;

IEdmModel GetEdmModel()
{
var model = new EdmModel();
var personType = new EdmEntityType("Sample.NS", "Person");
personType.AddKeys(personType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false));
model.MarkAsImmutable();

var addressType = new EdmComplexType("Sample.NS", "Address");
// DO NOT DO THIS! Modifying the model after calling MarkAsImmutable
// can lead to unexpected behavior.
model.AddElement(addressType);
return model;
}
```
14 changes: 14 additions & 0 deletions Odata-docs/performance/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: "Performance Guidelines"
description: Overview of performance improvements tips and guidelines when using OData libraries.
date: 2024-01-23
ms.date: 1/23/2023
author: habbes
ms.author: clhabins
---

# Performance guidelines

**Applies To**:[!INCLUDE[appliesto-webapi](../includes/appliesto-webapi-v8.md)][!INCLUDE[appliesto-webapi](../includes/appliesto-webapi-v7.md)][!INCLUDE[appliesto-odatalib](../includes/appliesto-odatalib-v7.md)]

This section contains a series of articles that provide tips and guidelines for improving performance when working with OData libraries. If your OData service is experiencing performance issues, consider implementing the guidelines in this section and measure performance using appropriate benchmarks and metrics.
22 changes: 22 additions & 0 deletions Odata-docs/performance/using-utf8jsonwriter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: "Use Utf8JsonWriter"
description: A guide on how to improve performance using Utf8JsonWriter.
date: 2024-01-23
ms.date: 1/23/2024
author: habbes
ms.author: clhabins
---

# Use `Utf8JsonWriter`

**Applies To**:[!INCLUDE[appliesto-webapi](../includes/appliesto-webapi-v8.md)][!INCLUDE[appliesto-webapi](../includes/appliesto-webapi-v7.md)][!INCLUDE[appliesto-odatalib](../includes/appliesto-odatalib-v7.md)]

The OData core library implements a wrapper on top of the `Utf8JsonWriter` to complement the default JSON writer. This writer offers better performance and fewer memory allocations compared to using the JSON writer that's used by default in `Microsoft.OData.Core`.

To enable `Utf8JsonWriter`, check out any of the following guides depending on the library you are using in your application:

- [Enable `Ut8fJsonWriter` when using `Microsoft.OData.Core` library directly](/odata/odatalib/using-utf8jsonwriter-for-better-performance)
- [Enable `Utf8JsonWriter` when using `Microsoft.AspNetCore.OData` 8.x library](/odata/webapi-8/tutorials/using-utf8jsonwriter-to-improve-serialization-performance)
- [Enable `Ut8fJsonWriter` when using `Microsoft.AspNetCore.OData` 7.x library](/odata/webapi/using-utf8jsonwriter-to-improve-serialization-performance)

When using `Utf8JsonWriter`, consider using the [`UnsafeRelaxedJsonEscaping`](/dotnet/api/system.text.encodings.web.javascriptencoder.unsaferelaxedjsonescaping) encoder to perform escaping as described [here](/odata/odatalib/using-utf8jsonwriter-for-better-performance#choosing-a-javascriptencoder). This encoder is more relaxed and escapes fewer characters. This can improve performance over the default encoder which escapes more characters. Frequent escaping of large strings may result in additional performance and memory overhead.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: "Using Utf8JsonWriter to improve serialization performance"
description: Learn how to configure ASP.NET Core OData 8 to use Utf8JsonWriter to improve serialization performance.
date: 2023-08-11
ms.date: 08/11/2023
author: habbes
ms.author: clhabins
---

# Using Utf8JsonWriter to improve serialization performance

**Applies To**:[!INCLUDE[appliesto-webapi](../../includes/appliesto-webapi-v8.md)]

`Microsoft.OData.Core` version 7.12.2 introduced a new JSON writer that’s based on .NET’s [`Utf8JsonWriter`](/dotnet/api/system.text.json.utf8jsonwriter). This writer offers better performance than the JSON writer that is used by default in `Microsoft.OData.Core`. Reference `Microsoft.AspNetCore.OData 8.0.11` or higher to use the new writer. To learn more, [visit this page](../../odatalib/v7/using-utf8jsonwriter-for-better-performance.md). In this article, we are going to show you how to configure your ASP.NET Core OData application to use the `Utf8JsonWriter`.

You register OData services through use of `AddRouteComponents` method. To use the `Utf8JsonWriter`, register the `IStreamBasedJsonWriterFactory` service. The `Microsoft.OData.Core` library provides a default implementation of this interface - `DefaultStreamBasedJsonWriterFactory`.

Here's how you can do this in your application. In your application startup code, import the `Microsoft.OData.Json` namespace and register the `IStreamBasedJsonWriterFactory` service as follows:

```c#
services.AddControllers()
.AddOData(options =>
options.AddRouteCompontents(
routePrefix: "odata",
model: model,
configureServices: services =>
{
services.AddSingleton<IStreamBasedJsonWriterFactory>(_ => DefaultStreamBasedJsonWriterFactory.Default);
});
```

That is all that's required to substitute the default JSON writer with the `Utf8JsonWriter`.

0 comments on commit 712d53d

Please sign in to comment.