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

Updated docs with more information regarding IQuerable. #660

Merged
merged 1 commit into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 12 additions & 9 deletions docs/using-the-sdk/basics-getdata.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ using (var context = await pnpContextFactory.CreateAsync("SiteToWorkWith"))
// Data is loaded into the context
await context.Web.LoadAsync(p => p.Title, p => p.Lists);

// We're using AsRequested() to query the already loaded domain models, if not a new query would
// issued to load the lists
// We're using AsRequested() to query the already loaded domain models,
// otherwise a new query would be issued to load the lists
foreach (var list in context.Web.Lists.AsRequested())
{
// do something with the list
Expand All @@ -75,8 +75,8 @@ using (var context = await pnpContextFactory.CreateAsync("SiteToWorkWith"))
// Load the data into variable
var web = await context.Web.GetAsync(p => p.Title, p => p.Lists);

// We're using AsRequested() to query the already loaded domain models, if not a new query would
// issued to load the lists
// We're using AsRequested() to query the already loaded domain models,
// otherwise a new query would be issued to load the lists
foreach (var list in web.Lists.AsRequested())
{
// do something with the list
Expand All @@ -92,23 +92,26 @@ Previous chapter showed how to load data starting from a single model (e.g. load
using (var context = await pnpContextFactory.CreateAsync("SiteToWorkWith"))
{
// Option A: Load the Lists using a model load => no filtering option
var lists = await context.Web.GetAsync(p => p.Title, p => p.Lists);
var web = await context.Web.GetAsync(p => p.Title, p => p.Lists);
var lists = web.Lists.AsRequested();

// Option B: Load the Lists using a LINQ query ==> filtering is possible,
// only lists with title "Site Pages" are returned
var lists = await context.Web.Lists.Where(p => p.Title == "Site Pages").ToListAsync();

// Option C: we assume there's only one list with that title so we can use FirstOrDefaultAsync
var sitePagesList = await context.Web.Lists.Where(p => p.Title == "Site Pages").FirstOrDefaultAsync();
var sitePagesList = await context.Web.Lists.FirstOrDefaultAsync(p => p.Title == "Site Pages");
}
```

Like with loading the model in the previous chapter you've two ways of using the data: query the data that was loaded in the context or query the data loaded into a variable:

Below sample shows the various options for loading and using collections.
Below sample shows the various options for loading and using collections.

> [!Note]
> - When you want to enumerate or query (via LINQ) already loaded data you need to first use the `AsRequested()` method to return the domain model objects as an `IEnumerable`.
>
> - When you want to enumerate or query (via LINQ) already loaded data you need to first use the `AsRequested()` method to return the domain model objects as an `IList`.
> - `IQueryable` is an interface used by almost all collections (like `Lists`, `Fields`, etc.) in PnP Core SDK. It's very powerful, but should be used carefully to avoid some common performance pitfalls. Read [IQueryable performance considerations](basics-iqueryable.md) to learn more.
> - When using a filter via the `Where` LINQ statement then always use an operator: `Where(p => p.BoolProperty == true)` works, but `Where(p => p.BoolProperty)` is ignored.

```csharp
Expand All @@ -127,7 +130,7 @@ using (var context = await pnpContextFactory.CreateAsync("SiteToWorkWith"))
// are not loaded into the context
var lists = await context.Web.Lists.Where(p => p.Title == "Site Pages").ToListAsync();

foreach(var list in lists.AsRequested())
foreach(var list in lists)
{
// Use list
}
Expand Down
135 changes: 135 additions & 0 deletions docs/using-the-sdk/basics-iqueryable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# [`IQueryable`](https://docs.microsoft.com/en-us/dotnet/api/system.linq.iqueryable) performance considerations

In the [Requesting model collections](basics-getdata.md#requesting-model-collections) section you saw quite a lot of different examples of how to query collections. Almost all collections inside PnP Core SDK implement an [`IQueryable`](https://docs.microsoft.com/en-us/dotnet/api/system.linq.iqueryable) interface.

Thanks to this you can use LINQ expressions to dynamically filter or asynchronously load collection elements on demand. All your LINQ expressions will be accurately translated to the REST OData query operations (like `$filter`, `$select`, `$expand`, etc).

Having below code:

```csharp
var lists = await context.Web.Lists
.Where(l => l.Hidden == false && l.TemplateType == ListTemplateType.DocumentLibrary)
.QueryProperties(p => p.Title, p => p.TemplateType, p => p.ContentTypes.QueryProperties(p => p.Name)).ToListAsync();
```

upon execution will be translated to the below REST OData query:

```bash
_api/web/lists?$select=Id,Title,BaseTemplate,ContentTypes/Name,ContentTypes/StringId&$expand=ContentTypes&$filter=(BaseTemplate+eq+101)
```

It's a very powerful feature, however let's take a closer look at this technique to avoid some common performance issues.

> [!Important]
>
> The most important rule of `IQueryable` is that an `IQueryable` doesn't fire a request when it's declared, but only when it's enumerated over (inside foreach cycle or when calling `ToList()`/`ToListAsync()`).

## Loading collections into the PnPContext

Let's have a sample query to get a web's lists:

❌ *not efficient:*

```csharp
// All lists loaded into the context
await context.Web.LoadAsync(p => p.Lists);

foreach (var list in context.Web.Lists)
{
// do something with the list here
}
```

What's wrong with this code? It works just fine, however it sends two identical HTTP requests to the SharePoint server to get lists (one in `LoadAsync(p => p.Lists)` and the second one in the `foreach` cycle). Why does it happen? Because `Lists` property implements `IQueryable`, inside `foreach` cycle you effectively enumerate the `IQueryable`, as a result, it sends an HTTP request to get data.

How to fix the code? Use `AsRequested()`:

✅ *better:*

```csharp
// All lists loaded into the context
await context.Web.LoadAsync(p => p.Lists);

foreach (var list in context.Web.Lists.AsRequested())
{
// do something with the list here
}
```

As mentioned earlier, `AsRequested()` method returns an already loaded collection of items, you should use this method to avoid multiple unnecessary HTTP requests. In this case, we enumerate a collection loaded in memory before.

Alternatively, you can also use just one cycle without `LoadAsync(p => p.Lists)`:

✅ *better:*

```csharp
await foreach (var list in context.Web.Lists)
{
// do something with list here
}
```

In this case, list collection will be requested at the beginning of the `foreach` cycle. Do remember though, that if you iterate over collection again somewhere in your code path, an additional request will be sent.

## Load related properties

The below code has a similar problem with the query efficiency:

❌ *not efficient:*

```csharp
var list = await context.Web.Lists.GetByTitleAsync("Documents", l => l.Fields);
var fields = await list.Fields.Where(l => l.InternalName.StartsWith("tax")).ToListAsync();
```

The first line loads a list by title and also loads related property - all list fields. On the second line we again send HTTP request to further filter fields. But what we need instead is to filter already loaded fields:

```csharp
var fields = list.Fields.AsRequested().Where(l => l.InternalName.StartsWith("tax")).ToList();
```

To make it even more efficient, you should change it like this:

✅ *better:*

```csharp
var list = await context.Web.Lists.GetByTitleAsync("Documents");
var fields = await list.Fields.Where(l => l.InternalName.StartsWith("tax")).ToListAsync();
```

It doesn't make sense to load all related fields with the list request. Thus we simply send a separate request with a filter (will be translated to the `$filter=startswith` OData query) to get desired fields.

## Cycles and/or method calls

Could you guess what's the problem with the below code:

❌ *not efficient:*

```csharp
var filteredList = context.Web.Lists.Where(l => l.TemplateType == ListTemplateType.DocumentLibrary);

for (int i = 0; i < 10; i++)
{
DoSmth(filteredList);
}

private bool DoSmth(IEnumerable<IList> lists)
{
foreach (var list in lists)
{
// do smth with list
}
}
```

It also works just fine, however has an issue, that the above code sends 10 HTTP requests to get lists data. The `filteredList` is an instance of `IQueryable<IList>`, that's why it doesn't execute the request immediately, but only inside the `foreach` cycle in the `Check` function. Every time we visit the function, we send an HTTP request to get lists.

How to fix it? Change the filter query so that it executes immediately using `ToList()` or `ToListAsync()` methods:

✅ *better:*

```csharp
var filteredList = await context.Web.Lists.Where(l => l.TemplateType == ListTemplateType.DocumentLibrary).ToListAsync();
```

The code above executes the request instantly and loads all items into the memory, thus we don't have the issue with multiple HTTP queries. The type of `filteredList` will be `IList`, not `IQueryable`.
2 changes: 2 additions & 0 deletions docs/using-the-sdk/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
href: basics-getdata-paging.md
- name: Adding, updating and deleting data
href: basics-addupdatedelete.md
- name: IQueryable performance considerations
href: basics-iqueryable.md
- name: Advanced PnPContext use
href: basics-context.md
- name: Batching requests
Expand Down
4 changes: 4 additions & 0 deletions src/sdk/PnP.Core/Model/Security/Public/ISharePointGroup.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using PnP.Core.Services;
using System.Linq;
using System.Threading.Tasks;

namespace PnP.Core.Model.Security
Expand Down Expand Up @@ -61,6 +62,9 @@ public interface ISharePointGroup : IDataModel<ISharePointGroup>, IDataModelGet<

/// <summary>
/// Members of this group
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public ISharePointUserCollection Users { get; }

Expand Down
7 changes: 7 additions & 0 deletions src/sdk/PnP.Core/Model/SharePoint/Core/Public/IComment.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using PnP.Core.Model.Security;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace PnP.Core.Model.SharePoint
Expand Down Expand Up @@ -73,6 +74,9 @@ public interface IComment : IDataModel<IComment>, IDataModelGet<IComment>, IData

/// <summary>
/// List of users who have liked the comment.
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public ICommentLikeUserEntityCollection LikedBy { get; }

Expand All @@ -83,6 +87,9 @@ public interface IComment : IDataModel<IComment>, IDataModelGet<IComment>, IData

/// <summary>
/// List of users who are at mentioned in this comment.
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public ICommentLikeUserEntityCollection Mentions { get; }

Expand Down
10 changes: 9 additions & 1 deletion src/sdk/PnP.Core/Model/SharePoint/Core/Public/IContentType.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace PnP.Core.Model.SharePoint
using System.Linq;

namespace PnP.Core.Model.SharePoint
{
/// <summary>
/// Public interface to define a Content Type object of SharePoint Online
Expand Down Expand Up @@ -129,11 +131,17 @@ public interface IContentType : IDataModel<IContentType>, IDataModelGet<IContent

/// <summary>
/// Gets the collection of field links of the Content Type.
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IFieldLinkCollection FieldLinks { get; }

/// <summary>
/// Gets the collection of fields of the Content Type.
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IFieldCollection Fields { get; }

Expand Down
4 changes: 4 additions & 0 deletions src/sdk/PnP.Core/Model/SharePoint/Core/Public/IFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using PnP.Core.Services;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace PnP.Core.Model.SharePoint
Expand Down Expand Up @@ -181,6 +182,9 @@ public interface IFile : IDataModel<IFile>, IDataModelGet<IFile>, IDataModelLoad

/// <summary>
/// Gets a value that returns a collection of file version objects that represent the versions of the file.
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IFileVersionCollection Versions { get; }

Expand Down
4 changes: 4 additions & 0 deletions src/sdk/PnP.Core/Model/SharePoint/Core/Public/IFolder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using PnP.Core.Services;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace PnP.Core.Model.SharePoint
Expand Down Expand Up @@ -82,6 +83,9 @@ public interface IFolder : IDataModel<IFolder>, IDataModelGet<IFolder>, IDataMod

/// <summary>
/// Gets the collection of list folders contained in the list folder.
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IFolderCollection Folders { get; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace PnP.Core.Model.SharePoint
using System.Linq;

namespace PnP.Core.Model.SharePoint
{
/// <summary>
/// Defines if and who liked a list item.
Expand All @@ -18,6 +20,9 @@ public interface ILikedByInformation : IDataModel<ILikedByInformation>, IDataMod

/// <summary>
/// The people that liked this list item.
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public ICommentLikeUserEntityCollection LikedBy { get; }
}
Expand Down
19 changes: 17 additions & 2 deletions src/sdk/PnP.Core/Model/SharePoint/Core/Public/IList.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using PnP.Core.Model.Security;
using PnP.Core.Services;
using PnP.Core.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;

Expand Down Expand Up @@ -233,16 +233,25 @@ public interface IList : IDataModel<IList>, IDataModelGet<IList>, IDataModelLoad

/// <summary>
/// Collection of list items in the current List object
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IListItemCollection Items { get; }

/// <summary>
/// Collection of content types for this list
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IContentTypeCollection ContentTypes { get; }

/// <summary>
/// Collection of fields for this list
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IFieldCollection Fields { get; }

Expand All @@ -253,11 +262,17 @@ public interface IList : IDataModel<IList>, IDataModelGet<IList>, IDataModelLoad

/// <summary>
/// Get a list of the views
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IViewCollection Views { get; }

/// <summary>
/// Collection of list webhooks
/// Implements <see cref="IQueryable{T}"/>. <br />
/// See <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-getdata.html#requesting-model-collections">Requesting model collections</see>
/// and <see href="https://pnp.github.io/pnpcore/using-the-sdk/basics-iqueryable.html">IQueryable performance considerations</see> to learn more.
/// </summary>
public IListSubscriptionCollection Webhooks { get; }

Expand Down
Loading