Skip to content

docs(sample): run a second, complete caching stack on its own Redis connection - #181

Open
cosmin-staicu wants to merge 2 commits into
mainfrom
sample/second-redis-connection
Open

docs(sample): run a second, complete caching stack on its own Redis connection#181
cosmin-staicu wants to merge 2 commits into
mainfrom
sample/second-redis-connection

Conversation

@cosmin-staicu

Copy link
Copy Markdown
Member

What

Two complete caching stacks in the sample app, each on its own Redis server: ICache, IHashCache, ISetCache, IDistributedLock, broadcast topics, warm-up and planned maintenance, twice. No library API changes.

The library wires one Redis connection per container, and several Redis-backed classes (RedisDistributedLock, RedisConnectionWarmup, UiPathDistributedCache) are internal, so a consumer cannot assemble a second stack by hand. What works today is running AddCaching a second time in a child ServiceCollection and bridging what consumers resolve into the application container as keyed services.

The child reads the same Caching section with only Connections:Redis replaced, key by key, by Connections:SecondaryRedis. App name, policies and provider options stay identical by construction; the split is visibly a matter of which server a key lands on. A ~25-line hosted service forwards the child's hosted services (warm-up, planned maintenance, stream health maintainer) to the root host. Disposing the root disposes the child.

Changes

  • samples/UiPath.Caching.Sample/SecondaryCaching.cs: the bridge (AddSecondaryCaching), keyed under "secondary".
  • Secondary* controller twins for every surface, plus new SetCache and DistributedLock controllers on both stacks; RedisConnectionController split into a base and two derived controllers.
  • Aspire AppHost provisions a cache-secondary container and injects Caching__Connections__SecondaryRedis__ConnectionString.
  • Both connections now WarmUpOnStart. The operations that check IsConnected first (every ISetCache call, L2 reads) never open a connection themselves, so without warm-up the set endpoints silently no-op until the first string write. Found while verifying; behaviour is pre-existing and unrelated to the second connection.
  • docs/recipes/second-redis-connection.md: the pattern, what is shared vs. separate, when not to use it. Linked from docs/index.md and docs/sample-app.md.
  • tests/.../Config/SecondCachingContainerTests.cs: pins the shape the recipe relies on (separate connectors/factories/locks/caches, shared logger and clock, connection-key override and inheritance, empty secondary section refused, hosted-service bridge, disposal).

Verified

  • Full unit suite: 1727 passed, 0 failed (12 skipped, as before).
  • Live, against two local redis-server processes (6379/6380), one app process: sets/strings/hashes/locks on both stacks, keys land only on their own server, both handshakes at startup, /healthz 200.

Follow-up (separate PR)

A library-level AddNamedCaching(name, ...) doing this bridge officially, with guard rails and typed keyed access. Outline in the recipe's "When not to use".

🤖 Generated with Claude Code

https://claude.ai/code/session_01K2d7HhcRTtYYHgCKAD1hzm

…onnection

The library wires one Redis connection per container. Everything Redis-backed
(caches, set caches, distributed lock, broadcast topics, warm-up, planned
maintenance) takes the single IRedisConnector, and several of those classes are
internal, so a consumer cannot assemble a second stack by hand.

What works today is running AddCaching a second time in a child
ServiceCollection and bridging what consumers resolve into the application
container as keyed services. The child reads the same "Caching" section with
only Connections:Redis replaced, key by key, by Connections:SecondaryRedis, so
app name, policies and provider options stay identical by construction and the
split is visibly a matter of which server a key lands on. A small hosted
service forwards the child's warm-up, planned-maintenance and stream-maintainer
services to the root host; disposing the root disposes the child.

The sample app wires this end to end: SecondaryCaching.cs holds the bridge,
every surface gets a Secondary* controller twin (ICache, IHashCache, ISetCache,
IDistributedLock, connection status), and the Aspire AppHost provisions a
second Redis container and injects its connection string. Both connections now
warm up on start: the operations that check IsConnected first (all set-cache
calls, L2 reads) never open a connection themselves, so without warm-up the
set endpoints would silently no-op until the first string write.

A recipe documents the pattern and what is shared versus separate; a test pins
the shape it relies on. No library API changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2d7HhcRTtYYHgCKAD1hzm
Signed-off-by: Cosmin Staicu <cosmin.staicu@uipath.com>
… and the annotated settings

appsettings.all.json is the every-knob reference that docs/reference/settings.md
mirrors, and neither showed Connections:SecondaryRedis, so the configuration for
the second stack could only be found by reading the recipe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K2d7HhcRTtYYHgCKAD1hzm
Signed-off-by: Cosmin Staicu <cosmin.staicu@uipath.com>
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Secondary connection validation can silently reuse the primary Redis endpoint, and documentation contains an invalid example.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds a sample and recipe for running two caching stacks against separate Redis servers.

Changes:

  • Builds a secondary stack in a child DI container and exposes keyed services.
  • Adds secondary Redis infrastructure, controllers, configuration, and tests.
  • Documents the setup and operational behavior.
File summaries
File Description
tests/UiPath.Caching.Tests/Config/SecondCachingContainerTests.cs Tests child-container isolation and lifecycle.
samples/UiPath.Caching.Sample/UiPath.Caching.Sample.csproj References the Queue package.
samples/UiPath.Caching.Sample/SecondaryCaching.cs Registers the secondary caching stack.
samples/UiPath.Caching.Sample/Program.cs Enables set caching and secondary registration.
samples/UiPath.Caching.Sample/Controllers/SetCacheController.cs Adds primary and secondary set endpoints.
samples/UiPath.Caching.Sample/Controllers/SecondaryCacheController.cs Adds secondary cache and hash endpoints.
samples/UiPath.Caching.Sample/Controllers/RedisConnectionController.cs Adds secondary connection diagnostics.
samples/UiPath.Caching.Sample/Controllers/DistributedLockController.cs Adds lock demonstration endpoints.
samples/UiPath.Caching.Sample/appsettings.json Configures warm-up and secondary Redis.
samples/UiPath.Caching.Sample/appsettings.all.json Documents secondary connection settings.
samples/UiPath.Caching.Sample.AppHost/AppHost.cs Provisions and injects secondary Redis.
docs/sample-app.md Documents the expanded sample.
docs/reference/settings.md Describes secondary connection configuration.
docs/recipes/second-redis-connection.md Adds the implementation recipe.
docs/index.md Links the new recipe.
CHANGELOG.md Records the sample and recipe.
Review details
  • Files reviewed: 16/16 changed files
  • Comments generated: 6
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +56 to +59
if (overrides.Count == 0)
{
throw new InvalidOperationException($"'{SecondaryConnectionSection}' is empty; the secondary caching stack needs at least a ConnectionString there.");
}
Comment on lines +72 to +77
public void An_empty_secondary_connection_section_is_refused_at_registration()
{
var act = () => Build(settings: [("Caching:Connections:SecondaryRedis:ConnectionString", null)]);

act.Should().Throw<InvalidOperationException>().WithMessage("*Caching:Connections:SecondaryRedis*");
}
Comment on lines +176 to +179
if (overrides.Count == 0)
{
throw new InvalidOperationException("'Caching:Connections:SecondaryRedis' is empty; the secondary caching stack needs at least a ConnectionString there.");
}
Comment on lines +71 to +74
if (overrides.Count == 0)
{
throw new InvalidOperationException($"'{SecondaryConnectionSection}' is empty; the secondary caching stack needs at least a ConnectionString there.");
}

The child container is the whole second stack. Everything `AddCaching` and the builder chain register in it, from the `RedisConnector` down to the broadcast topic providers and the L1 memory caches of `AddInMemoryRedis`, lives once per container, so nothing Redis-facing is shared. Disposing the application container disposes the keyed `IServiceProvider` and with it the child graph.

The child reads the **same** `Caching` section, so `AppShortName`, `KeyCasing`, policies, provider options and the queue options are identical by construction. Only `Connections:Redis` differs: every key under `Connections:SecondaryRedis` is laid over it, so the second server can also carry its own `ConnectionStringExtraParams`, `WarmUpOnStart` or `ProfilerEnabled` when it needs them, and inherits the primary's values otherwise. If the second server must differ in *cache* options too, bind the child from its own section instead: `child.AddCaching(configuration.GetSection("CachingSecondary"), chain, opt => section.Bind(opt))`; the two sections then have to agree on `KeyCasing`, because `AddCaching` seeds the process-wide `CacheKey.DefaultCasing`.
Comment on lines +6 to +7
// The secondary stack is used the way the primary is: through its ICacheFactory, which picks the provider
// (InMemoryRedis, Redis, InMemory) from the CachingSecondary section's DefaultCache.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants