Following on from Dynamics CRM Paging Cookies – Some gotchas!, here is a helper class we use for all our paging needs.

An example of its usage:

[sourcecode language=”csharp”]
const string fetchXml = @"
<fetch mapping=’logical’ count=’5000′ version=’1.0′ page='{0}’ {1}>
<entity name=’account’>
<attribute name=’name’ />
</entity>
</fetch>";

var accounts = new PagedRetriever<Tuple<Guid, string>>(
entity => Tuple.Create( entity.Id,
(string)entity.Attributes["name"]),
(page, cookie) => String.Format(fetchXml, page, cookie))
.GetData(service);
[/sourcecode]

The utility class has a couple of nice features:

    Laziness – Extra pages are only retrieved as they are enumerated
    Memory efficient – Entities are converted on the fly, saving a huge amount of memory if a large enumeration is converted to a list

Here is the complete code for the Paged Retriever class:

[sourcecode language=”csharp”]
public class PagedRetriever<T>
{
private readonly Func<Entity, T> converter;
private readonly Func<int, string, string> pagedFetchXml;
private readonly bool usePagingCookie;

/// <summary>
/// Utility class to retrieve paged results from CRM
/// </summary>
/// <param name="converter">Converts the returned entity to a wrapper type</param>
/// <param name="pagedFetchXml">pagedFetchXml takes the pagenumber and the pagingCookie and returns the fetchXml</param>
/// <param name="usePagingCookie">Whether the paging cookie should be used. Set to yes for performance if the primary entity id is unique in the resultset.</param>
public PagedRetriever(Func<Entity, T> converter, Func<int, string, string> pagedFetchXml, bool usePagingCookie = true)
{
this.converter = converter;
this.pagedFetchXml = pagedFetchXml;
this.usePagingCookie = usePagingCookie;
}

/// <summary>
/// Get all entities using the standard RetrieveMultiple call
/// </summary>
/// <param name="service"></param>
/// <returns></returns>
public IEnumerable<T> GetData(IOrganizationService service)
{
return GetData(query => service.RetrieveMultiple(query));
}

/// <summary>
/// Gets all entities using a custom collection producer (eg. a RetrieveByResourcesServiceRequest)
/// </summary>
/// <param name="retriever"></param>
/// <returns></returns>
public IEnumerable<T> GetData(Func<QueryBase, EntityCollection> retriever)
{
int page = 1;
string pagingCookie = String.Empty;
while (true)
{
var pagedXml = pagedFetchXml(page, pagingCookie);

var entityCollection = retriever(new FetchExpression(pagedXml));
if (entityCollection == null)
{
break;
}

foreach (var convertedEntity in entityCollection.Entities.Select(converter))
{
yield return convertedEntity;
}

if (!entityCollection.MoreRecords)
{
yield break;
}

if (usePagingCookie && !String.IsNullOrEmpty(entityCollection.PagingCookie))
{
pagingCookie = "paging-cookie=’" + System.Web.HttpUtility.HtmlEncode(entityCollection.PagingCookie) + "’";
}

page++;
}
}
}
[/sourcecode]