Garnet is a high-performance remote cache-store from Microsoft Research implementing the Redis RESP wire protocol in C#/.NET. It uses Tsavorite as its storage engine. Full developer docs: https://microsoft.github.io/garnet/docs/dev/onboarding
Note: Some website docs may reference the older two-store architecture (separate main store and object store). This branch uses a unified single-store design — see the Architecture section below for the current model.
# Build the entire solution
dotnet build
# Run all Garnet tests
dotnet test test/Garnet.test -f net10.0 -c Debug -l "console;verbosity=detailed"
# Run all cluster tests
dotnet test test/Garnet.test.cluster -f net10.0 -c Debug -l "console;verbosity=detailed"
# Run a single test by fully qualified name
dotnet test test/Garnet.test -f net10.0 -c Debug --filter "FullyQualifiedName~RespTests.PingTest"
# Run all tests in a single test class
dotnet test test/Garnet.test -f net10.0 -c Debug --filter "FullyQualifiedName~RespTests"
# Build and test Tsavorite independently (has its own solution)
dotnet build libs/storage/Tsavorite/cs/test/Tsavorite.test.csproj
dotnet test libs/storage/Tsavorite/cs/test/Tsavorite.test.csproj -f net10.0 -c Debug -l "console;verbosity=detailed"
# Check formatting (CI enforces this)
dotnet format Garnet.slnx --verify-no-changes
dotnet format libs/storage/Tsavorite/cs/Tsavorite.slnx --verify-no-changes
# Run the server locally (from repo root)
cd main/GarnetServer && dotnet run -c Debug -f net10.0 -- --logger-level Trace -m 4g -i 64mTarget frameworks are net8.0 and net10.0. CI runs tests on both, in Debug and Release, on Ubuntu and Windows.
Garnet uses a single Tsavorite key-value store instance (TsavoriteKV<StoreFunctions, StoreAllocator>) that holds both raw strings and complex objects. The store is accessed through three different context types, each with its own input/output types and session functions:
| Context | Input/Output Types | Session Functions | Used For |
|---|---|---|---|
| String context | StringInput / StringOutput |
MainSessionFunctions |
Raw string commands (GET, SET, APPEND, INCR, etc.) |
| Object context | ObjectInput / ObjectOutput |
ObjectSessionFunctions |
Collection commands (HSET, LPUSH, ZADD, SADD, etc.) |
| Unified context | UnifiedInput / UnifiedOutput |
UnifiedSessionFunctions |
Type-agnostic commands (EXISTS, DELETE, TYPE, TTL, EXPIRE, RENAME, etc.) |
All three contexts operate on the same underlying store. At the storage level, each record's RecordInfo has a ValueIsObject bit that indicates whether the value is a raw string (inline bytes) or a heap object reference, enabling the unified store to differentiate between the two value types. The GarnetApi struct is generic over all three context types:
public partial struct GarnetApi<TStringContext, TObjectContext, TUnifiedContext>Two concrete instantiations are used: BasicGarnetApi (normal operations) and TransactionalGarnetApi (within transactions). Type aliases for all context variants are defined in libs/GlobalUsings.cs.
The single store is held by GarnetDatabase (libs/server/GarnetDatabase.cs) and managed by StoreWrapper (libs/server/StoreWrapper.cs). Each record carries a ValueIsObject bit in its RecordInfo header to distinguish raw string values from serialized object values.
Each context type has parallel directory structures:
- Functions (Tsavorite callbacks for RMW, Read, Upsert, Delete):
libs/server/Storage/Functions/MainStore/— string operationslibs/server/Storage/Functions/ObjectStore/— collection operationslibs/server/Storage/Functions/UnifiedStore/— type-agnostic operations
- Session ops (StorageSession methods wrapping Tsavorite API):
libs/server/Storage/Session/MainStore/— string ops (MainStoreOps.cs, BitmapOps.cs, HyperLogLogOps.cs)libs/server/Storage/Session/ObjectStore/— collection ops ([ObjectName]Ops.cs)libs/server/Storage/Session/UnifiedStore/— unified ops (UnifiedStoreOps.cs)
- Object implementations:
libs/server/Objects/[ObjectName]/— per-type logic (Hash, List, Set, SortedSet, SortedSetGeo)
- Network/Session (
libs/common/Networking/,libs/server/Sessions/) — Shared-memory network design where TLS and storage ops run on IO completion threads.GarnetServerTcpaccepts connections, createsServerTcpNetworkHandlerper client.GarnetProvidercreatesRespServerSessioninstances to handle RESP messages. - RESP Command Processing (
libs/server/Resp/) — Commands are defined asRespCommandenum values and dispatched via switch expressions inProcessBasicCommands/ProcessArrayCommands. TheRespServerSessionclass is split across multiple partial.csfiles organized by command category. - Storage API (
libs/server/API/) — Narrow-waist API (IGarnetApiinheritsIGarnetReadApi+IGarnetAdvancedApi) with read, upsert, delete, and atomic read-modify-write operations. Command handlers are generic overTGarnetApifor testability.StorageSessionwraps Tsavorite API calls. API methods for string, object, and unified commands are split acrossGarnetApi.cs,GarnetApiObjectCommands.cs, andGarnetApiUnifiedCommands.cs. - Tsavorite Engine (
libs/storage/Tsavorite/cs/src/core/) — Has its own solution (Tsavorite.slnx) and test project. Provides concurrent key-value storage with checkpointing, tiered storage, recovery, and epoch-based memory reclamation. Relies heavily onSpan<T>andSpanBytefor zero-copy memory management. - Cluster (
libs/cluster/) — Sharding, replication, gossip protocol, key migration. Interface defined inlibs/server/Cluster/IClusterProvider.cs, implementation inlibs/cluster/Server/. - Database Management (
libs/server/Databases/) — Factory pattern withSingleDatabaseManagerandMultiDatabaseManagerimplementations behindIDatabaseManager. Multi-database only available when cluster mode is off. EachRespServerSessionmanages a map ofGarnetDatabaseSessioninstances (one per database index).
The codebase uses using aliases extensively for complex generic store types. libs/GlobalUsings.cs defines the key aliases: BasicGarnetApi, TransactionalGarnetApi, StringBasicContext, ObjectBasicContext, UnifiedBasicContext, StoreAllocator, and their transactional variants. See also the top of RespServerSession.cs and StoreWrapper.cs.
Full guide: https://microsoft.github.io/garnet/docs/dev/garnet-api
- Define the command: Add enum value to
RespCommandinlibs/server/Resp/Parser/RespCommand.cs. For object commands (List, SortedSet, Hash, Set), also add a value to the[ObjectName]Operationenum inlibs/server/Objects/[ObjectName]/[ObjectName]Object.cs. - Add parsing logic: In
libs/server/Resp/Parser/RespCommandHashLookupData.cs, add an entry toPopulatePrimaryTable()(e.g.,Add("MYNEWCMD", RespCommand.MYNEWCMD)). For commands with subcommands, sethasSub: trueand add a subcommand table. The hash table provides O(1) lookup for all command name lengths. - Declare the API method: Add method signature to
IGarnetReadApi(read-only) orIGarnetApi(read-write) inlibs/server/API/IGarnetApi.cs. - Implement the network handler: Add a method to
RespServerSession(the class is split across ~22 partial.csfiles — object commands go inlibs/server/Resp/Objects/[ObjectName]Commands.cs, others inlibs/server/Resp/BasicCommands.cs,ArrayCommands.cs,AdminCommands.cs,KeyAdminCommands.cs, etc.). The handler parses arguments from the network buffer viaparseState.GetArgSliceByRef(i)(returnsref PinnedSpanByte), calls the storage API, and writes the RESP response usingRespWriteUtilshelper methods, then callsSendAndReset()to flush the response buffer. - Add dispatch route: In
libs/server/Resp/RespServerSession.cs, add a case toProcessBasicCommandsorProcessArrayCommandscalling the handler from step 4. - Implement storage logic: Add method to
StorageSession. Choose the appropriate context based on the command type:- String commands: Add to
libs/server/Storage/Session/MainStore/MainStoreOps.cs. Call Tsavorite'sReadorRMWvia the string context. For RMW, implement init/update logic inlibs/server/Storage/Functions/MainStore/RMWMethods.cs. - Object (collection) commands: Add to
libs/server/Storage/Session/ObjectStore/[ObjectName]Ops.cs. CallReadObjectStoreOperationorRMWObjectStoreOperationvia the object context, then implement the case inlibs/server/Objects/[ObjectName]/[ObjectName]ObjectImpl.cs. - Type-agnostic commands (EXISTS, DELETE, TTL, EXPIRE, TYPE, etc.): Add to
libs/server/Storage/Session/UnifiedStore/UnifiedStoreOps.cs. Use the unified context. Implement callbacks inlibs/server/Storage/Functions/UnifiedStore/RMWMethods.cs.
- String commands: Add to
- Transaction support: For standard commands, define
KeySpecsin the command's metadata — the framework automatically handles key locking viaTxnKeyManager.LockKeys(). For custom multi-key operations, manually calltxnManager.SaveKeyEntryToLock(key, lockType)inlibs/server/Transaction/TxnKeyManager.cs; the key is aPinnedSpanBytein the unified key-space, so object-vs-string handling is managed internally by the transaction layer. - Tests: Add tests using both
StackExchange.RedisandLightClientwhere applicable. Object command tests go intest/Garnet.test/Resp[ObjectName]Tests.cs, others intest/Garnet.test/RespTests.csor similar. - Documentation: Update the appropriate markdown file under
website/docs/commands/and mark the command as supported inwebsite/docs/commands/api-compatibility.md. - Command info metadata: Add the command to
playground/CommandInfoUpdater/SupportedCommand.cs, then run the updater tool:cd playground/CommandInfoUpdater dotnet run -- --output ../../libs/resources
Tip: Write a basic test calling the new command first, then implement missing logic as you debug.
Four extensibility points, all in C#. See https://microsoft.github.io/garnet/docs/dev/custom-commands
CustomRawStringFunctions— Custom operations on raw strings (example:main/GarnetServer/Extensions/DeleteIfMatch.cs)CustomObjectBase+CustomObjectFactory— Custom data types for the object store (example:main/GarnetServer/Extensions/MyDictObject.cs)CustomTransactionProcedure— Server-side multi-key transactions (example:main/GarnetServer/Extensions/ReadWriteTxn.cs)CustomProcedure— Non-transactional server-side stored procedures (example:main/GarnetServer/Extensions/Sum.cs)
Register server-side via server.Register (a RegisterApi instance on GarnetServer) with .NewCommand(), .NewTransactionProc(), .NewProcedure(), or .NewType(). Client-side registration uses the REGISTERCS admin command with assemblies on the server.
All C# files require this header (enforced by .editorconfig file_header_template):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.This same header is used throughout the entire codebase, including Tsavorite files.
- Framework: NUnit with
[TestFixture],[Test],[SetUp],[TearDown] - Allure required: All test fixtures must inherit from
AllureTestBaseand have the[AllureNUnit]attribute. CI enforces this via assembly reflection checks — builds will fail if any test fixture is missing either. - Server lifecycle: Create in
[SetUp]viaTestUtils.CreateGarnetServer(TestUtils.MethodTestDir), call.Start(), then.Dispose()in[TearDown]. Common optional parameters includeenableAOF,lowMemory,enableTLS,enableCluster,tryRecover,disableObjects,useAcl, anddefaultPassword. - Teardown: Always call
TestUtils.OnTearDown()(checks for leakedLightEpochinstances) - Test directory cleanup:
TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true)at the start of[SetUp] - Namespace: Test files use
Garnet.testnamespace (even files in subdirectories likeDiskANN/) - Clients in tests: Use
StackExchange.Redisfor high-level operations,LightClientfor raw RESP protocol testing
[TestFixture]
[AllureNUnit]
public class MyTests : AllureTestBase
{
GarnetServer server;
[SetUp]
public void Setup()
{
TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true);
server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir);
server.Start();
}
[TearDown]
public void TearDown()
{
server.Dispose();
TestUtils.OnTearDown();
}
[Test]
public void MyTest()
{
using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
var db = redis.GetDatabase(0);
// ... test using StackExchange.Redis
}
}To add a new Garnet server setting:
- Add property to
Optionsclass inlibs/host/Configuration/Options.cswith[Option]attribute - Add default value in
libs/host/defaults.conf - If needed in core code, add matching property to
GarnetServerOptions(libs/server/Servers/GarnetServerOptions.cs) and map it inOptions.GetServerOptions() - Add tests in
test/Garnet.test/GarnetServerConfigTests.cs
- 4-space indentation, Allman braces (opening brace on new line)
varpreferred when type is apparentunsafeandAllowUnsafeBlocksenabled globally- Private/internal fields: camelCase; constants and statics: PascalCase
TreatWarningsAsErrorsis enabled — all warnings must be resolved- Central package version management via
Directory.Packages.props - XML doc comments (
/// <summary>) are strongly recommended on public methods, with<param>tags for each parameter; analyzer rules for missing docs are currently configured as suggestions (see.editorconfig) - Comment format:
// Comment starting with a capital letter(one space after//)
- Use
Span<T>andSpanByteextensively for zero-copy memory management — avoid allocations on hot paths - Use
[MethodImpl(MethodImplOptions.AggressiveInlining)]for hot-path methods - Use
[MethodImpl(MethodImplOptions.NoInlining)]for cold-path and exception-throwing methods - Tsavorite uses
LightEpochfor epoch-based safe memory reclamation — acquire epoch protection before store operations, release after LightEpochinstances track ownership — only dispose if owned- In parallel tests, share a
LightEpochinstance acrossGarnetClientinstances
StorageSession has two scratch buffer types — use the right one:
-
ScratchBufferBuilder(SBB) — Single contiguous buffer for temporary workspace. All data is laid out sequentially in one buffer. On expansion, the previous data is copied into a new larger buffer and the old buffer is freed — any existing pointers into the old buffer become invalid. Use for building command inputs, Lua serialization, or any data that is consumed immediately and then rewound. Do not returnPinnedSpanBytefrom SBB to callers — it may be invalidated by subsequent allocations. Always rewind after use. Debug builds enforce single-outstanding-slice discipline via asserts.- Key APIs:
CreateArgSlice(returnsPinnedSpanByte, must rewind),CreateArgSliceAsOffset(returns(Offset, Length), safe for multi-alloc since offsets survive reallocation),ViewRemainingArgSlice/ViewFullArgSlice(immediate-use views, do not store),MoveOffset,Reset,RewindScratchBuffer.
- Key APIs:
-
ScratchBufferAllocator(SBA) — Maintains a collection of fragmented pinned buffers (viaGC.AllocateArray(_, true)). When the current buffer fills, a new one is allocated and the old buffer is kept rooted in a stack — so previously returnedPinnedSpanBytevalues remain valid. Use forPinnedSpanBytevalues returned viaoutparameters orIGarnetApithat callers retain across multiple API calls. Reset between batches.- Key APIs:
CreateArgSlice,ViewRemainingArgSlice,Reset.
- Key APIs:
Rules:
- Any
StorageSessionorIGarnetApimethod returningPinnedSpanByteviaoutmust use SBA, not SBB. - When using SBB's
CreateArgSlice, alwaysRewindScratchBufferafter use — debug asserts enforce at most one outstanding slice. - For multiple allocations without rewind, use
CreateArgSliceAsOffset(returns offsets that survive reallocation). - When copying from
IMemoryOwner<byte>(e.g.,ObjectOutput.SpanByteAndMemory.Memory), alwaysDispose()after copying — do not leak pooled buffers. ViewFullArgSliceandViewRemainingArgSlicereturn immediate-use views — do not store or return them.
To update the version and make a new release, increment the VersionPrefix in Version.props at the repo root and submit a PR with that change.
- Create a GitHub Issue (Enhancement / Bug / Task)
- Branch naming:
<username>/branch-name - Include unit tests with Allure wiring (see Test Structure above)
- Link PR to the issue in the development section