OData performance tips

When connecting to an OData endpoint, there are a number of things to keep in mind in order to get good performance. We'll discuss these issues a bit in this article.

Background

At first sight, OData may appear to be just like fetching content from a database, since you can use LINQ to write queries, and you get back objects, just like when using an ORM. But since all requests to an OData endpoint has to go to another server that might be far away, you need to be a bit more careful in how you write your code.

Note that this doesn't mean that OData itself is slow, it's just some physical limitation that come into play when fetching content from a remote server.

Latency kills performance

In a normal website, you fetch content from a database, running either on the same server, the same rack or at worst, in the same data center. This usually gives you a latency on each request of in the low single digits milliseconds, or even slower for good setups.

When fetching data over HTTP to another server that can be in a different part of a country or even in another continent, latency plays a MUCH bigger role. In this interesting article, we can see that the average round trip time to Google is 100 ms, and in the US 50-60 ms.  

So how is this a problem? If you only make a single OData request in a page, this latency is never a problem. But if you make a number of consecutive calls, where each call gives you a latency of up to 100 ms, it quickly adds up, and the site will appear slow. You also have to add the response time of the OData endpoint to get the round trip time. If you're not careful you quickly get response times of multiple seconds, which is not good enough for real-time integrations.

Advice to limit the effect of latency:

The best way to limit the effect of increased latency is to limit the number of consecutive OData requests you make in each page view on your site. For example, you should try to avoid having a loop where you make an OData request for each iteration in the loop.

OData response size

Another factor to keep in mind when integrating with an OData endpoint is that response sizes can get very big if you're not careful with how you write your queries. The xml format used by default in OData is not the most effective data format, but the thing that affects the response size the most are what data you are asking for. 

If you only ask for collections of full objects, you can easily get responses that are several megabytes in size. Even with fast internet connections, it takes time to transfer a lot data. It also takes more time both for the OData endpoint to build the response, and for you own server to make use of the response. This means it can have a huge impact on performance.  

Advice to limit response size

So to improve performance, you need to try to keep response sizes as low as possible. This can be done in two main ways:

Only request exactly the entities you need.

Do not filter the response on your end if you can avoid it, and make sure only ask the OData endpoint for the entities you need. While it can be easier to do filtering locally, it can have a huge impact on performance if you transfer more data than you need.

Reduce the amount of data per entity.

You recude the amount of data using projection. Projection can be defined as: "Projection refers to the operation of transforming an object into a new form that often consists only of those properties that will be subsequently used".

Projection has an added benefit of making your client application less dependent on schema changes on the OData endpoint. If you're not using projection, and the classes you're using are changed, you need to update the service reference in Visual Studio in order to create a new proxy class. If you're not returning the full objects, you're not using the proxy classes, so there is no conflict (unless properties or classes you're referring to are removed). 

To give you an example of how projection can help, I'll make a couple of examples from the OData endpoint in the Shopdemo site. The site contains around 2000 shoes. Since a lot of them are variants of each other, there are 104 different main variants.

To get all the main variants, you can write this query:

from s in ShoeProducts where s.Subvariant == false select s

The above query fetches the full object (without the relation properties which you have to specifically expand if you want to include them) the main variants.

The first "cold" request takes 14 seconds. When the data has been cached, the query takes 0.84 sek

The raw OData url for the query: 

http://shopdemo.azurewebsites.net/odata/ShoeProducts()?$filter=Subvariant eq false


Let's change the query to only select the Name and Id properties using projection:

from s in ShoeProducts where s.Subvariant == false select new{ Name = s.Name, Id = s.Id }

The altered query now takes 1.2 sek when not cached, and only 0.18 sek when cached. The speedup is more than 10x when the data isn't cached and almost 5x faster when cached.

If we look at the size of the response, the query asking for the whole entities is over 800k. The query asking for the name and id returns a response of 65k. 

The raw OData url for the query: 

http://shopdemo.azurewebsites.net/odata/ShoeProducts()?$filter=Subvariant%20eq%20false&$select=Id,Name


Another important issue is relation properties. Very often you have to expand one or several of the relation properties in a query. This means it includes the full related object(s) for the expanded properties. This can be very heavy, especially if you have a several relation properties. Rather than doing expand, we suggest fetching the properties on the related object(s) directly.

Let's compare these two approaches. First with expand:

from s in ShoeProducts.Expand("Variants") where s.Subvariant == false select s

This query takes 25 seconds when the data isn't cached, and 16-17 seconds when cached. This seems very slow, but this actually involves returning 2040 products, each with many properties. 

The response is almost 16MB in size. Just transfering that amount of data usually takes quite a few seconds. That's also one of the main reasons why the cached version is so slow.

Returning such a big response is not usable in a real-time integration. So let's try to reduce the amount of data we fetch using projection:

from s in ShoeProducts where s.Subvariant == false select new {Name = s.Name, Id=s.Id, SubVariants =  s.Variants.Select(x=>x.Name)}

This takes 4.9 seconds in the first request (not cached), and 0.9 - 1.6 seconds when cached. So there is a massive improvement by limiting what's returned.

Raw OData url: http://shopdemo.azurewebsites.net/odata/ShoeProducts()?$filter=Subvariant eq false&$expand=Variants&$select=Name,Id,Variants/Name

This altered query's response size is: 1.43MB, while a huge improvement, is still way too big for real-time integration. But this isn't a very realistic test, as it returns all the products, and the names of all the sub variants. Rarely do you need to display hundreds of products on one page.

To make it a bit more realistic, let's limit it to the 20 first products, and their variants:

(from s in ShoeProducts where s.Subvariant == false select new {Name = s.Name, Id=s.Id, SubVariants =  s.Variants.Select(x=>x.Name)}).Take(20)

By limiting the number of products we ask for, we get much better performance. The first request takes 1.33 seconds. When the data has been cached in the Relatude OData endpoint, it takes only 0.29 seconds, within what we can accept for real-time integrations.

The response size is also much smaller at 267kb. 

Raw OData url: http://shopdemo.azurewebsites.net/odata/ShoeProducts()?$filter=Subvariant%20eq%20false&$top=20&$expand=Variants&$select=Name,Id,Variants/Name

Summary

We've looked at some of the common pitfalls when integrating with OData.

There are two main areas to focus on, the first that you need to minimize the number of sequential requests made to the endpoint, due to the combined effects of latency.

The other main area is to limit the amount of data that's returned from your queries. By using projection and paging, you can drastically reduce the amount of data retrieved from the OData endpoint.