Getting Started with the Hazelcast .NET Client
The Hazelcast .NET client is the entry point to all interactions with a Hazelcast cluster in a .NET application. This guide will get you up and running quickly with the Hazelcast .NET client and demonstrate some of its key features. Let’s cover a few prerequisites before we dive in. Before going further, you should:
- Understand what Hazelcast is and why you want to use it. If you’re not sure, start by reading the Hazelcast overview and use cases.
- Are use using the .NET Framework or .NET Core and C#. If you’re coding in Visual Basic or F#, you can still use the Hazelcast .NET client, and this guide will still be useful, but you’ll need to translate the code samples into your language of choice.
- Have access to a Hazelcast cluster you can connect to. If you don’t, we recommend signing up for a Hazelcast Cloud account to get access to a Hazelcast cluster and Management Center.
The client is available as a NuGet package, supporting Framework 4.6.2 and above and .NET Core versions 2.1 and 3.1. As it targets both .NET Standards 2.0 and 2.1, .NET 5 and .NET 6 should also function as expected. This getting started guide will give you an end-to-end hands-on introduction to using the Hazelcast .NET client, covering:
- Installing and importing the client
- Configuring the client and connecting to a cluster
- Working with Hazelcast distributed data structures
- Handling Events
- Using Transactions
When you’re ready for a deeper dive, you can find the complete reference manual for the client on GitHub.
Setting Up
Before we can get started, we’ll need to install the Hazelcast.NET package from NuGet. There are three ways we can do this. First, we can do it via the Visual Studio GUI: Or, we can use the Visual Studio package manager console:
PM> Install-Package Hazelcast.NET
Or, we can install the package through the .NET CLI:
> dotnet add package Hazelcast.NET
Client Configuration
Once we’ve added the package, we’re ready to use the Hazelcast client in a .NET application. We create the client with a static factory that we need to dispose of to close connections. We can wrap all this up in a single using statement:
await using var client = await HazelcastClientFactory.StartNewClientAsync();
Here, we rely on the default configuration behavior using environment variables or configuration files. Visit our guide for complete details on all available configuration options. For our example, we can build the HazelcastOptions function programmatically:
var options = HazelcastOptions.Build();
options.Networking.Addresses.Add("127.0.0.1");
options.ClusterName = "dev";
options.ClientName = "MyClient";
Then, we can start a new Hazelcast client connection using the client factory:
await using var client
= await HazelcastClientFactory.StartNewClientAsync(options);
This code sets the following options:
Networking.Addresses
: The client uses this list to find and connect to a running cluster member server. This initial member then sends the list of other members to the client. We added 127.0.0.1.ClusterName
: This needs to match the name set on the cluster to which the client is connecting.ClientName
: This is optional. If we don’t specify a client name, the client generates it.
In this case, the client’s name is MyClient so that it is easy to spot in the Management Center. It’s a good idea to set up the Management Center alongside your clusters from the outset to get good information about how your client code is working. When we run the application, we see that our client has successfully connected to the dev cluster.
Working with Distributed Objects
The client can work with distributed objects that the cluster manages. A unique name identifies each object. Before we get started, we should note that as with the client object itself, it’s essential to dispose of distributed objects after use. Disposing of objects frees up client-side resources only and leaves the data intact in the cluster. To remove an object from a cluster, you must destroy it. Hazelcast distributed objects implement IAsyncDisposable, so it’s best to create them with the await using
statement:
await using var map =
await client.GetMapAsync<string, string>("country-abbreviations");
This ensures that the .NET CLR will automatically dispose of the object when it goes out of scope.
Distributed Objects Supported by the .NET Client
The Hazelcast .NET client supports all of Hazelcast’s distributed objects. They also share similar functionality for create, read, update, and delete (CRUD) operations. All of the following distributed objects, except RingBuffer, expose events.
- Map: A distributed map that stores key-value pairs. It works similarly to a .NET
Dictionary<K,V>
. - List: A distributed list implementation similar to a .NET
List<T>
. - Queue: A distributed queue implementation similar to a .NET
Queue<T>
. - Topic: A distribution mechanism for publishing messages sent to multiple subscribers. This is also known as a publish and subscribe model. Publishing and subscribing operations are cluster-wide. When a member subscribes to a topic, it registers for messages published by any member in the cluster, including the new members that joined after you add the listener.
- Set: A distributed set implementation similar to a .NET
HashSet<T>
. - RingBuffer: A ring buffer stores content in a ring-like structure. A ring buffer has a fixed capacity, so it won’t grow to the extent that it could endanger the system’s stability. If the ring buffer’s capacity is exceeded, then the oldest item in the ring buffer is overwritten. You can control the behavior when adding a new item to a ring buffer with 0 capacity using the
OverflowPolicy
enumeration, with the option to overwrite or fail.
You can find the full documentation for the Hazelcast .NET API distributed objects on GitHub. Since a map is one of the most commonly used distributed data structures, in the next section, we’ll take a closer look at using a Hazelcast map in C#.
Maps
A Hazelcast map is a distributed key-value store corresponding to a .NET Dictionary, with data partitioned in cluster members to facilitate horizontal scalability.
await using var map =
await client.GetMapAsync<string, int>("person-age");
The GetMapAsync method returns an existing object in the cluster with the name provided or creates a new object. We can then set three new key-value pairs on our map:
await map.SetAsync("person1", 17));
await map.SetAsync("person2", 20));
await map.SetAsync("person3", 45);
Here, we’ve made create a map with string keys and integer values.
However, we could have used any valid C# built-in or user-defined type — for example, a map of string values: await using var map =
await client.GetMapAsync<string, int>("example-distributed-map");
Or, we could use a map of a type defined by a Person class:
await using var map =
await client.GetMapAsync<string, Person>("example-distributed-map");
SetAsync
adds or updates an entry with an infinite time-to-live. After executing the code above, we should be able to see our three entries in Management Center:
Fetching All Map Entries
The snippet below fetches all entries in the map and then outputs them to a console:
var entries = await map.GetEntriesAsync();
foreach (var kvp in entries)
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
There are a couple of things to note here:
- The returned dictionary is read-only.
- This method doesn’t pull entries from the Hazelcast cluster’s MapStore. It only shows entries that are cached locally. If you want to load all entries from the cluster, you can use the
LoadAllAsync
method.
Querying a Map
We can fetch a single value from a map using GetAsync:
await using var map = await using client.GetMapAsync<string, Person>("people");
var bob = await map.GetAsync("Bob");
We can also perform distributed queries on maps to find all values matching specific criteria. The following snippet shows a query with a single condition that retrieves all people from a map where the Age property of each person is greater than 18 and outputs them to the console:
var values =
await map.GetValuesAsync(Predicates.GreaterThanOrEqualTo("Age", 18));
foreach (var value in values)
Console.WriteLine($"Person: {value}");
We can construct a more complicated Predicate with multiple conditions:
var query = Predicates.And(
Predicates.LessThan("Age", 10),
Predicates.Between("Age", 18, 21)
);
var result = await map.GetValuesAsync(query);
We can also write them in SQL:
var sqlQuery = Predicates.Sql("Age >= 18");
var sqlResult= await map.GetValuesAsync(sqlQuery);
For a complete list of predicates available, see the API reference for distributed querying.
How Entries Leave a Map
In addition to entries expiring after their time to live, there are several ways entries can leave a map, each of which has different results.
Removing Individual Entries
There are several methods available for removing individual entries from a map. The method you choose depends on the results you want to achieve, as described in the following table:
Method | Erase in-memory | Erase MapSore | Return |
map.EvictAsync(key) |
Y | N | bool |
map.DeleteAsync(key) |
Y | Y | void |
map.RemoveAsync(key) |
Y | Y | removed value |
map.RemoveAsync(key, value) |
Y | Y | bool |
Removing Selected Map Entries
To remove multiple entries from a map, you can use map.RemoveAllAsync
in combination with a predicate to remove entries that match given criteria. If you’re working with a MapStore, this removes the entries from both the in-memory store and the external store. Unlike the map.RemoveAsync
method, this doesn’t return the removed values. await map.RemoveAllAsync(Predicates.GreaterThanOrEqualTo("age", 18));
Removing All Map Data
You can remove all data from a map or remove the map itself. See the table below for the different methods and their results:
Method | Result |
map.EvictAllAsync() |
|
map.ClearAsync() |
|
map.DestroyAsync() |
|
Other Map Types
In addition to the map, the Hazelcast .NET client also supports two other specialized map types:
- MultiMap, which offers support for storing multiple values under a single key.
- Replicated map, which replicates data in all cluster members (instead of partitioning) for faster access at the expense of higher memory consumption on the server.
Events
All Hazelcast .NET distributed objects, other than the ring buffer, expose events to which you can subscribe. For example, in our map object we can execute some code whenever an event removes an entry:
var id = await map.SubscribeAsync(events => events
.EntryRemoved((sender, args) => {
// Code to execute when an entry is removed from the map
}));
In the next example, we subscribe to many events, output the relevant properties to a console for demonstration purposes, then unsubscribe from all events. var id = await map.SubscribeAsync(handle => handle
.EntryUpdated((m, a) =>
Console.WriteLine($"Updated '{a.Key}': To: '{a.Value}' From:
'{a.OldValue}'"))
.EntryRemoved((m, a) =>
Console.WriteLine($"Removed '{a.Key}': '{a.OldValue}'", a.Key, a.OldValue))
.EntryAdded((m, a) => Console.WriteLine($"\nAdded '{a.Key}': '{a.Value}'"))
.EntryExpired((m, a) =>
Console.WriteLine($"Expired '{a.Key}': '{a.OldValue}'"))
.Evicted((m, a) =>
Console.WriteLine($"All { a.NumberOfAffectedEntries} map entries evicted"))
);
await map.UnsubscribeAsync(id);
The following table lists all map object events available in the Hazelcast .NET client.
Event | Trigger |
EntryAdded | Entry is added to the map. |
EntryEvicted | Entry is removed from the map due to a size-based eviction. |
EntryExpired | Entry is removed from the map due to expiration-based eviction (this happens when time-to-live or maximum idle seconds are configured for the entries). If your listener implements EntryExpired and EntryEvicted together, the listener may receive both expiration and eviction events for the same entry. This is because size-based eviction removes entries regardless of whether entries are expired or not. |
EntryLoaded | Entry is loaded by a MapLoader implementation. |
EntryMerged | WAN-replicated entry is merged. |
EntryRemoved | Entry is directly removed from the map, for example, using the map.RemoveAsync method or the REST DELETE call. |
EntryUpdated | Entry is updated. |
EventLost | An event was lost and not added to the event journal. |
Cleared | All entries of a map are removed using the map.ClearAsync method. |
Evicted | All entries of a map are removed using the map.EvictAllAsync method. |
Transactions
The client can create Hazelcast transactions. Transactions work in much the same way they do in a database. All commands in the transaction must complete successfully. Otherwise, the transaction rolls back, and none of the changes are committed to the Hazelcast cluster.
await using (var transaction = await client.BeginTransactionAsync())
{
// ... code to execute in transaction
transaction.Complete();
}
By including a call to the transaction.Complete()
method, one of two possible outcomes can happen when disposing of the transaction:
- The transaction was successful and is committed.
- The transaction fails, so it rolls back.
By default, Hazelcast client transactions use a two-phase commit. To use a one-phase commit, the client contains an overload for the BeginTransactionAsync()
method that accepts a TransactionOptions
object:
var options = new TransactionOptions
{
Type = TransactionType.OnePhase
};
await using (var transaction = await client.BeginTransactionAsync(options))
{
// ... code to execute in transaction
transaction.Complete();
}
Next Steps
This concludes our hands-on introduction to using the Hazelcast .NET client. Now that you’re familiar with how to install, import, and configure the client, and we’ve reviewed how to use the Hazelcast distributed data structures, start working with the Hazelcast .NET library for yourself. Check out the reference manual for a deep dive into the .NET client’s functionality, and visit the community channels for help along the way.