Chapter 10. Accessing Coherence from .NET

The material we have covered in the previous chapter applies equally to all Coherence*Extend clients, regardless of the platform. However, each platform also has its own idiosyncrasies and conventions, which affect both the API and the client-side configuration to a certain extent. In this chapter we will discuss these differences when it comes to .NET and teach you how to access Coherence from .NET applications.

If you are a Java developer not interested in writing .NET applications, you can safely skip this chapter. However, if you are a .NET developer trying to learn Coherence, keep in mind that the following sections will likely prove to be inadequate if you dive directly into them without reading the prerequisite material in the preceding chapters first.

Coherence, when used properly, is a major foundational component of the application. It is important to understand its role in the overall architecture, as well as its feature set, in order to get the most from the following sections.

Now that you've been warned, let's dig in.

.NET client configuration

One of the first questions you are likely to have when you try to implement a Coherence client in .NET is how to configure everything—how to let .NET application know which operational, cache, and POF configuration files to use.

If you come from the Java world, you are used to placing configuration files with well-known names into a classpath, or telling Coherence explicitly where to find them by setting appropriate system properties. However, neither the classpath nor system properties exist in .NET, so we obviously need a different approach.

The good news is that there are several approaches to .NET client configuration, and which one is most appropriate for your situation will depend mostly on the type of application you are building. For example, while placing files with well-known names into the application directory or specifying them explicitly in the application configuration file might work for a desktop or ASP.NET web application, it is not appropriate for a Microsoft Excel add-in that needs to access Coherence. In the latter case, assembly embedded resources and programmatic configuration are much more appropriate.

We will discuss each of these options in detail, but before we do that let's take a look at how Coherence configuration files in .NET differ from their Java counterparts and create the sample files we will reference throughout the remainder of this chapter.

Coherence configuration files in .NET

Probably the biggest difference between Java and .NET configuration files is that the former use DTD for validation, while the latter use XML Schema. The main reason for this is that Microsoft Visual Studio, by far the most commonly used .NET IDE, can only provide IntelliSense for XML Schema-based XML files.

Note

Enabling Visual Studio IntelliSense for Coherence configuration files

If you installed Coherence for .NET using the Oracle-provided installer, the XML Schema files necessary for the Intellisense support should have been copied to the appropriate Visual Studio directory automatically. However, if for some reason IntelliSense doesn't work when you are editing Coherence configuration files in Visual Studio, you can enable it manually.

In order to do that, you need to copy all three schema files (coherence.xsd, cache-config.xsd, and coherence-pof-config.xsd) from the config directory of your Coherence for .NET installation to the Xml/Schemas subdirectory of your Visual Studio installation.

Other than that, you will likely notice that you don't have as many configuration options in .NET as you do in Java. For example, out of the myriad of cache schemes available in the Java cache configuration file, you will only be able to configure local, near, and remote schemes in .NET—this makes perfect sense considering the fact that a .NET application is always a Coherence*Extend client and never a full-blown member of the cluster.

So, without any further ado, let's take a look at different configuration artifacts in .NET.

Operational descriptor

The .NET operational descriptor is a significantly trimmed-down version of the Java operational descriptor. Because most of the operational settings are cluster-related, they do not apply to the .NET client, so the only things you can configure in the .NET operational descriptor are the logger, default cache factory, and the network filters.

For details on how to configure the logger please refer to the Coherence User's Guide, as there are several options available, including the ability to use one of the popular open source logging frameworks, such as Log4Net, NLog, or Logging Application Block from the Microsoft Enterprise Library by the way of Common.Logging abstraction library.

If you want to use a custom implementation of the configurable cache factory you can specify it within the configurable-cache-factory-config element. However, this is a fairly advanced feature and more likely than not you will never need to use it.

That leaves us with the network filters as the last thing we can configure, so let's see how we can register the PasswordBasedEncryptionFilter we created earlier:

<coherence xmlns="http://schemas.tangosol.com/coherence">
<cluster-config>
<filters>
<filter>
<filter-name>symmetric-encryption</filter-name>
<filter-class>
PasswordBasedEncryptionFilter, Coherence.Encryption
</filter-class>
<init-params>
<init-param id="1">
<param-name>password</param-name>
<param-value>my!paSSwoRD</param-value>
</init-param>
</init-params>
</filter>
</filters>
</cluster-config>
<logging-config>
...
</logging-config>
</coherence>

As you can see, the filter registration is very similar to the one in Java. The only major differences are the namespace declaration within the root element and how the class name is specified. The former is simply the side effect of a switch from a DTD to an XML Schema-based validation, while the latter reflects the difference in class loading mechanisms in Java and .NET.

On a .NET client, you need to include the assembly name the class should be loaded from in addition to the fully qualified class name. In the previous example, PasswordBasedEncryptionFilter class will be loaded from the Coherence.Encryption assembly. This rule will apply whenever a class name needs to be specified in one of the configuration files for a .NET application.

Cache configuration

As I mentioned earlier, you will only be able to configure a subset of the cache schemes on a .NET client, but this simply reflects the fact that not all of the cache schemes that are available within the cluster make sense on a Coherence*Extend client. For example, it doesn't make any sense to configure a replicated or partitioned scheme on the client.

The schemes that you can configure are local, near, and remote cache schemes, as well as the remote invocation service. We have already discussed how to configure each of these in the preceding sections, so let's just take a look at the sample .NET cache configuration file.

<cache-config xmlns="http://schemas.tangosol.com/cache">
<caching-scheme-mapping>
<cache-mapping>
<cache-name>*</cache-name>
<scheme-name>extend-tcp</scheme-name>
</cache-mapping>
</caching-scheme-mapping>
<caching-schemes>
<remote-cache-scheme>
<scheme-name>extend-tcp</scheme-name>
<service-name>ExtendTcpCacheService</service-name>
<initiator-config>
<tcp-initiator>
<remote-addresses>
<socket-address>
<address>localhost</address>
<port>9099</port>
</socket-address>
</remote-addresses>
<connect-timeout>30s</connect-timeout>
</tcp-initiator>
<outgoing-message-handler>
<heartbeat-interval>1s</heartbeat-interval>
<heartbeat-timeout>10s</heartbeat-timeout>
<request-timeout>30s</request-timeout>
</outgoing-message-handler>
<use-filters>
<filter-name>symmetric-encryption</filter-name>
</use-filters>
</initiator-config>
</remote-cache-scheme>
</caching-schemes>
</cache-config>

All of this should be familiar by now, aside from the namespace declaration for the root element, so we will not spend any more time discussing it.

POF configuration

Apart from the namespace declaration and the differences in how class names are specified, the POF configuration file is pretty much identical to the one in Java.

<pof-config xmlns="http://schemas.tangosol.com/pof">
<user-type-list>
<include>
assembly://Coherence/Tangosol.Config/coherence-pof-config.xml
</include>
<user-type>
<type-id>1000</type-id>
<class-name>
BankTerminal.Domain.Account, BankTerminal
</class-name>
</user-type>
<user-type>
<type-id>1001</type-id>
<class-name>
BankTerminal.Domain.Transaction, BankTerminal
</class-name>
</user-type>
</user-type-list>
</pof-config>

The important thing is that the type identifiers match the ones configured within the cluster, and that you include the standard coherence-pof-config.xml file, which contains definitions for all of the built-in Coherence types that are required either for the Extend protocol itself or for the Coherence API, such as built-in filters, extractors, and aggregators.

You have probably noticed that the include element's contents is somewhat different and are probably wondering what this assembly:// thing is all about, so it is a good time to discuss the resource loading mechanism in Coherence for .NET.

Resource loading in Coherence for .NET

Unlike Java, .NET doesn't have the notion of a classpath, so it is not possible to simply package standard configuration files into an assembly and let the class loader resolve them at runtime.

To complicate things even further, there is no single best place where configuration files should be located. For desktop and web applications it probably makes most sense to store them on the file system, within the application directory, but for other types of applications you might need to embed them into the application assembly.

In order to support a variety of possible sources for configuration files and other resources, Coherence for .NET introduces a higher-level abstraction for resource resolution in the form of the Tangosol.IO.Resources.IResource interface and Tangosol.IO.Resources.ResourceLoader class.

Resource abstraction

The IResource interface represents any resource that an input stream can be obtained from. There are four IResource implementations that ship with Coherence for .NET:

Class Name

Description

FileResource

Provides access to files on a file system, and is the default resource type in all applications except for ASP.NET web applications.

WebResource

Provides access to files within a web application and is the default resource type in ASP.NET web applications.

EmbeddedResource

Provides access to assembly-embedded resources.

UrlResource

Provides access to HTTP or FTP resources.

While most of the resource implementations mentioned previously should be fairly obvious, the distinction between the file and web resources is not so clear and deserves further discussion. Both file and web resources effectively represent physical files on disk, but they behave quite differently when resolving paths.

The FileResource will resolve paths relative to the current working directory, which can lead to some not so pleasant surprises if used in ASP.NET web applications. The problem is that the current working directory for an ASP.NET application is the directory IIS was started from, typically C:WindowsSystem32, which is probably not where you intend to put configuration files for your application.

The WebResource, on the other hand, resolves paths relative to the path of the current HTTP request, but you can force it to resolve them against the application root directory by specifying ~ as a path prefix, as we'll see shortly.

The bottom line is that you should never use FileResource within a web application, as paths are likely not going to be resolved the way you expect.

Protocols and resource loader

The ResourceLoader is responsible for loading resources using the appropriate IResource implementation based on the URI-like resource name. It uses the protocol component of the resource name to determine which IResource implementation should be used:

Protocol

Resource

Examples

file

FileResource

file://config/my-pof-config.xml file://c:/config/my-cache-config.xml c:MyDircoherence.xml (if file is default)

web

WebResource

web://config/my-pof-config.xml (relative to current) web://~/config/my-pof-config.xml (relative to root) ~/my-pof-config.xml (if web is default)

assembly asm

EmbeddedResource

assembly://MyAssembly/My.Name.Space/pof.xml

http ftp

UrlResource

http://config.mycompany.com/my-pof-config.xml ftp://user:[email protected]/config/pof.xml

The assembly://Coherence/Tangosol.Config/coherence-pof-config.xml reference in our POF configuration file should make more sense now, but the knowledge of the resource abstraction and the resource loading mechanism will also come in handy in the next section, when we look into various approaches to .NET client configuration.

Approaches to .NET client configuration

As I mentioned earlier, different application types require different configuration approaches, so Coherence for .NET supports three: convention-based configuration, explicit configuration within the standard .NET configuration files, such as Web.config or App.config, and finally, programmatic configuration.

In this section we will look into all three approaches and discuss when each one is appropriate.

Convention-based configuration

The convention-based configuration mechanism depends on files with well-known names being placed in the application directory, and works in both desktop and ASP.NET web applications. It is by far the easiest way to configure the Coherence for .NET client and should generally be preferred to other mechanisms.

In order to use it, you need to create the following files:

  • coherence.xml: This is for the operational descriptor

  • coherence-cache-config.xml: This is for the cache configuration file

  • coherence-pof-config.xml: This is for the POF configuration file

Coherence for .NET client library will automatically detect these files on startup and use them to configure itself, unless they are overridden using one of the following two mechanisms.

Explicit configuration

In some cases it might make sense to place configuration files in a shared directory on the network, or to use files with different names. For example, you might need to keep separate sets of configuration files for development, testing, and production.

If you need to do that, you will have to configure the Coherence for .Net client explicitly. This can be accomplished in two ways: either programmatically, which we will discuss in the next section, or using the standard .NET configuration mechanism, and specifying configuration file names within the App.config or Web.config file.

In order to achieve the latter, you need to register a custom configuration section handler within your main application configuration file, and add a coherence section to it:

<configuration>
<configSections>
<section name="coherence"
type="Tangosol.Config.CoherenceConfigHandler, Coherence"/>
</configSections>
<coherence>
<cache-factory-config>
Configcoherence-dev.xml
</cache-factory-config>
<cache-config>
Configcache-config-dev.xml
</cache-config>
<pof-config>
Configpof-config-dev.xml
</pof-config>
</coherence>
</configuration>

In the previous example, we are using custom file names and are loading them from a Config subdirectory within the main directory of our application.

The previous example will work fine for desktop applications, as the paths specified will be resolved by the FileResource, relative to the directory where the application executable is located.

However, in a web application the paths would be resolved relative to the request path, which may or may not be what you expected. To eliminate the guesswork and the possibility for error from the equation, you should always force resolution against the root directory of a web application by prefixing paths with a tilde character:

<coherence>
<cache-factory-config>
web://~/Config/coherence.xml
</cache-factory-config>
<cache-config>
web://~/Config/cache-config.xml
</cache-config>
<pof-config>
web://~/Config/pof-config.xml
</pof-config>
</coherence>

The usage of the web:// prefix in the previous code snippet is optional, because WebResource is the default in ASP.NET applications. That said, it is the best practice to specify it, in order to eliminate any guesswork on the part of an unsuspecting reader.

Programmatic configuration

Finally, in some cases neither of the approaches previously mentioned will work. One of the common examples is a Microsoft Office add-in that depends on Coherence for .NET client library. The problem with an Office add-in is that in order to use the default configuration, you would have to place the configuration files within the directory where the actual Office application's executable is located, which is not very practical. You also don't have the option of specifying paths to configuration files explicitly, as there is no .NET configuration file to speak of.

Fortunately, the solution is quite simple&mdash;you can embed configuration files into your add-in's assembly, and configure Coherence client programmatically during add-in initialization:

const string baseUrl = "assembly://CoherenceRTD/CoherenceRTD.Config/";
CacheFactory.SetCacheFactoryConfig(baseUrl + "coherence.xml");
CacheFactory.SetCacheConfig(baseUrl + "cache-config.xml");
CacheFactory.SetPofConfig(baseUrl + "pof-config.xml");

In this example, we are loading the embedded resources coherence.xml, cache-config.xml, and pof-config.xml, from a CoherenceRTD.Config namespace in a CoherenceRTD assembly.

Note

Coherence integration with Excel

The configuration example mentioned previously is actually taken from a proof-of-concept for an Excel RTD Server that uses Coherence as a data source, originally created by Dave Felcey from Oracle and myself, and subsequently significantly improved by Dave.

While not the focus of this section, this integration is definitely very interesting. I guess there is just something geeky cool about an Excel worksheet and graphs being updated in real-time as the data in the Coherence cluster changes.

Dave was kind enough to provide both the final demo and the instructions for its usage on his blog at http://blogs.oracle.com/felcey/, so you should definitely check it out if Excel integration with Coherence is something you are interested in.

That concludes the section on Coherence for .NET client configuration. It's time to roll up the sleeves and write some real code.

Implementing the client application

In the remainder of this chapter we will do a quick run-down through Coherence for .NET API and cover pretty much the same topics and in the same order as we did in Chapters 2 through 7.

Even if you are not particularly interested in .NET, this should provide a quick refresher of the topics we have covered so far, so you should probably not skip this section. You will also see that C# is very much like Java and that the Coherence for .NET API is very similar to the Coherence Java API, so following the examples should be fairly straightforward.

So let's start from the beginning…

Basic Cache Operations

In order to obtain a reference to a named cache, you need to use the Tangosol.Net.CacheFactory class and invoke the GetCache method on it:

INamedCache cache = CacheFactory.GetCache("countries");

Once you have a cache instance, you will probably want to put some objects into it and try to read them back. Similar to how NamedCache interface extends java.util.Map, the INamedCache interface extends the standard .NET System.Collections.IDictionary interface.

That means that you can use standard members defined by the IDictionary interface, such as Count, Add, Remove, and Clear, as well as the indexer, to manipulate data in the cache:

cache.Add("SRB", "Serbia");
cache.Add("USA", "United States");
cache["USA"] = "United States of America";
cache.Remove("SRB");
Console.WriteLine("USA = " + cache["USA"]);
Console.WriteLine("Cache size = " + cache.Count);
cache.Clear();

Following the IDictionary contract, the Add method will throw an exception if you attempt to add an object with a key that already exists in the cache, so in order to replace an existing entry you need to use an indexer.

Alternatively, you can use one of the methods defined in the Tangosol.Net.ICache interface, which extends IDictionary:

public interface ICache : IDictionary
{
object Insert(object key, object value);
object Insert(object key, object value, long millis);
void InsertAll(IDictionary dictionary);
IDictionary GetAll(ICollection keys);
}

The first three methods behave the same way put and putAll do in Java&mdash;they allow you to both insert new entries and overwrite the existing ones. You also have an option of specifying an expiration time for an entry in milliseconds, or batching multiple entries into a dictionary instance and inserting them into a cache in a single call, which can provide significant performance boost.

Similarly, the GetAll method allows you to retrieve multiple entries by key in a single network call, thus significantly improving read performance.

Now that we know how to put objects into the cache and get them back, it is time to implement some Coherence-friendly data objects.

Implementing data objects

In order for the most of the Coherence features to work, your .NET data objects will have to use POF as a serialization format and will probably need to have a parallel Java implementation that is available within the cluster.

While you can technically use .NET binary, or even XML serialization for your data objects, this is strongly discouraged in all but the simplest cases and is a feature that should only be used in early prototyping, if ever, in my opinion.

The problem with non-portable serialization format is that there is no way for a Coherence cluster node to deserialize them, which eliminates most of the cluster-side functionality: you loose the ability to execute queries, aggregators and entry processors, and pretty much turn an extremely powerful piece of software into a distributed hashtable. You will be able to do gets and puts, but unfortunately, that is pretty much all you'll be able to do.

The situation with parallel implementations of Java and .NET classes is somewhat trickier. Up until Coherence 3.5, Java classes were absolutely required if you wanted to do any cluster-side processing, including queries, aggregations, entry processors, and so on.

However, as of Coherence 3.5 this is not strictly necessary, as you can use PofExtractor when creating indexes and executing queries or aggregations. Because PofExtractor works directly with binary data and does not need to deserialize your objects within the cluster, you can get by without the Java classes. You can even use PofUpdater to update binary data within the cluster directly.

That said, if you have the necessary expertise and don't mind a bit of extra work, my opinion is that implementing parallel class hierarchies is still the best approach, for several reasons.

For one, if you want to implement data affinity or to persist cached objects using the read/write-through approach, you will need to have Java classes, as those features depend on them.

It is also much easier to work with strongly typed Java classes within your entry processors and aggregators than with binary data, even though the latter is entirely possible using PofValue, a low-level feature of Coherence 3.5 that both PofExtractor and PofUpdater are based on, but which is also available for direct consumption within your own code.

Finally, having Java classes within the cluster makes debugging much easier, and that is likely where a lot of the activity on a typical project will be spent. Investing some time up-front to create the necessary classes will pay for itself many times over during the course of the project.

With that out of the way, let's see how we can make .NET classes portable and be able to use Coherence to the fullest extent possible.

Implementing the IPortableObject interface

Just as in Java, there are two possible approaches to making your objects portable. The first one is to implement the Tangosol.IO.POF.IPortableObject interface directly within your class:

public class Customer : IPortableObject
{
// ---- data members -------------------------------------
private long id;
private String name;
private DateTime dateOfBirth;
// ---- constructors -------------------------------------
public Customer()
{
// deserialization constructor
}
public Customer(long id, String name, DateTime dateOfBirth)
{
...
}
// ---- properties omitted for brevity -----------------
// ---- IPortableObject implementation -------------------
public virtual void ReadExternal(IPofReader reader)
{
id = reader.ReadInt64(0);
name = reader.ReadString(1);
dateOfBirth = reader.ReadDateTime(2);
}
public virtual void WriteExternal(IPofWriter writer)
{
writer.WriteInt64( 0, id);
writer.WriteString( 1, name);
writer.WriteDateTime(2, dateOf�Birth�);
}
// ---- Equals, GetHashCode and ToString (omitted) -------
}

As you can see, implementing IPortableObject directly is very straightforward, so let's move on to the second approach.

Implementing the external serializer

The second approach is to create a separate class that implements a Tangosol.IO.Pof.IPofSerializer interface and associate it with your data class within the POF context. To complete the EnumPofSerializer example from Chapter 4, Implementing Domain Objects, here is the implementation of its .NET counterpart:

public class EnumPofSerializer : IPofSerializer
{
public void Serialize(IPofWriter writer, object o)
{
if (o == null || !o.GetType().IsEnum)
{
throw new ArgumentException(
"EnumPofSerializer can only be used to serialize enum types.");
}
writer.WriteString(0, o.ToString());
writer.WriteRemainder(null);
}
public object Deserialize(IPofReader reader)
{
IPofContext pofContext = reader.PofContext;
Type enumType = pofContext.GetType(reader.UserTypeId);
if (!enumType.IsEnum)
{
throw new ArgumentException(
"EnumPofSerializer can only be used to deserialize enum types.");
}
object enumValue = Enum.Parse(enumType, reader.ReadString(0));
reader.ReadRemainder();
return enumValue;
}
}

Again, the previous code snippet should be very easy to follow and feel familiar if you remember everything you read about POF in Chapter 4.

With serialization out of the way and portable objects in the cache, let's see how we can run the queries from a .NET client.

Executing queries

Executing queries from a Coherence for .NET client is very similar to executing queries from a Java application: you simply create a filter and execute it using one of the methods defined by the Tangosol.Net.Cache.IQueryCache interface:

public interface IQueryCache : ICache
{
object[] GetKeys(IFilter filter);
object[] GetValues(IFilter filter);
object[] GetValues(IFilter filter, IComparer comparer);
ICacheEntry[] GetEntries(IFilter filter);
ICacheEntry[] GetEntries(IFilter filter, IComparer comparer);
void AddIndex(IValueExtractor extractor, bool isOrdered,
IComparer comparer);
void RemoveIndex(IValueExtractor extractor);
}

As you can see, in addition to methods that allow you to retrieve keys and entries based on a filter, the Coherence for .NET library also provides two overloads of the GetValues method, which are not present in the Java API at the moment.

Methods that accept an IComparer instance as a second argument will sort the results before returning them, either using a specified comparer instance, or in their natural ordering, if null is specified as the second argument and the values within the result implement the IComparable interface. The sorting is performed on the client, so asking for the sorted results will not put any additional load on the cache server nodes.

The only exception to the previously mentioned scenario is the LimitFilter, which will have to perform the sorting within the cluster in order to determine the first page of the results to return. This is a quite expensive operation in a distributed environment, so you should avoid it if possible, or look for an alternative solution to the problem. Using Coherence Tools TopAggregator is one possible approach you should consider if cluster-side sorting is required.

Implementing filters and value extractors

Just as in Java, you need to use value extractors when defining your filters. All of the filters and value extractors described in Chapter 5, Querying the Data Grid, are present in Coherence for .NET as well, so you can use them just as easily as you would in Java.

However, there are a few important things you need to understand, especially if you will be writing your own IFilter or IValueExtractor implementations.

For one, filters and value extractors can execute either within the cluster or on the client, depending on the type of the cache they are used with. If you issue a query against a remote cache, the filter and any value extractors that are part of its state will be serialized and transmitted to the proxy server for execution. The proxy server will then deserialize the filter into the appropriate Java implementation and execute it against the cache servers.

On the other hand, filters that execute against a local or a continuous query cache (unless the CQC is used to cache only keys but not the values) will be evaluated directly on the client, as the scope of the query is well defined and all the objects are already present on the client. This can significantly improve performance and reduce the load on the cache servers.

However, in order for this to work, both the filters and value extractors must have parallel implementations in Java and .NET. The standard filters are all implemented this way, but if you implement your own you will have to ensure that the .NET and Java implementations match and that both are properly registered within the corresponding POF configuration files, using the same type identifier.

In the remainder of this section, we will implement in C# the PropertyExtractor and the StartsWithFilter we have already implemented in Java in Chapter 5. This should provide you with enough guidance to make the implementation of your own filters and extractors a breeze.

Implementing PropertyExtractor in C#

As I mentioned in Chapter 5, the main reason I dislike a built-in ReflectionExtractor is that it requires you to specify the full name of the accessor method. While this is bearable in Java, it feels really unnatural when working in C#, where properties are a first-class language construct. It also leaks to the very surface the fact that the class the extractor will ultimately be executed against is written in Java, which is not a good thing.

However, what's even worse is that the approach is different depending on whether the extraction needs to be performed locally or within the cluster. For example, when extracting values from the objects in a continuous query cache, you need to specify the .NET property name:

IValueExtractor ext = new ReflectionExtractor("Address");

However, when using the same extractor within the cluster you will have to specify the name of the Java accessor method:

IValueExtractor ext = new ReflectionExtractor("getAddress");

Having to write different code based on the type of cache it will be executed against is never a good thing, as it can lead to nasty bugs if you change cache configuration during development. I personally wish that we had spotted and corrected this problem before releasing Coherence for .NET, but better late than never.

Now that we have PropertyExtractor on the Java side, all we need to do is to implement its .NET equivalent, which will make this issue go away:

public class PropertyExtractor
: AbstractExtractor, IValueExtractor, IPortableObject
{
// ---- data members -------------------------------------
private const BindingFlags BINDING_FLAGS =
BindingFlags.Public
| BindingFlags.Instance
| BindingFlags.IgnoreCase;
private String m_propertyName;
[NonSerialized]
private PropertyInfo m_property;
// ---- constructors -------------------------------------
public PropertyExtractor()
{}
public PropertyExtractor(String propertyName)
: this(propertyName, VALUE)
{}
public PropertyExtractor(String propertyName, int target)
{
if (String.IsNullOrEmpty(propertyName))
{
throw new ArgumentNullException(
"propertyName", "Property name cannot be null");
}
m_propertyName = propertyName;
m_target = target;
}
// ---- IValueExtractor implementation -------------------
public override Object Extract(Object target)
{
if (target == null)
{
return null;
}
Type targetType = target.GetType();
try
{
PropertyInfo property = m_property;
if (property == null || property.DeclaringType != targetType)
{
m_property = property =
targetType.GetProperty(m_propertyName, BINDING_FLAGS);
}
return property.GetValue(target, null);
}
catch (Exception)
{
throw new Exception("Property " + m_propertyName +
" does not exist in the class " + targetType);
}
}
// ---- IPortableObject implementation -------------------
public void ReadExternal(IPofReader reader)
{
m_propertyName = reader.ReadString(0);
m_target = reader.ReadInt32( 1);
}
public void WriteExternal(IPofWriter writer)
{
writer.WriteString(0, m_propertyName);
writer.WriteInt32( 1, m_target);
}
// ---- Equals, GetHashCode and ToString omitted -------
}

There are few things worth discussing regarding the previous code snippet. As of Coherence 3.5, value extractors can define the target they should execute against by setting the protected m_target field defined in the AbstractExtractor class to either the KEY or VALUE constant, also defined in that class. However, it is important to understand that for backwards compatibility reasons subclasses of the AbstractExtractor are responsible for both initialization and serialization of the m_target field, which is why we initialize it in the constructor and serialize it as if it was a direct member of the PropertyExtractor class.

The name of the field is somewhat unfortunate, as it is very similar to the name of the argument that is passed to the Extract method, even though the two have nothing in common&mdash;the first one is used to determine if the extractor should be executed against the key or the value of a cache entry, and the second one is the actual target object that the extraction should be performed against.

With the PropertyExtractor in place, we can now perform the extraction the same way in both Java and .NET, and regardless of the type of the cache that our code executes against.

Executing the aggregators and entry processors

The Coherence for .NET client library fully supports entry processor execution and cluster-wide aggregations via the Tangosol.Net.Cache.IInvocableCache interface, which is pretty much equivalent to the InvocableMap Java interface we discussed in Chapters 5 and 6:

public interface IInvocableCache : ICache
{
Object Invoke(Object key, IEntryProcessor agent);
IDictionary InvokeAll(ICollection keys, IEntryProcessor agent);
IDictionary InvokeAll(IFilter filter, IEntryProcessor agent);
Object Aggregate(ICollection keys, IEntryAggregator agent);
Object Aggregate(IFilter filter, IEntryAggregator agent);
}

As you can see, the IInvocableCache interface defines methods that allow you to invoke IEntryProcessor on a single entry or on a collection of entries by specifying either the keys or the filter that will determine a set of target entries for a processor.

There are also two methods that allow you to perform aggregation on a subset of cache entries, using either one of the built-in implementations of IEntryAggregator or a custom one you created.

Aggregators and entry processors can be executed both locally on the client and remotely within the cluster. However, the first scenario is somewhat unlikely&mdash;you will typically use them only for remote execution, especially considering the fact that entry processors are by far the best way to implement mutating operations over Extend.

Because of this, even though you still need to have both Java and .NET class for each aggregator and entry processor, it is usually not necessary to implement complete logic on the client. Instead, all you need to do is to capture necessary state on the client and ensure that it is serialized in the format the cluster-side Java aggregator or entry processor expects. Of course, if you do want to be able to execute them locally, you will need to implement the logic within your .NET classes as well.

For example, our sample .NET application uses entry processors to withdraw money from or deposit money into an account. They both extend from the abstract base AccountProcessor class, which implements the IEntryProcessor interface and common serialization code.

This is what the AccountProcessor class looks like:

public abstract class AccountProcessor
: AbstractProcessor, IPortableObject
{
protected AccountProcessor()
{
}
protected AccountProcessor(Money amount, string description)
{
m_amount = amount;
m_description = description;
}
public override object Process(IInvocableCacheEntry entry)
{
throw new NotSupportedException()
}
public void ReadExternal(IPofReader reader)
{
m_amount = (Money) reader.ReadObject(0);
m_description = reader.ReadString(1);
}
public void WriteExternal(IPofWriter writer)
{
writer.WriteObject(0, m_amount);
writer.WriteString(1, m_description);
}
protected Money m_amount;
protected string m_description;
}

As you can see, this class implements the IEntryProcessor.Process method by simply throwing NotSupportedException. In this case that is perfectly fine&mdash;we don't expect this processor to ever be executed on the .NET client. We just need it to capture state (amount and transaction description) that should be transmitted to the server, where it will be deserialized as a fully implemented Java processor and executed.

This makes .NET implementation of the Deposit/WithdrawProcessor trivial:

public class DepositProcessor : AccountProcessor
{
public DepositProcessor()
{}
public DepositProcessor(Money amount, string description)
: base(amount, description)
{}
}

The WithdrawProcessor looks pretty much the same, so I will omit it. The only reason we even need these two classes is to let the server know which Java class to instantiate during deserialization, as in this case processor type implies the operation we want to perform.

Listening for cache events

.NET really shines when it comes to desktop application development, so it is completely normal that the Coherence for .NET client is most often used to access a Coherence cluster from Windows Forms or WPF-based desktop applications.

One of the most powerful and interesting Coherence features from a desktop developer's perspective is the ability to register for and receive events as the data in the cluster changes, which can then be used to update the application UI in real time.

In this section we will look at standalone cache listeners and the continuous query cache, as well as the approaches to incorporate them into .NET desktop applications. We will also discuss some of the obstacles related to event handling in desktop applications, and the mechanisms built into Coherence for .NET to overcome them.

Cache listeners

Event handling is one of the areas that are significantly different in Java and .NET. While Java applications typically use classic Observer pattern and define listener interfaces, whose implementations can then be registered with the observed object, .NET promotes events to first-class language constructs and provides the ability to handle events by simply registering delegates with each individual event.

Both approaches have pros and cons. In Java, you are forced to implement all the methods defined by the listener interface even when you only care about one of them, which is why most listener interfaces also have matching abstract base classes that implement event handling methods as no-ops.

On the other hand, in situations when events are closely related and you are expected to handle all of them, the listener class nicely encapsulates all the logic related to event handling.

The situation in .NET is quite the opposite: it is easy to register for a single event, but if you care about multiple events you need to register for each one separately. In the latter case .NET code tends to be scattered across multiple delegate methods, which makes maintenance a bit more difficult.

Fortunately, the Coherence for .NET library supports both approaches. You can register to receive events "the Java way" using the Tangosol.Net.Cache.IObservableCache interface:

public interface IObservableCache : ICache
{
void AddCacheListener(ICacheListener listener);
void RemoveCacheListener(ICacheListener listener);
void AddCacheListener(ICacheListener listener,
Object key, bool isLite);
void RemoveCacheListener(ICacheListener listener,
Object key);
void AddCacheListener(ICacheListener listener,
IFilter filter, bool isLite);
void RemoveCacheListener(ICacheListener listener,
IFilter filter);
}

As you can see, all variants of Add/RemoveCacheListener methods accept an instance of the Tangosol.NetCache.ICacheListener interface, which is identical to the MapListener Java interface we discussed in Chapter 7, Processing Data Grid Events:

public interface ICacheListener
{
void EntryInserted(CacheEventArgs evt);
void EntryUpdated(CacheEventArgs evt);
void EntryDeleted(CacheEventArgs evt);
}

The usage mechanics of cache listeners are exactly the same as we discussed in Chapter 7, so we will not repeat the details here. The bottom line is that, just as in Java, you can register to receive the events for a particular object, a set of objects defined by the filter, or a cache as a whole.

However, the Coherence for .NET library also provides several helper classes that allow you to handle the events "the .NET way". The general-purpose one is Tangosol.Net.Cache. Support.DelegatingCacheListener, which defines the CacheEventHandler delegate and EntryInserted, EntryUpdated, and EntryDeleted events:

public delegate void CacheEventHandler(object sender,
CacheEventArgs args);
public class DelegatingCacheListener : ICacheListener
{
public virtual event CacheEventHandler EntryInserted;
public virtual event CacheEventHandler EntryUpdated;
public virtual event CacheEventHandler EntryDeleted;
...
}

The delegate handling the event will receive two arguments: a reference to a cache that an event occurred in as a sender argument, and the details about the event as an args argument. The latter contains all the information related to the event, such as entry key, as well as the old and new values for non-lite events.

This allows you to easily register for cache events without having to implement the listener interface yourself:

IObservableCache cache = CacheFactory.GetCache("accounts");
DelegatingCacheListener listener = new DelegatingCacheListener();
listener.EntryUpdated += AccountUpdated;
cache.AddCacheListener(listener);
private static void AccountUpdated(object sender,
CacheEventArgs args)
{
// handle event...
}

Event marshalling in Windows Forms applications

In addition to the DelegatingCacheListener, Coherence for .NET contains another, more specific ICacheListener implementation that allows you to achieve the same goal and a bit more.

One of the problems with cache events within Windows Forms applications is that all the events received by a client are raised on a background thread, which means that they need to be marshalled to the UI thread if you need to do anything UI-related within the event handlers.

Because one of the main reasons for receiving events in a desktop application is to update the user interface in real time, Coherence provides an ICacheListener implementation that marshals the events for you, in the form of the Tangosol.Net.Cache.Support.WindowsFormsCacheListener class.

All you need to do is to pass the form that the event listener is associated with to its constructor, and the WindowsFormsCacheListener will handle the rest. The code to register for events within the code-behind class for a form will typically look similar to the following:

WindowsFormsCacheListener listener =
new WindowsFormsCacheListener�(this);
listener.EntryUpdated += this.AccountUpdated;
cache.AddCacheListener(listener);
private void AccountUpdated(object sender, CacheEventArgs args)
{
// refresh account details on the UI
}

It is worth noting that event marshalling from a background to the UI thread is also required in Windows Presentation Foundation (WPF) applications, but it needs to be done in a slightly different way.

Unfortunately, as of Coherence 3.5 there is no built-in listener implementation that does that (primarily because it would create dependency on .NET 3.5), and it is out of the scope of this book to discuss how to create one. However, you can find a WpfCacheListener implementation in the sample application, in case you need it.

Continuous Query Cache

As we have seen in Chapter 7, a continuous query cache is very useful in all types of applications, as it allows you to bring a subset or even all of the data from a partitioned cache into a client process, while guaranteeing that locally cached data is never stale&mdash;as the data within the cluster changes, it is automatically updated within each instance of a continuous query cache as well.

However, a continuous query cache is particularly interesting when used within desktop applications, as it enables easy creation of highly dynamic user interfaces that are automatically updated as the data on a backend changes. Typical examples where this might be useful are trading desktops and logistic applications, but many other applications could benefit from a more dynamic user experience as well.

The reason this kind of functionality is easy to implement with a CQC is that in addition to keeping itself in sync with a back cache, CQC is also observable&mdash;as the data within it changes based on events received from a cluster, CQC raises its own events that client applications can listen to and update UI components appropriately.

We have discussed in Chapter 7 how instances of a continuous query cache are created and the various arguments you can specify to control its behavior. I will not repeat that here, as the mechanics are exactly the same&mdash;you simply create an instance of the Tangosol.Net.Cache.ContinuousQueryCache class, passing the target cache and a filter to use to its constructor.

Instead, in the remainder of this section we will focus on how you can bind CQC, or any other observable cache, to a Windows Forms DataGridView control. It is not as straightforward as I'd like, but fortunately it is not too difficult either.

Data binding with Windows Presentation Foundation (WPF)

We need to show real-time account and transaction information to bank employees using the BranchTerminal application, a .NET component of our multi-platform sample application.

We know that we can use CQC to create a live view of the data we are interested in on the client. However, we can't bind UI controls to the CQC directly. While we could bind controls to the Values property of a CQC, this property returns a static, point-in-time snapshot of the CQC contents.

WPF data binding supports dynamic update of UI controls only if the data source they are bound to implements appropriate notification interfaces and raises the change events these interfaces define. Individual objects need to implement the INotifyPropertyChanged interface and raise the PropertyChange event whenever one of their properties changes. Similarly, collections need to implement the INotifyCollectionChanged interface and raise the CollectionChanged event whenever an element is added, updated, or removed from a collection.

CQC does not (and it shouldn't) implement either of these interfaces, so you cannot bind controls to it directly. However, .NET provides the ObservableCollection class, which implements both of these interfaces and is a great source for data binding. So the only thing we need to do is to create an adapter that will expose CQC contents as an ObservableCollection.

Accounts class that does exactly what we need:

public class Accounts :
ObservableCollection<Account>, IDisposable
{
private readonly ContinuousQueryCache m_view;
public Accounts(DispatcherObject control)
{
WpfCacheListener listener = new WpfCacheListener(control);
listener.EntryDeleted += OnDelete;
listener.EntryInserted += OnAdd;
listener.EntryUpdated += OnUpdate;
INamedCache accounts = CacheFactory.GetCache("accounts");
m_view = new ContinuousQueryCache(accounts,
AlwaysFilter.Instance, listener);
}
public void OnAdd(object sender, CacheEventArgs evt)
{
Add((Account) evt.NewValue);
}
public void OnUpdate(object sender, CacheEventArgs evt)
{
int index = IndexOf((Account) evt.OldValue);
SetItem(index, (Account) evt.NewValue);
}
public void OnDelete(object sender, CacheEventArgs evt)
{
Remove((Account) evt.OldValue);
}
public void Dispose()
{
m_view.Release();
}
}

As you can see, our Accounts class extends ObservableCollection and uses CQC events internally to keep its contents in sync with the CQC it wraps.

There are two important things to notice:

  1. We pass the listener that is responsible for keeping a contents of our adapter class in sync with CQC as a third argument to a CQC constructor. This allows us to capture all CQC events, including the ones resulting from initial CQC population.

  2. The listener we use is an instance of WpfCacheListener. This ensures that all event handlers are invoked on the UI thread, which is necessary because the change event from the underlying ObservableCollection will be raised and handled on the same thread.

Now that we have an observable Accounts collection, we need to bind a ListView control on our application's main screen to it.

The first step is to expose it as a property within the window class that contains our ListView control. In the case of our sample application, this is AccountsWindow class:

public partial class AccountsWindow
{
private readonly Accounts m_accounts;
private readonly Transactions m_transactions;
public AccountsWindow()
{
try
{
m_accounts = new Accounts(this);
m_transactions = new Transactions(this);
InitializeComponent();
}
catch (Exception ex)
{
...
}
}
public Accounts Accounts
{
get { return m_accounts; }
}
...
}

As you can see, we are passing the window itself as an argument to the Accounts constructor, in order to provide a dispatcher object that WpfCacheListener should marshal events to.

The next step is to define the necessary data bindings within the XAML file for the AccountsWindow:

<Window
x:Class="Seovic.Samples.Bank.UI.AccountsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Accounts" Height="600" Width="800" WindowState="Maximized"
Unloaded="Window_Unloaded">
WPFusing, for data binding<Window.Resources>
<CollectionViewSource x:Key="AccountsViewSource"
Source="{Binding Accounts}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Customer.Name"/>
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<cm:SortDescription PropertyName="Customer.Name"
Direction="Ascending" />
<cm:SortDescription PropertyName="Description"
Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
...
<ListView
ItemsSource="{Binding Source={StaticResource AccountsViewSource}}"
SelectionChanged="AccountList_SelectionChanged"
Grid.Row="1" Grid.Column="0" Name="AccountList"
Background="White">
...
</Window>

First we specify DataContext for the AccountsWindow to be the window itself. This allows us to access the Accounts property we exposed in the previous step.

That is exactly what we do in the following step by creating a CollectionViewSource that defines grouping and sort order for our data, and binding it to the Accounts property.

Finally, we specify CollectionViewSource as the item source for our ListView, which ensures that the items within the view are grouped and sorted properly, and automatically refreshed as the underlying Accounts collection changes.

When everything is said and done, the end result looks similar to the following screenshot:

Data binding with Windows Presentation Foundation (WPF)

You should ignore the fact that the random number generator wasn't very generous to Ivan and myself, and focus on the functionality.

When you click on the account on the left, the list of transactions on the right is updated based on the selected account and transaction period. Whether you post transactions from the .NET client, Java web application, or C++ command-line client, both the account balance and the list of transactions will be updated in real time.

Unfortunately, it is impossible to show that in print, but don't take my word for it&mdash;download the sample application and see for yourself.

Summary

In this chapter we looked at the Coherence for .NET library and how you can use it to allow .NET applications to leverage everything Coherence has to offer. We discussed the configuration of .NET clients and have shown .NET API usage examples for pretty much all the topics we discussed in Chapters 2 through 7.

In the next chapter we will go over the same material one more time, but this time from a C++ developer's perspective. I am pleased to say that you are in for a treat, as the chapter that follows was written by Mark Falco, the Oracle engineer who led the implementation of the Coherence C++ client library. Enjoy!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.181.57