Cms.LightSdk.Sapico.AspNet 10.5.0

Cms.LightSdk.Sapico.AspNet

ASP.NET Core integration for the Sapico CMS Content Targeting engine. This package provides middleware, service registration, and OpenTelemetry integration for consuming CMS resources with edge resolution in ASP.NET 10 applications.

Features

  • Automatic EvaluationContext — extracts visitor/session IDs, UTM params, referrer, user-agent, geo-location, and authenticated user info from each request
  • Cookie Management — sets _cms_vid (persistent visitor ID) and _cms_sid (session ID with returning-visitor flag)
  • OpenTelemetry Enrichment — automatically tags every span with cms.visitor_id, cms.session_id, cms.tenant_slug, cms.is_returning
  • FusionCache with Eager Refresh — in-memory L1 caching with fail-safe (stale-if-error) and automatic background refresh at 90% of TTL
  • SQLite Disk Cache (L2) — optional persistent cache via SQLite, automatically managed by FusionCache so the app works even when the API is unavailable
  • Edge Resolution — local rule evaluation using EvaluationContext without server round-trips

Installation

dotnet add package Cms.LightSdk.Sapico.AspNet --version 10.0.*

Quick Start

1. Register Services

using Cms.LightSdk.Sapico.AspNet;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCmsTargeting(opts =>
{
    opts.BaseUrl = builder.Configuration["CmsApi:BaseUrl"] ?? "https://cms.sapico.me";
    opts.TenantSlug = builder.Configuration["CmsApi:TenantSlug"] ?? "my-tenant";
    opts.SiteSlug = builder.Configuration["CmsApi:SiteSlug"] ?? "default";
    opts.ApiKey = builder.Configuration["CmsApi:ApiKey"];  // from Connectors settings
    opts.DefaultCacheDuration = TimeSpan.FromMinutes(30);

    // Optional: per-resource cache overrides
    opts.CachePolicy("homepage", TimeSpan.FromMinutes(5));
    opts.CachePolicy("product-*", TimeSpan.FromHours(1));
});

2. Add Middleware

var app = builder.Build();

app.UseCmsTargeting();  // Must come before your endpoints

3. Inject and Use CmsClient

app.MapGet("/", async (CmsClient cmsClient, HttpContext ctx) =>
{
    var evalCtx = ctx.GetCmsContext();
    var resource = await cmsClient.GetResolvedResourceAsync("homepage", evalCtx);

    // resource.Archetypes now contain only the variants that matched targeting rules
    return Results.Ok(resource);
});

Configuration Options

CmsClientOptions

Property Type Default Description
BaseUrl string string.Empty Base URL of the CMS API (required)
TenantSlug string string.Empty Tenant identifier (required)
SiteSlug string string.Empty Site identifier within the tenant (required)
ApiKey string? null API key for authenticated access — generate one in the CMS dashboard under Connectors settings. Sent as X-Api-Key header on every request.
DefaultCacheDuration TimeSpan 1 day Default TTL for cached resources
DiskCachePath string? null Path to a SQLite database file for persistent L2 disk cache (e.g. cms-cache.db). When set, FusionCache automatically persists and restores entries across restarts.

Cache Policies

opts.CachePolicy("homepage", TimeSpan.FromMinutes(5));      // exact slug
opts.CachePolicy("product-*", TimeSpan.FromHours(1));      // wildcard prefix
opts.CachePolicy("*", TimeSpan.FromDays(7));               // global fallback

Per-slug policies take precedence over DefaultCacheDuration. Wildcards (*) match any slug ending with the pattern.

OpenTelemetry

// Optional: add OpenTelemetry with custom service name
builder.Services.AddCmsOpenTelemetry("MySite");

OpenTelemetry exports to OTLP using environment variables:

  • OTEL_EXPORTER_OTLP_ENDPOINT — OTLP collector URL
  • OTEL_EXPORTER_OTLP_HEADERS — auth headers (e.g., api-key=xxx)

Both ASP.NET Core and HttpClient instrumentation are auto-configured.

HTTP Cookies

The middleware manages two cookies:

Cookie Purpose Lifetime
_cms_vid Persistent visitor ID (anonymous) 365 days
_cms_sid Session ID + returning-flag Browser session

The session cookie value encodes the returning status:

  • First visit: plain GUID (e.g., abc123)
  • Returning visit: r: prefix (e.g., r:abc123)

This allows IsReturningVisitor to persist across all requests in the session.

EvaluationContext

The EvaluationContext captures the full request context for edge resolution:

public class EvaluationContext
{
    public string? Utm { get; set; }                    // ?utm query param
    public string? Location { get; set; }               // X-Geo-Location header or ?loc
    public string? Referrer { get; set; }               // Referer header
    public string? UserAgent { get; set; }              // User-Agent header
    public string? VisitorId { get; set; }              // _cms_vid cookie
    public string? SessionId { get; set; }              // _cms_sid (without r: prefix)
    public bool IsReturningVisitor { get; set; }        // true if session cookie has r: prefix
    public DateTimeOffset Now { get; set; }             // evaluation timestamp
    public Dictionary<string, string> Cookies { get; }  // all request cookies
    public Dictionary<string, string> QueryParams { get; }  // all query params
    public Dictionary<string, string> Headers { get; }  // all request headers

    // Authenticated user (if any)
    public string? UserEmail { get; set; }
    public string? UserDomain { get; set; }
    public List<string> UserRoles { get; set; } = new();
    public Dictionary<string, string> UserClaims { get; set; } = new();

    public string? GetAttributeValue(string attribute);  // resolves rule attribute paths
}

Access it in endpoints:

app.MapGet("/", (HttpContext ctx) =>
{
    var evalCtx = ctx.GetCmsContext();  // extension method
    var visitorId = evalCtx?.VisitorId;
    var userEmail = evalCtx?.UserEmail;
    var isReturning = evalCtx?.IsReturningVisitor;
});

Targeting Attributes (Rule Clauses)

The EvaluationContext.GetAttributeValue() method resolves attribute paths used in rule clauses:

Attribute Source
utm ?utm query param
loc X-Geo-Location header or ?loc
source Referer header
session.id SessionId
session.returning "true" or "false"
user.email Authenticated user's email claim
user.domain Domain part of email
user.role:* Matches any user role against wildcard pattern
user.claim:xyz Specific claim by type
cookie:name Request cookie by name
header:name Request header by name (case-insensitive)
query:name Query parameter by name

CmsClient Methods

GetResourceAsync

Fetches a resource by slug (raw, no edge resolution):

CmsResource? resource = await cmsClient.GetResourceAsync("homepage");

Returns null if not found or API unavailable. Uses cache-aside with stale-if-error.

GetResolvedResourceAsync

Fetches and applies edge resolution locally:

CmsResource? resource = await cmsClient.GetResolvedResourceAsync("homepage", evalCtx);

The returned resource contains only archetypes whose rules matched the context (or have no rules).

GetBestMatchAsync

Tries multiple slug variants and returns the first matching variant:

CmsResource? resource = await cmsClient.GetBestMatchAsync(
    new[] { "homepage-de", "homepage-nl", "homepage" },
    evalCtx
);

Order matters — most specific variants first. Falls back to the first resource without rules if no targeted variant matches.

Middleware Order

UseCmsTargeting() should be placed early in the pipeline, before endpoints that read EvaluationContext:

app.UseCmsTargeting();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/", ...);

Advanced Usage

Custom Cookies & Headers

All request cookies and headers are automatically captured into EvaluationContext. Access them directly or via GetAttributeValue:

// In a rule: cookie:ab_test_variant == "v2"
var variant = evalCtx.GetAttributeValue("cookie:ab_test_variant");

// Direct access
var allCookies = evalCtx.Cookies;

Disk Cache (Offline Resilience)

Enable a persistent SQLite-backed L2 cache so the app can serve resources even when the API is unreachable:

builder.Services.AddCmsTargeting(opts =>
{
    opts.BaseUrl = "https://cms.sapico.me";
    opts.TenantSlug = "my-tenant";
    opts.DiskCachePath = Path.Combine(
        builder.Environment.ContentRootPath, "cms-cache.db");
});

How it works:

FusionCache is configured with two layers when DiskCachePath is set:

  1. L1 (Memory) — fast in-process cache with eager refresh at 90% TTL.
  2. L2 (SQLite) — persistent disk cache via NeoSmart.Caching.Sqlite. Entries are automatically serialized/deserialized by FusionCache.Serialization.SystemTextJson.

On every cache miss, FusionCache checks L2 before calling the API. On every successful API fetch, the result is written to both L1 and L2. If the API is down, fail-safe returns the last known value from either layer.

This two-layer approach ensures zero-downtime deployments and resilience to API outages — no custom code required.

Combining with Authentication

The SDK extracts user claims from HttpContext.User. Ensure authentication runs before the CMS middleware:

app.UseAuthentication();
app.UseCmsTargeting();  // after auth so User is populated

Claims used by targeting rules:

  • email or http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
  • role or http://schemas.microsoft.com/ws/2008/06/identity/claims/role
  • Any custom user.claim:xyz rule looks up the claim by type

ASP.NET 10 Compatibility

This package targets net10.0 and requires:

  • ASP.NET Core 10.0
  • .NET 10.0 SDK or later

Schema

Archetypes are stored as JSON with a $type discriminator:

{
  "$type": "catalog",
  "title": "Homepage Hero",
  "body": "Welcome..."
}

The rules archetype uses clauses for targeting:

{
  "$type": "rules",
  "clauses": [
    { "attribute": "user.role", "pattern": "admin", "negate": false },
    { "attribute": "utm", "pattern": "summer_sale", "negate": false }
  ]
}

All clauses must match (AND logic). negate: true inverts the match.

Troubleshooting

Issue Cause Fix
GetCmsContext() returns null UseCmsTargeting() not called or called after endpoint Move app.UseCmsTargeting() before endpoint mapping
Visitor ID changes on every request Cookies disabled in browser Check browser cookie settings; cookies must be enabled
No user email in EvaluationContext User not authenticated or missing email claim Ensure authentication runs first; verify claim types
Rules not matching Wrong attribute path or case sensitivity Attribute paths are case-insensitive; use GetAttributeValue() to debug
Stale data after TTL Eager refresh not triggering Resources are refreshed automatically at 90% of their cache duration. Ensure the API is reachable for background refresh.

License

MIT — See repository for details.

Showing the top 20 packages that depend on Cms.LightSdk.Sapico.AspNet.

Packages Downloads
Cms.LightSdk.Sapico.AspNet.Mvc
MVC and Razor Pages helpers for the Sapico CMS SDK. Provides GetContentAsync extension methods for Controller and PageModel.
7
Cms.LightSdk.Sapico.AspNet.Mvc
MVC and Razor Pages helpers for the Sapico CMS SDK. Provides GetContentAsync extension methods for Controller and PageModel.
4
Cms.LightSdk.Sapico.AspNet.RazorPages
Razor Pages and MVC controller helpers for the Sapico CMS SDK. Provides GetContentAsync extension methods for PageModel and Controller.
3

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • SQLite-backed L2 disk cache via NeoSmart.Caching.Sqlite + FusionCache.Serialization.SystemTextJson — replaces custom JSON file cache with FusionCache's built-in distributed cache layer (!42, #53)
  • New DiskCachePath option — path to a SQLite .db file for persistent caching

Changed

  • Cache refresh now relies entirely on FusionCache's SetEagerRefresh(0.9f) — no separate background service (!42, #54)
  • FileCachePath is now obsolete — use DiskCachePath instead (backward-compatible shim provided)

Fixed

  • GetResolvedResourceAsync and GetBestMatchAsync now route through FusionCache (L1+L2) instead of bypassing it via direct CmsClient delegation — previously these methods had no disk cache resilience (!42, #53)

Removed

  • CmsCacheRefreshService background hosted service — redundant with FusionCache's built-in eager refresh (!42, #54)
  • TrackAccess() method — no longer needed without the background polling service (!42, #54)
  • Custom JSON file cache (constructor preload + SaveToFileCache) — replaced by FusionCache L2 disk cache (!42, #53)

[10.3.0] — 2026-04-14

Fixed

  • Updated dependency on Cms.LightSdk to 10.2.0

[10.2.0] — 2026-07-17

Fixed

  • Remove duplicate X-Api-Key header and BaseAddress setup from AddHttpClient factory in AddCmsTargeting; CmsClient constructor is now the single source of truth

[10.1.0] — 2026-07-16

Changed

  • API routes restructured to RESTful tenant-scoped paths (/api/tenants/{tenantSlug}/resources, /api/tenants/{tenantSlug}/datasources, etc.)
  • SiteSlug now included in all resource API responses
  • CmsClient.GetResourceAsync updated to use new tenant-scoped route

[10.0.0] — 2025-07-14

Changed

  • Version scheme now follows ASP.NET major version for consistency (10.0.x = .NET 10)
  • Added PackageIcon, PackageReleaseNotes, and enriched NuGet metadata

Fixed

  • README: Added missing ApiKey and SiteSlug to Quick Start and Configuration Options
  • Added PackageReadmeFile so README renders on NuGet/BaGet package page

[0.5.1] — 2025-07-14

Fixed

  • README: Added missing ApiKey and SiteSlug to the Quick Start code example
  • README: Added SiteSlug row to the Configuration Options table
  • README: Expanded ApiKey description with where to generate it (Connectors settings) and how it's sent (X-Api-Key header)

[0.1.1] — 2026-04-12

Changed

  • Include README.md and CHANGELOG.md in NuGet package (Pack=true in .csproj) so consumers see documentation on nuget.org

[0.1.0] — 2026-04-05

Added

  • AddCmsTargeting() — registers CmsClient, background refresh service, and in-memory cache
  • AddCmsOpenTelemetry() — configures OpenTelemetry with OTLP exporter and auto-instrumentation
  • UseCmsTargeting() — middleware that auto-injects EvaluationContext and sets visitor/session cookies
  • CmsTargetingMiddleware — enriches OpenTelemetry spans with CMS visitor attributes
  • CmsCacheRefreshService — background service for demand-driven cache warming
  • EvaluationContextFactory.FromHttpContext() — builds full evaluation context from request
  • HttpContextExtensions.GetCmsContext() — retrieve EvaluationContext in endpoints
  • Wildcard cache policy support (CachePolicy("slug-*", duration))
  • File-based cache persistence (optional via FileCachePath)
  • Stale-if-error cache fallback on API failures

Fixed

  • n/a

Version Downloads Last updated
10.8.0 9 04/20/2026
10.7.0 5 04/20/2026
10.6.0 35 04/18/2026
10.5.0 4 04/18/2026
10.4.0 43 04/16/2026
10.3.0 24 04/14/2026
10.2.0 4 04/12/2026
10.1.0 4 04/12/2026
10.0.0 5 04/12/2026
0.5.1 5 04/12/2026
0.5.0 7 04/12/2026