This guide covers the Infinispan .NET client for the Hot Rod binary protocol.
Getting Started
Installation
Add the NuGet package to your project:
dotnet add package Infinispan.Hotrod
Or add the package reference directly to your .csproj:
<PackageReference Include="Infinispan.Hotrod" Version="10.0.0-beta.2" />
Quick Start
using Infinispan.Hotrod;
// Connect with a URI (simplest approach)
var client = InfinispanClient.FromUri("hotrod://admin:password@localhost:11222");
// Create a typed cache using the builder
var cache = client.NewCache<string>("default")
.WithEncoding(MediaType.PlainText)
.Build();
await cache.Put("greeting", "hello world");
var value = await cache.Get("greeting");
Console.WriteLine(value); // hello world
client.Dispose();
You can also create the client programmatically:
var client = new InfinispanClient();
client.AddHost("localhost", 11222);
client.User = "admin";
client.Password = "password";
See Connecting to Infinispan for full connection options including TLS, multiple hosts, and query parameters.
Connecting to Infinispan
Creating a Client from a URI
The simplest way to create a client is from a Hot Rod URI:
var client = InfinispanClient.FromUri("hotrod://localhost:11222");
The URI supports credentials, multiple hosts, TLS, and query parameters:
// With authentication
var client = InfinispanClient.FromUri("hotrod://admin:password@localhost:11222");
// Multiple servers
var client = InfinispanClient.FromUri("hotrod://server1:11222,server2:11222,server3:11222");
// TLS (use hotrods:// scheme)
var client = InfinispanClient.FromUri("hotrods://admin:password@localhost:11222");
// With query parameters
var client = InfinispanClient.FromUri(
"hotrod://admin:pass@localhost:11222?sasl_mechanism=SCRAM-SHA-256");
URI Format
hotrod://[user:password@]host1[:port1][,host2[:port2]...][?param=value&...] hotrods://... (same, with TLS enabled)
Supported Query Parameters
| Parameter | Description |
|---|---|
|
SASL authentication mechanism (e.g. |
|
OAuth2 bearer token for |
|
Path to PEM-encoded CA certificate for TLS verification. |
|
Path to client certificate for mutual TLS. |
|
Path to client private key for mutual TLS. |
|
TLS Server Name Indication (SNI) hostname. |
|
Enable/disable TLS hostname verification ( |
|
Client intelligence mode: |
|
Hot Rod protocol version: |
|
TCP connection timeout in milliseconds. |
|
Socket read/write timeout in milliseconds. |
Creating a Client Programmatically
You can also create an InfinispanClient and configure it with properties:
var client = new InfinispanClient();
client.AddHost("server1", 11222);
client.AddHost("server2", 11222);
The client discovers other cluster members automatically via topology updates.
Client Properties
Version-
Hot Rod protocol version. Supported values:
ProtocolVersion.Version31(3.1),ProtocolVersion.Version40(4.0),ProtocolVersion.Version41(4.1). Defaults toVersion41. ClientIntelligence-
Controls how the client interacts with the cluster topology. See Client Intelligence for details.
-
ClientIntelligence.Basic— Connects to a single server, no topology awareness. -
ClientIntelligence.TopologyAware— Discovers cluster members via topology updates. -
ClientIntelligence.HashDistributionAware— Routes operations to the primary owner based on consistent hashing.Defaults to
TopologyAware.
-
ForceReturnValue-
When
true, the server includes the previous value in responses toPut,Remove, andReplaceoperations. Defaults tofalse.
Multiple Clusters
You can configure multiple named clusters for failover. If the active cluster becomes unavailable, the client switches to the next available cluster:
var client = new InfinispanClient();
client.AddHost("PRIMARY", "dc1-server1", 11222);
client.AddHost("PRIMARY", "dc1-server2", 11222);
client.AddHost("BACKUP", "dc2-server1", 11222);
client.AddHost("BACKUP", "dc2-server2", 11222);
You can also switch clusters manually:
client.SwitchCluster("BACKUP");
Client Intelligence
The ClientIntelligence property controls how the client discovers and routes to cluster members:
-
Basic (
ClientIntelligence.Basic) — Connects to a single server. No topology awareness. -
Topology-aware (
ClientIntelligence.TopologyAware) — Discovers cluster members via topology updates. The client load-balances across known members. This is the default. -
Hash-distribution-aware (
ClientIntelligence.HashDistributionAware) — Routes operations to the primary owner of the key based on consistent hashing. Reduces network hops for key-based operations.
client.ClientIntelligence = ClientIntelligence.HashDistributionAware;
Security
Authentication
The client supports the following SASL authentication mechanisms:
-
PLAIN — Simple username/password authentication.
-
SCRAM-SHA-256 — Challenge-response authentication with SHA-256.
-
SCRAM-SHA-384 — Challenge-response authentication with SHA-384.
-
SCRAM-SHA-512 — Challenge-response authentication with SHA-512.
-
GSSAPI — Kerberos authentication via GSSAPI.
-
EXTERNAL — Authentication using a client TLS certificate.
Set credentials and the authentication mechanism using a URI:
var client = InfinispanClient.FromUri(
"hotrod://admin:password@localhost:11222?sasl_mechanism=SCRAM-SHA-256");
Or configure the client programmatically:
var client = new InfinispanClient();
client.AddHost("localhost", 11222);
client.User = "admin";
client.Password = "password";
client.AuthMech = "SCRAM-SHA-256";
client.Domain = "infinispan";
When User and Password are set but AuthMech is not specified, the client negotiates the mechanism with the server.
Kerberos (GSSAPI) Authentication
The client supports Kerberos authentication via the GSSAPI SASL mechanism.
This uses the .NET NegotiateAuthentication API, which supports Kerberos on all platforms.
The Domain property is used as the Kerberos service name target (hotrod/<domain>):
var client = new InfinispanClient();
client.AddHost("server.example.com", 11222);
client.User = "user@EXAMPLE.COM";
client.Password = "password";
client.AuthMech = "GSSAPI";
client.Domain = "server.example.com";
The client can also use an existing Kerberos ticket from the system credential cache by omitting the password and using DefaultCredentials:
var client = new InfinispanClient();
client.AddHost("server.example.com", 11222);
client.AuthMech = "GSSAPI";
client.Domain = "server.example.com";
| GSSAPI requires a properly configured Kerberos environment (KDC, keytab, or ticket cache). |
Client Certificate (EXTERNAL) Authentication
The EXTERNAL mechanism delegates authentication to the TLS layer. The server identifies the client by its TLS certificate, so no username or password is needed.
using System.Security.Cryptography.X509Certificates;
var client = new InfinispanClient();
client.AddHost("server.example.com", 11222);
client.AuthMech = "EXTERNAL";
client.UseTLS = true;
client.CACert = chain; // server CA chain
The client TLS certificate must be configured at the transport level.
| The Infinispan server must be configured to require client certificates and map them to security identities. |
TLS
Enable TLS by setting UseTLS to true:
client.UseTLS = true;
To verify the server certificate, provide an X509Chain:
using System.Security.Cryptography.X509Certificates;
var chain = new X509Chain();
chain.ChainPolicy.CustomTrustStore.Add(
new X509Certificate2("/path/to/ca.pem"));
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
client.UseTLS = true;
client.CACert = chain;
When CACert is null, the client skips server certificate verification.
This should only be used in development environments.
Cache Operations
Basic Operations
Call NewCache on the client to obtain a typed cache handle, then use Put, Get, and Remove:
var cache = client.NewCache<string>("my-cache")
.WithEncoding(MediaType.PlainText)
.Build();
// Store an entry.
await cache.Put("key", "value");
// Retrieve an entry.
var value = await cache.Get("key");
// Delete an entry.
await cache.Remove("key");
You can also create caches with explicit marshallers:
var cache = client.NewCache(
new StringMarshaller(), new StringMarshaller(), "my-cache");
Additional operations:
PutIfAbsent(key, value)-
Stores the entry only if the key does not already exist.
Replace(key, value)-
Replaces the value only if the key already exists. Returns a tuple
(PrevValue, Replaced). ContainsKey(key)-
Returns
trueif the cache contains an entry for the given key. Clear()-
Removes all entries from the cache.
Size()-
Returns the number of entries in the cache.
IsEmpty()-
Returns
trueif the cache has no entries. Stats()-
Returns server-side cache statistics such as
timeSinceStart,currentNumberOfEntries,stores,retrievals,hits, andmisses. Ping()-
Pings the server and returns media type and protocol information.
Versioned and Metadata Operations
Use GetWithVersion to retrieve the value along with its version number for optimistic locking:
var vwv = await cache.GetWithVersion("key");
if (vwv != null)
{
Console.WriteLine($"value={vwv.Value} version={vwv.Version}");
}
Use GetWithMetadata to retrieve the value along with server-side metadata (version, creation time, lifespan, last-used time, and max idle time):
var vwm = await cache.GetWithMetadata("key");
if (vwm != null)
{
Console.WriteLine($"value={vwm.Value} version={vwm.Version}");
Console.WriteLine($"created={vwm.Created} lifespan={vwm.Lifespan}");
Console.WriteLine($"lastUsed={vwm.LastUsed} maxIdle={vwm.MaxIdle}");
}
Use the version for optimistic locking with ReplaceWithVersion and RemoveWithVersion:
bool replaced = await cache.ReplaceWithVersion("key", "new-value", vwv.Version);
var (prev, removed) = await cache.RemoveWithVersion("key", vwv.Version);
Bulk Operations
PutAll stores multiple entries in a single operation:
var entries = new Dictionary<string, string>
{
{ "key1", "val1" },
{ "key2", "val2" },
{ "key3", "val3" }
};
await cache.PutAll(entries);
GetAll retrieves multiple keys in a single operation:
var keys = new HashSet<string> { "key1", "key2" };
var result = await cache.GetAll(keys);
KeySet returns the set of all keys:
var keys = await cache.KeySet();
Partitioned Bulk Operations
When the client has hash-distribution-aware intelligence, PutAllPart and GetAllPart split operations by segment owner and dispatch them in parallel:
var partResult = cache.GetAllPart(keys);
partResult.WaitAll();
var entries = partResult.Result();
These operations are not atomic and may partially fail.
Expiration
Use ExpirationTime to control entry expiration on Put, PutIfAbsent, Replace, and PutAll:
await cache.Put("key", "value",
lifespan: new ExpirationTime { Unit = TimeUnit.SECONDS, Value = 30 },
maxidle: new ExpirationTime { Unit = TimeUnit.SECONDS, Value = 10 });
lifespan-
The maximum time an entry can live in the cache before expiring, regardless of access.
maxidle-
The maximum idle time. The entry expires if it is not accessed within this duration.
Supported time units: SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS, MINUTES, HOURS, DAYS.
Use TimeUnit.DEFAULT to inherit the cache’s configured default, or TimeUnit.INFINITE for no expiration.
Operation Flags
Cache-level flags modify server-side behavior:
ForceReturnValue-
Forces the server to return the previous value in mutation responses.
UseCacheDefaultLifespan-
Uses the cache’s default lifespan instead of the value specified in the operation.
UseCacheDefaultMaxIdle-
Uses the cache’s default max idle time instead of the value specified in the operation.
cache.ForceReturnValue = true;
var previous = await cache.Put("key", "new-value");
Typed Caches and Marshalling
Custom Marshallers
The client uses typed caches with pluggable marshallers.
A Marshaller<T> converts between a type T and byte[] for Hot Rod wire encoding.
To create a custom marshaller, extend Marshaller<T>:
public class JsonMarshaller<T> : Marshaller<T>
{
public override byte[] Marshall(T t)
{
var json = JsonSerializer.Serialize(t);
return Encoding.UTF8.GetBytes(json);
}
public override T Unmarshall(byte[] buff)
{
var json = Encoding.UTF8.GetString(buff);
return JsonSerializer.Deserialize<T>(json);
}
}
Then use it when creating a cache:
var cache = client.NewCache(
new StringMarshaller(),
new JsonMarshaller<Person>(),
"people");
await cache.Put("john", new Person { Name = "John", Age = 30 });
var person = await cache.Get("john");
Built-in Marshallers
The client includes the following built-in marshallers:
StringMarshaller-
Converts strings to and from
byte[]using a configurable encoding. Defaults to ASCII.// ASCII (default) var ascii = new StringMarshaller(); // UTF-8 var utf8 = new StringMarshaller(Encoding.UTF8); ByteArrayMarshaller-
A no-op identity marshaller for
byte[]values.var cache = client.NewCache( new StringMarshaller(), ByteArrayMarshaller.Instance, "binary-cache");
Protocol Buffers
For interoperability with Java and other Hot Rod clients, use Protocol Buffers for marshalling.
The client includes a built-in ProtobufMarshaller<T> that works with any Google.Protobuf.IMessage<T> type.
When you use the CacheBuilder with MediaType.Protobuf encoding, the marshaller is selected automatically:
var cache = client.NewCache<Person>("people")
.WithEncoding(MediaType.Protobuf)
.Build();
You can also use the marshaller explicitly with the traditional NewCache API:
var cache = client.NewCache(
new StringMarshaller(),
new ProtobufMarshaller<Person>(),
"people");
Near Caching
Overview
Near caching stores recently accessed cache entries in a local bounded cache to avoid server round-trips on repeated reads. The .NET client uses server-side event listeners for invalidation: when other clients modify entries, the server sends invalidation events and the client automatically removes stale entries from the local cache.
Configuration
var cache = client.NewCache<string>("my-cache")
.WithEncoding(MediaType.PlainText)
.Build();
await cache.EnableNearCache(maxEntries: 5000);
The maxEntries parameter sets the maximum number of entries in the local LRU cache.
The default is 10000 entries.
Behavior
The near cache intercepts Get, Put, and Remove operations:
-
Get: If the key is in the local cache, the value is returned immediately without a server round-trip. If the key is not present locally, the client fetches it from the server and stores it in the local cache.
-
Put and Remove: The operation is sent to the server first, then the local entry is eagerly invalidated.
When other clients modify entries, the server sends events through the near cache listener. The client automatically removes invalidated entries from the local cache. On listener errors, the entire near cache is cleared as a safety measure.
Statistics
You can inspect near cache performance through the NearCacheStats property:
var stats = cache.NearCacheStats;
if (stats != null)
{
Console.WriteLine($"hits={stats.Hits} misses={stats.Misses}");
Console.WriteLine($"invalidations={stats.Invalidations} size={stats.Size}");
}
Returns null if near caching is not enabled.
Event Listeners
Cache Entry Events
You can register listeners to receive notifications when cache entries are created, modified, removed, or expired.
Implement the IClientListener interface or extend AbstractClientListener:
public class MyListener : AbstractClientListener
{
private string _id = Guid.NewGuid().ToString();
public override string ListenerID { get => _id; set => _id = value; }
public override void OnEvent(Event e)
{
Console.WriteLine($"type={e.Type} key={Encoding.ASCII.GetString(e.Key)}");
}
public override void OnError(Exception ex)
{
Console.WriteLine($"Listener error: {ex?.Message}");
}
}
Register and remove the listener on a cache:
var listener = new MyListener();
await cache.AddListener(listener);
// Wait for events...
listener.Wait();
await cache.RemoveListener(listener);
Event Types
The Event.Type field indicates the type of event:
-
EventType.CREATED— A new entry was created. -
EventType.MODIFIED— An existing entry was modified. -
EventType.REMOVED— An entry was removed. -
EventType.EXPIRED— An entry expired.
Event Fields
Each Event contains the following fields:
Key-
The key of the affected entry as
byte[]. Version-
The entry version after the event.
Type-
The event type.
Retried-
Whether this is a retried event delivery.
ListenerID-
The identifier of the listener that received the event.
Include Current State
Pass includeState: true to AddListener to receive synthetic events for all existing entries upon registration:
await cache.AddListener(listener, includeState: true);
Queries
Overview
The .NET client supports two ways to query indexed caches:
-
Ickle queries — write query strings directly using the Ickle query language.
-
LINQ queries — write idiomatic C# using standard LINQ operators, which are automatically translated to Ickle.
Both approaches require a cache with an indexed protobuf schema registered on the server.
The examples below assume a cache created as follows:
using Infinispan.Hotrod;
using Infinispan.Hotrod.Linq;
var cache = client.NewCache<Person>("people")
.WithEncoding(MediaType.Protobuf)
.Build();
Filtering
-
LINQ method syntax
-
LINQ query syntax
-
Ickle string
// Simple comparison
var adults = await cache.AsQueryable()
.Where(p => p.Age >= 18)
.ToListAsync();
// Combined conditions with AND / OR
var filtered = await cache.AsQueryable()
.Where(p => p.BornIn == "London" && p.Age > 25)
.ToListAsync();
// Negation
var notLondon = await cache.AsQueryable()
.Where(p => !(p.BornIn == "London"))
.ToListAsync();
var adults = await (
from p in cache.AsQueryable()
where p.Age >= 18
select p
).ToListAsync();
var filtered = await (
from p in cache.AsQueryable()
where p.BornIn == "London" && p.Age > 25
select p
).ToListAsync();
var notLondon = await (
from p in cache.AsQueryable()
where !(p.BornIn == "London")
select p
).ToListAsync();
var adults = await cache.Query<Person>(
"FROM example.Person p WHERE p.age >= 18");
var filtered = await cache.Query<Person>(
"FROM example.Person p WHERE p.bornIn = :city AND p.age > :minAge",
new Dictionary<string, object> { ["city"] = "London", ["minAge"] = 25 });
var notLondon = await cache.Query<Person>(
"FROM example.Person p WHERE p.bornIn != 'London'");
Captured Variables and Named Parameters
Captured variables in LINQ expressions are automatically extracted and sent as named parameters.
With Ickle strings, use :paramName placeholders and an IDictionary<string, object>.
-
LINQ method syntax
-
LINQ query syntax
-
Ickle string
string city = "London";
int minYear = 1990;
var results = await cache.AsQueryable()
.Where(p => p.BornIn == city && p.BornYear > minYear)
.ToListAsync();
string city = "London";
int minYear = 1990;
var results = await (
from p in cache.AsQueryable()
where p.BornIn == city && p.BornYear > minYear
select p
).ToListAsync();
string city = "London";
int minYear = 1990;
var results = await cache.Query<Person>(
"FROM example.Person p WHERE p.bornIn = :city AND p.bornYear > :minYear",
new Dictionary<string, object> { ["city"] = city, ["minYear"] = minYear });
All three generate the same query: FROM example.Person e WHERE (e.bornIn = :p0 AND e.bornYear > :p1).
Supported parameter types: string, int, long, double, float, bool, and float[].
String Matching
Use standard string methods in LINQ, which translate to Ickle LIKE clauses.
-
LINQ method syntax
-
LINQ query syntax
-
Ickle string
// Contains -> LIKE '%value%'
var results = await cache.AsQueryable()
.Where(p => p.FirstName.Contains("arr"))
.ToListAsync();
// StartsWith -> LIKE 'value%'
var results = await cache.AsQueryable()
.Where(p => p.FirstName.StartsWith("Ha"))
.ToListAsync();
// EndsWith -> LIKE '%value'
var results = await cache.AsQueryable()
.Where(p => p.LastName.EndsWith("er"))
.ToListAsync();
var results = await (
from p in cache.AsQueryable()
where p.FirstName.Contains("arr")
select p
).ToListAsync();
var results = await (
from p in cache.AsQueryable()
where p.FirstName.StartsWith("Ha")
select p
).ToListAsync();
var results = await (
from p in cache.AsQueryable()
where p.LastName.EndsWith("er")
select p
).ToListAsync();
var results = await cache.Query<Person>(
"FROM example.Person p WHERE p.firstName LIKE '%arr%'");
var results = await cache.Query<Person>(
"FROM example.Person p WHERE p.firstName LIKE 'Ha%'");
var results = await cache.Query<Person>(
"FROM example.Person p WHERE p.lastName LIKE '%er'");
Sorting
-
LINQ method syntax
-
LINQ query syntax
-
Ickle string
var sorted = await cache.AsQueryable()
.OrderBy(p => p.LastName)
.ThenByDescending(p => p.Age)
.ToListAsync();
var sorted = await (
from p in cache.AsQueryable()
orderby p.LastName, p.Age descending
select p
).ToListAsync();
var sorted = await cache.Query<Person>(
"FROM example.Person p ORDER BY p.lastName, p.age DESC");
Pagination
Use Skip and Take in LINQ, or StartOffset and MaxResults on a QueryRequest.
Skip and Take are only available in LINQ method syntax. You can combine query syntax with method syntax for pagination.
|
-
LINQ method syntax
-
LINQ query + method syntax
-
Ickle string
var page = await cache.AsQueryable()
.OrderBy(p => p.LastName)
.Skip(20)
.Take(10)
.ToListAsync();
var page = await (
from p in cache.AsQueryable()
where p.BornIn == "London"
orderby p.LastName
select p
).Skip(20).Take(10).ToListAsync();
using Org.Infinispan.Query.Remote.Client;
var request = new QueryRequest();
request.QueryString = "FROM example.Person p ORDER BY p.lastName";
request.StartOffset = 20;
request.MaxResults = 10;
var response = await cache.Query(request);
Projections
Use Select in LINQ to retrieve only specific fields, which generates an Ickle SELECT clause.
-
LINQ method syntax
-
LINQ query syntax
-
Ickle string
var names = await cache.AsQueryable()
.Where(p => p.BornIn == "London")
.Select(p => new { p.FirstName, p.LastName })
.ToListAsync();
foreach (var n in names)
Console.WriteLine($"{n.FirstName} {n.LastName}");
var names = await (
from p in cache.AsQueryable()
where p.BornIn == "London"
select new { p.FirstName, p.LastName }
).ToListAsync();
foreach (var n in names)
Console.WriteLine($"{n.FirstName} {n.LastName}");
var results = await cache.Query(
"SELECT p.firstName, p.lastName FROM example.Person p WHERE p.bornIn = 'London'");
foreach (Object[] row in results)
Console.WriteLine($"{row[0]} {row[1]}");
Terminal Operators
Terminal operators retrieve single values or counts. These are only available in LINQ method syntax.
-
LINQ method syntax
-
Ickle string
// Count
int count = await cache.AsQueryable()
.Where(p => p.BornIn == "London")
.CountAsync();
// First (throws if empty)
var first = await cache.AsQueryable()
.Where(p => p.LastName == "Potter")
.FirstAsync();
// FirstOrDefault (returns default if empty)
var maybe = await cache.AsQueryable()
.Where(p => p.LastName == "Dumbledore")
.FirstOrDefaultAsync();
// Single (throws if not exactly one)
var unique = await cache.AsQueryable()
.Where(p => p.Id == 42)
.SingleAsync();
// LongCount
long total = await cache.AsQueryable()
.LongCountAsync();
// Terminal with predicate
var harry = await cache.AsQueryable()
.FirstAsync(p => p.FirstName == "Harry");
// Count — use the raw QueryRequest API for totalResults
var request = new QueryRequest();
request.QueryString = "FROM example.Person p WHERE p.bornIn = 'London'";
request.MaxResults = 0;
var response = await cache.Query(request);
long count = response.TotalResults;
// First — limit to 1 result
var first = (await cache.Query<Person>(
"FROM example.Person p WHERE p.lastName = :name",
new Dictionary<string, object> { ["name"] = "Potter" }))[0];
Combining Operators
Chain multiple operators together for complex queries.
-
LINQ method syntax
-
LINQ query syntax
-
Ickle string
var topYoungLondoners = await cache.AsQueryable()
.Where(p => p.BornIn == "London")
.Where(p => p.BornYear >= 1990)
.OrderByDescending(p => p.BornYear)
.Take(5)
.ToListAsync();
var topYoungLondoners = await (
from p in cache.AsQueryable()
where p.BornIn == "London" && p.BornYear >= 1990
orderby p.BornYear descending
select p
).Take(5).ToListAsync();
var request = new QueryRequest();
request.QueryString =
"FROM example.Person p WHERE p.bornIn = 'London' AND p.bornYear >= 1990 ORDER BY p.bornYear DESC";
request.MaxResults = 5;
var response = await cache.Query(request);
Multiple Where calls in LINQ are combined with AND.
Operator Reference
| LINQ | Ickle |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Uses |
|
|
Property Name Mapping
The LINQ provider automatically maps C# property names (PascalCase) to protobuf field names (camelCase) using the protobuf descriptor.
For example, p.FirstName in C# maps to e.firstName in the generated Ickle query.
No manual configuration is needed when using protobuf-generated types.
Continuous Queries
Continuous queries let you receive real-time notifications when cache entries match (or stop matching) a query.
Overview
A continuous query registers an Ickle query with the server and receives events whenever:
-
An entry starts matching the query (Joining)
-
A matching entry is updated and still matches (Updated)
-
An entry stops matching the query (Leaving)
Prerequisites
-
An indexed cache with a registered protobuf schema
-
The cache must use
application/x-protostreammedia type
Creating a Continuous Query
await using var cq = cache.ContinuousQuery(
"FROM example.Person WHERE age >= 18");
The ContinuousQuery implements IAsyncDisposable, so the await using pattern automatically removes the listener when the variable goes out of scope.
Receiving Events
Events are delivered through a System.Threading.Channels.ChannelReader<CQEvent>:
await using var cq = cache.ContinuousQuery(
"FROM example.Person WHERE age >= 18");
await foreach (var ev in cq.Events.ReadAllAsync(cancellationToken))
{
Console.WriteLine($"Type={ev.Type}, Key={ev.Key}");
if (ev.Type == CQResultType.Joining)
{
// Entry now matches the query
}
else if (ev.Type == CQResultType.Leaving)
{
// Entry no longer matches the query
}
}
CQEvent Properties
Type-
The event type:
Joining,Updated, orLeaving. Key-
The raw key bytes of the affected entry.
Value-
The raw value bytes (may be null for
Leavingevents). Projections-
A list of raw byte arrays if the query uses projections.
Lifecycle
The continuous query listener is active from creation until disposal.
Call DisposeAsync() (or use await using) to unregister the listener from the server:
var cq = cache.ContinuousQuery("FROM example.Person WHERE age >= 18");
// ... process events ...
await cq.DisposeAsync();
Iteration
You can iterate over all entries in a cache using RetrieveEntries.
The iterator fetches entries in batches from the server using IAsyncEnumerable.
await foreach (var entry in cache.RetrieveEntries(batchSize: 500))
{
Console.WriteLine($"key={entry.Key} value={entry.Value}");
}
Iteration with Metadata
Use RetrieveEntriesWithMetadata to include entry metadata (version, creation time, lifespan, last-used time, max idle) with each entry:
await foreach (var entry in cache.RetrieveEntriesWithMetadata(batchSize: 500))
{
Console.WriteLine($"key={entry.Key} value={entry.Value.Value}");
Console.WriteLine($"version={entry.Value.Version} created={entry.Value.Created}");
Console.WriteLine($"lifespan={entry.Value.Lifespan} maxIdle={entry.Value.MaxIdle}");
}
Convenience Methods
EntrySet(batchSize)-
An alias for
RetrieveEntries. Returns all cache entries as key-value pairs. Values(batchSize)-
Iterates over all cache values, discarding the keys.
await foreach (var value in cache.Values())
{
Console.WriteLine(value);
}
The default batch size for all iteration methods is 1000 entries per server round-trip.
Transactions
The .NET client supports optimistic transactions with version-based conflict detection.
Basic Usage
Begin a transaction on a cache, perform operations, and then commit or rollback:
var tx = cache.BeginTransaction(timeoutMs: 30000);
var value = await tx.Get("key");
await tx.Put("key", "new-value");
await tx.Remove("other-key");
await tx.CommitAsync();
If a conflict is detected during commit (another client modified an entry after the transaction read it), an InfinispanException is thrown.
Transaction Operations
Get(key)-
Reads a value within the transaction. The first read fetches from the server and tracks the entry version for optimistic locking. Subsequent reads of the same key return the locally buffered value.
Put(key, value)-
Buffers a write operation. Reads the current version from the server if the key hasn’t been read yet in this transaction.
PutBlind(key, value)-
Buffers a write without reading the current value. Useful for unconditional writes when you don’t need conflict detection on the key.
Remove(key)-
Buffers a remove operation.
Commit Strategies
CommitAsync()-
One-phase commit. Prepares and commits in a single round-trip. Suitable for transactions modifying a single cache.
CommitTwoPhaseAsync()-
Two-phase commit (XA protocol). Sends a prepare followed by a separate commit.
RollbackAsync()-
Rolls back all buffered modifications.
Expiration in Transactions
You can set expiration on entries written within a transaction:
var tx = cache.BeginTransaction();
await tx.Put("key", "value",
lifespan: new ExpirationTime { Unit = TimeUnit.MINUTES, Value = 5 });
await tx.CommitAsync();
Distributed Counters
Infinispan distributed counters provide cluster-wide atomic counters. The .NET Hot Rod client supports both strong and weak counters with operations such as get, set, add, compare-and-swap, and reset.
Counter Types
- Strong counters
-
Provide strong consistency guarantees. All operations are atomic and consistent across the cluster.
- Weak counters
-
Optimized for high-throughput counting. They trade strong consistency for performance by using a configurable concurrency level.
Counter Configuration
Counters are configured with:
-
Type:
StrongorWeak -
Storage:
Volatile(lost on restart) orPersistent(survives restart) -
Bounded: Whether the counter has lower and upper bounds
-
Initial value: The starting value
Creating and Using Counters
var client = InfinispanClient.FromUri("hotrod://admin:password@127.0.0.1:11222");
// Get the counter manager
var counters = client.Counters();
// Define a strong counter
await counters.Define("my-counter", new CounterConfiguration
{
Type = CounterType.Strong,
Storage = CounterStorage.Persistent,
InitialValue = 0
});
// Define a bounded strong counter
await counters.Define("bounded-counter", new CounterConfiguration
{
Type = CounterType.Strong,
Storage = CounterStorage.Volatile,
Bounded = true,
LowerBound = 0,
UpperBound = 100,
InitialValue = 50
});
// Define a weak counter
await counters.Define("weak-counter", new CounterConfiguration
{
Type = CounterType.Weak,
Storage = CounterStorage.Volatile,
ConcurrencyLevel = 4,
InitialValue = 0
});
Counter Operations
// Get a counter handle
var counter = counters.Counter("my-counter");
// Get the current value
long value = await counter.Get();
// Add a delta and get the result
long newValue = await counter.AddAndGet(5);
// Set a value and get the previous one
long previous = await counter.GetAndSet(42);
// Compare and swap
var (oldValue, success) = await counter.CompareAndSwap(42, 100);
if (success)
{
Console.WriteLine("Swapped successfully");
}
// Reset to initial value
await counter.Reset();
Counter Manager Operations
// Check if a counter is defined
bool exists = await counters.IsDefined("my-counter");
// Get counter configuration
CounterConfiguration config = await counters.GetConfiguration("my-counter");
// List all counter names
IList<string> names = await counters.Names();
// Remove a counter
await counters.Remove("my-counter");
Multimaps
Infinispan multimaps allow you to associate multiple values with a single key.
The .NET Hot Rod client provides a MultimapCache<K, V> API that maps each key to a collection of values.
Creating a Multimap
Use the NewMultimap factory method on InfinispanClient:
var client = InfinispanClient.FromUri("hotrod://admin:password@127.0.0.1:11222");
var multimap = client.NewMultimap(
new StringMarshaller(), new StringMarshaller(), "my-multimap");
The supportsDuplicates parameter controls whether the same value can appear more than once for a given key:
var multimap = client.NewMultimap(
new StringMarshaller(), new StringMarshaller(), "my-multimap",
supportsDuplicates: true);
Storing Values
Put adds a value to the collection for a key.
Multiple calls with the same key accumulate values:
await multimap.Put("colors", "red");
await multimap.Put("colors", "blue");
await multimap.Put("colors", "green");
Retrieving Values
Get returns all values associated with a key.
If the key does not exist, an empty list is returned:
IList<string> colors = await multimap.Get("colors");
// colors = ["red", "blue", "green"]
IList<string> empty = await multimap.Get("nonexistent");
// empty = []
Removing Entries
Remove all values for a key:
bool removed = await multimap.RemoveKey("colors");
Remove a specific value from a key:
await multimap.Put("colors", "red");
await multimap.Put("colors", "blue");
bool removed = await multimap.RemoveEntry("colors", "red");
// Only "blue" remains
Both methods return false if the key (or key-value pair) did not exist.
Checking Membership
bool hasKey = await multimap.ContainsKey("colors");
bool hasValue = await multimap.ContainsValue("red");
bool hasEntry = await multimap.ContainsEntry("colors", "red");
Size
Size returns the total number of values across all keys:
await multimap.Put("k1", "v1");
await multimap.Put("k1", "v2");
await multimap.Put("k2", "v3");
long size = await multimap.Size();
// size = 3
Cache Administration
The CacheAdmin API lets you create, remove, and manage caches, templates, and protobuf schemas on the server at runtime through the Hot Rod protocol.
Getting the Admin Handle
var client = InfinispanClient.FromUri("hotrod://admin:password@127.0.0.1:11222");
var admin = client.Administration();
Creating Caches
Create a cache with an XML configuration:
await admin.CreateCache("my-cache", "<distributed-cache/>");
Create a cache from a server-side template:
await admin.CreateCacheWithTemplate("my-cache", "my-template");
Use GetOrCreateCache for idempotent creation (no error if the cache already exists):
await admin.GetOrCreateCache("my-cache");
await admin.GetOrCreateCacheWithTemplate("my-cache", "my-template");
Removing Caches
await admin.RemoveCache("my-cache");
Listing Caches
ISet<string> names = await admin.GetCacheNames();
Cache Configuration
Update a cache configuration attribute at runtime:
await admin.UpdateConfigurationAttribute(
"my-cache", "memory.max-count", "5000");
Assign an alias to a cache:
await admin.AssignAlias("my-cache", "cache-alias");
Index Management
Rebuild the index for an indexed cache:
await admin.ReindexCache("my-indexed-cache");
Update the index schema after a protobuf schema change:
await admin.UpdateIndexSchema("my-indexed-cache");
Templates
Create and remove cache configuration templates:
await admin.CreateTemplate("my-template", "<local-cache/>");
await admin.RemoveTemplate("my-template");
Admin Flags
Cache and template operations accept optional AdminFlag values:
AdminFlag.Permanent-
The cache or template survives server restarts (persisted to the global state).
AdminFlag.Volatile-
The cache or template is not persisted.
await admin.CreateCache("volatile-cache", flags: AdminFlag.Volatile);
Schema Management
The SchemaAdmin API manages protobuf schemas used for queries and protostream encoding.
var schemas = admin.Schemas();
Registering Schemas
var proto = @"
package example;
message Person {
required string name = 1;
required int32 age = 2;
}
";
// Create a new schema (fails if it already exists)
await schemas.Create("example.proto", proto);
// Update an existing schema
await schemas.Update("example.proto", proto);
// Create or update (idempotent)
await schemas.Save("example.proto", proto);
Deleting Schemas
await schemas.Delete("example.proto");
Executing Arbitrary Server Tasks
For advanced use cases, the underlying Exec method is available on the client to execute any server task by name:
var parameters = new List<(string Name, byte[] Value)>
{
("param1", Encoding.UTF8.GetBytes("value1"))
};
byte[] result = await client.Exec("my-server-task", parameters);
ASP.NET Core Integration
The client provides an IDistributedCache implementation for integration with ASP.NET Core session state, response caching, and any component that uses the standard distributed cache abstraction.
Dependency Injection
Register Infinispan as the distributed cache provider in Program.cs or Startup.cs:
using Infinispan.Hotrod;
builder.Services.AddInfinispanCache(options =>
{
options.CacheName = "sessions";
options.ConfigureClient = client =>
{
client.AddHost("infinispan-server", 11222);
client.User = "admin";
client.Password = "password";
client.AuthMech = "SCRAM-SHA-256";
};
});
This registers InfinispanDistributedCache as the IDistributedCache singleton.
Configuration Options
CacheName-
The name of the Infinispan cache to use. Defaults to
"default". ConfigureClient-
A callback to configure the underlying
InfinispanClient(add hosts, set credentials, enable TLS, etc.).
Direct Usage
You can also use InfinispanDistributedCache directly without dependency injection:
using var client = InfinispanClient.FromUri("hotrod://admin:password@localhost:11222");
using var cache = new InfinispanDistributedCache(client, "my-cache");
await cache.SetAsync("key", Encoding.UTF8.GetBytes("value"),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(5)
});
var data = await cache.GetAsync("key");
Expiration Mapping
The DistributedCacheEntryOptions are mapped to Infinispan expiration as follows:
AbsoluteExpirationRelativeToNow-
Mapped to the Hot Rod entry lifespan.
AbsoluteExpiration-
Converted to a relative duration from the current time and mapped to the entry lifespan.
SlidingExpiration-
Mapped to the Hot Rod entry max idle time.
Refresh
Calling Refresh or RefreshAsync performs a Get operation on the server, which resets the server-side idle timer for entries with a sliding expiration:
await cache.RefreshAsync("session-key");
HybridCache
The client integrates with .NET’s HybridCache, which combines a fast in-process L1 cache with a distributed L2 cache and provides stampede protection (only one caller fetches on a cache miss).
Register both Infinispan and HybridCache in a single call:
builder.Services.AddInfinispanHybridCache(
infinispan =>
{
infinispan.CacheName = "hybrid";
infinispan.ConfigureClient = client =>
{
client.AddHost("infinispan-server", 11222);
client.User = "admin";
client.Password = "password";
};
},
hybrid =>
{
hybrid.MaximumPayloadBytes = 1024 * 1024;
hybrid.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
});
The second parameter for HybridCacheOptions is optional.
When omitted, the defaults are used.
Then inject HybridCache and use it:
public class MyService(HybridCache cache)
{
public async Task<Product> GetProductAsync(string id)
{
return await cache.GetOrCreateAsync(
$"product:{id}",
async cancel => await LoadProductFromDatabase(id, cancel));
}
}
Infinispan serves as the L2 distributed layer.
The L1 in-process cache is managed by HybridCache automatically.
Building and Testing
Building
dotnet build
Running Tests
Tests use xUnit and require a running Infinispan server (via Testcontainers or a manual instance):
dotnet test