This is the online documentation for PostSharp 5.0.
Download PDF or CHM. Go to v4.3 or v5.0

Working with Cache Dependencies

Cache dependencies have two major use cases. First, dependencies can act as as a middle layer between the cached methods (typically the read methods) and the invalidating methods (typically the update methods) and therefore reduce the coupling between the read and update methods. Second, cache dependencies can be used to represent external dependencies, such as file system dependencies or SQL dependencies.

Compared to direct invalidation, using dependencies exhibits lower performance and higher resource consumption in the caching back-end because of the need to store and synchronize the graph of dependencies. For details about direct invalidation, see Removing Items From the Cache.

This topic contains the following sections:

Working with string dependencies

Eventually, all dependencies are represented as strings. Although we recommend to use one of the strongly-typed approaches described below, it is good to understand how string dependencies work.

To assign a string dependency to a cached return value of a method an to invalidate it:

  1. Add call to the CachingServices.CurrentContext.AddDependency method to the cached method.

  2. Add call to the CachingServices.Invalidation.Invalidate method to the invalidating method.

Note Note

Dependency properly work with recursive method calls. If a cached method A calls another cached method B, all dependencies of B are automatically dependencies of A, even if A was cached when A was being evaluated.

Example

In this example, the GetValue method assigns a string dependency to its cached return value. The Update method invalidates the dependency. This causes the related cached return value to be invalidated as well.

C#
using System;
using System.Collections.Generic;
using System.Threading;
using PostSharp.Patterns.Caching;
using PostSharp.Patterns.Caching.Backends;

namespace PostSharp.Samples.Caching.StringDependencies
{
    class Database
    {
        private Dictionary<int, string> data = new Dictionary<int, string>();

        private static string GetValueDependencyString( int id ) => $"value:{id}";

        [Cache]
        public string GetValue( int id )
        {
            Console.WriteLine( $">> Retrieving {id} from the database..." );
            Thread.Sleep( 1000 );
            CachingServices.CurrentContext.AddDependency( GetValueDependencyString( id ) );
            return this.data[id];
        }

        public void Update( int id, string value )
        {
            this.data[id] = value;
            CachingServices.Invalidation.Invalidate( GetValueDependencyString( id ) );
        }
    }

    class Program
    {
        static void Main( string[] args )
        {
            CachingServices.DefaultBackend = new MemoryCachingBackend();

            Database db = new Database();

            db.Update( 1, "first" );

            Console.WriteLine( "Retrieving value of 1 for the 1st time should hit the database." );
            Console.WriteLine( "Retrieved: " + db.GetValue( 1 ) );

            Console.WriteLine( "Retrieving value of 1 for the 2nd time should NOT hit the database." );
            Console.WriteLine( "Retrieved: " + db.GetValue( 1 ) );

            db.Update( 1, "second" );

            Console.WriteLine( "Retrieving updated value of 1 for the 1st time should hit the database." );
            Console.WriteLine( "Retrieved: " + db.GetValue( 1 ) );
        }
    }
}

The output of this sample is:

Retrieving value of 1 for the 1st time should hit the database.
>> Retrieving 1 from the database...
Retrieved: first
Retrieving value of 1 for the 2nd time should NOT hit the database.
Retrieved: first
Retrieving updated value of 1 for the 1st time should hit the database.
>> Retrieving 1 from the database...
Retrieved: second
Working with object-oriented dependencies through the ICacheDependency interface

Working with string dependencies can be error-prone because the code generating the string is duplicated in the invalidated and the invalidating method. A better approach is to encapsulate the cache key generation logic, i.e. to represent the cache dependency as an object, and add some key-generation logic to this object.

If you own the source code of the class you want to use as a cache dependency, the easiest approach is to implement the ICacheDependency interface.

Note Note

This approach can be used to implement support for other kinds of dependencies, like file system dependencies or SQL dependencies.

Example

In the following example, the Customer class represents a business entity. Instances of this class are being cached. At the same time, they serve as object dependencies, therefore the Customer class implements the ICacheDependency interface. The GetValue method assigns an object dependency of type Customer to its cached return value. The Update method invalidates the dependency. This causes the related cached return value to be invalidated as well.

C#
using System;
using System.Collections.Generic;
using System.Threading;
using PostSharp.Patterns.Caching;
using PostSharp.Patterns.Caching.Backends;
using PostSharp.Patterns.Caching.Dependencies;

namespace PostSharp.Samples.Caching.ICacheDependencies
{
    class Customer : ICacheDependency
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public bool Equals( ICacheDependency other )
        {
            Customer otherCustomer = other as Customer;
            return otherCustomer != null && this.Id == otherCustomer.Id;
        }

        public string GetCacheKey() => $"{nameof(Customer)}:{this.Id}";
    }

    class Database
    {
        private Dictionary<int, Customer> customers = new Dictionary<int, Customer>();

        [Cache]
        public Customer GetCustomer( int id )
        {
            Console.WriteLine( $">> Retrieving {id} from the database..." );
            Thread.Sleep( 1000 );
            Customer customer = this.customers[id];
            CachingServices.CurrentContext.AddDependency( customer );
            return customer;
        }

        public void Update( Customer customer )
        {
            this.customers[customer.Id] = customer;
            CachingServices.Invalidation.Invalidate( customer );
        }
    }

    class Program
    {
        static void Main( string[] args )
        {
            CachingServices.DefaultBackend = new MemoryCachingBackend();

            Database db = new Database();

            db.Update( new Customer() {Id = 1, Name = "Alice"} );

            Console.WriteLine( "Retrieving value of 1 for the 1st time should hit the database." );
            Console.WriteLine( "Retrieved: " + db.GetCustomer( 1 ).Name );

            Console.WriteLine( "Retrieving value of 1 for the 2nd time should NOT hit the database." );
            Console.WriteLine( "Retrieved: " + db.GetCustomer( 1 ).Name );

            db.Update( new Customer() {Id = 1, Name = "Bob"} );

            Console.WriteLine( "Retrieving updated value of 1 for the 1st time should hit the database." );
            Console.WriteLine( "Retrieved: " + db.GetCustomer( 1 ).Name );
        }
    }
}

The output of this sample is:

Retrieving value of 1 for the 1st time should hit the database.
>> Retrieving 1 from the database...
Retrieved: Alice
Retrieving value of 1 for the 2nd time should NOT hit the database.
Retrieved: Alice
Retrieving updated value of 1 for the 1st time should hit the database.
>> Retrieving 1 from the database...
Retrieved: Bob
Working with object-oriented dependencies through a formatter

The previous approach requires implementing an interface in the source code of the business entity. If you cannot modify the source code of a dependency class, the best approach is to implement a formatter for this class and to register it.

See Customizing Cache Keys for details.