diff --git a/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml
index aba7fb9a01..03d4f38fc7 100644
--- a/.github/workflows/bit.full.ci.yml
+++ b/.github/workflows/bit.full.ci.yml
@@ -70,7 +70,7 @@ jobs:
run: cd src && dotnet workload install wasm-tools
- name: Create project from template with PostgreSQL
- run: dotnet new bit-bp --name TestPostgreSQL --database PostgreSQL --module Sales --signalR --aspire
+ run: dotnet new bit-bp --name TestPostgreSQL --database PostgreSQL --module Sales --signalR --aspire --redis
- name: Create appsettings.json for Client.Web
run: |
@@ -229,13 +229,13 @@ jobs:
- name: Build sample configuration 1
run: |
- dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --module Admin --offlineDb --appInsights --sentry --signalR --notification --cloudflare --ads --aspire
+ dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --module Admin --offlineDb --appInsights --sentry --signalR --notification --cloudflare --ads --aspire --redis
dotnet build TestProject/TestProject.sln -p:InvariantGlobalization=false -p:Environment=Staging
rm -r "TestProject"
- name: Build sample configuration 2
run: |
- dotnet new bit-bp --name TestProject2 --database Other --filesStorage S3 --api Standalone --captcha None --pipeline None --module None --offlineDb false --appInsights false --sentry false --signalR false --notification false --cloudflare false --ads false --aspire true
+ dotnet new bit-bp --name TestProject2 --database Other --filesStorage S3 --api Standalone --captcha None --pipeline None --module None --offlineDb false --appInsights false --sentry false --signalR false --notification false --cloudflare false --ads false --aspire true --redis false
dotnet build TestProject2/TestProject2.slnx -p:InvariantGlobalization=true -p:Environment=Development
rm -r "TestProject2"
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/14- Response Caching System.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/14- Response Caching System.md
index 54a08b7e07..855e10f42c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/14- Response Caching System.md
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/14- Response Caching System.md
@@ -494,6 +494,28 @@ The project uses the **FusionCache** library for server-side caching:
---
+## Redis Infrastructure
+
+The project uses **two separate Redis instances** for different purposes:
+
+### 1. redis-cache Ephemeral Cache
+- **No persistence** (data stored only in memory)
+- **Use Cases**:
+ - **FusionCache** L2 distributed cache and backplane for multi-server cache synchronization
+ - **SignalR backplane** for real-time messaging across servers
+- **Why**: Cache data is regenerable, no need for disk I/O overhead
+
+### 2. redis-persistent - Persistent Storage
+- **AOF enabled** with synchronous disk writes for maximum durability
+- **Use Cases**:
+ - **Hangfire** background job queues and state
+ - **Distributed locking** for coordinating operations
+- **Why**: Critical data that cannot be easily regenerated must survive restarts
+
+**Benefits**: Separation allows ephemeral cache to run faster while ensuring critical infrastructure data is never lost.
+
+---
+
### Monitor Cache Headers
The system adds custom headers to help debug caching:
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.github/copilot-instructions.md b/src/Templates/Boilerplate/Bit.Boilerplate/.github/copilot-instructions.md
index be29f22378..25ff109adf 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/.github/copilot-instructions.md
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/.github/copilot-instructions.md
@@ -22,6 +22,9 @@ You will be working with the following key technologies:
* **SignalR**: Real-time communication
+
+* **Redis**: Distributed caching storage and backplane, hangfire job storage, signalr backplane and distributed lock.
+
* **Hangfire**: Background job processing
* **OData**: Advanced querying capabilities
* **Bit.BlazorUI**: The primary UI component library
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json
index 8000cce67a..9e832fd431 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json
@@ -14,6 +14,9 @@
{
"id": "aspire"
},
+ {
+ "id": "redis"
+ },
{
"id": "database"
},
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
index 87882c4880..15ba9c3ec0 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
@@ -173,6 +173,13 @@
"defaultValue": "true",
"description": "Adds aspire"
},
+ "redis": {
+ "displayName": "Add redis?",
+ "type": "parameter",
+ "datatype": "bool",
+ "defaultValue": "false",
+ "description": "Adds redis"
+ },
"notification": {
"displayName": "Add push notification?",
"type": "parameter",
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
index 54640b774a..6f2510d1dd 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
@@ -47,7 +47,7 @@
-
+
@@ -107,6 +107,13 @@
+
+
+
+
+
+
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj
index 5a0b38d0c4..f0178c4d41 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj
@@ -70,6 +70,9 @@
+
+
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
index 4200b0e758..1b6a51ad71 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
@@ -29,6 +29,10 @@
using FluentEmail.Core;
using FluentStorage.Blobs;
using Hangfire.EntityFrameworkCore;
+//#if (redis == true)
+using StackExchange.Redis;
+using Hangfire.Redis.StackExchange;
+//#endif
//#if (notification == true)
using AdsPush;
using AdsPush.Abstraction;
@@ -161,10 +165,18 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde
services.AddScoped();
//#endif
+ // Register distributed lock factory
+ //#if (redis == true)
+ services.AddTransient(sp => new Func((string lockKey) =>
+ {
+ return new Medallion.Threading.Redis.RedisDistributedLock(lockKey, sp.GetRequiredService().GetDatabase());
+ }));
+ //#else
services.AddTransient(sp => new Func((string lockKey) =>
{
return new Medallion.Threading.FileSystem.FileDistributedLock(new(Path.Combine(Path.GetTempPath(), $"Boilerplate-{lockKey}.lock")));
}));
+ //#endif
services.AddSingleton();
services.AddSingleton(sp => (IProblemDetailsWriter)sp.GetRequiredService());
@@ -240,6 +252,15 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde
options.PayloadSerializerOptions.TypeInfoResolverChain.Add(chain);
}
});
+
+ // Use Redis as SignalR backplane for scaling out across multiple server instances
+ //#if (redis == true)
+ signalRBuilder.AddStackExchangeRedis(configuration.GetRequiredConnectionString("redis-cache"), options =>
+ {
+ options.Configuration.ChannelPrefix = RedisChannel.Literal("signalr:");
+ });
+ //#endif
+
if (string.IsNullOrEmpty(configuration["Azure:SignalR:ConnectionString"]) is false)
{
signalRBuilder.AddAzureSignalR(options =>
@@ -503,9 +524,17 @@ void AddDbContext(DbContextOptionsBuilder options)
}
//#endif
- builder.Services.AddHangfire(configuration =>
+ // Configure Hangfire to use Redis for persistent background job storage
+ builder.Services.AddHangfire((sp, hangfireConfiguration) =>
{
- var efCoreStorage = configuration.UseEFCoreStorage(optionsBuilder =>
+ //#if (redis == true)
+ hangfireConfiguration.UseRedisStorage(sp.GetRequiredService(), new RedisStorageOptions
+ {
+ Prefix = "hangfire:",
+ Db = 1, // Use a dedicated Redis database for Hangfire
+ });
+ //#else
+ var efCoreStorage = hangfireConfiguration.UseEFCoreStorage(optionsBuilder =>
{
if (appSettings.Hangfire?.UseIsolatedStorage is true)
{
@@ -529,11 +558,12 @@ void AddDbContext(DbContextOptionsBuilder options)
{
efCoreStorage.UseDatabaseCreator();
}
+ //#endif
- configuration.UseRecommendedSerializerSettings();
- configuration.UseSimpleAssemblyNameTypeSerializer();
- configuration.UseIgnoredAssemblyVersionTypeResolver();
- configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
+ hangfireConfiguration.UseRecommendedSerializerSettings();
+ hangfireConfiguration.UseSimpleAssemblyNameTypeSerializer();
+ hangfireConfiguration.UseIgnoredAssemblyVersionTypeResolver();
+ hangfireConfiguration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
});
builder.Services.AddHangfireServer(options =>
@@ -552,7 +582,7 @@ private static void AddIdentity(WebApplicationBuilder builder)
configuration.Bind(appSettings);
var identityOptions = appSettings.Identity;
- services.AddIdentity()
+ services.AddIdentity()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders()
.AddErrorDescriber()
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json
index d89c14fe96..4b0d25809e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json
@@ -23,6 +23,12 @@
"s3__DigitalOceanSample": "Endpoint=https://ams3.digitaloceanspaces.com;BucketName=myBucketName;Region=ams3;AccessKey=XXX;SecretKey=XXX;",
"s3__CloudflareR2Sample": "Not supported yet: https://github.com/robinrodricks/FluentStorage/issues/114",
//#endif
+ //#if (redis == true)
+ "redis-cache": "localhost:6379",
+ "redis-cache_Comment": "Redis for FusionCache L2 distributed cache layer and SignalR backplane - no persistence needed, acts as fast ephemeral cache",
+ "redis-persistent": "localhost:6379",
+ "redis-persistent_Comment": "Redis for Hangfire background jobs and distributed locking - requires persistence for durability",
+ //#endif
"smtp": "Endpoint=smtp://smtp.ethereal.email:587;UserName=madisen7@ethereal.email;Password=QYcYfjBXjqxMAZfZya",
"smtp_Comment": "You can also use https://ethereal.email/create for testing purposes.",
"ConnectionStrings_Comment": "The ConnectionStrings section contains database connection strings used by the application."
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Boilerplate.Server.AppHost.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Boilerplate.Server.AppHost.csproj
index 9953470f73..aa4a612288 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Boilerplate.Server.AppHost.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Boilerplate.Server.AppHost.csproj
@@ -14,6 +14,7 @@
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Program.cs
index 682dd986cd..b9dd2f17b7 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Program.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Program.cs
@@ -5,6 +5,25 @@
// Check out appsettings.Development.json for credentials/passwords settings.
+//#if(redis == true)
+// Redis cache for FusionCache hybrid caching (L2 cache) and SignalR backplane - no persistence needed
+var redisCache = builder.AddRedis("redis-cache")
+ .WithRedisInsight()
+ .WithRedisCommander();
+
+// Redis for Hangfire background jobs, and distributed locking - persistent with AOF for durability
+var redisPersistent = builder.AddRedis("redis-persistent")
+ .WithRedisInsight()
+ .WithRedisCommander()
+ .WithDataVolume()
+ .WithArgs(
+ "--appendonly", "yes", // Enable AOF (Append only file) for data durability
+ "--appendfsync", "always", // Sync to disk on every write for maximum durability. Temporarily disable it programmatically using C# code during bulk operations if needed.
+ "--save", "", // Disables RDB snapshots
+ "--maxmemory-policy", "noeviction" // Raise error when memory limit is reached instead of evicting keys
+ );
+//#endif
+
//#if (database == "SqlServer")
var sqlDatabase = builder.AddSqlServer("sqlserver")
.WithDbGate(config => config.WithDataVolume())
@@ -84,6 +103,10 @@
serverApiProject.WithReference(s3Storage);
//#endif
serverApiProject.WithReference(keycloak);
+//#if (redis == true)
+serverApiProject.WithReference(redisCache).WaitFor(redisCache);
+serverApiProject.WithReference(redisPersistent).WaitFor(redisPersistent);
+//#endif
//#else
//#if (database == "SqlServer")
@@ -101,6 +124,10 @@
serverWebProject.WithReference(s3Storage);
//#endif
serverWebProject.WithReference(keycloak);
+//#if (redis == true)
+serverWebProject.WithReference(redisCache).WaitFor(redisCache);
+serverWebProject.WithReference(redisPersistent).WaitFor(redisPersistent);
+//#endif
//#endif
if (builder.ExecutionContext.IsRunMode) // The following project is only added for testing purposes.
@@ -121,12 +148,12 @@
//#if (api == "Standalone")
builder.AddDevTunnel("api-dev-tunnel")
.WithAnonymousAccess()
- .WithReference(serverApiProject.WithHttpEndpoint(name: "devTunnel").GetEndpoint("devTunnel"));
+ .WithReference(serverApiProject.WithHttpEndpoint(name: "devTunnel", port: 5031).GetEndpoint("devTunnel"));
//#endif
var tunnel = builder.AddDevTunnel("web-dev-tunnel")
.WithAnonymousAccess()
- .WithReference(serverWebProject.WithHttpEndpoint(name: "devTunnel").GetEndpoint("devTunnel"));
+ .WithReference(serverWebProject.WithHttpEndpoint(name: "devTunnel", port: 5000).GetEndpoint("devTunnel"));
if (OperatingSystem.IsWindows())
{
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Boilerplate.Server.Shared.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Boilerplate.Server.Shared.csproj
index 48f3946f50..13ff70e31f 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Boilerplate.Server.Shared.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Boilerplate.Server.Shared.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -23,6 +23,9 @@
+
+
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Extensions/WebApplicationBuilderExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Extensions/WebApplicationBuilderExtensions.cs
index 6060bc2ddc..135275d941 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Extensions/WebApplicationBuilderExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Extensions/WebApplicationBuilderExtensions.cs
@@ -46,10 +46,33 @@ public static TBuilder AddServerSharedServices(this TBuilder builder)
}, excludeDefaultPolicy: true);
});
+ //#if(redis == true)
+ // Add default Redis connection for Hangfire, SignalR backplane, and distributed locking (persistence Redis with AOF)
+ builder.AddRedisClient("redis-persistent", config => config.DisableTracing = true);
+
+ // Add optional Redis connection for caching (ephemeral Redis without persistence)
+ builder.AddKeyedRedisClient("redis-cache", config => config.DisableTracing = true /*FusionCache is already handling cache traces*/);
+ //#endif
+
services.AddFusionCache()
// Auto-clone cached objects to avoid further issues after scaling out and switching to distributed caching.
.WithOptions(opt => opt.DefaultEntryOptions.EnableAutoClone = true)
+ //#if(redis == true)
+ // Use Redis backplane for cache synchronization across multiple server instances
+ .WithDistributedCache(sp =>
+ new Caching.StackExchangeRedis.RedisCache(new Caching.StackExchangeRedis.RedisCacheOptions
+ {
+ ConnectionMultiplexerFactory = async () => sp.GetRequiredKeyedService("redis-cache"),
+ })
+ )
+ .WithBackplane(sp => new ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis.RedisBackplane(
+ new ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis.RedisBackplaneOptions
+ {
+ ConnectionMultiplexerFactory = async () => sp.GetRequiredKeyedService("redis-cache"),
+ }))
+ //#endif
.WithSerializer(new FusionCacheSystemTextJsonSerializer());
+
services.AddFusionOutputCache(); // For ASP.NET Core Output Caching with FusionCache
services.AddHttpContextAccessor();
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json
index f84cabd261..80268bcaaf 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json
@@ -6,6 +6,8 @@
"postgresdb": "User ID=postgres;Password=postgres;Host=localhost;Database=BoilerplateDb;",
"mysqldb": "Server=localhost;Port=3306;Database=BoilerplateDb;Uid=root;Pwd=123456;",
"azureblobstorage": "UseDevelopmentStorage=true",
+ "redis-cache": "localhost:6379",
+ "redis-persistent": "localhost:6379",
"smtp": "Endpoint=smtp://smtp.ethereal.email:587;UserName=madisen7@ethereal.email;Password=QYcYfjBXjqxMAZfZya",
"s3": "Endpoint=http://localhost:9000;AccessKey=minioadmin;SecretKey=minioadmin;"
},