Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perf guidelines #303

Merged
merged 28 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -101,6 +101,8 @@
href: /odata/webapi-8/tutorials/basic-crud
- 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 @@ -390,6 +392,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 @@ -16,11 +16,11 @@ The `ODataMessageWriter` internally uses an implementation of [`IJsonWriter`](/d

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).

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 new [`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,7 +39,7 @@ 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. 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 `Utf8JsonWriter`-based writer 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`. Follow [this issue](https://github.com/OData/odata.net/issues/2422) to find out when this will be resolved.

### Supported .NET versions

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line below:
"on .NET Core 3.1 and above (i.e. .NET 6 etc.)" => "on .NET Core 3.1 and above"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case .NET Core 5 applies too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't mention .NET 5 because it was already out of support by then. I wasn't sure if someone who's not conversant with the .NET evolution would know that .NET 6+ is in the same line of success as .NET Core 3.1 given that name change from .NET Core to .NET. Maybe I could say .NET Core 3.1 and .NET 5+

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) construtor.

```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
Copy link
Contributor

@ElizabethOkerio ElizabethOkerio Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this Microsoft.AspNetCore 7 only or even Microsoft.AspNet.OData

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. ODataUtf8JsonWriter is not supported in Microsoft.AspNet.OData because it depends on Utf8JsonWriter which is not supported in .NET Framework.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@habbes I don't think this particular article is related to ODataUtf8JsonWriter, is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. You are correct. I had confused this with a different conversation. Let me revert back with a response.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do this in AspNet.OData 7 as well using the MapODataServiceRoute method like the MVC routing approach below. The only difference is that the AspNet 7.x configuration setup looks different from the AspNetCore one (there's no Startup class with a Configure method, there's a WebApiConfig class with a Register method that takes a HttpConfiguration argument.

But the OData part is identical. I think readers would still be able to easily adapt this sample to their AspNet.OData code.


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 optimization 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 character. 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`.