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_mechanism

SASL authentication mechanism (e.g. PLAIN, SCRAM-SHA-256). Auto-detected from credentials if omitted.

token

OAuth2 bearer token for OAUTHBEARER authentication.

trust_store_file_name, trust_ca

Path to PEM-encoded CA certificate for TLS verification.

key_store_file_name, client_cert

Path to client certificate for mutual TLS.

key_store_password, client_key

Path to client private key for mutual TLS.

sni_host_name, sni_host

TLS Server Name Indication (SNI) hostname.

ssl_hostname_validation, verify_hostname

Enable/disable TLS hostname verification (true/false).

client_intelligence

Client intelligence mode: basic, topology_aware, hash_distribution_aware. See Client Intelligence.

protocol_version, version

Hot Rod protocol version: version31, version40, version41.

connect_timeout

TCP connection timeout in milliseconds.

socket_timeout

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 to Version41.

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 to Put, Remove, and Replace operations. Defaults to false.

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 true if 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 true if the cache has no entries.

Stats()

Returns server-side cache statistics such as timeSinceStart, currentNumberOfEntries, stores, retrievals, hits, and misses.

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

Where(p ⇒ p.X == v)

WHERE e.x = :p0

Where(p ⇒ p.X > v && p.Y == w)

WHERE (e.x > :p0 AND e.y = :p1)

Where(p ⇒ p.Name.Contains("oh"))

WHERE e.name LIKE :p0

OrderBy(p ⇒ p.X)

ORDER BY e.x

OrderByDescending(p ⇒ p.X)

ORDER BY e.x DESC

Skip(n)

startOffset = n

Take(n)

maxResults = n

Select(p ⇒ new { p.X, p.Y })

SELECT e.x, e.y

Count() / CountAsync()

Uses TotalResults

First() / FirstAsync()

maxResults = 1

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-protostream media 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, or Leaving.

Key

The raw key bytes of the affected entry.

Value

The raw value bytes (may be null for Leaving events).

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: Strong or Weak

  • Storage: Volatile (lost on restart) or Persistent (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