Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
name: Publish Release to GitHub Packages
name: Publish

on:
push:
tags:
- "v*.*.*"
- "v*.*.*-*"

permissions:
contents: read
packages: write

defaults:
run:
shell: bash

jobs:
publish:
nuget:
name: NuGet
environment: nuget
runs-on: ubuntu-latest

steps:
Expand All @@ -24,18 +28,18 @@ jobs:
with:
dotnet-version: 8.x.x

- name: Build and strong name
run: dotnet build --configuration Release --no-incremental -p:version=${GITHUB_REF#refs/*/v} -p:SignAssembly=true -p:AssemblyOriginatorKeyFile=../key.snk

- name: Pack SDK
run: dotnet pack -p:version=${GITHUB_REF#refs/*/v} -o ./publish

- name: Configure NuGet source
run: dotnet nuget add source --username ${GITHUB_REPOSITORY_OWNER} --password ${GITHUB_TOKEN} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/${GITHUB_REPOSITORY_OWNER}/index.json"
- name: Write strong name key file
run: |
set +x # Disable command echoing for security
echo "$STRONG_NAME_KEY" > NatsDistributedCache.snk
chmod 600 NatsDistributedCache.snk
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
STRONG_NAME_KEY: ${{secrets.STRONG_NAME_KEY}}

- name: Pack SDK
run: dotnet pack -c Release -p:version=${GITHUB_REF#refs/*/v} -o ./publish

- name: Publish to GitHub Packages
run: dotnet nuget push ./publish/*.nupkg --skip-duplicate --source github
- name: Publish to NuGet.org
run: dotnet nuget push ./publish/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate
env:
NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
NUGET_API_KEY: ${{secrets.NUGET_API_KEY}}
47 changes: 47 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Contributing

## Updating Packages

Use the `dotnet outdated` tool to update packages in `.csproj` files. When updating packages in a project with lock
files, always use the `-n` flag to prevent automatic restoration. To update tools themselves, edit
`.config/dotnet-tools.json`.

```bash
# all updates
# view
dotnet outdated
# apply all (with -n to prevent automatic restore)
dotnet outdated -n -u
# prompt
dotnet outdated -n -u:prompt

# minor updates only
# view
dotnet outdated -vl Major
# apply all (with -n to prevent automatic restore)
dotnet outdated -n -vl Major -u
# prompt
dotnet outdated -n -vl Major -u:prompt

After updating dependencies, you must update the lock files for all supported platforms by running the update script (see next section).

## Updating NuGet Lock Files

This project uses NuGet package lock files for reproducible builds across different platforms. When packages are updated, the lock files need to be regenerated for all supported platforms.

Use the provided script to update all platform-specific lock files:

```bash
./dev/update-nuget-lockfiles.sh
```

This will generate lock files for:

- Linux x64: `packages.linux-x64.lock.json`
- macOS ARM64: `packages.osx-arm64.lock.json`
- Windows x64: `packages.win-x64.lock.json`

These lock files are used automatically based on the runtime identifier specified during build/restore.

**Important**: Always run this script after updating package dependencies to ensure all platform-specific lock files are
properly regenerated.
76 changes: 44 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,59 @@
![NuGet Version](https://img.shields.io/nuget/v/CodeCargo.Nats.DistributedCache?cacheSeconds=3600&color=516bf1)

# CodeCargo.Nats.DistributedCache

## Updating Packages
## Overview

A .NET 8+ library for integrating NATS as a distributed cache in ASP.NET Core applications. Supports the new HybridCache system for fast, scalable caching.

## Requirements

- NATS 2.11 or later
- A NATS KV bucket with `LimitMarkerTTL` set for per-key TTL support. Example:
```csharp
// assuming an INatsConnection natsConnection
var kvContext = natsConnection.CreateKeyValueStoreContext();
await kvContext.CreateOrUpdateStoreAsync(
new NatsKVConfig("cache") { LimitMarkerTTL = TimeSpan.FromSeconds(1), });
```

Use the `dotnet outdated` tool to update packages in `.csproj` files. When updating packages in a project with lock
files, always use the `-n` flag to prevent automatic restoration. To update tools themselves, edit
`.config/dotnet-tools.json`.
## Installation

```bash
# all updates
# view
dotnet outdated
# apply all (with -n to prevent automatic restore)
dotnet outdated -n -u
# prompt
dotnet outdated -n -u:prompt
dotnet add package CodeCargo.Nats.DistributedCache
```

# minor updates only
# view
dotnet outdated -vl Major
# apply all (with -n to prevent automatic restore)
dotnet outdated -n -vl Major -u
# prompt
dotnet outdated -n -vl Major -u:prompt
## Usage

After updating dependencies, you must update the lock files for all supported platforms by running the update script (see next section).
```csharp
using Microsoft.Extensions.DependencyInjection;
using NATS.Client.Core;
using CodeCargo.NatsDistributedCache;

## Updating NuGet Lock Files
var builder = WebApplication.CreateBuilder(args);

This project uses NuGet package lock files for reproducible builds across different platforms. When packages are updated, the lock files need to be regenerated for all supported platforms.
// Add a NATS connection
var natsOpts = NatsOpts.Default with { Url = "nats://localhost:4222" }
builder.Services.AddNats(opts => natsOpts);

Use the provided script to update all platform-specific lock files:
// Add a NATS distributed cache
builder.Services.AddNatsDistributedCache(options =>
{
options.BucketName = "cache";
});

```bash
./dev/update-nuget-lockfiles.sh
```
// (Optional) Add HybridCache
var hybridCacheServices = builder.Services.AddHybridCache();

This will generate lock files for:
// (Optional) Use NATS Serializer for HybridCache
hybridCacheServices.AddSerializerFactory(
natsOpts.SerializerRegistry.ToHybridCacheSerializerFactory());

- Linux x64: `packages.linux-x64.lock.json`
- macOS ARM64: `packages.osx-arm64.lock.json`
- Windows x64: `packages.win-x64.lock.json`
var app = builder.Build();
app.Run();
```

These lock files are used automatically based on the runtime identifier specified during build/restore.
## Additional Resources

**Important**: Always run this script after updating package dependencies to ensure all platform-specific lock files are
properly regenerated.
* [ASP.NET Core Hybrid Cache Documentation](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0)
* [NATS .NET Client Documentation](https://nats-io.github.io/nats.net/api/NATS.Client.Core.NatsOpts.html)
57 changes: 57 additions & 0 deletions dev/generate-snk.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/bin/bash
set -e

# Navigate to the root directory containing the .sln file
cd "$(dirname "$0")/.."

# Find the .sln file in the current directory
CUR_DIR="$(pwd)"
SLN_FILE=$(ls "$CUR_DIR"/*.sln 2>/dev/null | head -n 1)
if [ -z "$SLN_FILE" ]; then
echo "Error: No .sln file found."
exit 1
fi

# Extract the solution name without the extension
SLN_NAME=$(basename "$SLN_FILE" .sln)

# Get the current date
DATE="$(date "+%Y-%m-%d")"

# Define key names
KEYS_DIR="$CUR_DIR/keys"
SNK_FILE="${KEYS_DIR}/${SLN_NAME}.${DATE}.snk"
PUB_FILE="${KEYS_DIR}/${SLN_NAME}.${DATE}.pub"

# Function to run sn tool using Docker
run_sn_docker() {
docker run --rm \
-v "$KEYS_DIR:/mnt/keys" \
-w "/mnt/keys" \
-u "$(id -u):$(id -g)" \
mono:latest sn "$@"
}

# Create the keys directory if it doesn't exist
mkdir -p "$KEYS_DIR"

# Add .gitignore to prevent committing private keys
echo "*.snk" > "${KEYS_DIR}/.gitignore"

# Generate the SNK file directly using sn tool
echo "Generating SNK file..."
run_sn_docker -k "$(basename "${SNK_FILE}")"

# Extract the public key
echo "Extracting public key..."
PUB_KEY_NAME="$(basename "${PUB_FILE}")"
run_sn_docker -p "$(basename "${SNK_FILE}")" "${PUB_KEY_NAME}"

# Display the public key token (helpful for reference)
echo "Public key token:"
run_sn_docker -tp "${PUB_KEY_NAME}"

# Print success message
echo "Generated SNK file: ${SNK_FILE}"
echo "Generated public key file: ${PUB_FILE}"
echo "Done."
1 change: 1 addition & 0 deletions keys/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.snk
Binary file added keys/NatsDistributedCache.2025-05-12.pub
Binary file not shown.
79 changes: 39 additions & 40 deletions src/NatsDistributedCache/NatsCacheServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,52 @@
using Microsoft.Extensions.Options;
using NATS.Client.Core;

namespace CodeCargo.Nats.DistributedCache
namespace CodeCargo.Nats.DistributedCache;

/// <summary>
/// Extension methods for setting up NATS distributed cache related services in an <see cref="IServiceCollection" />.
/// </summary>
public static class NatsCacheServiceCollectionExtensions
{
/// <summary>
/// Extension methods for setting up NATS distributed cache related services in an <see cref="IServiceCollection" />.
/// Adds NATS distributed caching services to the specified <see cref="IServiceCollection" />.
/// </summary>
public static class NatsCacheServiceCollectionExtensions
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="configureOptions">An <see cref="Action{NatsCacheOptions}"/> to configure the provided
/// <see cref="NatsCacheOptions"/>.</param>
/// <param name="connectionServiceKey">If set, used keyed service to resolve <see cref="INatsConnection"/></param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddNatsDistributedCache(
this IServiceCollection services,
Action<NatsCacheOptions> configureOptions,
object? connectionServiceKey = null)
{
/// <summary>
/// Adds NATS distributed caching services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="configureOptions">An <see cref="Action{NatsCacheOptions}"/> to configure the provided
/// <see cref="NatsCacheOptions"/>.</param>
/// <param name="connectionServiceKey">If set, used keyed service to resolve <see cref="INatsConnection"/></param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddNatsDistributedCache(
this IServiceCollection services,
Action<NatsCacheOptions> configureOptions,
object? connectionServiceKey = null)
services.AddOptions();
services.Configure(configureOptions);
services.Add(ServiceDescriptor.Singleton<IDistributedCache, NatsCacheImpl>(serviceProvider =>
{
services.AddOptions();
services.Configure(configureOptions);
services.Add(ServiceDescriptor.Singleton<IDistributedCache, NatsCacheImpl>(serviceProvider =>
{
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<NatsCacheOptions>>();
var logger = serviceProvider.GetService<ILogger<NatsCache>>();

var natsConnection = connectionServiceKey == null
? serviceProvider.GetRequiredService<INatsConnection>()
: serviceProvider.GetRequiredKeyedService<INatsConnection>(connectionServiceKey);
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<NatsCacheOptions>>();
var logger = serviceProvider.GetService<ILogger<NatsCache>>();

return logger != null
? new NatsCacheImpl(optionsAccessor, logger, serviceProvider, natsConnection)
: new NatsCacheImpl(optionsAccessor, serviceProvider, natsConnection);
}));
var natsConnection = connectionServiceKey == null
? serviceProvider.GetRequiredService<INatsConnection>()
: serviceProvider.GetRequiredKeyedService<INatsConnection>(connectionServiceKey);

return services;
}
return logger != null
? new NatsCacheImpl(optionsAccessor, logger, serviceProvider, natsConnection)
: new NatsCacheImpl(optionsAccessor, serviceProvider, natsConnection);
}));

/// <summary>
/// Creates an <see cref="IHybridCacheSerializerFactory"/> that uses the provided
/// <see cref="INatsSerializerRegistry"/> to perform serialization.
/// </summary>
/// <param name="serializerRegistry">The <see cref="INatsSerializerRegistry"/> instance</param>
/// <returns>The <see cref="IHybridCacheSerializerFactory"/> instance</returns>
public static IHybridCacheSerializerFactory ToHybridCacheSerializerFactory(
this INatsSerializerRegistry serializerRegistry) =>
new NatsHybridCacheSerializerFactory(serializerRegistry);
return services;
}

/// <summary>
/// Creates an <see cref="IHybridCacheSerializerFactory"/> that uses the provided
/// <see cref="INatsSerializerRegistry"/> to perform serialization.
/// </summary>
/// <param name="serializerRegistry">The <see cref="INatsSerializerRegistry"/> instance</param>
/// <returns>The <see cref="IHybridCacheSerializerFactory"/> instance</returns>
public static IHybridCacheSerializerFactory ToHybridCacheSerializerFactory(
this INatsSerializerRegistry serializerRegistry) =>
new NatsHybridCacheSerializerFactory(serializerRegistry);
}
26 changes: 25 additions & 1 deletion src/NatsDistributedCache/NatsDistributedCache.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,38 @@
<Nullable>enable</Nullable>
<RootNamespace>CodeCargo.Nats.DistributedCache</RootNamespace>
<Description>NATS implementation of Microsoft.Extensions.Caching.Distributed.IDistributedCache.</Description>
<PackageTags>cache;caching;distributed;nats</PackageTags>

<!-- Assembly signing (only if key file exists) -->
<SignAssembly Condition="Exists('$(MSBuildThisFileDirectory)..\..\keys\NatsDistributedCache.2025-05-12.snk')">true</SignAssembly>
<AssemblyOriginatorKeyFile Condition="Exists('$(MSBuildThisFileDirectory)..\..\keys\NatsDistributedCache.2025-05-12.snk')">$(MSBuildThisFileDirectory)..\..\keys\NatsDistributedCache.2025-05-12.snk</AssemblyOriginatorKeyFile>

<!-- NuGet metadata -->
<Authors>CodeCargo</Authors>
<Company>CodeCargo</Company>
<Copyright>Copyright © $([System.DateTime]::Now.Year) CodeCargo</Copyright>
<PackageId>CodeCargo.Nats.DistributedCache</PackageId>
<PackageTags>distributed cache;hybrid cache;nats</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/code-cargo/NatsDistributedCache</PackageProjectUrl>
<RepositoryUrl>https://github.com/code-cargo/NatsDistributedCache</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.4" />
<PackageReference Include="NATS.Client.KeyValueStore" Version="2.6.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
Loading