Export (0) Print
Expand All
Abortable Thread Pool
The Analytic Hierarchy Process
API Test Automation in .NET
Asynchronous HttpWebRequests, Interface Implementation, and More
Bad Code? FxCop to the Rescue
Basics of .NET Internationalization
Behind the Scenes: Discover the Design Patterns You're Already Using in the .NET Framework
BigInteger, GetFiles, and More
Binary Serialization of DataSets
Building Voice User Interfaces
Can't Commit?: Volatile Resource Managers in .NET Bring Transactions to the Common Type
CLR Inside Out: Base Class Library Performance Tips and Tricks
CLR Inside Out: Ensuring .NET Framework 2.0 Compatibility
CLR Inside Out: Extending System.Diagnostics
CLR Profiler: No Code Can Hide from the Profiling API in the .NET Framework 2.0
Concurrent Affairs: Build a Richer Thread Synchronization Lock
Custom Cultures: Extend Your Code's Global Reach With New Features In The .NET Framework 2.0
Cutting Edge: Collections and Data Binding
Const in C#, Exception Filters, IWin32Window, and More
Creating a Custom Metrics Tool
DataGridView
DataSets vs. Collections
Determining .NET Assembly and Method References
Experimenting with F#
File Copy Progress, Custom Thread Pools
Finalizers, Assembly Names, MethodInfo, and More
Got Directory Services?: New Ways to Manage Active Directory using the .NET Framework 2.0
High Availability: Keep Your Code Running with the Reliability Features of the .NET Framework
How Microsoft Uses Reflection
ICustomTypeDescriptor, Part 2
ICustomTypeDescriptor, Part 1
Iterating NTFS Streams
JIT and Run: Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects
Lightweight UI Test Automation with .NET
Low-Level UI Test Automation
Make Your Apps Fly with the New Enterprise Performance Tool
Managed Spy: Deliver The Power Of Spy++ To Windows Forms With Our New Tool
Memory Models: Understand the Impact of Low-Lock Techniques in Multithreaded Apps
Microsoft Java Virtual Machine Update
Microsoft .NET Framework Delivers the Platform for an Integrated, Service-Oriented Web, Part 2
Mini Dump Snapshots and the New SOS
Mutant Power: Create A Simple Mutation Testing System With The .NET Framework
NamedGZipStream, Covariance and Contravariance
.NET Internationalization Utilities
.NET Profiling: Write Profilers With Ease Using High-Level Wrapper Classes
No More Hangs: Advanced Techniques To Avoid And Detect Deadlocks In .NET Apps
The Perfect Host: Create and Host Custom Designers with the .NET Framework 2.0
Phoenix Rising
Scheme Is Love
Security Enhancements in the .NET Framework 2.0
Sepia Tone, StringLogicalComparer, and More
Software Testing Paradoxes
Stay Alert: Use Managed Code To Generate A Secure Audit Trail
Stream Decorator, Single-Instance Apps
StringStream, Methods with Timeouts
SUPERASSERT Goes .NET
Tailor Your Application by Building a Custom Forms Designer with .NET
Test Harness Design Patterns
ThreadPoolPriority, and MethodImplAttribute
ThreadPoolWait and HandleLeakTracker
Three Vital FXCop Rules
A Tidal Wave of Change
To Confirm is Useless, to Undo Divine
Touch All the Bases: Give Your .NET App Brains and Brawn with the Intelligence of Neural Networks
Transactions for Memory
Trustworthy Software
Tune in to Channel 9
UDP Delivers: Take Total Control Of Your Networking With .NET and UDP
UI on the Fly: Use the .NET Framework to Generate and Execute Custom Controls at Run Time
Unexpected Errors in Managed Applications
Unhandled Exceptions and Tracing in the .NET Framework 2.0
Using Combinations to Improve Your Software Test Case Generation
Wandering Code: Write Mobile Agents In .NET To Roam And Interact On Your Network
What Makes Good Code Good?
XML Comments, Late-bound COM, and More
Expand Minimize

Using Reflection Emit to Cache .NET Assemblies

 

Simon Guest
Microsoft Corporation

February 2002

Applies to:
   Microsoft® .NET Framework
   Microsoft® Visual Studio® .NET 2002

Summary: Details a non-intrusive caching solution that uses Reflection Emit in the Microsoft .NET Framework. (26 printed pages)

Download Installer.msi.

Contents

Introduction
The Challenge
Solving the Caching Problem using the ACGEN Tool
Customizing the ACGEN Tool
How the ACGEN Tool Works
Conclusion

Introduction

There are few automated ways of caching methods within a Microsoft .NET Assembly or Web Service without writing a significant amount of the logic into the application. And sometimes, if the Web Service or .NET Assembly is from a third party, it isn't possible.

This article outlines a non-intrusive caching solution that uses Reflection Emit in the Microsoft .NET Framework. Reflection Emit is the ability to generate assemblies, types, methods, and MSIL (Microsoft Intermediate Language) on the fly in .NET. This generated code can then either by run dynamically or saved to disk as an assembly.

This article is divided into two parts: The first outlines a sample scenario and explains the problem, requirements and introduces a tool that was developed to meet these requirements. The second part covers how this tool works in detail, with some ideas on how the approach could be used in other applications.

This article has been written for developers and architects who are familiar with the System.Reflection namespace in .NET, and have an appreciation of MSIL (Microsoft Intermediate Language). It is recommended that the audience read and understand the System.Reflection and the System.Reflection.Emit samples that are provided as part of the Microsoft .NET Framework SDK.

The Challenge

To help understand the concepts, we will use the following example in our explanations and sample code.

Our Sample Scenario

Imagine we have written a 'StockTicker' Class (as either a .NET assembly or a Web Service).

This class exposes a public method called GetQuote. The GetQuote method takes a parameter of type String, which is the symbol of the stock to obtain a price for. The GetQuote method returns a value of type Double, which happens to be the current stock quote for the symbol passed.

This method could look as follows: public double GetQuote(String Symbol).

By referencing the StockTicker Class from our client application, we can call the GetQuote method and pass the value MSFT. As expected, a value of the stock price for this symbol is returned.

Simple stuff so far. However, the next time we call this method—even if the symbol we pass is the same ("MSFT"), the GetQuote method will still have to perform the lookup for the stock quote—even though it may have seen the last request only a short time ago. Now let's imagine that the GetQuote method takes some time to retrieve the quote (e.g. it has to extract the quote price from a streaming quote server elsewhere on the network), and that the quotes are typically only updated every 15 minutes or so.

The performance of the overall application would be increased, and the network hops reduced, by returning a cached result rather than going away and recalculating the result—as long as the method is called with the same parameter within a certain period of time. Traditionally, if we wanted to add this functionality we would have to write extra code for the StockTicker service itself. To create our own caching methods, we could store the incoming values in memory, check to see whether they had already been called within a certain period of time, and if so, return the cached result instead of re-doing the calculation. The problem with this approach is that we require some direct modification of the service method itself. What if we don't have access to the source code? What if the exposed method is via a Web Service that is maintained by another company we work with? And what if caching is required for a number of users/instances, but is not required nor is desirable for others?

Looking at our Requirements

We want some standard way of universally caching methods from any .NET assembly or exposed Web Service. From our previous scenario, we can derive the following requirements:

  • The caching must be performed on the client to minimize the number of network hops required for cached results.
  • The caching of these methods must require no additional coding to the exposed method/assembly.
  • The caching must not include any major changes to the client calling the application.
  • It must be tailored such that certain methods in an assembly/Web Service can be included/excluded as desired.
  • It must include the ability to provide a TTL (Time to Live) value for each application.
  • It must include the ability to use a custom caching algorithm/methodology if so desired.

Solving the Caching Problem using the ACGEN Tool

ACGEN (Assembly Cache GENerator)

ACGEN, a command line utility, is a tool that we will use in this article to present a solution to the caching requirements. The primary usage of this tool is to provide a complete example of the power of using Reflection Emit in .NET.

ACGEN works by creating a 'cached proxy' of any .NET assembly. The cached proxy that is generated 'looks' very similar to the original assembly, and can be referenced by the calling application in exactly the same way as the original.

When a method of the cached assembly is called, a check is made, based on the parameters passed to the method, to see whether the result of the method call is stored in a cache. (The actual cache values live in a separate assembly called the CacheAPI).

If there is a matching call, the cached result is returned—if not, the method in the original assembly is called, and the resulting value is stored to the cache for the next time before being passed back to the calling application. All of this functionality is transparent to the developer.

To install the source code example

To install the source code example supplied with this article, follow these instructions:

  1. Download and run the installer.msi file.
  2. During installation, select a destination directory for the source code. Underneath this directory, four sub-directories will be created, ACGEN, CacheAPI, MyStockTicker and MyStockTickerClient.
  3. ACGEN contains the source for the tool itself. CacheAPI contains the source for the Caching API example. MyStockTicker contains a dummy stock ticker assembly as described in this article. MyStockTickerClient contains a simple Windows Form application used to call the MyStockTicker class.
  4. Open Microsoft Visual Studio .NET and create a blank solution.
  5. In Solution Explorer, select Add Existing Project and add the .csproj file located in each of the four subdirectories to assemble the entire project.
  6. Click Build.

To test the installation without caching

  1. In Solution Explorer, right-click MyStockTickerClient and select Set as Startup Project. Run the application. The client Windows Form should be displayed.
  2. Enter the value MSFT in the text box and click the Get Quote button. A dialog box should be displayed showing a dummy stock price, and (more importantly!) a second dialog box will display how long the operation took. This should be around 500ms.
  3. Repeat the operation a couple of times. Note how every operation takes 500ms, even though the symbol we are asking for remains the same. (Other stock symbols you can also try are MYCO and MYCO2).
    Note   To simulate the service having to 'do some work' in order to return the quote, a 500ms time delay has been added to our dummy service. This is purely to prove that the caching implementation is working during our test, but this could be realistic in a production environment where the value was being obtained from a Web Service via the Internet.

We will now use the ACGEN tool to generate a cached proxy of the MyStockTicker assembly. To do this:

  1. Close the client application and return to Visual Studio. NET.
  2. Open an MS-DOS command prompt.
  3. At the command prompt, change directory to the output directory of the ACGEN project (For example, CD C:\CodeExample\ACGEN\bin\Debug).
  4. From this directory, type the following command:
    acgen ..\..\..\MyStockTicker\bin\Debug\MyStockTicker.dll
    

    This should produce the following:

    Microsoft (R) .NET Assembly Cache Generator
    [Version ACGEN, Version=1.1.0.2, Culture=neutral, 
       PublicKeyToken=null]
    Copyright (C) Microsoft Corp 2000-2001. All Rights Reserved.
    
    Saving assembly Cached_MyStockTicker.dll
    
  5. In Visual Studio.NET Solution Explorer, in the MyStockTickerClient, add a reference (right-click the MyStockTickerClient project and click Add Reference).
  6. In the Add Reference Dialog box, click Browse and navigate to the MyStockTicker\bin\Debug directory. Open the Cached_MyStockTicker.dll assembly and click OK.
  7. View the code for the Form1.cs file in the MyStockTickerClient. Scroll down to line 25 and replace the following line:
    private MyStockTicker.Service StockTicker= new 
          MyStockTicker.Service();
    

    with

    private Cached_MyStockTicker.Service StockTicker = 
          new Cached_MyStockTicker.Service();
    
  8. Click Build and re-run the MyStockTickerClient application using the same stock ticker symbol as before.

If the tool has worked correctly, you should observe the following:

On the first attempt to retrieve the stock value, the operation should take the same time as before (around 500ms). This is the first time that we've called the StockTicker, so the cache will be empty.

On the second and future attempts however, the response from the StockTicker assembly should be much quicker (around the 10ms range). The cached proxy that we have built intercepts the call to the assembly, returning a result from cache if the stock symbol was the same. Returning the value from cache is obviously much quicker than retrieving the value from the original assembly again.

After 10 seconds (10000ms) has elapsed, the value in the cache will expire. (The default TTL for the cache is 10000ms). Once this value becomes invalidated, the client will have to make another call to the assembly to retrieve the value and repopulate the cache.

Customizing the ACGEN Tool

In the above example, we called ACGEN from the command line with few parameters.

Calling ACGEN from the command line with no parameters, however, lists all of the available options. These are shown as follows:

C:\acgen>acgen
Microsoft (R) .NET Assembly Cache Generator.
[Version acgen, Version=1.1.650.26975, Culture=neutral, 
   PublicKeyToken=null]
Copyright (C) Microsoft Corp 2000-2001.  All Rights Reserved.

Usage :

acgen <assembly to cache> [OPTIONS], where OPTIONS are :

[/CACHEAPI:<filename>]       Filename of the Cache API to use.
                             (Default CacheAPI.DLL in the same dir)
[/OUTPUTASSEMBLY:<filename>] Filename of the output assembly to 
   generate.
                             (Default = 'Cached_'+orig. in the same 
   dir.)
[/PREFIX:<prefixString>]     Specifies the prefix for the new 
   namespace.
                             (Default = 'Cached_')
[/DERIVED]                   Cache derived / inherited methods and 
   properties
[/VERBOSE]                   Produces Verbose Output.
[/NOLOGO]                    Supresses the logo

To select what methods to cache, use either:

[/ATTRIBUTED]                The original assembly is attributed
                             with the CacheAPI.CachedAttribute class.

or

[/INCLUDE:<MethodName(s)>]   Only cache on these specified methods.
[/EXCLUDE:<MethodName(s)>]   Do not cache on these specified methods.
[/TTL:<ms>]                  Cache TTL Default for all methods in ms.
                             (Default = 1000)

The following options are available:

<assembly to cache>

The first parameter is required. This specifies which assembly to cache. For example, if we have an Assembly called MyStockTicker.DLL, the following could be used:

acgen MyStockTicker.dll

A path to the DLL can also be specified. (The cached proxy will be saved in the same directory as the original assembly).

acgen y:\MySource\MyStockTicker.dll
/CACHEAPI:<filename>

The ACGEN tool ships with a simple cache that uses a hash table and queue to store values.

If a custom caching algorithm is required, a custom assembly can be referenced using the /CACHEAPI parameter. (Note: To work correctly, the custom assembly must implement a strictly defined interface—this is detailed in the next section).

By default, if no value is passed with the CACHEAPI parameter, the ACGEN tool will look for a Caching API in a DLL called CacheAPI.DLL in the same directory as the tool.

Alternative locations for the Caching .dll can be specified by path:

acgen MyStockTicker.dll /CACHEAPI:x:\myprogs\cache\altcache.dll

This Caching API should also be accessible to the calling client application (you will notice in the source code example we have a reference to CacheAPI defined in the project file). A future extension of the tool could be to place this CacheAPI assembly in the GAC (Global Assembly Cache), making it a shared assembly.

/OUTPUTASSEMBLY:<filename>

This parameter specifies the name of the assembly to generate. If an output assembly name is not specified, the tool appends a Cached_ prefix before the name.

For example:

acgen MyStockTicker.dll 

will produce a cached assembly called Cached_MyStockTicker.dll

acgen MyStockTicker.dll /OUTPUTASSEMBLY:MyNewStockTicker.dll

will produce a cached assembly called MyNewStockTicker.dll

/PREFIX:<prefix>

The /PREFIX parameter specifies the prefix for the namespace of the generated assembly. The default is 'Cached_'. For example, if the namespace of the original assembly is 'MyStockTickerFunctions', then the new namespace name will be 'Cached_MyStockTickerFunctions', unless specified with this parameter.

Using a different namespace prefix allows both the cached and non-cached methods to be used in the same calling application.

/DERIVED

If the original assembly type derives from a class and the /DERIVED parameter is specified, then the derived methods will also be cached.

/VERBOSE

The /VERBOSE parameter produces verbose output used for troubleshooting.

/NOLOGO

The /NOLOGO parameter suppresses the logo/banner.

Specifying Which Methods in the Original Assembly to Cache

One of the design goals of the solution is to give the developer the ability to specify which methods in the assembly should and should not be cached, and how long to retain the results in cache.

Additional parameters can be used from the command line to specify these methods. If none of the following options are specified, then all methods that return a value in the original assembly will be cached, with a default TTL of 10000ms.

/TTL:<ms>

Specifies the TTL (Time To Live) value in milliseconds for the cached assembly.

/INCLUDE:<MethodNames>

or

/EXCLUDE:<MethodNames>

Allows methods in the original assembly to be included/excluded as deemed necessary. For example, if only certain methods in our example should be cached, the following could be used:

acgen MyStockTicker.dll /include:GetQuote,GetOptionQuote

If however, all of the methods should be cached with the exception of only a few, the exclude method can be used:

acgen MySecurity.dll /exclude:GetPassword,SetPassword

Multiple methods must be divided by a comma with no spaces, and should include the class name (if multiple classes are present in the assembly). If methods specified after include or exclude parameter cannot be found in the original assembly, they will be ignored.

Specifying the CachedAttribute Attribute in the Original Assembly

/ATTRIBUTED

The /ATTRIBUTED parameter can be used to signify that only methods in the original assembly that have been marked with the [CacheAPI.CachedAttribute] attribute will be cached.

For scenarios where the source code of the original assembly is available, the CacheAPI class provides an Attribute class called [CachedAttribute]. To use this on methods within the original assembly, simple add a reference to the CacheAPI class, and prefix each method that needs to be cached with this attribute.

For example:

[CachedAttribute]
public String GetQuote(String Symbol)
{
….
}

specifies that the GetQuote Method will be cached when the /ATTRIBUTED parameter is supplied.

The CachedAttribute also allows the TTL (in ms) to be specified on a method by method basis :

[CachedAttribute(50000)]
public String GetQuote(String Symbol)
{
….
}

To specify the TTL for each method, simply place the value as the first parameter of the attribute. As with the command line option, tf the [CachedAttribute] attribute is applied without a TTL value, a value of 10000 is used by default.

Referencing the Cached Assembly from your Application

Referencing the cached assembly in Visual Studio .NET is simply a case of adding the reference in to the client project in Solution Explorer (by right-clicking the References folder within the project and clicking Add Reference).

Once we have added the reference, we simply need to change the namespace of the assembly to the cached equivalent. For example:

MyStockTicker.Service StockTicker = new MyStockTicker.Service();
Console.WriteLine("Result = "+StockTicker.GetQuote("MSFT"));

We simply replace the reference to the MyStockTicker namespace with the cached version:

Cached_MyStockTicker.Service StockTicker = new 
      Cached_MyStockTicker.Service();
Console.WriteLine("Result = "+StockTicker.GetQuote("MSFT"));


We recompile, run the client application, and methods within the MyStockTicker class are now cached based on our settings.

Caching a Web Service

The ACGEN tool can also be used to cache output from Web Services. Unlike the Microsoft ASP.NET OutputCache, this caching is performed on the client instead of the server. This offers the benefit of returning the result from the cache to the client application without having to make a call over the network.

To do this, we need to generate a Web Service proxy DLL before running the ACGEN utility.

From the command line, wsdl.exe (a tool that comes as part of the .NET Framework SDK) can be used to generate such an assembly. Wsdl.exe takes a parameter that must specify the URL of the Web Service.

For example:

C:\acgen>wsdl http://localhost/StockTickerWebService/Service1.asmx?WSDL

Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.0.2914.16]
Copyright (C) Microsoft Corp. 1998-2001. All rights reserved.

Writing file 'C:\acgen\Service1.cs'.

Once we have the proxy representation of the Web Service, we can simply compile from the command line to generate the actual DLL:

C:\acgen>csc /target:library Service1.cs
Microsoft (R) Visual C# Compiler Version 7.00.9254 
   [CLR version v1.0.2914]
Copyright (C) Microsoft Corp 2000-2001. All rights reserved.

If we now look in the same directory, we have a Service1.dll. This .dll can be run against the ACGEN tool to create a cached version:

C:\acgen>acgen Service1.dll

Microsoft (R) .NET Assembly Cache Generator.
[Version acgen, Version=1.0.642.26356, Culture=neutral, 
   PublicKeyToken=null]
Copyright (C) Microsoft Corp 2000-2001. All Rights Reserved.

Saving assembly Cached_Service1.dll


To reference the cached Web Service from within the Visual Studio .NET IDE, it is now just a case of adding a reference to the cached DLL generated by the ACGEN tool. This new reference replaces the existing Web Reference to the Web Service.

How the ACGEN Tool Works

The previous section covered the tool itself and how to use it to cache one or more methods in an assembly. We are now going to look at the mechanics of the tool and how it is able to generate the cached proxy.

The tool uses a number of steps to examine the original assembly, produce a second assembly (the cached proxy), and insert MSIL (MS Intermediate Language) using Reflection Emit. We will cover these steps in some detail.

Examining the Original Assembly

Using reflection, the ACGEN tool starts by reading the namespace, types, constructors and methods within the original assembly. During the process of reading these details, a second assembly (the cached proxy) is created.

Figure 1. The cached proxy is created by the ACGEN tool

To keep the proxy unique, the second assembly's output DLL name and namespace are prefixed with 'Cached_' (as outlined in the previous section, the tool allows this to be changed).

This allows the end user to reference both the regular and cached versions of the assembly from within Visual Studio .NET, if so desired. It was anticipated that there may be situations where method calls would require caching to be 'turned on and off' in the same application. By renaming the namespace and DLL, we can call both assemblies from the same calling application to achieve this.

If we look at the code required to create the cached proxy, we can observe the following:

AssemblyName outputAssemblyName = new AssemblyName();
outputAssemblyName.Name = SHORT_OUTPUTASSEMBLY_NAME_NO_DLL;
AppDomain currentAppDomain = Thread.GetDomain();

...

outputAssembly = currentAppDomain.DefineDynamicAssembly
   (outputAssemblyName,AssemblyBuilderAccess.Save);


First, we create an assembly name for the cached proxy. With the current AppDomain referenced, we call the DefineDynamicAssembly method to create an assembly using Reflection Emit. Passed to this method are the name of the assembly, and a constant value to indicate we wish to save this output to disk (instead of running the code on the fly).

Once the framework of the assembly has been created, we then need to start iterating through the types in the original assembly. For each type that we find in the original assembly, we create a new type with the same name in the cached proxy.

Creating a New Type in the Cached Proxy

To create a new type in the cached proxy, we use TypeBuilder within Reflection Emit. Using the DefineType method we can create a new type by supplying the name and protection level.

TypeBuilder outputClass = outputModule.DefineType
      (PREFIX+cachedAssemblyType.FullName,TypeAttributes.Public);

When we create a new type we also create two additional field values within the type. These are used as references. One is a reference to the original assembly, the other is a reference to the Cache API. Within Reflection Emit we have access to a FieldBuilder that allows us to create fields on the fly.

FieldBuilder cached_Class = outputClass.DefineField
(SHORT_ASSEMBLY_NAME_NO_DLL,cachedAssemblyType,FieldAttributes.Private);

FieldBuilder cacheEngine = outputClass.DefineField
("cacheEngine",cacheEngineAssemblyType,FieldAttributes.Private);

If we use ILDASM (The MSIL Disassembler) to investigate the Cached_MyStockTicker.dll, we will see the following output:

.namespace Cached_MyStockTicker
{
  .class public auto ansi Service
         extends [mscorlib]System.Object
  {
    .field private class [MyStockTicker]MyStockTicker.Service 
      MyStockTicker
    .field private class [CACHEAPI]CacheAPI.Engine cacheEngine

As shown in the above output, the namespace (Cached_MyStockTicker) has been created with a public class called Service.

Within this class, the MyStockTicker.Service field will be used to reference the original assembly of type MyStockTicker. The CacheAPI.Engine field will be used to reference the Caching API.

Writing the Constructor for the New Cached Type

We are now starting to build up the 'skeletal' new assembly that contains the definition for the new cached type. We now need to start populating this assembly with code. The first part of code to write is the constructor for the new type.

The new constructor must initialize the two fields that we created (that reference the original assembly and the CacheAPI) and make a call to the super constructor (i.e. the constructor of the original assembly).

To do this, we first iterate through all of the constructors in the type (remember that a type can have more than one constructor based on the parameters passed!). For each constructor found in the original type, we use the ConstructorBuilder in Reflection Emit to create a replica constructor in our own cached proxy:

ConstructorBuilder constructor = 
   outputClass.DefineConstructor(MethodAttributes.Public,
      CallingConventions.Standard,ctorParams);

To generate the code to initialize the two fields and to call the super constructor of the original assembly, we need to firstly get an IL generator for the constructor and then Emit the IL we require. To obtain the IL generator, we can call the GetILGenerator method from the constructor itself.

ILGenerator constructorIL = constructor.GetILGenerator();
  

We then have to initialize the first field in the constructor (this is the reference to the type in the original assembly).

constructorIL.Emit(OpCodes.Ldarg_0);
constructorIL.Emit(OpCodes.Newobj,
   cachedAssemblyType.GetConstructor(ctorParams));
constructorIL.Emit(OpCodes.Stfld,cached_Class);

You'll notice that we are calling the constructor in the original assembly with the same parameters that the cached constructor was called with (ctorParams was extracted when we examined the original assembly). This is important, for if the cached assembly is created with parameters in the constructor, we want to create a reference to the original that matches it.

The following code is used to create the reference to the CacheAPI (we refer to it as the Cache Engine in the code) and to call the super constructor of the original assembly.

constructorIL.Emit(OpCodes.Ldarg_0);
constructorIL.Emit(OpCodes.Newobj,
      cacheEngineAssemblyType.GetConstructor(new Type[0]));
constructorIL.Emit(OpCodes.Stfld,cacheEngine);

constructorIL.Emit(OpCodes.Ldarg_0);
ConstructorInfo superConstructor = typeof(Object).GetConstructor(new 
      Type[0]);
constructorIL.Emit(OpCodes.Call,superConstructor);

Using this code produces the following IL:

.method public specialname rtspecialname 
        instance void  .ctor() cil managed
{
  // Code size       29 (0x1d)
  .maxstack  2
  IL_0000:  ldarg.0
  IL_0001:  newobj     instance void
 [MyStockTicker]MyStockTicker.Service::.ctor()
  IL_0006:  stfld      class [MyStockTicker]MyStockTicker.Service
 Cached_MyStockTicker.Service::MyStockTicker
  IL_000b:  ldarg.0
  IL_000c:  newobj     instance void [CACHEAPI]CacheAPI.Engine::.ctor()
  IL_0011:  stfld      class [CACHEAPI]CacheAPI.Engine
 Cached_MyStockTicker.Service::cacheEngine
  IL_0016:  ldarg.0
  IL_0017:  call       instance void [mscorlib]System.Object::.ctor()
  IL_001c:  ret
} // end of method Service::.ctor

As can be shown, both fields are initialized in the constructor of the new type. If we were to compare this to similar commands in C#, this initialization would be similar to:

MyStockTicker Cached_MyStockTicker = new MyStockTicker();
CacheAPI.Engine cacheEngine = new CacheAPI.Engine();

At this point, we now have an assembly declared with types and all of the constructors that these types require. We now need to investigate the methods themselves and create equivalents within the cached proxy using Reflection Emit.

Writing the Methods in the Cached Proxy

Before we look at the code, it is important to understand the logic of each method that we are going to be writing in the cached proxy.

What we will do is create a method in the cached proxy (keeping the same name) for each of the methods in the original assembly. As can be shown in our diagram below, a duplicate GetQuote method will be created using Reflection Emit.

After this is created, we will then populate this method with three steps (in IL) that do the following:

  1. Try the cache to see if it has a return result for the method, based on the parameters passed.
  2. If the cache does not have a return result (an exception will be thrown to determine this), a call will be made to the original method.
  3. The result from the original method will be stored in the cache and return to the calling application.

Figure 2. The schematic for the ''GetQuote' method in the cached proxy

Where (and What) is the Cache ?

The initial thinking behind the design of the tool was to include all of the caching code required. It was thought that this code could be emitted using Reflection.Emit in the same way that the other parts of the assembly were being generated.

After a number of prototypes it became clear that any caching code that was included would not be flexible enough to meet the needs of every problem. For example, some requirements may dictate that the cache is stored in memory to provide a fast, temporary store—others may require that the cache is stored in a more permanent, shared location on a central server. We could never predict every scenario.

To overcome this, we define an interface that could be implemented by anyone. This interface includes the GetFromCache and StoreToCache methods shown in the above diagram, along with the CacheException that is thrown if a value cannot be retrieved from cache. The actual interface is defined as follows:

public abstract object GetFromCache(String key);

public abstract void StoreToCache(String key, object objectToStore,
       long TTL);

public CacheEngineException(String Message);


The GetFromCache call accepts a key (of type String) and must return an object that relates to that key. If no object is available in the cache, a CacheEngineException must be thrown. This exception will instruct the wrapped assembly to make a method call to the original.

Once this call has been made, the result is passed to StoreToCache. StoreToCache accepts a key (to store the result under), the result itself (which is passed as an object) and a TTL (Time To Live) value for this object. Most caches are designed to dispose of old data after a certain period of time—the TTL value allows for this.

Now that we have this interface defined, we can see how the try/catch block fits in:

Figure 3. The schematic for the ''GetQuote' method in the cached proxy showing the call to the Cache

When the method in the cached proxy assembly is called, it first tries to get the result for that method by asking the cache. If no result is found, a call is made to the original method with the same parameters. The result of this call is stored in the cache, and then passed back to the calling application.

Specifying the Key for the Cache

We now have a cache to call, but we need a unique key with which to reference each method.

It was decided that the following items should make up the key for the cache:

  • Strong name of the original assembly
  • Return type of the method
  • Method name
  • Hash code for each passed parameter

The hash code is important if variable names are passed instead of values. For example, when a method is called we can differentiate between:

GetQuote("MSFT") and GetQuote("MYCO")

but we cannot so easily differentiate between

GetQuote(x) and GetQuote(x)

as the values of x may have changed between calls. To overcome this, a hash code is generated for the values of x and is used instead of the values.

Note   The GetHashCode algorithm supplied in .NET doesn't necessarily guarantee uniqueness between values—this was understood, and it was decided that the system was unique enough for our tool. If we were concerned about the uniqueness of this value, we could look at an algorithm which produced a more unique signature for each value.

For our example, a key could look as follows:

[Version MyStockTicker, Version=1.0.646.19029, Culture=neutral, 
   PublicKeyToken=null] Double Service.GetQuote
(2088827544)

The key is made up from the strong name, the return type of the method, the fully qualified method name, and the hash code of the parameters passed (for the example, we're assuming that the text "MSFT" generates a hash code of 2088827544!).

Generating the Cache Key in IL

Now that we have a format for the key, we need emit the IL to construct the key for this method, and the method itself.

We start by creating the method itself:

MethodBuilder outputMethod = 
   outputClass.DefineMethod(cachedAssemblyMethod.Name,
      MethodAttributes.Public,outputReturnType,reqdParams);
                  

As with the constructor, we use the ILGenerator to start emitting IL to the cached proxy:

ILGenerator outputIL = outputMethod.GetILGenerator();

We then declare a number of local variables that are going to be used. The first local is used for building up the cache key itself, the second and third are used to store the object returned from the cache and the original assembly respectively. The fourth is used for storing the hash code for each of the parameters, and the fifth is used for a string representation of this value.

outputIL.DeclareLocal(typeof(string));
outputIL.DeclareLocal(typeof(System.Object));
outputIL.DeclareLocal(outputReturnType);
outputIL.DeclareLocal(typeof(int));
outputIL.DeclareLocal(typeof(string));

We can build up the first part of the Cache Key (known in the code as cachedMethodID) as follows:

String cachedMethodID = cachedAssembly.FullName+" ";
cachedMethodID += outputReturnType+"    
   "+cachedAssemblyType.Name+"."+cachedAssemblyMethod.Name+"(";

outputIL.Emit(OpCodes.Ldstr,cachedMethodID);
outputIL.Emit(OpCodes.Stloc_0);


For our example, this would give us the following in IL:

.method public instance float64  GetQuote(string A_1) cil managed
{
  // Code size       157 (0x9d)
  .maxstack  8
  .locals init (string V_0,
           object V_1,
           float64 V_2,
           int32 V_3,
           string V_4)
  IL_0000:  ldstr      "MyStockTicker, Version=1.0.766.37543, 
      Culture=neut"
  + "ral, PublicKeyToken=null System.Double Service.GetQuote("
  IL_0005:  stloc.0

This gives us the first part of the key.

Obviously, the values of the parameters passed to this method are not going to be known until runtime—therefore we can't specify them when we are emitting the assembly. However we do know how many values are being passed to this method (in our GetQuote example, we only have one).

Therefore, we iterate through the parameters at runtime, get the values passed, generate the hash code for these values and append the values to the Cache Key.

For each parameter, we call a block of IL code to add the hash code to the key. This is emitted with the following block of code:

outputIL.Emit(OpCodes.Ldloc_0);
outputIL.Emit(OpCodes.Ldarg_S,b);
outputIL.Emit(OpCodes.Callvirt,
      typeof(System.Object).GetMethod("GetHashCode"));

outputIL.Emit(OpCodes.Stloc_3);
outputIL.Emit(OpCodes.Ldloca_S,3);
outputIL.Emit(OpCodes.Callvirt,
      typeof(System.Int32).GetMethod("ToString",new Type[]{}));

outputIL.Emit(OpCodes.Stloc_S,4);         
outputIL.Emit(OpCodes.Ldloc_S,4);
outputIL.Emit(OpCodes.Call,typeof(System.String).GetMethod("Concat",
      new Type[]{typeof(String),typeof(String)}));

outputIL.Emit(OpCodes.Stloc_0);

The above code loads the current Cache Key string (ldloc_0) and the n'th argument that has been passed (we are iterating through the parameters—this is part of a for loop) and generates a hash code for this parameter. This has code is stored into the third local, and a ToString method stores a string representation of it into the fourth. This string representation is then concatenated with the original value that was loaded at the start.

After each of the values a comma is added to break up the parameters in the cache key. If the final parameter is being dealt with, a closed bracket is added instead. If we were to look at the IL generated by these Reflection Emit statements, we would see the following:

      
  IL_0006:  ldloc.0
  IL_0007:  ldarg.s    A_1
  IL_000c:  callvirt   instance string 
      [mscorlib]System.Object::ToString()
  IL_0011:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0016:  stloc.0
  IL_0017:  ldloc.0
  IL_0018:  ldarg.s    A_1
  IL_001d:  callvirt   instance int32 
      [mscorlib]System.Object::GetHashCode()
  IL_0022:  stloc.3
  IL_0023:  ldloca.s   V_3
  IL_0028:  callvirt   instance string 
      [mscorlib]System.Int32::ToString()
  IL_002d:  stloc.s    V_4
  IL_0032:  ldloc.s    V_4
  IL_0037:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_003c:  stloc.0
  IL_003d:  ldloc.0
  IL_003e:  ldstr      ")"
  IL_0043:  callvirt   instance string 
      [mscorlib]System.Object::ToString()
  IL_0048:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_004d:  stloc.0
 

To summarize, we wrote the IL to first load the strong name, return type and method name in to the key—then iterate through each of the parameters passed to the method, appending the hash code value of each to the key as required.

This gives us the unique key that is critical to making the cache work.

Calling the Caching API

We now have a unique key for the method being generated and the parameters that it has been called with. We now need to call the caching API with this key.

To do this in IL, we start by defining the required try block. This is done with the BeginExceptionBlock method:

outputIL.BeginExceptionBlock();

We now need to call the GetFromCache method in the Cache API. We use the private field reference to the CacheAPI (defined in the type) to call this:

outputIL.Emit(OpCodes.Ldarg_0);
outputIL.Emit(OpCodes.Ldfld,cacheEngine);
outputIL.Emit(OpCodes.Ldloc_0);
outputIL.Emit(OpCodes.Callvirt,
   cacheEngineAssemblyType.GetMethod("GetFromCache"));

As shown above, we have used the OpCodes.Callvirt opcode in order to call the required method. Before we store the result in the local we need to do two operations.

The GetFromCache method returns a value of type System.Object—before we can store this in a local register of type System.Double we must first convert it. We use the UnBox OpCode to first convert the value type. We then make a call to a ConvertType function (which is listed at the bottom of the acgenengine.cs source code). This function makes sure it is in the correct format on the evaluation stack.

Once this is complete, we can use the OpCode.Stloc_2 command to store the value in the local register.

BoxTypeOpCode(outputIL,OpCodes.Unbox,outputReturnType);
ConvertType(outputIL,outputReturnType);
OutputIL.Emit(OpCodes.Stloc_2);

If we look at the IL generated for the above commands, we get the following:

.try
  {
    IL_004e:  ldarg.0
    IL_004f:  ldfld      class [CacheAPI]CacheAPI.Engine
Cached_MyStockTicker.Service::cacheEngine
    IL_0054:  ldloc.0
    IL_0055:  callvirt   instance object
[CacheAPI]CacheAPI.Engine::GetFromCache(string)
    IL_005a:  unbox      [mscorlib]System.Double
    IL_005f:  ldind.r8
    IL_0060:  stloc.2
    IL_0061:  leave      IL_0099
  }  // end .try

The end .try IL output is automatically generated when we start outputting the catch block using Reflection Emit.

The Catch Block

The code in the catch block is executed if no matching value was found in the cache (i.e. a cache miss occurred). This code needs to do two operations—call the method in the original assembly (with of course the same parameter values as were passed to this method) and store this value in the cache before returning.

The call to the method in the original assembly is constructed using a similar process to when we generated the key for the cache. We know how many parameters to pass, but we don't know the values of these parameters until runtime.

The BeginCatchBlock method is used to start the catch.

outputIL.BeginCatchBlock(typeof(System.Exception));

We then iterate through the parameters required to call the original method and load them from locals within IL.

outputIL.Emit(OpCodes.Ldarg_0);
outputIL.Emit(OpCodes.Ldfld,cached_Class);

if (reqdParams.Length != 0)
{
   for (int c=1; c<=reqdParams.Length; c++)
   {
      outputIL.Emit(OpCodes.Ldarg_S,c);
   }
}

Once these are loaded, we then make the call to the original assembly, and store the result returned in the defined local for later use.

outputIL.Emit(OpCodes.Callvirt,
   cachedAssemblyType.GetMethod(cachedAssemblyMethod.Name));
outputIL.Emit(OpCodes.Stloc_2);

These commands produce IL that looks similar to the following:

  catch [mscorlib]System.Exception 
  {
    IL_0066:  pop
    IL_0067:  ldarg.0
    IL_0068:  ldfld      class [MyStockTicker]MyStockTicker.Service 
Cached_MyStockTicker.Service::MyStockTicker
    IL_006d:  ldarg.s    A_1
    IL_0072:  callvirt   instance float64 
[MyStockTicker]MyStockTicker.Service::GetQuote(string)
    IL_0077:  stloc.2

We now have the result after calling the method in the original assembly.

Before returning the value to the caller, we need to store this result in the cache for possible use later. We need to make a call to the StoreToCache method in the CacheAPI.

To start, we load the reference to the cache engine, and the two of the locals we require to pass to the StoreToCache method—the first local is the Cache Key, the second is the actual object itself (that was returned from the above call to the original assembly).

outputIL.Emit(OpCodes.Ldfld,cacheEngine);
outputIL.Emit(OpCodes.Ldloc_0);
outputIL.Emit(OpCodes.Ldloc_2);               
BoxTypeOpCode(outputIL,OpCodes.Box,outputReturnType);

Before we pass the value to the StoreToCache method, we need to box it (makes a copy of the value type for use in the object). To achieve this in the code sample, we use another function that emits the correct box command based on the type of the value.

We then need to load the TTL value (this is calculated based on what was passed to the tool)—and convert it to an Int64 value (The StoreToCache interface defines that the TTL has to be an Int64 value type).

outputIL.Emit(OpCodes.Ldc_I4,TTL);
outputIL.Emit(OpCodes.Conv_I8);


We are now ready to call the StoreToCache method. This is done with the following Callvirt IL command:

outputIL.Emit(OpCodes.Callvirt,
   cacheEngineAssemblyType.GetMethod("StoreToCache"));  

…and the catch block can be finalized.

outputIL.EndExceptionBlock();

If we were to look at the IL generated, we should see something similar to:

    IL_0078:  ldarg.0
    IL_0079:  ldfld      class [CacheAPI]CacheAPI.Engine 
Cached_MyStockTicker.Service::cacheEngine
    IL_007e:  ldloc.0
    IL_007f:  ldloc.2
    IL_0080:  box        [mscorlib]System.Double
    IL_0085:  ldc.i4     0x2710
    IL_008a:  nop
    IL_008b:  nop
    IL_008c:  nop
    IL_008d:  nop
    IL_008e:  conv.i8
    IL_008f:  callvirt   instance void 
[CacheAPI]CacheAPI.Engine::StoreToCache(string, object, int64)
    IL_0094:  leave      IL_0099
  }  // end handler

Once the StoreToCache method has been called, we can exit the catch block. All we need to do now is to return the value to the caller. We use the following code to clean up and return the value to the calling application.

Label jumpToLabel5 = outputIL.DefineLabel();
outputIL.Emit(OpCodes.Br_S,jumpToLabel5);
outputIL.MarkLabel(jumpToLabel5);

outputIL.Emit(OpCodes.Ldloc_2);
outputIL.Emit(OpCodes.Ret);

This produces the following IL to end the method we have created in the cached proxy:

  IL_0099:  br.s       IL_009b
  IL_009b:  ldloc.2
  IL_009c:  ret
} // end of method Service::GetQuote

Once all methods and types have been iterated, we 'bake' the assembly with the CreateType method:

outputClass.CreateType();

We can then save the assembly to disk and exit the tool.

AssemblyBuilder savedAssembly = (AssemblyBuilder)outputAssembly;
savedAssembly.Save(SHORT_OUTPUTASSEMBLY_NAME);

Conclusion

Small Extract

This article shows only a small portion of the code of the ACGEN tool, specifically the parts that use Reflection Emit. The source code for the ACGEN tool makes use of several parts of the .NET Framework, which can be examined offline.

Using Reflection Emit as a Solution for Similar Problems

The main purpose of this article is to introduce some of the functionality provided by the Reflection Emit namespace in .NET.

One of the key success factors of this tool is the ability to abstract the actual caching algorithms from the code that performs the Reflection Emit/IL output. A developer can write a completely alternative caching algorithm, and providing it exposes the same interface, it will work with no problems.

This leads to thoughts about other uses of the application, effectively using Reflection Emit to 'intercept' method calls in other assemblies. This could be a powerful tool. A few examples of this might include: making a call to an API that monitors time taken to run a particular piece of code (i.e. a profiling utility); an extension of this could be to log each method call to provide a detailed overview of methods called, and parameters that were passed. The number of methods called by our client application could be compared to the total number of methods in a particular assembly—giving a 'coverage analysis' for testing purposes.

This article and its caching example offer insight into the uses of Reflection and Reflection Emit to help you create your own powerful and flexible applications in .NET.

Show:
© 2014 Microsoft