Microsoft. Xrm. Client (Part 3a): CachedOrganizationService

In this series we have been looking at the Developer Extensions provided by the Microsoft.Xrm.Client assembly:

Part 1 - CrmOrganizationServiceContext and when should I use it?

Part 2 - Simplified Connection Management & Thread Safety

This 3rd part in the series demonstrates when and how to use the CachedOrganizationService.

When writing client applications and portals that connect to Dynamics CRM there are many situations where you need to retrieve data and use it in multiple places. In these situations it is common practice to implement a caching strategy which although can be easy implemented using custom code can quickly add complexity to your code if you're not careful.

The CachedOrganizationService provides a wrapper around the OrganizationServiceProxy and provides a caching service that is essentially transparent to your code with the cache being automatically invalidated when records are updated by the client. The CachedOrganizationService inherits from OrganizationService and uses the same CrmConnection instantiation so you can almost swap your existing OrganizationService with a CachedORganizationService so that your code can benefit from caching without any changes. There are always some pieces of data that you don't want to cache, and so you will need to plan your caching strategy carefully.

Using an CachedOrganizationService

You have two choices when it comes to instantiating the objects required:

  1. Manual Instantiation – Full control over the combination of the CrmConnection, OrganizationService & OrganizationServiceContext
  2. Configuration Manager Instantiation – App/Web.config controlled instantiation using a key name.

Part 3b will show how to use the Configuration Management, but for now we'll explicitly instantiate the objects so you can understand how they work together.

CrmConnection connection = new CrmConnection("CRM");       
using (OrganizationService service = new CachedOrganizationService(connection))
using (CrmOrganizationServiceContext context = new CrmOrganizationServiceContext(service))
{
…
}

Using the CachedOrganizationService to create your Service Context gives your application automatic caching of queries. Each query results is stored against the query used and if when performing further queries, if there is a matching query, the results are returned from the cache rather than using a server query.

Cached Queries

In the following example, the second query will not result in any server request, since the same query has already been executed.

QueryByAttribute request = new QueryByAttribute(Account.EntityLogicalName);
request.Attributes.Add("name");
request.Values.Add("Big Account");
request.ColumnSet = new ColumnSet("name");

// First query will be sent to the server
Account acc1 = (Account)service.RetrieveMultiple(request).Entities[0];

// This query will be returned from cache
Account acc2 = (Account)service.RetrieveMultiple(request).Entities[0];

If another query is executed that requests different attribute values (or has different criteria), then the query is executed to get the additional values:

QueryByAttribute request2 = new QueryByAttribute(Account.EntityLogicalName);
request.Attributes.Add("name");
request.Values.Add("Big Account");
request.ColumnSet = new ColumnSet("name","accountnumber");

// This query will be sent to the server because the query is different
Account acc3 = (Account)service.RetrieveMultiple(request).Entities[0];

Cloned or Shared

By default, the CachedOrganizationSevice will return a cloned instance of the cached results, but it can be configured to return the same instances:

((CachedOrganizationService)service).Cache.ReturnMode =
        OrganizationServiceCacheReturnMode.Shared;

QueryByAttribute request = new QueryByAttribute(Account.EntityLogicalName);
request.Attributes.Add("name");
request.Values.Add("Big Account");
request.ColumnSet = new ColumnSet("name");

// First query will be sent to the server
Account acc1 = (Account)service.RetrieveMultiple(request).Entities[0];

// This query will be returned from cache
Account acc2 = (Account)service.RetrieveMultiple(request).Entities[0];
Assert.AreSame(acc1, acc2);

The assertion will pass because a ReturnMode of 'Shared' will return the existing values in the cache and not cloned copies (the default behaviour).

Automatic Invalidated Cache on Update

If you then go on to update/delete and entity that exists in a cached query result, then the cache is automatically invalidated resulting in refresh the next time it is requested.

Coupling with CrmOrganizationServiceContext

In Part 1 we saw that the CrmOrganizationServiceContext provided a 'Lazy Load' mechanism for relationships, however it would execute a metadata request and query every time the relationship Entity set was queried. When this is coupled with the CachedOrganizationService it gives us the complete solution. In the following example, we perform two LINQ queries against the Account.contact_customer_accounts relationship, the first returns all the related contacts (all attributes), and the second simply retrieves the results from the cache. You don't need to worry about what is loaded and what is not.

// Query 1
Console.WriteLine("Query Expected");
Xrm.Account acc = (from a in context.CreateQuery()
                   where a.Name == "Big Account"
                   select new Account
                   {
                       AccountId = a.AccountId,
                       Name = a.Name,
                   }).Take(1).FirstOrDefault();

// Get the contacts from server
Console.WriteLine("Query Expected");
var accounts = (from c in acc.contact_customer_accounts
                select new Contact
                {
                    FirstName = c.FirstName,
                    LastName = c.LastName
                }).ToArray();

// Get the contacts again - from cahce this time
Console.WriteLine("No Query Expected");
var accounts2 = (from c in acc.contact_customer_accounts
                 select new Contact
                 {
                     FirstName = c.FirstName,
                     LastName = c.LastName,
                     ParentCustomerId = c.ParentCustomerId
                 }).ToArray();

Thread Safety

Provided you are using the CachedOrganizationService with the CrmConnection class, then all the same multi-threading benefits apply. The Client Authentication will only be performed initially and when the token expires, and the cache automatically handles locking when being access by multiple threads.

Pseudo Cache with OrganizationServiceContext.MergeOption = AppendOnly

The OrganizationServiceContext has built in client side tracking of objects and a sort of cache when using a MergeOption Mode of AppendOnly (the default setting). With MergOption=AppendOnly once an entity object has been added to the context, it will not be replaced by a instance on subsequent LINQ queries. Instead, the existing object is re-used so that any changes made on the client remain. This means that even if a new attribute is requested and the CachedOrganizationService executes the query accordingly, it will look as though it hasn't been a new query because the OrganizationServiceContext still returns the object that it is currently tracking.

// Query 1
Xrm.Account acc = (from a in context.CreateQuery()
                   where a.Name == "Big Account"
                   select new Account
                   {
                       AccountId = a.AccountId,
                       Name = a.Name,
                   }).Take(1).FirstOrDefault();

Assert.IsNull(acc.AccountNumber); // We didn’t request the AccountNumber

// Query 2
// Because there is an additional attribute value requested, this will query the server
Xrm.Account acc2 = (from a in context.CreateQuery()
                    where a.Name == "Big Account"
                    select new Account
                    {
                        AccountId = a.AccountId,
                        Name = a.Name,
                        AccountNumber = a.AccountNumber
                    }).Take(1).FirstOrDefault();

Assert.AreSame(acc, acc2); // MergeOption=AppendOnly preserves existing objects and so the first tracked object is returned
Assert.IsNull(acc.AccountNumber); // Account Number will be null even though it was returned from the server

This can lead to the conclusion that the CachedOrganizationService isn't detecting that we want to return a new attribute that wasn't included in the first query, but it actually isn't anything to do with caching since the OrganizationServiceContext will behave like this even if there was no CachedOrganizationService in use. If you were to use a MergeOption of NoTracking, PreserveChanges or Overwrite changes you wouldn't see the above behaviour because the 2nd query would always return a new instance of the account with the AcccountNumber attribute value loaded.

Next in this series I'll show you how to configure the CachedOrganisationSevice and OrganizationServiceContext using the web/app.config.

@ScottDurow

Pingbacks and trackbacks (2)+

Comments are closed