По соображениям производительности коллекции сущностей часто разбиваются на страницы, и каждая страница возвращается с URL-адресом следующей страницы. Класс PageIterator упрощает использование страничных коллекций.
PageIterator обрабатывает перечисление текущей страницы и запрос последующих страниц автоматически.
Кроме того, можно использовать @odata.nextLink
свойство для запроса последующих страниц вручную.
Если вы отправляете дополнительные заголовки запросов в исходном запросе, эти заголовки не включаются по умолчанию в последующие запросы страницы. Если эти заголовки требуется отправлять в последующие запросы, их необходимо явно настроить.
Итерации по всем сообщениям
В следующем примере показано перебор всех сообщений в почтовом ящике пользователя.
Совет
В этом примере задается небольшой размер страницы с top
помощью параметра для демонстрационных целей. Вы можете задать размер страницы до 999, чтобы свести к минимуму количество необходимых запросов.
var messages = await graphClient.Me.Messages
.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Top = 10;
requestConfiguration.QueryParameters.Select =
["sender", "subject", "body"];
requestConfiguration.Headers.Add(
"Prefer", "outlook.body-content-type=\"text\"");
});
if (messages == null)
{
return;
}
var pageIterator = PageIterator<Message, MessageCollectionResponse>
.CreatePageIterator(
graphClient,
messages,
// Callback executed for each item in
// the collection
(msg) =>
{
Console.WriteLine(msg.Subject);
return true;
},
// Used to configure subsequent page
// requests
(req) =>
{
// Re-add the header to subsequent requests
req.Headers.Add("Prefer", "outlook.body-content-type=\"text\"");
return req;
});
await pageIterator.IterateAsync();
import (
"context"
"fmt"
"log"
"time"
abstractions "github.com/microsoft/kiota-abstractions-go"
graph "github.com/microsoftgraph/msgraph-sdk-go"
graphcore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/users"
)
headers := abstractions.NewRequestHeaders()
headers.Add("Prefer", "outlook.body-content-type=\"text\"")
var pageSize int32 = 10
query := users.ItemMessagesRequestBuilderGetQueryParameters{
Select: []string{"body", "sender", "subject"},
Top: &pageSize,
}
options := users.ItemMessagesRequestBuilderGetRequestConfiguration{
Headers: headers,
QueryParameters: &query,
}
result, err := graphClient.Me().Messages().Get(context.Background(), &options)
if err != nil {
log.Fatalf("Error getting messages: %v\n", err)
}
// Initialize iterator
pageIterator, err := graphcore.NewPageIterator[*models.Message](
result,
graphClient.GetAdapter(),
models.CreateMessageCollectionResponseFromDiscriminatorValue)
if err != nil {
log.Fatalf("Error creating page iterator: %v\n", err)
}
// Any custom headers sent in original request should also be added
// to the iterator
pageIterator.SetHeaders(headers)
// Iterate over all pages
err = pageIterator.Iterate(
context.Background(),
func(message *models.Message) bool {
fmt.Printf("%s\n", *message.GetSubject())
// Return true to continue the iteration
return true
})
if err != nil {
log.Fatalf("Error iterating over messages: %v\n", err)
}
ArrayList<Message> messages = new ArrayList<>();
MessageCollectionResponse messageResponse = graphClient.me().messages().get( requestConfiguration -> {
requestConfiguration.headers.add("Prefer", "outlook.body-content-type=\"text\"");
requestConfiguration.queryParameters.select = new String[] {"sender, subject, body"};
requestConfiguration.queryParameters.top = 10;
});
PageIterator<Message, MessageCollectionResponse> pageIterator =
new PageIterator.Builder<Message, MessageCollectionResponse>()
.client(graphClient)
// Response from the first request
.collectionPage(Objects.requireNonNull(messageResponse))
// Factory to create a new collection response
.collectionPageFactory(MessageCollectionResponse::createFromDiscriminatorValue)
// Used to configure subsequent requests
.requestConfigurator( requestInfo -> {
// Re-add the header and query parameters to subsequent requests
requestInfo.headers.add("Prefer", "outlook.body-content-type=\"text\"");
requestInfo.addQueryParameter("%24select", new String[] {"sender, subject, body"});
requestInfo.addQueryParameter("%24top", 10);
return requestInfo;
})
// Callback executed for each item in the collection
.processPageItemCallback( message -> {
messages.add(message);
return true;
}).build();
pageIterator.iterate();
$query = new MessagesRequestBuilderGetQueryParameters(
top: 10,
select: ['sender', 'subject', 'body']);
$config = new MessagesRequestBuilderGetRequestConfiguration(
queryParameters: $query,
headers: ['Prefer' => 'outlook.body-content-type="text"']);
$messages = $graphClient->me()
->messages()
->get($config)
->wait();
// Microsoft\Graph\Core\Tasks\PageIterator
$pageIterator = new PageIterator($messages, $graphClient->getRequestAdapter());
$callback = function($message): bool {
/** @var Models\Message $message */
print($message->getSubject().PHP_EOL);
// Return true to continue iteration
return true;
};
// Re-add the header to subsequent requests
$pageIterator->setHeaders(['Prefer' => 'outlook.body-content-type="text"']);
$pageIterator->iterate($callback);
const response: PageCollection = await graphClient
.api('/me/messages?$top=10&$select=sender,subject,body')
.header('Prefer', 'outlook.body-content-type="text"')
.get();
// A callback function to be called for every item in the collection.
// This call back should return boolean indicating whether not to
// continue the iteration process.
const callback: PageIteratorCallback = (message: Message) => {
console.log(message.subject);
return true;
};
// A set of request options to be applied to
// all subsequent page requests
const requestOptions: GraphRequestOptions = {
// Re-add the header to subsequent requests
headers: {
Prefer: 'outlook.body-content-type="text"',
},
};
// Creating a new page iterator instance with client a graph client
// instance, page collection response from request and callback
const pageIterator = new PageIterator(
graphClient,
response,
callback,
requestOptions,
);
// This iterates the collection until the nextLink is drained out.
await pageIterator.iterate();
Остановка и возобновление итерации
В некоторых сценариях для выполнения других действий требуется остановить процесс итерации. Можно приостановить итерацию, вернувшись false
из обратного вызова итерации. Итерацию можно возобновить, вызвав resume
метод в PageIterator.
int count = 0;
int pauseAfter = 25;
var messages = await graphClient.Me.Messages
.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Top = 10;
requestConfiguration.QueryParameters.Select =
["sender", "subject"];
});
if (messages == null)
{
return;
}
var pageIterator = PageIterator<Message, MessageCollectionResponse>
.CreatePageIterator(
graphClient,
messages,
(msg) =>
{
Console.WriteLine(msg.Subject);
count++;
// If we've iterated over the limit,
// stop the iteration by returning false
return count < pauseAfter;
});
await pageIterator.IterateAsync();
while (pageIterator.State != PagingState.Complete)
{
Console.WriteLine("Iteration paused for 5 seconds...");
await Task.Delay(5000);
// Reset count
count = 0;
await pageIterator.ResumeAsync();
}
import (
"context"
"fmt"
"log"
"time"
abstractions "github.com/microsoft/kiota-abstractions-go"
graph "github.com/microsoftgraph/msgraph-sdk-go"
graphcore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/users"
)
var pageSize int32 = 10
query := users.ItemMessagesRequestBuilderGetQueryParameters{
Select: []string{"body", "sender", "subject"},
Top: &pageSize,
}
options := users.ItemMessagesRequestBuilderGetRequestConfiguration{
QueryParameters: &query,
}
result, err := graphClient.Me().Messages().Get(context.Background(), &options)
if err != nil {
log.Fatalf("Error getting messages: %v\n", err)
}
// Initialize iterator
pageIterator, err := graphcore.NewPageIterator[*models.Message](
result,
graphClient.GetAdapter(),
models.CreateMessageCollectionResponseFromDiscriminatorValue)
if err != nil {
log.Fatalf("Error creating page iterator: %v\n", err)
}
// Pause iterating after 25
var count, pauseAfter = 0, 25
// Iterate over all pages
err = pageIterator.Iterate(
context.Background(),
func(message *models.Message) bool {
count++
fmt.Printf("%d: %s\n", count, *message.GetSubject())
// Once count = 25, this returns false,
// Which pauses the iteration
return count < pauseAfter
})
if err != nil {
log.Fatalf("Error iterating over messages: %v\n", err)
}
// Pause 5 seconds
fmt.Printf("Iterated first %d messages, pausing for 5 seconds...\n", pauseAfter)
time.Sleep(5 * time.Second)
fmt.Printf("Resuming iteration...\n")
// Resume iteration
err = pageIterator.Iterate(
context.Background(),
func(message *models.Message) bool {
count++
fmt.Printf("%d: %s\n", count, *message.GetSubject())
// Return true to continue the iteration
return true
})
if err != nil {
log.Fatalf("Error iterating over messages: %v\n", err)
}
int iterations = 1;
ArrayList<Message> messages = new ArrayList<>();
int pauseAfter = iterations*25;
MessageCollectionResponse messageResponse = graphClient.me().messages().get( requestConfiguration -> {
requestConfiguration.queryParameters.top = 10;
requestConfiguration.queryParameters.select = new String[] {"sender, subject"};
});
PageIterator<Message, MessageCollectionResponse> pageIterator =
new PageIterator.Builder<Message, MessageCollectionResponse>()
.client(graphClient)
.collectionPage(Objects.requireNonNull(messageResponse))
.collectionPageFactory(MessageCollectionResponse::createFromDiscriminatorValue)
.requestConfigurator( requestInfo -> {
requestInfo.addQueryParameter("%24select", new String[] {"sender, subject"});
requestInfo.addQueryParameter("%24top", 10);
return requestInfo;
})
.processPageItemCallback( message -> {
messages.add(message);
// Pause paging by returning false after 25 messages
return messages.size() < pauseAfter;
}).build();
pageIterator.iterate();
// Resume paging
while (pageIterator.getPageIteratorState() != PageIterator.PageIteratorState.COMPLETE) {
iterations+=1;
pageIterator.resume();
}
$count = 0;
$messages = $graphClient->me()
->messages()
->get()
->wait();
// Microsoft\Graph\Core\Tasks\PageIterator
$pageIterator = new PageIterator($messages, $graphClient->getRequestAdapter());
$callback = function($message) use (&$count): bool {
/** @var Models\Message $message */
$count++;
print($count.'. '.$message->getSubject().PHP_EOL);
// Return true to continue iteration
// Return false once first 5 have been processed
return $count < 5;
};
$pageIterator->iterate($callback);
print('Pausing iteration after first 5'.PHP_EOL);
sleep(5);
// Process next 5
$count = 0;
$pageIterator->iterate($callback);
let count = 0;
const pauseAfter = 25;
const response: PageCollection = await graphClient
.api('/me/messages?$top=10&$select=sender,subject,body')
.get();
const callback: PageIteratorCallback = (message: Message) => {
console.log(message.subject);
count++;
// If we've iterated over the limit,
// stop the iteration by returning false
return count < pauseAfter;
};
const pageIterator = new PageIterator(graphClient, response, callback);
await pageIterator.iterate();
while (!pageIterator.isComplete()) {
console.log('Iteration paused for 5 seconds...');
await new Promise((resolve) => setTimeout(resolve, 5000));
// Reset count
count = 0;
await pageIterator.resume();
}
Запрос последующих страниц вручную
В качестве альтернативы использованию класса PageIterator можно вручную проверка ответ для @odata.nextLink
свойства и запросить следующую страницу.
var messages = await graphClient.Me.Messages
.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Top = 10;
});
while (messages?.Value != null)
{
foreach (var message in messages.Value)
{
Console.WriteLine(message.Subject);
}
// If OdataNextLink has a value, there is another page
if (!string.IsNullOrEmpty(messages.OdataNextLink))
{
// Pass the OdataNextLink to the WithUrl method
// to request the next page
messages = await graphClient.Me.Messages
.WithUrl(messages.OdataNextLink)
.GetAsync();
}
else
{
// No more results, exit loop
break;
}
}
import (
"context"
"fmt"
"log"
"time"
abstractions "github.com/microsoft/kiota-abstractions-go"
graph "github.com/microsoftgraph/msgraph-sdk-go"
graphcore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/users"
)
var pageSize int32 = 10
query := users.ItemMessagesRequestBuilderGetQueryParameters{
Top: &pageSize,
}
options := users.ItemMessagesRequestBuilderGetRequestConfiguration{
QueryParameters: &query,
}
result, err := graphClient.Me().Messages().Get(context.Background(), &options)
if err != nil {
log.Fatalf("Error getting messages: %v\n", err)
}
for {
for _, message := range result.GetValue() {
fmt.Printf("%s\n", *message.GetSubject())
}
nextPageUrl := result.GetOdataNextLink()
if nextPageUrl != nil {
result, err = graphClient.Me().Messages().
WithUrl(*nextPageUrl).
Get(context.Background(), nil)
if err != nil {
log.Fatalf("Error getting messages: %v\n", err)
}
} else {
break
}
}
MessageCollectionResponse messagesPage = graphClient.me().messages().get( requestConfiguration -> {
requestConfiguration.headers.add("Prefer", "outlook.body-content-type=\"text\"");
requestConfiguration.queryParameters.select = new String[] {"sender, subject, body"};
requestConfiguration.queryParameters.top = 10;
});
while (messagesPage != null) {
final List<Message> messages = messagesPage.getValue();
for (Message message : messages) {
System.out.println(message.getSubject());
}
// Get the next page
final String odataNextLink = messagesPage.getOdataNextLink();
if (odataNextLink == null || odataNextLink.isEmpty()) {
break;
} else {
messagesPage = graphClient.me().messages().withUrl(odataNextLink).get();
}
}
/** @var MessageCollectionResponse $messages */
$messages = $graphClient->me()
->messages()
->get()
->wait();
while (null !== $messages->getValue())
{
foreach($messages->getValue() as $message) {
/** @var Models\Message $message */
print($message->getSubject().PHP_EOL);
}
if (null !== $messages->getOdataNextLink()) {
$messages = $graphClient->me()
->messages()
->withUrl($messages->getOdataNextLink())
->get()
->wait();
}
else {
break;
}
}
let response: PageCollection = await graphClient
.api('/me/messages?$top=10')
.get();
while (response.value.length > 0) {
for (const message of response.value as Message[]) {
console.log(message.subject);
}
if (response['@odata.nextLink']) {
response = await graphClient.api(response['@odata.nextLink']).get();
} else {
break;
}
}
Обработка ошибок
Предотвращение ошибок DirectoryPageTokenNotFoundException
При разбиении по страницам через большие наборы данных может возникнуть DirectoryPageTokenNotFoundException
ошибка, которая не позволяет клиентскому приложению успешно получить последующие страницы. Эта ошибка возникает, когда клиентское приложение использует маркер из операции повтора для запроса следующей страницы результатов.
Чтобы избежать этой ошибки, не используйте маркеры из операций повторных попыток для последующих запросов страниц, так как эти маркеры не гарантируются для будущих запросов. Вместо этого сохраните маркер из последнего успешного ответа и используйте его для следующего запроса страницы. Таким образом @odata.nextLink
, значение, используемое для повторных попыток, следует использовать для последующего запроса страницы.
Пример сценария
- Извлеките страницу 1 и получите маркер Token1.
- Используйте "Token1", чтобы запросить страницу 2.
- Если возникает ошибка сети, повторите запрос.
- Во время повторных попыток вы получите новый токен "RetryToken".
- Не используйте RetryToken для запроса страницы 3, так как это может вызвать ошибку
DirectoryPageTokenNotFoundException
.
- Вместо этого используйте "Token1" (токен из последнего успешного ответа без повторных попыток), чтобы запросить страницу 3.